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
17 changes: 15 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ reads it today):
"dep_map": { "Catch2Main": "catch2_main" },
"exclude_targets": ["benchmark", "some_tool"],
"include_tests": false,
"toolchain_includes": ["/opt/sdk/usr/include"],
"ignore": {
"defines": ["BORINGSSL_DISPATCH_TEST"],
"flags": ["-fvisibility=hidden"],
Expand Down Expand Up @@ -154,6 +155,17 @@ Fields:
wins). `include_prefixes` just deletes the path (a blind spot) — use only when
there's no counterpart to map to. Search **order** is not enforced (presence
only) — see `docs/FUTURE-include-order-collision-check.md`.
- **`toolchain_includes`** (top level) — include roots the Bazel toolchain adds
**at execution time** (a compiler wrapper's `-idirafter <sdk>/usr/include`,
sysroot builtin dirs), so `aquery` never shows them while the reference spells
them out on every TU. An **assertion, not a suppression**: each root is
*required* on every CMake TU (absence is an `includes_diff` error) and counts
as present on the Bazel side. Matched by directory identity, not prefix, so a
root beneath it (`<sdk>/usr/include/json-c`) is still verified. Don't guess
the roots — ask the driver, with the command and env the Bazel toolchain uses:
`python3 scripts/probe_toolchain.py [--env K=V] -- <compiler>` prints its
search lists, injected include flags and a snippet to paste. A plain host
build whose reference passes no such roots needs none.

The `ignore` and `target_map`/`exclude_targets` lists are applied at **diff
time** to **both sides**, so you can tune them and re-diff without re-running
Expand Down Expand Up @@ -313,7 +325,7 @@ correctness flags).
| `missing_target` | add the missing `cc_binary`, or `target_map` a renamed exe, or `exclude_targets` if out of scope |
| `missing_tu` | add the source to some library's `srcs`, or `exclude_targets` if it's a vendored/out-of-scope subtree |
| `defines_diff` | add each `cmake_only` define to `defines`, or `ignore.defines` it |
| `includes_diff` | add the missing CMake include root to `includes`; for a dep whose root is spelled differently each side, `ignore.include_map` it (preferred) or `ignore.include_prefixes` it |
| `includes_diff` | add the missing CMake include root to `includes`; for a dep whose root is spelled differently each side, `ignore.include_map` it (preferred) or `ignore.include_prefixes` it; a root the compiler wrapper/sysroot adds itself goes in `toolchain_includes`. Under `bazel_only` with a `toolchain_includes` detail: the reference no longer carries a declared root |
| `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 CMake repeats at link) |
| `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`) |
Expand Down Expand Up @@ -399,7 +411,8 @@ build brings (no action graph to extract, traversal-order-dependent output,
```bash
python3 tests/test_engine.py && python3 tests/test_extractors.py \
&& python3 tests/test_maven.py && python3 tests/test_extract_npm.py \
&& python3 tests/test_triage.py && python3 tests/test_configure.py
&& python3 tests/test_triage.py && python3 tests/test_configure.py \
&& python3 tests/test_probe_toolchain.py
```

Extractor tests run against fixtures that mirror the documented File API,
Expand Down
60 changes: 59 additions & 1 deletion scripts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@
Applied at DIFF time and to BOTH sides, so tuning them and
re-diffing needs no re-extraction.

* toolchain_includes : include roots the BAZEL TOOLCHAIN supplies without
them appearing in aquery's argv (a compiler wrapper's runtime
-idirafter, the sysroot's builtin dirs). Not a suppression:
the reference is REQUIRED to carry each one, and only the
Bazel side is excused. See MigrationConfig.toolchain_includes.

Because it's a file, every suppression is an explicit, reviewable, version-
controlled line -- a durable record of why a given difference was accepted.

Example any2bazel.json:
{
"target_map": { },
"toolchain_includes": ["/opt/sdk/staging/usr/include"],
"ignore": {
"defines": ["BORINGSSL_DISPATCH_TEST"],
"flags": ["-Wctad-maybe-unsupported", "-fvisibility=hidden"],
Expand All @@ -46,8 +53,9 @@

import json
import os
import posixpath
from dataclasses import dataclass, field
from typing import Dict, List, Set
from typing import Dict, List, Optional, Set

CONFIG_FILENAME = "any2bazel.json"

Expand Down Expand Up @@ -81,6 +89,30 @@ class MigrationConfig:
# 'external/absl+' and 'bazel-out/.../external/absl+' twin both -> '@absl').
# Longest from-prefix wins. Applied before ignore_include_prefixes.
include_map: tuple = () # tuple of (from_prefix, to_token)
# Include roots the BAZEL TOOLCHAIN provides IMPLICITLY -- a compiler
# wrapper that adds `-idirafter <sdk>/usr/include` when it runs, the
# sysroot's builtin dirs (cxx_builtin_include_directories) -- which the
# reference build spells out explicitly on every TU (e.g. in
# CMAKE_C_FLAGS) but which `bazel aquery` can never show, because they are
# added at EXECUTION time. The asymmetric lever for that:
# * CMake side: each root is REQUIRED on every TU. Its absence is an
# includes_diff error (the config asserts a fact about the reference;
# if the reference stops carrying the root the assertion is stale, or
# the reference regressed, and either way someone must look).
# * Bazel side: the root counts as present; it is dropped from the
# comparison instead of being demanded of the argv.
# This is the opposite of ignore_include_prefixes, which deletes the root
# from BOTH sides and so never notices the reference losing it.
# Matching is by DIRECTORY IDENTITY (normalized path equality), not by
# prefix: a root beneath a toolchain root -- `<sdk>/usr/include/json-c`
# under `<sdk>/usr/include` -- is a different search directory with its
# own meaning and is compared on its own merits (mapped by include_map if
# spelled differently per side, else required verbatim). A prefix match
# would swallow it exactly the way include_prefixes does. Populate from the
# toolchain, not from memory: scripts/probe_toolchain.py prints the
# search list the (wrapped) compiler actually uses. A plain host build
# whose reference passes no such roots needs none.
toolchain_includes: tuple = ()
# Source-path PREFIX REWRITES for TRANSLATION-UNIT keys, applied to both
# sides before the TU-set comparison -- the compile-source analogue of
# include_map. Same (from_prefix, to_token) shape, longest-prefix-wins.
Expand Down Expand Up @@ -112,6 +144,12 @@ class MigrationConfig:
# test-binary existence/count check. See PARTICIPATING_ROLES in diff.py.
include_tests: bool = False

def __post_init__(self):
# Normalize the declared toolchain roots once, so the identity test
# in toolchain_root() is plain equality against canonical spellings.
self.toolchain_includes = tuple(
r for r in (_norm_root(t) for t in self.toolchain_includes) if r)

def flag_ignored(self, flag: str) -> bool:
return (flag in self.ignore_flags
or any(flag.startswith(p) for p in self.ignore_flag_prefixes))
Expand Down Expand Up @@ -157,10 +195,29 @@ def map_source(self, source: str) -> str:
def include_ignored(self, include: str) -> bool:
return any(include.startswith(p) for p in self.ignore_include_prefixes)

def toolchain_root(self, include: str) -> Optional[str]:
"""The declared toolchain root that `include` IS (the same directory,
after path normalization), or None. Deliberately not a prefix match --
see the toolchain_includes field: a subdirectory of a toolchain root is
its own search directory and must be compared, not excused."""
norm = _norm_root(include)
return norm if norm in self.toolchain_includes else None

def target_excluded(self, name: str) -> bool:
return name in self.exclude_targets


def _norm_root(path: str) -> str:
"""Canonical spelling of an include directory for identity comparison:
POSIX separators, `.`/`..` segments collapsed, no trailing slash. The same
normalization canonicalize.py applies to the models' include paths, so a
config entry and a model root that name one directory compare equal."""
p = (path or "").strip().replace(os.sep, "/")
if not p:
return ""
return posixpath.normpath(p)


def load(path: str) -> MigrationConfig:
"""Load config from an explicit file path, or return empty if absent."""
if not path or not os.path.isfile(path):
Expand All @@ -181,6 +238,7 @@ def load(path: str) -> MigrationConfig:
(e["from"], e["to"]) for e in ig.get("include_map", [])),
source_map=tuple(
(e["from"], e["to"]) for e in ig.get("source_map", [])),
toolchain_includes=tuple(obj.get("toolchain_includes", []) or ()),
exclude_targets=set(obj.get("exclude_targets", [])),
bazel_args=tuple(obj.get("bazel_args", [])),
include_tests=bool(obj.get("include_tests", False)),
Expand Down
60 changes: 49 additions & 11 deletions scripts/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import json
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple

from canonicalize import BAZEL_TOLERATED_FLAG_PREFIXES
from config import MigrationConfig
Expand Down Expand Up @@ -110,12 +110,17 @@ def _diff_tu(target: str, a: TranslationUnit, b: TranslationUnit,
))

# includes: PRESENCE check (order currently NOT enforced -- see below).
# Two normalizations, applied to both sides first:
# 1. include_map: rewrite differing spellings of the same dep root to a
# Three normalizations, applied to both sides first:
# 1. toolchain_includes: a root the Bazel toolchain supplies at execution
# time (wrapper -idirafter, sysroot builtin dir) is set aside on both
# sides -- excused from the Bazel argv, but REQUIRED on the CMake side
# (checked below). Decided first, on the exact directory, so neither
# lever below can rewrite or delete it out from under the check.
# 2. include_map: rewrite differing spellings of the same dep root to a
# canonical token, so the check is PRESERVED (the dep must still be
# present). Collapse adjacent dups the rewrite produces (Bazel's
# external/X and bazel-out/.../external/X both -> the token).
# 2. include_ignored: drop blind-spot prefixes entirely.
# 3. include_ignored: drop blind-spot prefixes entirely.
#
# We require every CMake include root to be PRESENT on the Bazel side, but
# do NOT (yet) enforce relative ORDER. Order only changes the build when the
Expand All @@ -124,8 +129,8 @@ def _diff_tu(target: str, a: TranslationUnit, b: TranslationUnit,
# verifier exists, enforcing order produced benign false positives (e.g.
# boringssl: project `include` vs vendored gtest roots, disjoint headers,
# reordered). See docs/FUTURE-include-order-collision-check.md.
a_inc = _norm_includes(a.includes, cfg)
b_inc = _norm_includes(b.includes, cfg)
a_inc, a_tool = _norm_includes(a.includes, cfg)
b_inc, _ = _norm_includes(b.includes, cfg)
missing = [i for i in a_inc if i not in set(b_inc)]
if missing:
out.append(Discrepancy(
Expand All @@ -136,6 +141,23 @@ def _diff_tu(target: str, a: TranslationUnit, b: TranslationUnit,
cmake_only=missing,
bazel_only=[i for i in b_inc if i not in set(a_inc)],
))
# toolchain_includes is an ASSERTION about the reference: every declared
# root must be on this CMake TU. One that is not means the config is stale
# or the reference lost the root -- and the Bazel side, which was excused
# from it above, now carries a search directory the reference does not.
# Reported under bazel_only (implicitly present there, absent in CMake);
# ignore.include_prefixes would have deleted it from both sides silently.
absent = [t for t in cfg.toolchain_includes if t not in a_tool]
if absent:
out.append(Discrepancy(
kind=Kind.INCLUDES_DIFF.value,
severity=Severity.ERROR.value,
target=target, tu=a.source,
detail="toolchain include root (toolchain_includes) not on the "
"cmake side: stale config, or the reference lost it",
cmake_only=[],
bazel_only=absent,
))

# other flags: asymmetric subset -- A must be subset of B.
# Reviewer-approved ignores are dropped from both sides first.
Expand Down Expand Up @@ -173,18 +195,34 @@ def _diff_link_flags(name: str, ta: "TargetView", tb: "TargetView",
return []


def _norm_includes(includes, cfg) -> tuple:
"""Apply include_map rewrites then drop ignored prefixes, collapsing the
consecutive duplicates a rewrite can create (e.g. external/X and its
bazel-out twin both map to the same token). Order preserved."""
def _norm_includes(includes, cfg) -> Tuple[tuple, frozenset]:
"""Normalize one side's include roots for comparison. Returns
(roots, toolchain_roots):

roots -- include_map rewrites applied, then ignored prefixes
dropped, consecutive duplicates a rewrite can create
collapsed (external/X and its bazel-out twin both map
to one token). Order preserved.
toolchain_roots -- the cfg.toolchain_includes entries this TU carries
explicitly (normalized spelling). Set aside BEFORE
the map/ignore steps, on directory identity, so a
toolchain root can neither be rewritten to a token
nor deleted by a prefix; _diff_tu requires each
declared root in the CMake side's set.
"""
out = []
tool = set()
for inc in includes:
root = cfg.toolchain_root(inc)
if root is not None:
tool.add(root)
continue
mapped = cfg.map_include(inc)
if cfg.include_ignored(mapped) or cfg.include_ignored(inc):
continue
if not out or out[-1] != mapped:
out.append(mapped)
return tuple(out)
return tuple(out), frozenset(tool)


# Roles the parity diff actually compares. Everything else (dashboard,
Expand Down
Loading