| # Copyright 2020 The Meson development team | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import pathlib | |
| import pickle | |
| import re | |
| import os | |
| import sys | |
| import typing as T | |
| from ..backend.ninjabackend import TargetDependencyScannerInfo, ninja_quote | |
| from ..compilers.compilers import lang_suffixes | |
| CPP_IMPORT_RE = re.compile(r'\w*import ([a-zA-Z0-9]+);') | |
| CPP_EXPORT_RE = re.compile(r'\w*export module ([a-zA-Z0-9]+);') | |
| FORTRAN_INCLUDE_PAT = r"^\s*include\s*['\"](\w+\.\w+)['\"]" | |
| FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$" | |
| FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)" | |
| FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)" | |
| FORTRAN_MODULE_RE = re.compile(FORTRAN_MODULE_PAT, re.IGNORECASE) | |
| FORTRAN_SUBMOD_RE = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE) | |
| FORTRAN_USE_RE = re.compile(FORTRAN_USE_PAT, re.IGNORECASE) | |
| class DependencyScanner: | |
| def __init__(self, pickle_file: str, outfile: str, sources: T.List[str]): | |
| with open(pickle_file, 'rb') as pf: | |
| self.target_data = pickle.load(pf) # type: TargetDependencyScannerInfo | |
| self.outfile = outfile | |
| self.sources = sources | |
| self.provided_by = {} # type: T.Dict[str, str] | |
| self.exports = {} # type: T.Dict[str, str] | |
| self.needs = {} # type: T.Dict[str, T.List[str]] | |
| self.sources_with_exports = [] # type: T.List[str] | |
| def scan_file(self, fname: str) -> None: | |
| suffix = os.path.splitext(fname)[1][1:].lower() | |
| if suffix in lang_suffixes['fortran']: | |
| self.scan_fortran_file(fname) | |
| elif suffix in lang_suffixes['cpp']: | |
| self.scan_cpp_file(fname) | |
| else: | |
| sys.exit(f'Can not scan files with suffix .{suffix}.') | |
| def scan_fortran_file(self, fname: str) -> None: | |
| fpath = pathlib.Path(fname) | |
| modules_in_this_file = set() | |
| for line in fpath.read_text().split('\n'): | |
| import_match = FORTRAN_USE_RE.match(line) | |
| export_match = FORTRAN_MODULE_RE.match(line) | |
| submodule_export_match = FORTRAN_SUBMOD_RE.match(line) | |
| if import_match: | |
| needed = import_match.group(1).lower() | |
| # In Fortran you have an using declaration also for the module | |
| # you define in the same file. Prevent circular dependencies. | |
| if needed not in modules_in_this_file: | |
| if fname in self.needs: | |
| self.needs[fname].append(needed) | |
| else: | |
| self.needs[fname] = [needed] | |
| if export_match: | |
| exported_module = export_match.group(1).lower() | |
| assert(exported_module not in modules_in_this_file) | |
| modules_in_this_file.add(exported_module) | |
| if exported_module in self.provided_by: | |
| raise RuntimeError(f'Multiple files provide module {exported_module}.') | |
| self.sources_with_exports.append(fname) | |
| self.provided_by[exported_module] = fname | |
| self.exports[fname] = exported_module | |
| if submodule_export_match: | |
| # Store submodule "Foo" "Bar" as "foo:bar". | |
| # A submodule declaration can be both an import and an export declaration: | |
| # | |
| # submodule (a1:a2) a3 | |
| # - requires a1@a2.smod | |
| # - produces a1@a3.smod | |
| parent_module_name_full = submodule_export_match.group(1).lower() | |
| parent_module_name = parent_module_name_full.split(':')[0] | |
| submodule_name = submodule_export_match.group(2).lower() | |
| concat_name = f'{parent_module_name}:{submodule_name}' | |
| self.sources_with_exports.append(fname) | |
| self.provided_by[concat_name] = fname | |
| self.exports[fname] = concat_name | |
| # Fortran requires that the immediate parent module must be built | |
| # before the current one. Thus: | |
| # | |
| # submodule (parent) parent <- requires parent.mod (really parent.smod, but they are created at the same time) | |
| # submodule (a1:a2) a3 <- requires a1@a2.smod | |
| # | |
| # a3 does not depend on the a1 parent module directly, only transitively. | |
| if fname in self.needs: | |
| self.needs[fname].append(parent_module_name_full) | |
| else: | |
| self.needs[fname] = [parent_module_name_full] | |
| def scan_cpp_file(self, fname: str) -> None: | |
| fpath = pathlib.Path(fname) | |
| for line in fpath.read_text().split('\n'): | |
| import_match = CPP_IMPORT_RE.match(line) | |
| export_match = CPP_EXPORT_RE.match(line) | |
| if import_match: | |
| needed = import_match.group(1) | |
| if fname in self.needs: | |
| self.needs[fname].append(needed) | |
| else: | |
| self.needs[fname] = [needed] | |
| if export_match: | |
| exported_module = export_match.group(1) | |
| if exported_module in self.provided_by: | |
| raise RuntimeError(f'Multiple files provide module {exported_module}.') | |
| self.sources_with_exports.append(fname) | |
| self.provided_by[exported_module] = fname | |
| self.exports[fname] = exported_module | |
| def objname_for(self, src: str) -> str: | |
| objname = self.target_data.source2object[src] | |
| assert(isinstance(objname, str)) | |
| return objname | |
| def module_name_for(self, src: str) -> str: | |
| suffix = os.path.splitext(src)[1][1:].lower() | |
| if suffix in lang_suffixes['fortran']: | |
| exported = self.exports[src] | |
| # Module foo:bar goes to a file name foo@bar.smod | |
| # Module Foo goes to a file name foo.mod | |
| namebase = exported.replace(':', '@') | |
| if ':' in exported: | |
| extension = 'smod' | |
| else: | |
| extension = 'mod' | |
| return os.path.join(self.target_data.private_dir, f'{namebase}.{extension}') | |
| elif suffix in lang_suffixes['cpp']: | |
| return '{}.ifc'.format(self.exports[src]) | |
| else: | |
| raise RuntimeError('Unreachable code.') | |
| def scan(self) -> int: | |
| for s in self.sources: | |
| self.scan_file(s) | |
| with open(self.outfile, 'w') as ofile: | |
| ofile.write('ninja_dyndep_version = 1\n') | |
| for src in self.sources: | |
| objfilename = self.objname_for(src) | |
| mods_and_submods_needed = [] | |
| module_files_generated = [] | |
| module_files_needed = [] | |
| if src in self.sources_with_exports: | |
| module_files_generated.append(self.module_name_for(src)) | |
| if src in self.needs: | |
| for modname in self.needs[src]: | |
| if modname not in self.provided_by: | |
| # Nothing provides this module, we assume that it | |
| # comes from a dependency library somewhere and is | |
| # already built by the time this compilation starts. | |
| pass | |
| else: | |
| mods_and_submods_needed.append(modname) | |
| for modname in mods_and_submods_needed: | |
| provider_src = self.provided_by[modname] | |
| provider_modfile = self.module_name_for(provider_src) | |
| # Prune self-dependencies | |
| if provider_src != src: | |
| module_files_needed.append(provider_modfile) | |
| quoted_objfilename = ninja_quote(objfilename, True) | |
| quoted_module_files_generated = [ninja_quote(x, True) for x in module_files_generated] | |
| quoted_module_files_needed = [ninja_quote(x, True) for x in module_files_needed] | |
| if quoted_module_files_generated: | |
| mod_gen = '| ' + ' '.join(quoted_module_files_generated) | |
| else: | |
| mod_gen = '' | |
| if quoted_module_files_needed: | |
| mod_dep = '| ' + ' '.join(quoted_module_files_needed) | |
| else: | |
| mod_dep = '' | |
| build_line = 'build {} {}: dyndep {}'.format(quoted_objfilename, | |
| mod_gen, | |
| mod_dep) | |
| ofile.write(build_line + '\n') | |
| return 0 | |
| def run(args: T.List[str]) -> int: | |
| pickle_file = args[0] | |
| outfile = args[1] | |
| sources = args[2:] | |
| scanner = DependencyScanner(pickle_file, outfile, sources) | |
| return scanner.scan() |