diff --git a/setup.py b/setup.py index ce1ec0c5..ff40047a 100644 --- a/setup.py +++ b/setup.py @@ -55,5 +55,10 @@ 'tests': tests, 'docs': docs, 'dev': [*tests, *docs, *features], - } + }, + entry_points={ + 'console_scripts': [ + 'fab=fab.cli:cli_fab' + ] + }, ) diff --git a/source/fab/cli.py b/source/fab/cli.py new file mode 100644 index 00000000..43463d73 --- /dev/null +++ b/source/fab/cli.py @@ -0,0 +1,84 @@ +# ############################################################################## +# (c) Crown copyright Met Office. All rights reserved. +# For further details please refer to the file COPYRIGHT +# which you should have received as part of this distribution +# ############################################################################## +from argparse import ArgumentParser +from pathlib import Path +from typing import Dict, Optional + +import fab +from fab.artefacts import CollectionGetter +from fab.build_config import BuildConfig +from fab.constants import PRAGMAD_C +from fab.steps.analyse import Analyse +from fab.steps.c_pragma_injector import CPragmaInjector +from fab.steps.compile_c import CompileC +from fab.steps.compile_fortran import CompileFortran, get_fortran_compiler, get_fortran_preprocessor +from fab.steps.find_source_files import FindSourceFiles +from fab.steps.grab.folder import GrabFolder +from fab.steps.link import LinkExe +from fab.steps.preprocess import c_preprocessor, fortran_preprocessor +from fab.steps.root_inc_files import RootIncFiles + + +def _generic_build_config(folder: Path, kwargs: Optional[Dict] = None) -> BuildConfig: + folder = folder.resolve() + kwargs = kwargs or {} + + # Within the fab workspace, we'll create a project workspace. + # Ideally we'd just use folder.name, but to avoid clashes, we'll use the full absolute path. + label = '/'.join(folder.parts[1:]) + + fpp, fpp_flags = get_fortran_preprocessor() + fc, fc_flags = get_fortran_compiler() + + # linker and flags depend on compiler + linkers = { + 'gfortran': ('gcc', ['-lgfortran']), + # 'ifort': (..., [...]) + } + try: + linker, linker_flags = linkers[fc] + except KeyError: + raise NotImplementedError(f"Fab's zero configuration mode does not yet work with compiler '{fc}'") + + config = BuildConfig( + project_label=label, + steps=[ + GrabFolder(folder), + FindSourceFiles(), + + RootIncFiles(), # JULES helper, get rid of this eventually + + fortran_preprocessor(preprocessor=fpp, common_flags=fpp_flags), + + CPragmaInjector(), + c_preprocessor(source=CollectionGetter(PRAGMAD_C)), + + Analyse(find_programs=True), + + CompileFortran(compiler=fc, common_flags=fc_flags), + CompileC(), + + LinkExe(linker=linker, flags=linker_flags), + ], + **kwargs, + ) + + return config + + +def cli_fab(): + """ + Running Fab from the command line will attempt to build the project in the current or given folder. + + """ + arg_parser = ArgumentParser() + arg_parser.add_argument('folder', nargs='?', default='.', type=Path) + arg_parser.add_argument('--version', action='version', version=f'%(prog)s {fab.__version__}') + args = arg_parser.parse_args() + + config = _generic_build_config(args.folder) + + config.run() diff --git a/source/fab/parse/fortran.py b/source/fab/parse/fortran.py index 44366756..dc55f017 100644 --- a/source/fab/parse/fortran.py +++ b/source/fab/parse/fortran.py @@ -11,7 +11,6 @@ from pathlib import Path from typing import Union, Optional, Iterable, Dict, Any, Set -from fab.dep_tree import AnalysedDependent from fparser.two.Fortran2003 import ( # type: ignore Use_Stmt, Module_Stmt, Program_Stmt, Subroutine_Stmt, Function_Stmt, Language_Binding_Spec, Char_Literal_Constant, Interface_Block, Name, Comment, Module, Call_Stmt, Derived_Type_Def, Derived_Type_Stmt, @@ -21,6 +20,7 @@ from fparser.two.Fortran2008 import ( # type: ignore Type_Declaration_Stmt, Attr_Spec_List, Entity_Decl_List) +from fab.dep_tree import AnalysedDependent from fab.parse.fortran_common import iter_content, _has_ancestor_type, _typed_child, FortranAnalyserBase from fab.util import file_checksum, string_checksum @@ -37,6 +37,7 @@ class AnalysedFortran(AnalysedDependent): """ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, + program_defs: Optional[Iterable[str]] = None, module_defs: Optional[Iterable[str]] = None, symbol_defs: Optional[Iterable[str]] = None, module_deps: Optional[Iterable[str]] = None, symbol_deps: Optional[Iterable[str]] = None, mo_commented_file_deps: Optional[Iterable[str]] = None, file_deps: Optional[Iterable[Path]] = None, @@ -46,6 +47,8 @@ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, The source file that was analysed. :param file_hash: The hash of the source. If omitted, Fab will evaluate lazily. + :param program_defs: + Set of program names defined by this source file. :param module_defs: Set of module names defined by this source file. A subset of symbol_defs @@ -69,6 +72,7 @@ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, super().__init__(fpath=fpath, file_hash=file_hash, symbol_defs=symbol_defs, symbol_deps=symbol_deps, file_deps=file_deps) + self.program_defs: Set[str] = set(program_defs or []) self.module_defs: Set[str] = set(module_defs or []) self.module_deps: Set[str] = set(module_deps or []) self.mo_commented_file_deps: Set[str] = set(mo_commented_file_deps or []) @@ -79,6 +83,10 @@ def __init__(self, fpath: Union[str, Path], file_hash: Optional[int] = None, self.validate() + def add_program_def(self, name): + self.program_defs.add(name.lower()) + self.add_symbol_def(name) + def add_module_def(self, name): self.module_defs.add(name.lower()) self.add_symbol_def(name) @@ -97,6 +105,7 @@ def field_names(cls): # we're not using the super class because we want to insert, not append the order of our attributes return [ 'fpath', 'file_hash', + 'program_defs', 'module_defs', 'symbol_defs', 'module_deps', 'symbol_deps', 'mo_commented_file_deps', @@ -109,6 +118,7 @@ def to_dict(self) -> Dict[str, Any]: # We sort the lists for reproducibility in testing. result = super().to_dict() result.update({ + "program_defs": list(sorted(self.program_defs)), "module_defs": list(sorted(self.module_defs)), "module_deps": list(sorted(self.module_deps)), "mo_commented_file_deps": list(sorted(self.mo_commented_file_deps)), @@ -122,6 +132,7 @@ def from_dict(cls, d): result = cls( fpath=Path(d["fpath"]), file_hash=d["file_hash"], + program_defs=set(d["program_defs"]), module_defs=set(d["module_defs"]), symbol_defs=set(d["symbol_defs"]), module_deps=set(d["module_deps"]), @@ -137,12 +148,14 @@ def from_dict(cls, d): def validate(self): assert self.file_hash is not None + assert all([d and len(d) for d in self.program_defs]), "bad program definitions" assert all([d and len(d) for d in self.module_defs]), "bad module definitions" 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.module_deps]), "bad module dependencies" assert all([d and len(d) for d in self.symbol_deps]), "bad symbol dependencies" # todo: this feels a little clanky. + assert self.program_defs <= self.symbol_defs, "programs definitions must also be symbol definitions" assert self.module_defs <= self.symbol_defs, "modules definitions must also be symbol definitions" assert self.module_deps <= self.symbol_deps, "modules dependencies must also be symbol dependencies" @@ -185,7 +198,7 @@ def walk_nodes(self, fpath, file_hash, node_tree) -> AnalysedFortran: analysed_fortran.add_symbol_dep(called_name.string) elif obj_type == Program_Stmt: - analysed_fortran.add_symbol_def(str(obj.get_name())) + analysed_fortran.add_program_def(str(obj.get_name())) elif obj_type == Module_Stmt: analysed_fortran.add_module_def(str(obj.get_name())) diff --git a/source/fab/steps/analyse.py b/source/fab/steps/analyse.py index b8039122..bcf3c46b 100644 --- a/source/fab/steps/analyse.py +++ b/source/fab/steps/analyse.py @@ -33,20 +33,21 @@ You'll have to manually read the file to determine which symbol definitions and dependencies it contains. """ - +from itertools import chain import logging import sys import warnings from pathlib import Path from typing import Dict, List, Iterable, Set, Optional, Union +from fab import FabException from fab.artefacts import ArtefactsGetter, CollectionConcat, SuffixFilter from fab.constants import BUILD_TREES -from fab.dep_tree import AnalysedDependent, extract_sub_tree, validate_dependencies +from fab.dep_tree import extract_sub_tree, validate_dependencies, AnalysedDependent from fab.mo import add_mo_commented_file_deps from fab.parse import AnalysedFile, EmptySourceFile -from fab.parse.c import CAnalyser -from fab.parse.fortran import FortranParserWorkaround, FortranAnalyser +from fab.parse.c import AnalysedC, CAnalyser +from fab.parse.fortran import AnalysedFortran, FortranParserWorkaround, FortranAnalyser from fab.steps import Step from fab.util import TimerLogger, by_type @@ -78,7 +79,8 @@ class Analyse(Step): # todo: allow the user to specify a different output artefact collection name? def __init__(self, source: Optional[ArtefactsGetter] = None, - root_symbol: Optional[Union[str, List[str]]] = None, # todo: iterable is more correct + root_symbol: Optional[Union[str, List[str]]] = None, + find_programs: bool = False, std: str = "f2008", special_measure_analysis_results: Optional[Iterable[FortranParserWorkaround]] = None, unreferenced_deps: Optional[Iterable[str]] = None, @@ -95,6 +97,9 @@ def __init__(self, :param source: An :class:`~fab.util.ArtefactsGetter` to get the source files. + :param find_programs: + Instructs the analyser to automatically identify program definitions in the source. + Alternatively, the required programs can be specified with the root_symbol argument. :param root_symbol: When building an executable, provide the Fortran Program name(s), or 'main' for C. If None, build tree extraction will not be performed and the entire source will be used @@ -121,8 +126,12 @@ def __init__(self, # because the files they refer to probably don't exist yet, # because we're just creating steps at this point, so there's been no grab... + if find_programs and root_symbol: + raise ValueError("find_programs and root_symbol can't be used together") + super().__init__(name) self.source_getter = source or DEFAULT_SOURCE_GETTER + self.find_programs = find_programs self.root_symbols: Optional[List[str]] = [root_symbol] if isinstance(root_symbol, str) else root_symbol self.special_measure_analysis_results: List[FortranParserWorkaround] = \ list(special_measure_analysis_results or []) @@ -166,6 +175,21 @@ def run(self, artefact_store: Dict, config): analysed_files = self._parse_files(files=files) self._add_manual_results(analysed_files) + # shall we search the results for fortran programs and a c function called main? + if self.find_programs: + # find fortran programs + sets_of_programs = [af.program_defs for af in by_type(analysed_files, AnalysedFortran)] + self.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: + self.root_symbols.append('main') + if len(c_with_main) > 1: + raise FabException("multiple c main() functions found") + + logger.info(f'automatically found the following programs to build: {", ".join(self.root_symbols)}') + # analyse project_source_tree, symbol_table = self._analyse_dependencies(analysed_files) diff --git a/source/fab/steps/compile_fortran.py b/source/fab/steps/compile_fortran.py index 972d4b38..b60abfa8 100644 --- a/source/fab/steps/compile_fortran.py +++ b/source/fab/steps/compile_fortran.py @@ -344,6 +344,53 @@ def compile_file(self, analysed_file, flags, output_fpath): value={'time_taken': timer.taken, 'start': timer.start}) +def get_fortran_preprocessor(): + """ + Identify the fortran preprocessor and any flags from the environment. + + Initially looks for the `FPP` environment variable, then tries to call the `fpp` and `cpp` command line tools. + + Returns the executable and flags. + + The returned flags will always include `-P` to suppress line numbers. + This fparser ticket requests line number handling https://github.com/stfc/fparser/issues/390 . + + """ + fpp, fpp_flags = None, None + + try: + fpp, fpp_flags = get_tool(os.getenv('FPP')) + logger.info(f"The environment defined FPP as '{fpp}'") + except ValueError: + pass + + if not fpp: + try: + run_command(['which', 'fpp']) + fpp, fpp_flags = 'fpp', ['-P'] + logger.info('detected fpp') + except RuntimeError: + # fpp not available + pass + + if not fpp: + try: + run_command(['which', 'cpp']) + fpp, fpp_flags = 'cpp', ['-traditional-cpp', '-P'] + logger.info('detected cpp') + except RuntimeError: + # fpp not available + pass + + if not fpp: + raise RuntimeError('no fortran preprocessor specified or discovered') + + if '-P' not in fpp_flags: + fpp_flags.append('-P') + + return fpp, fpp_flags + + def get_fortran_compiler(compiler: Optional[str] = None): """ Get the fortran compiler specified by the `$FC` environment variable, @@ -354,8 +401,38 @@ def get_fortran_compiler(compiler: Optional[str] = None): :param compiler: Use this string instead of the $FC environment variable. + Returns the tool and a list of flags. + """ - return get_tool(compiler or os.getenv('FC', '')) # type: ignore + fortran_compiler = None + try: + fortran_compiler = get_tool(compiler or os.getenv('FC', '')) # type: ignore + except ValueError: + # tool not specified + pass + + if not fortran_compiler: + try: + run_command(['gfortran', '--help']) + fortran_compiler = 'gfortran', [] + logger.info('detected gfortran') + except RuntimeError: + # gfortran not available + pass + + if not fortran_compiler: + try: + run_command(['ifort', '--help']) + fortran_compiler = 'ifort', [] + logger.info('detected ifort') + except RuntimeError: + # gfortran not available + pass + + if not fortran_compiler: + raise RuntimeError('no fortran compiler specified or discovered') + + return fortran_compiler def get_mod_hashes(analysed_files: Set[AnalysedFortran], config) -> Dict[str, int]: diff --git a/source/fab/steps/preprocess.py b/source/fab/steps/preprocess.py index dc239652..5b791322 100644 --- a/source/fab/steps/preprocess.py +++ b/source/fab/steps/preprocess.py @@ -35,18 +35,19 @@ class PreProcessor(Step): LABEL: str def __init__(self, + preprocessor: str, source: Optional[ArtefactsGetter] = None, output_collection=None, output_suffix=None, - preprocessor: Optional[str] = None, common_flags: Optional[List[str]] = None, + common_flags: Optional[List[str]] = None, path_flags: Optional[List] = None, name=None): """ + :param preprocessor: + The preprocessor executable. :param source: Defines the files to preprocess. Defaults to DEFAULT_SOURCE. :param output_collection: The name of the output artefact collection, defaulting to DEFAULT_OUTPUT_NAME. :param output_suffix: Defaults to DEFAULT_OUTPUT_SUFFIX. - :param preprocessor: - The preprocessor executable. :param common_flags: Used to construct a :class:`~fab.config.FlagsConfig` object. :param path_flags: @@ -60,7 +61,7 @@ def __init__(self, # todo: should we manage known preprocessors like we do compilers, so we can ensure the -P flag is added? # Command line tools are sometimes specified with flags attached, e.g 'cpp -traditional-cpp' - preprocessor_split = (preprocessor or os.getenv('FPP', 'fpp -P')).split() # type: ignore + preprocessor_split = preprocessor.split() self.preprocessor = preprocessor_split[0] common_flags = preprocessor_split[1:] + (common_flags or []) diff --git a/source/fab/tools.py b/source/fab/tools.py index 46c874e6..de97cca3 100644 --- a/source/fab/tools.py +++ b/source/fab/tools.py @@ -109,7 +109,7 @@ def run_command(command: List[str], env=None, cwd: Optional[Union[Path, str]] = return res.stdout.decode() -def get_tool(tool_str: str = '') -> Tuple[str, List[str]]: +def get_tool(tool_str: Optional[str] = None) -> Tuple[str, List[str]]: """ Get the compiler, preprocessor, etc, from the given string. @@ -121,6 +121,8 @@ def get_tool(tool_str: str = '') -> Tuple[str, List[str]]: The environment variable from which to find the tool. """ + tool_str = tool_str or '' + tool_split = tool_str.split() if not tool_split: raise ValueError(f"Tool not specified in '{tool_str}'. Cannot continue.") diff --git a/system-tests-new/FortranDependencies/test_FortranDependencies.py b/system-tests-new/FortranDependencies/test_FortranDependencies.py index e3e651e3..f67b33cb 100644 --- a/system-tests-new/FortranDependencies/test_FortranDependencies.py +++ b/system-tests-new/FortranDependencies/test_FortranDependencies.py @@ -54,11 +54,13 @@ def test_FortranDependencies(tmp_path): # check the analysis results assert AnalysedFortran.load(config.prebuild_folder / 'first.193489053.an') == AnalysedFortran( fpath=config.source_root / 'first.f90', file_hash=193489053, + program_defs={'first'}, module_defs=None, symbol_defs={'first'}, module_deps={'greeting_mod', 'constants_mod'}, symbol_deps={'greeting_mod', 'constants_mod', 'greet'}) assert AnalysedFortran.load(config.prebuild_folder / 'two.2557739057.an') == AnalysedFortran( fpath=config.source_root / 'two.f90', file_hash=2557739057, + program_defs={'second'}, module_defs=None, symbol_defs={'second'}, module_deps={'constants_mod', 'bye_mod'}, symbol_deps={'constants_mod', 'bye_mod', 'farewell'}) diff --git a/system-tests-new/prebuild/test_prebuild.py b/system-tests-new/prebuild/test_prebuild.py index 9fbcc493..b578c362 100644 --- a/system-tests-new/prebuild/test_prebuild.py +++ b/system-tests-new/prebuild/test_prebuild.py @@ -15,10 +15,14 @@ from fab.steps.find_source_files import FindSourceFiles -@mock.patch.dict(os.environ, {'FFLAGS': ''}) +@mock.patch.dict(os.environ) class TestFortranPrebuild(object): def build_config(self, fab_workspace, grab_prebuild_folder=None): + # remove FFLAGS from the *mocked*, i.e copy of, the environment variables + if os.getenv('FFLAGS'): + del os.environ['FFLAGS'] + logging.getLogger('fab').setLevel(logging.WARNING) build_config = BuildConfig( diff --git a/system-tests-new/zero_config/test_zero_config.py b/system-tests-new/zero_config/test_zero_config.py new file mode 100644 index 00000000..23d3657c --- /dev/null +++ b/system-tests-new/zero_config/test_zero_config.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from fab.cli import _generic_build_config + + +class TestZeroConfig(object): + + def test_fortran_dependencies(self, tmp_path): + # test the sample project in the fortran dependencies system test + config = _generic_build_config( + Path(__file__).parent.parent / 'FortranDependencies', + kwargs={'fab_workspace': tmp_path, 'multiprocessing': False}) + config.run() + assert (config.project_workspace / 'first.exe').exists() + assert (config.project_workspace / 'second.exe').exists() + + def test_c_fortran_interop(self, tmp_path): + # test the sample project in the fortran dependencies system test + config = _generic_build_config( + Path(__file__).parent.parent / 'CFortranInterop', + kwargs={'fab_workspace': tmp_path, 'multiprocessing': False, 'verbose': True}) + config.run() + assert (config.project_workspace / 'main.exe').exists() diff --git a/tests/unit_tests/parse/fortran/test_fortran.py b/tests/unit_tests/parse/fortran/test_fortran.py index aabea95e..5765be6d 100644 --- a/tests/unit_tests/parse/fortran/test_fortran.py +++ b/tests/unit_tests/parse/fortran/test_fortran.py @@ -138,6 +138,7 @@ def as_dict(self): return { 'fpath': 'foo.f90', 'file_hash': 123, + 'program_defs': [], 'module_defs': ['my_mod1', 'my_mod2'], 'symbol_defs': ['my_func1', 'my_func2', 'my_mod1', 'my_mod2'], 'module_deps': ['other_mod1', 'other_mod2'], diff --git a/tests/unit_tests/parse/fortran/test_fortran_analyser.py b/tests/unit_tests/parse/fortran/test_fortran_analyser.py index 25a1de20..2834f901 100644 --- a/tests/unit_tests/parse/fortran/test_fortran_analyser.py +++ b/tests/unit_tests/parse/fortran/test_fortran_analyser.py @@ -71,6 +71,7 @@ def test_program_file(self, fortran_analyser, module_fpath, module_expected): module_expected.fpath = Path(tmp_file.name) module_expected._file_hash = 768896775 + module_expected.program_defs = {'foo_mod'} module_expected.module_defs = set() module_expected.symbol_defs.update({'internal_sub', 'internal_func'}) diff --git a/tests/unit_tests/steps/test_compile_fortran.py b/tests/unit_tests/steps/test_compile_fortran.py index 12a85bed..ce487a1c 100644 --- a/tests/unit_tests/steps/test_compile_fortran.py +++ b/tests/unit_tests/steps/test_compile_fortran.py @@ -8,7 +8,7 @@ from fab.build_config import BuildConfig from fab.constants import BUILD_TREES, OBJECT_FILES from fab.parse.fortran import AnalysedFortran -from fab.steps.compile_fortran import CompileFortran, get_mod_hashes +from fab.steps.compile_fortran import CompileFortran, get_fortran_compiler, get_fortran_preprocessor, get_mod_hashes from fab.util import CompiledFile @@ -395,7 +395,7 @@ def test_obj_missing(self): class test_constructor(object): def test_bare(self): - with mock.patch.dict(os.environ, FC='foofc', FFLAGS=''): + with mock.patch.dict(os.environ, FC='foofc', clear=True): cf = CompileFortran() assert cf.compiler == 'foofc' assert cf.flags.common_flags == [] @@ -429,7 +429,7 @@ def test_precedence(self): assert cf.compiler == 'barfc' def test_no_compiler(self): - with mock.patch.dict(os.environ, FC=''): + with mock.patch.dict(os.environ, clear=True): with pytest.raises(ValueError): CompileFortran() @@ -457,3 +457,71 @@ def test_vanilla(self): result = get_mod_hashes(analysed_files=analysed_files, config=config) assert result == {'foo': 123, 'bar': 456} + + +class Test_get_fortran_preprocessor(object): + + def test_from_env(self): + with mock.patch.dict(os.environ, values={'FPP': 'foo_pp --foo'}): + fpp, fpp_flags = get_fortran_preprocessor() + + assert fpp == 'foo_pp' + assert fpp_flags == ['--foo', '-P'] + + def test_empty_env_fpp(self): + def mock_run_command(command): + if 'fpp' not in command: + raise RuntimeError('foo') + + with mock.patch.dict(os.environ, clear=True): + with mock.patch('fab.steps.compile_fortran.run_command', side_effect=mock_run_command): + fpp, fpp_flags = get_fortran_preprocessor() + + assert fpp == 'fpp' + assert fpp_flags == ['-P'] + + def test_empty_env_cpp(self): + def mock_run_command(command): + if 'cpp' not in command: + raise RuntimeError('foo') + + with mock.patch.dict(os.environ, clear=True): + with mock.patch('fab.steps.compile_fortran.run_command', side_effect=mock_run_command): + fpp, fpp_flags = get_fortran_preprocessor() + + assert fpp == 'cpp' + assert fpp_flags == ['-traditional-cpp', '-P'] + + +class Test_get_fortran_compiler(object): + + def test_from_env(self): + with mock.patch.dict(os.environ, values={'FC': 'foo_c --foo'}): + fc, fc_flags = get_fortran_compiler() + + assert fc == 'foo_c' + assert fc_flags == ['--foo'] + + def test_empty_env_gfortran(self): + def mock_run_command(command): + if 'gfortran' not in command: + raise RuntimeError('foo') + + with mock.patch.dict(os.environ, clear=True): + with mock.patch('fab.steps.compile_fortran.run_command', side_effect=mock_run_command): + fc, fc_flags = get_fortran_compiler() + + assert fc == 'gfortran' + assert fc_flags == [] + + def test_empty_env_ifort(self): + def mock_run_command(command): + if 'ifort' not in command: + raise RuntimeError('foo') + + with mock.patch.dict(os.environ, clear=True): + with mock.patch('fab.steps.compile_fortran.run_command', side_effect=mock_run_command): + fc, fc_flags = get_fortran_compiler() + + assert fc == 'ifort' + assert fc_flags == []