Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanups, fixes, and speedups for the depscanner #13021

Merged
93 changes: 58 additions & 35 deletions mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2012-2017 The Meson development team
# Copyright © 2023 Intel Corporation

from __future__ import annotations

Expand Down Expand Up @@ -134,10 +135,23 @@ def ninja_quote(text: str, is_build_line: bool = False) -> str:
raise MesonException(errmsg)
return quote_re.sub(r'$\g<0>', text)


@dataclass
class TargetDependencyScannerInfo:
def __init__(self, private_dir: str, source2object: T.Dict[str, str]):
self.private_dir = private_dir
self.source2object = source2object

"""Information passed to the depscanner about a target.

:param private_dir: The private scratch directory for the target.
:param source2object: A mapping of source file names to the objects that
will be created from them.
:param sources: a list of sources mapping them to the language rules to use
to scan them.
"""

private_dir: str
source2object: T.Dict[str, str]
sources: T.List[T.Tuple[str, Literal['cpp', 'fortran']]]


@unique
class Quoting(Enum):
Expand Down Expand Up @@ -480,6 +494,7 @@ def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Inter
self.created_llvm_ir_rule = PerMachine(False, False)
self.rust_crates: T.Dict[str, RustCrate] = {}
self.implicit_meson_outs = []
self._uses_dyndeps = False

def create_phony_target(self, dummy_outfile: str, rulename: str, phony_infilename: str) -> NinjaBuildElement:
'''
Expand Down Expand Up @@ -655,7 +670,8 @@ def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional
os.replace(tempfilename, outfilename)
mlog.cmd_ci_include(outfilename) # For CI debugging
# Refresh Ninja's caches. https://github.com/ninja-build/ninja/pull/1685
if mesonlib.version_compare(self.ninja_version, '>=1.10.0') and os.path.exists(os.path.join(self.environment.build_dir, '.ninja_log')):
# Cannot use when running with dyndeps: https://github.com/ninja-build/ninja/issues/1952
if mesonlib.version_compare(self.ninja_version, '>=1.10.0') and os.path.exists(os.path.join(self.environment.build_dir, '.ninja_log')) and not self._uses_dyndeps:
subprocess.call(self.ninja_command + ['-t', 'restat'], cwd=self.environment.build_dir)
subprocess.call(self.ninja_command + ['-t', 'cleandead'], cwd=self.environment.build_dir)
self.generate_compdb()
Expand Down Expand Up @@ -844,8 +860,8 @@ def generate_target(self, target):
self.generate_custom_target(target)
if isinstance(target, build.RunTarget):
self.generate_run_target(target)
compiled_sources = []
source2object = {}
compiled_sources: T.List[str] = []
source2object: T.Dict[str, str] = {}
name = target.get_id()
if name in self.processed_targets:
return
Expand Down Expand Up @@ -928,7 +944,7 @@ def generate_target(self, target):
# this target's sources (generated sources and preexisting sources).
# This will be set as dependencies of all the target's sources. At the
# same time, also deal with generated sources that need to be compiled.
generated_source_files = []
generated_source_files: T.List[File] = []
for rel_src in generated_sources.keys():
raw_src = File.from_built_relative(rel_src)
if self.environment.is_source(rel_src):
Expand Down Expand Up @@ -1073,48 +1089,53 @@ def should_use_dyndeps_for_target(self, target: 'build.BuildTarget') -> bool:
return False
return True

def generate_dependency_scan_target(self, target: build.BuildTarget, compiled_sources, source2object, generated_source_files: T.List[mesonlib.File],
def generate_dependency_scan_target(self, target: build.BuildTarget,
compiled_sources: T.List[str],
source2object: T.Dict[str, str],
generated_source_files: T.List[mesonlib.File],
object_deps: T.List['mesonlib.FileOrString']) -> None:
if not self.should_use_dyndeps_for_target(target):
return
self._uses_dyndeps = True
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('\\', '/')
json_abs = os.path.join(self.get_target_private_dir_abs(target), f'{target.name}-deps.json').replace('\\', '/')
rule_name = 'depscan'
scan_sources = self.select_sources_to_scan(compiled_sources)

# Dump the sources as a json list. This avoids potential problems where
# the number of sources passed to depscan exceeds the limit imposed by
# the OS.
with open(json_abs, 'w', encoding='utf-8') as f:
json.dump(scan_sources, f)
elem = NinjaBuildElement(self.all_outputs, depscan_file, rule_name, json_abs)
elem.add_item('picklefile', pickle_file)
scan_sources = list(self.select_sources_to_scan(compiled_sources))

scaninfo = TargetDependencyScannerInfo(
self.get_target_private_dir(target), source2object, scan_sources)

write = True
if os.path.exists(pickle_abs):
with open(pickle_abs, 'rb') as p:
old = pickle.load(p)
write = old != scaninfo

if write:
with open(pickle_abs, 'wb') as p:
pickle.dump(scaninfo, p)

elem = NinjaBuildElement(self.all_outputs, depscan_file, rule_name, pickle_file)
# Add any generated outputs to the order deps of the scan target, so
# that those sources are present
for g in generated_source_files:
elem.orderdeps.add(g.relative_name())
elem.orderdeps.update(object_deps)
scaninfo = TargetDependencyScannerInfo(self.get_target_private_dir(target), source2object)
with open(pickle_abs, 'wb') as p:
pickle.dump(scaninfo, p)
self.add_build(elem)

def select_sources_to_scan(self, compiled_sources):
def select_sources_to_scan(self, compiled_sources: T.List[str]
) -> T.Iterable[T.Tuple[str, Literal['cpp', 'fortran']]]:
# in practice pick up C++ and Fortran files. If some other language
# requires scanning (possibly Java to deal with inner class files)
# then add them here.
all_suffixes = set(compilers.lang_suffixes['cpp']) | set(compilers.lang_suffixes['fortran'])
selected_sources = []
for source in compiled_sources:
ext = os.path.splitext(source)[1][1:]
if ext != 'C':
ext = ext.lower()
if ext in all_suffixes:
selected_sources.append(source)
return selected_sources
if ext.lower() in compilers.lang_suffixes['cpp'] or ext == 'C':
yield source, 'cpp'
elif ext.lower() in compilers.lang_suffixes['fortran']:
yield source, 'fortran'

def process_target_dependencies(self, target):
for t in target.get_dependencies():
Expand Down Expand Up @@ -2752,7 +2773,7 @@ def get_link_debugfile_name(self, linker, target) -> T.Optional[str]:
def get_link_debugfile_args(self, linker, target):
return linker.get_link_debugfile_args(self.get_target_debug_filename(target))

def generate_llvm_ir_compile(self, target, src):
def generate_llvm_ir_compile(self, target, src: mesonlib.FileOrString):
base_proxy = target.get_options()
compiler = get_compiler_for_source(target.compilers.values(), src)
commands = compiler.compiler_args()
Expand All @@ -2771,10 +2792,11 @@ def generate_llvm_ir_compile(self, target, src):
rel_obj = os.path.join(self.get_target_private_dir(target), obj_basename)
rel_obj += '.' + self.environment.machines[target.for_machine].get_object_suffix()
commands += self.get_compile_debugfile_args(compiler, target, rel_obj)
if isinstance(src, File) and src.is_built:
rel_src = src.fname
elif isinstance(src, File):
rel_src = src.rel_to_builddir(self.build_to_src)
if isinstance(src, File):
if src.is_built:
rel_src = src.fname
else:
rel_src = src.rel_to_builddir(self.build_to_src)
else:
raise InvalidArguments(f'Invalid source type: {src!r}')
# Write the Ninja build command
Expand Down Expand Up @@ -2913,7 +2935,8 @@ def generate_single_compile(self, target: build.BuildTarget, src,
is_generated: bool = False, header_deps=None,
order_deps: T.Optional[T.List['mesonlib.FileOrString']] = None,
extra_args: T.Optional[T.List[str]] = None,
unity_sources: T.Optional[T.List[mesonlib.FileOrString]] = None) -> None:
unity_sources: T.Optional[T.List[mesonlib.FileOrString]] = None,
) -> T.Tuple[str, str]:
"""
Compiles C/C++, ObjC/ObjC++, Fortran, and D sources
"""
Expand Down
73 changes: 24 additions & 49 deletions mesonbuild/scripts/depscan.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 The Meson development team
# Copyright © 2023 Intel Corporation

from __future__ import annotations

import json
import collections
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 typing_extensions import Literal
from ..backend.ninjabackend import TargetDependencyScannerInfo

CPP_IMPORT_RE = re.compile(r'\w*import ([a-zA-Z0-9]+);')
Expand All @@ -30,26 +30,21 @@
FORTRAN_USE_RE = re.compile(FORTRAN_USE_PAT, re.IGNORECASE)

class DependencyScanner:
def __init__(self, pickle_file: str, outfile: str, sources: T.List[str]):
def __init__(self, pickle_file: str, outfile: str):
with open(pickle_file, 'rb') as pf:
self.target_data: TargetDependencyScannerInfo = pickle.load(pf)
self.outfile = outfile
self.sources = sources
self.sources = self.target_data.sources
self.provided_by: T.Dict[str, str] = {}
self.exports: T.Dict[str, str] = {}
self.needs: T.Dict[str, T.List[str]] = {}
self.needs: collections.defaultdict[str, T.List[str]] = collections.defaultdict(list)
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']:
def scan_file(self, fname: str, lang: Literal['cpp', 'fortran']) -> None:
if lang == '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}.')
self.scan_cpp_file(fname)

def scan_fortran_file(self, fname: str) -> None:
fpath = pathlib.Path(fname)
Expand All @@ -63,10 +58,7 @@ def scan_fortran_file(self, fname: str) -> None:
# 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]
self.needs[fname].append(needed)
if export_match:
exported_module = export_match.group(1).lower()
assert exported_module not in modules_in_this_file
Expand Down Expand Up @@ -97,10 +89,7 @@ def scan_fortran_file(self, fname: str) -> None:
# 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]
self.needs[fname].append(parent_module_name_full)

def scan_cpp_file(self, fname: str) -> None:
fpath = pathlib.Path(fname)
Expand All @@ -109,10 +98,7 @@ def scan_cpp_file(self, fname: str) -> None:
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]
self.needs[fname].append(needed)
if export_match:
exported_module = export_match.group(1)
if exported_module in self.provided_by:
Expand All @@ -121,14 +107,8 @@ def scan_cpp_file(self, fname: str) -> None:
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']:
def module_name_for(self, src: str, lang: Literal['cpp', 'fortran']) -> str:
if lang == '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
Expand All @@ -138,23 +118,20 @@ def module_name_for(self, src: str) -> str:
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.')
return '{}.ifc'.format(self.exports[src])

def scan(self) -> int:
for s in self.sources:
self.scan_file(s)
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 in self.sources:
objfilename = self.objname_for(src)
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))
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:
Expand All @@ -167,7 +144,7 @@ def scan(self) -> int:

for modname in mods_and_submods_needed:
provider_src = self.provided_by[modname]
provider_modfile = self.module_name_for(provider_src)
provider_modfile = self.module_name_for(provider_src, lang)
# Prune self-dependencies
if provider_src != src:
module_files_needed.append(provider_modfile)
Expand All @@ -190,9 +167,7 @@ def scan(self) -> int:
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)
assert len(args) == 2, 'got wrong number of arguments!'
outfile, pickle_file = args
scanner = DependencyScanner(pickle_file, outfile)
return scanner.scan()
Loading