Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 11 additions & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
129 changes: 85 additions & 44 deletions compile_commands-update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <version>` 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 <locale> 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 <locale> 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"
Expand All @@ -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 <SDK>/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"
Loading