From 21d4f8e78391a412136e74632e97b4e0480a4b50 Mon Sep 17 00:00:00 2001 From: Lucas Alber Date: Thu, 6 Aug 2026 11:28:08 +0200 Subject: [PATCH] cmake: propagate generated-file dependencies to the compile order The CMake subproject translation dropped two kinds of compile-order dependency on generated files: * OBJECT_DEPENDS, set via set_source_files_properties(), was not parsed at all, so a source including a generated header had no edge to the custom target producing it. * Only the direct custom-target dependencies of a target were collected. CMake's ninja backend emits cmake_object_order_depends_target_* edges for the transitive closure, so a custom target reachable only through another regular target was lost. Both make clean builds fail non-deterministically with a missing generated header. Parse OBJECT_DEPENDS in the trace parser, resolve file-level dependencies to the custom target generating the file, and walk the dependency graph transitively when collecting the custom targets a target must wait for. Fixes #9062 --- mesonbuild/cmake/interpreter.py | 46 ++++++++++++++++++++++++++++----- mesonbuild/cmake/traceparser.py | 32 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index 291157f2f827..284ddfd102cc 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -382,6 +382,21 @@ def postprocess(self, output_target_map: OutputTargetMap, root_src_dir: Path, su elif self.type.upper() not in ['EXECUTABLE', 'OBJECT_LIBRARY']: mlog.warning('CMake: Target', mlog.bold(self.cmake_name), 'not found in CMake trace. This can lead to build errors') + # Handle set_source_files_properties(... OBJECT_DEPENDS ...): compiling a + # source may depend on (usually generated) files. Attach these at the target + # level so the generating custom targets are built first. + if trace.object_depends: + src_keys = set() + for x in self.sources + self.generated: + p = x if x.is_absolute() else self.src_dir / x + src_keys.add(p.resolve().as_posix()) + for src, deps in trace.object_depends.items(): + src_path = Path(src) + if not src_path.is_absolute(): + continue + if src_path.resolve().as_posix() in src_keys: + self.depends_raw += deps + temp = [] for cmd in self.link_libraries: # Let meson handle this arcane magic @@ -502,7 +517,14 @@ def handle_frameworks(flags: T.List[str]) -> T.List[str]: for arg in self.depends_raw: dep_tgt = output_target_map.target(arg) if dep_tgt: - self.depends.append(dep_tgt) + if dep_tgt not in self.depends: + self.depends.append(dep_tgt) + continue + # File-level dependencies (e.g. from OBJECT_DEPENDS) resolve to the + # custom target generating the file + gen = output_target_map.generated(Path(arg)) + if gen and gen not in self.depends: + self.depends.append(gen) def process_object_libs(self, obj_target_list: T.List['ConverterTarget'], linker_workaround: bool) -> None: # Try to detect the object library(s) from the generated input sources @@ -1130,12 +1152,24 @@ def process_target(tgt: ConverterTarget) -> None: if i.name not in processed: process_target(i) objec_libs += [extract_tgt(i)] - for i in tgt.depends: - if not isinstance(i, ConverterCustomTarget): + # CMake guarantees that all dependencies of a target -- including those + # of linked and add_dependencies targets, transitively -- are built + # before any of the target's sources compile (the ninja backend emits + # cmake_object_order_depends_target_* edges for this). Collect all + # reachable custom targets so their generated files exist in time. + dep_stack = list(tgt.depends) + visited_deps: T.Set[str] = set() + while dep_stack: + i = dep_stack.pop(0) + if i.name in visited_deps: continue - if i.name not in processed: - process_custom_target(i) - dependencies += [extract_tgt(i)] + visited_deps.add(i.name) + if isinstance(i, ConverterCustomTarget): + if i.name not in processed: + process_custom_target(i) + dependencies += [extract_tgt(i)] + elif isinstance(i, ConverterTarget): + dep_stack += i.depends # Generate the source list and handle generated sources sources += tgt.sources diff --git a/mesonbuild/cmake/traceparser.py b/mesonbuild/cmake/traceparser.py index 38a2d31a595a..a0e4348e00d1 100644 --- a/mesonbuild/cmake/traceparser.py +++ b/mesonbuild/cmake/traceparser.py @@ -92,6 +92,10 @@ def __init__(self, cmake_version: str, build_dir: Path, env: 'Environment', perm self.explicit_headers: T.Set[Path] = set() + # Object dependencies set via set_source_files_properties(... OBJECT_DEPENDS ...), + # mapping the source file to the files its compilation depends on + self.object_depends: T.Dict[str, T.List[str]] = {} + # T.List of targes that were added with add_custom_command to generate files self.custom_targets: T.List[CMakeGeneratorTarget] = [] @@ -120,6 +124,7 @@ def __init__(self, cmake_version: str, build_dir: Path, env: 'Environment', perm 'add_custom_target': self._cmake_add_custom_target, 'set_property': self._cmake_set_property, 'set_target_properties': self._cmake_set_target_properties, + 'set_source_files_properties': self._cmake_set_source_files_properties, 'target_compile_definitions': self._cmake_target_compile_definitions, 'target_compile_options': self._cmake_target_compile_options, 'target_include_directories': self._cmake_target_include_directories, @@ -605,6 +610,33 @@ def _cmake_set_target_properties(self, tline: CMakeTraceLine) -> None: self.targets[i].properties[name] = value + def _cmake_set_source_files_properties(self, tline: CMakeTraceLine) -> None: + # DOC: https://cmake.org/cmake/help/latest/command/set_source_files_properties.html + args = list(tline.args) + + sources: T.List[str] = [] + idx = 0 + while idx < len(args) and args[idx] != 'PROPERTIES': + if args[idx] in {'DIRECTORY', 'TARGET_DIRECTORY'}: + # skip the scope arguments + idx += 1 + while idx < len(args) and args[idx] not in {'DIRECTORY', 'TARGET_DIRECTORY', 'PROPERTIES'}: + idx += 1 + continue + sources += args[idx].split(';') + idx += 1 + + object_depends: T.List[str] = [] + idx += 1 + while idx + 1 < len(args): + if args[idx] == 'OBJECT_DEPENDS': + object_depends += args[idx + 1].split(';') + idx += 2 + + if object_depends: + for i in sources: + self.object_depends.setdefault(i, []).extend(object_depends) + def _cmake_add_dependencies(self, tline: CMakeTraceLine) -> None: # DOC: https://cmake.org/cmake/help/latest/command/add_dependencies.html args = list(tline.args)