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")