summaryrefslogtreecommitdiffstats
path: root/mesonbuild/scripts/depscan.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-29 04:41:38 +0000
commit7b6e527f440cd7e6f8be2b07cee320ee6ca18786 (patch)
tree4a2738d69fa2814659fdadddf5826282e73d81f4 /mesonbuild/scripts/depscan.py
parentInitial commit. (diff)
downloadmeson-7b6e527f440cd7e6f8be2b07cee320ee6ca18786.tar.xz
meson-7b6e527f440cd7e6f8be2b07cee320ee6ca18786.zip
Adding upstream version 1.0.1.upstream/1.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mesonbuild/scripts/depscan.py')
-rw-r--r--mesonbuild/scripts/depscan.py208
1 files changed, 208 insertions, 0 deletions
diff --git a/mesonbuild/scripts/depscan.py b/mesonbuild/scripts/depscan.py
new file mode 100644
index 0000000..3ae14c0
--- /dev/null
+++ b/mesonbuild/scripts/depscan.py
@@ -0,0 +1,208 @@
+# 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.
+from __future__ import annotations
+
+import json
+import os
+import pathlib
+import pickle
+import re
+import sys
+import typing as T
+
+from ..backend.ninjabackend import ninja_quote
+from ..compilers.compilers import lang_suffixes
+
+if T.TYPE_CHECKING:
+ from ..backend.ninjabackend import TargetDependencyScannerInfo
+
+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: TargetDependencyScannerInfo = pickle.load(pf)
+ self.outfile = outfile
+ self.sources = sources
+ self.provided_by: T.Dict[str, str] = {}
+ self.exports: T.Dict[str, str] = {}
+ self.needs: T.Dict[str, T.List[str]] = {}
+ self.sources_with_exports: T.List[str] = []
+
+ def scan_file(self, fname: str) -> None:
+ suffix = os.path.splitext(fname)[1][1:]
+ if suffix != 'C':
+ suffix = suffix.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(encoding='utf-8', errors='ignore').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(encoding='utf-8', errors='ignore').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', encoding='utf-8') 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:
+ assert len(args) == 3, 'got wrong number of arguments!'
+ pickle_file, outfile, jsonfile = args
+ with open(jsonfile, encoding='utf-8') as f:
+ sources = json.load(f)
+ scanner = DependencyScanner(pickle_file, outfile, sources)
+ return scanner.scan()