summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mesonbuild/backend/ninjabackend.py59
-rw-r--r--mesonbuild/scripts/depaccumulate.py129
-rw-r--r--mesonbuild/scripts/depscan.py133
3 files changed, 254 insertions, 67 deletions
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
index a75befd73..00cf4ad02 100644
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -1030,7 +1030,12 @@ class NinjaBackend(backends.Backend):
obj_targets = [t for t in od if t.uses_fortran()]
obj_list.extend(o)
- fortran_order_deps = [File(True, *os.path.split(self.get_target_filename(t))) for t in obj_targets]
+ # We don't need this order dep if we're using dyndeps, as the
+ # depscanner will handle this for us, which produces a better dependency
+ # graph
+ fortran_order_deps: T.List[File] = []
+ if not self.use_dyndeps_for_fortran():
+ fortran_order_deps = [File(True, *os.path.split(self.get_target_filename(t))) for t in obj_targets]
fortran_inc_args: T.List[str] = []
if target.uses_fortran():
fortran_inc_args = mesonlib.listify([target.compilers['fortran'].get_include_args(
@@ -1144,7 +1149,7 @@ class NinjaBackend(backends.Backend):
if not self.should_use_dyndeps_for_target(target):
return
self._uses_dyndeps = True
- depscan_file = self.get_dep_scan_file_for(target)
+ json_file, depscan_file = self.get_dep_scan_file_for(target)
pickle_base = target.name + '.dat'
pickle_file = os.path.join(self.get_target_private_dir(target), pickle_base).replace('\\', '/')
pickle_abs = os.path.join(self.get_target_private_dir_abs(target), pickle_base).replace('\\', '/')
@@ -1164,20 +1169,25 @@ class NinjaBackend(backends.Backend):
with open(pickle_abs, 'wb') as p:
pickle.dump(scaninfo, p)
- elem = NinjaBuildElement(self.all_outputs, depscan_file, rule_name, pickle_file)
+ elem = NinjaBuildElement(self.all_outputs, json_file, rule_name, pickle_file)
# A full dependency is required on all scanned sources, if any of them
# are updated we need to rescan, as they may have changed the modules
# they use or export.
for s in scan_sources:
elem.deps.add(s[0])
- # We need a full dependency on the output depfiles of other targets. If
- # they change we need to completely
+ elem.orderdeps.update(object_deps)
+ elem.add_item('name', target.name)
+ self.add_build(elem)
+
+ infiles: T.Set[str] = set()
for t in target.get_all_linked_targets():
if self.should_use_dyndeps_for_target(t):
- elem.deps.add(os.path.join(self.get_target_dir(t), t.get_filename()))
- elem.deps.update({os.path.join(self.get_target_dir(t), t.get_filename())
- for t in self.flatten_object_list(target)[1]})
- elem.orderdeps.update(object_deps)
+ infiles.add(self.get_dep_scan_file_for(t)[0])
+ _, od = self.flatten_object_list(target)
+ infiles.update({self.get_dep_scan_file_for(t)[0] for t in od if t.uses_fortran()})
+
+ elem = NinjaBuildElement(self.all_outputs, depscan_file, 'depaccumulate', [json_file] + sorted(infiles))
+ elem.add_item('name', target.name)
self.add_build(elem)
def select_sources_to_scan(self, compiled_sources: T.List[str],
@@ -2638,10 +2648,19 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
if rulename in self.ruledict:
# Scanning command is the same for native and cross compilation.
return
+
command = self.environment.get_build_command() + \
['--internal', 'depscan']
args = ['$picklefile', '$out', '$in']
- description = 'Scanning modules'
+ description = 'Scanning target $name for modules'
+ rule = NinjaRule(rulename, command, args, description)
+ self.add_rule(rule)
+
+ rulename = 'depaccumulate'
+ command = self.environment.get_build_command() + \
+ ['--internal', 'depaccumulate']
+ args = ['$out', '$in']
+ description = 'Generating dynamic dependency information for target $name'
rule = NinjaRule(rulename, command, args, description)
self.add_rule(rule)
@@ -3160,8 +3179,9 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
d = os.path.join(self.get_target_private_dir(target), d)
element.add_orderdep(d)
element.add_dep(pch_dep)
- for i in self.get_fortran_module_deps(target, compiler):
- element.add_dep(i)
+ if not self.use_dyndeps_for_fortran():
+ for i in self.get_fortran_module_deps(target, compiler):
+ element.add_dep(i)
if dep_file:
element.add_item('DEPFILE', dep_file)
if compiler.get_language() == 'cuda':
@@ -3204,12 +3224,13 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
extension = extension.lower()
if not (extension in compilers.lang_suffixes['fortran'] or extension in compilers.lang_suffixes['cpp']):
return
- dep_scan_file = self.get_dep_scan_file_for(target)
+ dep_scan_file = self.get_dep_scan_file_for(target)[1]
element.add_item('dyndep', dep_scan_file)
element.add_orderdep(dep_scan_file)
- def get_dep_scan_file_for(self, target: build.BuildTarget) -> str:
- return os.path.join(self.get_target_private_dir(target), 'depscan.dd')
+ def get_dep_scan_file_for(self, target: build.BuildTarget) -> T.Tuple[str, str]:
+ priv = self.get_target_private_dir(target)
+ return os.path.join(priv, 'depscan.json'), os.path.join(priv, 'depscan.dd')
def add_header_deps(self, target, ninja_element, header_deps):
for d in header_deps:
@@ -3232,9 +3253,11 @@ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47485'''))
# The real deps are then detected via dep file generation from the compiler. This breaks on compilers that
# produce incorrect dep files but such is life. A full dependency is
# required to ensure that if a new module is added to an existing file that
- # we correctly rebuild.
- def get_fortran_module_deps(self, target, compiler) -> T.List[str]:
- if compiler.language != 'fortran':
+ # we correctly rebuild
+ def get_fortran_module_deps(self, target: build.BuildTarget, compiler: Compiler) -> T.List[str]:
+ # If we have dyndeps then we don't need this, since the depscanner will
+ # do all of things described above.
+ if compiler.language != 'fortran' or self.use_dyndeps_for_fortran():
return []
return [
os.path.join(self.get_target_dir(lt), lt.get_filename())
diff --git a/mesonbuild/scripts/depaccumulate.py b/mesonbuild/scripts/depaccumulate.py
new file mode 100644
index 000000000..7576390d4
--- /dev/null
+++ b/mesonbuild/scripts/depaccumulate.py
@@ -0,0 +1,129 @@
+# SPDX-License-Identifier: Apache-2.0
+# Copyright © 2021-2024 Intel Corporation
+
+"""Accumulator for p1689r5 module dependencies.
+
+See: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html
+"""
+
+from __future__ import annotations
+import json
+import re
+import textwrap
+import typing as T
+
+if T.TYPE_CHECKING:
+ from .depscan import Description, Rule
+
+# The quoting logic has been copied from the ninjabackend to avoid having to
+# import half of Meson just to quote outputs, which is a performance problem
+_QUOTE_PAT = re.compile(r'[$ :\n]')
+
+
+def quote(text: str) -> str:
+ # Fast path for when no quoting is necessary
+ if not _QUOTE_PAT.search(text):
+ return text
+ if '\n' in text:
+ errmsg = textwrap.dedent(f'''\
+ Ninja does not support newlines in rules. The content was:
+
+ {text}
+
+ Please report this error with a test case to the Meson bug tracker.''')
+ raise RuntimeError(errmsg)
+ return _QUOTE_PAT.sub(r'$\g<0>', text)
+
+
+_PROVIDER_CACHE: T.Dict[str, str] = {}
+
+
+def get_provider(rules: T.List[Rule], name: str) -> T.Optional[str]:
+ """Get the object that a module from another Target provides
+
+ We must rely on the object file here instead of the module itself, because
+ the object rule is part of the generated build.ninja, while the module is
+ only declared inside a dyndep. This creates for the dyndep generator to
+ depend on previous dyndeps as order deps. Since the module
+ interface file will be generated when the object is generated we can rely on
+ that in proxy and simplify generation.
+
+ :param rules: The list of rules to check
+ :param name: The logical-name to look for
+ :raises RuntimeError: If no provider can be found
+ :return: The object file of the rule providing the module
+ """
+ # Cache the result for performance reasons
+ if name in _PROVIDER_CACHE:
+ return _PROVIDER_CACHE[name]
+
+ for r in rules:
+ for p in r.get('provides', []):
+ if p['logical-name'] == name:
+ obj = r['primary-output']
+ _PROVIDER_CACHE[name] = obj
+ return obj
+ return None
+
+
+def process_rules(rules: T.List[Rule],
+ extra_rules: T.List[Rule],
+ ) -> T.Iterable[T.Tuple[str, T.Optional[T.List[str]], T.List[str]]]:
+ """Process the rules for this Target
+
+ :param rules: the rules for this target
+ :param extra_rules: the rules for all of the targets this one links with, to use their provides
+ :yield: A tuple of the output, the exported modules, and the consumed modules
+ """
+ for rule in rules:
+ prov: T.Optional[T.List[str]] = None
+ req: T.List[str] = []
+ if 'provides' in rule:
+ prov = [p['compiled-module-path'] for p in rule['provides']]
+ if 'requires' in rule:
+ for p in rule['requires']:
+ modfile = p.get('compiled-module-path')
+ if modfile is not None:
+ req.append(modfile)
+ else:
+ # We can't error if this is not found because of compiler
+ # provided modules
+ found = get_provider(extra_rules, p['logical-name'])
+ if found:
+ req.append(found)
+ yield rule['primary-output'], prov, req
+
+
+def formatter(files: T.Optional[T.List[str]]) -> str:
+ if files:
+ fmt = ' '.join(quote(f) for f in files)
+ return f'| {fmt}'
+ return ''
+
+
+def gen(outfile: str, desc: Description, extra_rules: T.List[Rule]) -> int:
+ with open(outfile, 'w', encoding='utf-8') as f:
+ f.write('ninja_dyndep_version = 1\n\n')
+
+ for obj, provides, requires in process_rules(desc['rules'], extra_rules):
+ ins = formatter(requires)
+ out = formatter(provides)
+ f.write(f'build {quote(obj)} {out}: dyndep {ins}\n\n')
+
+ return 0
+
+
+def run(args: T.List[str]) -> int:
+ assert len(args) >= 2, 'got wrong number of arguments!'
+ outfile, jsonfile, *jsondeps = args
+ with open(jsonfile, 'r', encoding='utf-8') as f:
+ desc: Description = json.load(f)
+
+ # All rules, necessary for fulfilling across TU and target boundaries
+ rules = desc['rules'].copy()
+ for dep in jsondeps:
+ with open(dep, encoding='utf-8') as f:
+ d: Description = json.load(f)
+ rules.extend(d['rules'])
+
+ return gen(outfile, desc, rules)
diff --git a/mesonbuild/scripts/depscan.py b/mesonbuild/scripts/depscan.py
index 44e805447..6bd5cde9a 100644
--- a/mesonbuild/scripts/depscan.py
+++ b/mesonbuild/scripts/depscan.py
@@ -1,22 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 The Meson development team
-# Copyright © 2023 Intel Corporation
+# Copyright © 2023-2024 Intel Corporation
from __future__ import annotations
import collections
+import json
import os
import pathlib
import pickle
import re
import typing as T
-from ..backend.ninjabackend import ninja_quote
-
if T.TYPE_CHECKING:
- from typing_extensions import Literal
+ from typing_extensions import Literal, TypedDict, NotRequired
from ..backend.ninjabackend import TargetDependencyScannerInfo
+ Require = TypedDict(
+ 'Require',
+ {
+ 'logical-name': str,
+ 'compiled-module-path': NotRequired[str],
+ 'source-path': NotRequired[str],
+ 'unique-on-source-path': NotRequired[bool],
+ 'lookup-method': NotRequired[Literal['by-name', 'include-angle', 'include-quote']]
+ },
+ )
+
+ Provide = TypedDict(
+ 'Provide',
+ {
+ 'logical-name': str,
+ 'compiled-module-path': NotRequired[str],
+ 'source-path': NotRequired[str],
+ 'unique-on-source-path': NotRequired[bool],
+ 'is-interface': NotRequired[bool],
+ },
+ )
+
+ Rule = TypedDict(
+ 'Rule',
+ {
+ 'primary-output': NotRequired[str],
+ 'outputs': NotRequired[T.List[str]],
+ 'provides': NotRequired[T.List[Provide]],
+ 'requires': NotRequired[T.List[Require]],
+ }
+ )
+
+ class Description(TypedDict):
+
+ version: int
+ revision: int
+ rules: T.List[Rule]
+
+
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]+);')
@@ -37,7 +75,7 @@ class DependencyScanner:
self.sources = self.target_data.sources
self.provided_by: T.Dict[str, str] = {}
self.exports: T.Dict[str, str] = {}
- self.needs: collections.defaultdict[str, T.List[str]] = collections.defaultdict(list)
+ self.imports: collections.defaultdict[str, T.List[str]] = collections.defaultdict(list)
self.sources_with_exports: T.List[str] = []
def scan_file(self, fname: str, lang: Literal['cpp', 'fortran']) -> None:
@@ -58,7 +96,7 @@ class DependencyScanner:
# 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:
- self.needs[fname].append(needed)
+ self.imports[fname].append(needed)
if export_match:
exported_module = export_match.group(1).lower()
assert exported_module not in modules_in_this_file
@@ -89,7 +127,7 @@ class DependencyScanner:
# submodule (a1:a2) a3 <- requires a1@a2.smod
#
# a3 does not depend on the a1 parent module directly, only transitively.
- self.needs[fname].append(parent_module_name_full)
+ self.imports[fname].append(parent_module_name_full)
def scan_cpp_file(self, fname: str) -> None:
fpath = pathlib.Path(fname)
@@ -98,7 +136,7 @@ class DependencyScanner:
export_match = CPP_EXPORT_RE.match(line)
if import_match:
needed = import_match.group(1)
- self.needs[fname].append(needed)
+ self.imports[fname].append(needed)
if export_match:
exported_module = export_match.group(1)
if exported_module in self.provided_by:
@@ -123,47 +161,44 @@ class DependencyScanner:
def scan(self) -> int:
for s, lang in self.sources:
self.scan_file(s, lang)
- with open(self.outfile, 'w', encoding='utf-8') as ofile:
- ofile.write('ninja_dyndep_version = 1\n')
- for src, lang in self.sources:
- objfilename = self.target_data.source2object[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, lang))
- 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, lang)
- # 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')
+ description: Description = {
+ 'version': 1,
+ 'revision': 0,
+ 'rules': [],
+ }
+ for src, lang in self.sources:
+ rule: Rule = {
+ 'primary-output': self.target_data.source2object[src],
+ 'requires': [],
+ 'provides': [],
+ }
+ if src in self.sources_with_exports:
+ rule['outputs'] = [self.module_name_for(src, lang)]
+ if src in self.imports:
+ for modname in self.imports[src]:
+ provider_src = self.provided_by.get(modname)
+ if provider_src == src:
+ continue
+ rule['requires'].append({
+ 'logical-name': modname,
+ })
+ if provider_src:
+ rule['requires'][-1].update({
+ 'source-path': provider_src,
+ 'compiled-module-path': self.module_name_for(provider_src, lang),
+ })
+ if src in self.exports:
+ modname = self.exports[src]
+ rule['provides'].append({
+ 'logical-name': modname,
+ 'source-path': src,
+ 'compiled-module-path': self.module_name_for(src, lang),
+ })
+ description['rules'].append(rule)
+
+ with open(self.outfile, 'w', encoding='utf-8') as f:
+ json.dump(description, f)
+
return 0
def run(args: T.List[str]) -> int: