Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Bazel aquery jsonproto ──extract_bazel.py───────┘
- **Compile parity** — every TU compiled with equivalent flags/defines/
includes (project-wide TU-set), and the external link-dependency closure
matches.
- **Link consistency** — every name-aligned executable/shared-lib links with
- **Link consistency** — every name-aligned executable links with
equivalent link flags. The per-binary stage: the *artifacts* agree, not just
the TUs.

Expand All @@ -61,6 +61,15 @@ Bazel aquery jsonproto ──extract_bazel.py───────┘
Bazel flags are tolerated (listed in `diff.json` as `bazel_only_flags`). No
artifact/symbol diff or test execution yet.

**`converged: true` is parity of command lines, nothing more.** Check
separately what it cannot see:
- **Generated file contents.** A value that reaches only a generated file
(`configure_file` output, genrule output) never appears on a command line.
Byte-compare each generated file against the reference build's.
- **How shared libraries link.** A shared library is compared by its TUs
only; its link flags and the libraries it links are not checked (the
external-dep check is project-wide). Run a binary that loads each one.

### How targets are matched

- **Roles:** each target gets a `role` (`production`, `test`, `dashboard`,
Expand Down Expand Up @@ -305,14 +314,16 @@ comparisons.
The diff reports both parity stages at once: **compile-parity** kinds
(`missing_tu`, `defines_diff`, `includes_diff`, `flags_diff`, `missing_dep`,
`missing_target`) and the **link-consistency** kind (`link_flags_diff`, one per
name-aligned executable/shared library). Fix compile parity first — link flags
are easiest to reason about once the TUs underneath agree.
name-aligned executable). Fix compile parity first — link flags are easiest to
reason about once the TUs underneath agree.

`error`-severity kinds (above) block convergence and are what you fix. The diff
also emits **WARN-only** kinds that don't block `converged` — `extra_tu` /
`extra_target` (compiled/built on the Bazel side but not the reference) and
`extra_test_tu` — note them but they need no action unless they point to
something you didn't intend to add.
something you didn't intend to add. The WARN `configured_files_not_compared`
does need action: byte-compare each listed file against the one your Bazel
build generates.

#### Triage findings {#triage}

Expand Down
2 changes: 2 additions & 0 deletions docs/REFERENCE-cmake.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ python3 "$SKILL_DIR/scripts/extract_cmake.py" <build> <repo_root> model.cmake.js
> the content differ are TODO (see
> [TODO-configure-time-generation.md](TODO-configure-time-generation.md)). This
> is distinct from build-time codegen (genrules), which is also unmodeled.
> `diff.py` lists them in a `configured_files_not_compared` warning (see
> [Diff against reference build](../SKILL.md#diff)).

The project flags passed to `cmake` define the platform/options the Bazel
aquery must mirror.
Expand Down
44 changes: 34 additions & 10 deletions scripts/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
Parity has two stages, both reported here:
COMPILE PARITY -- per-TU flag/define/include equivalence (project-wide
TU-set) + external link-dependency closure.
LINK CONSISTENCY -- per name-aligned executable/shared-lib, equivalent link
flags (link_flags_diff).
LINK CONSISTENCY -- per name-aligned executable, equivalent link flags
(link_flags_diff). Shared libraries are compared by
their TUs only; their link lines are not checked.

ASYMMETRY: a flag present in CMake but missing in Bazel is a discrepancy
(under-specified Bazel target -> likely wrong build). A flag present only in
Expand Down Expand Up @@ -73,7 +74,7 @@ class Kind(str, Enum):
DEFINES_DIFF = "defines_diff"
INCLUDES_DIFF = "includes_diff"
FLAGS_DIFF = "flags_diff"
LINK_FLAGS_DIFF = "link_flags_diff" # executable/shared-lib link flags
LINK_FLAGS_DIFF = "link_flags_diff" # executable link flags
MISSING_DEP = "missing_dep"
# Java (CompileGroup) source-set comparison:
MISSING_JAVA_SRC = "missing_java_src" # .java compiled in A but not B
Expand All @@ -82,6 +83,8 @@ class Kind(str, Enum):
MISSING_TEST_TU = "missing_test_tu" # test source compiled in cmake, not bazel
EXTRA_TEST_TU = "extra_test_tu" # test source compiled in bazel, not cmake
TEST_BINARY_COUNT = "test_binary_count" # differing number of test executables
# configure-time generated files exist but their CONTENTS are not compared:
CONFIGURED_FILES = "configured_files_not_compared"


@dataclass
Expand Down Expand Up @@ -185,7 +188,7 @@ def _diff_tu(target: str, a: TranslationUnit, b: TranslationUnit,
def _diff_link_flags(name: str, ta: "TargetView", tb: "TargetView",
cfg: "MigrationConfig",
notes: Optional[DiffNotes] = None) -> List[Discrepancy]:
"""Asymmetric link-flag subset for one aligned executable/shared-lib: every
"""Asymmetric link-flag subset for one aligned executable: every
CMake link flag must be present on the Bazel side; extra Bazel link flags
are tolerated (and recorded in `notes`). Reviewer-approved differences
(cfg.link_flag_ignored) are dropped from both sides first. Mirrors the
Expand Down Expand Up @@ -546,9 +549,30 @@ def diff_models(a: CanonicalModel, b: CanonicalModel,
# ---- tests (opt-in) ----------------------------------------------------
if cfg.include_tests:
out.extend(_diff_tests(a_views, b_views, cfg, a_all, b_all, notes))

# ---- configure-time generated files: recorded, NOT compared -------------
# configure_file() outputs never reach a command line, so a value that
# differs only inside one (a config.h define, a version string) is
# invisible to everything above. Say so in the worklist itself, where the
# reader of diff.json will see it, rather than only in the docs.
out.extend(_configured_files_warning(a))
return out


def _configured_files_warning(a: CanonicalModel) -> List[Discrepancy]:
files = a.configured_files
if not files:
return []
names = sorted(files)
n_inputs = sum(1 for c in files.values() if c.is_compile_input)
return [Discrepancy(
Kind.CONFIGURED_FILES.value, Severity.WARN.value, "<configure>",
f"{len(names)} configure-time generated file(s), {n_inputs} on a compile "
"include path, are not covered by this comparison: byte-compare each "
"against the file the Bazel build generates",
cmake_only=names)]


def _diff_tests(a_views: Dict[str, TargetView], b_views: Dict[str, TargetView],
cfg: "MigrationConfig", a_all: set, b_all: set,
notes: Optional[DiffNotes] = None) -> List[Discrepancy]:
Expand All @@ -566,12 +590,12 @@ def _diff_tests(a_views: Dict[str, TargetView], b_views: Dict[str, TargetView],
sources compile but are never linked into runnable tests.

SCOPE: tests get the COMPILE-PARITY stage only. LINK CONSISTENCY (per-binary
link flags and link-dep closure) runs for PRODUCTION executables/shared
libs, not for test binaries: tests align by source-set union, not by binary
identity, so there's no per-binary link command to compare. So with tests
on, "converged" means production artifacts are link-consistent AND all test
SOURCES compile equivalently -- not that each test binary links identically.
Extending link consistency to per-test-binary is a natural future increment.
link flags) runs for PRODUCTION executables, not for test binaries: tests
align by source-set union, not by binary identity, so there's no
per-binary link command to compare. So with tests on, "converged" means
production artifacts are link-consistent AND all test SOURCES compile
equivalently -- not that each test binary links identically. Extending
link consistency to per-test-binary is a natural future increment.
"""
out: List[Discrepancy] = []
a_tests = {n for n, t in a_views.items()
Expand Down
17 changes: 17 additions & 0 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,23 @@ def test_ndebug_without_notes_reports_sides_only():
assert "bazel_added" not in nd and nd["differs"] is False, nd


def test_configured_files_raise_a_non_blocking_warning():
from model import ConfiguredFile
a = _model(tu_from_raw("src/a.cpp", ["-DX"], is_bazel=False), is_bazel=False)
b = _model(tu_from_raw("src/a.cpp", ["-DX"], is_bazel=True), is_bazel=True)
assert diff_models(a, b) == [] # nothing configured -> no warning
a.add_configured_file(ConfiguredFile("config.h", "/b/config.h",
is_compile_input=True))
a.add_configured_file(ConfiguredFile("foo.pc", "/b/foo.pc"))
discs = diff_models(a, b)
res = summarize(discs)
assert res["converged"] and res["warnings"] == 1, res
w = discs[0]
assert w.kind == "configured_files_not_compared" and w.severity == "warn"
assert w.cmake_only == ["config.h", "foo.pc"]
assert w.detail.startswith("2 configure-time generated file(s), 1 on a")


def test_ignore_flags_and_defines_suppress_diffs():
# A reviewer-approved flag/define difference is suppressed via config and
# the diff converges; without the config it's an error.
Expand Down
26 changes: 26 additions & 0 deletions tests/test_extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ def _model_of(target):
return m


def test_configured_files_reach_the_diff_end_to_end():
# The trace -> extract_cmake -> model JSON -> diff path, with the command
# spelled in capitals as large projects write it.
from serialize import dump_model, load_model
with tempfile.TemporaryDirectory() as root:
build = _write_cmake_fixture(root)
trace = os.path.join(root, "trace.jsonl")
with open(trace, "w") as f:
f.write(json.dumps({"version": {"major": 1, "minor": 2}}) + "\n")
f.write(json.dumps({"cmd": "CONFIGURE_FILE", "file": REPO + "/CMakeLists.txt",
"line": 3, "args": [REPO + "/config.h.in",
build + "/config.h"]}) + "\n")
path = os.path.join(root, "model.cmake.json")
dump_model(extract_cmake.extract(build, REPO, trace), path)
a = load_model(path)
assert list(a.configured_files) == ["config.h"], a.configured_files

aq_path = os.path.join(root, "aquery.json")
with open(aq_path, "w") as f:
json.dump(AQUERY, f)
res = summarize(diff_models(a, extract_bazel.extract(aq_path, REPO)))
assert res["converged"], res
kinds = [d["kind"] for d in res["discrepancies"]]
assert kinds == ["configured_files_not_compared"], kinds


def test_cmake_extracts_canonical_flags():
with tempfile.TemporaryDirectory() as root:
build = _write_cmake_fixture(root)
Expand Down
Loading