Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ccf83d5
Check bazel_args with Bazel's own flag parser
anakanemison Nov 2, 2026
57aed8b
Decide target_map renames in one place, and list renamed exclusions
anakanemison Sep 26, 2026
8fdabc2
Compare every variant of a multi-variant source, deterministically
anakanemison Sep 26, 2026
f46ae8b
Report Bazel-only flags beside the verdict
anakanemison Sep 26, 2026
6dca07c
Report each side's optimization and debug level in the diff summary
anakanemison Sep 26, 2026
bbefda3
Apply -U in flag canonicalization: the last -D/-U wins
anakanemison Sep 26, 2026
264f184
Report Bazel-only defines and NDEBUG state beside the verdict
anakanemison Sep 26, 2026
e965f37
Warn in diff.json when configure-time generated files are not compared
anakanemison Sep 26, 2026
3aea0a3
State the convergence boundary where convergence is defined
anakanemison Sep 26, 2026
d6822a8
Say that shared-library link lines are not compared
anakanemison Sep 26, 2026
306d4f1
Report compile-only flags on CMake link lines as a warning
anakanemison Sep 26, 2026
da2e00c
Compare link-interface flags from CMake's libraries fragments
anakanemison Sep 26, 2026
98245b4
Count link-only targets' external libraries in the link-closure check
anakanemison Oct 16, 2026
48188bc
Say what to do when the toolchain refuses to disable linker param files
anakanemison Oct 16, 2026
3d6dc88
Require parity for the default bazel invocation
anakanemison Sep 26, 2026
4f25e37
Pin the reference configuration and record its command
anakanemison Sep 26, 2026
651c209
Tell the agent to map the build type to flags, not to a compilation mode
anakanemison Sep 26, 2026
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
89 changes: 71 additions & 18 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,24 @@ 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
equivalent link flags. The per-binary stage: the *artifacts* agree, not just
the TUs.
- **Link consistency** — every name-aligned executable links with
equivalent link flags, including those a dependency adds through its link
interface (`-fopenmp`, `-Xlinker -export-dynamic`). The per-binary stage:
the *artifacts* agree, not just the TUs.

Done when both stages converge. The comparison is **asymmetric**: every
correctness-relevant reference flag must be present on the Bazel side; extra
Bazel flags are tolerated. No artifact/symbol diff or test execution yet.
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

Expand All @@ -76,11 +87,13 @@ Bazel aquery jsonproto ──extract_bazel.py───────┘
### Canonicalization

`canonicalize.py` strips noise before comparing flags. Universal mechanics
(driver/wrapper paths, `-c`/`-o`, sysroot, reproducibility defines, `-O*`/`-g*`)
are dropped in code — never your concern. Judgment calls (warning-set or cosmetic
differences) go in `any2bazel.json`'s `ignore`. Correctness flags (`-std=*`,
`-fno-exceptions`, `-fno-rtti`, …) must never be ignored — they surface as hard
errors by design.
(driver/wrapper paths, `-c`/`-o`, sysroot, reproducibility defines) are dropped
in code — never your concern. `-O*`/`-g*` are dropped from the per-TU
comparison too, but the level still matters: `levels` in `diff.json` reports
each side's, and `differs: true` there is real. Judgment calls (warning-set or
cosmetic differences) go in `any2bazel.json`'s `ignore`. Correctness flags
(`-std=*`, `-fno-exceptions`, `-fno-rtti`, …) must never be ignored — they
surface as hard errors by design.

## The migration config: `any2bazel.json`

Expand All @@ -95,6 +108,7 @@ whether its frontend reads it:
"dep_map": { "Catch2Main": "catch2_main" },
"exclude_targets": ["benchmark", "some_tool"],
"include_tests": false,
"reference": { "configure_command": "cmake -S . -B build -G Ninja" },
"ignore": {
"defines": ["BORINGSSL_DISPATCH_TEST"],
"flags": ["-fvisibility=hidden"],
Expand All @@ -112,9 +126,12 @@ whether its frontend reads it:
```

Fields:
- **`bazel_args`** — the extra args aquery must run with to mirror the real
build (see [Extract the Bazel action graph](#extract-bazel-graph)). Recorded so
the comparison is reproducible; read by the operator, not the diff.
- **`bazel_args`** — the extra **flags** aquery must run with (see
[Extract the Bazel action graph](#extract-bazel-graph)): only what the
project's documented build passes, as in the example (BoringSSL expects
`-fno-exceptions` at top level). Never a command (`["aquery", ...]`); check
it with Bazel before running aquery. Recorded so the comparison is
reproducible.
- **`target_map`** — `reference_name → bazel_name` for intentionally renamed
**executables**. Bazel targets are keyed by full label, so the value is
usually `:name` (e.g. `"bssl": ":bssl"`). Libraries need no mapping.
Expand All @@ -126,6 +143,11 @@ Fields:
third-party/vendored code Bazel pulls as an external module, or tooling out
of scope. The **only** lever for `missing_tu`/`missing_target` on whole
subtrees. Excluded targets still appear under `excluded.config_excluded`.
- **`reference.configure_command`** — the exact configure command the
reference model came from (see
[Extract the reference model](#extract-reference-model));
`reference.test_configure_command` holds the test-inclusive one
([Diff tests](#diff-tests)). Recorded for reviewers; not read by the diff.
- **`include_tests`** (default `false`) — opt in to also diffing **test**
targets. OFF by default because it requires BOTH models to be extracted with
tests enabled and the **same** test scope (symmetric configure + aquery);
Expand Down Expand Up @@ -223,7 +245,15 @@ python3 scripts/extract_<frontend>.py <…> <repo_root> model.<frontend>.json
Note the platform and options the reference build was configured with; the
Bazel side must mirror them.

### Create initial BUILD.bazel files
Configure the reference build with the project's **documented default**
options: turning anything off (a product, the tests, distribution flags)
changes what "migrated" means, so surface it when
[confirming scope](#confirm-scope). Record the exact command as
`any2bazel.json`'s `reference.configure_command` and never change it after the
first diff; the test-inclusive configure for [Diff tests](#diff-tests) is
recorded apart, as `test_configure_command`.

### Create initial BUILD.bazel files {#create-build-files}

Read `model.<frontend>.json`. For each production target emit a `cc_library` /
`cc_binary` with `srcs`, `hdrs`, `copts`, `defines`, `includes`, `deps`. Library
Expand All @@ -237,6 +267,9 @@ rather than recalled). Put `common --check_direct_dependencies=error` in the
generated `.bazelrc` so a declared version that MVS overrides fails the build
instead of being a warning nobody reads.

The reference guide covers what is specific to translating its build system
(for CMake: the build type).

### Main migration loop {#main-migration-loop}

#### Extract the Bazel action graph {#extract-bazel-graph}
Expand All @@ -255,12 +288,22 @@ instead of being a warning nobody reads.
> - Use the **same platform/options** as the reference build in
> [Extract the reference model](#extract-reference-model), or the two sides
> aren't comparable.
> - For settings **you** add to mirror the reference (e.g. Release's level),
> use `.bazelrc` `build` lines, not a named `--config`. Parity must hold for
> the project's documented build command: a plain `bazel build //...` unless
> the project documents flags, and only those go in `bazel_args`.
> - **Always** call aquery with `--features=-compiler_param_file` and
> `--features=-linker_param_file` to disable use of param files. Param files
> are never generated during `aquery` calls, so when they're in use they'll
> always point to files that are either stale, or do not exist at all.
> Some toolchains refuse this: analysis fails with "unconditionally implies
> feature 'linker_param_file'" (the toolchain rules_cc autodetects on Linux
> does). Then drop that flag, and check that no action's `arguments` in the
> output hold an `@….params` entry: if none does, nothing is hidden.

```bash
# bazel_args, as Bazel parses them; it refuses anything that is not an aquery flag
python3 -c 'import json,os,sys; os.execvp("bazel", ["bazel", "canonicalize-flags", "--for_command=aquery", "--", *json.load(open(sys.argv[1])).get("bazel_args", [])])' any2bazel.json
bazel aquery 'mnemonic("CppCompile|ObjcCompile|CppLink|CppArchive", //...)' \
--features=-compiler_param_file --features=-linker_param_file \
[--config=<name>] [--copt=... --cxxopt=...] \
Expand All @@ -283,7 +326,13 @@ python3 scripts/diff.py model.<frontend>.json model.bazel.json \
```
`diff.json` has `converged` (⇔ zero `error` discrepancies), a `discrepancies`
worklist (each with `kind`, `severity`, `target`, `tu`, `cmake_only`,
`bazel_only`), and an `excluded` map of non-participating targets by role.
`bazel_only`), an `excluded` map of non-participating targets by role,
`levels` (optimization/debug level per side), `ndebug` (assertion state per
side, and on how many compared TU pairs Bazel adds or lacks `NDEBUG`), and
`bazel_only_flags`: flags and defines only the Bazel side has. Those flags are
never a discrepancy and those defines only a WARN (the comparison is
one-directional), so a `-w` or `-Wno-error=` you add converges silently;
review the list before reporting.
`cmake_only` is a historical name: for every frontend it means "present in the
reference, absent in Bazel".
Synthetic target names `<libraries>`, `<external>` (and `<tests>` when
Expand All @@ -293,14 +342,18 @@ 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. `link_compile_only_flags` (warnings,
`-w`, `-pedantic`, `-D`/`-U`, and front-end flags such as `-fexceptions`,
`-fno-common` and `-fvisibility=` that CMake repeats on link lines) needs none
either. 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 Expand Up @@ -337,7 +390,7 @@ correctness flags).
| `defines_diff` | add each `cmake_only` define to `defines`, or `ignore.defines` it |
| `includes_diff` | add the missing reference include root to `includes`; for a dep whose root is spelled differently each side, `ignore.include_map` it (preferred) or `ignore.include_prefixes` it |
| `flags_diff` | add each `cmake_only` flag to `copts`, or `ignore.flags` it |
| `link_flags_diff`| add each `cmake_only` flag to the target's `linkopts`, or `ignore.link_flags` it if benign (e.g. a compile flag the reference repeats at link) |
| `link_flags_diff`| add each `cmake_only` flag to the target's `linkopts`, or `ignore.link_flags` it if benign (a compile flag the reference repeats at link that is not already reported as `link_compile_only_flags`). A flag that differs only by a reference build-tree path (`-Wl,--version-script,<build>/x.map`) is still a correctness flag: `ignore.link_flags` that exact spelling only after confirming the Bazel link passes an equivalent script with the same content |
| `missing_dep` | add the missing external/system dep to the target's `deps`/`linkopts`; if it's just a name spelled differently per build (`Catch2Main` vs `catch2_main`, `OpenSSL::SSL` vs `ssl`), add a `dep_map` entry. External deps are captured from both `-l` flags and archive-file inputs (e.g. `external/catch2+/libcatch2_main.a`) |
| `missing_test_tu`| (tests on) add the test source to a `cc_test`, or `exclude_targets` if out of scope |
| `test_binary_count` | (tests on, warning) differing number of test executables — investigate which side has the extra/missing binary |
Expand Down
23 changes: 22 additions & 1 deletion docs/REFERENCE-cmake.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,29 @@ 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.

## Writing the BUILD files

What is specific to CMake when
[creating the BUILD files](../SKILL.md#create-build-files).

### Build type

`--compilation_mode` is not a translation of `CMAKE_BUILD_TYPE`: Release is
`-O3 -DNDEBUG` where Bazel's `opt` is `-O2 -DNDEBUG`, and Debug is `-g` where
Bazel's default `fastbuild` has neither. A project may set its own default
build type in `CMakeLists.txt` even when none is passed. Read the reference's
level (`levels` in `diff.json`, or `CMAKE_<LANG>_FLAGS_<CONFIG>` in the cache)
and pin it in `.bazelrc`, e.g. `build -c opt` plus `build --copt=-O3`. `opt`
also turns assertions off where the reference keeps them (`ndebug` in
`diff.json`); `build --copt=-UNDEBUG` undoes the toolchain's `-DNDEBUG`, not
one in a rule's `copts`.

## CMake-specific comparison notes

- **Target names** in `target_map` and `exclude_targets` keys are CMake target
Expand All @@ -53,7 +72,9 @@ aquery must mirror.
`catch2_main` / `ssl`.
- **Link flags**: CMake repeats compile/codegen flags (`-fvisibility=hidden`,
`-fno-common`) on the link line, where they're benign, while Bazel doesn't.
This is the common `ignore.link_flags` case.
The differ reports warnings, `-D`/`-U` and the common front-end `-f` flags
as `link_compile_only_flags`; any other benign one is an `ignore.link_flags`
case.

## Tests

Expand Down
Loading
Loading