From 692e3928a84cf57273bb732023c93c2c10ec88fc Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:34:59 +0100 Subject: [PATCH] tooling: extract the compile DB in the develop config (--config=clang-tidy) Adopt helly25/mbo#280's approach: extract compile_commands.json in the exact configuration we develop in, rather than the default build config with a post-hoc compiler substitution. A new .bazelrc `clang-tidy` config layers the hermetic clang toolchain (--config=clang) plus an explicit libc++ (std + link), and compile_commands-update.sh runs the extractor's internal aquery under it (passed as a runtime flag, which the fork forwards to aquery). So clang-tidy / clangd parse with the compiler, standard library, include paths and feature macros the real --config=clang builds use, on every platform. Two xff-specific deviations from mbo#280: - Keep --bcce-compiler: xff's toolchain names `cc_wrapper.sh` in the extracted command, not a real clang++. clang-tidy reads that leading token to derive the driver target + resource dir, and a shell script leaves it with the wrong builtins (SDK / libc++ headers fail to parse). Substituting the hermetic clang++ binary fixes that; all OTHER flags still come from the config. - macOS: drop the SDK libc++ redirection. On macOS toolchains_llvm emits `-nostdinc++ -cxx-isystem /usr/include/c++/v1` (Xcode's libc++) while -resource-dir is the hermetic clang's; that mismatch silently degrades clang-tidy's analysis (spurious unused-variable / const-correctness findings). The hermetic clang++ finds its own libc++ by default, so a gated post-filter drops just those two flags; -isysroot stays for the system C headers. Linux never emits the SDK -cxx-isystem, so the filter is a no-op there. Also materialize the generated / virtual-include headers in the SAME config (bazel build --config=clang-tidy //...) so the forests land where the DB points, and drop the now-redundant ~15-line default-config header-build note. Verified: DB names the hermetic clang++ with libc++, and clang-tidy parses the tree clean. --- .bazelrc | 12 ++++ .github/workflows/main.yml | 20 +++--- compile_commands-update.sh | 129 ++++++++++++++++++++++++------------- 3 files changed, 108 insertions(+), 53 deletions(-) diff --git a/.bazelrc b/.bazelrc index f462473..b6f61c4 100644 --- a/.bazelrc +++ b/.bazelrc @@ -63,6 +63,18 @@ common:clang --cxxopt=-gmlt --host_cxxopt=-gmlt common:clang --linkopt=-fuse-ld=lld --host_linkopt=-fuse-ld=lld common:clang --define is_clang=true +# --config=clang-tidy : the configuration we DEVELOP in, and the one the compile +# DB (compile_commands.json) is extracted in - so clang-tidy / clangd parse with +# exactly the compiler, standard library, include paths and feature macros the +# `--config=clang` builds use, not whatever toolchain bazel would autodetect. The +# stdlib is pinned to libc++ ON PURPOSE: the hermetic clang defaults to libc++ on +# macOS but the system libstdc++ on Linux, and that per-platform mix silently +# corrupted clang-tidy's type/member analysis. Making it explicit keeps the parse +# self-consistent everywhere. compile_commands-update.sh extracts under this config. +common:clang-tidy --config=clang +common:clang-tidy --cxxopt=-stdlib=libc++ +common:clang-tidy --linkopt=-stdlib=libc++ + common:symbolizer --strip=never common:symbolizer --run_under=//tools:run_under_symbolizer diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 326188e..b02a0d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -197,8 +197,9 @@ jobs: # clang-tidy (hermetic clang-22) over the compile DB. Mirrors helly25/mbo's job and OWNS clang-tidy # in CI: it needs a compile_commands.json this job builds (the pre-commit / trunk jobs skip the # local-only hook, which has no compile DB there). Scoped to sources changed against main on a - # branch; the whole tree on main. Uses --config=clang, so it pulls the hermetic LLVM each run (not - # cached, like the sanitizer jobs); only a small disk cache of build outputs is persisted. + # branch; the whole tree on main. The compile DB is extracted in --config=clang-tidy (the develop + # config: hermetic clang + libc++), so it pulls the hermetic LLVM each run (not cached, like the + # sanitizer jobs); only a small disk cache of build outputs is persisted. # Report-only for now (continue-on-error on the lint STEP): .clang-tidy sets WarningsAsErrors '*' # and the test tree is not clang-tidy-clean yet. Building the compile DB still gates (no job-level # continue-on-error). Drop the step's continue-on-error to promote this to a hard gate once the @@ -238,15 +239,16 @@ jobs: restore-keys: | bazel-disk-clang-tidy- - name: Route bazel at the disk cache - # compile_commands-update.sh runs bazel internally (--config=clang); feed it the disk cache - # via the try-import'd .bazelrc.user. LLVM re-fetches each run (deliberately not cached). + # compile_commands-update.sh runs bazel internally (--config=clang-tidy); feed it the disk + # cache via the try-import'd .bazelrc.user. LLVM re-fetches each run (deliberately not cached). run: echo "common --disk_cache=$HOME/.cache/bazel-disk" >.bazelrc.user - name: Generate compile_commands.json - # A gitignored local artifact, so build it here. The script fetches the hermetic LLVM and - # records ITS clang, which is what makes the commands parseable by the matching clang-tidy. - # It also runs `bazel build //...` first (same default config as its aquery) to materialize - # the generated / virtual-include headers the recorded commands reference - without which - # clang-tidy would abort on `'xff/license/notice.h' file not found` on a fresh runner. + # A gitignored local artifact, so build it here. The script extracts the DB in + # --config=clang-tidy and names the hermetic clang++, which is what makes the commands + # parseable by the matching clang-tidy. It also runs `bazel build --config=clang-tidy //...` + # first (the same config as its aquery) to materialize the generated / virtual-include + # headers the recorded commands reference - without which clang-tidy would abort on + # `'xff/license/notice.h' file not found` on a fresh runner. run: ./compile_commands-update.sh - name: Summarize the compile DB run: | diff --git a/compile_commands-update.sh b/compile_commands-update.sh index 92eb4aa..94bd0ec 100755 --- a/compile_commands-update.sh +++ b/compile_commands-update.sh @@ -15,13 +15,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Generate `compile_commands.json` describing the HERMETIC clang, so that clangd -# and `tools/clang_tidy.sh` parse with the same compiler the `--config=clang` -# builds use. Note that `--config=clang` cannot do this: it only configures the -# build of the extractor tool itself, and never reaches the `aquery` the tool -# runs internally, so the recorded commands still named the autodetected local -# (Apple) clang. The fork's runtime flags below are the supported override; they -# must follow `--`, or `bazel run` hands them to bazel rather than to the tool. +# Generate `compile_commands.json` by extracting the compile commands in the exact +# configuration we develop in: `--config=clang-tidy` (see .bazelrc), which layers +# the hermetic LLVM/clang toolchain (`--config=clang`) and an explicit libc++ on +# top. Because the extractor's internal `aquery` runs in that same config, every +# recorded command carries the standard library, include paths and feature macros +# the `--config=clang` builds use - so clang-tidy / clangd parse exactly what the +# real build does, not whatever toolchain bazel would otherwise autodetect (which +# differs per platform: libc++ on macOS but the system libstdc++ on Linux, the mix +# that silently corrupted clang-tidy's type/member analysis). +# +# The `--config=clang-tidy` below is a RUNTIME arg to the extractor tool (it must +# follow `--`, or `bazel run` hands it to bazel); the tool forwards every non +# `--bcce-*` runtime arg to its internal aquery, which is how the config reaches it. set -euo pipefail @@ -54,40 +60,35 @@ function resolve_clang() { resolve_clang if [ -z "${CLANG}" ]; then # A fresh checkout (or CI runner) has not materialized the toolchain yet: it is - # only fetched once something is actually built with `--config=clang`. Build the - # smallest cc target there is to trigger that, then look again. + # only fetched once something is actually built with `--config=clang`. The header + # build below (also `--config=clang-tidy`) triggers that, so just build first. echo "Hermetic clang++ not present; fetching the toolchain via a probe build ..." 1>&2 - bazel build --config=clang //tools:show_compiler >/dev/null \ - || die "probe build '//tools:show_compiler --config=clang' failed; cannot fetch the LLVM toolchain" + bazel build --config=clang-tidy //tools:show_compiler >/dev/null \ + || die "probe build '//tools:show_compiler --config=clang-tidy' failed; cannot fetch the LLVM toolchain" resolve_clang fi - -[ -n "${CLANG}" ] || die "Cannot find the hermetic clang++ even after a '--config=clang' build" +[ -n "${CLANG}" ] || die "Cannot find the hermetic clang++ even after a '--config=clang-tidy' build" # Sources reachable both normally and through a build-machine tool are compiled # twice (target + exec configuration), and both commands would be emitted. Keep # only the target-configuration one: clang-tidy works per entry, so the exec copy # is duplicate linting for a near-identical result. Files compiled ONLY in the # exec configuration keep their command, so nothing leaves the compile DB. -declare -a BCCE_ARGS=("--bcce-compiler=${CLANG}" "--bcce-prefer-target-config") - -# Force libc++ in the compile DB so clang-tidy / clangd ALWAYS parse against the -# hermetic clang's own libc++, on every platform. Without this the recorded -# commands inherit the toolchain default, which is libc++ on macOS but the system -# libstdc++ on Linux. tools/clang_tidy.sh then prepends the hermetic libc++ -# `include/c++/v1` (so `#include ` beats helly25_mbo's plain-text `version` -# file, which its -isystem'd repo root would otherwise shadow) - injecting libc++ -# headers into a libstdc++ parse. That stdlib mix silently corrupts clang-tidy's -# type/member analysis (std::string_view resolving to int, phantom -# const-correctness / convert-to-static / member-init findings). Pinning libc++ -# here makes the parse self-consistent everywhere, matching the clean macOS run. -BCCE_ARGS+=("--bcce-copt=-stdlib=libc++") +declare -a BCCE_ARGS=("--bcce-prefer-target-config") + +# Name the hermetic clang++ BINARY (not the toolchain's `cc_wrapper.sh`, which is +# what the extracted command line starts with). clang-tidy runs its own clang to +# parse, but reads this leading argument to derive the driver's target triple and +# resource directory (its built-in headers). A shell-script "compiler" leaves it +# with the wrong builtins, so the macOS SDK / libc++ headers fail to parse. All the +# OTHER flags still come from `--config=clang-tidy`; only the compiler token is +# substituted so clang-tidy can introspect a real clang. +BCCE_ARGS+=("--bcce-compiler=${CLANG}") # The hermetic clang carries its own libc++ but no system C headers: without the -# SDK sysroot its support headers fail on `'time.h' file not found`. -# The bazel `--config=clang` toolchain supplies this itself; the extracted -# commands come from the autodetected toolchain, which relies on Apple clang's -# built-in default, so it has to be made explicit here. Linux needs no such flag. +# SDK sysroot its support headers fail on `'time.h' file not found`. The +# bazel `--config=clang` toolchain supplies this itself, but the extracted commands +# do not carry it, so make it explicit here. Linux needs no such flag. if [ "$(uname -s)" = "Darwin" ]; then SDKROOT_PATH="$(xcrun --show-sdk-path 2>/dev/null || true)" [ -n "${SDKROOT_PATH}" ] || die "'xcrun --show-sdk-path' failed; install the Xcode command line tools" @@ -97,18 +98,58 @@ fi # Materialize the generated / virtual-include headers the recorded commands will # reference (e.g. xff/license/notice.h, xff/regex/backend.h from the local # @xff_extras_api module, served through bazel-out `_virtual_includes` symlink -# forests that exist only once their cc_library is built). refresh_all records -# those include paths but does not build them, so a fresh checkout / CI runner has -# the paths but not the files and clang-tidy aborts with `'xff/.../foo.h' file not -# found`. Build them in the SAME (default) config the aquery below runs in - the -# `--bcce-*` args are extractor tool args, not bazel build flags, so they never -# change this config, which guarantees the outputs land exactly where the DB -# points. Headers are compiler-independent, so the default toolchain is correct; -# do NOT add `--config=clang` here (it would place the forests under a different, -# mismatched output dir). -echo "Building generated / virtual-include headers so the compile DB resolves ..." 1>&2 -bazel build //... >/dev/null \ - || die "'bazel build //...' failed; cannot materialize the headers the compile DB references" - -bazel run @bazel_compile_commands_extractor//:refresh_all -- "${BCCE_ARGS[@]}" +# forests that exist only once their cc_library is built). The aquery records those +# include paths but does not build them, so a fresh checkout / CI runner has the +# paths but not the files and clang-tidy aborts with `'xff/.../foo.h' file not +# found`. Build them in the SAME config the aquery runs in (`--config=clang-tidy`), +# so the forests land exactly where the DB points; a fresh checkout / CI runner also +# fetches the hermetic toolchain here. +echo "Building generated / virtual-include headers in --config=clang-tidy so the compile DB resolves ..." 1>&2 +bazel build --config=clang-tidy //... >/dev/null \ + || die "'bazel build --config=clang-tidy //...' failed; cannot materialize the headers the compile DB references" + +bazel run @bazel_compile_commands_extractor//:refresh_all -- --config=clang-tidy "${BCCE_ARGS[@]}" + +# macOS only: the `--config=clang` toolchain (toolchains_llvm) points libc++ at the Xcode SDK by +# emitting `-nostdinc++ -cxx-isystem /usr/include/c++/v1`, while `-resource-dir` is the +# hermetic clang's. clang-tidy parses with the hermetic clang++ (see --bcce-compiler), so that SDK +# libc++ against a hermetic resource dir is a mismatch that silently degrades its analysis (spurious +# unused-variable / const-correctness findings). The hermetic clang++ finds its OWN libc++ when left +# to its default search, so drop just those two flags from the DB; `-isysroot` stays for the system +# C headers. Linux never emits the SDK `-cxx-isystem`, so this is a no-op there and stays gated. +if [ "$(uname -s)" = "Darwin" ]; then + echo "macOS: dropping the SDK libc++ -cxx-isystem so the hermetic clang++ uses its own libc++ ..." 1>&2 + python3 - compile_commands.json <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, encoding="utf-8") as f: + entries = json.load(f) + + +def strip(args): + out = [] + skip = False + for i, arg in enumerate(args): + if skip: + skip = False + continue + if arg == "-nostdinc++": + continue + nxt = args[i + 1] if i + 1 < len(args) else "" + if arg == "-cxx-isystem" and nxt.endswith("/c++/v1") and "MacOSX.sdk" in nxt: + skip = True # also drop the path argument that follows + continue + out.append(arg) + return out + + +for entry in entries: + entry["arguments"] = strip(entry["arguments"]) +with open(path, "w", encoding="utf-8") as f: + json.dump(entries, f, indent=2) + f.write("\n") +PY +fi echo "OK"