From b368c3a7d188edadd020de3ee31904679794dffb Mon Sep 17 00:00:00 2001 From: Mateusz Olczyk Date: Tue, 20 Oct 2026 18:24:15 +0200 Subject: [PATCH] Resolve CMake File API source paths against the top-level source dir The File API reports `sources[].path` relative to the top-level source directory (`codemodel.paths.source`, i.e. `cmake -S`) whenever the file is inside it, and absolute otherwise. extract_cmake passed those paths through unchanged, and reconstruct only rebases absolute paths onto ``. This is fine when `-S` is the repo root, but when the CMake project lives in a subdirectory of the Bazel workspace (`cmake -S sub/proj`), TU keys came out as `src/a.c` instead of `sub/proj/src/a.c`. Those sources then looked missing on the CMake side and extra on the Bazel side, even though both builds compiled the same files. Only sources under the project dir were affected; sources outside it were already reported absolute and keyed correctly, so a target could show a mix of correct and bogus keys. extract_cmake now makes source paths absolute against `codemodel.paths.source` (falling back to `` for replies without `paths`), in both the synthesized `-c` argument and the action inputs. Repo-relative keying then works as it already does for include directories. Out-of-tree absolute sources are unchanged. A new extractor test covers a project configured from a subdirectory with one in-tree and one out-of-tree source; it fails without the fix. docs/REFERENCE-cmake.md notes that `-S` may be any directory under ``. --- docs/REFERENCE-cmake.md | 5 +++++ scripts/extract_cmake.py | 15 +++++++++++---- tests/test_extractors.py | 26 +++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/REFERENCE-cmake.md b/docs/REFERENCE-cmake.md index 49ed665..1d2155a 100644 --- a/docs/REFERENCE-cmake.md +++ b/docs/REFERENCE-cmake.md @@ -48,6 +48,11 @@ aquery must mirror. - **Target names** in `target_map` and `exclude_targets` keys are CMake target names (e.g. `"bssl": ":bssl"`). +- **Project in a subdirectory**: `cmake -S` may be any directory under + `` (e.g. one CMake project per firmware image inside a larger + Bazel workspace). The File API reports in-tree sources relative to that + top-level source dir; the extractor resolves them against the codemodel's + `paths.source`, so TU keys stay relative to ``. - **`dep_map`**: CMake spells external link deps as an archive basename (`Catch2Main`) or an imported target (`OpenSSL::SSL`), while Bazel spells them `catch2_main` / `ssl`. diff --git a/scripts/extract_cmake.py b/scripts/extract_cmake.py index f070f1f..4e6b451 100644 --- a/scripts/extract_cmake.py +++ b/scripts/extract_cmake.py @@ -29,7 +29,8 @@ python3 extract_cmake.py model.cmake.json The reply lands in /.cmake/api/v1/reply/. We read the codemodel index, -then each per-target jsonFile. +then each per-target jsonFile. may be or any directory under +it: source paths are resolved against the codemodel's top-level source dir. """ from __future__ import annotations @@ -139,10 +140,14 @@ def _target_id_to_name(codemodel: dict) -> Dict[str, str]: return out -def _parse_target(tobj: dict, repo_root: str) -> Target: +def _parse_target(tobj: dict, source_dir: str) -> Target: name = tobj["name"] kind = _KIND.get(tobj.get("type", ""), TargetKind.UNKNOWN) - sources = [s["path"] for s in tobj.get("sources", [])] + # The File API gives sources inside the top-level source dir relative to IT + # (not to repo_root, which differs when `cmake -S` is a subdirectory), and + # others absolute. Make them all absolute so they key repo-relative later. + sources = [os.path.normpath(os.path.join(source_dir, s["path"])) + for s in tobj.get("sources", [])] # Synthesize one CppCompile Action per source: an argv the differ parses the # same way it parses Bazel's. CMake has no real command line, so we build the @@ -230,6 +235,8 @@ def extract(build_dir: str, repo_root: str, reply_dir = os.path.join(build_dir, ".cmake", "api", "v1", "reply") codemodel = _find_codemodel(reply_dir) id_to_name = _target_id_to_name(codemodel) + # top-level source dir (`cmake -S`); may be any directory under repo_root + source_dir = codemodel.get("paths", {}).get("source") or repo_root model = CanonicalModel(build_system=BuildSystem.CMAKE, repo_root=repo_root) include_dirs = set() @@ -237,7 +244,7 @@ def extract(build_dir: str, repo_root: str, for tref in cfg["targets"]: with open(os.path.join(reply_dir, tref["jsonFile"])) as f: tobj = json.load(f) - target = _parse_target(tobj, repo_root) + target = _parse_target(tobj, source_dir) _attach_deps(target, tobj, id_to_name) model.add(target) for cg in tobj.get("compileGroups", []): diff --git a/tests/test_extractors.py b/tests/test_extractors.py index fd282b7..82b62bd 100644 --- a/tests/test_extractors.py +++ b/tests/test_extractors.py @@ -104,15 +104,15 @@ def _view(model, name): } -def _write_cmake_fixture(root): +def _write_cmake_fixture(root, codemodel=CODEMODEL, target=TARGET_MYLIB): reply = os.path.join(root, "build", ".cmake", "api", "v1", "reply") os.makedirs(reply) with open(os.path.join(reply, "index-0.json"), "w") as f: json.dump({"objects": [{"kind": "codemodel", "jsonFile": "codemodel.json"}]}, f) with open(os.path.join(reply, "codemodel.json"), "w") as f: - json.dump(CODEMODEL, f) + json.dump(codemodel, f) with open(os.path.join(reply, "target-mylib.json"), "w") as f: - json.dump(TARGET_MYLIB, f) + json.dump(target, f) return os.path.join(root, "build") @@ -150,6 +150,26 @@ def test_cmake_extracts_canonical_flags(): assert "-std=c++17" in tu.flags +def test_cmake_sources_resolve_against_top_level_source_dir(): + # `cmake -S sub/proj` with repo_root one level up: the File API gives + # in-tree sources relative to the TOP-LEVEL SOURCE DIR (paths.source), not + # to repo_root, and out-of-tree sources absolute. TU keys must still be + # repo-relative, else every source looks missing/extra against Bazel. + codemodel = dict(CODEMODEL, paths={"source": REPO + "/sub/proj", + "build": REPO + "/sub/proj/build"}) + target = dict(TARGET_MYLIB, + sources=[{"path": "src/a.cpp"}, {"path": "/opt/sdk/s.c"}], + compileGroups=[dict(TARGET_MYLIB["compileGroups"][0], + sourceIndexes=[0, 1])]) + with tempfile.TemporaryDirectory() as root: + build = _write_cmake_fixture(root, codemodel, target) + a = extract_cmake.extract(build, REPO) + tus = _view(a, "mylib").tu_map() + assert sorted(tus) == ["/opt/sdk/s.c", "sub/proj/src/a.cpp"], sorted(tus) + compile_inputs = [act.inputs for act in a.targets["mylib"].actions] + assert (REPO + "/sub/proj/src/a.cpp",) in compile_inputs, compile_inputs + + def test_bazel_infers_kind_and_drops_defaults(): with tempfile.TemporaryDirectory() as root: aq_path = os.path.join(root, "aquery.json")