Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Documentation/source/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
84 changes: 58 additions & 26 deletions source/fab/dep_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand All @@ -178,15 +207,18 @@ 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.

"""
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))}")
8 changes: 7 additions & 1 deletion source/fab/parse/c.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
14 changes: 14 additions & 0 deletions source/fab/parse/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 12 additions & 9 deletions source/fab/steps/analyse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -149,21 +148,25 @@ 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?
if find_programs:
# 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)}')

Expand Down
2 changes: 2 additions & 0 deletions source/fab/steps/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/system_tests/CFortranInterop/test_CFortranInterop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
6 changes: 4 additions & 2 deletions tests/system_tests/CUserHeader/test_CUserHeader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])

Expand All @@ -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())
2 changes: 1 addition & 1 deletion tests/system_tests/MinimalC/test_MinimalC.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions tests/system_tests/zero_config/test_zero_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand All @@ -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()
12 changes: 12 additions & 0 deletions tests/unit_tests/parse/c/other_main_program.c
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 14 additions & 1 deletion tests/unit_tests/parse/c/test_c_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down