diff --git a/Documentation/source/glossary.rst b/Documentation/source/glossary.rst index b9ff75cb..ec97cc9d 100644 --- a/Documentation/source/glossary.rst +++ b/Documentation/source/glossary.rst @@ -65,7 +65,9 @@ Glossary A folder inside the :term:`Fab Workspace`, containing all source and output from a build config. Root Symbol - The name of a Fortran PROGRAM, or ``"main"`` for C code. Fab builds an executable for every root symbol it's given. + The name of a Fortran PROGRAM, or ``"main"`` for C code. Fab builds an executable for every root symbol it's given. Internally, any ``main`` + symbol will be suffixed with the name of the C file, which allows + Fab to manage several C main programs in one build tree. Source Tree The :class:`~fab.steps.analyse.analyse` step produces a dependency tree of the entire project source. diff --git a/source/fab/dep_tree.py b/source/fab/dep_tree.py index e61949f2..82c04972 100644 --- a/source/fab/dep_tree.py +++ b/source/fab/dep_tree.py @@ -4,11 +4,13 @@ # which you should have received as part of this distribution ############################################################################## """ -Classes and helper functions related to the dependency tree, as created by the analysis stage. +Classes and helper functions related to the dependency tree, as created by +the analysis stage. """ -# todo: we've since adopted the term "source tree", so we should probably rename this module to match. +# todo: we've since adopted the term "source tree", so we should probably +# rename this module to match. from abc import ABC import logging from pathlib import Path @@ -20,18 +22,24 @@ # Todo: Better name? It's an analysed file in a dependency tree -# (as opposed to an analysed x90 for example, which isn't part of this tree dependency analysis). +# (as opposed to an analysed x90 for example, which isn't part of this +# tree dependency analysis). class AnalysedDependent(AnalysedFile, ABC): """ - An :class:`~fab.parse.AnalysedFile` which can depend on others, and be a dependency. - Instances of this class are nodes in a source dependency tree. + An :class:`~fab.parse.AnalysedFile` which can depend on others, and be + a dependency. Instances of this class are nodes in a source dependency + tree. During parsing, the symbol definitions and dependencies are filled in. - During dependency analysis, symbol dependencies are turned into file dependencies. + During dependency analysis, symbol dependencies are turned into file + dependencies. """ - def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, - symbol_defs: Optional[Iterable[str]] = None, symbol_deps: Optional[Iterable[str]] = None, + def __init__(self, + fpath: Union[str, Path], + file_hash: Optional[int] = None, + symbol_defs: Optional[Iterable[str]] = None, + symbol_deps: Optional[Iterable[str]] = None, file_deps: Optional[Iterable[Path]] = None): """ :param fpath: @@ -45,7 +53,8 @@ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, Can include symbols in the same file. :param file_deps: Other files on which this source depends. Must not include itself. - This attribute is calculated during symbol analysis, after everything has been parsed. + This attribute is calculated during symbol analysis, after + everything has been parsed. """ super().__init__(fpath=fpath, file_hash=file_hash) @@ -54,19 +63,34 @@ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, self.symbol_deps: set[str] = set(symbol_deps or {}) self.file_deps: set[Path] = set(file_deps or []) - assert all([d and len(d) for d in self.symbol_defs]), "bad symbol definitions" - assert all([d and len(d) for d in self.symbol_deps]), "bad symbol dependencies" + assert all([d and len(d) for d in self.symbol_defs]), \ + "bad symbol definitions" + assert all([d and len(d) for d in self.symbol_deps]), \ + "bad symbol dependencies" - def add_symbol_def(self, name): + def add_symbol_def(self, name: str) -> None: + """ + Adds a symbol definition. + + :param name: the symbol name to add. + """ assert name and len(name) - self.symbol_defs.add(name.lower()) + self.symbol_defs.add(name) + + def add_symbol_dep(self, name: str) -> None: + """ + Adds a dependency to a symbol. - def add_symbol_dep(self, name): + :param name: name of the symbol that is required. + """ assert name and len(name) - self.symbol_deps.add(name.lower()) + self.symbol_deps.add(name) - def add_file_dep(self, name): - self.file_deps.add(Path(name)) + def add_file_dep(self, file_name: str) -> None: + """ + Adds a dependency to a file; + """ + self.file_deps.add(Path(file_name)) @classmethod def field_names(cls): @@ -99,20 +123,23 @@ def from_dict(cls, d): def extract_sub_tree(source_tree: dict[Path, AnalysedDependent], - root: Path, verbose=False)\ + root: Path, + verbose=False)\ -> dict[Path, AnalysedDependent]: """ - Extract the subtree required to build the target, from the full source tree of all analysed source files. + Extract the subtree required to build the target, from the full + source tree of all analysed source files. :param source_tree: The source tree of analysed files. :param root: - The root of the dependency tree, this is the filename containing the Fortran program. + The root of the dependency tree, this is the filename containing the + Fortran program. :param verbose: Log missing dependencies. """ - result: dict[Path, AnalysedDependent] = dict() + result: dict[Path, AnalysedDependent] = {} missing: set[Path] = set() _extract_sub_tree(src_tree=source_tree, @@ -157,10 +184,12 @@ def _extract_sub_tree(src_tree: dict[Path, AnalysedDependent], # add this child dep _extract_sub_tree( - src_tree=src_tree, key=file_dep, dst_tree=dst_tree, missing=missing, verbose=verbose, indent=indent + 1) + src_tree=src_tree, key=file_dep, dst_tree=dst_tree, + missing=missing, verbose=verbose, indent=indent + 1) -def filter_source_tree(source_tree: dict[Path, AnalysedDependent], suffixes: Iterable[str]) -> list[AnalysedDependent]: +def filter_source_tree(source_tree: dict[Path, AnalysedDependent], + suffixes: Iterable[str]) -> list[AnalysedDependent]: """ Pull out files with the given extensions from a source tree. @@ -178,7 +207,8 @@ def filter_source_tree(source_tree: dict[Path, AnalysedDependent], suffixes: Ite def validate_dependencies(source_tree): """ - If any dep is missing from the tree, then it's unknown code and we won't be able to compile. + If any dep is missing from the tree, then it's unknown code and we won't + be able to compile. :param source_tree: The source tree of analysed files. @@ -186,7 +216,9 @@ def validate_dependencies(source_tree): """ missing = set() for f in source_tree.values(): - missing.update([str(file_dep) for file_dep in f.file_deps if file_dep not in source_tree]) + missing.update([str(file_dep) for file_dep in f.file_deps + if file_dep not in source_tree]) if missing: - logger.error(f"Unknown dependencies, expecting build to fail: {', '.join(sorted(missing))}") + logger.error(f"Unknown dependencies, expecting build to fail: " + f"{', '.join(sorted(missing))}") diff --git a/source/fab/parse/c.py b/source/fab/parse/c.py index 63a844c4..504bd419 100644 --- a/source/fab/parse/c.py +++ b/source/fab/parse/c.py @@ -175,7 +175,13 @@ def _process_symbol_declaration(self, analysed_file, node, usr_symbols): # the rest of the application logger.debug(' * Is defined in this file') # todo: ignore if inside user pragmas? - analysed_file.add_symbol_def(node.spelling) + if node.spelling == "main": + # To allow multiple main programs in c, change the + # 'main' symbol to include the file name: + main_symbol = f"main@{analysed_file.fpath.stem}" + analysed_file.add_symbol_def(main_symbol) + else: + analysed_file.add_symbol_def(node.spelling) else: # Record any user included symbols in case they're referenced later in the code if self._check_for_include(node.location.line) == "usr_include": diff --git a/source/fab/parse/fortran.py b/source/fab/parse/fortran.py index 04b2bdb3..ff76601e 100644 --- a/source/fab/parse/fortran.py +++ b/source/fab/parse/fortran.py @@ -95,6 +95,20 @@ def __init__(self, fpath: Union[str, Path], self.validate() + def add_symbol_def(self, name: str): + """ + Add a Fortran symbol definition, which is case independent. + """ + assert name and len(name) + super().add_symbol_def(name.lower()) + + def add_symbol_dep(self, name: str): + """ + Add a Fortran dependency, which is case independent + """ + assert name and len(name) + super().add_symbol_dep(name.lower()) + def add_program_def(self, name): self.program_defs.add(name.lower()) self.add_symbol_def(name) diff --git a/source/fab/steps/analyse.py b/source/fab/steps/analyse.py index 4c369b02..4f2ad4ff 100644 --- a/source/fab/steps/analyse.py +++ b/source/fab/steps/analyse.py @@ -40,7 +40,6 @@ from pathlib import Path from typing import Iterable, Optional, Union -from fab import FabException from fab.artefacts import ArtefactsGetter, ArtefactSet, CollectionConcat from fab.dep_tree import extract_sub_tree, validate_dependencies, AnalysedDependent from fab.mo import add_mo_commented_file_deps @@ -149,7 +148,9 @@ def analyse( # parse files: list[Path] = source_getter(config.artefact_store) - analysed_files = _parse_files(config, files=files, fortran_analyser=fortran_analyser, c_analyser=c_analyser) + analysed_files = _parse_files(config, files=files, + fortran_analyser=fortran_analyser, + c_analyser=c_analyser) _add_manual_results(special_measure_analysis_results, analysed_files) # shall we search the results for fortran programs and a c function called main? @@ -157,13 +158,15 @@ def analyse( # find fortran programs sets_of_programs = [af.program_defs for af in by_type(analysed_files, AnalysedFortran)] root_symbols = list(chain(*sets_of_programs)) - - # find c main() - c_with_main = list(filter(lambda c: 'main' in c.symbol_defs, by_type(analysed_files, AnalysedC))) - if c_with_main: - root_symbols.append('main') - if len(c_with_main) > 1: - raise FabException("multiple c main() functions found") + # find c main() symbols. In order to support building multiple + # C programs, each `main` symbol is replaced with `main@filename` during + # parsing. + for analysed_c in analysed_files: + if not isinstance(analysed_c, AnalysedC): + continue + main_symbol = f"main@{analysed_c.fpath.stem}" + if main_symbol in analysed_c.symbol_defs: + root_symbols.append(main_symbol) logger.info(f'automatically found the following programs to build: {", ".join(root_symbols)}') diff --git a/source/fab/steps/link.py b/source/fab/steps/link.py index 78736b78..94f58f1e 100644 --- a/source/fab/steps/link.py +++ b/source/fab/steps/link.py @@ -93,6 +93,8 @@ def link_exe(config, flags = flags or [] for root, objects in target_objects.items(): + if root.startswith("main@"): + root = root[len("main@"):] exe_path = config.project_workspace / f'{root}' linker.link(objects, exe_path, config=config, libs=libs, add_flags=flags) diff --git a/tests/system_tests/CFortranInterop/test_CFortranInterop.py b/tests/system_tests/CFortranInterop/test_CFortranInterop.py index 84c01347..0db00a87 100644 --- a/tests/system_tests/CFortranInterop/test_CFortranInterop.py +++ b/tests/system_tests/CFortranInterop/test_CFortranInterop.py @@ -35,7 +35,7 @@ def test_CFortranInterop(tmp_path): c_pragma_injector(config) preprocess_c(config) preprocess_fortran(config) - analyse(config, root_symbols='main') + analyse(config, root_symbols='main@c_roundtrip') compile_c(config, common_flags=['-c', '-std=c99']) with warns(UserWarning, match="Removing managed flag"): compile_fortran(config, common_flags=['-c']) diff --git a/tests/system_tests/CUserHeader/test_CUserHeader.py b/tests/system_tests/CUserHeader/test_CUserHeader.py index 1a376884..018d4f86 100644 --- a/tests/system_tests/CUserHeader/test_CUserHeader.py +++ b/tests/system_tests/CUserHeader/test_CUserHeader.py @@ -34,7 +34,7 @@ def test_CUseHeader(tmp_path): find_source_files(config) c_pragma_injector(config) preprocess_c(config) - analyse(config, root_symbols='main') + analyse(config, root_symbols='main@mainprog') compile_c(config, common_flags=['-c', '-std=c99']) link_exe(config, flags=['-lgfortran']) @@ -44,4 +44,6 @@ def test_CUseHeader(tmp_path): command = [str(list(config.artefact_store[ArtefactSet.EXECUTABLES])[0])] res = subprocess.run(command, capture_output=True) output = res.stdout.decode() - assert output == ''.join(open(PROJECT_SOURCE / 'expected.exec.txt').readlines()) + with open(PROJECT_SOURCE / 'expected.exec.txt', 'r', + encoding="utf-8") as fd: + assert output == ''.join(fd.readlines()) diff --git a/tests/system_tests/MinimalC/test_MinimalC.py b/tests/system_tests/MinimalC/test_MinimalC.py index 7959fd17..7431fca0 100644 --- a/tests/system_tests/MinimalC/test_MinimalC.py +++ b/tests/system_tests/MinimalC/test_MinimalC.py @@ -34,7 +34,7 @@ def test_minimal_c(tmp_path): find_source_files(config) c_pragma_injector(config) preprocess_c(config) - analyse(config, root_symbols='main') + analyse(config, root_symbols='main@main') compile_c(config, common_flags=['-c', '-std=c99']) link_exe(config) diff --git a/tests/system_tests/zero_config/test_zero_config.py b/tests/system_tests/zero_config/test_zero_config.py index abbeb95a..b33b3e56 100644 --- a/tests/system_tests/zero_config/test_zero_config.py +++ b/tests/system_tests/zero_config/test_zero_config.py @@ -57,7 +57,7 @@ def test_c(self, tmp_path: Path) -> None: 'fab_workspace': tmp_path, 'multiprocessing': False} config = cli_fab(folder=tmp_path / 'source', kwargs=kwargs) - assert (config.project_workspace / 'main').exists() + assert (config.project_workspace / 'mainprog').exists() def test_c_fortran(self, tmp_path: Path) -> None: """ @@ -75,4 +75,4 @@ def test_c_fortran(self, tmp_path: Path) -> None: 'fab_workspace': tmp_path / 'fab', 'multiprocessing': False} config = cli_fab(folder=tmp_path / 'source', kwargs=kwargs) - assert (config.project_workspace / 'main').exists() + assert (config.project_workspace / 'c_roundtrip').exists() diff --git a/tests/unit_tests/parse/c/other_main_program.c b/tests/unit_tests/parse/c/other_main_program.c new file mode 100644 index 00000000..7b38e4f4 --- /dev/null +++ b/tests/unit_tests/parse/c/other_main_program.c @@ -0,0 +1,12 @@ + +/* Just a mostly empty program to check that + multiple c main programs work as expected, and that C symbols are + stored case sensitive +*/ + +int CaSeSeNsItIvE() { // external linkage, is a definition + return 1; +} + +void main(void) { // external linkage, is a definition +} diff --git a/tests/unit_tests/parse/c/test_c_analyser.py b/tests/unit_tests/parse/c/test_c_analyser.py index 4341ba4c..26274d01 100644 --- a/tests/unit_tests/parse/c/test_c_analyser.py +++ b/tests/unit_tests/parse/c/test_c_analyser.py @@ -30,12 +30,25 @@ def test_simple_result(tmp_path: Path, fpath=fpath, file_hash=1429445462, symbol_deps={'usr_var', 'usr_func'}, - symbol_defs={'func_decl', 'func_def', 'var_def', 'var_extern_def', 'main'}, + symbol_defs={'func_decl', 'func_def', 'var_def', 'var_extern_def', + 'main@test_c_analyser'}, ) assert analysis == expected assert isinstance(analysis, AnalysedC) assert artefact == c_analyser._config.prebuild_folder / f'test_c_analyser.{analysis.file_hash}.an' + with mock.patch('fab.parse.AnalysedFile.save'): + fpath = Path(__file__).parent / "other_main_program.c" + analysis, artefact = c_analyser.run(fpath) + + expected = AnalysedC( + fpath=fpath, + file_hash=1458424101, + symbol_deps={}, + symbol_defs={'CaSeSeNsItIvE', 'main@other_main_program'}, + ) + assert analysis == expected + class Test__locate_include_regions: