diff --git a/.bazelignore b/.bazelignore index ce33f0a9..49eae431 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1 +1 @@ -cli/src/test/resources/workspaces +tests/resources/workspaces diff --git a/.bazelrc b/.bazelrc index 39610cdf..cb9c4327 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,11 +1,5 @@ run -c opt --show_loading_progress=false --show_progress=false --ui_event_filters=error run:verbose -c dbg --show_loading_progress=true --show_progress=true --ui_event_filters=info,error,debug -# https://github.com/mockito/mockito/issues/1879 -test --sandbox_tmpfs_path=/tmp - -# Use a hermetic JDK for tests. -common --java_runtime_version=remotejdk_21 - # Enable platform-specific config common --enable_platform_specific_config @@ -18,6 +12,13 @@ build:macos --host_cxxopt=-std=c++17 # Use MSVC syntax for Windows build:windows --cxxopt=/std:c++17 build:windows --host_cxxopt=/std:c++17 +# On Windows the `ring` crate's build script (pulled in by rust-s3's rustls +# backend) only finds its generated headers through the legacy external +# runfiles layout; without this it fails with "Cannot open include file: +# 'ring_core_generated/prefix_symbols.h'". `run` and `test` inherit `build`, +# `query` does not, so the nested Bazel the CLI shells out to is unaffected. +# The release and BCR-presubmit Windows builds pass the same flag explicitly. +build:windows --legacy_external_runfiles # Release artifacts: `bazel build //release:bazel-diff-rust --config=release`. # CI uses nothing but this config, so the flags a published binary is built with @@ -78,12 +79,11 @@ build --output_groups=+rustfmt_checks # coverage mode anyway, so plain `bazel test` runs are never affected. coverage --coverage_output_generator=//tools/coverage:lcov_merger -# Releases run the suite on cold runners (see release.yaml, which splits it -# into a Java job and a Rust job), where the long suites -- //tests:e2e_test -# (size = "enormous") and //cli:E2ETest (timeout = "eternal") -- have no -# headroom left: v44.0.0 died on +# Releases run the suite on cold runners (see release.yaml), where the long +# e2e suite -- //tests:e2e_test, one `moderate` target per case plus the +# `eternal` :e2e_test_all -- has no headroom left: v44.0.0 died on # "//tests:e2e_test TIMEOUT in 3600.0s" and published nothing. Per-size timeouts -# are there to catch hangs on PRs, where each suite gets its own runner and an +# are there to catch hangs on PRs, where the suite gets its own runner and an # overrun means something is wrong. A release has no such signal to give, so the # only bound left is GitHub's 6h job limit; a single value overrides all four # size categories. Not named `release`: `test` inherits `build` configs, so diff --git a/.bcr/presubmit.yml b/.bcr/presubmit.yml index 493fbda5..b58a483b 100644 --- a/.bcr/presubmit.yml +++ b/.bcr/presubmit.yml @@ -76,5 +76,4 @@ bcr_test_module: build_targets: - '//:bazel-diff-rust' test_targets: - - '//cli/...' - '//:rust_tests' \ No newline at end of file diff --git a/.claude/skills/coverage-status/SKILL.md b/.claude/skills/coverage-status/SKILL.md index 9c28fd6a..5d258d46 100644 --- a/.claude/skills/coverage-status/SKILL.md +++ b/.claude/skills/coverage-status/SKILL.md @@ -5,7 +5,7 @@ description: Use to check the current main-source line coverage of the bazel-dif # Checking coverage status -bazel-diff has a 90% main-source line-coverage gate enforced in CI ([.github/workflows/ci.yaml](../../../.github/workflows/ci.yaml)). The enforcement code lives in [tools/coverage_check.py](../../../tools/coverage_check.py); the gate runs on every `test-jre21` matrix entry as `bazelisk run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat`. +bazel-diff has a 90% main-source line-coverage gate enforced in CI ([.github/workflows/ci.yaml](../../../.github/workflows/ci.yaml)). The enforcement code lives in [tools/coverage_check.py](../../../tools/coverage_check.py); the gate runs on every `test-jre21` matrix entry (the job id is historical; it is the coverage job) as `bazelisk run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat`. ## Fastest path: just check the current state @@ -13,7 +13,7 @@ bazel-diff has a 90% main-source line-coverage gate enforced in CI ([.github/wor make coverage ``` -This runs `bazel coverage --combined_report=lcov //cli/... //tools:coverage_check_test`, then `bazel run //tools:coverage-check` against the combined report. It prints a per-file table sorted worst-first, the overall percentage, and PASS/FAIL against the threshold. Exits non-zero when below. +This runs `bazel coverage --combined_report=lcov //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/...`, then `bazel run //tools:coverage-check` against the combined report (once for the Rust sources, once scoped to `tools/go/`). It prints a per-file table sorted worst-first, the overall percentage, and PASS/FAIL against the threshold. Exits non-zero when below. If a coverage report already exists in `bazel-out/_coverage/_coverage_report.dat` (e.g. from a prior `bazel coverage` invocation) and you only want to re-check the threshold without re-running the test suite: @@ -36,27 +36,28 @@ The per-file table looks like: ``` COV% LINES (hit/total) FILE -------- ----------------- ---- - 0.00% 0 / 4 cli/src/main/kotlin/com/bazel_diff/Main.kt - 62.67% 47 / 75 cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt + 84.10% 412 / 490 src/server.rs + 93.20% 891 / 956 src/hash.rs ... - 100.00% 23 / 23 cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt + 100.00% 23 / 23 src/model.rs -Overall main-source line coverage: 90.37% (1455 / 1610) +Overall main-source line coverage: 95.37% (4120 / 4320) Threshold: 90.00% PASS ``` - **Sort order is ascending** — worst-covered files appear first, so the gap is at the top. -- **Test sources (`/test/`) are excluded** — Bazel's Kotlin instrumentation reports them too, but they don't count toward the threshold (otherwise well-covered tests would hide thin production-code coverage). -- **Files with `LF:0` are dropped** — that's an "no instrumentation data" signal (e.g. Bazel's Python coverage when no toolchain coverage tool is configured), and treating it as 0% would tank the threshold. +- **Only production sources count** — the default include list is `src/`, `tools/coverage/src/` and `tools/coverage_check.py`; `tests/` never counts, so well-covered tests cannot hide thin production-code coverage. +- **Files with `LF:0` are dropped** — that's a "no instrumentation data" signal (e.g. Bazel's Python coverage when no toolchain coverage tool is configured), and treating it as 0% would tank the threshold. - **Threshold is inclusive at the boundary** — exactly 90.00% passes; 89.99% fails. ## Looking at CI's number, not local -Local coverage may be lower than CI's because the slow `//cli:E2ETest` is often skipped or fails on dev machines (JDK-env sandbox issues). The number CI sees comes from the `test-jre21` matrix; download the `coverage-report-jre21-*` artifact from a recent run for an exact snapshot, or just read the "Overall main-source line coverage:" line in the workflow log. +The number CI sees comes from the `test-jre21` matrix; download the `coverage-report-jre21-*` artifact from a recent run for an exact snapshot, or just read the "Overall main-source line coverage:" line in the workflow log. The e2e suite (`//tests:e2e_test`) drives the binary as a subprocess and is not part of the coverage run, so a code path only an e2e case reaches shows as uncovered here. ## Configuration - **Threshold**: `COVERAGE_THRESHOLD` env var (default 90). Set in [.github/workflows/ci.yaml](../../../.github/workflows/ci.yaml) for CI, or on the command line to experiment: `COVERAGE_THRESHOLD=85 make coverage`. -- **Include prefixes**: `--include` flag or `$COVERAGE_INCLUDE` env var (default `cli/src/main/,tools/coverage_check.py`). Only files starting with one of these prefixes count. -- **Self-coverage of the checker**: `tools/coverage_check.py` is itself in the default include list, but Bazel's Python coverage emits `LF:0` for it today because the Python toolchain in `MODULE.bazel` has no `coverage_tool` configured. The 24-case py_test (`bazel test //tools:coverage_check_test`) is the actual proof the checker is exercised. +- **Include prefixes**: `--include` flag or `$COVERAGE_INCLUDE` env var (default `src/,tools/coverage/src/,tools/coverage_check.py`). Only files starting with one of these prefixes count. +- **Self-coverage of the checker**: `tools/coverage_check.py` is itself in the default include list, but Bazel's Python coverage emits `LF:0` for it today because the Python toolchain in `MODULE.bazel` has no `coverage_tool` configured. The py_test (`bazel test //tools:coverage_check_test`) is the actual proof the checker is exercised. +- **Per-target minimums**: independent of the repo-wide gate, `//src:rust_tests`, `//src:cli_tests`, `//tools/coverage:lcov_merger_test` and `//tools/go/sample:sample_test` each declare a 90% minimum enforced inside the coverage run by [tools/coverage](../../../tools/coverage/README.md). diff --git a/.claude/skills/improve-coverage/SKILL.md b/.claude/skills/improve-coverage/SKILL.md index fe51a888..45dd95cb 100644 --- a/.claude/skills/improve-coverage/SKILL.md +++ b/.claude/skills/improve-coverage/SKILL.md @@ -1,6 +1,6 @@ --- name: improve-coverage -description: Use when you need to raise main-source line coverage in the bazel-diff repo, write tests for an under-covered Kotlin file, fix a CI failure on the 90% coverage gate, or pick the highest-leverage files to test next. Triggers on requests like "the coverage gate is failing, fix it", "write tests for X", "we need more coverage", or "what should I test to get to 90%". +description: Use when you need to raise main-source line coverage in the bazel-diff repo, write tests for an under-covered Rust module, fix a CI failure on the 90% coverage gate, or pick the highest-leverage files to test next. Triggers on requests like "the coverage gate is failing, fix it", "write tests for X", "we need more coverage", or "what should I test to get to 90%". --- # Improving coverage to clear the 90% gate @@ -11,52 +11,37 @@ bazel-diff enforces a 90% main-source line-coverage gate on every PR (see [cover Run `make coverage` (or check the latest CI artifact) and look at the top of the sorted table. Prioritise files by **uncovered-lines-per-test-effort**, not by lowest percentage: -- **Highest-leverage**: small files at 0% (e.g. enum classes, value objects, single-method utilities) — one short unit test usually moves the needle without much code. -- **Highest absolute gain**: large files with moderate coverage (e.g. `BazelQueryService.kt` at 303 lines / 93%) — closing a small percentage gap covers many lines. -- **Lowest leverage**: tiny files at 50–80% where the remaining branches are error paths needing fault injection or refactors. - -A worked example: the PR that added the gate ([#356](https://github.com/Tinder/bazel-diff/pull/356)) raised coverage from 88.76% → 90.37% by adding five small files of tests — `BazelTargetTypeTest`, `VersionProviderTest`, `BazelDiffTest`, `StderrLoggerTest`, `BazelTargetTest` — for a total of 26 newly-covered lines. +- **Highest-leverage**: small, pure functions at low coverage (label normalisation, path handling, converters) — one short `#[test]` usually moves the needle without much code. +- **Highest absolute gain**: large modules with moderate coverage (`src/hash.rs`, `src/server.rs`) — closing a small percentage gap covers many lines. +- **Lowest leverage**: error paths that need fault injection (a Bazel subprocess failing mid-stream, an S3 request erroring) — reach for them only once the cheap lines are gone. ## 2. Write the test Existing tests follow a consistent shape: -- Live under `cli/src/test/kotlin/...` mirroring the main source path. -- Use **JUnit 4** (`@Test`, `@Before`, `@After`, `org.junit.Assert.assertThrows`). -- Use **assertk** for assertions (`assertk.assertThat`, with explicit imports for each assertion like `assertk.assertions.isEqualTo`). Forgetting `import assertk.assertions.contains` on a `String.contains` assertion produces a confusing receiver-mismatch error — import every assertion you use. -- Use **mockito-kotlin** when mocking is required (existing examples: `cli/src/test/kotlin/com/bazel_diff/bazel/BazelClientTest.kt`). -- Use **koin** for DI-test setup (existing pattern: `cli/src/test/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractorIssue335Test.kt`). - -Tiny files (enums, value objects, small command classes) usually only need a few targeted tests. Looking at the bytes via `assertThat(BazelTargetType.entries).hasSize(N).containsExactlyInAnyOrder(...)` is enough to cover an enum's declaration lines. - -## 3. Register the test target in cli/BUILD +- Unit tests live in a `#[cfg(test)] mod tests` at the bottom of the module they cover (`src/*.rs`). They run as `//src:rust_tests`; tests of `main.rs` itself run as `//src:cli_tests`. +- Plain `assert!`/`assert_eq!`; no assertion crate. +- Build proto inputs (`Target`, `Rule`, `Attribute`) by hand with the generated structs rather than parsing text — see the helpers already in `src/hash.rs`'s test module. +- Anything that needs a filesystem uses `tempfile::tempdir()`; nothing in a unit test shells out to Bazel. Behaviour that needs a real Bazel belongs in `tests/e2e/` (and does **not** count toward the coverage number, because the e2e suite runs the binary as a subprocess). -Every test needs its own `kt_jvm_test` entry: - -```python -kt_jvm_test( - name = "BazelTargetTypeTest", - test_class = "com.bazel_diff.bazel.BazelTargetTypeTest", - runtime_deps = [":cli-test-lib"], -) -``` +## 3. No BUILD edit needed -The `:cli-test-lib` glob picks up the new test source automatically; the explicit `kt_jvm_test` rule is what makes it executable via `bazel test //cli:`. +`//src:bazel_diff_lib` globs `src/**/*.rs`, so a new `#[test]` is picked up by `//src:rust_tests` automatically. A new e2e case needs `make regen-e2e` instead (see [tools/e2e/README.md](../../../tools/e2e/README.md)). ## 4. Verify locally before pushing ```bash -bazel test //cli: # one-off run of the new test -make coverage # full gate +bazel test //src:rust_tests --test_arg= # one-off run of the new test +make coverage # full gate ``` -The local number may be lower than CI's because `//cli:E2ETest` often fails or is excluded on dev machines (JDK-env sandbox issues). If you've added tests for a file that's also exercised by E2E (e.g. `BazelQueryService.kt`), the CI delta will be smaller than the local delta — count only the lines that weren't already covered by E2E. +`bazel coverage` also enforces per-target minimums (90% on `//src:rust_tests` scoped to `src/`), so a target can fail the coverage run even when the repo-wide number passes; the test log carries the per-file breakdown. ## 5. Things that don't work / aren't worth attempting -- **`Main.kt`** — calls `exitProcess(...)` which kills the JVM; can't be tested in-process without a SecurityManager hack or refactor. Stays at 0%; the threshold tolerates it. -- **`throw IllegalArgumentException(...)` branches in resource-loading code** like `VersionProvider.kt` — the production code resolves the classloader from `this::class.java`, with no injection seam to swap it for one missing the resource. Refactor or skip. -- **`else -> BazelTargetType.UNKNOWN` branches** — only reachable when Bazel's `Build.Target.Discriminator` adds a new enum value the production code doesn't recognise. Triggering today would require a hand-forged proto with a reserved discriminator number, which the protobuf builder rejects. +- **`fn main()` in `src/main.rs`** — exits the process; the argument parsing it delegates to is what `//src:cli_tests` covers. Stays uncovered; the threshold tolerates it. +- **Bazel/git subprocess failure branches** in `src/bazel.rs` and `src/server.rs` — need a real failing subprocess. Cover them from an e2e case if the behaviour matters; don't chase them for the number. +- **Match arms for proto enum values Bazel does not emit** — only reachable with a hand-forged discriminator. Skip. ## When the gate fails on a flake, not on a coverage drop diff --git a/.gitattributes b/.gitattributes index b3d263ff..aecae0e1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # Test fixture workspaces are deliberately constructed inputs to the E2E tests # (e.g. module_bazel_comment exercises comment preservation, cquery_failing_target # encodes a specific failure scenario). Exclude them from aspect_rules_lint -# formatters (buildifier/ktfmt) so the cron jobs never rewrite the very content -# under test. `rules-lint-ignored` is honored by //cli/format and //cli/format:buildifier. -cli/src/test/resources/** rules-lint-ignored +# formatters (buildifier) so the cron jobs never rewrite the very content +# under test. `rules-lint-ignored` is honored by //tools/format:buildifier. +tests/resources/** rules-lint-ignored diff --git a/.github/workflows/buildifier.yaml b/.github/workflows/buildifier.yaml index a36e1e03..9e390c55 100644 --- a/.github/workflows/buildifier.yaml +++ b/.github/workflows/buildifier.yaml @@ -1,7 +1,7 @@ name: Buildifier # Runs buildifier over all Starlark/BUILD files on a daily schedule and opens a -# PR only if the formatter produced changes. Mirrors the ktfmt format cron: it +# PR only if the formatter produced changes. Mirrors the rustfmt format cron: it # force-pushes a dedicated, bot-owned branch so re-runs reuse one PR instead of # accumulating stale branches, and it never touches master directly. # @@ -10,7 +10,7 @@ name: Buildifier # generated PR limited to formatting-only changes. on: schedule: - # 12:30 UTC daily (between the ktfmt format and coverage-badge crons). + # 12:30 UTC daily (between the rustfmt format and coverage-badge crons). - cron: '30 12 * * *' workflow_dispatch: @@ -21,11 +21,6 @@ jobs: contents: write pull-requests: write steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -35,7 +30,7 @@ jobs: run: go install github.com/bazelbuild/bazelisk@latest && export PATH=$PATH:$(go env GOPATH)/bin - uses: actions/checkout@v4 - name: Run buildifier - run: ~/go/bin/bazelisk run //cli/format:buildifier --enable_bzlmod=true --enable_workspace=false --lockfile_mode=off + run: ~/go/bin/bazelisk run //tools/format:buildifier --enable_bzlmod=true --enable_workspace=false --lockfile_mode=off - name: Open PR if formatting changed env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -60,5 +55,5 @@ jobs: --base master \ --head "$BRANCH" \ --title "ci: apply buildifier formatting" \ - --body "Automated buildifier run via \`bazel run //cli/format:buildifier\`. This PR contains formatting-only changes to BUILD/Starlark files." + --body "Automated buildifier run via \`bazel run //tools/format:buildifier\`. This PR contains formatting-only changes to BUILD/Starlark files." fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 627202d5..86d51b6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,12 +6,18 @@ on: pull_request: branches: [ master ] +# Job ids and display names are load-bearing: branch protection on master +# requires them by name (`Rust candidate (Bazel 8.x)`, `test-jre21 (...)`, +# `test-jre11-run-example (...)`, `release-artifacts (...)`). Several predate +# the Kotlin implementation's removal and no longer describe what they run; +# rename them only together with the branch-protection rule. jobs: # Every Rust check here runs through Bazel, with no cargo and no host Rust # toolchain: rules_rust downloads the compiler pinned in MODULE.bazel, so CI # and a local `bazel test` agree on the exact rustc. //:rust_clippy_check and # //:rust_format_check below stand in for `cargo clippy -- -D warnings` and - # `cargo fmt --all -- --check`. + # `cargo fmt --all -- --check`. //tools/... is the Python/Go release, e2e-split + # and coverage tooling. rust-candidate: name: Rust candidate (Bazel ${{ matrix.bazel }}) runs-on: ubuntu-latest @@ -37,12 +43,15 @@ jobs: env: USE_BAZEL_VERSION: ${{ matrix.bazel }} CARGO_BAZEL_ISOLATED: 'false' - run: ~/go/bin/bazelisk test //:rust_tests //:rust_clippy_check //:rust_format_check //tools:benchmark_test //tools:perf_gate_test --enable_bzlmod=true --enable_workspace=false + run: ~/go/bin/bazelisk test //:rust_tests //:rust_clippy_check //:rust_format_check //tools/... --enable_bzlmod=true --enable_workspace=false rust-candidate-e2e: name: Rust candidate E2E runs-on: ubuntu-latest steps: + # bazel-diff itself needs no JDK, but the suite drives a nested Bazel over + # fixture workspaces with Java targets, and Bazel's default + # --java_runtime_version=local_jdk wants a host JDK for those. - name: Setup Java JDK uses: actions/setup-java@v4 with: @@ -75,9 +84,9 @@ jobs: # The e2e suites are split into one Bazel test target per case by # //tools/e2e:regen, whose output is checked in. This job is the gate that - # keeps the checked-in split honest: :regen_check re-derives the case lists - # from the Kotlin and Rust sources and fails when they differ, so an e2e test - # added, renamed or removed without `make regen-e2e` cannot merge. + # keeps the checked-in split honest: :regen_check re-derives the case list + # from the sources and fails when they differ, so an e2e test added, renamed + # or removed without `make regen-e2e` cannot merge. # :split_e2e_tests_test covers the generator itself. e2e-split-regen: name: E2E split regen @@ -139,6 +148,9 @@ jobs: --output_groups=-clippy_checks,-rustfmt_checks \ --enable_bzlmod=true --enable_workspace=false + # The coverage gate. The job id is historical (it used to run the JVM suites + # on a JDK 21) and is kept because branch protection requires the + # `test-jre21 (, )` checks by that name. test-jre21: runs-on: ${{ matrix.os }} strategy: @@ -154,11 +166,6 @@ jobs: - os: macos-latest bazel: '9.x' steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -174,37 +181,17 @@ jobs: done export PATH=$PATH:$(go env GOPATH)/bin - uses: actions/checkout@v4 - - name: Free disk space on Ubuntu - if: runner.os == 'Linux' - run: | - # //cli:E2ETest plus coverage can exhaust the default ubuntu-latest image. - # Reclaim space from preinstalled toolchains this job does not use. - # Keep /usr/local/lib/android: rules_android resolves ANDROID_HOME there. - sudo rm -rf /usr/share/dotnet /opt/ghc - sudo docker image prune --all --force || true - # /mnt has more headroom than / on hosted runners; keep Bazel outputs off root. - if [ -d /mnt ]; then - sudo mkdir -p /mnt/bazel-output - sudo chown "$USER" /mnt/bazel-output - echo "BAZEL_OUTPUT_USER_ROOT=/mnt/bazel-output" >> "$GITHUB_ENV" - fi - df -h / /mnt 2>/dev/null || df -h / - name: Build license target env: USE_BAZEL_VERSION: ${{ matrix.bazel }} run: ~/go/bin/bazelisk build //:license --enable_bzlmod=true --enable_workspace=false - - name: Run bazel-diff tests + - name: Run bazel-diff tests with coverage env: USE_BAZEL_VERSION: ${{ matrix.bazel }} - # Retries guard against flaky E2E tests that hit the network (e.g. bazel mod - # show_repo / BCR calls) failing the whole job on a transient blip. + # Retries guard against transient toolchain/crate fetch errors failing the whole job. run: | - BAZEL_STARTUP=() - if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then - BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") - fi for i in 1 2 3; do - ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" coverage --combined_report=lcov //cli/... //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/... --enable_bzlmod=true --enable_workspace=false && break + ~/go/bin/bazelisk coverage --combined_report=lcov //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/... --enable_bzlmod=true --enable_workspace=false && break if [ "$i" = 3 ]; then exit 1; fi echo "coverage run failed (attempt $i); retrying in 15s..." sleep 15 @@ -218,32 +205,21 @@ jobs: if-no-files-found: warn - name: Enforce coverage threshold (>= 90% main-source line coverage) env: - # Must match the `Run bazel-diff tests` step above, otherwise bazelisk - # downloads the default bazel from .bazelversion and starts a fresh - # server whose output base loses track of the coverage report symlink - # (seen flaking on macos-latest x bazel 9.x). + # Must match the coverage step above, otherwise bazelisk downloads the + # default bazel from .bazelversion and starts a fresh server whose + # output base loses track of the coverage report symlink (seen flaking + # on macos-latest x bazel 9.x). USE_BAZEL_VERSION: ${{ matrix.bazel }} COVERAGE_THRESHOLD: '90' - run: | - BAZEL_STARTUP=() - if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then - BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") - fi - ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" run //tools:coverage-check -- --badge-json coverage.json bazel-out/_coverage/_coverage_report.dat + run: ~/go/bin/bazelisk run //tools:coverage-check -- --badge-json coverage.json bazel-out/_coverage/_coverage_report.dat - name: Enforce Go coverage threshold (>= 90% line coverage under tools/go/) env: - # Must match the `Run bazel-diff tests` step above; see the note on the - # Kotlin enforcement step for why USE_BAZEL_VERSION has to be pinned here. + # Must match the coverage step above; see the note on the previous step. USE_BAZEL_VERSION: ${{ matrix.bazel }} - # Gate Go independently of the Kotlin overall so thin Go coverage can't hide - # behind well-covered Kotlin (and vice versa). --include scopes the report to + # Gate Go independently of the Rust overall so thin Go coverage can't hide + # behind well-covered Rust (and vice versa). --include scopes the report to # Go production source only (tools/go/, which excludes tools/coverage_check.py). - run: | - BAZEL_STARTUP=() - if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then - BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") - fi - ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat + run: ~/go/bin/bazelisk run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat - name: Upload test logs uses: actions/upload-artifact@v4 if: always() @@ -251,6 +227,10 @@ jobs: name: test-logs-jre21-${{ matrix.os }}-bazel-${{ matrix.bazel }} path: bazel-testlogs/ if-no-files-found: warn + # Runs the example script end to end against this repository, on every Bazel + # version and workspace mode the tool supports. The job id is historical (the + # JVM CLI was exercised on a JDK 11 here) and is kept because branch protection + # requires the `test-jre11-run-example (...)` checks by that name. test-jre11-run-example: runs-on: ${{ matrix.os }} strategy: @@ -296,12 +276,6 @@ jobs: disable_workspace: 'true' extra_flags: '' steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '11' - id: java - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -340,7 +314,7 @@ jobs: BAZEL_DIFF_FORCE_CHECKOUT: true BAZEL_EXTRA_COMMAND_OPTIONS: ${{ matrix.extra_flags }} # Retries guard against transient release-asset fetch errors (e.g. a 500 from - # GitHub Releases downloading rules_kotlin/other Bazel module deps). + # GitHub Releases downloading Bazel module deps). run: | for i in 1 2 3; do ./bazel-diff-example.sh "$GITHUB_WORKSPACE" ~/go/bin/bazelisk $(git rev-parse HEAD~1) $(git rev-parse HEAD) && break @@ -357,8 +331,16 @@ jobs: BAZEL_EXTRA_COMMAND_OPTIONS: ${{ matrix.extra_flags }} shell: pwsh # Retries guard against transient release-asset fetch errors (e.g. a 500 from - # GitHub Releases downloading rules_kotlin/other Bazel module deps). + # GitHub Releases downloading Bazel module deps). + # + # The system bazelrc shortens the output root for every Bazel the example + # script runs (the outer build and the nested queries alike): MSVC's link.exe + # is MAX_PATH-bound (260), and under the default root the Rust stdlib rlib + # path overflows it with LNK1181. Same fix as bazel_startup_flags in the + # release-artifacts job below; a startup option cannot live in a --config. run: | + New-Item -ItemType Directory -Force -Path "$env:ProgramData" | Out-Null + Add-Content -Path "$env:ProgramData\bazel.bazelrc" -Value "startup --output_user_root=C:/b" $prevRev = git rev-parse HEAD~1 $currRev = git rev-parse HEAD for ($i = 1; $i -le 3; $i++) { @@ -370,7 +352,7 @@ jobs: } # Verifies per-PR that everything a release ships still builds, on every # platform it ships for. It publishes nothing: release.yaml owns that, via - # release_prep.sh (JAR + source archive) and its rust-binaries matrix. This + # release_prep.sh (source archive) and its rust-binaries matrix. This # job predates that flow -- it used to *be* the deploy -- so its uploads are # a build check whose output happens to be downloadable, nothing more. release-artifacts: @@ -410,33 +392,24 @@ jobs: # config. include: - os: ubuntu-latest - java: '11' rust_asset: bazel-diff-rust-linux-amd64 release_config: release-musl bazel_startup_flags: '' bazel_extra_flags: '' - upload_jar_and_archive: true + upload_archive: true - os: macos-latest - java: '11' rust_asset: bazel-diff-rust-macos-arm64 release_config: release bazel_startup_flags: '' bazel_extra_flags: '' - upload_jar_and_archive: false + upload_archive: false - os: windows-latest - java: '11' rust_asset: bazel-diff-rust-windows-amd64.exe release_config: release bazel_startup_flags: '--output_user_root=C:/b' bazel_extra_flags: '--legacy_external_runfiles' - upload_jar_and_archive: false + upload_archive: false steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: ${{ matrix.java }} - id: java - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -464,24 +437,15 @@ jobs: } echo "$(go env GOPATH)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - uses: actions/checkout@v4 - - name: Build deployable JAR - if: matrix.upload_jar_and_archive - run: bazelisk build //cli:bazel-diff_deploy.jar - - uses: actions/upload-artifact@v4 - if: matrix.upload_jar_and_archive - with: - name: bazel-diff_deploy.jar - path: bazel-bin/cli/bazel-diff_deploy.jar - if-no-files-found: error # The same script release_prep.sh packs the shipped archive with, so this # check can't pass on an archive the release would not produce. It used to # call `make release_source_archive`, a second tar recipe that had already # drifted (no __pycache__/*.pyc excludes). - name: Build release source archive - if: matrix.upload_jar_and_archive + if: matrix.upload_archive run: .github/workflows/pack_release_archive.sh archives/release.tar.gz - uses: actions/upload-artifact@v4 - if: matrix.upload_jar_and_archive + if: matrix.upload_archive with: name: release.tar.gz path: archives/release.tar.gz diff --git a/.github/workflows/coverage_badge.yaml b/.github/workflows/coverage_badge.yaml index f0cf5262..83eac14c 100644 --- a/.github/workflows/coverage_badge.yaml +++ b/.github/workflows/coverage_badge.yaml @@ -16,11 +16,6 @@ jobs: contents: write pull-requests: write steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -39,11 +34,10 @@ jobs: - name: Run bazel-diff tests with coverage env: USE_BAZEL_VERSION: '9.x' - # Retries guard against flaky E2E tests that hit the network (e.g. bazel mod - # show_repo / BCR calls in the go_mod_change fixture) failing the whole daily cron. + # Retries guard against transient toolchain/crate fetch errors failing the daily cron. run: | for i in 1 2 3; do - ~/go/bin/bazelisk coverage --combined_report=lcov //cli/... //src:cli_tests //src:rust_tests //tools:coverage_check_test --enable_bzlmod=true --enable_workspace=false && break + ~/go/bin/bazelisk coverage --combined_report=lcov //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... --enable_bzlmod=true --enable_workspace=false && break if [ "$i" = 3 ]; then exit 1; fi echo "coverage run failed (attempt $i); retrying in 15s..." sleep 15 diff --git a/.github/workflows/firecracker-e2e.yml b/.github/workflows/firecracker-e2e.yml index b1afb4be..b52c1a46 100644 --- a/.github/workflows/firecracker-e2e.yml +++ b/.github/workflows/firecracker-e2e.yml @@ -82,11 +82,14 @@ jobs: sudo chmod 666 /dev/kvm ls -l /dev/kvm - - name: Build bazel-diff (deploy jar + launcher) + # Two builds of the same crate: the host-native binary drives the local + # (cold) leg, and the static musl amd64 asset is baked into the guest + # rootfs, where it needs no libc. + - name: Build bazel-diff (host binary + static guest binary) run: | - ~/go/bin/bazelisk build //cli:bazel-diff_deploy.jar + ~/go/bin/bazelisk build //release:bazel-diff-rust --config=release-musl ~/go/bin/bazelisk run //:bazel-diff --script_path=/tmp/bazel_diff - ls -l bazel-bin/cli/bazel-diff_deploy.jar /tmp/bazel_diff + ls -l bazel-bin/release/bazel-diff-rust-linux-amd64 /tmp/bazel_diff - name: Build + unit-test the orchestrator working-directory: tools/firecracker @@ -131,7 +134,7 @@ jobs: run: | sudo -E env \ ARCH="$ARCH" KERNEL_VER="$KERNEL_VER" OUT="$IMG" SIZE_MB="$SIZE_MB" \ - BAZEL_DIFF_JAR="$PWD/bazel-bin/cli/bazel-diff_deploy.jar" \ + BAZEL_DIFF_BIN="$(readlink -f bazel-bin/release/bazel-diff-rust-linux-amd64)" \ BAZEL_BIN="${{ steps.bazel.outputs.bin }}" \ WORKSPACE_SRC="$WS" SSH_PUBKEY=/tmp/fc_guest.pub \ tools/firecracker/bench/build_guest_image.sh diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index 6ce9a510..f6050746 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -1,7 +1,7 @@ name: Format -# Runs ktfmt and rustfmt over the codebase on a daily schedule and opens a PR -# only if a formatter produced changes. Mirrors the coverage-badge cron: it +# Runs rustfmt over the codebase on a daily schedule and opens a PR only if +# the formatter produced changes. Mirrors the coverage-badge cron: it # force-pushes a dedicated, bot-owned branch so re-runs reuse one PR instead of # accumulating stale branches, and it never touches master directly. on: @@ -17,11 +17,6 @@ jobs: contents: write pull-requests: write steps: - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - name: Setup Go environment uses: actions/setup-go@v5 with: @@ -37,13 +32,11 @@ jobs: done export PATH=$PATH:$(go env GOPATH)/bin - uses: actions/checkout@v4 - - name: Run ktfmt + - name: Run rustfmt # Pinned to the repo's .bazelversion (no USE_BAZEL_VERSION) and run with # --lockfile_mode=off so MODULE.bazel.lock is never rewritten, keeping the # generated PR limited to formatting-only changes. - run: ~/go/bin/bazelisk run //cli/format --enable_bzlmod=true --enable_workspace=false --lockfile_mode=off - - name: Run rustfmt - run: ~/go/bin/bazelisk run //cli/format:rustfmt --enable_bzlmod=true --enable_workspace=false --lockfile_mode=off + run: ~/go/bin/bazelisk run //tools/format:rustfmt --enable_bzlmod=true --enable_workspace=false --lockfile_mode=off - name: Open PR if formatting changed env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -57,7 +50,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -B "$BRANCH" git add -A - git commit -m "ci: apply ktfmt and rustfmt formatting" + git commit -m "ci: apply rustfmt formatting" # Force-push the dedicated, bot-owned branch so re-runs reuse one PR # instead of accumulating stale branches. This never touches master. git push --force origin "$BRANCH" @@ -67,6 +60,6 @@ jobs: gh pr create \ --base master \ --head "$BRANCH" \ - --title "ci: apply ktfmt and rustfmt formatting" \ - --body "Automated ktfmt and rustfmt run via \`bazel run //cli/format\` and \`bazel run //cli/format:rustfmt\`. This PR contains formatting-only changes." + --title "ci: apply rustfmt formatting" \ + --body "Automated rustfmt run via \`bazel run //tools/format:rustfmt\`. This PR contains formatting-only changes." fi diff --git a/.github/workflows/perf-gate.yaml b/.github/workflows/perf-gate.yaml deleted file mode 100644 index 9f246a9f..00000000 --- a/.github/workflows/perf-gate.yaml +++ /dev/null @@ -1,123 +0,0 @@ -name: Performance Gate - -# Fails the build when the Rust implementation is not faster than the Kotlin one. -# -# tools/perf_gate.py generates its own workloads (a synthetic `streamed_proto` fixture, the -# workspace it describes, and hash-file pairs), so this job needs no checkout of another -# repository, no Bazel server and no Hyperfine -- only the two bazel-diff binaries. Both are -# built `-c opt`, because comparing a debug build against an optimized one measures the build -# flags rather than the code. -# -# Runs on every pull request: "Rust is faster" is a property of the code, and a regression is -# cheapest to catch in the change that caused it. The gate pairs its measurements (both binaries -# every round, alternating which goes first) and takes medians over several rounds rather than -# trusting a single timing. -# -# Two legs (see the matrix below): -# * glibc -- the host-native dynamic build. Stable on any runner, so it is the blocking gate -# that proves the Rust logic is faster than Kotlin. -# * musl -- the statically-linked binary actually published for Linux. musl's single-arena -# malloc would serialize the parallel proto decode, so this leg guards allocator scalability: -# the release links mimalloc as its #[global_allocator], and this leg keeps it that way. It -# only reproduces with enough cores (>= 8; the free 2-vCPU runner cannot surface it), so it -# runs on a larger runner over the allocator-sensitive workload. -# See docs/kotlin-rust-perf-gate.md for the protocol and for what to do when this job fails. -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - workflow_dispatch: - -jobs: - perf-gate: - name: ${{ matrix.title }} - runs-on: ${{ matrix.runner }} - continue-on-error: ${{ matrix.continue_on_error }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - include: - - title: Rust (glibc) vs Kotlin performance gate - libc: glibc - runner: ubuntu-latest - rust_build: -c opt //src:bazel-diff - rust_binary: bazel-bin/src/bazel-diff - gate_workloads: "" - continue_on_error: false - # `ubuntu-latest-8-cores` must map to an org-provisioned larger runner (or a - # self-hosted one) with at least 8 vCPUs -- the allocator storm does not appear - # below that, so a 2-vCPU runner here would be a false green. Rename to match the - # label your org exposes. - - title: Rust (musl) allocator-scalability gate - libc: musl - runner: ubuntu-latest-8-cores - rust_build: --config=release-musl //release:bazel-diff-rust - rust_binary: bazel-bin/release/bazel-diff-rust-linux-amd64 - gate_workloads: --workload generate-hashes-dense - continue_on_error: false - steps: - # No host Rust toolchain: the Rust binary is built by Bazel, which brings - # its own rustc pinned in MODULE.bazel. - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - name: Setup Go environment - uses: actions/setup-go@v5 - with: - go-version: ^1.17 - - name: Setup Bazelisk - run: go install github.com/bazelbuild/bazelisk@latest - - uses: actions/checkout@v4 - - name: Unit-test the gate itself - # Implementation-agnostic, so run it once on the cheap runner only. - if: matrix.libc == 'glibc' - env: - USE_BAZEL_VERSION: '8.x' - CARGO_BAZEL_ISOLATED: 'false' - run: ~/go/bin/bazelisk test //tools:perf_gate_test - - name: Build Kotlin and Rust binaries - env: - USE_BAZEL_VERSION: '8.x' - CARGO_BAZEL_ISOLATED: 'false' - # Kotlin is always the host build; only the Rust target/config varies per leg. - run: | - ~/go/bin/bazelisk build -c opt //cli:bazel-diff - ~/go/bin/bazelisk build ${{ matrix.rust_build }} - - name: Run the performance gate - env: - USE_BAZEL_VERSION: '8.x' - CARGO_BAZEL_ISOLATED: 'false' - # Invoked directly rather than through `bazel run` so the JSON report lands in the - # workspace instead of the runfiles tree, and so Bazel is not holding the output-base - # lock (or competing for cores) while timings are taken. - run: | - python3 tools/perf_gate.py \ - --kotlin-binary bazel-bin/cli/bazel-diff \ - --rust-binary ${{ matrix.rust_binary }} \ - ${{ matrix.gate_workloads }} \ - --rounds 7 \ - --warmup-rounds 2 \ - --json perf-gate-${{ matrix.libc }}.json \ - | tee perf-gate-${{ matrix.libc }}.txt - - name: Publish summary - if: always() - run: | - { - echo '## ${{ matrix.title }}' - echo '```' - cat perf-gate-${{ matrix.libc }}.txt || echo 'the gate produced no report' - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload report - if: always() - uses: actions/upload-artifact@v4 - with: - name: perf-gate-report-${{ matrix.libc }} - path: | - perf-gate-${{ matrix.libc }}.json - perf-gate-${{ matrix.libc }}.txt - if-no-files-found: ignore diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dacbfe35..95f3e9ea 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,17 +2,14 @@ # Uses bazel-contrib release ruleset: build, attest, and publish to GitHub Releases. # A separate matrix job attaches host-native Rust CLI binaries to the draft release. # -# The test suites gate the release from two independent jobs -- `java-tests` and -# `rust-tests` -- rather than from one `bazel test //...` inside the reusable -# workflow's build job. They run concurrently on their own runners, so a Kotlin -# failure no longer cancels the Rust suite's results (or vice versa): a release -# candidate gets both verdicts from one run instead of one verdict at a time. -# It also splits the two long suites -- //cli:E2ETest and //tests:e2e_test -- -# across two cold runners, each with its own 6h GitHub job limit. +# The test suites gate the release from the `rust-tests` job rather than from +# one `bazel test //...` inside the reusable workflow's build job: the long e2e +# suite (//tests:e2e_test) gets a cold runner to itself, with its own 6h GitHub +# job limit, and the reusable workflow's build job only runs the fast tooling +# tests. # -# Between them the three test commands cover every test in //...; keep it that +# Between them the two test commands cover every test in //...; keep it that # way when adding a package: -# java-tests //cli/... (Kotlin/Java) # rust-tests //src/..., //tests/..., //tools/coverage/..., the Rust lint # gates //:rust_clippy_check and //:rust_format_check # release //tools:all, //tools/e2e/..., //tools/go/... @@ -33,47 +30,16 @@ permissions: attestations: write contents: write jobs: - # The Kotlin/Java half of the release gate. //cli/... is the whole JVM CLI, - # including //cli:E2ETest (timeout = "eternal"), which is why this suite gets - # a runner to itself. Java is set up explicitly even though .bazelrc pins a - # hermetic --java_runtime_version for tests, so this job matches what the - # `test-jre21` job in ci.yaml already runs these suites under. - java-tests: - name: Java tests - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ inputs.tag_name || github.ref_name }} - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" - - name: Setup Go environment - uses: actions/setup-go@v5 - with: - go-version: ^1.17 - - name: Setup Bazelisk - run: | - go install github.com/bazelbuild/bazelisk@latest - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - # No USE_BAZEL_VERSION: bazelisk takes the version from .bazelversion, - # which is the one a release is cut with. --config=no-test-timeouts is in - # .bazelrc; it is what keeps the long suites from timing out on a cold - # release runner (see the comment on that config). - - name: Run Kotlin/Java tests - run: bazelisk test //cli/... --config=no-test-timeouts - # The Rust half: the three Rust packages plus the two lint gates, which live - # in the root package because they pin their own roots (see //BUILD). The + # The three Rust packages plus the two lint gates, which live in the root + # package because they pin their own roots (see //BUILD). The # packages are named recursively rather than through the //:rust_tests suite # ci.yaml uses, so a Rust test added to //src or //tests is in the release # gate whether or not it also gets added to that hand-maintained suite. # # //tests:e2e_test spawns a nested Bazel per fixture workspace, so this job - # needs the Java and $BAZEL setup that `rust-candidate-e2e` in ci.yaml uses -- - # without $BAZEL the suite shells out to the outer Bazel's own binary. + # needs the Java (fixtures with Java targets) and $BAZEL setup that + # `rust-candidate-e2e` in ci.yaml uses -- without $BAZEL the suite shells out + # to the outer Bazel's own binary. # USE_BAZEL_VERSION keeps those nested Bazelisk processes on the same exact # release version the outer invocation reads from .bazelversion. rust-tests: @@ -117,17 +83,15 @@ jobs: path: bazel-testlogs/ if-no-files-found: warn release: - # Both suites gate the release, but neither gates the other: they run - # concurrently and the reusable workflow starts only once both are green. - needs: [java-tests, rust-tests] + needs: [rust-tests] uses: bazel-contrib/.github/.github/workflows/release_ruleset.yaml@v7.4.0 with: release_files: archives/*.* # Replaces the reusable workflow's default (`bazel test //...`), which is - # the only way to change what its build job runs. The Kotlin and Rust - # suites moved to the two jobs above; what is left is the Python and Go - # release tooling, which is fast enough to keep here rather than spend a - # third runner on. --config=no-test-timeouts is in .bazelrc. + # the only way to change what its build job runs. The Rust suites moved + # to the job above; what is left is the Python and Go release tooling, + # which is fast enough to keep here rather than spend another runner on. + # --config=no-test-timeouts is in .bazelrc. bazel_test_command: "bazel test //tools:all //tools/e2e/... //tools/go/... --config=no-test-timeouts" prerelease: false draft: true @@ -188,11 +152,6 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ inputs.tag_name || github.ref_name }} - - name: Setup Java JDK - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "21" - name: Setup Go environment uses: actions/setup-go@v5 with: diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh index 23563b90..6f6c82db 100755 --- a/.github/workflows/release_prep.sh +++ b/.github/workflows/release_prep.sh @@ -8,10 +8,6 @@ TAG=$1 "$(dirname "$0")/pack_release_archive.sh" archives/release.tar.gz -make release_deploy_jar &> /dev/null - -cp bazel-bin/cli/bazel-diff_deploy.jar archives/bazel-diff_deploy.jar - SHA=$(shasum -a 256 archives/release.tar.gz | awk '{print $1}') cat << EOF @@ -23,21 +19,14 @@ Add to your \`MODULE.bazel\` file: bazel_dep(name = "bazel-diff", version = "${TAG#v}") \`\`\` -## Using WORKSPACE +Then run the tool with \`bazel run @bazel-diff//:bazel-diff\`. -\`\`\`starlark -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -http_archive( - name = "bazel-diff", - sha256 = "${SHA}", - strip_prefix = "", - url = "https://github.com/Tinder/bazel-diff/releases/download/${TAG}/release.tar.gz", -) -\`\`\` +Source archive SHA-256: \`${SHA}\` -## Experimental Rust binary +## Prebuilt binaries -Prebuilt host-native CLIs (attached by the release workflow): +Statically linked (Linux) and host-native (macOS, Windows) CLIs, attached by the +release workflow: - Linux amd64: https://github.com/Tinder/bazel-diff/releases/download/${TAG}/bazel-diff-rust-linux-amd64 - Linux arm64: https://github.com/Tinder/bazel-diff/releases/download/${TAG}/bazel-diff-rust-linux-arm64 diff --git a/.github/workflows/serve-harness.yml b/.github/workflows/serve-harness.yml index fa8afd57..5c0ee10f 100644 --- a/.github/workflows/serve-harness.yml +++ b/.github/workflows/serve-harness.yml @@ -1,8 +1,8 @@ name: Serve Harness # Integration validation for the `bazel-diff serve` query service. Runs tools/serve_harness.py, -# which builds //cli:bazel-diff and drives the real binary over HTTP against a live git:// remote -# (git daemon) across full/shallow/partial clones -- coverage the in-process E2ETest cannot reach. +# which builds //src:bazel-diff and drives the real binary over HTTP against a live git:// remote +# (git daemon) across full/shallow/partial clones -- coverage the e2e suite cannot reach. # See the harness module docstring for the scenario matrix. # Cron-only for now while we confirm the harness is stable on CI runners. Once it has a healthy diff --git a/.github/workflows/serve-stress-alpine-real.yml b/.github/workflows/serve-stress-alpine-real.yml index 0984e2f8..d8e011f2 100644 --- a/.github/workflows/serve-stress-alpine-real.yml +++ b/.github/workflows/serve-stress-alpine-real.yml @@ -70,12 +70,14 @@ jobs: - name: Setup Bazelisk run: go install github.com/bazelbuild/bazelisk@latest - uses: actions/checkout@v4 - - name: Build bazel-diff deploy jar (glibc host) - run: ~/go/bin/bazelisk build //cli:bazel-diff_deploy.jar + # The same statically linked musl asset the release publishes for Linux amd64, so the + # container needs no JDK or libc for bazel-diff itself. + - name: Build bazel-diff (static musl binary, cross-built on the glibc host) + run: ~/go/bin/bazelisk build //release:bazel-diff-rust --config=release-musl - name: Stage container inputs run: | mkdir -p "$STAGE/tools" - cp bazel-bin/cli/bazel-diff_deploy.jar "$STAGE/bazel-diff.jar" + cp -L bazel-bin/release/bazel-diff-rust-linux-amd64 "$STAGE/bazel-diff" cp tools/serve_stress.py tools/serve_harness.py tools/serve_stress_alpine.sh "$STAGE/tools/" cp .bazelversion "$STAGE/" FALLBACK_BV=$(tr -d '[:space:]' < .bazelversion) diff --git a/.github/workflows/serve-stress-alpine.yml b/.github/workflows/serve-stress-alpine.yml index f891921c..83cfb8c2 100644 --- a/.github/workflows/serve-stress-alpine.yml +++ b/.github/workflows/serve-stress-alpine.yml @@ -4,18 +4,19 @@ name: Serve Stress (Alpine 8 GiB) # harness as serve-stress.yml (tools/serve_stress.py -- concurrent cold/hot load, error mix, # index.lock heal/storm, remote outage, lame-duck), but run inside an x86_64 Alpine (musl) # container hard-capped at 8 GiB RAM and a small CPU quota. The goal is to see where serve -# chokes when memory and cores are scarce -- JVM heap pressure (container-aware default heap is -# ~2 GiB at this cap), `bazel query` server cost, git checkout latency under CPU starvation, -# and OOM-kill behavior -- signals the 4-core/16 GiB glibc runners of the other stress crons -# never produce. Alongside the harness's metrics/summary, the run emits a footprint JSON +# chokes when memory and cores are scarce -- serve's own memory footprint, `bazel query` server +# cost (the Bazel server JVM is where the heap pressure lives), git checkout latency under CPU +# starvation, and OOM-kill behavior -- signals the 4-core/16 GiB glibc runners of the other +# stress crons never produce. Alongside the harness's metrics/summary, the run emits a footprint JSON # (cgroup memory.peak against the cap + oom/oom_kill counts) as a 90-day artifact so the # low-resource numbers can be trended over time and compared against serve-stress.yml's. # Real-repo coverage under the same Alpine/8 GiB path lives in serve-stress-alpine-real.yml. # # Split design (see tools/serve_stress_alpine.sh for the container side): bazel-diff is BUILT # on the glibc host -- building it inside Alpine would need the whole Bazel toolchain working -# under musl, the most fragile possible path -- and only RUN in the container, on Alpine's musl -# OpenJDK. The one glibc-bound runtime piece is the `bazel` binary serve shells out to for +# under musl, the most fragile possible path -- and only RUN in the container. It is the static +# musl release asset, so it needs nothing from the container. The one glibc-bound runtime piece +# is the `bazel` binary serve shells out to for # `bazel query`; the container script proves a flavor works (official release under gcompat # with the server JVM redirected to the musl JDK, falling back to Alpine's packaged bazel) # with a real probe query before committing the 30+ minute harness run to it. @@ -66,17 +67,19 @@ jobs: - name: Setup Bazelisk run: go install github.com/bazelbuild/bazelisk@latest - uses: actions/checkout@v4 - - name: Build bazel-diff deploy jar (glibc host) - run: ~/go/bin/bazelisk build //cli:bazel-diff_deploy.jar + # The same statically linked musl asset the release publishes for Linux amd64, so the + # container needs no JDK or libc for bazel-diff itself. + - name: Build bazel-diff (static musl binary, cross-built on the glibc host) + run: ~/go/bin/bazelisk build //release:bazel-diff-rust --config=release-musl - name: Stage container inputs - # Everything the container needs goes into one mounted directory: the deploy jar, the + # Everything the container needs goes into one mounted directory: the binary, the # harness sources, .bazelversion (the harness copies it into fabricated workspaces), # and the official bazel binary matching .bazelversion -- downloaded here on the host # so the container needs no network beyond apk. bazel-bin is a symlink into the host's # output base, hence the copy rather than mounting the repo. run: | mkdir -p "$STAGE/tools" - cp bazel-bin/cli/bazel-diff_deploy.jar "$STAGE/bazel-diff.jar" + cp -L bazel-bin/release/bazel-diff-rust-linux-amd64 "$STAGE/bazel-diff" cp tools/serve_stress.py tools/serve_harness.py tools/serve_stress_alpine.sh "$STAGE/tools/" cp .bazelversion "$STAGE/" BV=$(cat .bazelversion) diff --git a/.github/workflows/serve-stress.yml b/.github/workflows/serve-stress.yml index 294cd268..ce4d1958 100644 --- a/.github/workflows/serve-stress.yml +++ b/.github/workflows/serve-stress.yml @@ -6,12 +6,6 @@ name: Serve Stress # storm, a git-remote outage, lame-duck startup) and captures throughput / response-time # metrics for every phase. Complements the functional matrix in serve-harness.yml. # -# Runs the hermetic profile against both bazel-diff implementations (Kotlin //cli:bazel-diff -# and Rust //src:bazel-diff, which implement the same `serve` subcommand) so their throughput / -# latency can be compared leg-by-leg; fail-fast:false so a regression in one implementation -# doesn't hide the other's results. A final job downloads both legs' metrics and prints a -# side-by-side comparison into the run's step summary. -# # Scheduled runs use the full profile; on-demand runs from the Actions tab can select the # reduced quick profile. Note: scheduled + dispatched workflows run from the default branch, # so this must land on master before the cron fires. @@ -28,15 +22,6 @@ on: jobs: ServeStress: - strategy: - fail-fast: false # a regression in one implementation must not hide the other's data - matrix: - include: - - impl: kotlin - target: "//cli:bazel-diff" - - impl: rust - target: "//src:bazel-diff" - name: ServeStress (${{ matrix.impl }}) runs-on: ubuntu-latest timeout-minutes: 45 steps: @@ -53,56 +38,29 @@ jobs: - name: Setup Bazelisk run: go install github.com/bazelbuild/bazelisk@latest - uses: actions/checkout@v4 - - name: Run serve stress harness (${{ matrix.impl }}) + - name: Run serve stress harness # git daemon ships with git (preinstalled on the runner); python3 is preinstalled. # --bazel points the harness (and the serve subprocesses it spawns) at bazelisk; the # bazel version comes from .bazelversion, which the harness copies into its workspaces. run: > python3 tools/serve_stress.py --bazel "$HOME/go/bin/bazelisk" - --target "${{ matrix.target }}" - --metrics-out "serve-stress-${{ matrix.impl }}-metrics.json" - --summary-out "serve-stress-${{ matrix.impl }}-summary.md" + --metrics-out "serve-stress-metrics.json" + --summary-out "serve-stress-summary.md" ${{ inputs.quick && '--quick' || '' }} - name: Publish step summary if: always() run: | - if [ -f "serve-stress-${{ matrix.impl }}-summary.md" ]; then - cat "serve-stress-${{ matrix.impl }}-summary.md" >> "$GITHUB_STEP_SUMMARY" + if [ -f "serve-stress-summary.md" ]; then + cat "serve-stress-summary.md" >> "$GITHUB_STEP_SUMMARY" fi - name: Upload metrics artifact if: always() uses: actions/upload-artifact@v4 with: - name: serve-stress-${{ matrix.impl }}-${{ github.run_id }} + name: serve-stress-${{ github.run_id }} path: | - serve-stress-${{ matrix.impl }}-metrics.json - serve-stress-${{ matrix.impl }}-summary.md + serve-stress-metrics.json + serve-stress-summary.md if-no-files-found: warn retention-days: 90 - - CompareImplementations: - name: Compare Rust vs Kotlin - needs: ServeStress - if: always() - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Download Kotlin metrics - uses: actions/download-artifact@v4 - continue-on-error: true - with: - name: serve-stress-kotlin-${{ github.run_id }} - path: kotlin-metrics - - name: Download Rust metrics - uses: actions/download-artifact@v4 - continue-on-error: true - with: - name: serve-stress-rust-${{ github.run_id }} - path: rust-metrics - - name: Build comparison summary - run: | - python3 tools/serve_stress_compare.py \ - --kotlin kotlin-metrics/serve-stress-kotlin-metrics.json \ - --rust rust-metrics/serve-stress-rust-metrics.json \ - >> "$GITHUB_STEP_SUMMARY" diff --git a/BUILD b/BUILD index a86edcea..846c1a34 100644 --- a/BUILD +++ b/BUILD @@ -1,4 +1,3 @@ -load("@rules_kotlin//kotlin:core.bzl", "define_kt_toolchain") load("@rules_license//rules:license.bzl", "license") load("@rules_rust//rust:defs.bzl", "rust_clippy_test", "rustfmt_test") @@ -13,9 +12,13 @@ exports_files( alias( name = "bazel-diff", - actual = "//cli:bazel-diff", + actual = "//src:bazel-diff", + visibility = ["//visibility:public"], ) +# Kept for consumers that adopted the Rust binary while it was published next +# to the JVM one under this name (`bazel run @bazel-diff//:bazel-diff-rust`). +# It is the same binary as //:bazel-diff; prefer that label. alias( name = "bazel-diff-rust", actual = "//src:bazel-diff", @@ -44,8 +47,8 @@ test_suite( _RUST_LINT_ROOTS = [ "//src:bazel-diff", "//src:bazel_diff_lib", - # The un-split, whole-crate e2e target (//tests:e2e_test is now a - # test_suite over one target per case, and the per-case targets carry + # The un-split, whole-crate e2e target (//tests:e2e_test is a test_suite + # over one target per case, and the per-case targets carry # no-clippy/no-rustfmt so the crate is linted once rather than 38 times). "//tests:e2e_test_all", "//tools/coverage:lcov_merger", @@ -66,7 +69,7 @@ rustfmt_test( alias( name = "format", - actual = "//cli/format:format", + actual = "//tools/format:rustfmt", ) package( @@ -83,8 +86,3 @@ license( package_url = "https://github.com/Tinder/bazel-diff", package_version = "47.0.0", ) - -define_kt_toolchain( - name = "kotlin_toolchain", - jvm_target = "11", -) diff --git a/MODULE.bazel b/MODULE.bazel index 1f043c8d..f921e618 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,48 +8,17 @@ bazel_dep(name = "aspect_rules_lint", version = "2.1.0", dev_dependency = True) bazel_dep(name = "buildifier_prebuilt", version = "8.5.1.2", dev_dependency = True) bazel_dep(name = "aspect_bazel_lib", version = "2.22.5", dev_dependency = True) -bazel_dep(name = "bazel_skylib", version = "1.9.0") - # Direct dep because //release selects on @platforms//os and @platforms//cpu to # name the release binaries. bazel_dep(name = "platforms", version = "1.1.0") -bazel_dep(name = "rules_proto", version = "7.1.0") -bazel_dep(name = "rules_java", version = "9.7.0") -bazel_dep(name = "rules_kotlin", version = "2.4.0") bazel_dep(name = "rules_license", version = "1.0.0") -# kt_jvm_test hardcodes its _lcov_merger to Bazel's built-in merger instead of -# reading the coverage fragment's output_generator configuration field, so it -# ignores --coverage_output_generator (see tools/coverage/README.md — that flag -# is how per-target coverage minimums are enforced). Patch it to use the -# configuration field, like rules_go/rules_rust/rules_java do. Root-module-only: -# overrides are ignored when bazel-diff is consumed as a dependency. -single_version_override( - module_name = "rules_kotlin", - patch_strip = 1, - patches = ["//tools/coverage:rules_kotlin_lcov_merger.patch"], -) - -bazel_dep(name = "rules_jvm_external", version = "6.10") - -# Add protobuf and grpc for Bazel 9 compatibility +# protoc for the build script that compiles rust/proto/*.proto into the crate. bazel_dep(name = "protobuf", version = "33.4") -bazel_dep(name = "stardoc", version = "0.7.2") -single_version_override( - module_name = "grpc-java", - version = "1.78.0", -) - -bazel_dep(name = "grpc", version = "1.76.0", repo_name = "com_github_grpc_grpc") - -# Override transitive dependencies to Bazel 9 compatible versions. -# Marked dev_dependency: these are only needed when building bazel-diff itself -# (as the root module). rules_nodejs and rules_foreign_cc are not referenced by -# any bazel-diff target, and rules_python is supplied transitively (proto codegen) -# regardless, so consumers resolve their own versions via MVS. -bazel_dep(name = "rules_nodejs", version = "6.7.3", dev_dependency = True) -bazel_dep(name = "rules_foreign_cc", version = "0.15.1", dev_dependency = True) +# rules_python is only needed when building bazel-diff itself (the release, +# coverage and e2e tooling under //tools is Python). Marked dev_dependency so +# consumers of bazel-diff as a module don't inherit it via MVS. bazel_dep(name = "rules_python", version = "1.8.4", dev_dependency = True) # Go support is internal to building/testing bazel-diff (tooling under tools/go, @@ -233,62 +202,6 @@ rust.repository_set( versions = ["1.85.0"], ) -maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") -maven.install( - name = "bazel_diff_maven", - artifacts = [ - "com.google.code.gson:gson:2.14.0@jar", - "com.google.guava:guava:33.6.0-jre", - "com.willowtreeapps.assertk:assertk-jvm:0.28.1", - "info.picocli:picocli:4.7.7@jar", - "io.insert-koin:koin-core-jvm:4.2.2", - "io.insert-koin:koin-test-junit4:4.2.2", - "junit:junit:4.13.2", - "org.apache.commons:commons-pool2:2.13.1", - "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0", - "org.mockito.kotlin:mockito-kotlin:6.3.0", - "software.amazon.awssdk:url-connection-client:2.46.7", - ], - fail_if_repin_required = True, - lock_file = "//:maven_install.json", -) - -# AWS SDK for the `serve` S3 hash-cache backend. The Netty/Apache HTTP clients are -# excluded (the service explicitly uses the lightweight url-connection-client above) -# to keep the deploy jar lean. `sts` is runtime-only: DefaultCredentialsProvider needs -# it to assume the pod's IAM role via web identity (IRSA) when running on EKS. -maven.artifact( - name = "bazel_diff_maven", - artifact = "s3", - exclusions = [ - "software.amazon.awssdk:netty-nio-client", - "software.amazon.awssdk:apache-client", - "software.amazon.awssdk:apache5-client", - ], - group = "software.amazon.awssdk", - version = "2.46.7", -) -maven.artifact( - name = "bazel_diff_maven", - artifact = "sts", - exclusions = [ - "software.amazon.awssdk:netty-nio-client", - "software.amazon.awssdk:apache-client", - "software.amazon.awssdk:apache5-client", - ], - group = "software.amazon.awssdk", - version = "2.46.7", -) -use_repo( - maven, - bazel_diff_maven = "bazel_diff_maven", -) - -non_module_repositories = use_extension("//:extensions.bzl", "non_module_repositories", dev_dependency = True) -use_repo(non_module_repositories, "ktfmt") - -register_toolchains("//:kotlin_toolchain") - crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") crate.from_cargo( name = "bazel_diff_crates", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7b98a8fa..f759675c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -4,39 +4,24 @@ "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", - "https://bcr.bazel.build/modules/abseil-cpp/20220623.1/MODULE.bazel": "73ae41b6818d423a11fd79d95aedef1258f304448193d4db4ff90e5e7a0f076c", "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", - "https://bcr.bazel.build/modules/abseil-cpp/20240116.0/MODULE.bazel": "98dc378d64c12a4e4741ad3362f87fb737ee6a0886b2d90c3cdbb4d93ea3e0bf", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", - "https://bcr.bazel.build/modules/abseil-cpp/20240722.0/MODULE.bazel": "88668a07647adbdc14cb3a7cd116fb23c9dda37a90a1681590b6c9d8339a5b84", - "https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5", "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", - "https://bcr.bazel.build/modules/abseil-cpp/20250512.0/MODULE.bazel": "c4d02dd22cd87458516655a45512060246ee2a4732f1fbe948a5bd9eb614e626", "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.0/MODULE.bazel": "c43c16ca2c432566cdb78913964497259903ebe8fb7d9b57b38e9f1425b427b8", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", - "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", - "https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a", - "https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b", + "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/source.json": "d725d73707d01bb46ab3ca59ba408b8e9bd336642ca77a2269d4bfb8bbfd413d", "https://bcr.bazel.build/modules/ape/1.0.1/MODULE.bazel": "37411cfd13bfc28cd264674d660a3ecb3b5b35b9dbe4c0b2be098683641b3fee", "https://bcr.bazel.build/modules/ape/1.0.1/source.json": "96bc5909d1e3ccc4203272815ef874dbfd99651e240c05049f12193d16c1110b", "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", - "https://bcr.bazel.build/modules/apple_support/1.13.0/MODULE.bazel": "7c8cdea7e031b7f9f67f0b497adf6d2c6a2675e9304ca93a9af6ed84eef5a524", "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", - "https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b", - "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", - "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", "https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442", "https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8", - "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", - "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://bcr.bazel.build/modules/apple_support/1.24.1/source.json": "cf725267cbacc5f028ef13bb77e7f2c2e0066923a4dab1025e4a0511b1ed258a", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f", - "https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.42.2/MODULE.bazel": "2e0d8ab25c57a14f56ace1c8e881b69050417ff91b2fb7718dc00d201f3c3478", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.0.0/MODULE.bazel": "e118477db5c49419a88d78ebc7a2c2cea9d49600fe0f490c1903324a2c16ecd9", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", @@ -46,7 +31,6 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", "https://bcr.bazel.build/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b", - "https://bcr.bazel.build/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95", "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/MODULE.bazel": "01a1014e95e6816b68ecee2584ae929c7d6a1b72e4333ab1ff2d2c6c30babdf1", "https://bcr.bazel.build/modules/aspect_rules_js/1.40.0/source.json": "b6fd491369e9ef888fdef64b839023a2360caaea8eb370d2cfbfdd2a96721311", "https://bcr.bazel.build/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", @@ -58,7 +42,6 @@ "https://bcr.bazel.build/modules/bazel_features/1.0.0/MODULE.bazel": "d7f022dc887efb96e1ee51cec7b2e48d41e36ff59a6e4f216c40e4029e1585bf", "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", - "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", @@ -66,7 +49,6 @@ "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", - "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", @@ -74,19 +56,16 @@ "https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", - "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.43.0/MODULE.bazel": "defa2226f06ba20550d6548c3a2ea2a7929634437a52973869c20c225450eb91", "https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e", "https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", - "https://bcr.bazel.build/modules/bazel_jar_jar/0.1.7/MODULE.bazel": "d2736a1dbfd8f72befc532823b5112f2597a28064ce4aac280c65bee7d8830a0", "https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878", "https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", - "https://bcr.bazel.build/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287", - "https://bcr.bazel.build/modules/bazel_lib/3.1.0/source.json": "aaf7c2dc816219f4cb356c9d65f2555fb7f9543e537199f74a921f7877d23dfb", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -101,47 +80,14 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.0/MODULE.bazel": "2fb3fb53675f6adfc1ca5bfbd5cfb655ae350fba4706d924a8ec7e3ba945671c", "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", - "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/MODULE.bazel": "396c1ef53835aafe3d42ce6619080531ee770648303731f16cfaa33fa056bf0c", - "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/source.json": "abaf8ac9d2ab2f47bda9af4c0c080ff7907378888e1f4bc62a0539dd13ba61e8", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel": "e76479eae70bd4e8f5f4c2dfc5d03ab971cfb18750246c7b3f3454c5c2ee6629", - "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/source.json": "9395c4679444bc47bf7e51a710366a4480aa371c6f6bed01868e2fabcf11acec", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20230215-5c22014/MODULE.bazel": "4b03dc0d04375fa0271174badcd202ed249870c8e895b26664fd7298abea7282", - "https://bcr.bazel.build/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "d0405b762c5e87cd445b7015f2b8da5400ef9a8dbca0bfefa6c1cea79d528a97", - "https://bcr.bazel.build/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb", - "https://bcr.bazel.build/modules/boringssl/0.20241024.0/MODULE.bazel": "b540cff73d948cb79cb0bc108d7cef391d2098a25adabfda5043e4ef548dbc87", - "https://bcr.bazel.build/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", "https://bcr.bazel.build/modules/buildifier_prebuilt/6.4.0/MODULE.bazel": "37389c6b5a40c59410b4226d3bb54b08637f393d66e2fa57925c6fcf68e64bf4", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.5.1.2/MODULE.bazel": "9a6e0a2e87d1e3da679e157da5192ea351d5739ca1ff51831c2b736d5b6034de", "https://bcr.bazel.build/modules/buildifier_prebuilt/8.5.1.2/source.json": "33e11b3bf11e39cb762480a7e6ea1d24d044636135cdd8b8e74b07ebcd3b8d8b", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", - "https://bcr.bazel.build/modules/c-ares/1.15.0/MODULE.bazel": "ba0a78360fdc83f02f437a9e7df0532ad1fbaa59b722f6e715c11effebaa0166", - "https://bcr.bazel.build/modules/c-ares/1.19.1/MODULE.bazel": "73bca21720772370ff91cc8e88bbbaf14897720c6473e87c1ddc0f848284c313", - "https://bcr.bazel.build/modules/c-ares/1.34.5.bcr.1/MODULE.bazel": "f4632f68dbc075342966477d9c94a8a4a299d91e155980b042e1cd9f5a7ebcf5", - "https://bcr.bazel.build/modules/c-ares/1.34.5.bcr.1/source.json": "55fae1e004176f6cb36efcc77de9894f799d985f482b8f82fec989d556745a84", - "https://bcr.bazel.build/modules/cel-spec/0.15.0/MODULE.bazel": "e1eed53d233acbdcf024b4b0bc1528116d92c29713251b5154078ab1348cb600", - "https://bcr.bazel.build/modules/cel-spec/0.24.0/MODULE.bazel": "e310c7aff8490ed689ccafd32729b77a660b9547f5a5ba9b20e967011c324b36", - "https://bcr.bazel.build/modules/cel-spec/0.24.0/source.json": "522d08bc22524e07863276dd0f038f446a83166e91281dcfc07d5b8433c8d89e", - "https://bcr.bazel.build/modules/civetweb/1.16/MODULE.bazel": "46a38f9daeb57392e3827fce7d40926be0c802bd23cdd6bfd3a96c804de42fae", - "https://bcr.bazel.build/modules/civetweb/1.16/source.json": "ba8b9585adb8355cb51b999d57172fd05e7a762c56b8d4bac6db42c99de3beb7", - "https://bcr.bazel.build/modules/curl/8.4.0/MODULE.bazel": "0bc250aa1cb69590049383df7a9537c809591fcf876c620f5f097c58fdc9bc10", - "https://bcr.bazel.build/modules/curl/8.7.1/MODULE.bazel": "088221c35a2939c555e6e47cb31a81c15f8b59f4daa8009b1e9271a502d33485", - "https://bcr.bazel.build/modules/curl/8.8.0/MODULE.bazel": "7da3b3e79b0b4ee8f8c95d640bc6ad7b430ce66ef6e9c9d2bc29b3b5ef85f6fe", - "https://bcr.bazel.build/modules/curl/8.8.0/source.json": "d7d138b6878cf38891692fee0649ace35357fd549b425614d571786f054374d4", - "https://bcr.bazel.build/modules/cython/3.0.11-1/MODULE.bazel": "868b3f5c956c3657420d2302004c6bb92606bfa47e314bab7f2ba0630c7c966c", - "https://bcr.bazel.build/modules/cython/3.0.11-1/source.json": "da318be900b8ca9c3d1018839d3bebc5a8e1645620d0848fa2c696d4ecf7c296", "https://bcr.bazel.build/modules/download_utils/1.0.1/MODULE.bazel": "f1d0afade59e37de978506d6bbf08d7fe5f94964e86944aaf58efcead827b41b", "https://bcr.bazel.build/modules/download_utils/1.0.1/source.json": "05ddc5a3b1f7d8f3e5e0fd1617479e1cf72d63d59ab2b1f0463557a14fc6be0a", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20241214-918efc9/MODULE.bazel": "24e05f6f52f37be63a795192848555a2c8c855e7814dbc1ed419fb04a7005464", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20250128-4de3c74/MODULE.bazel": "1fe72489212c530086e3ffb0e018b2bfef4663200ca03571570f9f006bef1d75", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20251105-4a2b9a3/MODULE.bazel": "b66e87a0e0c2207f07e35c321388eb1feb036344565977444b52912c53a84466", - "https://bcr.bazel.build/modules/envoy_api/0.0.0-20251105-4a2b9a3/source.json": "c4780edf780977f2ab7d00a189432c5b0b2fa08c6e4e2e09d2950499364a687d", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", "https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996", @@ -150,88 +96,28 @@ "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", - "https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4", - "https://bcr.bazel.build/modules/gazelle/0.39.1/MODULE.bazel": "1fa3fefad240e535066fd0e6950dfccd627d36dc699ee0034645e51dbde3980f", - "https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", - "https://bcr.bazel.build/modules/gazelle/0.46.0/MODULE.bazel": "3dec215dacf2427df87b524a2c99da387882a18d753f0b1b38675992bd0a99c6", "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", - "https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e", - "https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7", - "https://bcr.bazel.build/modules/google_benchmark/1.9.4/MODULE.bazel": "3bab7c17c10580f87b647478a72a05621f88abc275afb97b578c828f56e59d45", - "https://bcr.bazel.build/modules/google_benchmark/1.9.4/source.json": "8e0036f76a5c2aa9c16ca0da57d8065cff69edeed58f1f85584c588c0ef723a5", - "https://bcr.bazel.build/modules/googleapis-cc/1.0.0/MODULE.bazel": "cf01757e7590c56140a4b81638ff2b3e7074769e6271720bbf738fcda25b6fc2", - "https://bcr.bazel.build/modules/googleapis-cc/1.0.0/source.json": "ab0e3a2ee9968a8848f59872fbbfa3e1f768597d71d2229e6caa319d357967c7", - "https://bcr.bazel.build/modules/googleapis-go/1.0.0/MODULE.bazel": "0a207a4c49da28c5cc1f7b3aeb23c2f7828c85c14aa8d9db0e30357a8d2250ed", - "https://bcr.bazel.build/modules/googleapis-go/1.0.0/source.json": "ef189be4e7853e1ebc6123fe20b71822bf9896bd1f8eed8f68505c4585f72a48", - "https://bcr.bazel.build/modules/googleapis-java/1.0.0/MODULE.bazel": "d633989337d069b5a95e6101777319681d7a4af4677e36801f11839d6512095c", - "https://bcr.bazel.build/modules/googleapis-java/1.0.0/source.json": "ee59e2de37e4b531172870ac0296afa38f1ea004105ee21b2793c31a9d0ddccd", - "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24", - "https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20240326-1c8d509c5/MODULE.bazel": "a4b7e46393c1cdcc5a00e6f85524467c48c565256b22b5fae20f84ab4a999a68", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20240819-fe8ba054a/MODULE.bazel": "117b7c7be7327ed5d6c482274533f2dbd78631313f607094d4625c28203cacdf", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/MODULE.bazel": "ee6c30f82ecd476e61f019fb1151aaab380ea419958ff274ef2f0efca7969f5c", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20251003-2193a2bf/MODULE.bazel": "cc9e5ed294ed9ebf42cdbbdddd2df29048519e3797004df1e3f369f31ff4f2d4", - "https://bcr.bazel.build/modules/googleapis/0.0.0-20251003-2193a2bf/source.json": "21558a194c519e27262cca9cf031bb166a666a8b7fb89993b700c828f8dc0857", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", - "https://bcr.bazel.build/modules/googletest/1.16.0/MODULE.bazel": "a175623c69e94fca4ca7acbc12031e637b0c489318cd4805606981d4d7adb34a", "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", - "https://bcr.bazel.build/modules/grpc-java/1.78.0/MODULE.bazel": "48f790fbb95625245295df1283e0dba344a4e30b4a9a9cefbabfa92bd84f3691", - "https://bcr.bazel.build/modules/grpc-proto/0.0.0-20240627-ec30f58/MODULE.bazel": "88de79051e668a04726e9ea94a481ec6f1692086735fd6f488ab908b3b909238", - "https://bcr.bazel.build/modules/grpc/1.41.0/MODULE.bazel": "5bcbfc2b274dabea628f0649dc50c90cf36543b1cfc31624832538644ad1aae8", - "https://bcr.bazel.build/modules/grpc/1.56.3.bcr.1/MODULE.bazel": "cd5b1eb276b806ec5ab85032921f24acc51735a69ace781be586880af20ab33f", - "https://bcr.bazel.build/modules/grpc/1.62.1/MODULE.bazel": "2998211594b8a79a6b459c4e797cfa19f0fb8b3be3149760ec7b8c99abfd426f", - "https://bcr.bazel.build/modules/grpc/1.63.1.bcr.1/MODULE.bazel": "d7b9fef03bd175e6825237b521b18a3c29f1ac15f8aa52c8a1a0f3bd8f33d54b", - "https://bcr.bazel.build/modules/grpc/1.66.0.bcr.2/MODULE.bazel": "0fa2b0fd028ce354febf0fe90f1ed8fecfbfc33118cddd95ac0418cc283333a0", - "https://bcr.bazel.build/modules/grpc/1.66.0.bcr.3/MODULE.bazel": "f6047e89faf488f5e3e65cb2594c6f5e86992abec7487163ff6b623526e543b0", - "https://bcr.bazel.build/modules/grpc/1.69.0/MODULE.bazel": "4e26e05c9e1ef291ccbc96aad8e457b1b8abedbc141623831629da2f8168eef6", - "https://bcr.bazel.build/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a", - "https://bcr.bazel.build/modules/grpc/1.74.1/MODULE.bazel": "09523be10ba2bfd999683671d0f8f22fb5b20ec77ad89b05ef58ff19a1b65c82", - "https://bcr.bazel.build/modules/grpc/1.76.0/MODULE.bazel": "7373bd407fcb183b8bf379fa35dfe29feebdc6d8ec1bc40004cec6c7b2544be0", - "https://bcr.bazel.build/modules/grpc/1.76.0/source.json": "5c1deef5b02cb7dd52df9cb628d956e02db3ec40bd165a465bc10b7bf47aa16a", "https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", "https://bcr.bazel.build/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", - "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/MODULE.bazel": "e5362dadc90aab6724c83a2cc1e67cbed9c89a05d97fb1f90053c8deb1e445c8", - "https://bcr.bazel.build/modules/libpfm/4.11.0.bcr.1/source.json": "0646414d9037f8aad148781dd760bec90b0b25ac12fda5e03f8aadbd6b9c61e6", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", - "https://bcr.bazel.build/modules/mbedtls/3.6.0/MODULE.bazel": "8e380e4698107c5f8766264d4df92e36766248447858db28187151d884995a09", - "https://bcr.bazel.build/modules/mbedtls/3.6.0/source.json": "1dbe7eb5258050afcc3806b9d43050f71c6f539ce0175535c670df606790b30c", - "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/MODULE.bazel": "87023db2f55fc3a9949c7b08dc711fae4d4be339a80a99d04453c4bb3998eefc", - "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/source.json": "296c63a90c6813e53b3812d24245711981fc7e563d98fe15625f55181494488a", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/MODULE.bazel": "cc18734138dd18c912c6ce2a59186db28f85d8058c99c9f21b46ca3e0aba0ebe", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/source.json": "7c135f9d42bb3b045669c3c6ab3bb3c208e00b46aca4422eea64c29811a5b240", - "https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "02201d2921dadb4ec90c4980eca4b2a02904eddcf6fa02f3da7594fb7b0d821c", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/MODULE.bazel": "789706a714855f92c5c8cfcf1ef32bbb64dcd3b7c9066756ad7986ec59709d29", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/source.json": "aadf3f53e08b72376506b7c4ea3d167010c9efb160d7d6e1e304ed646bac1b36", - "https://bcr.bazel.build/modules/opencensus-proto/0.4.1/MODULE.bazel": "4a2e8b4d0b544002502474d611a5a183aa282251e14f6a01afe841c0c1b10372", - "https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/MODULE.bazel": "49c0c07e8fb87b480bccb842cfee1b32617f11dac590f732573c69058699a3d1", - "https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/source.json": "0c0872e048bbea052a9c541fb47019481a19201ba5555a71d762ad591bf94e1f", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.14.2/MODULE.bazel": "089a5613c2a159c7dfde098dabfc61e966889c7d6a81a98422a84c51535ed17d", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.16.0/MODULE.bazel": "b7379a140f538cea3f749179a2d481ed81942cc6f7b05a6113723eb34ac3b3e7", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/MODULE.bazel": "3455326c08b28415648a3d60d8e3c811847ebdbe64474f75b25878f25585aea1", - "https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/source.json": "4e48137e4c3ecb99401ff99876df8fa330598d7da051869bec643446e8a8ff95", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.1.0/MODULE.bazel": "a49f406e99bf05ab43ed4f5b3322fbd33adfd484b6546948929d1316299b68bf", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.3.1/MODULE.bazel": "0141a50e989576ee064c11ce8dd5ec89993525bd9f9a09c5618e4dacc8df9352", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.4.0.bcr.1/MODULE.bazel": "5ceaf25e11170d22eded4c8032728b4a3f273765fccda32f9e94f463755c4167", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/MODULE.bazel": "7543d91a53b98e7b5b37c5a0865b93bff12c1ee022b1e322cd236b968894b030", - "https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143", - "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec", - "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", - "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", - "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", @@ -243,22 +129,9 @@ "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", "https://bcr.bazel.build/modules/platforms/1.1.0/MODULE.bazel": "1c0c09f5bdcf4b3f924720d2478a3711cb39f4977019ca5988685e5b7e18b3d2", "https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344", - "https://bcr.bazel.build/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/MODULE.bazel": "116ad46e97c1d2aeb020fe2899a342a7e703574ce7c0faf7e4810f938c974a9a", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/source.json": "e813cce2d450708cfcb26e309c5172583a7440776edf354e83e6788c768e5cca", - "https://bcr.bazel.build/modules/prometheus-cpp/1.3.0/MODULE.bazel": "ce82e086bbc0b60267e970f6a54b2ca6d0f22d3eb6633e00e2cc2899c700f3d8", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", - "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", - "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", - "https://bcr.bazel.build/modules/protobuf/25.6/MODULE.bazel": "fc0ae073b47c7ede88b825ff79e64f1c058967c7a87a86cdf4abecd9e0516625", - "https://bcr.bazel.build/modules/protobuf/26.0.bcr.1/MODULE.bazel": "8f04d38c2da40a3715ff6bdce4d32c5981e6432557571482d43a62c31a24c2cf", - "https://bcr.bazel.build/modules/protobuf/26.0.bcr.2/MODULE.bazel": "62e0b84ca727bdeb55a6fe1ef180e6b191bbe548a58305ea1426c158067be534", - "https://bcr.bazel.build/modules/protobuf/26.0/MODULE.bazel": "8402da964092af40097f4a205eec2a33fd4a7748dc43632b7d1629bfd9a2b856", - "https://bcr.bazel.build/modules/protobuf/27.0-rc2/MODULE.bazel": "b2b0dbafd57b6bec0ca9b251da02e628c357dab53a097570aa7d79d020f107cf", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", - "https://bcr.bazel.build/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295", - "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", @@ -266,40 +139,19 @@ "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", - "https://bcr.bazel.build/modules/protobuf/30.0/MODULE.bazel": "0e736de5d52ad7824113f47e65256a26ee74b689ba859c5447a0663e5a075409", - "https://bcr.bazel.build/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e", - "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", - "https://bcr.bazel.build/modules/protobuf/33.0/MODULE.bazel": "c5270efb4aad37a2f893536076518793f409ea7df07a06df995d848d1690f21c", "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.0.4.bcr.2/MODULE.bazel": "c4bd2c850211ff5b7dadf9d2d0496c1c922fdedc303c775b01dfd3b3efc907ed", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.0.4/MODULE.bazel": "b8913c154b16177990f6126d2d2477d187f9ddc568e95ee3e2d50fc65d2c494a", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.1/MODULE.bazel": "4bf09676b62fa587ae07e073420a76ec8766dcce7545e5f8c68cfa8e484b5120", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.2/MODULE.bazel": "3bd4b14a8e7c78dbef973280deabaa139db1fe350aa92da03730a31f59082068", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.2/source.json": "14c28a5527fcd699f5efbf83a046666efabed3384364bd48428de89dfdc8110e", - "https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1/MODULE.bazel": "52b51f50533ec4fbd5d613cd093773f979ac2e035d954e02ca11de383f502505", "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", - "https://bcr.bazel.build/modules/pybind11_bazel/3.0.0/MODULE.bazel": "a2bfa6020ed603a00d944161c63173c7f109774e99bee0c2cd8dbf24159f8134", - "https://bcr.bazel.build/modules/pybind11_bazel/3.0.0/source.json": "d8f5104d4c21d272bf327ebe44366fb0b4c036cdaa1f5cceb21a408ca4ef2ef8", - "https://bcr.bazel.build/modules/rapidjson/1.1.0.bcr.20241007/MODULE.bazel": "82fbcb2e42f9e0040e76ccc74c06c3e46dfd33c64ca359293f8b84df0e6dff4c", - "https://bcr.bazel.build/modules/rapidjson/1.1.0.bcr.20241007/source.json": "5c42389ad0e21fc06b95ad7c0b730008271624a2fa3292e0eab5f30e15adeee3", - "https://bcr.bazel.build/modules/re2/2021-09-01/MODULE.bazel": "bcb6b96f3b071e6fe2d8bed9cc8ada137a105f9d2c5912e91d27528b3d123833", + "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", - "https://bcr.bazel.build/modules/re2/2024-05-01/MODULE.bazel": "55a3f059538f381107824e7d00df5df6d061ba1fb80e874e4909c0f0549e8f3e", "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", + "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4", "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", - "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/MODULE.bazel": "3d9d4995833fc0334fc5c88b56a05288dd25d651544cd7b2233bbd6357bbeba0", - "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/source.json": "7df1394aabda1c9bc188a302f5d54b1c657924edd04ebc57d2be29dbd7efd141", "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", - "https://bcr.bazel.build/modules/rules_android/0.6.6/MODULE.bazel": "b0fb569752aab65ab1a9db0a8f6cfaf5aa1754965e17e95dcf0e4d88e192a68d", - "https://bcr.bazel.build/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363", - "https://bcr.bazel.build/modules/rules_android/0.7.1/source.json": "151440aed3f0f73a00d4ed5cec5d31f63a6fef9b95d8fab1eb1810150fa525f2", - "https://bcr.bazel.build/modules/rules_apple/3.13.0/MODULE.bazel": "b4559a2c6281ca3165275bb36c1f0ac74666632adc5bdb680e366de7ce845f43", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", - "https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", + "https://bcr.bazel.build/modules/rules_apple/3.16.0/source.json": "d8b5fe461272018cc07cfafce11fe369c7525330804c37eec5a82f84cd475366", "https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", "https://bcr.bazel.build/modules/rules_buf/0.5.2/MODULE.bazel": "5f2492d284ab9bedf2668178303abf5f3cd7d8cdf85d768951008e88456e9c6a", "https://bcr.bazel.build/modules/rules_buf/0.5.2/source.json": "41876d4834c0832de4b393de6e55dfd1cb3b25d3109e4ba90eb7fb57c560e0d9", @@ -311,28 +163,17 @@ "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", - "https://bcr.bazel.build/modules/rules_cc/0.0.5/MODULE.bazel": "be41f87587998fe8890cd82ea4e848ed8eb799e053c224f78f3ff7fe1a1d9b74", "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", - "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", - "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", - "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", - "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", - "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", "https://bcr.bazel.build/modules/rules_cc/0.2.18/MODULE.bazel": "4460ec36adc8f722a6a2a4ac9374cb91f2acebadaa93fc37966129afb3dece87", "https://bcr.bazel.build/modules/rules_cc/0.2.18/source.json": "abad668ff2fd63ada1ac49bf386d37e27048b89a3465a6fd968bb832b00a09d3", "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", - "https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1", "https://bcr.bazel.build/modules/rules_diff/1.0.0/MODULE.bazel": "1739509d8db9a6cd7d3584822340d3dfe1f9f27e62462fbca60aa061d88741b2", "https://bcr.bazel.build/modules/rules_diff/1.0.0/source.json": "fc3824aed007b4db160ffb994036c6e558550857b6634a8e9ccee3e74c659312", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.15.1/MODULE.bazel": "c2c60d26c79fda484acb95cdbec46e89d6b28b4845cb277160ce1e0c8622bb88", - "https://bcr.bazel.build/modules/rules_foreign_cc/0.15.1/source.json": "a161811a63ba8a859086da3b7ff3ad04f2e9c255d7727b41087103fc0eb22f55", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", "https://bcr.bazel.build/modules/rules_go/0.33.0/MODULE.bazel": "a2b11b64cd24bf94f57454f53288a5dacfe6cb86453eee7761b7637728c1910c", @@ -340,67 +181,40 @@ "https://bcr.bazel.build/modules/rules_go/0.39.1/MODULE.bazel": "d34fb2a249403a5f4339c754f1e63dc9e5ad70b47c5e97faee1441fc6636cd61", "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", - "https://bcr.bazel.build/modules/rules_go/0.45.1/MODULE.bazel": "6d7884f0edf890024eba8ab31a621faa98714df0ec9d512389519f0edff0281a", "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", - "https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03", - "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", - "https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", - "https://bcr.bazel.build/modules/rules_go/0.58.3/MODULE.bazel": "5582119a4a39558d8d1b1634bcae46043d4f43a31415e861c3551b2860040b5e", - "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", "https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb", "https://bcr.bazel.build/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", - "https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", - "https://bcr.bazel.build/modules/rules_java/5.5.0/MODULE.bazel": "486ad1aa15cdc881af632b4b1448b0136c76025a1fe1ad1b65c5899376b83a50", "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", - "https://bcr.bazel.build/modules/rules_java/6.3.1/MODULE.bazel": "5a3471c8b84d53d58d5f6e316313680d7dd2c70afac696dbe14b761b0b5c6a06", "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", "https://bcr.bazel.build/modules/rules_java/7.0.6/MODULE.bazel": "6ddb07d9857a1a3accc9f6d005f20c969c4659c7710e6269a51db3527e0ea969", - "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", - "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", - "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", - "https://bcr.bazel.build/modules/rules_java/8.13.0/MODULE.bazel": "0444ebf737d144cf2bb2ccb368e7f1cce735264285f2a3711785827c1686625e", "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", - "https://bcr.bazel.build/modules/rules_java/8.16.1/MODULE.bazel": "0f20b1cecaa8e52f60a8f071e59a20b4e3b9a67f6c56c802ea256f6face692d3", + "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", - "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71", "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", - "https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a", - "https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2", - "https://bcr.bazel.build/modules/rules_java/9.7.0/MODULE.bazel": "3ce6bd55fdd4fcb3323197736b7d976e0eedcc0eea78ca6186d20d314ba12e12", - "https://bcr.bazel.build/modules/rules_java/9.7.0/source.json": "23b356565156e0fbc71e5dad7cf8e1b951466b7ae3fd731cb1d2c95ca82b1d70", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", - "https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495", "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", - "https://bcr.bazel.build/modules/rules_jvm_external/6.10/MODULE.bazel": "33e636ca6bc9ee0fa090a38aa33c631ded2d8cf6fead4124181d1b35dc474f7c", - "https://bcr.bazel.build/modules/rules_jvm_external/6.10/source.json": "c191249787625db72616a3fb3cc2786ab57355a2e3b615402b8b3b66b0f995b7", - "https://bcr.bazel.build/modules/rules_jvm_external/6.2/MODULE.bazel": "36a6e52487a855f33cb960724eb56547fa87e2c98a0474c3acad94339d7f8e99", "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", - "https://bcr.bazel.build/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55", "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", - "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274", + "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", - "https://bcr.bazel.build/modules/rules_kotlin/2.1.3/MODULE.bazel": "ce7def6d576aa8d3a9c6d10e13b4d157296229674371f67dbf788dae0afae3d5", - "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/MODULE.bazel": "38dac18bb76c0a47ff60dfcd95c666985cbc46374f28ea4eeb868bdbc58c5bec", - "https://bcr.bazel.build/modules/rules_kotlin/2.4.0/source.json": "07b6a307448817c071c4ba90dcb03f801e008959f8dfd8b152a241cc0ee01a23", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", - "https://bcr.bazel.build/modules/rules_license/0.0.8/MODULE.bazel": "5669c6fe49b5134dbf534db681ad3d67a2d49cfc197e4a95f1ca2fd7f3aebe96", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", "https://bcr.bazel.build/modules/rules_multirun/0.9.0/MODULE.bazel": "32d628ef586b5b23f67e55886b7bc38913ea4160420d66ae90521dda2ff37df0", @@ -409,10 +223,7 @@ "https://bcr.bazel.build/modules/rules_multitool/0.11.0/source.json": "0b86574a1eaff37c33aafaff095ea16d6ac846beb94ffc74c4fcf626f8f80681", "https://bcr.bazel.build/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", - "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09", - "https://bcr.bazel.build/modules/rules_perl/0.2.4/MODULE.bazel": "5f5af7be4bf5fb88d91af7469518f0fd2161718aefc606188f7cd51f436ca938", - "https://bcr.bazel.build/modules/rules_perl/0.2.4/source.json": "574317d6b3c7e4843fe611b76f15e62a1889949f5570702e1ee4ad335ea3c339", + "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/source.json": "6a6ca0940914d55c550d1417cad13a56c9900e23f651a762d8ccc5a64adcf661", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", @@ -425,38 +236,22 @@ "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", - "https://bcr.bazel.build/modules/rules_python/0.20.0/MODULE.bazel": "bfe14d17f20e3fe900b9588f526f52c967a6f281e47a1d6b988679bd15082286", - "https://bcr.bazel.build/modules/rules_python/0.22.0/MODULE.bazel": "b8057bafa11a9e0f4b08fc3b7cd7bee0dcbccea209ac6fc9a3ff051cd03e19e9", - "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", "https://bcr.bazel.build/modules/rules_python/0.26.0/MODULE.bazel": "42cb98cd15954e83b96b540dcc6d5a618eb061f056147ac4ea46e687a066a7c7", "https://bcr.bazel.build/modules/rules_python/0.27.1/MODULE.bazel": "65dc875cc1a06c30d5bbdba7ab021fd9e551a6579e408a3943a61303e2228a53", "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", - "https://bcr.bazel.build/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82", "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", - "https://bcr.bazel.build/modules/rules_python/0.32.2/MODULE.bazel": "01052470fc30b49de91fb8483d26bea6f664500cfad0b078d4605b03e3a83ed4", "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", - "https://bcr.bazel.build/modules/rules_python/0.35.0/MODULE.bazel": "c3657951764cdcdb5a7370d5e885fad5e8c1583320aad18d46f9f110d2c22755", - "https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500", - "https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", - "https://bcr.bazel.build/modules/rules_python/1.2.0/MODULE.bazel": "5aeeb48b2a6c19d668b48adf2b8a2b209a6310c230db0ce77450f148a89846e4", - "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", - "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", - "https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f", "https://bcr.bazel.build/modules/rules_python/1.8.4/MODULE.bazel": "33e3971e66161a3e955f7a0d411a8d1f291c4ce4c561851512466f3c77ff8ece", "https://bcr.bazel.build/modules/rules_python/1.8.4/source.json": "9fbc0e57bae52cddcc3831d668bce87a47e0c655104a85098d4459dd9a3b0a10", - "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", - "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea", - "https://bcr.bazel.build/modules/rules_rust/0.51.0/MODULE.bazel": "2b6d1617ac8503bfdcc0e4520c20539d4bba3a691100bee01afe193ceb0310f9", "https://bcr.bazel.build/modules/rules_rust/0.67.0/MODULE.bazel": "87c3816c4321352dcfd9e9e26b58e84efc5b21351ae3ef8fb5d0d57bde7237f5", "https://bcr.bazel.build/modules/rules_rust/0.73.0/MODULE.bazel": "25e3b077128612754c4add1b4c90d20a6be06566b623dee6e32038d0e8f93062", "https://bcr.bazel.build/modules/rules_rust/0.73.0/source.json": "8eeb3d9ba7c57916b63887a651e8f84c2f68b7243af9e712d728c2a0b7882255", - "https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", @@ -464,11 +259,8 @@ "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", - "https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9", "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", - "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", + "https://bcr.bazel.build/modules/rules_swift/2.1.1/source.json": "40fc69dfaac64deddbb75bd99cdac55f4427d9ca0afbe408576a65428427a186", "https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", @@ -480,8 +272,7 @@ "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb", "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", "https://bcr.bazel.build/modules/tar.bzl/0.5.1/source.json": "deed3094f7cc779ed1d37a68403847b0e38d9dd9d931e03cb90825f3368b515f", @@ -489,51 +280,17 @@ "https://bcr.bazel.build/modules/toolchain_utils/1.0.2/source.json": "88769ec576dddacafd8cca4631812cf8eead89f10a29d9405d9f7a553de6bf87", "https://bcr.bazel.build/modules/toolchains_musl/0.1.27.bcr.1/MODULE.bazel": "c5d1f5eb87305fefec90144c8b5677c917a3e5c212e045fd917e1637e59df39e", "https://bcr.bazel.build/modules/toolchains_musl/0.1.27.bcr.1/source.json": "f102c527042e7d213c4be481e479ea00b723edec385e9bf9df709d0b5be15ecb", - "https://bcr.bazel.build/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", - "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", - "https://bcr.bazel.build/modules/upb/0.0.0-20230907-e7430e6/MODULE.bazel": "3a7dedadf70346e678dc059dbe44d05cbf3ab17f1ce43a1c7a42edc7cbf93fd9", - "https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/MODULE.bazel": "cea509976a77e34131411684ef05a1d6ad194dd71a8d5816643bc5b0af16dc0f", - "https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/source.json": "7227e1fcad55f3f3cab1a08691ecd753cb29cc6380a47bc650851be9f9ad6d20", "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", "https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", - "https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.1/MODULE.bazel": "6a9fe6e3fc865715a7be9823ce694ceb01e364c35f7a846bf0d2b34762bc066b", - "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", - "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198", - "https://bcr.bazel.build/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72" + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" }, "selectedYankedVersions": {}, "moduleExtensions": { - "//:extensions.bzl%non_module_repositories": { - "general": { - "bzlTransitiveDigest": "zCfz5Y/Y2oEPiKR8kMtWhPJvz8pncvnqG4M0okjVuwc=", - "usagesDigest": "/H76v8NTgGSnO7xO7wmEhEJlyRvvSqpsFxLVjXM3Vnw=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "ktfmt": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", - "attributes": { - "integrity": "sha256-l/x/vRlNAan6RdgUfAVSQDAD1VusSridhNe7TV4/SN4=", - "url": "https://repo1.maven.org/maven2/com/facebook/ktfmt/0.46/ktfmt-0.46-jar-with-dependencies.jar" - } - } - }, - "recordedRepoMappingEntries": [ - [ - "", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, "@@aspect_rules_js+//npm:extensions.bzl%pnpm": { "general": { "bzlTransitiveDigest": "euYL3N7V/245TC8NJl5dm4Pq2GdagvtnpjGG8t9vBxY=", @@ -674,136 +431,89 @@ ] } }, - "@@envoy_api+//bazel:repositories.bzl%non_module_deps": { + "@@rules_buf+//buf:extensions.bzl%buf": { "general": { - "bzlTransitiveDigest": "GVR9ZmHmKQd+k6FY/jDmYTlwLU/H7IRoXDz3w4dqRdI=", - "usagesDigest": "lOhJkV09ITWn6LOK9fLMuf1t3969wdr45lToQ2MVQoU=", + "bzlTransitiveDigest": "dSWqckK2ILN7aDIDHfv+Qrl1fb1hF7o7MDXY6T8C41s=", + "usagesDigest": "vxN6C2h72rUERbAmd1476FWpxdxo1NhYoY5JSFXJT3g=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "prometheus_metrics_model": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "rules_buf_toolchains": { + "repoRuleId": "@@rules_buf+//buf/internal:toolchain.bzl%buf_download_releases", "attributes": { - "urls": [ - "https://github.com/prometheus/client_model/archive/v0.6.2.tar.gz" - ], - "sha256": "47c5ea7949f68e7f7b344350c59b6bd31eeb921f0eec6c3a566e27cf1951470c", - "strip_prefix": "client_model-0.6.2", - "build_file_content": "\nload(\"@envoy_api//bazel:api_build_system.bzl\", \"api_cc_py_proto_library\")\nload(\"@io_bazel_rules_go//proto:def.bzl\", \"go_proto_library\")\n\napi_cc_py_proto_library(\n name = \"client_model\",\n srcs = [\n \"io/prometheus/client/metrics.proto\",\n ],\n visibility = [\"//visibility:public\"],\n)\n\ngo_proto_library(\n name = \"client_model_go_proto\",\n importpath = \"github.com/prometheus/client_model/go\",\n proto = \":client_model\",\n visibility = [\"//visibility:public\"],\n)\n" + "version": "v1.47.2", + "sha256": "1b37b75dc0a777a0cba17fa2604bc9906e55bb4c578823d8b7a8fe3fc9fe4439" } } }, "recordedRepoMappingEntries": [ [ - "envoy_api+", + "rules_buf+", "bazel_tools", "bazel_tools" - ], - [ - "envoy_api+", - "envoy_api", - "envoy_api+" ] ] } }, - "@@googleapis+//:extensions.bzl%switched_rules": { + "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { - "bzlTransitiveDigest": "1ZTg77R3Ks/ksx8QMNIoTRJetbJPRlzGFlJa9hoUn+Y=", - "usagesDigest": "phuibqqjsm5RJqquWNJ7BXJvnPPgSCBreyRLEH/JjIA=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": {}, - "recordedRepoMappingEntries": [] - } - }, - "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { - "general": { - "bzlTransitiveDigest": "xeN+uPP1PKslnv5h3+uyhGaFXC4IIzcFwX/nQHlNwZk=", - "usagesDigest": "zr/niBQ/s2fHozWAsg4vI70wAxcuFjG+QtM15qGkq9o=", + "bzlTransitiveDigest": "03Qju4tW0vE+0RBuZGuV2A4Hx6AiSkdNahYvworx2aM=", + "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, "generatedRepoSpecs": { - "apksig": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "com_github_jetbrains_kotlin_git": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", "attributes": { - "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz", - "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD" + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" } - } - }, - "recordedRepoMappingEntries": [ - [ - "rules_android+", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, - "@@rules_android+//bzlmod_extensions:com_android_dex.bzl%com_android_dex_extension": { - "general": { - "bzlTransitiveDigest": "2icqERrdWnpamgt/akEgAtfBsjNG1/st+AAFRZx9aH4=", - "usagesDigest": "c1Y/KGGjUYCyd8zNIVTUh1bynVXRFz6xGKaSCBpQANM=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "com_android_dex": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + }, + "com_github_jetbrains_kotlin": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", "attributes": { - "url": "https://android.googlesource.com/platform/dalvik/+archive/5a81c499a569731e2395f7c8d13c0e0d4e17a2b6.tar.gz", - "build_file": "@@rules_android+//bzlmod_extensions:com_android_dex.BUILD" + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" } - } - }, - "recordedRepoMappingEntries": [ - [ - "rules_android+", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, - "@@rules_android+//rules/android_sdk_repository:rule.bzl%android_sdk_repository_extension": { - "general": { - "bzlTransitiveDigest": "+rMrzIrv7sImYmkbXJYv+gFpTJQ79X3MpwwMLI2A+oA=", - "usagesDigest": "iEGI2aNDMkHt9LXCdViLNUUOslpiVj2DrevWWXZEFnU=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "androidsdk": { - "repoRuleId": "@@rules_android+//rules/android_sdk_repository:rule.bzl%_android_sdk_repository", - "attributes": {} - } - }, - "recordedRepoMappingEntries": [] - } - }, - "@@rules_buf+//buf:extensions.bzl%buf": { - "general": { - "bzlTransitiveDigest": "dSWqckK2ILN7aDIDHfv+Qrl1fb1hF7o7MDXY6T8C41s=", - "usagesDigest": "vxN6C2h72rUERbAmd1476FWpxdxo1NhYoY5JSFXJT3g=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "rules_buf_toolchains": { - "repoRuleId": "@@rules_buf+//buf/internal:toolchain.bzl%buf_download_releases", + }, + "com_github_google_ksp": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", "attributes": { - "version": "v1.47.2", - "sha256": "1b37b75dc0a777a0cba17fa2604bc9906e55bb4c578823d8b7a8fe3fc9fe4439" + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] } } }, "recordedRepoMappingEntries": [ [ - "rules_buf+", + "rules_kotlin+", "bazel_tools", "bazel_tools" ] @@ -915,8 +625,8 @@ }, "@@rules_nodejs+//nodejs:extensions.bzl%node": { "general": { - "bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=", - "usagesDigest": "l5Z2+8YxTTnxb5NhHGrLYUoTLfhNQO6ePdhNS/te4oA=", + "bzlTransitiveDigest": "FmfMiNXAxRoLWw3NloQbssosE1egrSvzirbQnso7j7E=", + "usagesDigest": "+/eD+bbRTXQvCxo1j0JSXtArQ5RZAZSjW14VyIMoWxY=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -929,7 +639,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "linux_amd64" } @@ -942,7 +652,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "linux_arm64" } @@ -955,7 +665,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "linux_s390x" } @@ -968,7 +678,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "linux_ppc64le" } @@ -981,7 +691,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "darwin_amd64" } @@ -994,7 +704,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "darwin_arm64" } @@ -1007,7 +717,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "windows_amd64" } @@ -1020,7 +730,7 @@ "node_urls": [ "https://nodejs.org/dist/v{version}/{filename}" ], - "node_version": "22.22.0", + "node_version": "20.19.5", "include_headers": false, "platform": "windows_arm64" } @@ -1047,80 +757,6 @@ "recordedRepoMappingEntries": [] } }, - "@@rules_perl+//perl:extensions.bzl%perl_repositories": { - "general": { - "bzlTransitiveDigest": "rEvFEEQbGp20qoG38ex+bV2NMG2GhUnSh3CZscNcozE=", - "usagesDigest": "qSSNDdCNVxNhY36wMndEAFacdhR0ooLTmumfad0km9s=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "perl_darwin_arm64": { - "repoRuleId": "@@rules_perl+//perl:repo.bzl%perl_download", - "attributes": { - "strip_prefix": "perl-darwin-arm64", - "sha256": "285769f3c50c339fb59a3987b216ae3c5c573b95babe6875a1ef56fb178433da", - "urls": [ - "https://github.com/skaji/relocatable-perl/releases/download/5.36.0.1/perl-darwin-arm64.tar.xz" - ] - } - }, - "perl_darwin_amd64": { - "repoRuleId": "@@rules_perl+//perl:repo.bzl%perl_download", - "attributes": { - "strip_prefix": "perl-darwin-amd64", - "sha256": "63bc5ee36f5394d71c50cca6cafdd333ee58f9eaa40bca63c85f9bd06f2c1fd6", - "urls": [ - "https://github.com/skaji/relocatable-perl/releases/download/5.36.0.1/perl-darwin-amd64.tar.xz" - ] - } - }, - "perl_linux_amd64": { - "repoRuleId": "@@rules_perl+//perl:repo.bzl%perl_download", - "attributes": { - "strip_prefix": "perl-linux-amd64", - "sha256": "3bdffa9d7a3f97c0207314637b260ba5115b1d0829f97e3e2e301191a4d4d076", - "urls": [ - "https://github.com/skaji/relocatable-perl/releases/download/5.36.0.1/perl-linux-amd64.tar.xz" - ] - } - }, - "perl_linux_arm64": { - "repoRuleId": "@@rules_perl+//perl:repo.bzl%perl_download", - "attributes": { - "strip_prefix": "perl-linux-arm64", - "sha256": "6fa4ece99e790ecbc2861f6ecb7b52694c01c2eeb215b4370f16a3b12d952117", - "urls": [ - "https://github.com/skaji/relocatable-perl/releases/download/5.36.0.1/perl-linux-arm64.tar.xz" - ] - } - }, - "perl_windows_x86_64": { - "repoRuleId": "@@rules_perl+//perl:repo.bzl%perl_download", - "attributes": { - "strip_prefix": "", - "sha256": "aeb973da474f14210d3e1a1f942dcf779e2ae7e71e4c535e6c53ebabe632cc98", - "urls": [ - "https://mirror.bazel.build/strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit.zip", - "https://strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit.zip" - ] - } - } - }, - "recordedRepoMappingEntries": [ - [ - "rules_perl+", - "bazel_tools", - "bazel_tools" - ], - [ - "rules_perl+", - "rules_perl", - "rules_perl+" - ] - ] - } - }, "@@rules_python+//python/extensions:config.bzl%config": { "general": { "bzlTransitiveDigest": "EcMcbtKZvYmd5Mi1Fpg4EeBBztLHEE5tjO5tLDBYDuU=", @@ -1617,380 +1253,6 @@ }, "facts": { "@@rules_go+//go:extensions.bzl%go_sdk": { - "1.20.2": { - "darwin_amd64": [ - "go1.20.2.darwin-amd64.tar.gz", - "c93b8ced9517d07e1cd4c362c6e2d5242cb139e29b417a328fbf19aded08764c" - ], - "darwin_arm64": [ - "go1.20.2.darwin-arm64.tar.gz", - "7343c87f19e79c0063532e82e1c4d6f42175a32d99f7a4d15e658e88bf97f885" - ], - "freebsd_386": [ - "go1.20.2.freebsd-386.tar.gz", - "14f9be2004e042b3a64d0facb0c020756a9084a5c7333e33b0752b393b6016ea" - ], - "freebsd_amd64": [ - "go1.20.2.freebsd-amd64.tar.gz", - "b41b67b4f1b56797a7cecf6ee7f47fcf4f93960b2788a3683c07dd009d30b2a4" - ], - "linux_386": [ - "go1.20.2.linux-386.tar.gz", - "ee240ed33ae57504c41f04c12236aeaa17fbeb6ea9fcd096cd9dc7a89d10d4db" - ], - "linux_amd64": [ - "go1.20.2.linux-amd64.tar.gz", - "4eaea32f59cde4dc635fbc42161031d13e1c780b87097f4b4234cfce671f1768" - ], - "linux_arm64": [ - "go1.20.2.linux-arm64.tar.gz", - "78d632915bb75e9a6356a47a42625fd1a785c83a64a643fedd8f61e31b1b3bef" - ], - "linux_armv6l": [ - "go1.20.2.linux-armv6l.tar.gz", - "d79d56bafd6b52b8d8cbe3f8e967caaac5383a23d7a4fa9ac0e89778cd16a076" - ], - "linux_ppc64le": [ - "go1.20.2.linux-ppc64le.tar.gz", - "850564ddb760cb703db63bf20182dc4407abd2ff090a95fa66d6634d172fd095" - ], - "linux_s390x": [ - "go1.20.2.linux-s390x.tar.gz", - "8da24c5c4205fe8115f594237e5db7bcb1d23df67bc1fa9a999954b1976896e8" - ], - "windows_386": [ - "go1.20.2.windows-386.zip", - "31838b291117495bbb93683603e98d5118bfabd2eb318b4d07540bfd524bab86" - ], - "windows_amd64": [ - "go1.20.2.windows-amd64.zip", - "fe439f0e438f7555a7f5f7194ddb6f4a07b0de1fa414385d19f2aeb26d9f43db" - ], - "windows_arm64": [ - "go1.20.2.windows-arm64.zip", - "ac5010c8b8b22849228a8dea698d58b9c7be2195d30c6d778cce0f709858fa64" - ] - }, - "1.22.0": { - "aix_ppc64": [ - "go1.22.0.aix-ppc64.tar.gz", - "190e105fc4133a8b5bb1492f368fa89aa4b729270441120714be7ee82e871ebc" - ], - "darwin_amd64": [ - "go1.22.0.darwin-amd64.tar.gz", - "ebca81df938d2d1047cc992be6c6c759543cf309d401b86af38a6aed3d4090f4" - ], - "darwin_arm64": [ - "go1.22.0.darwin-arm64.tar.gz", - "bf8e388b09134164717cd52d3285a4ab3b68691b80515212da0e9f56f518fb1e" - ], - "dragonfly_amd64": [ - "go1.22.0.dragonfly-amd64.tar.gz", - "357ab446200effa26c73ccaf3e8551426428950bf2490859fb296a09e53228b1" - ], - "freebsd_386": [ - "go1.22.0.freebsd-386.tar.gz", - "b8065da37783e8b9e7086365a54d74537e832c92311b61101a66989ab2458d8e" - ], - "freebsd_amd64": [ - "go1.22.0.freebsd-amd64.tar.gz", - "50f421c7f217083ac94aab1e09400cb9c2fea7d337679ec11f1638a11460da30" - ], - "freebsd_arm64": [ - "go1.22.0.freebsd-arm64.tar.gz", - "e23385e5c640787fa02cd58f2301ea09e162c4d99f8ca9fa6d52766f428a933d" - ], - "freebsd_armv6l": [ - "go1.22.0.freebsd-arm.tar.gz", - "c9c8b305f90903536f4981bad9f029828c2483b3216ca1783777344fbe603f2d" - ], - "freebsd_riscv64": [ - "go1.22.0.freebsd-riscv64.tar.gz", - "c8f94d1de6024546194d58e7b9370dc7ea06176aad94a675b0062c25c40cb645" - ], - "illumos_amd64": [ - "go1.22.0.illumos-amd64.tar.gz", - "d6792f11ad6ee5fc42d2fe51e1f1683471aa2ee4f20e3ad1be22a4afdbd38e7f" - ], - "linux_386": [ - "go1.22.0.linux-386.tar.gz", - "1e209c4abde069067ac9afb341c8003db6a210f8173c77777f02d3a524313da3" - ], - "linux_amd64": [ - "go1.22.0.linux-amd64.tar.gz", - "f6c8a87aa03b92c4b0bf3d558e28ea03006eb29db78917daec5cfb6ec1046265" - ], - "linux_arm64": [ - "go1.22.0.linux-arm64.tar.gz", - "6a63fef0e050146f275bf02a0896badfe77c11b6f05499bb647e7bd613a45a10" - ], - "linux_armv6l": [ - "go1.22.0.linux-armv6l.tar.gz", - "0525f92f79df7ed5877147bce7b955f159f3962711b69faac66bc7121d36dcc4" - ], - "linux_loong64": [ - "go1.22.0.linux-loong64.tar.gz", - "b4b1d425cc113608452a32768469b6e34e538fd072bde9f508a75c8dbbdb843f" - ], - "linux_mips": [ - "go1.22.0.linux-mips.tar.gz", - "ec0f9194df750c78492c02e4c70ffe6f3864f2511e47c894415320db752719f8" - ], - "linux_mips64": [ - "go1.22.0.linux-mips64.tar.gz", - "47e938d215e4968ab42afb7307792e5e64184a717e8f176d0de7c411af96d63d" - ], - "linux_mips64le": [ - "go1.22.0.linux-mips64le.tar.gz", - "c162a1a516b8bc8839fc0c0283ad90f6b511e5aca3da4939ed4800f124b9e72c" - ], - "linux_mipsle": [ - "go1.22.0.linux-mipsle.tar.gz", - "6ce3e9a06e3a8ca0312dc1f85046b6914c19852eba5393c5cbf26acf698f8977" - ], - "linux_ppc64": [ - "go1.22.0.linux-ppc64.tar.gz", - "5ae23bf460731eb078f5700b01a50a303308b9f7174a2994892e28bf061f7c85" - ], - "linux_ppc64le": [ - "go1.22.0.linux-ppc64le.tar.gz", - "0e57f421df9449066f00155ce98a5be93744b3d81b00ee4c2c9b511be2a31d93" - ], - "linux_riscv64": [ - "go1.22.0.linux-riscv64.tar.gz", - "afe9cedcdbd6fdff27c57efd30aa5ce0f666f471fed5fa96cd4fb38d6b577086" - ], - "linux_s390x": [ - "go1.22.0.linux-s390x.tar.gz", - "2e546a3583ba7bd3988f8f476245698f6a93dfa9fe206a8ca8f85c1ceecb2446" - ], - "netbsd_386": [ - "go1.22.0.netbsd-386.tar.gz", - "9b7e2dbd43a169bff18bf533a6c2f462eebe04126ab01c138d0d669c046e1658" - ], - "netbsd_amd64": [ - "go1.22.0.netbsd-amd64.tar.gz", - "b11995c271d2256dfe85cf54882ca3655e18c49c4d7db0502bff9977767894e7" - ], - "netbsd_arm64": [ - "go1.22.0.netbsd-arm64.tar.gz", - "499918ccfedde78264d194244d251bf41d95cf606cc0adad13b36b8103cb974f" - ], - "netbsd_armv6l": [ - "go1.22.0.netbsd-arm.tar.gz", - "b57a3aa9c862300ec0ee8609ce5e0f430f132044f351677fd34711a504081872" - ], - "openbsd_386": [ - "go1.22.0.openbsd-386.tar.gz", - "379e0829104c18a139d78b33378e6bd3ab2e0792f6c43b4c38e7f20d8d77b99d" - ], - "openbsd_amd64": [ - "go1.22.0.openbsd-amd64.tar.gz", - "ceb0c97ffc3bfaf74e1df843cb8571d7fc3173a08432f0f42112495df6a31520" - ], - "openbsd_arm64": [ - "go1.22.0.openbsd-arm64.tar.gz", - "358801cab7122ab50d7d92727644f26d818e9e973403f09e85c2e935a625db75" - ], - "openbsd_armv6l": [ - "go1.22.0.openbsd-arm.tar.gz", - "8af5aea3df539bc95ed412c0a176fe84baced70ea1dd29f4aa82d0e9ce27fd9f" - ], - "plan9_386": [ - "go1.22.0.plan9-386.tar.gz", - "fa42c545c9025c45ca9af176dc13a0f4af0cc26bacff6fcb35bb4a170ac538e8" - ], - "plan9_amd64": [ - "go1.22.0.plan9-amd64.tar.gz", - "d8cf64f37a1dfd8e190c5a303c43ab2d49324868f098d88a3106072d137a5a0b" - ], - "plan9_armv6l": [ - "go1.22.0.plan9-arm.tar.gz", - "86fd6165f0cbb47ad551094f74b3e5a6c5e09de858d8b99de72d978d41be6e2a" - ], - "solaris_amd64": [ - "go1.22.0.solaris-amd64.tar.gz", - "a6c12651768d3a74f16104502b4b7bef513ea6b646d99990a28d934c261d1689" - ], - "windows_386": [ - "go1.22.0.windows-386.zip", - "553d44928509965cbda02a45b35ab01cf8b925534bc526a34e2d9dc7794b57e8" - ], - "windows_amd64": [ - "go1.22.0.windows-amd64.zip", - "78b3158fe3aa358e0b6c9f26ecd338f9a11441e88bc434ae2e9f0ca2b0cc4dd3" - ], - "windows_arm64": [ - "go1.22.0.windows-arm64.zip", - "31a61e41d06a3bb2189a303f5f3e777ca4b454eff439f0a67bc2b166330021f4" - ], - "windows_armv6l": [ - "go1.22.0.windows-arm.zip", - "495c7dfaea4e2bf48643662bb622e4ce6378d6d9840015238ad4b8792b99ddbf" - ] - }, - "1.22.4": { - "aix_ppc64": [ - "go1.22.4.aix-ppc64.tar.gz", - "b9647fa9fc83a0cc5d4f092a19eaeaecf45f063a5aa7d4962fde65aeb7ae6ce1" - ], - "darwin_amd64": [ - "go1.22.4.darwin-amd64.tar.gz", - "c95967f50aa4ace34af0c236cbdb49a9a3e80ee2ad09d85775cb4462a5c19ed3" - ], - "darwin_arm64": [ - "go1.22.4.darwin-arm64.tar.gz", - "242b78dc4c8f3d5435d28a0d2cec9b4c1aa999b601fb8aa59fb4e5a1364bf827" - ], - "dragonfly_amd64": [ - "go1.22.4.dragonfly-amd64.tar.gz", - "f2fbb51af4719d3616efb482d6ed2b96579b474156f85a7ddc6f126764feec4b" - ], - "freebsd_386": [ - "go1.22.4.freebsd-386.tar.gz", - "7c54884bb9f274884651d41e61d1bc12738863ad1497e97ea19ad0e9aa6bf7b5" - ], - "freebsd_amd64": [ - "go1.22.4.freebsd-amd64.tar.gz", - "88d44500e1701dd35797619774d6dd51bf60f45a8338b0a82ddc018e4e63fb78" - ], - "freebsd_arm64": [ - "go1.22.4.freebsd-arm64.tar.gz", - "726dc093cf020277be45debf03c3b02b43c2efb3e2a5d4fba8f52579d65327dc" - ], - "freebsd_armv6l": [ - "go1.22.4.freebsd-arm.tar.gz", - "3d9efe47db142a22679aba46b1772e3900b0d87ae13bd2b3bc80dbf2ac0b2cd6" - ], - "freebsd_riscv64": [ - "go1.22.4.freebsd-riscv64.tar.gz", - "5f6b67e5e32f1d6ccb2d4dcb44934a5e2e870a877ba7443d86ec43cfc28afa71" - ], - "illumos_amd64": [ - "go1.22.4.illumos-amd64.tar.gz", - "d56ecc2f85b6418a21ef83879594d0c42ab4f65391a676bb12254870e6690d63" - ], - "linux_386": [ - "go1.22.4.linux-386.tar.gz", - "47a2a8d249a91eb8605c33bceec63aedda0441a43eac47b4721e3975ff916cec" - ], - "linux_amd64": [ - "go1.22.4.linux-amd64.tar.gz", - "ba79d4526102575196273416239cca418a651e049c2b099f3159db85e7bade7d" - ], - "linux_arm64": [ - "go1.22.4.linux-arm64.tar.gz", - "a8e177c354d2e4a1b61020aca3562e27ea3e8f8247eca3170e3fa1e0c2f9e771" - ], - "linux_armv6l": [ - "go1.22.4.linux-armv6l.tar.gz", - "e2b143fbacbc9cbd448e9ef41ac3981f0488ce849af1cf37e2341d09670661de" - ], - "linux_loong64": [ - "go1.22.4.linux-loong64.tar.gz", - "e2ff9436e4b34bf6926b06d97916e26d67a909a2effec17967245900f0816f1d" - ], - "linux_mips": [ - "go1.22.4.linux-mips.tar.gz", - "73f0dcc60458c4770593b05a7bc01cc0d31fc98f948c0c2334812c7a1f2fc3f1" - ], - "linux_mips64": [ - "go1.22.4.linux-mips64.tar.gz", - "417af97fc2630a647052375768be4c38adcc5af946352ea5b28613ea81ca5d45" - ], - "linux_mips64le": [ - "go1.22.4.linux-mips64le.tar.gz", - "7486e2d7dd8c98eb44df815ace35a7fe7f30b7c02326e3741bd934077508139b" - ], - "linux_mipsle": [ - "go1.22.4.linux-mipsle.tar.gz", - "69479c8aad301e459a8365b40cad1074a0dbba5defb9291669f94809c4c4be6e" - ], - "linux_ppc64": [ - "go1.22.4.linux-ppc64.tar.gz", - "dd238847e65bc3e2745caca475a5db6522a2fcf85cf6c38fc36a06642b19efd7" - ], - "linux_ppc64le": [ - "go1.22.4.linux-ppc64le.tar.gz", - "a3e5834657ef92523f570f798fed42f1f87bc18222a16815ec76b84169649ec4" - ], - "linux_riscv64": [ - "go1.22.4.linux-riscv64.tar.gz", - "56a827ff7dc6245bcd7a1e9288dffaa1d8b0fd7468562264c1523daf3b4f1b4a" - ], - "linux_s390x": [ - "go1.22.4.linux-s390x.tar.gz", - "7590c3e278e2dc6040aae0a39da3ca1eb2e3921673a7304cc34d588c45889eec" - ], - "netbsd_386": [ - "go1.22.4.netbsd-386.tar.gz", - "ddd2eebe34471a2502de6c5dad04ab27c9fc80cbde7a9ad5b3c66ecec4504e1d" - ], - "netbsd_amd64": [ - "go1.22.4.netbsd-amd64.tar.gz", - "33af79f6f935f6fbacc5d23876450b3567b79348fc065beef8e64081127dd234" - ], - "netbsd_arm64": [ - "go1.22.4.netbsd-arm64.tar.gz", - "c9a2971dec9f6d320c6f2b049b2353c6d0a2d35e87b8a4b2d78a2f0d62545f8e" - ], - "netbsd_armv6l": [ - "go1.22.4.netbsd-arm.tar.gz", - "fa3550ebd5375a70b3bcd342b5a71f4bd271dcbbfaf4eabefa2144ab5d8924b6" - ], - "openbsd_386": [ - "go1.22.4.openbsd-386.tar.gz", - "d21af022331bfdc2b5b161d616c3a1a4573d33cf7a30416ee509a8f3641deb47" - ], - "openbsd_amd64": [ - "go1.22.4.openbsd-amd64.tar.gz", - "72c0094c43f7e5722ec49c2a3e9dfa7a1123ac43a5f3a63eecf3e3795d3ff0ae" - ], - "openbsd_arm64": [ - "go1.22.4.openbsd-arm64.tar.gz", - "a7ab8d4e0b02bf06ed144ba42c61c0e93ee00f2b433415dfd4ad4b6e79f31650" - ], - "openbsd_armv6l": [ - "go1.22.4.openbsd-arm.tar.gz", - "1096831ea3c5ea3ca57d14251d9eda3786889531eb40d7d6775dcaa324d4b065" - ], - "openbsd_ppc64": [ - "go1.22.4.openbsd-ppc64.tar.gz", - "9716327c8a628358798898dc5148c49dbbeb5196bf2cbf088e550721a6e4f60b" - ], - "plan9_386": [ - "go1.22.4.plan9-386.tar.gz", - "a8dd4503c95c32a502a616ab78870a19889c9325fe9bd31eb16dd69346e4bfa8" - ], - "plan9_amd64": [ - "go1.22.4.plan9-amd64.tar.gz", - "5423a25808d76fe5aca8607a2e5ac5673abf45446b168cb5e9d8519ee9fe39a1" - ], - "plan9_armv6l": [ - "go1.22.4.plan9-arm.tar.gz", - "6af939ad583f5c85c09c53728ab7d38c3cc2b39167562d6c18a07c5c6608b370" - ], - "solaris_amd64": [ - "go1.22.4.solaris-amd64.tar.gz", - "e8cabe69c03085725afdb32a6f9998191a3e55a747b270d835fd05000d56abba" - ], - "windows_386": [ - "go1.22.4.windows-386.zip", - "aca4e2c37278a10f1c70dd0df142f7d66b50334fcee48978d409202d308d6d25" - ], - "windows_amd64": [ - "go1.22.4.windows-amd64.zip", - "26321c4d945a0035d8a5bc4a1965b0df401ff8ceac66ce2daadabf9030419a98" - ], - "windows_arm64": [ - "go1.22.4.windows-arm64.zip", - "8a2daa9ea28cbdafddc6171aefed384f4e5b6e714fb52116fe9ed25a132f37ed" - ], - "windows_armv6l": [ - "go1.22.4.windows-arm.zip", - "5fcd0671a49cecf39b41021621ee1b6e7aa1370f37122b72e80d4fd4185833b6" - ] - }, "1.23.1": { "aix_ppc64": [ "go1.23.1.aix-ppc64.tar.gz", @@ -2157,168 +1419,6 @@ "64ad0954d2c33f556fb1018d62de091254aa6e3a94f1c8a8b16af0d3701d194e" ] }, - "1.24.0": { - "aix_ppc64": [ - "go1.24.0.aix-ppc64.tar.gz", - "5d04588154d5923bd8e26b76111806340ec55c41af1b05623ea744fcb3d6bc22" - ], - "darwin_amd64": [ - "go1.24.0.darwin-amd64.tar.gz", - "7af054e5088b68c24b3d6e135e5ca8d91bbd5a05cb7f7f0187367b3e6e9e05ee" - ], - "darwin_arm64": [ - "go1.24.0.darwin-arm64.tar.gz", - "fd9cfb5dd6c75a347cfc641a253f0db1cebaca16b0dd37965351c6184ba595e4" - ], - "dragonfly_amd64": [ - "go1.24.0.dragonfly-amd64.tar.gz", - "d0dc34ad86aea746abe245994c68a9e1ad8f46ba8c4af901cd5861a4dd4c21df" - ], - "freebsd_386": [ - "go1.24.0.freebsd-386.tar.gz", - "4ee02b1f3812aff4da79c79464ee4038ca61ad74b3a9619850f30435f81c2536" - ], - "freebsd_amd64": [ - "go1.24.0.freebsd-amd64.tar.gz", - "838191001f9324da904dece35a586a3156d548687db87ac9461aa3d38fc88b09" - ], - "freebsd_arm": [ - "go1.24.0.freebsd-arm.tar.gz", - "ce6ad4e84a40a8a1d848b7e31b0cddfd1cee8f7959e7dc358a8fa8b5566ea718" - ], - "freebsd_arm64": [ - "go1.24.0.freebsd-arm64.tar.gz", - "511f7b0cac4c4ed1066d324072ce223b906ad6b2a85f2e1c5d260eb7d08b5901" - ], - "freebsd_riscv64": [ - "go1.24.0.freebsd-riscv64.tar.gz", - "a1e4072630dc589a2975ef51317b52c7d8599bf6f389fc59033c01e0a0fa705a" - ], - "illumos_amd64": [ - "go1.24.0.illumos-amd64.tar.gz", - "7593e9dcee9f07c3df6d099f7d259f5734a6c0dccc5f28962f18e7f501c9bb21" - ], - "linux_386": [ - "go1.24.0.linux-386.tar.gz", - "90521453a59c6ce20364d2dc7c38532949b033b602ba12d782caeb90af1b0624" - ], - "linux_amd64": [ - "go1.24.0.linux-amd64.tar.gz", - "dea9ca38a0b852a74e81c26134671af7c0fbe65d81b0dc1c5bfe22cf7d4c8858" - ], - "linux_arm64": [ - "go1.24.0.linux-arm64.tar.gz", - "c3fa6d16ffa261091a5617145553c71d21435ce547e44cc6dfb7470865527cc7" - ], - "linux_armv6l": [ - "go1.24.0.linux-armv6l.tar.gz", - "695dc54fa14cd3124fa6900d7b5ae39eeac23f7a4ecea81656070160fac2c54a" - ], - "linux_loong64": [ - "go1.24.0.linux-loong64.tar.gz", - "a201e4c9b7e6d29ed64c43296ed88e81a66f82f2093ce45b766d2c526941396f" - ], - "linux_mips": [ - "go1.24.0.linux-mips.tar.gz", - "f3ac039aae78ad0bfb08106406c2e62eaf763dd82ebaf0ecd539adadd1d729a6" - ], - "linux_mips64": [ - "go1.24.0.linux-mips64.tar.gz", - "f2e6456d45e024831b1da8d88b1bb6392cca9500c1b00841f525d76c9e9553e0" - ], - "linux_mips64le": [ - "go1.24.0.linux-mips64le.tar.gz", - "b847893ff119389c939adc2b8516b6500204b7cb49d5e19b25e1c2091d2c74c6" - ], - "linux_mipsle": [ - "go1.24.0.linux-mipsle.tar.gz", - "bd4aed27d02746c237c3921e97029ac6b6fe687a67436b8f52ff1f698d330bd9" - ], - "linux_ppc64": [ - "go1.24.0.linux-ppc64.tar.gz", - "007123c9b06c41729a4bb3f166f4df7196adf4e33c2d2ab0e7e990175f0ce1d4" - ], - "linux_ppc64le": [ - "go1.24.0.linux-ppc64le.tar.gz", - "a871a43de7d26c91dd90cb6e0adacb214c9e35ee2188c617c91c08c017efe81a" - ], - "linux_riscv64": [ - "go1.24.0.linux-riscv64.tar.gz", - "620dcf48c6297519aad6c81f8e344926dc0ab09a2a79f1e306964aece95a553d" - ], - "linux_s390x": [ - "go1.24.0.linux-s390x.tar.gz", - "544d78b077c6b54bf78958c4a8285abec2d21f668fb007261c77418cd2edbb46" - ], - "netbsd_386": [ - "go1.24.0.netbsd-386.tar.gz", - "8b143a7edefbaa2a0b0246c9df2df1bac9fbed909d8615a375c08da7744e697d" - ], - "netbsd_amd64": [ - "go1.24.0.netbsd-amd64.tar.gz", - "67150a6dd7bdb9c4e88d77f46ee8c4dc99d5e71deca4912d8c2c85f7a16d0262" - ], - "netbsd_arm": [ - "go1.24.0.netbsd-arm.tar.gz", - "446b2539f11218fd6f6f6e3dd90b20ae55a06afe129885eeb3df51eb344eb0f6" - ], - "netbsd_arm64": [ - "go1.24.0.netbsd-arm64.tar.gz", - "370115b6ff7d30b29431223de348eb11ab65e3c92627532d97fd55f63f94e7a8" - ], - "openbsd_386": [ - "go1.24.0.openbsd-386.tar.gz", - "cbda5f15f06ed9630f122a53542d9de13d149643633c74f1dcb45e79649b788a" - ], - "openbsd_amd64": [ - "go1.24.0.openbsd-amd64.tar.gz", - "926f601d0e655ab1e8d7f357fd82542e5cf206c38c4e2f9fccf0706987d38836" - ], - "openbsd_arm": [ - "go1.24.0.openbsd-arm.tar.gz", - "8a54892f8c933c541fff144a825d0fdc41bae14b0832aab703cb75eb4cb64f2c" - ], - "openbsd_arm64": [ - "go1.24.0.openbsd-arm64.tar.gz", - "ef7fddcef0a22c7900c178b7687cf5aa25c2a9d46a3cc330b77a6de6e6c2396b" - ], - "openbsd_ppc64": [ - "go1.24.0.openbsd-ppc64.tar.gz", - "b3b5e2e2b53489ded2c2c21900ddcbbdb7991632bb5b42f05f125d71675e0b76" - ], - "openbsd_riscv64": [ - "go1.24.0.openbsd-riscv64.tar.gz", - "fbcb1dbf1269b4079dc4fd0b15f3274b9d635f1a7e319c3fc1a907b03280348e" - ], - "plan9_386": [ - "go1.24.0.plan9-386.tar.gz", - "33b4221e1c174a16e3f661deab6c60838ac4ae6cb869a4da1d1115773ceed88b" - ], - "plan9_amd64": [ - "go1.24.0.plan9-amd64.tar.gz", - "111a89014019cdbd69c2978de9b3e201f77e35183c8ab3606fba339d38f28549" - ], - "plan9_arm": [ - "go1.24.0.plan9-arm.tar.gz", - "8da3d3997049f40ebe0cd336a9bb9e4bfa4832df3c90a32f07383371d6d74849" - ], - "solaris_amd64": [ - "go1.24.0.solaris-amd64.tar.gz", - "b6069da21dc95ccdbd047675b584e5480ffc3eba35f9e7c8b0e7b317aaf01e2c" - ], - "windows_386": [ - "go1.24.0.windows-386.zip", - "b53c28a4c2863ec50ab4a1dbebe818ef6177f86773b6f43475d40a5d9aa4ec9e" - ], - "windows_amd64": [ - "go1.24.0.windows-amd64.zip", - "96b7280979205813759ee6947be7e3bb497da85c482711116c00522e3bb41ff1" - ], - "windows_arm64": [ - "go1.24.0.windows-arm64.zip", - "53f73450fb66075d16be9f206e9177bd972b528168271918c4747903b5596c3d" - ] - }, "1.25.0": { "aix_ppc64": [ "go1.25.0.aix-ppc64.tar.gz", diff --git a/Makefile b/Makefile index c233d660..ea0fbb36 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,6 @@ release_source_archive: .github/workflows/pack_release_archive.sh archives/release.tar.gz -.PHONY: release_deploy_jar -release_deploy_jar: - bazel \ - build \ - //cli:bazel-diff_deploy.jar \ - -c opt - # Builds the same artifact CI publishes, named the same way: # bazel-bin/release/bazel-diff-rust--[.exe]. .PHONY: release_rust_binary @@ -41,23 +34,21 @@ release_rust_binary_linux_arm64: //release:bazel-diff-rust \ --config=release-musl-arm64 -.PHONY: build_rust -build_rust: - bazel build //:bazel-diff-rust -c opt +.PHONY: build +build: + bazel build //:bazel-diff -c opt -# Both go through Bazel so they use the same formatters CI gates on. `cargo fmt -# --all` is not equivalent: it only sees the root crate, missing tools/coverage, -# and it uses whatever rustfmt is on PATH rather than the pinned one. +# Goes through Bazel so it uses the same rustfmt CI gates on. `cargo fmt --all` +# is not equivalent: it only sees the root crate, missing tools/coverage, and +# it uses whatever rustfmt is on PATH rather than the pinned one. .PHONY: format format: - bazel run //cli/format - bazel run //cli/format:rustfmt + bazel run //tools/format:rustfmt # Regenerates the per-case e2e test targets from the e2e sources. Run it after -# adding, renaming or removing a `@Test` method under -# cli/src/test/kotlin/com/bazel_diff/e2e/ or a `#[test]` fn under tests/e2e/, -# and commit the result -- `e2e-split-regen` in ci.yaml fails the build if the -# checked-in split is stale. See tools/e2e/README.md. +# adding, renaming or removing a `#[test]` fn under tests/e2e/, and commit the +# result -- `e2e-split-regen` in ci.yaml fails the build if the checked-in +# split is stale. See tools/e2e/README.md. .PHONY: regen-e2e regen-e2e: bazel run //tools/e2e:regen @@ -71,9 +62,11 @@ regen-e2e-check: generate-readme: bazel run //tools:generate-readme +COVERAGE_TARGETS = //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/... + .PHONY: coverage coverage: - bazel coverage --combined_report=lcov //cli/... //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/... + bazel coverage --combined_report=lcov $(COVERAGE_TARGETS) bazel run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat bazel run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat @@ -88,60 +81,6 @@ coverage-test: .PHONY: coverage-html coverage-html: - bazel coverage --combined_report=lcov //cli/... //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/... + bazel coverage --combined_report=lcov $(COVERAGE_TARGETS) bazel run //tools:coverage-check -- bazel-out/_coverage/_coverage_report.dat --html coverage-html @echo "Open coverage-html/index.html in a browser to inspect." - -.PHONY: coverage_rust -coverage_rust: - bazel coverage //src:cli_tests //src:rust_tests - -.PHONY: benchmark -benchmark: - @test -n "$(WORKSPACE)" || (echo "usage: make benchmark WORKSPACE=/path/to/bazel [BAZEL=/path/to/bazelisk] [HYPERFINE=/path/to/hyperfine] [STREAMED_PROTO=/path/to/targets.pb] [INCLUDE_BAZEL=1] [ITERATIONS=10] [WARMUP=3] [RSS_RUNS=5] [JSON=benchmark.json]" >&2; exit 2) - $(or $(BAZEL),bazel) run -c opt //tools:benchmark -- \ - --workspace "$(WORKSPACE)" \ - --bazel "$(or $(BAZEL),bazel)" \ - --hyperfine "$(or $(HYPERFINE),hyperfine)" \ - --iterations "$(or $(ITERATIONS),10)" \ - --warmup "$(or $(WARMUP),3)" \ - --rss-runs "$(or $(RSS_RUNS),5)" \ - $(if $(STREAMED_PROTO),--streamed-proto "$(STREAMED_PROTO)",) \ - $(if $(INCLUDE_BAZEL),--include-bazel,) \ - $(if $(JSON),--json "$(JSON)",) - -# Hermetic Kotlin-vs-Rust performance gate. Unlike `make benchmark` this needs no -# workspace, no Bazel server and no Hyperfine: it generates its own fixtures and fails -# (exit 1) if Rust is not faster than Kotlin on every workload. JSON=... must be an -# absolute path -- `bazel run` executes from the runfiles tree, not the repo root. -.PHONY: perf-gate -perf-gate: - $(or $(BAZEL),bazel) run -c opt //tools:perf-gate -- \ - --rounds "$(or $(ROUNDS),5)" \ - --warmup-rounds "$(or $(WARMUP),1)" \ - --scale "$(or $(SCALE),1)" \ - $(if $(WORKLOAD),--workload "$(WORKLOAD)",) \ - $(if $(RSS_RUNS),--rss-runs "$(RSS_RUNS)",) \ - $(if $(JSON),--json "$(JSON)",) - -# The gate against the statically-linked musl binary that actually ships for Linux, not the -# host glibc build `perf-gate` uses. musl's single-arena malloc serializes the parallel proto -# decode, so this exposes allocator-scalability regressions -- but only on a many-core host -# (>= 8 cores); on a 2-core machine there is too little contention and it passes regardless. -# Defaults to the allocator-sensitive workload; override with WORKLOAD= to run others. -.PHONY: perf-gate-musl -perf-gate-musl: - $(or $(BAZEL),bazel) build //cli:bazel-diff -c opt - $(or $(BAZEL),bazel) build //release:bazel-diff-rust --config=release-musl - python3 tools/perf_gate.py \ - --kotlin-binary bazel-bin/cli/bazel-diff \ - --rust-binary bazel-bin/release/bazel-diff-rust-linux-amd64 \ - --workload "$(or $(WORKLOAD),generate-hashes-dense)" \ - --rounds "$(or $(ROUNDS),7)" \ - --warmup-rounds "$(or $(WARMUP),2)" \ - --scale "$(or $(SCALE),1)" \ - $(if $(JSON),--json "$(JSON)",) - -.PHONY: perf-gate-test -perf-gate-test: - $(or $(BAZEL),bazel) test //tools:perf_gate_test diff --git a/README.md b/README.md index c55176c4..eebedd05 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ `bazel-diff` offers several key advantages over rolling your own target diffing solution -1. `bazel-diff` is designed for very large Bazel projects. We use Java Protobuf's `parseDelimitedFrom` method alongside Bazel Query's `streamed_proto` output option. These two together allow you to parse Gigabyte or larger protobuf messages. We have tested it with projects containing tens of thousands of targets. +1. `bazel-diff` is designed for very large Bazel projects. We stream Bazel Query's `streamed_proto` output message by message instead of loading it whole, which allows you to parse Gigabyte or larger protobuf outputs. We have tested it with projects containing hundreds of thousands of targets. 2. We avoid usage of large command line query lists when interacting with Bazel, [issue here](https://github.com/bazelbuild/bazel/issues/8609). When you interact with Bazel with thousands of query parameters you can reach an upper maximum limit, seeing this error `bash: /usr/local/bin/bazel: Argument list too long`. `bazel-diff` is smart enough to avoid these errors. 3. `bazel-diff` has been tested with file renames, deletions, and modifications. Works on `bzl` files, `WORKSPACE` files, `BUILD` files and regular files @@ -25,8 +25,7 @@ This approach was inspired by the [following BazelConf talk](https://www.youtube ## Prerequisites * Git -* Bazel 3.3.0 or higher -* Java 8 JDK or higher (Bazel requires this) +* Bazel 7 or higher (Bazel itself needs a JDK; `bazel-diff` is a single static binary and does not) ## Getting Started @@ -391,460 +390,250 @@ Notes and operational guidance: `bazel-diff` Command ```terminal -Usage: bazel-diff [-hvV] [COMMAND] -Writes to a file the impacted targets between two Bazel graph JSON files - -h, --help Show this help message and exit. - -v, --verbose Display query string, missing files and elapsed time - -V, --version Print version information and exit. +Writes impacted targets between two Bazel graph hash files + +Usage: bazel-diff [OPTIONS] + Commands: - generate-hashes Writes to a file the SHA256 hashes for each Bazel - Target in the provided workspace. - get-impacted-targets Command-line utility to analyze the state of the bazel - build graph - explain Explains why a target was impacted: the upstream target - (s) whose own hash changed, and the dependency path - from each down to the queried target. Renders as - text, JSON, Graphviz DOT, or a Mermaid flowchart. - warmup Record-side entrypoint for Firecracker snapshots: runs - generate-hashes for the base revision, writes base - hashes + fingerprint to known paths, and exits 0 only - once the Bazel server is warm (the host's 'safe to - snapshot' signal). - fingerprint Computes the snapshot cache key over the inputs that - affect the build graph (bazel version, MODULE.bazel. - lock, .bazelrc, bazel-diff version, flag set) and - writes it as JSON. Used to decide whether a - Firecracker snapshot is safe to consume. - serve Runs bazel-diff as a long-running HTTP query service - that returns the impacted targets between two git - revisions, caching generated hashes per commit SHA. + generate-hashes Write canonical hashes for Bazel targets in a workspace + get-impacted-targets Compare two hash files and report impacted targets + explain Explain why a target was impacted: the upstream target(s) whose own hash changed, and the dependency path from each down to the queried target. Renders as text, JSON, Graphviz DOT, or a Mermaid flowchart. + warmup Warm Bazel and write snapshot hashes and fingerprint metadata + fingerprint Compute the snapshot/cache fingerprint for the current workspace + serve Run the HTTP impacted-target query service + help Print this message or the help of the given subcommand(s) + +Options: + -v, --verbose + -h, --help Print help + -V, --version Print version ``` ### `generate-hashes` command ```terminal -Usage: bazel-diff generate-hashes [-hkvV] [--[no-]excludeExternalTargets] [-- - [no-]includeTargetType] [--[no-]useCquery] - [-b=] - [--contentHashPath=] - [--cqueryExpression=] - [-d=] - [--excludeTargetsQuery=] - [--fineGrainedHashExternalReposFile=] - [-m=] [-s=] - -w= - [--alwaysAffectedTags=]... - [-co=]... - [--cqueryCommandOptions= - ]... - [--fineGrainedHashExternalRepos=]... - [--ignoredRuleHashingAttributes=]... - [-so=]... - [-tt=[,...]]... - -Writes to a file the SHA256 hashes for each Bazel Target in the provided -workspace. - The filepath to write the resulting JSON of - dictionary target => SHA-256 values. If not - specified, the JSON will be written to STDOUT. - --alwaysAffectedTags= - Comma separated list of Bazel target tags (e.g. - `external`). Any target whose `tags` attribute - contains one of these values is always reported as - impacted: a per-invocation sentinel is mixed into - its hash so a diff of two `generate-hashes` runs - always marks it changed. Use this for non-hermetic - targets that read undeclared workspace state at - execution time (e.g. repo-scanning linters such as - buildifier/gofmt/eslint tests) which would - otherwise hash as unchanged and be wrongly skipped - by target determination. This is the Bazel target - `external` *tag* and is unrelated to - --excludeExternalTargets / - --fineGrainedHashExternalRepos, which concern - external *repositories*. - -b, --bazelPath= - Path to Bazel binary. If not specified, the Bazel - binary available in PATH will be used. - -co, --bazelCommandOptions= - Additional space separated Bazel command options used - when invoking `bazel query` - --contentHashPath= - Path to content hash json file. It's a map which maps - a workspace-relative file path (and - `external//` for fine-grained - external repo files) to its content hash. Files in - this map will skip content hashing and use provided - value - --cqueryCommandOptions= - Additional space separated Bazel command options used - when invoking `bazel cquery`. This flag is has no - effect if `--useCquery`is false. - --cqueryExpression= - Custom cquery expression to use instead of the - default 'deps(//...:all-targets)'. This allows you - to exclude problematic targets (e.g., analysis_test - targets that are designed to fail). Example: 'deps - (//...:all-targets) except //path/to/failing: - target'. This flag has no effect if `--useCquery` - is false. - -d, --depEdgesFile= - Path to the file where dependency edges are written - to. If not specified, the dependency edges will not - be written to a file. Needed for computing build - graph distance metrics. See bazel-diff docs for - more details about build graph distance metrics. - --[no-]excludeExternalTargets - If true, exclude external targets (do not query - //external:all-targets). When Bzlmod is enabled - (detected via bazel mod graph), external targets - are excluded automatically. Set this when using - Bazel with --enable_workspace=false in other - configurations. Defaults to false. - --excludeTargetsQuery= - A Bazel query expression whose matched targets are - excluded from the generated hashes via the `except` - operator. Applied to the main target universe for - both `query` and `cquery`. Use this to drop targets - you never want reported as impacted, e.g. - `manual`-tagged targets: --excludeTargetsQuery='attr - ("tags", "[\[ ]manual[,\]]", //...)'. Excluded - targets are absent from hashing, so a kept target - that depended on one no longer tracks changes to it. - --fineGrainedHashExternalRepos= - Comma separate list of external repos in which - fine-grained hashes are computed for the targets. - By default, external repos are treated as an opaque - blob. If an external repo is specified here, - bazel-diff instead computes the hash for individual - targets. For example, one wants to specify `maven` - here if they user rules_jvm_external so that - individual third party dependency change won't - invalidate all targets in the mono repo. - --fineGrainedHashExternalReposFile= - A text file containing a newline separated list of - external repos. Similar to - --fineGrainedHashExternalRepos but helps you avoid - exceeding max arg length. Mutually exclusive with - --fineGrainedHashExternalRepos. - -h, --help Show this help message and exit. - --ignoredRuleHashingAttributes= - Attributes that should be ignored when hashing rule - targets. - --[no-]includeTargetType - Whether include target type in the generated JSON or - not. - If false, the generate JSON schema is: {"": - ""} - If true, the generate JSON schema is: {"": - "#" } - -k, --[no-]keep_going This flag controls if `bazel query` will be executed - with the `--keep_going` flag or not. Enabling this - flag lets `bazel query` tolerate failures in your - Bazel graph, but may silently drop targets that - fail to resolve and produce non-deterministic - hashes. Disabling it catches configuration issues - by failing loudly. Defaults to `false` - -m, --modified-filepaths= - A text file containing a newline separated list of - filepaths (relative to the workspace) these - filepaths should represent the modified files - between the specified revisions and will be used to - scope what files are hashed during hash generation. - -s, --seed-filepaths= - A text file containing a newline separated list of - filepaths. Each file in this list will be read and - its content will be used as a SHA256 seed when - determining affected targets in the build graph. - Invalidating any of these files will effectively - mark all targets as affected. - -so, --bazelStartupOptions= - Additional space separated Bazel client startup - options used when invoking Bazel - -tt, --targetType=[,...] - The types of targets to filter. Use comma (,) to - separate multiple values, e.g. - '--targetType=SourceFile,Rule,GeneratedFile'. - --[no-]useCquery If true, use cquery instead of query when generating - dependency graphs. Using cquery would yield more - accurate build graph at the cost of slower query - execution. When this is set, one usually also wants - to set `--cqueryCommandOptions` to specify a - targeting platform. Note that this flag only works - with Bazel 6.2.0 or above because lower versions - does not support `--query_file` flag. - -v, --verbose Display query string, missing files and elapsed time - -V, --version Print version information and exit. - -w, --workspacePath= - Path to Bazel workspace directory. +Write canonical hashes for Bazel targets in a workspace + +Usage: bazel-diff generate-hashes [OPTIONS] --workspacePath [OUTPUT_PATH] + +Arguments: + [OUTPUT_PATH] + +Options: + -v, --verbose + + -w, --workspacePath + Path to the Bazel workspace + -b, --bazelPath + Path to the Bazel or Bazelisk executable [default: bazel] + -s, --seed-filepaths + File containing workspace-relative paths whose contents seed every target hash + --bazelStartupOptions + Additional space-separated Bazel startup options + --bazelCommandOptions + Additional space-separated `bazel query` options + --cqueryCommandOptions + Additional space-separated `bazel cquery` options + --fineGrainedHashExternalRepos + + --fineGrainedHashExternalReposFile + + --useCquery[=] + [default: false] [possible values: true, false] + --cqueryExpression + + -k, --keep_going[=] + [default: false] [possible values: true, false] + --ignoredRuleHashingAttributes + + --excludeExternalTargets[=] + [default: false] [possible values: true, false] + --excludeTargetsQuery + + --alwaysAffectedTags + + --contentHashPath + + --includeTargetType[=] + [default: false] [possible values: true, false] + --targetType + + -d, --depEdgesFile + + -m, --modified-filepaths + + -h, --help + Print help + -V, --version + Print version ``` ### `get-impacted-targets` command ```terminal -Missing required options: '--startingHashes=', '--finalHashes=', '--workspacePath=' -Usage: bazel-diff get-impacted-targets [-v] [--[no-]excludeExternalTargets] [-- - [no-]noBazelrc] [-b=] - [-d=] - -fh= - [-o=] - -sh= - -w= - [-so=]... - [-tt=[,...]]... -Command-line utility to analyze the state of the bazel build graph - -b, --bazelPath= - Path to Bazel binary. If not specified, the Bazel - binary available in PATH will be used. - -d, --depEdgesFile= - Path to the file where dependency edges are. If - specified, build graph distance metrics will be - computed from the given hash data. - --[no-]excludeExternalTargets - If true, drop labels starting with '//external:' from - the impacted-targets output. These synthetic labels - are produced for bzlmod-managed external repos so - generate-hashes can detect dep changes, but they are - not buildable in bzlmod-only mode (Bazel 8.6.0+ with - --enable_workspace=false) and will fail downstream - `bazel build`. See https://github. - com/Tinder/bazel-diff/issues/326. When unset, - defaults to true if Bzlmod is detected (via `bazel - mod graph`), false otherwise. - -fh, --finalHashes= - The path to the JSON file of target hashes for the - final revision. Run 'generate-hashes' to get this - value. - --[no-]noBazelrc Don't use .bazelrc - -o, --output= - Filepath to write the impacted Bazel targets to. If - using depEdgesFile: formatted in json, otherwise: - newline separated. If not specified, the output will - be written to STDOUT. - -sh, --startingHashes= - The path to the JSON file of target hashes for the - initial revision. Run 'generate-hashes' to get this - value. - -so, --bazelStartupOptions= - Additional space separated Bazel client startup - options used when invoking Bazel - -tt, --targetType=[,...] - The types of targets to filter. Use comma (,) to - separate multiple values, e.g. - '--targetType=SourceFile,Rule,GeneratedFile'. - -v, --verbose Display query string, missing files and elapsed time - -w, --workspacePath= - Path to Bazel workspace directory. Required for module - change detection. +Compare two hash files and report impacted targets + +Usage: bazel-diff get-impacted-targets [OPTIONS] --startingHashes --finalHashes --workspacePath + +Options: + --startingHashes + + -v, --verbose + + --finalHashes + + -d, --depEdgesFile + + --targetType + + -o, --output + + -w, --workspacePath + + -b, --bazelPath + [default: bazel] + --bazelStartupOptions + + --noBazelrc[=] + [default: false] [possible values: true, false] + --excludeExternalTargets[=] + [possible values: true, false] + -h, --help + Print help + -V, --version + Print version ``` ### `explain` command ```terminal -Usage: bazel-diff explain [-hvV] -d= [-f=] - -fh= [--maxDepth=] - [--maxRootCauses=] [-o=] - -sh= -t= -Explains why a target was impacted: the upstream target(s) whose own hash -changed, and the dependency path from each down to the queried target. Renders -as text, JSON, Graphviz DOT, or a Mermaid flowchart. - -d, --depEdgesFile= - Path to the dependency-edges file written by - 'generate-hashes --depEdgesFile'. Required: - attribution is a walk over these edges. - -f, --format= Output format: TEXT, JSON, DOT, MERMAID. 'dot' and - 'mermaid' emit a node-link graph of the blame - subgraph, with edges pointing from root cause to - queried target. - -fh, --finalHashes= - The path to the JSON file of target hashes for the - final revision. Run 'generate-hashes' to get this - value. - -h, --help Show this help message and exit. - --maxDepth= - Stop searching this many dependency hops above the - queried target. Root causes further upstream are - then not reported (a warning is logged). -1 means - no bound. Default: -1. - --maxRootCauses= - Report at most this many root causes, nearest first. - The full count is always reported alongside. 0 - means no limit. Default: 25. - -o, --output= - Filepath to write the explanation to. Defaults to - STDOUT. - -sh, --startingHashes= - The path to the JSON file of target hashes for the - initial revision. Run 'generate-hashes' to get this - value. - -t, --target= The impacted Bazel label to explain, e.g. - '//service/a:app'. - -v, --verbose Display query string, missing files and elapsed time - -V, --version Print version information and exit. +Explain why a target was impacted: the upstream target(s) whose own hash changed, and the dependency path from each down to the queried target. Renders as text, JSON, Graphviz DOT, or a Mermaid flowchart. + +Usage: bazel-diff explain [OPTIONS] --startingHashes --finalHashes --depEdgesFile --target + +Options: + --startingHashes + The JSON file of target hashes for the initial revision, from `generate-hashes` + + -v, --verbose + + + --finalHashes + The JSON file of target hashes for the final revision, from `generate-hashes` + + -d, --depEdgesFile + The dependency-edges file written by `generate-hashes --depEdgesFile`. Required: attribution is a walk over these edges + + -t, --target + The impacted Bazel label to explain, e.g. '//service/a:app' + + -f, --format + Output format + + Possible values: + - text + - json + - dot: A Graphviz node-link graph of the blame subgraph, edges from root cause to queried target + - mermaid: A Mermaid `flowchart TD` of the same graph + + [default: text] + + -o, --output + Filepath to write the explanation to. Defaults to STDOUT + + --maxRootCauses + Report at most this many root causes, nearest first. The full count is always reported alongside. 0 means no limit + + [default: 25] + + --maxDepth + Stop searching this many dependency hops above the queried target. Root causes further upstream are then not reported (a warning is printed). -1 means no bound + + [default: -1] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version ``` ### `serve` command ```terminal -Usage: bazel-diff serve [-hkvV] [--[no-]excludeExternalTargets] - [--no-initial-fetch] [--[no-]s3ForcePathStyle] [--[no-] - trackDeps] [--[no-]useCquery] [-b=] - --cacheDir= [--cacheMaxAge=] - [--cacheMaxEntries=] - [--cacheMaxSize=] - [--cachePruneInterval=] - [--cqueryExpression=] - [--excludeTargetsQuery=] - [--fineGrainedHashExternalReposFile=] [--gitPath=] [--port=] - [--portFile=] - [--requestTimeout=] - [-s=] [--s3Bucket=] - [--s3Endpoint=] [--s3Prefix=] - [--s3Region=] -w= - [-co=]... - [--cqueryCommandOptions=]... - [--fineGrainedHashExternalRepos=]... - [--ignoredRuleHashingAttributes=]... [-so=]... - [--warmupRevision=]... -Runs bazel-diff as a long-running HTTP query service that returns the impacted -targets between two git revisions, caching generated hashes per commit SHA. - -b, --bazelPath= - Path to Bazel binary. If not specified, the Bazel - binary available in PATH will be used. - --cacheDir= Directory where generated hashes are cached per - commit SHA. Persists across restarts. - --cacheMaxAge= - Evict cached hashes not read or written within this - window, so the cache does not grow without bound - over time. Duration like 7d, 36h, 90m (units - d/h/m/s). Unset means no age limit. Enforced by a - background sweeper (see --cachePruneInterval). - --cacheMaxEntries= - Keep at most this many cached commit-SHA entries, - evicting the least-recently-used first. Unset - means no count limit. - --cacheMaxSize= - Keep the cache's total on-disk size at or below - this, evicting the least-recently-used entries - first. Size like 10GB, 500MB, or a bare byte - count (base 1024). Unset means no size limit. - --cachePruneInterval= - How often the background sweeper enforces the - --cacheMax* limits. Duration like 1h, 30m. - Defaults to 1h. No effect unless a --cacheMax* - limit is set. - -co, --bazelCommandOptions= - Additional space separated Bazel command options - used when invoking `bazel query` - --cqueryCommandOptions= - Additional space separated Bazel command options - used when invoking `bazel cquery`. No effect - unless --useCquery is set. - --cqueryExpression= - Custom cquery expression to use instead of the - default. No effect unless --useCquery. - --[no-]excludeExternalTargets - If true, exclude external targets (do not query - //external:all-targets). - --excludeTargetsQuery= - A Bazel query expression whose matched targets are - excluded from the served hashes via the `except` - operator, e.g. `manual`-tagged targets: - --excludeTargetsQuery='attr("tags", "[\[ ]manual[, - \]]", //...)'. - --fineGrainedHashExternalRepos= - Comma separated list of external repos for which - fine-grained hashes are computed. - --fineGrainedHashExternalReposFile= - A text file with a newline separated list of - external repos. Mutually exclusive with - --fineGrainedHashExternalRepos. - --gitPath= Path to the git binary used for fetch/checkout - operations. Defaults to 'git' on the PATH. - -h, --help Show this help message and exit. - --ignoredRuleHashingAttributes= - Attributes that should be ignored when hashing rule - targets. - -k, --[no-]keep_going Run `bazel query` with --keep_going. Defaults to - false. - --no-initial-fetch Skip the initial 'git fetch' before reporting - healthy. Useful for local/offline testing. - --port= Port to listen on. Defaults to 8080. Pass 0 to bind - any free port, and --portFile to find out which - one. - --portFile= Write the bound port to this file once the server - is listening. The point is '--port 0 --portFile - ': a caller that picks a port itself has to - open a socket to find a free one and close it - again before this process can bind, and anything - else on the machine can take the port in between. - Binding 0 and reporting back has no such window. - The file appears only after the bind succeeds, so - waiting for it is also waiting for the listener. - --requestTimeout= - Maximum seconds an /impacted_targets - (_with_distances) or /dependency_edges request - may run before the server abandons it and - responds 504. 0 (the default) means no timeout. - This bounds the request the client waits on; an - in-flight bazel query may keep running in the - background and still populate the per-SHA cache. - -s, --seed-filepaths= - A text file with a newline separated list of - filepaths used as a SHA256 seed for all targets. - --s3Bucket= S3 bucket used as a shared hash cache behind the - local --cacheDir tier. Generated hashes are - published to the bucket and local cache misses - fall back to it, so replicas behind a load - balancer share one cache. Credentials and region - come from the AWS default provider chains (env - vars, profile, IRSA, IMDS). Unset (the default) - means local-disk caching only. - --s3Endpoint= - Custom S3 endpoint URL for S3-compatible stores - (MinIO, LocalStack). Usually combined with - --s3ForcePathStyle. - --[no-]s3ForcePathStyle - Use path-style S3 addressing (bucket in the URL - path, not the hostname), required by most - S3-compatible stores. Defaults to false. - --s3Prefix= Key prefix for cache objects in --s3Bucket, e.g. - 'bazel-diff/my-repo'. Defaults to no prefix - (objects at the bucket root). - --s3Region= AWS region of --s3Bucket. Defaults to the SDK's - default region chain (env vars, profile, IMDS). - -so, --bazelStartupOptions= - Additional space separated Bazel client startup - options used when invoking Bazel - --[no-]trackDeps Track dependency edges and persist them per commit - SHA so build-graph distance metrics can be served - via /impacted_targets_with_distances and the - generate-hashes graph via /dependency_edges. - Increases cache size and memory. Defaults to - false. - --[no-]useCquery If true, use cquery instead of query when - generating dependency graphs. - -v, --verbose Display query string, missing files and elapsed time - -V, --version Print version information and exit. - -w, --workspacePath= - Path to the Bazel workspace git clone the service - checks out and queries. - --warmupRevision= - Comma separated git revisions (branch/tag/SHA) - whose hashes are generated and cached at startup, - before the server reports healthy, so the first - real request is warm and the Bazel analysis - server is primed. Best-effort: a revision that - fails to warm is logged and the server still - becomes ready (serving it cold on demand). - Increases time-to-healthy, so size - deploy/health-check timeouts accordingly. +Run the HTTP impacted-target query service + +Usage: bazel-diff serve [OPTIONS] --workspacePath --cacheDir + +Options: + -v, --verbose + + -w, --workspacePath + Path to the Bazel workspace + -b, --bazelPath + Path to the Bazel or Bazelisk executable [default: bazel] + -s, --seed-filepaths + File containing workspace-relative paths whose contents seed every target hash + --bazelStartupOptions + Additional space-separated Bazel startup options + --bazelCommandOptions + Additional space-separated `bazel query` options + --cqueryCommandOptions + Additional space-separated `bazel cquery` options + --fineGrainedHashExternalRepos + + --fineGrainedHashExternalReposFile + + --useCquery[=] + [default: false] [possible values: true, false] + --cqueryExpression + + -k, --keep_going[=] + [default: false] [possible values: true, false] + --ignoredRuleHashingAttributes + + --excludeExternalTargets[=] + [default: false] [possible values: true, false] + --excludeTargetsQuery + + --alwaysAffectedTags + + --gitPath + [default: git] + --port + [default: 8080] + --requestTimeout + [default: 0] + --cacheDir + + --trackDeps[=] + [default: false] [possible values: true, false] + --no-initial-fetch + + --warmupRevision + + --cacheMaxAge + + --cacheMaxEntries + + --cacheMaxSize + + --cachePruneInterval + [default: 1h] + --s3Bucket + + --s3Prefix + [default: ""] + --s3Region + + --s3Endpoint + + --s3ForcePathStyle + + -h, --help + Print help + -V, --version + Print version ``` @@ -858,61 +647,46 @@ content of the file are converted into a SHA256 value. ## Installing -### Integrate into your project (recommended) - -First, add the following snippet to your project: +### Prebuilt binaries (recommended) -#### Bzlmod snippet +Every [release](https://github.com/Tinder/bazel-diff/releases) ships a single self-contained +binary per platform. The Linux binaries are statically linked against musl, so they run on any +distribution (including Alpine and images older than the build runner) with no libc or JVM +requirement: -```bazel -bazel_dep(name = "bazel-diff", version = "47.0.0") -``` +```terminal +# Linux amd64 +curl -Lo bazel-diff https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-linux-amd64 +chmod +x bazel-diff -You can now run the tool with: +# Linux arm64 +curl -Lo bazel-diff https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-linux-arm64 +chmod +x bazel-diff -```terminal -bazel run @bazel-diff//cli:bazel-diff +# macOS arm64 +curl -Lo bazel-diff https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-macos-arm64 +chmod +x bazel-diff ``` -#### WORKSPACE snippet +Windows amd64: download `bazel-diff-rust-windows-amd64.exe` from the +[latest release](https://github.com/Tinder/bazel-diff/releases/latest). -```bazel -http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") -http_jar( - name = "bazel-diff", - urls = [ - "https://github.com/Tinder/bazel-diff/releases/download/7.0.0/bazel-diff_deploy.jar" - ], - sha256 = "0b9e32f9c20e570846b083743fe967ae54d13e2a1f7364983e0a7792979442be", -) -``` +### Integrate into your project -Second, add in your root `BUILD.bazel` file: +Add the following to your `MODULE.bazel`: ```bazel -load("@rules_java//java:defs.bzl", "java_binary") - -java_binary( - name = "bazel-diff", - main_class = "com.bazel_diff.Main", - runtime_deps = ["@bazel-diff//jar"], -) +bazel_dep(name = "bazel-diff", version = "47.0.0") ``` -That's it! You can now run the tool with: +You can now run the tool with: ```terminal -bazel run //:bazel-diff +bazel run @bazel-diff//:bazel-diff -- --help ``` -> Note, in releases prior to 2.0.0 the value for the `main_class` attribute is just `BazelDiff` - -### Run Via JAR Release - -```terminal -curl -Lo bazel-diff.jar https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff_deploy.jar -java -jar bazel-diff.jar -h -``` +(`@bazel-diff//:bazel-diff-rust` still resolves to the same binary for projects that adopted +it under that name.) bazel-diff is bzlmod-only; there is no `WORKSPACE` integration. ### Build from Source @@ -921,61 +695,31 @@ After cloning down the repo, you are good to go, Bazel will handle the rest To run the project ```terminal -bazel run :bazel-diff -- bazel-diff -h +bazel run :bazel-diff -- --help ``` -#### Debugging (when running from source) - -To run `bazel-diff` with debug logging, run your commands with the `verbose` config like so: +To build the same binaries a release publishes (Bazel names the output for the platform it +was built for, `bazel-bin/release/bazel-diff-rust--[.exe]`): ```terminal -bazel run :bazel-diff --config=verbose -- bazel-diff -h -``` - -### Build your own deployable JAR - -```terminal -bazel build //cli:bazel-diff_deploy.jar -java -jar bazel-bin/cli/bazel-diff_deploy.jar # This JAR can be run anywhere +make release_rust_binary # bazel build //release:bazel-diff-rust --config=release +make release_rust_binary_linux # ... --config=release-musl +make release_rust_binary_linux_arm64 # ... --config=release-musl-arm64 ``` -### Build from source in your Bazel Project - -Add the following to your `WORKSPACE` file to add the external repositories, replacing the `RELEASE_ARCHIVE_URL` with the archive url of the bazel-diff release you wish to depend on: - -```bazel -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -http_archive( - name = "bazel-diff", - urls = [ - "RELEASE_ARCHIVE_URL", - ], - sha256 = "UPDATE_ME", - strip_prefix = "UPDATE_ME" -) - -load("@bazel-diff//:repositories.bzl", "bazel_diff_dependencies") - -bazel_diff_dependencies() - -load("@rules_jvm_external//:defs.bzl", "maven_install") -load("@bazel-diff//:artifacts.bzl", "BAZEL_DIFF_MAVEN_ARTIFACTS") +`--config=release-musl` and `--config=release-musl-arm64` target +`//platforms:linux_x86_64_musl` and `//platforms:linux_aarch64_musl`, which select a musl Rust +std and a musl C toolchain, so the Linux assets are statically linked instead of inheriting the +build runner's glibc as a version floor. They are cross-compiles: the same commands produce +`bazel-diff-rust-linux-amd64` and `bazel-diff-rust-linux-arm64` on a glibc Linux host and on an +Apple Silicon Mac. -maven_install( - name = "bazel_diff_maven", - artifacts = BAZEL_DIFF_MAVEN_ARTIFACTS, - repositories = [ - "http://uk.maven.org/maven2", - "https://jcenter.bintray.com/", - ], -) -``` +#### Debugging (when running from source) -Now you can simply run `bazel-diff` from your project: +To run `bazel-diff` with debug logging, run your commands with the `verbose` config like so: ```terminal -bazel run @bazel-diff//cli:bazel-diff -- bazel-diff -h +bazel run :bazel-diff --config=verbose -- --help ``` ## Contributors @@ -1070,93 +814,34 @@ Precision CI at Scale: Target-Aware Workflows with Bazel Diff - Maxwell Elliott ## Running the tests -To run the tests simply run - -```terminal -bazel test //... -``` - -## Experimental Rust candidate - -This branch includes a proposed Rust implementation at `//:bazel-diff-rust`. The existing Kotlin -implementation remains the default `//:bazel-diff` target and the released JAR. - -```terminal -bazel run //:bazel-diff-rust -- --help -``` - -GitHub Releases also ship prebuilt binaries: +The unit tests, lint gates and tooling tests: ```terminal -# Linux amd64 (statically linked against musl -- no glibc requirement, so it -# runs on any distribution, including Alpine and images older than the runner) -curl -Lo bazel-diff-rust https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-linux-amd64 -chmod +x bazel-diff-rust - -# Linux arm64 (same musl static linking) -curl -Lo bazel-diff-rust https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-linux-arm64 -chmod +x bazel-diff-rust - -# macOS arm64 -curl -Lo bazel-diff-rust https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-macos-arm64 -chmod +x bazel-diff-rust +bazel test //:rust_tests //:rust_clippy_check //:rust_format_check //tools/... ``` -Windows amd64: download -`bazel-diff-rust-windows-amd64.exe` from the -[latest release](https://github.com/Tinder/bazel-diff/releases/latest). - -Those assets are produced by Bazel alone -- CI runs nothing but the commands below and uploads -whatever lands in `bazel-bin/release/`, so `//release:bazel-diff-rust` names the binary for the -platform it was built for (`bazel-diff-rust--`, plus `.exe` on Windows): +The end-to-end suite drives the real binary against fixture workspaces with a nested Bazel, +one target per case (see [`tools/e2e/README.md`](tools/e2e/README.md)): ```terminal -make release_rust_binary # bazel build //release:bazel-diff-rust --config=release -make release_rust_binary_linux # ... --config=release-musl -make release_rust_binary_linux_arm64 # ... --config=release-musl-arm64 +bazel test //tests:e2e_test ``` -`--config=release-musl` and `--config=release-musl-arm64` target -`//platforms:linux_x86_64_musl` and `//platforms:linux_aarch64_musl`, which select a musl Rust -std and a musl C toolchain, so the Linux assets are statically linked instead of inheriting the -build runner's glibc as a version floor. They are cross-compiles: the same commands produce -`bazel-diff-rust-linux-amd64` and `bazel-diff-rust-linux-arm64` on a glibc Linux host and on an -Apple Silicon Mac. - -### Performance gate - -The Rust candidate is expected to be faster than Kotlin, and CI enforces it. `make perf-gate` -runs both binaries over generated workloads -- a synthetic `streamed_proto` graph plus hash-file -pairs, with no real workspace, Bazel server or Hyperfine involved -- and exits non-zero unless -Rust wins on median wall time, on start-up-adjusted wall time, and in every paired round. - -```terminal -make perf-gate -make perf-gate SCALE=4 ROUNDS=9 JSON=/tmp/perf-gate.json -``` - -Outputs are compared before timings are reported, so a "speedup" can never come from the two -implementations doing different work. See -[`docs/kotlin-rust-perf-gate.md`](docs/kotlin-rust-perf-gate.md) for the protocol, the workload -list and what to do when the gate fails. For measuring the two implementations against a real -repository instead, see [`docs/kotlin-vs-rust-benchmark.md`](docs/kotlin-vs-rust-benchmark.md). - ## Code coverage -CI enforces a minimum **90% line coverage** on production sources. Kotlin -(`cli/src/main/...`), Go (`tools/go/...`), and the experimental Rust -implementation (`src/...`) are gated **independently** at 90% each, so thin -coverage in one language can't hide behind well-covered code in another. To -run the same checks locally: +CI enforces a minimum **90% line coverage** on production sources. Rust +(`src/...`, `tools/coverage/src/...`) and Go (`tools/go/...`) are gated +**independently** at 90% each, so thin coverage in one language can't hide +behind well-covered code in another. To run the same checks locally: ```terminal make coverage ``` This invokes -`bazel coverage --combined_report=lcov //cli/... //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/...` +`bazel coverage --combined_report=lcov //src:cli_tests //src:rust_tests //tools:coverage_check_test //tools/coverage/... //tools/go/...` and then runs `//tools:coverage-check` twice against the resulting LCOV report — once for -the Kotlin main sources and once scoped to `tools/go/` (`--include tools/go/`). The check is +the Rust sources and once scoped to `tools/go/` (`--include tools/go/`). The check is a Python `py_binary` ([`tools/coverage_check.py`](tools/coverage_check.py)) that prints a per-file table sorted by coverage (worst first), the overall percentage, and exits non-zero if the scoped coverage is below the threshold. @@ -1189,14 +874,11 @@ coverage_enforced_test( ``` The default minimum is 90%. Go (`//tools/go/sample:sample_test`), the Rust -LCOV merger (`//tools/coverage:lcov_merger_test`), the experimental Rust -implementation (`//src:cli_tests` and `//src:rust_tests`), and the -primary-owner Kotlin/JVM tests under `//cli` all carry minimums. When a +LCOV merger (`//tools/coverage:lcov_merger_test`) and the CLI itself +(`//src:cli_tests` and `//src:rust_tests`) all carry minimums. When a target's merged report falls below its minimum, the coverage run fails that target and the test log contains a per-file breakdown. See [`tools/coverage/README.md`](tools/coverage/README.md) for details. -The Kotlin-to-Rust applicability and parity decisions are tracked in -[`docs/kotlin-rust-test-parity.md`](docs/kotlin-rust-test-parity.md). For an interactive HTML report (annotated source with covered/uncovered lines highlighted), use `make coverage-html`. This requires the `lcov` package diff --git a/WORKSPACE b/WORKSPACE deleted file mode 100644 index 70f90b92..00000000 --- a/WORKSPACE +++ /dev/null @@ -1,80 +0,0 @@ -workspace(name = "bazel_diff") - -load("//:repositories.bzl", "bazel_diff_dependencies") - -bazel_diff_dependencies() - -load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") - -bazel_skylib_workspace() - -load("@bazel_features//:deps.bzl", "bazel_features_deps") - -bazel_features_deps() - -load("@rules_python//python:repositories.bzl", "py_repositories") - -py_repositories() - -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") - -protobuf_deps() - -load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies") - -rules_proto_dependencies() - -load("@rules_proto//proto:toolchains.bzl", "rules_proto_toolchains") - -rules_proto_toolchains() - -load("@rules_java//java:repositories.bzl", "rules_java_dependencies", "rules_java_toolchains") - -rules_java_dependencies() - -rules_java_toolchains() - -load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps") - -rules_jvm_external_deps() - -load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup") - -rules_jvm_external_setup() - -load("@rules_jvm_external//:defs.bzl", "maven_install") -load("@rules_kotlin//kotlin:repositories.bzl", "kotlin_repositories") -load("//:artifacts.bzl", "BAZEL_DIFF_MAVEN_ARTIFACTS") - -kotlin_repositories() - -load("@rules_kotlin//kotlin:core.bzl", "kt_register_toolchains") - -kt_register_toolchains() - -maven_install( - name = "bazel_diff_maven", - artifacts = BAZEL_DIFF_MAVEN_ARTIFACTS, - maven_install_json = "//:maven_install.json", - repositories = [ - "https://repo1.maven.org/maven2/", - ], -) - -load("@bazel_diff_maven//:defs.bzl", "pinned_maven_install") - -pinned_maven_install() - -load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies") - -aspect_bazel_lib_dependencies() - -load( - "@aspect_rules_lint//format:repositories.bzl", - "fetch_ktfmt", - "rules_lint_dependencies", -) - -rules_lint_dependencies() - -fetch_ktfmt() diff --git a/artifacts.bzl b/artifacts.bzl deleted file mode 100644 index c241dba6..00000000 --- a/artifacts.bzl +++ /dev/null @@ -1,14 +0,0 @@ -""" Artifacts used to build bazel-diff """ - -BAZEL_DIFF_MAVEN_ARTIFACTS = [ - "com.google.code.gson:gson:jar:2.14.0", - "com.google.guava:guava:33.6.0-jre", - "com.willowtreeapps.assertk:assertk-jvm:0.28.1", - "info.picocli:picocli:jar:4.7.7", - "io.insert-koin:koin-core-jvm:4.2.2", - "io.insert-koin:koin-test-junit4:4.2.2", - "junit:junit:4.13.2", - "org.apache.commons:commons-pool2:2.13.1", - "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0", - "org.mockito.kotlin:mockito-kotlin:6.3.0", -] diff --git a/bazel-diff-example.ps1 b/bazel-diff-example.ps1 index b08efdaa..c05e9ad3 100644 --- a/bazel-diff-example.ps1 +++ b/bazel-diff-example.ps1 @@ -66,11 +66,6 @@ function Run-BazelDiff { param( [string[]]$Arguments ) - # Shut down the Bazel server first so its persistent workers exit. On Windows a - # live Kotlin worker keeps rules_kotlin's build.exe/compiler.jar locked, and the - # external repo refetch fails with "Permission denied" whenever the two revisions - # being diffed pin different rules_kotlin versions. - & $BazelPath shutdown & $BazelPath run @BazelExtraOptions //:bazel-diff -- @Arguments return $LASTEXITCODE } diff --git a/cli/BUILD b/cli/BUILD deleted file mode 100644 index 71dfea7c..00000000 --- a/cli/BUILD +++ /dev/null @@ -1,536 +0,0 @@ -load("@rules_java//java:defs.bzl", "java_binary") -load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test") -load("//tools/coverage:defs.bzl", "coverage_minimum_env") -load("//tools/e2e:defs.bzl", "kt_jvm_e2e_tests") -load("//tools/e2e:kotlin_e2e_cases.bzl", "KOTLIN_E2E_SUITES") - -config_setting( - name = "enable_debug", - values = { - "compilation_mode": "dbg", - }, -) - -genrule( - name = "version_file", - outs = ["src/main/resources/cli/version"], - cmd = "echo '{version}' > $@".format(version = module_version() if module_version() else "unknown"), - stamp = 1, -) - -java_binary( - name = "bazel-diff", - jvm_flags = select({ - ":enable_debug": ["-DDEBUG=true"], - "//conditions:default": [], - }), - main_class = "com.bazel_diff.Main", - visibility = ["//visibility:public"], - runtime_deps = [":cli-lib"], -) - -kt_jvm_library( - name = "cli-lib", - srcs = glob(["src/main/kotlin/**/*.kt"]), - resources = [":version_file"], - # sts is runtime-only: the SDK's DefaultCredentialsProvider loads it reflectively to - # assume a pod IAM role via web identity (IRSA) for the serve S3 cache backend. - runtime_deps = [ - "@bazel_diff_maven//:software_amazon_awssdk_sts", - ], - deps = [ - "//proto:analysis_v2_java_proto", - "//proto:build_java_proto", - "@bazel_diff_maven//:com_google_code_gson_gson", - "@bazel_diff_maven//:com_google_guava_guava", - "@bazel_diff_maven//:info_picocli_picocli", - "@bazel_diff_maven//:io_insert_koin_koin_core_jvm", - "@bazel_diff_maven//:org_apache_commons_commons_pool2", - "@bazel_diff_maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core_jvm", - "@bazel_diff_maven//:software_amazon_awssdk_regions", - "@bazel_diff_maven//:software_amazon_awssdk_s3", - "@bazel_diff_maven//:software_amazon_awssdk_sdk_core", - "@bazel_diff_maven//:software_amazon_awssdk_url_connection_client", - ], -) - -# Every primary-owner kt_jvm_test declares a per-target line-coverage minimum -# (default 90%), enforced by //tools/coverage:lcov_merger during -# `bazel coverage` only (plain `bazel test` is unaffected). Jacoco instruments -# the whole of :cli-lib for every test, so each check is scoped -# (coverage_include) to the source file(s) the test is responsible for. -# Partial/regression tests and E2ETest omit enforcement; the repo-wide -# coverage gate still covers the combined report. -kt_jvm_test( - name = "BuildGraphHasherTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt"], - ), - jvm_flags = [ - "-Dnet.bytebuddy.experimental=true", - ], - test_class = "com.bazel_diff.hash.BuildGraphHasherTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "TargetHashTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/TargetHash.kt"], - ), - test_class = "com.bazel_diff.hash.TargetHashTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "RuleHasherTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/RuleHasher.kt"], - ), - test_class = "com.bazel_diff.hash.RuleHasherTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "RuleHasherAlwaysAffectedTagsTest", - test_class = "com.bazel_diff.hash.RuleHasherAlwaysAffectedTagsTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "SourceFileHasherTest", - data = [ - ":src/test/kotlin/com/bazel_diff/hash/fixture/foo.ts", - ], - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/hash/SourceFileHasher.kt"], - ), - test_class = "com.bazel_diff.hash.SourceFileHasherTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ExplainImpactInteractorTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/ExplainImpactInteractor.kt"], - ), - test_class = "com.bazel_diff.interactor.ExplainImpactInteractorTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ImpactGraphRendererTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/ImpactGraphRenderer.kt"], - ), - test_class = "com.bazel_diff.interactor.ImpactGraphRendererTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ExplainCommandTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/ExplainCommand.kt"], - ), - test_class = "com.bazel_diff.cli.ExplainCommandTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "CalculateImpactedTargetsInteractorTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/CalculateImpactedTargetsInteractor.kt"], - ), - test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "CalculateImpactedTargetsInteractorIssue335Test", - test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorIssue335Test", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "CalculateImpactedTargetsInteractorModuleQueryTest", - test_class = "com.bazel_diff.interactor.CalculateImpactedTargetsInteractorModuleQueryTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "NormalisingPathConverterTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt"], - ), - test_class = "com.bazel_diff.cli.converter.NormalisingPathConverterTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ExplainFormatConverterTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/ExplainFormatConverter.kt"], - ), - test_class = "com.bazel_diff.cli.converter.ExplainFormatConverterTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "OptionsConverterTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt"], - ), - test_class = "com.bazel_diff.cli.converter.OptionsConverterTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "DurationConverterTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt"], - ), - test_class = "com.bazel_diff.cli.converter.DurationConverterTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ByteSizeConverterTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt"], - ), - test_class = "com.bazel_diff.cli.converter.ByteSizeConverterTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "DeserialiseHashesInteractorTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/DeserialiseHashesInteractor.kt"], - ), - test_class = "com.bazel_diff.interactor.DeserialiseHashesInteractorTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "FingerprintInteractorTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/interactor/FingerprintInteractor.kt"], - ), - test_class = "com.bazel_diff.interactor.FingerprintInteractorTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "FingerprintGathererTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt"], - ), - test_class = "com.bazel_diff.cli.FingerprintGathererTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "FingerprintCommandTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt"], - ), - test_class = "com.bazel_diff.cli.FingerprintCommandTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "WarmupCommandTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt"], - ), - test_class = "com.bazel_diff.cli.WarmupCommandTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelRuleTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt"], - ), - test_class = "com.bazel_diff.bazel.BazelRuleTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelClientTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt"], - ), - test_class = "com.bazel_diff.bazel.BazelClientTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelTargetTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt"], - ), - test_class = "com.bazel_diff.bazel.BazelTargetTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelTargetTypeTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt"], - ), - test_class = "com.bazel_diff.bazel.BazelTargetTypeTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelDiffTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt"], - ), - test_class = "com.bazel_diff.cli.BazelDiffTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "VersionProviderTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt"], - ), - test_class = "com.bazel_diff.cli.VersionProviderTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "StderrLoggerTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/log/StderrLogger.kt"], - ), - test_class = "com.bazel_diff.log.StderrLoggerTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelModServiceTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt"], - ), - test_class = "com.bazel_diff.bazel.BazelModServiceTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ModuleGraphParserTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt"], - ), - test_class = "com.bazel_diff.bazel.ModuleGraphParserTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "StderrPollutionRegressionTest", - test_class = "com.bazel_diff.bazel.StderrPollutionRegressionTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ProcessStdinHangRegressionTest", - test_class = "com.bazel_diff.process.ProcessStdinHangRegressionTest", - runtime_deps = [":cli-test-lib"], -) - -# The JUnit e2e suite, one Bazel target per `@Test` method. It used to be a -# single target with timeout = "eternal", because the class as a whole ran -# ~1150s on CI's macos-latest x bazel 8.x cell; split, each case gets its own -# 300s budget, its own cache entry and its own log, and the cases run in -# parallel instead of back to back. -# -# KOTLIN_E2E_SUITES is generated from the sources by `make regen-e2e`, so adding -# a `@Test` method to any class under cli/src/test/kotlin/com/bazel_diff/e2e/ -# creates its target with no edit here. //tools/e2e:regen_check fails CI when -# the two drift. -# -# No per-target coverage minimum: E2E exercises the real binary across the whole -# main tree, but the repo-wide coverage gate (and primary-owner unit tests) own -# the 90% floor. Enforcement here would duplicate that gate. -kt_jvm_e2e_tests( - data = [":workspaces"], - suites = KOTLIN_E2E_SUITES, - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ContentHashProviderTest", - data = [ - ":src/test/kotlin/com/bazel_diff/io/fixture/correct.json", - ":src/test/kotlin/com/bazel_diff/io/fixture/wrong.json", - ], - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/io/ContentHashProvider.kt"], - ), - test_class = "com.bazel_diff.io.ContentHashProviderTest", - runtime_deps = [ - ":cli-test-lib", - ], -) - -kt_jvm_test( - name = "ServeCommandTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt"], - ), - test_class = "com.bazel_diff.cli.ServeCommandTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "LocalDiskHashCacheStorageTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/HashCacheStorage.kt"], - ), - test_class = "com.bazel_diff.server.LocalDiskHashCacheStorageTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "S3HashCacheStorageTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/S3HashCacheStorage.kt"], - ), - jvm_flags = [ - "-Dnet.bytebuddy.experimental=true", - ], - test_class = "com.bazel_diff.server.S3HashCacheStorageTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "TieredHashCacheStorageTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/TieredHashCacheStorage.kt"], - ), - test_class = "com.bazel_diff.server.TieredHashCacheStorageTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "CachePrunerTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/CachePruner.kt"], - ), - test_class = "com.bazel_diff.server.CachePrunerTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "MetricsServiceTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/MetricsService.kt"], - ), - test_class = "com.bazel_diff.server.MetricsServiceTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "GitClientTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt"], - ), - test_class = "com.bazel_diff.server.GitClientTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "HashServiceTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/HashService.kt"], - ), - jvm_flags = [ - "-Dnet.bytebuddy.experimental=true", - ], - test_class = "com.bazel_diff.server.HashServiceTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "ImpactedTargetsServiceTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/ImpactedTargetsService.kt"], - ), - jvm_flags = [ - "-Dnet.bytebuddy.experimental=true", - ], - test_class = "com.bazel_diff.server.ImpactedTargetsServiceTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "BazelDiffServerTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/BazelDiffServer.kt"], - ), - test_class = "com.bazel_diff.server.BazelDiffServerTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_test( - name = "QueryProfilerTest", - env = coverage_minimum_env( - coverage_include = ["cli/src/main/kotlin/com/bazel_diff/server/QueryProfiler.kt"], - ), - test_class = "com.bazel_diff.server.QueryProfilerTest", - runtime_deps = [":cli-test-lib"], -) - -kt_jvm_library( - name = "cli-test-lib", - testonly = True, - srcs = glob(["src/test/kotlin/**/*.kt"]), - resources = glob(["src/test/resources/**/*"]), - deps = [ - ":cli-lib", - "@bazel_diff_maven//:com_willowtreeapps_assertk_assertk_jvm", - "@bazel_diff_maven//:io_insert_koin_koin_test_junit4", - "@bazel_diff_maven//:io_insert_koin_koin_test_jvm", - "@bazel_diff_maven//:junit_junit", - "@bazel_diff_maven//:org_mockito_kotlin_mockito_kotlin", - "@bazel_diff_maven//:software_amazon_awssdk_s3", - "@bazel_diff_maven//:software_amazon_awssdk_sdk_core", - ], -) - -# The Kotlin e2e sources //tools/e2e:regen_check re-derives the generated case -# list from. Globbed, not enumerated: a new e2e class is meant to need no edit -# here either. -filegroup( - name = "e2e_test_sources", - srcs = glob(["src/test/kotlin/com/bazel_diff/e2e/**/*.kt"]), - visibility = ["//tools/e2e:__pkg__"], -) - -filegroup( - name = "workspaces", - srcs = [ - "src/test/resources/workspaces", - ], -) - -# //tests:e2e_test reads both fixture trees straight off disk (it is not a JVM -# test, so it cannot pull the zips and goldens off a classpath the way -# //cli:E2ETest does) and resolves them relative to the runfiles root. -filegroup( - name = "e2e_resources", - srcs = [ - "src/test/resources/fixture", - "src/test/resources/workspaces", - ], - visibility = ["//tests:__pkg__"], -) - -java_binary( - name = "ktfmt", - main_class = "com.facebook.ktfmt.cli.Main", - visibility = ["//cli/format:__pkg__"], - runtime_deps = ["@ktfmt//jar"], -) diff --git a/cli/src/main/kotlin/com/bazel_diff/Main.kt b/cli/src/main/kotlin/com/bazel_diff/Main.kt deleted file mode 100644 index 2ed37ad6..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/Main.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.bazel_diff - -import com.bazel_diff.cli.BazelDiff -import kotlin.system.exitProcess -import picocli.CommandLine - -class Main { - companion object { - @JvmStatic - fun main(args: Array) { - val exitCode = CommandLine(BazelDiff()).execute(*args) - exitProcess(exitCode) - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt deleted file mode 100644 index d35ef2ee..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.bazel_diff.bazel - -import com.bazel_diff.log.Logger -import java.util.Calendar -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class BazelClient( - private val useCquery: Boolean, - private val cqueryExpression: String?, - private val fineGrainedHashExternalRepos: Set, - private val excludeExternalTargets: Boolean, - private val excludeTargetsQuery: String?, -) : KoinComponent { - private val logger: Logger by inject() - private val queryService: BazelQueryService by inject() - private val bazelModService: BazelModService by inject() - - /** - * Latched once a query proves `//external` is unavailable in this workspace (bzlmod-only mode - * where the `bazel mod graph` probe in [BazelModService] returned a false negative). Subsequent - * [queryAllTargets] calls -- e.g. later revisions served by a long-running `serve` process -- - * then skip `//external:all-targets` up front rather than failing and retrying every time. - */ - @Volatile private var externalPackageUnavailable = false - - /** - * Wraps [query] so that every target matching the user-supplied [excludeTargetsQuery] is removed - * from the result set via Bazel's `except` operator. Returns [query] unchanged when no exclude - * query is configured. This is how callers drop, e.g., `manual`-tagged targets from the graph - * (issue #392): `--excludeTargetsQuery='attr("tags", "[\[ ]manual[,\]]", //...)'`. Excluded - * targets are simply absent from hashing; any kept target that depended on one logs an "Unable to - * calculate digest" warning and hashes without that input, which is the intended behavior for - * leaf targets like `manual`-tagged tests/binaries. - */ - private fun withExcludeFilter(query: String): String = - excludeTargetsQuery?.takeIf { it.isNotBlank() }?.let { "($query) except ($it)" } ?: query - - suspend fun queryAllTargets(): List { - val queryEpoch = Calendar.getInstance().getTimeInMillis() - - // Skip //external:all-targets when explicitly excluded, or when an earlier query already proved - // //external is unavailable here (see [externalPackageUnavailable]). - // - // Bzlmod being enabled does not retire //external: a workspace can run both, and //external - // still holds the only version-bearing hash for every WORKSPACE repo. Ask for it and let the - // ExternalPackageUnavailableException path below drop it when it really is gone. - val repoTargetsQuery = - if (excludeExternalTargets || externalPackageUnavailable) { - emptyList() - } else { - listOf("//external:all-targets") - } - - // When Bzlmod is enabled and Bazel 8.6.0+ is available, query bzlmod-managed external repo - // definitions via `bazel mod show_repo --output=streamed_proto`. This creates synthetic - // //external:* targets from Build.Repository protos, enabling fine-grained hashing of - // bzlmod dependencies. - val bzlmodRepoTargets = - if (bazelModService.isBzlmodEnabled && queryService.canUseBzlmodShowRepo) { - logger.i { "Querying bzlmod-managed external repos via mod show_repo" } - queryService.queryBzlmodRepos() - } else { - emptyList() - } - - val targets = - if (useCquery) { - // Explicitly listing external repos here sometimes causes issues mentioned at - // https://bazel.build/query/cquery#recursive-target-patterns. Hence, we query all - // dependencies with `deps` - // instead. However, we still need to append all "//external:*" targets because - // fine-grained hash - // computation depends on hashing of source files in external repos as well, which is - // limited to repos - // explicitly mentioned in `fineGrainedHashExternalRepos` flag. Therefore, for any repos - // not mentioned there - // we are still relying on the repo-generation target under `//external` to compute the - // hash. - // - // In addition, we must include all source dependencies in this query in order for them to - // show up in - // `configuredRuleInput`. Hence, one must not filter them out with `kind(rule, deps(..))`. - val expression = withExcludeFilter(cqueryExpression ?: "deps(//...:all-targets)") - val mainTargets = queryService.query(expression, useCquery = true) - val repoTargets = - if (repoTargetsQuery.isNotEmpty()) { - try { - queryService.query(repoTargetsQuery.joinToString(" + ") { "'$it'" }) - } catch (e: ExternalPackageUnavailableException) { - // //external isn't queryable here despite the Bzlmod probe saying otherwise; the - // main deps() query already succeeded, so just drop the external repo targets. - noteExternalPackageUnavailable() - emptyList() - } - } else { - emptyList() - } - (mainTargets + repoTargets).distinctBy { it.name } - } else { - val buildTargetsQuery = - listOf("//...:all-targets") + - fineGrainedHashExternalRepos.map { "$it//...:all-targets" } - try { - queryService.query( - withExcludeFilter( - (repoTargetsQuery + buildTargetsQuery).joinToString(" + ") { "'$it'" })) - } catch (e: ExternalPackageUnavailableException) { - // The combined query failed because //external is unavailable. If we hadn't asked for - // it, the error came from elsewhere and must not be masked; otherwise retry without it. - if (repoTargetsQuery.isEmpty()) throw e - noteExternalPackageUnavailable() - queryService.query(withExcludeFilter(buildTargetsQuery.joinToString(" + ") { "'$it'" })) - } - } - val allTargets = (targets + bzlmodRepoTargets).distinctBy { it.name } - val queryDuration = Calendar.getInstance().getTimeInMillis() - queryEpoch - logger.i { "All targets queried in $queryDuration" } - return allTargets - } - - /** - * Latches [externalPackageUnavailable] the first time a query proves `//external` is unqueryable, - * emitting a one-time explanation. This means the Bzlmod probe (`bazel mod graph`) reported that - * Bzlmod was off when it is actually on -- common in restricted environments where `bazel mod - * graph` cannot resolve the module graph. Dropping `//external:all-targets` is correct here: when - * the package genuinely isn't available, there are no WORKSPACE-defined external targets to hash. - */ - private fun noteExternalPackageUnavailable() { - if (!externalPackageUnavailable) { - externalPackageUnavailable = true - logger.w { - "//external is not available in this workspace (WORKSPACE deprecated / bzlmod-only), but " + - "Bzlmod auto-detection did not catch it; dropping //external:all-targets from the " + - "query and continuing. Pass --excludeExternalTargets to skip it up front and silence " + - "this." - } - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt deleted file mode 100644 index 242a56e0..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.bazel_diff.bazel - -import com.bazel_diff.extensions.toHexString -import com.bazel_diff.hash.sha256 -import com.bazel_diff.log.Logger -import com.bazel_diff.process.Redirect -import com.bazel_diff.process.process -import java.io.File -import java.nio.charset.StandardCharsets -import java.nio.file.Path -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -/** - * Service that runs `bazel mod` to detect whether Bzlmod is enabled in the workspace. Used to - * decide whether to query //external:all-targets (disabled when Bzlmod is active). - */ -class BazelModService( - private val workingDirectory: Path, - private val bazelPath: Path, - private val startupOptions: List, - private val noBazelrc: Boolean, -) : KoinComponent { - private val logger: Logger by inject() - - /** - * True if Bzlmod is enabled (e.g. `bazel mod graph` succeeds). When true, //external is not - * available. - */ - val isBzlmodEnabled: Boolean by lazy { runBlocking { checkBzlmodEnabled() } } - - /** - * Returns the module dependency graph as a string for hashing purposes. This captures all module - * dependencies and their versions, allowing bazel-diff to detect when MODULE.bazel changes (e.g., - * when a module version is updated). - * - * @return The output of `bazel mod graph` if bzlmod is enabled, or null if disabled/error. - */ - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun getModuleGraph(): String? { - if (!isBzlmodEnabled) { - return null - } - - val cmd = - mutableListOf().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - add("mod") - add("graph") - } - logger.i { "Executing Bazel mod graph for hashing: ${cmd.joinToString()}" } - val result = - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - stderr = Redirect.SILENT, - workingDirectory = workingDirectory.toFile(), - destroyForcibly = true, - ) - - return if (result.resultCode == 0) { - result.output.joinToString("\n").trim() - } else { - logger.w { "Failed to get module graph" } - null - } - } - - /** - * Returns the module dependency graph in JSON format for precise change detection. - * - * @return The JSON output of `bazel mod graph --output=json` if bzlmod is enabled, or null if - * disabled/error. - */ - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun getModuleGraphJson(): String? { - if (!isBzlmodEnabled) { - return null - } - - val cmd = - mutableListOf().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - add("mod") - add("graph") - add("--output=json") - } - logger.i { "Executing Bazel mod graph JSON: ${cmd.joinToString()}" } - val result = - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - stderr = Redirect.SILENT, - workingDirectory = workingDirectory.toFile(), - destroyForcibly = true, - ) - - return if (result.resultCode == 0) { - result.output.joinToString("\n").trim() - } else { - logger.w { "Failed to get module graph JSON" } - null - } - } - - /** - * Computes a stable fingerprint of the currently resolved external dependency state. - * - * The hash includes: - * - bzlmod mode marker + `bazel mod graph --output=json` - * - repository-definition bytes from `bazel mod show_repo` (streamed proto when available) - */ - suspend fun getDependencyFingerprint(): String? { - if (!isBzlmodEnabled) { - return sha256 { putBytes("mode:legacy".toByteArray(StandardCharsets.UTF_8)) }.toHexString() - } - - val moduleGraphJson = getModuleGraphJson() ?: "" - val canonicalRepos = discoverCanonicalBzlmodRepos() - - val streamedShowRepo = showRepoStreamedProto(canonicalRepos) - if (streamedShowRepo != null && streamedShowRepo.exitCode == 0) { - return sha256 { - putBytes("mode:bzlmod\n".toByteArray(StandardCharsets.UTF_8)) - putBytes("moduleGraphJson:".toByteArray(StandardCharsets.UTF_8)) - putBytes(moduleGraphJson.toByteArray(StandardCharsets.UTF_8)) - putBytes("\nshowRepo:\n".toByteArray(StandardCharsets.UTF_8)) - putBytes(streamedShowRepo.stdout) - } - .toHexString() - } - - val showRepoText = resolveShowRepoTextFallback(canonicalRepos) ?: return null - return sha256 { - putBytes("mode:bzlmod\n".toByteArray(StandardCharsets.UTF_8)) - putBytes("moduleGraphJson:".toByteArray(StandardCharsets.UTF_8)) - putBytes(moduleGraphJson.toByteArray(StandardCharsets.UTF_8)) - putBytes("\nshowRepoText:\n".toByteArray(StandardCharsets.UTF_8)) - putBytes(showRepoText.toByteArray(StandardCharsets.UTF_8)) - } - .toHexString() - } - - /** - * Returns canonical bzlmod repo names in @@ form, discovered from `bazel mod - * dump_repo_mapping ""`. - */ - private fun discoverCanonicalBzlmodRepos(): List { - val output = runBazelRaw(listOf("mod", "dump_repo_mapping", "")) ?: return emptyList() - if (output.exitCode != 0) { - return emptyList() - } - return String(output.stdout, StandardCharsets.UTF_8) - .lineSequence() - .mapNotNull { line -> parseCanonicalRepoNames(line) } - .flatten() - .filter { it.contains('+') || it.contains('~') } - .map { "@@$it" } - .toSet() - .sorted() - } - - private fun parseCanonicalRepoNames(line: String): List? { - val parsed = runCatching { - @Suppress("UNCHECKED_CAST") - com.google.gson.Gson().fromJson(line.trim(), Map::class.java) as Map - } - if (parsed.isFailure) return null - return parsed.getOrNull()?.values?.mapNotNull { it as? String } - } - - private fun showRepoStreamedProto(canonicalRepos: List): RawCommandResult? { - val args = mutableListOf("mod", "show_repo") - if (canonicalRepos.isNotEmpty()) { - args.addAll(canonicalRepos) - } - args.add("--output=streamed_proto") - return runBazelRaw(args) - } - - private fun resolveShowRepoTextFallback(canonicalRepos: List): String? { - val allVisible = runBazelRaw(listOf("mod", "show_repo", "--all_visible_repos", "--output=text")) - if (allVisible != null && allVisible.exitCode == 0) { - return String(allVisible.stdout, StandardCharsets.UTF_8) - } - - val args = mutableListOf("mod", "show_repo") - if (canonicalRepos.isNotEmpty()) { - args.addAll(canonicalRepos) - } - args.add("--output=text") - val fallback = runBazelRaw(args) ?: return null - if (fallback.exitCode != 0) { - return null - } - return String(fallback.stdout, StandardCharsets.UTF_8) - } - - private data class RawCommandResult( - val exitCode: Int, - val stdout: ByteArray, - ) - - private fun runBazelRaw(args: List): RawCommandResult? { - val command = - mutableListOf().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - addAll(args) - } - return try { - val nullDevice = - if (System.getProperty("os.name").startsWith("Windows")) "NUL" else "/dev/null" - val process = - ProcessBuilder(command) - .directory(workingDirectory.toFile()) - .redirectError(ProcessBuilder.Redirect.to(File(nullDevice))) - .start() - val stdout = process.inputStream.readBytes() - val exitCode = process.waitFor() - if (exitCode != 0) { - logger.w { "Command failed (exit=$exitCode): ${command.joinToString(" ")}" } - } - RawCommandResult(exitCode = exitCode, stdout = stdout) - } catch (e: Exception) { - logger.w { "Failed to execute ${command.joinToString(" ")}: ${e.message}" } - null - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private suspend fun checkBzlmodEnabled(): Boolean { - val cmd = - mutableListOf().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - add("mod") - add("graph") - } - logger.i { "Executing Bazel mod graph: ${cmd.joinToString()}" } - val result = - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - stderr = Redirect.CAPTURE, - workingDirectory = workingDirectory.toFile(), - destroyForcibly = true, - ) - return result.resultCode == 0 - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt deleted file mode 100644 index 89c49bc7..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt +++ /dev/null @@ -1,651 +0,0 @@ -package com.bazel_diff.bazel - -import com.bazel_diff.log.Logger -import com.bazel_diff.process.Redirect -import com.bazel_diff.process.process -import com.google.devtools.build.lib.analysis.AnalysisProtosV2 -import com.google.devtools.build.lib.query2.proto.proto2api.Build -import java.io.File -import java.nio.file.Files -import java.nio.file.Path -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.runBlocking -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -private val versionComparator = - compareBy> { it.first }.thenBy { it.second }.thenBy { it.third } - -/** - * Thrown by [BazelQueryService.query] when a `bazel query` fails specifically because the - * `//external` package is unavailable. Bazel reports `no such package 'external'` once the - * WORKSPACE file is deprecated (bzlmod-only mode -- e.g. Bazel 8/9 with - * `--enable_workspace=false`). - * - * [BazelClient] catches this to transparently drop `//external:all-targets` and retry, self-healing - * the case where bzlmod auto-detection via `bazel mod graph` returned a false negative (e.g. a - * restricted deploy environment where `bazel mod graph` cannot resolve the module graph, so the - * `//external`-availability prediction disagrees with what Bazel actually does). - */ -class ExternalPackageUnavailableException(message: String) : RuntimeException(message) - -class BazelQueryService( - private val workingDirectory: Path, - private val bazelPath: Path, - private val startupOptions: List, - private val commandOptions: List, - private val cqueryOptions: List, - private val keepGoing: Boolean, - private val noBazelrc: Boolean, -) : KoinComponent { - private val logger: Logger by inject() - private val modService: BazelModService by inject() - private val version: Triple by lazy { runBlocking { determineBazelVersion() } } - - @OptIn(ExperimentalCoroutinesApi::class) - private suspend fun determineBazelVersion(): Triple { - val cmd = arrayOf(bazelPath.toString(), "version") - logger.i { "Executing Bazel version command: ${cmd.joinToString()}" } - val result = - process( - *cmd, - stdout = Redirect.CAPTURE, - workingDirectory = workingDirectory.toFile(), - stderr = Redirect.PRINT, - destroyForcibly = true, - ) - - if (result.resultCode != 0) { - throw RuntimeException("Bazel version command failed, exit code ${result.resultCode}") - } - - // "bazel version" outputs "Build label: X.Y.Z" on one of the lines; accept that or legacy - // "bazel X.Y.Z". - val versionString = - result.output - .firstOrNull { it.startsWith("Build label: ") } - ?.removePrefix("Build label: ") - ?.trim() - ?: result.output.firstOrNull { it.startsWith("bazel ") }?.removePrefix("bazel ")?.trim() - ?: throw RuntimeException( - "Bazel version command returned unexpected output: ${result.output}") - // Trim off any prerelease suffixes (e.g., 8.6.0-rc1 or 8.6.0rc1). - val version = - versionString - .split('-')[0] - .split('.') - .map { it.takeWhile { c -> c.isDigit() }.toInt() } - .toTypedArray() - return Triple(version[0], version[1], version[2]) - } - - // Use streamed_proto output for cquery if available. This is more efficient than the proto - // output. - // https://github.com/bazelbuild/bazel/commit/607d0f7335f95aa0ee236ba3c18ce2a232370cdb - private val canUseStreamedProtoWithCquery - get() = versionComparator.compare(version, Triple(7, 0, 0)) >= 0 - - // Use an output file for (c)query if supported. This avoids excessively large stdout, which is - // sent out on the BES. - // https://github.com/bazelbuild/bazel/commit/514e9052f2c603c53126fbd9436bdd3ad3a1b0c7 - private val canUseOutputFile - get() = versionComparator.compare(version, Triple(8, 2, 0)) >= 0 - - // Bazel 8.6.0+ / 9.0.1+ supports `bazel mod show_repo --output=streamed_proto`, which - // outputs Build.Repository protos for bzlmod-managed external repos. - // https://github.com/bazelbuild/bazel/pull/28010 - val canUseBzlmodShowRepo - get() = - versionComparator.compare(version, Triple(8, 6, 0)) >= 0 && - // 9.0.0 does not have the feature; it landed in 9.0.1. - version != Triple(9, 0, 0) - - suspend fun query(query: String, useCquery: Boolean = false): List { - // Unfortunately, there is still no direct way to tell if a target is compatible or not with the - // proto output - // by itself. So we do an extra cquery with the trick at - // https://bazel.build/extending/platforms#cquery-incompatible-target-detection to first find - // all compatible - // targets. - val compatibleTargetSet = - if (useCquery) { - val compatibleFile = runQuery(query, useCquery = true, outputCompatibleTargets = true) - val set = compatibleFile.useLines { it.filter { it.isNotBlank() }.toSet() } - compatibleFile.delete() - set - } else { - emptySet() - } - val outputFile = runQuery(query, useCquery) - - val targets = - outputFile.inputStream().buffered().use { proto -> - if (useCquery) { - if (canUseStreamedProtoWithCquery) { - mutableListOf() - .apply { - while (true) { - val result = - AnalysisProtosV2.CqueryResult.parseDelimitedFrom(proto) ?: break - // EOF - add(result) - } - } - .flatMap { it.resultsList } - } else { - AnalysisProtosV2.CqueryResult.parseFrom(proto).resultsList - } - .mapNotNull { toBazelTarget(it.target) } - .filter { it.name in compatibleTargetSet } - } else { - mutableListOf() - .apply { - while (true) { - val target = Build.Target.parseDelimitedFrom(proto) ?: break - // EOF - add(target) - } - } - .mapNotNull { toBazelTarget(it) } - } - } - - outputFile.delete() - return targets - } - - @OptIn(ExperimentalCoroutinesApi::class) - private suspend fun runQuery( - query: String, - useCquery: Boolean, - outputCompatibleTargets: Boolean = false - ): File { - val queryFile = Files.createTempFile(null, ".txt").toFile() - val outputFile = Files.createTempFile(null, ".bin").toFile() - var cqueryStarlarkFile: File? = null - - queryFile.writeText(query) - - val allowedExitCodes = mutableListOf(0) - - val cmd: MutableList = - ArrayList().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - if (useCquery) { - add("cquery") - if (!outputCompatibleTargets) { - // There is no need to query the transitions when querying for compatible targets. - add("--transitions=lite") - } - } else { - add("query") - } - add("--output") - if (useCquery) { - if (outputCompatibleTargets) { - add("starlark") - add("--starlark:file") - val starlarkFile = Files.createTempFile(null, ".cquery").toFile() - cqueryStarlarkFile = starlarkFile - starlarkFile.writeText( - """ - def format(target): - if providers(target) == None: - return "" - if "IncompatiblePlatformProvider" not in providers(target): - target_repr = repr(target) - if "() - val result = - process( - *cmd.toTypedArray(), - stdout = if (canUseOutputFile) Redirect.SILENT else Redirect.ToFile(outputFile), - workingDirectory = workingDirectory.toFile(), - stderr = - Redirect.Consume { flow -> - flow.collect { line -> - System.err.println(line) - stderrLines.add(line) - } - }, - destroyForcibly = true, - ) - - queryFile.delete() - cqueryStarlarkFile?.delete() - - if (!allowedExitCodes.contains(result.resultCode)) { - outputFile.delete() - val stderrText = stderrLines.joinToString("\n") - logger.w { "Bazel query failed with exit code ${result.resultCode}" } - if (indicatesExternalPackageUnavailable(stderrText)) { - throw ExternalPackageUnavailableException( - "Bazel query failed because the //external package is unavailable " + - "(WORKSPACE deprecated / bzlmod-only), exit code ${result.resultCode}") - } - throw RuntimeException( - "Bazel query failed, exit code ${result.resultCode}, allowed exit codes: ${allowedExitCodes.joinToString()}") - } - return outputFile - } - - /** - * True if [stderr] from a failed `bazel query` indicates the `//external` package is unavailable - * because the WORKSPACE file is deprecated (bzlmod-only mode). Matches the stable fragments of - * Bazel's error message: `no such package 'external'` / `//external package is not available`. - */ - private fun indicatesExternalPackageUnavailable(stderr: String): Boolean = - stderr.contains("no such package 'external'") || - stderr.contains("//external package is not available") - - /** - * Queries bzlmod-managed external repo definitions using `bazel mod show_repo`. Requires Bazel - * 8.6.0+ or 9.0.1+ which supports `--output=streamed_proto` for this command. - * - * The approach: - * 1. Run `bazel mod dump_repo_mapping ""` to discover the root module's apparent→canonical repo - * name mapping (e.g., "bazel_diff_maven" → "rules_jvm_external++maven+maven"). - * 2. Run `bazel mod show_repo @@... --output=streamed_proto` to get Repository proto - * definitions for each repo (works for both module repos and extension-generated repos). - * 3. Create synthetic `//external:` targets for each repo. This matches how - * `transformRuleInput` in BazelRule.kt collapses `@apparent_name//...` deps to - * `//external:apparent_name`, so the hashing pipeline can detect changes. - */ - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun queryBzlmodRepos(): List { - check(canUseBzlmodShowRepo) { "queryBzlmodRepos requires Bazel 8.6.0+ or 9.0.1+" } - - // Step 1: Get the root module's apparent → canonical repo mapping. - val repoMapping = discoverRepoMapping() - if (repoMapping.isEmpty()) { - logger.w { "No repo mappings discovered, skipping mod show_repo" } - return emptyList() - } - logger.i { "Discovered ${repoMapping.size} repo mappings" } - - // Build reverse map: canonical → list of apparent names - val canonicalToApparent = mutableMapOf>() - for ((apparent, canonical) in repoMapping) { - canonicalToApparent.getOrPut(canonical) { mutableListOf() }.add(apparent) - } - - // Step 2: Fetch repo definitions via `mod show_repo @@... --output=streamed_proto`. - // - // Ask only for repos `bazel mod` can resolve. A workspace running Bzlmod alongside WORKSPACE - // maps its WORKSPACE repos into the root repo mapping too, and show_repo fails the whole batch - // on the first one it does not know -- leaving every Bzlmod repo unhashed. A Bzlmod canonical - // name carries the module separator ('+', or '~' before Bazel 7.1); //external covers the rest. - val canonicalNames = - canonicalToApparent.keys.filter { it.contains('+') || it.contains('~') }.map { "@@$it" } - if (canonicalNames.isEmpty()) { - logger.w { "No bzlmod-canonical repos in the repo mapping, skipping mod show_repo" } - return emptyList() - } - val outputFile = Files.createTempFile(null, ".bin").toFile() - - val cmd: MutableList = - ArrayList().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - add("mod") - add("show_repo") - addAll(canonicalNames) - add("--output=streamed_proto") - } - - logger.i { "Querying bzlmod repos: ${cmd.joinToString()}" } - val result = - process( - *cmd.toTypedArray(), - stdout = Redirect.ToFile(outputFile), - workingDirectory = workingDirectory.toFile(), - stderr = Redirect.PRINT, - destroyForcibly = true, - ) - - if (result.resultCode != 0) { - outputFile.delete() - logger.w { - "bazel mod show_repo failed (exit code ${result.resultCode}), skipping bzlmod repos" - } - return emptyList() - } - - // Step 3: Parse Build.Repository messages and create synthetic targets for each apparent name. - val repos = - outputFile.inputStream().buffered().use { proto -> - mutableListOf().apply { - while (true) { - val repo = Build.Repository.parseDelimitedFrom(proto) ?: break - add(repo) - } - } - } - outputFile.delete() - - // Discover the bzlmod module-graph edges so we can encode the dep relationships between - // synthetic //external:* targets. Without this, a target that depends on @outer//... only - // sees //external:outer's *metadata* hash and never picks up content changes in @outer's - // own bzlmod deps (e.g. @inner). With these edges in place, RuleHasher follows the chain - // //:consumer -> //external:outer -> //external:inner during digest computation, so a - // change inside @inner propagates all the way to the main-repo consumer without the user - // having to enumerate every wrapping repo in --fineGrainedHashExternalRepos. See - // https://github.com/Tinder/bazel-diff/issues/184 (transitive build-time chain) and - // https://github.com/Tinder/bazel-diff/issues/197 (alias-wrap chain). - val moduleGraphJson = modService.getModuleGraphJson() - val moduleDepEdges = - if (moduleGraphJson != null) { - val parser = ModuleGraphParser() - // `bazel mod graph` can return cycles (e.g. rules_go <-> gazelle via the latter's - // dev_dependency). Emitting both directions as rule_inputs on the synthetic - // //external:* targets triggers RuleHasher.CircularDependencyException, so break - // cycles into a deterministic DAG before deriving dep edges. - parser.breakCycles(parser.parseModuleGraphDepEdges(moduleGraphJson)) - } else { - emptyMap() - } - // `bazel mod show_repo` does not populate Repository.module_key in current Bazel, so - // bridge from a module's `name` (always present in `bazel mod graph` output) to that - // repo's `canonical_name` by stripping any trailing `+` suffix produced by - // bzlmod's canonical-name scheme. This is best-effort: it works for the no-version-conflict - // case (canonical = "+" or "+"). Module-extension repos do not appear - // in `bazel mod graph` at all, so they get no synthetic dep edges -- their contents are - // captured via repo metadata + the per-repo content hash below. - val moduleNameToCanonical = mutableMapOf() - for (repo in repos) { - val canonical = repo.canonicalName - val moduleName = canonical.substringBefore('+').ifEmpty { canonical } - // Only register a name -> canonical edge if the canonical "looks like a module repo" - // (single `+`, no extension separator). Skip extension-generated repos like - // "rules_jvm_external++maven+maven". - if (canonical.count { it == '+' } == 1) { - moduleNameToCanonical[moduleName] = canonical - } - } - val canonicalToRootApparent: Map> = - canonicalToApparent.mapValues { it.value.toList() } - - val targets = mutableListOf() - for (repo in repos) { - // Derive this repo's bzlmod module name from its canonical name and look up its direct - // deps in the module graph. Translate each dep's module name -> its canonical name -> - // root-visible apparent name; that's what `BazelRule.transformRuleInput` collapses - // non-fine-grained `@//...` rule_inputs to, so adding `//external:` - // as a rule_input here is what wires up the dep chain. - val moduleName = - repo.canonicalName.takeIf { it.count { c -> c == '+' } == 1 }?.substringBefore('+') - val depApparentNames = - if (moduleName != null) { - moduleDepEdges[moduleName] - .orEmpty() - .mapNotNull { moduleNameToCanonical[it] } - .flatMap { canonicalToRootApparent[it].orEmpty() } - } else { - emptyList() - } - val apparentNames = canonicalToApparent[repo.canonicalName] - if (apparentNames != null) { - for (apparentName in apparentNames) { - targets.add(repositoryToTarget(repo, apparentName, depApparentNames)) - } - } else { - // Fallback: use canonical name if no apparent name mapping exists - targets.add(repositoryToTarget(repo, repo.canonicalName, depApparentNames)) - } - } - - logger.i { "Parsed ${repos.size} bzlmod repos → ${targets.size} synthetic targets" } - return targets - } - - /** - * Converts a Build.Repository proto into a synthetic BazelTarget.Rule named - * `//external:`. This mirrors how WORKSPACE repos appear as `//external:*` targets, - * and matches the names produced by `transformRuleInput` in BazelRule.kt. - * - * For each bzlmod dep of this repo (as discovered from `bazel mod graph`) a corresponding - * `//external:` is added to the rule's `rule_input` list, so [RuleHasher] - * follows the dep chain when computing the digest. For repos backed by a `local_repository` rule - * (which is what `local_path_override` lowers to), the contents of the local directory are also - * rolled into a synthetic `_bazel_diff_content_hash` attribute so file content changes inside the - * repo flip the synthetic target's hash. - */ - private fun repositoryToTarget( - repo: Build.Repository, - targetName: String, - depApparentNames: List - ): BazelTarget.Rule { - val ruleClass = repo.repoRuleName.ifEmpty { "bzlmod_repo" } - - val attributes = repo.attributeList.toMutableList() - val contentHash = computeLocalRepoContentHash(repo) - if (contentHash != null) { - attributes.add( - Build.Attribute.newBuilder() - .setName("_bazel_diff_content_hash") - .setType(Build.Attribute.Discriminator.STRING) - .setStringValue(contentHash) - .build()) - } - - val ruleBuilder = - Build.Rule.newBuilder() - .setName("//external:$targetName") - .setRuleClass(ruleClass) - .addAllAttribute(attributes) - for (dep in depApparentNames.toSortedSet()) { - if (dep != targetName) ruleBuilder.addRuleInput("//external:$dep") - } - - val target = - Build.Target.newBuilder() - .setType(Build.Target.Discriminator.RULE) - .setRule(ruleBuilder) - .build() - return BazelTarget.Rule(target) - } - - /** - * Returns a stable hex sha256 over the files inside a `local_repository`-backed repo on disk, or - * null if the repo is not local-backed or the directory cannot be read. - * - * `local_path_override(module_name = "X", path = "...")` in MODULE.bazel lowers to a - * `local_repository` rule, whose `path` attribute is relative to the workspace root. Hashing that - * directory makes file content edits surface in the synthetic //external:X target's digest, which - * fixes the "external repo file change is invisible" half of - * [#184](https://github.com/Tinder/bazel-diff/issues/184) / - * [#197](https://github.com/Tinder/bazel-diff/issues/197). - */ - private fun computeLocalRepoContentHash(repo: Build.Repository): String? { - if (repo.repoRuleName != "local_repository") return null - val pathAttr = - repo.attributeList.find { - it.name == "path" && it.type == Build.Attribute.Discriminator.STRING - } ?: return null - val pathStr = - pathAttr.stringValue.ifEmpty { - return null - } - val rawPath = java.nio.file.Paths.get(pathStr) - val repoDir = - (if (rawPath.isAbsolute) rawPath.toFile() else workingDirectory.resolve(rawPath).toFile()) - if (!repoDir.exists() || !repoDir.isDirectory) return null - - return try { - val digest = java.security.MessageDigest.getInstance("SHA-256") - repoDir - .walkTopDown() - .filter { it.isFile } - // Skip MODULE.bazel.lock: bazel auto-regenerates it on every invocation in ways - // that don't reflect a real source change (it depends on resolution state). Letting - // it flip the content hash makes generate-hashes non-deterministic across runs. - .filter { it.name != "MODULE.bazel.lock" } - .map { Pair(it.relativeTo(repoDir).invariantSeparatorsPath, it) } - .sortedBy { it.first } - .forEach { (relPath, file) -> - digest.update(relPath.toByteArray(Charsets.UTF_8)) - digest.update(0x00) - digest.update(file.readBytes()) - digest.update(0x00) - } - digest.digest().joinToString("") { "%02x".format(it) } - } catch (e: Exception) { - logger.w { "Failed to content-hash local repo at $repoDir: ${e.message}" } - null - } - } - - /** - * Discovers the root module's apparent→canonical repo name mapping by running `bazel mod - * dump_repo_mapping ""`. Returns a map of apparent name → canonical name. Filters out internal - * repos (bazel_tools, _builtins, local_config_*) that aren't relevant for dependency hashing. - */ - @OptIn(ExperimentalCoroutinesApi::class) - private suspend fun discoverRepoMapping(): Map { - val cmd: MutableList = - ArrayList().apply { - add(bazelPath.toString()) - if (noBazelrc) { - add("--bazelrc=/dev/null") - } - addAll(startupOptions) - add("mod") - add("dump_repo_mapping") - // Empty string = root module's repo mapping - add("") - } - - logger.i { "Discovering repo mapping: ${cmd.joinToString()}" } - val result = - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - workingDirectory = workingDirectory.toFile(), - stderr = Redirect.PRINT, - destroyForcibly = true, - ) - - if (result.resultCode != 0) { - logger.w { "bazel mod dump_repo_mapping failed (exit code ${result.resultCode})" } - return emptyMap() - } - - return try { - val mapping = mutableMapOf() - for (line in result.output) { - val trimmed = line.trim() - if (trimmed.isEmpty()) continue - val json = com.google.gson.JsonParser.parseString(trimmed).asJsonObject - for ((apparent, canonicalElem) in json.entrySet()) { - val canonical = canonicalElem.asString - // Skip internal/infrastructure repos not relevant for dependency hashing. - if (apparent.isEmpty() || - canonical.isEmpty() || - canonical.startsWith("bazel_tools") || - canonical.startsWith("_builtins") || - canonical.startsWith("local_config_") || - canonical.startsWith("rules_java_builtin") || - apparent == "bazel_tools" || - apparent == "local_config_platform") - continue - mapping[apparent] = canonical - } - } - mapping - } catch (e: Exception) { - logger.w { "Failed to parse dump_repo_mapping output: ${e.message}" } - emptyMap() - } - } - - private fun toBazelTarget(target: Build.Target): BazelTarget? { - return when (target.type) { - Build.Target.Discriminator.RULE -> BazelTarget.Rule(target) - Build.Target.Discriminator.SOURCE_FILE -> BazelTarget.SourceFile(target) - Build.Target.Discriminator.GENERATED_FILE -> BazelTarget.GeneratedFile(target) - Build.Target.Discriminator.PACKAGE_GROUP -> packageGroupToTarget(target) - else -> { - logger.w { "Unsupported target type in the build graph: ${target.type.name}" } - null - } - } - } - - /** - * Lowers a PACKAGE_GROUP query target into a synthetic RULE target so it flows through the - * regular rule-hashing pipeline instead of being dropped (issue #441): the group's `packages` - * allow-list becomes a hashed attribute, and its `includes` become rule_inputs so an edit to a - * nested package_group propagates to every group that includes it. [RuleHasher] separately - * follows rules' `visibility` attributes to these targets, which is what carries a group edit to - * the rules the group gates -- and from there, transitively, to their dependents. - */ - private fun packageGroupToTarget(target: Build.Target): BazelTarget.Rule { - val packageGroup = target.packageGroup - val rule = - Build.Rule.newBuilder() - .setName(packageGroup.name) - .setRuleClass("package_group") - .addAttribute( - Build.Attribute.newBuilder() - .setName("packages") - .setType(Build.Attribute.Discriminator.STRING_LIST) - .addAllStringListValue(packageGroup.containedPackageList)) - .addAllRuleInput(packageGroup.includedPackageGroupList) - return BazelTarget.Rule( - Build.Target.newBuilder().setType(Build.Target.Discriminator.RULE).setRule(rule).build()) - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt deleted file mode 100644 index d4f57602..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelRule.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.bazel_diff.bazel - -import com.bazel_diff.hash.safePutBytes -import com.bazel_diff.hash.sha256 -import com.google.devtools.build.lib.query2.proto.proto2api.Build - -// Ignore generator_location when computing a target's hash since it is likely to change and does -// not -// affect a target's generated actions. Internally, Bazel also does this when computing a target's -// hash: -// https://github.com/bazelbuild/bazel/blob/6971b016f1e258e3bb567a0f9fe7a88ad565d8f2/src/main/java/com/google/devtools/build/lib/query2/query/output/SyntheticAttributeHashCalculator.java#L78-L81 -private val DEFAULT_IGNORED_ATTRS = arrayOf("generator_location") - -// Separator used to fold a cquery dep-edge's `configurationChecksum` into a rule-input string. -// Bazel target/package names are restricted to `[A-Za-z0-9/._+=,@~-]*` plus `:` between package -// and target, so `|` is invalid in a label and is a safe in-band encoding that survives the -// existing `List` return type of `ruleInputList()`. `RuleHasher` splits on this character -// to recover the bare label for `allRulesMap` / `sourceDigests` lookups and dep tracking; the -// full encoded string is what gets mixed into the transitive hash, so two configured graphs that -// share dep labels but differ in per-edge configuration produce distinct rule digests. See #359. -const val CONFIGURED_RULE_INPUT_SEPARATOR: Char = '|' - -fun encodeConfiguredRuleInput(input: Build.ConfiguredRuleInput): String { - // Fall back to a bare label when the checksum is empty -- this matches the pre-#359 behaviour - // for the known Bazel quirk where cquery's `transitions=lite` output sometimes omits the - // configurationChecksum for an edge, and avoids appending a dangling separator. - val checksum = input.configurationChecksum - return if (checksum.isNullOrEmpty()) { - input.label - } else { - "${input.label}${CONFIGURED_RULE_INPUT_SEPARATOR}${checksum}" - } -} - -fun decodeConfiguredRuleInputLabel(encoded: String): String { - // No separator present -> the input is a bare label (non-cquery callers, empty-checksum - // fallback, or //external:* synthetic inputs), so this is a no-op. - return encoded.substringBefore(CONFIGURED_RULE_INPUT_SEPARATOR) -} - -class BazelRule(private val rule: Build.Rule) { - fun digest(ignoredAttrs: Set): ByteArray { - return sha256 { - safePutBytes(rule.ruleClassBytes.toByteArray()) - safePutBytes(rule.nameBytes.toByteArray()) - safePutBytes(rule.skylarkEnvironmentHashCodeBytes.toByteArray()) - // Hash attributes in a canonical (name-sorted) order so a rule's digest is invariant to the - // order Bazel happens to emit them in. Attribute names are unique within a rule (the proto - // is generated from the rule's attribute map), so a name sort fully canonicalizes the set. - // Mirrors target-determinator's `sortedAttributesForHashing` (commit d4b6125). - for (attribute in rule.attributeList.sortedBy { it.name }) { - if (!DEFAULT_IGNORED_ATTRS.contains(attribute.name) && - !ignoredAttrs.contains(attribute.name)) - safePutBytes(attribute.toByteArray()) - } - } - } - - fun ruleInputList(useCquery: Boolean, fineGrainedHashExternalRepos: Set): List { - // Transformed //external:* synthetic inputs so that targets depending on external repos pick - // up hash changes from //external:* synthetic targets (e.g. from bzlmod mod show_repo or - // WORKSPACE //external:all-targets). - val externalSyntheticInputs = - rule.ruleInputList - .map { ruleInput: String -> - transformRuleInput(fineGrainedHashExternalRepos, ruleInput) - } - .filter { it.startsWith("//external:") } - - val inputs = - if (useCquery) { - // configuredRuleInputList already covers the non-fine-grained deps (with per-edge - // configuration folded in), so only the //external:* synthetic inputs are added. - rule.configuredRuleInputList.map { encodeConfiguredRuleInput(it) } + - externalSyntheticInputs - } else { - rule.ruleInputList + externalSyntheticInputs - } - - // Canonicalize: dedupe and sort so a target's digest is invariant to the order Bazel happens - // to emit (configured) rule inputs in. This matters most under --useCquery, where the same dep - // label can surface across multiple configurations and cquery does not guarantee a stable - // emission order -- without canonicalization an unchanged target could hash differently - // between two otherwise-identical graphs. RuleHasher mixes these bytes into the digest in list - // order, so a deterministic order is what keeps the hash stable. Mirrors - // target-determinator's `canonicalizeRuleInputs` (commit d4b6125). - return inputs.distinct().sorted() - } - - val name: String = rule.name - - /** - * True for `package_group` targets. `bazel query` reports them under their own PACKAGE_GROUP - * discriminator; [BazelQueryService] lowers them to synthetic rules with this rule class so they - * participate in hashing (issue #441). - */ - val isPackageGroup: Boolean - get() = rule.ruleClass == "package_group" - - /** - * The package_group labels referenced by this rule's `visibility` attribute. A visibility value - * is either a package_group label or one of the special forms -- `//visibility:public`, - * `//visibility:private`, or a package spec (`//foo:__pkg__` / `//foo:__subpackages__`) -- which - * name no target of their own and are filtered out here. - * - * Visibility labels are "nodep" labels: `bazel query` never lists them in `rule_input`, so - * [ruleInputList] cannot surface them and [com.bazel_diff.hash.RuleHasher] follows them - * explicitly instead (issue #441). - */ - fun visibilityPackageGroupLabels(): List = - rule.attributeList - .firstOrNull { it.name == "visibility" } - ?.stringListValueList - .orEmpty() - .filter { - !it.startsWith("//visibility:") && - !it.endsWith(":__pkg__") && - !it.endsWith(":__subpackages__") - } - .distinct() - .sorted() - - /** - * Macro instantiation frames (`"::: "`) naming the BUILD/`.bzl` files that - * produced this rule. Empty unless the query ran with `--proto:instantiation_stack`. - */ - val instantiationStack: List - get() = rule.instantiationStackList - - /** - * The values of this rule's `tags` attribute, or an empty set when the attribute is absent. Bazel - * emits user-declared tags as a `STRING_LIST` attribute named `tags` in the query proto. - * bazel-diff reads them to honour `--alwaysAffectedTags`: a target carrying one of those tags is - * treated as always impacted, because it may read undeclared workspace state at execution time - * (issue #401). This is the Bazel target *tag* `external`, which is unrelated to the external - * *repository* handling behind `--excludeExternalTargets` / `--fineGrainedHashExternalRepos`. - */ - fun tags(): Set = - rule.attributeList.firstOrNull { it.name == "tags" }?.stringListValueList?.toSet() - ?: emptySet() - - private fun transformRuleInput( - fineGrainedHashExternalRepos: Set, - ruleInput: String - ): String { - if (isNotMainRepo(ruleInput) && - ruleInput.startsWith("@") && - // Match on the full repo name (up to the `//` boundary), not a bare string - // prefix: with hub-and-spoke repos (e.g. rules_python's `@pip` hub and - // `@pip_` spokes), a bare prefix check makes every spoke label look - // like it belongs to the fine-grained hub, so it is never rewritten to its - // `//external:` seed and changes stop propagating. - fineGrainedHashExternalRepos.none { ruleInput == it || ruleInput.startsWith("$it//") }) { - val splitRule = ruleInput.split("//".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - if (splitRule.size == 2) { - var externalRule = splitRule[0] - externalRule = externalRule.replaceFirst("@+".toRegex(), "") - return String.format("//external:%s", externalRule) - } - } - return ruleInput - } - - private fun isNotMainRepo(ruleInput: String): Boolean { - return !ruleInput.startsWith("//") && - !ruleInput.startsWith("@//") && - !ruleInput.startsWith("@@//") - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelSourceFileTarget.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelSourceFileTarget.kt deleted file mode 100644 index f8fd863d..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelSourceFileTarget.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.bazel_diff.bazel - -class BazelSourceFileTarget( - val name: String, - val seed: ByteArray, -) diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt deleted file mode 100644 index efa9a38c..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTarget.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.bazel_diff.bazel - -import com.google.devtools.build.lib.query2.proto.proto2api.Build - -sealed class BazelTarget(private val target: Build.Target) { - class SourceFile(target: Build.Target) : BazelTarget(target) { - init { - assert(target.hasSourceFile()) - } - - val sourceFileName: String = target.sourceFile.name - val subincludeList: List = target.sourceFile.subincludeList - override val name: String - get() = sourceFileName - } - - class Rule(target: Build.Target) : BazelTarget(target) { - init { - assert(target.hasRule()) - } - - val rule: BazelRule = BazelRule(target.rule) - override val name: String - get() = rule.name - } - - class GeneratedFile(target: Build.Target) : BazelTarget(target) { - init { - assert(target.hasGeneratedFile()) - } - - val generatedFileName: String = target.generatedFile.name - val generatingRuleName: String = target.generatedFile.generatingRule - override val name: String - get() = generatedFileName - } - - val type: BazelTargetType - get() = - when (target.type) { - Build.Target.Discriminator.RULE -> BazelTargetType.RULE - Build.Target.Discriminator.SOURCE_FILE -> BazelTargetType.SOURCE_FILE - Build.Target.Discriminator.GENERATED_FILE -> BazelTargetType.GENERATED_FILE - Build.Target.Discriminator.PACKAGE_GROUP -> BazelTargetType.PACKAGE_GROUP - Build.Target.Discriminator.ENVIRONMENT_GROUP -> BazelTargetType.ENVIRONMENT_GROUP - else -> BazelTargetType.UNKNOWN - } - - abstract val name: String -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt deleted file mode 100644 index 88a0e8d4..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelTargetType.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.bazel_diff.bazel - -enum class BazelTargetType { - RULE, - SOURCE_FILE, - GENERATED_FILE, - PACKAGE_GROUP, - ENVIRONMENT_GROUP, - UNKNOWN -} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt deleted file mode 100644 index f5e64b04..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt +++ /dev/null @@ -1,309 +0,0 @@ -package com.bazel_diff.bazel - -import com.google.gson.JsonObject -import com.google.gson.JsonParser - -/** Data class representing a module in the dependency graph. */ -data class Module(val key: String, val name: String, val version: String, val apparentName: String) - -/** - * Parses and compares Bazel module graphs to detect changes. - * - * Instead of including the entire module graph in the hash seed (which causes all targets to rehash - * when MODULE.bazel changes), this class identifies which specific modules changed so we can query - * only the targets that depend on those modules. - */ -class ModuleGraphParser { - /** - * Parses the JSON output from `bazel mod graph --output=json`. - * - * Tolerates a non-JSON prefix (e.g. leaked stderr from bazel-diff 17.0.1..18.0.5, which captured - * stderr into moduleGraphJson via Process.kt's captureAll -> - * ProcessBuilder.redirectErrorStream(true)). - * - * @param json The JSON string from bazel mod graph - * @return A map of module keys to Module objects - */ - fun parseModuleGraph(json: String): Map { - val modules = mutableMapOf() - - try { - val root = - try { - JsonParser.parseString(json).asJsonObject - } catch (_: Exception) { - val start = json.indexOf('{') - if (start < 0) return emptyMap() - JsonParser.parseString(json.substring(start)).asJsonObject - } - extractModules(root, modules) - } catch (e: Exception) { - // If parsing fails, return empty map - return emptyMap() - } - - return modules - } - - private fun extractModules(obj: JsonObject, modules: MutableMap) { - val key = obj.get("key")?.asString - val name = obj.get("name")?.asString - val version = obj.get("version")?.asString - val apparentName = obj.get("apparentName")?.asString - - // Bazel's MODULE.bazel spec requires module names to be non-empty; reject - // empty `name` so the synthetic `` entry of an unnamed MODULE.bazel - // doesn't reach downstream canonical-repo matching. - if (key != null && !name.isNullOrEmpty() && version != null && apparentName != null) { - modules[key] = Module(key, name, version, apparentName) - } - - // Recursively extract from dependencies - obj.get("dependencies")?.asJsonArray?.forEach { dep -> - if (dep.isJsonObject) { - extractModules(dep.asJsonObject, modules) - } - } - } - - /** - * Parses the JSON from `bazel mod graph --output=json` and returns each module's direct - * `bazel_dep` neighbours as a `module_name -> [dep_module_name, ...]` map. - * - * Module names (the `name` field of the `module(name = ...)` declaration) are used as the key - * here because the alternative -- `module_key` -- is not always populated on the - * `Build.Repository` protos returned by `bazel mod show_repo`, which is what consumers want to - * look up against. Module names are universally present and sufficient to find a unique row in - * the graph for the common no-multi-version case. - * - * The same module may appear in multiple places in the JSON tree (`bazel mod graph` inlines each - * module once and references it via `unexpanded` afterwards). This method walks every - * `dependencies` array it sees, so even the `unexpanded` references contribute an edge. The - * resulting map is keyed by the parent's `module_name` and contains the union of all direct dep - * names observed across the tree. - * - * Returns an empty map on parse failure (same tolerance as [parseModuleGraph]). - */ - fun parseModuleGraphDepEdges(json: String): Map> { - val edges = mutableMapOf>() - try { - val root = - try { - JsonParser.parseString(json).asJsonObject - } catch (_: Exception) { - val start = json.indexOf('{') - if (start < 0) return emptyMap() - JsonParser.parseString(json.substring(start)).asJsonObject - } - extractDepEdges(root, edges) - } catch (_: Exception) { - return emptyMap() - } - return edges.mapValues { it.value.toList() } - } - - private fun extractDepEdges(obj: JsonObject, edges: MutableMap>) { - val name = obj.get("name")?.asString ?: return - val deps = obj.get("dependencies")?.asJsonArray ?: return - val collected = edges.getOrPut(name) { mutableSetOf() } - for (dep in deps) { - if (!dep.isJsonObject) continue - val depObj = dep.asJsonObject - val depName = depObj.get("name")?.asString ?: continue - collected.add(depName) - // Even if this child is `unexpanded`, recurse to pick up edges from its own expansion - // elsewhere in the tree. - extractDepEdges(depObj, edges) - } - } - - /** - * Returns a copy of [edges] with back-edges removed so the result is acyclic. - * - * `bazel mod graph` legitimately contains cycles: for example `rules_go` declares `bazel_dep(name - * = "gazelle", dev_dependency = True)` while `gazelle` declares `bazel_dep(name = "rules_go")`, - * so the dep graph has `rules_go <-> gazelle`. Feeding both edges into - * [BazelQueryService.queryBzlmodRepos] as `rule_input`s on the synthetic `//external:*` targets - * makes `RuleHasher` recurse infinitely and throw `CircularDependencyException`. We need a - * cycle-free dep DAG before emitting edges. - * - * The algorithm is a single DFS, visiting nodes in lexicographic order with their out-edges also - * sorted. An edge to a node currently on the DFS path is a back-edge (it would close a cycle) and - * is dropped; every other edge is kept. The result is therefore (a) acyclic and (b) deterministic - * across runs. - * - * Dropping the back-edge is conservative: a content change in the dropped-edge target still - * surfaces via its own synthetic `//external:*` target's hash (each repo gets one), so main-repo - * consumers that depend on either side of the cycle still see the change. We only lose the - * ability to propagate through the cycle itself, which is fine because all SCC members are - * co-dependent and a change in any of them already invalidates their own hashes directly. - */ - fun breakCycles(edges: Map>): Map> { - val result = mutableMapOf>() - val visited = mutableSetOf() - val onPath = mutableSetOf() - - fun dfs(node: String) { - if (node in visited) return - onPath.add(node) - val kept = mutableListOf() - for (target in edges[node].orEmpty().sorted()) { - if (target in onPath) continue // back-edge - kept.add(target) - dfs(target) - } - result[node] = kept - onPath.remove(node) - visited.add(node) - } - - for (node in edges.keys.sorted()) dfs(node) - return result - } - - /** - * Result of parsing the bzlmod dependency graph as edges keyed by `apparentName`. - * - * Distinct from [parseModuleGraphDepEdges] (which is keyed by `name` for use by - * [BazelQueryService.queryBzlmodRepos] when synthesising `//external:` rule_input edges): - * this view keys by `apparentName` because the consumer is `--fineGrainedHashExternalRepos`, - * whose tokens are apparent repo names (e.g. `@inner_repo`). - * - * @property edges Map from a module's `apparentName` to the set of `apparentName`s of its direct - * bzlmod dependencies. Populated for every module reached when walking the JSON tree. - * @property rootApparentNames `apparentName`s for the root module(s) (key ``). The root is - * the main repo and is always queried via `//...:all-targets`, so dependents-expansion should - * never add it to a fine-grained set. - */ - data class GraphEdges( - val edges: Map>, - val rootApparentNames: Set, - ) - - /** - * Parses `bazel mod graph --output=json` into apparent-name dependency edges. Tolerates a - * non-JSON prefix (e.g. leaked stderr) using the same recovery as [parseModuleGraph]. - * - * Each module in the JSON tree contributes an edge for every entry in its `dependencies` array. - * `unexpanded` dependency stubs (modules that the bzlmod resolver already described elsewhere in - * the graph) still contribute the edge but are not recursed into to avoid duplicate work. - */ - fun parseModuleGraphEdges(json: String): GraphEdges { - val edges = mutableMapOf>() - val rootApparentNames = mutableSetOf() - try { - val root = - try { - JsonParser.parseString(json).asJsonObject - } catch (_: Exception) { - val start = json.indexOf('{') - if (start < 0) return GraphEdges(emptyMap(), emptySet()) - JsonParser.parseString(json.substring(start)).asJsonObject - } - walkEdges(root, edges, rootApparentNames, isRoot = true) - } catch (_: Exception) { - return GraphEdges(emptyMap(), emptySet()) - } - return GraphEdges(edges.mapValues { it.value.toSet() }, rootApparentNames.toSet()) - } - - private fun walkEdges( - obj: JsonObject, - edges: MutableMap>, - rootApparentNames: MutableSet, - isRoot: Boolean, - ) { - val apparentName = obj.get("apparentName")?.asString ?: return - if (isRoot) rootApparentNames.add(apparentName) - val mySet = edges.getOrPut(apparentName) { mutableSetOf() } - val deps = obj.get("dependencies")?.asJsonArray ?: return - for (dep in deps) { - if (!dep.isJsonObject) continue - val depObj = dep.asJsonObject - val depApparent = depObj.get("apparentName")?.asString ?: continue - mySet.add(depApparent) - val unexpanded = depObj.get("unexpanded")?.asBoolean ?: false - if (!unexpanded) { - walkEdges(depObj, edges, rootApparentNames, isRoot = false) - } - } - } - - /** - * Computes the set of `apparentName`s that transitively depend on any module in [targets] by - * traversing [edges] in reverse. Excludes [rootApparentNames] from the result. - * - * Background: `--fineGrainedHashExternalRepos` opts a leaf module (e.g. `@inner_repo`) into - * per-target hashing. When another bzlmod module (`@middle_repo`) wraps it via alias or re-export - * and the main repo depends only on the wrapper, the wrapper's targets must also be queried for - * the hash chain to reach the leaf. This is the engine for that auto-expansion (issue #197). The - * set returned here is what the caller should add to the user-supplied set. - */ - fun findTransitiveDependents( - edges: Map>, - targets: Set, - rootApparentNames: Set, - ): Set { - if (edges.isEmpty() || targets.isEmpty()) return emptySet() - val reverse = mutableMapOf>() - for ((from, deps) in edges) { - for (dep in deps) { - reverse.getOrPut(dep) { mutableSetOf() }.add(from) - } - } - val out = mutableSetOf() - val visited = targets.toMutableSet() - val queue: ArrayDeque = ArrayDeque(targets) - while (queue.isNotEmpty()) { - val cur = queue.removeFirst() - val parents = reverse[cur] ?: continue - for (p in parents) { - if (p in visited) continue - visited.add(p) - if (p !in rootApparentNames) out.add(p) - queue.addLast(p) - } - } - return out - } - - /** - * Compares two module graphs and returns the keys of modules that changed. - * - * A module is considered changed if: - * - It exists in the new graph but not the old graph (added) - * - It exists in the old graph but not the new graph (removed) - * - It exists in both but has a different version - * - * @param oldGraph Module graph from the starting revision - * @param newGraph Module graph from the final revision - * @return Set of module keys that changed - */ - fun findChangedModules( - oldGraph: Map, - newGraph: Map - ): Set { - val changed = mutableSetOf() - - // Find added and version-changed modules - newGraph.forEach { (key, newModule) -> - val oldModule = oldGraph[key] - if (oldModule == null) { - // Module was added - changed.add(key) - } else if (oldModule.version != newModule.version) { - // Module version changed - changed.add(key) - } - } - - // Find removed modules - oldGraph.keys.forEach { key -> - if (!newGraph.containsKey(key)) { - changed.add(key) - } - } - - return changed - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt b/cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt deleted file mode 100644 index f5e26065..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/BazelDiff.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.bazel_diff.cli - -import picocli.CommandLine -import picocli.CommandLine.Model.CommandSpec -import picocli.CommandLine.Spec - -@CommandLine.Command( - name = "bazel-diff", - description = ["Writes to a file the impacted targets between two Bazel graph JSON files"], - subcommands = - [ - GenerateHashesCommand::class, - GetImpactedTargetsCommand::class, - ExplainCommand::class, - WarmupCommand::class, - FingerprintCommand::class, - ServeCommand::class], - mixinStandardHelpOptions = true, - versionProvider = VersionProvider::class, -) -class BazelDiff : Runnable { - @CommandLine.Option( - names = ["-v", "--verbose"], - description = ["Display query string, missing files and elapsed time"], - scope = CommandLine.ScopeType.INHERIT, - ) - var verbose = false - - @CommandLine.Option( - names = ["--debug"], - hidden = true, - scope = CommandLine.ScopeType.INHERIT, - defaultValue = "\${sys:DEBUG:-false}") - var debug = false - - @Spec lateinit var spec: CommandSpec - - override fun run() { - throw CommandLine.ParameterException(spec.commandLine(), "Missing required subcommand") - } - - fun isVerbose(): Boolean { - return verbose || debug - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/ExplainCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/ExplainCommand.kt deleted file mode 100644 index 0afe6650..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/ExplainCommand.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.cli.converter.ExplainFormatConverter -import com.bazel_diff.di.loggingModule -import com.bazel_diff.di.serialisationModule -import com.bazel_diff.interactor.DeserialiseHashesInteractor -import com.bazel_diff.interactor.ExplainFormat -import com.bazel_diff.interactor.ExplainImpactInteractor -import com.bazel_diff.interactor.ImpactGraphRenderer -import com.bazel_diff.interactor.UnknownTargetException -import com.google.gson.Gson -import java.io.BufferedWriter -import java.io.File -import java.io.FileDescriptor -import java.io.FileWriter -import java.io.IOException -import java.util.concurrent.Callable -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.java.KoinJavaComponent -import picocli.CommandLine - -/** - * Explains *why* a single target was impacted: which upstream target(s) actually changed, and along - * which dependency path the change reached it. - * - * Answers [issue #479](https://github.com/Tinder/bazel-diff/issues/479). Pure post-processing over - * the same three files `get-impacted-targets` consumes -- it runs no Bazel query and needs no - * workspace, so it is cheap enough to run on a CI failure after the fact. - */ -@CommandLine.Command( - name = "explain", - mixinStandardHelpOptions = true, - description = - [ - "Explains why a target was impacted: the upstream target(s) whose own hash changed, " + - "and the dependency path from each down to the queried target. Renders as text, " + - "JSON, Graphviz DOT, or a Mermaid flowchart."], - versionProvider = VersionProvider::class) -class ExplainCommand : Callable { - @CommandLine.ParentCommand private lateinit var parent: BazelDiff - - @CommandLine.Option( - names = ["-sh", "--startingHashes"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The path to the JSON file of target hashes for the initial revision. Run 'generate-hashes' to get this value."], - required = true) - lateinit var startingHashesJSONPath: File - - @CommandLine.Option( - names = ["-fh", "--finalHashes"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The path to the JSON file of target hashes for the final revision. Run 'generate-hashes' to get this value."], - required = true) - lateinit var finalHashesJSONPath: File - - @CommandLine.Option( - names = ["-d", "--depEdgesFile"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "Path to the dependency-edges file written by 'generate-hashes --depEdgesFile'. " + - "Required: attribution is a walk over these edges."], - required = true) - lateinit var depEdgesJSONPath: File - - @CommandLine.Option( - names = ["-t", "--target"], - scope = CommandLine.ScopeType.LOCAL, - description = ["The impacted Bazel label to explain, e.g. '//service/a:app'."], - required = true) - lateinit var target: String - - @CommandLine.Option( - names = ["-f", "--format"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "Output format: \${COMPLETION-CANDIDATES}. 'dot' and 'mermaid' emit a node-link " + - "graph of the blame subgraph, with edges pointing from root cause to queried " + - "target."], - converter = [ExplainFormatConverter::class], - defaultValue = "TEXT") - var format: ExplainFormat = ExplainFormat.TEXT - - @CommandLine.Option( - names = ["-o", "--output"], - scope = CommandLine.ScopeType.LOCAL, - description = ["Filepath to write the explanation to. Defaults to STDOUT."]) - var outputPath: File? = null - - @CommandLine.Option( - names = ["--maxRootCauses"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "Report at most this many root causes, nearest first. The full count is always " + - "reported alongside. 0 means no limit. Default: \${DEFAULT-VALUE}."], - defaultValue = "25") - var maxRootCauses: Int = 25 - - @CommandLine.Option( - names = ["--maxDepth"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "Stop searching this many dependency hops above the queried target. Root causes " + - "further upstream are then not reported (a warning is logged). -1 means no " + - "bound. Default: \${DEFAULT-VALUE}."], - defaultValue = "-1") - var maxDepth: Int = -1 - - @CommandLine.Spec lateinit var spec: CommandLine.Model.CommandSpec - - override fun call(): Int { - org.koin.core.context.GlobalContext.stopKoin() - startKoin { modules(serialisationModule(), loggingModule(parent.isVerbose())) } - - return try { - validate() - val deserialiser = DeserialiseHashesInteractor() - val from = deserialiser.executeTargetHash(startingHashesJSONPath) - val to = deserialiser.executeTargetHash(finalHashesJSONPath) - val depEdges = deserialiser.deserializeDeps(depEdgesJSONPath) - - val explanation = - ExplainImpactInteractor() - .explain(from, to, depEdges, target, maxDepth = maxDepth, maxRoots = maxRootCauses) - - val gson = KoinJavaComponent.get(Gson::class.java) - val rendered = ImpactGraphRenderer(gson).render(explanation, format) - - try { - BufferedWriter( - when (val path = outputPath) { - null -> FileWriter(FileDescriptor.out) - else -> FileWriter(path) - }) - .use { it.write(rendered) } - CommandLine.ExitCode.OK - } catch (e: IOException) { - CommandLine.ExitCode.SOFTWARE - } - } catch (e: UnknownTargetException) { - throw CommandLine.ParameterException(spec.commandLine(), e.message) - } finally { - stopKoin() - } - } - - private fun validate() { - if (!startingHashesJSONPath.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect starting hashes: file doesn't exist or can't be read.") - } - if (!finalHashesJSONPath.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect final hashes: file doesn't exist or can't be read.") - } - if (!depEdgesJSONPath.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect dep edges file: file doesn't exist or can't be read.") - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt deleted file mode 100644 index cf7479b0..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintCommand.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.cli.converter.CommaSeparatedValueConverter -import com.bazel_diff.cli.converter.NormalisingPathConverter -import com.bazel_diff.cli.converter.OptionsConverter -import com.bazel_diff.interactor.FingerprintInteractor -import com.google.gson.GsonBuilder -import java.io.File -import java.nio.file.Path -import java.util.concurrent.Callable -import picocli.CommandLine - -/** - * Computes the Firecracker snapshot cache key ("fingerprint") for the current workspace + flag set - * and writes it as JSON. Used at record time to tag a snapshot and at consume time to validate a - * candidate snapshot before trusting it. See `docs/firecracker-snapshots.md` §5. - * - * Pure metadata: this command does not run `bazel query` and is cheap. The only external call is - * `bazel version`. - */ -@CommandLine.Command( - name = "fingerprint", - mixinStandardHelpOptions = true, - description = - [ - "Computes the snapshot cache key over the inputs that affect the build graph " + - "(bazel version, MODULE.bazel.lock, .bazelrc, bazel-diff version, flag set) and " + - "writes it as JSON. Used to decide whether a Firecracker snapshot is safe to consume."], - versionProvider = VersionProvider::class) -class FingerprintCommand : Callable { - @CommandLine.ParentCommand private lateinit var parent: BazelDiff - - @CommandLine.Option( - names = ["-w", "--workspacePath"], - description = ["Path to Bazel workspace directory."], - scope = CommandLine.ScopeType.INHERIT, - required = true, - converter = [NormalisingPathConverter::class]) - lateinit var workspacePath: Path - - @CommandLine.Option( - names = ["-b", "--bazelPath"], - description = - [ - "Path to Bazel binary. If not specified, the Bazel binary available in PATH will be used."], - scope = CommandLine.ScopeType.INHERIT, - defaultValue = "bazel", - ) - lateinit var bazelPath: Path - - // --- query-affecting flags (must match the corresponding generate-hashes flags) --- - - @CommandLine.Option( - names = ["-so", "--bazelStartupOptions"], - converter = [OptionsConverter::class], - description = - ["Bazel client startup options (must match the consuming generate-hashes run)."]) - var bazelStartupOptions: List = emptyList() - - @CommandLine.Option( - names = ["-co", "--bazelCommandOptions"], - converter = [OptionsConverter::class], - description = ["Bazel command options for `bazel query`."]) - var bazelCommandOptions: List = emptyList() - - @CommandLine.Option( - names = ["--cqueryCommandOptions"], - converter = [OptionsConverter::class], - description = ["Bazel command options for `bazel cquery`."]) - var cqueryCommandOptions: List = emptyList() - - @CommandLine.Option(names = ["--useCquery"], negatable = true) var useCquery = false - - @CommandLine.Option(names = ["--cqueryExpression"]) var cqueryExpression: String? = null - - @CommandLine.Option(names = ["--includeTargetType"], negatable = true) - var includeTargetType = false - - @CommandLine.Option(names = ["-tt", "--targetType"], split = ",") - var targetType: Set? = null - - @CommandLine.Option( - names = ["--fineGrainedHashExternalRepos"], converter = [CommaSeparatedValueConverter::class]) - var fineGrainedHashExternalRepos: Set = emptySet() - - @CommandLine.Option( - names = ["--ignoredRuleHashingAttributes"], converter = [CommaSeparatedValueConverter::class]) - var ignoredRuleHashingAttributes: Set = emptySet() - - @CommandLine.Option(names = ["--excludeExternalTargets"], negatable = true) - var excludeExternalTargets = false - - @CommandLine.Option( - names = ["-k", "--keep_going"], - negatable = true, - defaultValue = "false", - fallbackValue = "true") - var keepGoing = false - - @CommandLine.Option( - names = ["-o", "--output"], - description = ["Path to write the fingerprint JSON. Defaults to STDOUT."], - defaultValue = CommandLine.Parameters.NULL_VALUE) - var outputPath: File? = null - - override fun call(): Int { - val flags = - FingerprintGatherer.canonicalizeFlags( - bazelStartupOptions = bazelStartupOptions, - bazelCommandOptions = bazelCommandOptions, - cqueryCommandOptions = cqueryCommandOptions, - useCquery = useCquery, - cqueryExpression = cqueryExpression, - includeTargetType = includeTargetType, - targetType = targetType, - fineGrainedHashExternalRepos = fineGrainedHashExternalRepos, - ignoredRuleHashingAttributes = ignoredRuleHashingAttributes, - excludeExternalTargets = excludeExternalTargets, - keepGoing = keepGoing, - ) - val inputs = - FingerprintGatherer.gather( - workspacePath = workspacePath, - bazelPath = bazelPath, - bazelDiffVersion = VersionProvider().version.firstOrNull() ?: "unknown", - flags = flags, - ) - - val result = FingerprintInteractor().compute(inputs) - val json = renderFingerprintJson(result, inputs.flags) - - val out = outputPath - if (out != null && out.path != "-") { - out.writeText(json + "\n") - } else { - println(json) - } - return CommandLine.ExitCode.OK - } -} - -/** Renders the fingerprint result + flag set as pretty JSON. Shared with [WarmupCommand]. */ -internal fun renderFingerprintJson( - result: com.bazel_diff.interactor.FingerprintResult, - flags: Map, -): String { - val gson = GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create() - return gson.toJson( - linkedMapOf( - "fingerprint" to result.fingerprint, - "components" to result.components, - "flags" to flags.toSortedMap(), - )) -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt b/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt deleted file mode 100644 index fdfdf0a3..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/FingerprintGatherer.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.interactor.FingerprintInputs -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.TimeUnit - -/** - * Gathers the environment-specific [FingerprintInputs] (reads `MODULE.bazel.lock`, `.bazelrc` + - * imports, runs `bazel version`, reads bazel-diff's version) and canonicalizes the query-affecting - * flag set. - * - * Shared by [FingerprintCommand] and [WarmupCommand] so the cache key is computed identically on - * the record and consume sides. The pure hashing lives in - * [com.bazel_diff.interactor.FingerprintInteractor]; this object is the IO around it. - */ -object FingerprintGatherer { - - /** Canonicalize the flag set into stable key -> value strings. Lists are joined, sets sorted. */ - fun canonicalizeFlags( - bazelStartupOptions: List, - bazelCommandOptions: List, - cqueryCommandOptions: List, - useCquery: Boolean, - cqueryExpression: String?, - includeTargetType: Boolean, - targetType: Set?, - fineGrainedHashExternalRepos: Set, - ignoredRuleHashingAttributes: Set, - excludeExternalTargets: Boolean, - keepGoing: Boolean, - ): Map { - fun list(xs: List) = xs.joinToString(" ") - fun set(xs: Set) = xs.toSortedSet().joinToString(",") - return linkedMapOf( - "bazelStartupOptions" to list(bazelStartupOptions), - "bazelCommandOptions" to list(bazelCommandOptions), - "cqueryCommandOptions" to list(cqueryCommandOptions), - "useCquery" to useCquery.toString(), - "cqueryExpression" to (cqueryExpression ?: ""), - "includeTargetType" to includeTargetType.toString(), - "targetType" to (targetType?.let { set(it) } ?: ""), - "fineGrainedHashExternalRepos" to set(fineGrainedHashExternalRepos), - "ignoredRuleHashingAttributes" to set(ignoredRuleHashingAttributes), - "excludeExternalTargets" to excludeExternalTargets.toString(), - "keepGoing" to keepGoing.toString(), - ) - } - - fun gather( - workspacePath: Path, - bazelPath: Path, - bazelDiffVersion: String, - flags: Map, - ): FingerprintInputs = - FingerprintInputs( - bazelDiffVersion = bazelDiffVersion, - bazelVersion = readBazelVersion(workspacePath, bazelPath), - moduleLockContent = readOptional(workspacePath.resolve("MODULE.bazel.lock")), - bazelrcContents = readBazelrcs(workspacePath), - flags = flags, - ) - - private fun readOptional(path: Path): ByteArray? = - if (Files.isRegularFile(path)) Files.readAllBytes(path) else null - - /** Read `.bazelrc` plus any files it `import`s / `try-import`s (best-effort, one level deep). */ - private fun readBazelrcs(workspacePath: Path): Map { - val result = LinkedHashMap() - val root = workspacePath.resolve(".bazelrc") - readOptional(root)?.let { bytes -> - result[".bazelrc"] = bytes - String(bytes).lineSequence().forEach { line -> - val trimmed = line.trim() - if (trimmed.startsWith("import ") || trimmed.startsWith("try-import ")) { - val raw = trimmed.substringAfter(' ').trim() - val resolved = raw.replace("%workspace%", workspacePath.toString()) - val p = Path.of(resolved) - val abs = if (p.isAbsolute) p else workspacePath.resolve(resolved) - readOptional(abs)?.let { imported -> - result[workspacePath.relativize(abs).toString()] = imported - } - } - } - } - return result - } - - private fun readBazelVersion(workspacePath: Path, bazelPath: Path): String { - return try { - val proc = - ProcessBuilder(bazelPath.toString(), "version") - .directory(workspacePath.toFile()) - .redirectErrorStream(false) - .start() - val output = proc.inputStream.bufferedReader().readText() - proc.waitFor(60, TimeUnit.SECONDS) - output - .lineSequence() - .firstOrNull { it.startsWith("Build label: ") } - ?.removePrefix("Build label: ") - ?.trim() - ?: output - .lineSequence() - .firstOrNull { it.startsWith("bazel ") } - ?.removePrefix("bazel ") - ?.trim() - ?: "unknown" - } catch (e: Exception) { - "unknown" - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt deleted file mode 100644 index 467bac4e..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.cli.converter.CommaSeparatedValueConverter -import com.bazel_diff.cli.converter.NormalisingPathConverter -import com.bazel_diff.cli.converter.OptionsConverter -import com.bazel_diff.di.hasherModule -import com.bazel_diff.di.loggingModule -import com.bazel_diff.di.serialisationModule -import com.bazel_diff.interactor.GenerateHashesInteractor -import java.io.File -import java.nio.file.Path -import java.util.concurrent.Callable -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import picocli.CommandLine - -@CommandLine.Command( - name = "generate-hashes", - mixinStandardHelpOptions = true, - description = - ["Writes to a file the SHA256 hashes for each Bazel Target in the provided workspace."], - versionProvider = VersionProvider::class) -open class GenerateHashesCommand : Callable { - @CommandLine.ParentCommand private lateinit var parent: BazelDiff - - @CommandLine.Option( - names = ["-w", "--workspacePath"], - description = ["Path to Bazel workspace directory."], - scope = CommandLine.ScopeType.INHERIT, - required = true, - converter = [NormalisingPathConverter::class]) - lateinit var workspacePath: Path - - @CommandLine.Option( - names = ["-b", "--bazelPath"], - description = - [ - "Path to Bazel binary. If not specified, the Bazel binary available in PATH will be used."], - scope = CommandLine.ScopeType.INHERIT, - defaultValue = "bazel", - ) - lateinit var bazelPath: Path - - @CommandLine.Option( - names = ["--contentHashPath"], - description = - [ - "Path to content hash json file. It's a map which maps a workspace-relative file path (and `external//` for fine-grained external repo files) to its content hash. Files in this map will skip content hashing and use provided value"], - scope = CommandLine.ScopeType.INHERIT, - required = false) - var contentHashPath: File? = null - - @CommandLine.Option( - names = ["-so", "--bazelStartupOptions"], - description = - ["Additional space separated Bazel client startup options used when invoking Bazel"], - scope = CommandLine.ScopeType.INHERIT, - converter = [OptionsConverter::class], - ) - var bazelStartupOptions: List = emptyList() - - @CommandLine.Option( - names = ["-co", "--bazelCommandOptions"], - description = - ["Additional space separated Bazel command options used when invoking `bazel query`"], - scope = CommandLine.ScopeType.INHERIT, - converter = [OptionsConverter::class], - ) - var bazelCommandOptions: List = emptyList() - - @CommandLine.Option( - names = ["--fineGrainedHashExternalRepos"], - description = - [ - "Comma separate list of external repos in which fine-grained hashes are computed for the targets. By default, external repos are treated as an opaque blob. If an external repo is specified here, bazel-diff instead computes the hash for individual targets. For example, one wants to specify `maven` here if they user rules_jvm_external so that individual third party dependency change won't invalidate all targets in the mono repo."], - scope = CommandLine.ScopeType.INHERIT, - converter = [CommaSeparatedValueConverter::class], - ) - var fineGrainedHashExternalRepos: Set = emptySet() - - @CommandLine.Option( - names = ["--fineGrainedHashExternalReposFile"], - description = - [ - "A text file containing a newline separated list of external repos. Similar to --fineGrainedHashExternalRepos but helps you avoid exceeding max arg length. Mutually exclusive with --fineGrainedHashExternalRepos."]) - var fineGrainedHashExternalReposFile: File? = null - - @CommandLine.Option( - names = ["--useCquery"], - negatable = true, - description = - [ - "If true, use cquery instead of query when generating dependency graphs. Using cquery would yield more accurate build graph at the cost of slower query execution. When this is set, one usually also wants to set `--cqueryCommandOptions` to specify a targeting platform. Note that this flag only works with Bazel 6.2.0 or above because lower versions does not support `--query_file` flag."], - scope = CommandLine.ScopeType.INHERIT) - var useCquery = false - - @CommandLine.Option( - names = ["--includeTargetType"], - negatable = true, - description = - [ - "Whether include target type in the generated JSON or not.\n" + - "If false, the generate JSON schema is: {\"\": \"\"}\n" + - "If true, the generate JSON schema is: {\"\": \"#\" }"], - scope = CommandLine.ScopeType.INHERIT) - var includeTargetType = false - - @CommandLine.Option( - names = ["-tt", "--targetType"], - split = ",", - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The types of targets to filter. Use comma (,) to separate multiple values, e.g. '--targetType=SourceFile,Rule,GeneratedFile'."]) - var targetType: Set? = null - - @CommandLine.Option( - names = ["--cqueryCommandOptions"], - description = - [ - "Additional space separated Bazel command options used when invoking `bazel cquery`. This flag is has no effect if `--useCquery`is false."], - scope = CommandLine.ScopeType.INHERIT, - converter = [OptionsConverter::class], - ) - var cqueryCommandOptions: List = emptyList() - - @CommandLine.Option( - names = ["--cqueryExpression"], - description = - [ - "Custom cquery expression to use instead of the default 'deps(//...:all-targets)'. This allows you to exclude problematic targets (e.g., analysis_test targets that are designed to fail). Example: 'deps(//...:all-targets) except //path/to/failing:target'. This flag has no effect if `--useCquery` is false."], - scope = CommandLine.ScopeType.INHERIT) - var cqueryExpression: String? = null - - @CommandLine.Option( - names = ["-k", "--keep_going"], - negatable = true, - defaultValue = "false", - fallbackValue = "true", - description = - [ - "This flag controls if `bazel query` will be executed with the `--keep_going` flag or not. Enabling this flag lets `bazel query` tolerate failures in your Bazel graph, but may silently drop targets that fail to resolve and produce non-deterministic hashes. Disabling it catches configuration issues by failing loudly. Defaults to `false`"], - scope = CommandLine.ScopeType.INHERIT) - var keepGoing = false - - @CommandLine.Option( - names = ["-s", "--seed-filepaths"], - description = - [ - "A text file containing a newline separated list of filepaths. Each file in this list will be read and its content will be used as a SHA256 seed when determining affected targets in the build graph. Invalidating any of these files will effectively mark all targets as affected."]) - var seedFilepaths: File? = null - - @CommandLine.Parameters( - index = "0", - description = - [ - "The filepath to write the resulting JSON of dictionary target => SHA-256 values. If not specified, the JSON will be written to STDOUT."], - defaultValue = CommandLine.Parameters.NULL_VALUE) - var outputPath: File? = null - - @CommandLine.Option( - names = ["--ignoredRuleHashingAttributes"], - description = ["Attributes that should be ignored when hashing rule targets."], - scope = CommandLine.ScopeType.INHERIT, - converter = [CommaSeparatedValueConverter::class], - ) - var ignoredRuleHashingAttributes: Set = emptySet() - - @CommandLine.Option( - names = ["-d", "--depEdgesFile"], - description = - [ - "Path to the file where dependency edges are written to. If not specified, the dependency edges will not be written to a file. Needed for computing build graph distance metrics. See bazel-diff docs for more details about build graph distance metrics."], - scope = CommandLine.ScopeType.INHERIT, - defaultValue = CommandLine.Parameters.NULL_VALUE) - var depsMappingJSONPath: File? = null - - @CommandLine.Option( - names = ["-m", "--modified-filepaths"], - description = - [ - "A text file containing a newline separated list of filepaths (relative to the workspace) these filepaths should represent the modified files between the specified revisions and will be used to scope what files are hashed during hash generation."]) - var modifiedFilepaths: File? = null - - @CommandLine.Option( - names = ["--excludeExternalTargets"], - negatable = true, - description = - [ - "If true, exclude external targets (do not query //external:all-targets). When Bzlmod is enabled (detected via bazel mod graph), external targets are excluded automatically. Set this when using Bazel with --enable_workspace=false in other configurations. Defaults to false."], - scope = CommandLine.ScopeType.INHERIT) - var excludeExternalTargets = false - - @CommandLine.Option( - names = ["--excludeTargetsQuery"], - description = - [ - "A Bazel query expression whose matched targets are excluded from the generated " + - "hashes via the `except` operator. Applied to the main target universe for both " + - "`query` and `cquery`. Use this to drop targets you never want reported as " + - "impacted, e.g. `manual`-tagged targets: " + - "--excludeTargetsQuery='attr(\"tags\", \"[\\[ ]manual[,\\]]\", //...)'. " + - "Excluded targets are absent from hashing, so a kept target that depended on one " + - "no longer tracks changes to it."], - scope = CommandLine.ScopeType.INHERIT) - var excludeTargetsQuery: String? = null - - @CommandLine.Option( - names = ["--alwaysAffectedTags"], - description = - [ - "Comma separated list of Bazel target tags (e.g. `external`). Any target whose `tags` attribute contains one of these values is always reported as impacted: a per-invocation sentinel is mixed into its hash so a diff of two `generate-hashes` runs always marks it changed. Use this for non-hermetic targets that read undeclared workspace state at execution time (e.g. repo-scanning linters such as buildifier/gofmt/eslint tests) which would otherwise hash as unchanged and be wrongly skipped by target determination. This is the Bazel target `external` *tag* and is unrelated to --excludeExternalTargets / --fineGrainedHashExternalRepos, which concern external *repositories*."], - scope = CommandLine.ScopeType.INHERIT, - converter = [CommaSeparatedValueConverter::class], - ) - var alwaysAffectedTags: Set = emptySet() - - @CommandLine.Spec lateinit var spec: CommandLine.Model.CommandSpec - - override fun call(): Int { - validate(contentHashPath = contentHashPath) - - // Stop any existing Koin instance before starting a new one (for E2E tests) - org.koin.core.context.GlobalContext.stopKoin() - startKoin { - modules( - hasherModule( - workspacePath, - bazelPath, - contentHashPath, - bazelStartupOptions, - bazelCommandOptions, - cqueryCommandOptions, - useCquery, - cqueryExpression, - keepGoing, - depsMappingJSONPath != null, - fineGrainedHashExternalRepos, - fineGrainedHashExternalReposFile, - excludeExternalTargets, - excludeTargetsQuery, - alwaysAffectedTags, - ), - loggingModule(parent.verbose), - serialisationModule(), - ) - } - - return try { - when (GenerateHashesInteractor() - .execute( - seedFilepaths, - outputPath, - depsMappingJSONPath, - ignoredRuleHashingAttributes, - targetType, - includeTargetType, - modifiedFilepaths)) { - true -> CommandLine.ExitCode.OK - false -> CommandLine.ExitCode.SOFTWARE - } - } finally { - stopKoin() - } - } - - private fun validate(contentHashPath: File?) { - contentHashPath?.let { - if (!it.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), - "Incorrect contentHashFilePath: file doesn't exist or can't be read.") - } - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/GetImpactedTargetsCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/GetImpactedTargetsCommand.kt deleted file mode 100644 index 4f6197b8..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/GetImpactedTargetsCommand.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.cli.converter.NormalisingPathConverter -import com.bazel_diff.cli.converter.OptionsConverter -import com.bazel_diff.di.loggingModule -import com.bazel_diff.di.serialisationModule -import com.bazel_diff.interactor.CalculateImpactedTargetsInteractor -import com.bazel_diff.interactor.DeserialiseHashesInteractor -import java.io.BufferedWriter -import java.io.File -import java.io.FileDescriptor -import java.io.FileWriter -import java.io.IOException -import java.nio.file.Path -import java.util.concurrent.Callable -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import picocli.CommandLine - -@CommandLine.Command( - name = "get-impacted-targets", - description = ["Command-line utility to analyze the state of the bazel build graph"], -) -class GetImpactedTargetsCommand : Callable { - @CommandLine.ParentCommand private lateinit var parent: BazelDiff - - @CommandLine.Option( - names = ["-sh", "--startingHashes"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The path to the JSON file of target hashes for the initial revision. Run 'generate-hashes' to get this value."], - required = true, - ) - lateinit var startingHashesJSONPath: File - - @CommandLine.Option( - names = ["-fh", "--finalHashes"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The path to the JSON file of target hashes for the final revision. Run 'generate-hashes' to get this value."], - required = true, - ) - lateinit var finalHashesJSONPath: File - - @CommandLine.Option( - names = ["-d", "--depEdgesFile"], - description = - [ - "Path to the file where dependency edges are. If specified, build graph distance metrics will be computed from the given hash data."], - scope = CommandLine.ScopeType.INHERIT, - defaultValue = CommandLine.Parameters.NULL_VALUE) - var depsMappingJSONPath: File? = null - - @CommandLine.Option( - names = ["-tt", "--targetType"], - split = ",", - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "The types of targets to filter. Use comma (,) to separate multiple values, e.g. '--targetType=SourceFile,Rule,GeneratedFile'."]) - var targetType: Set? = null - - @CommandLine.Option( - names = ["-o", "--output"], - scope = CommandLine.ScopeType.LOCAL, - description = - [ - "Filepath to write the impacted Bazel targets to. If using depEdgesFile: formatted in json, otherwise: newline separated. If not specified, the output will be written to STDOUT."], - ) - var outputPath: File? = null - - @CommandLine.Option( - names = ["-w", "--workspacePath"], - description = ["Path to Bazel workspace directory. Required for module change detection."], - scope = CommandLine.ScopeType.LOCAL, - required = true, - converter = [NormalisingPathConverter::class]) - lateinit var workspacePath: Path - - @CommandLine.Option( - names = ["-b", "--bazelPath"], - description = - [ - "Path to Bazel binary. If not specified, the Bazel binary available in PATH will be used."], - scope = CommandLine.ScopeType.LOCAL, - defaultValue = CommandLine.Parameters.NULL_VALUE) - var bazelPath: Path? = null - - @CommandLine.Option( - names = ["-so", "--bazelStartupOptions"], - description = - ["Additional space separated Bazel client startup options used when invoking Bazel"], - scope = CommandLine.ScopeType.LOCAL, - converter = [OptionsConverter::class]) - var bazelStartupOptions: List = emptyList() - - @CommandLine.Option( - names = ["--noBazelrc"], - negatable = true, - description = ["Don't use .bazelrc"], - scope = CommandLine.ScopeType.LOCAL) - var noBazelrc = false - - @CommandLine.Option( - names = ["--excludeExternalTargets"], - negatable = true, - description = - [ - "If true, drop labels starting with '//external:' from the impacted-targets output. " + - "These synthetic labels are produced for bzlmod-managed external repos so " + - "generate-hashes can detect dep changes, but they are not buildable in " + - "bzlmod-only mode (Bazel 8.6.0+ with --enable_workspace=false) and will fail " + - "downstream `bazel build`. See https://github.com/Tinder/bazel-diff/issues/326. " + - "When unset, defaults to true if Bzlmod is detected (via `bazel mod graph`), " + - "false otherwise."], - scope = CommandLine.ScopeType.LOCAL, - defaultValue = CommandLine.Parameters.NULL_VALUE) - var excludeExternalTargets: Boolean? = null - - @CommandLine.Spec lateinit var spec: CommandLine.Model.CommandSpec - - override fun call(): Int { - // Stop any existing Koin instance before starting a new one (for E2E tests) - org.koin.core.context.GlobalContext.stopKoin() - - // Setup modules - include hasher module for module querying - val resolvedBazelPath = bazelPath ?: java.nio.file.Paths.get("bazel") - startKoin { - modules( - serialisationModule(), - loggingModule(parent.verbose), - com.bazel_diff.di.hasherModule( - workingDirectory = workspacePath, - bazelPath = resolvedBazelPath, - contentHashPath = null, - startupOptions = bazelStartupOptions, - commandOptions = emptyList(), - cqueryOptions = emptyList(), - useCquery = false, - cqueryExpression = null, - keepGoing = false, - trackDeps = false, - fineGrainedHashExternalRepos = emptySet(), - fineGrainedHashExternalReposFile = null, - excludeExternalTargets = false)) - } - - return try { - validate() - val deserialiser = DeserialiseHashesInteractor() - val fromData = deserialiser.executeTargetHashWithMetadata(startingHashesJSONPath) - val toData = deserialiser.executeTargetHashWithMetadata(finalHashesJSONPath) - - // If the user did not pass --[no-]excludeExternalTargets, default to true when bzlmod is - // enabled (synthetic //external:* labels are not buildable in bzlmod-only mode — #326), - // otherwise false to preserve WORKSPACE-mode behavior. - val resolvedExcludeExternalTargets = - excludeExternalTargets - ?: org.koin.java.KoinJavaComponent.get( - com.bazel_diff.bazel.BazelModService::class.java) - .isBzlmodEnabled - - val outputWriter = - BufferedWriter( - when (val path = outputPath) { - null -> FileWriter(FileDescriptor.out) - else -> FileWriter(path) - }) - - try { - if (depsMappingJSONPath != null) { - val depsMapping = deserialiser.deserializeDeps(depsMappingJSONPath!!) - CalculateImpactedTargetsInteractor() - .executeWithDistances( - fromData.hashes, - toData.hashes, - depsMapping, - outputWriter, - targetType, - fromData.moduleGraphJson, - toData.moduleGraphJson, - resolvedExcludeExternalTargets) - } else { - CalculateImpactedTargetsInteractor() - .execute( - fromData.hashes, - toData.hashes, - outputWriter, - targetType, - fromData.moduleGraphJson, - toData.moduleGraphJson, - resolvedExcludeExternalTargets) - } - CommandLine.ExitCode.OK - } catch (e: IOException) { - CommandLine.ExitCode.SOFTWARE - } - } finally { - stopKoin() - } - } - - private fun validate() { - if (!startingHashesJSONPath.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect starting hashes: file doesn't exist or can't be read.") - } - if (!finalHashesJSONPath.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect final hashes: file doesn't exist or can't be read.") - } - if (depsMappingJSONPath != null && !depsMappingJSONPath!!.canRead()) { - throw CommandLine.ParameterException( - spec.commandLine(), "Incorrect dep edges file: file doesn't exist or can't be read.") - } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt deleted file mode 100644 index 412fbdfd..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/ServeCommand.kt +++ /dev/null @@ -1,594 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.cli.converter.ByteSizeConverter -import com.bazel_diff.cli.converter.CommaSeparatedValueConverter -import com.bazel_diff.cli.converter.DurationConverter -import com.bazel_diff.cli.converter.NormalisingPathConverter -import com.bazel_diff.cli.converter.OptionsConverter -import com.bazel_diff.di.hasherModule -import com.bazel_diff.di.loggingModule -import com.bazel_diff.di.serialisationModule -import com.bazel_diff.extensions.toHexString -import com.bazel_diff.hash.sha256 -import com.bazel_diff.server.BazelDiffServer -import com.bazel_diff.server.CachePruneLimits -import com.bazel_diff.server.CachePruner -import com.bazel_diff.server.GitClient -import com.bazel_diff.server.HashCacheStorage -import com.bazel_diff.server.HashProvider -import com.bazel_diff.server.HashService -import com.bazel_diff.server.ImpactedTargetsService -import com.bazel_diff.server.LocalDiskHashCacheStorage -import com.bazel_diff.server.MetricsService -import com.bazel_diff.server.ProcessGitClient -import com.bazel_diff.server.PrunableHashCacheStorage -import com.bazel_diff.server.S3HashCacheStorage -import com.bazel_diff.server.TieredHashCacheStorage -import java.io.File -import java.net.URI -import java.nio.file.Path -import java.time.Duration -import java.util.concurrent.Callable -import java.util.concurrent.CountDownLatch -import java.util.concurrent.atomic.AtomicBoolean -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import picocli.CommandLine - -/** - * Long-running HTTP/JSON query service that answers affectedness queries between two git revisions - * (RFC: issue #29). - * - * The service is scoped to a single workspace clone at [workspacePath]. On startup it performs an - * initial `git fetch` and only then reports healthy. For each request it resolves the `from`/`to` - * revisions, generates (and caches, keyed by SHA) the hashes for each, and reuses the same - * affectedness logic as `get-impacted-targets`. - * - * Query-affecting flags mirror `generate-hashes` so the hashes the service produces match what a - * cold CLI run would produce. - */ -@CommandLine.Command( - name = "serve", - mixinStandardHelpOptions = true, - description = - [ - "Runs bazel-diff as a long-running HTTP query service that returns the impacted targets " + - "between two git revisions, caching generated hashes per commit SHA."], - versionProvider = VersionProvider::class) -open class ServeCommand : Callable { - @CommandLine.ParentCommand private lateinit var parent: BazelDiff - - @CommandLine.Option( - names = ["-w", "--workspacePath"], - description = ["Path to the Bazel workspace git clone the service checks out and queries."], - required = true, - converter = [NormalisingPathConverter::class]) - lateinit var workspacePath: Path - - @CommandLine.Option( - names = ["-b", "--bazelPath"], - description = - [ - "Path to Bazel binary. If not specified, the Bazel binary available in PATH will be used."], - defaultValue = "bazel") - lateinit var bazelPath: Path - - @CommandLine.Option( - names = ["--gitPath"], - description = - [ - "Path to the git binary used for fetch/checkout operations. Defaults to 'git' on the PATH."], - defaultValue = "git") - var gitPath: String = "git" - - @CommandLine.Option( - names = ["--port"], - description = - [ - "Port to listen on. Defaults to 8080. Pass 0 to bind any free port, and " + - "--portFile to find out which one."], - defaultValue = "8080") - var port: Int = 8080 - - @CommandLine.Option( - names = ["--portFile"], - description = - [ - "Write the bound port to this file once the server is listening. The point is " + - "'--port 0 --portFile ': a caller that picks a port itself has to open a " + - "socket to find a free one and close it again before this process can bind, and " + - "anything else on the machine can take the port in between. Binding 0 and " + - "reporting back has no such window. The file appears only after the bind " + - "succeeds, so waiting for it is also waiting for the listener."]) - var portFile: Path? = null - - @CommandLine.Option( - names = ["--requestTimeout"], - description = - [ - "Maximum seconds an /impacted_targets(_with_distances) or /dependency_edges " + - "request may run before the " + - "server abandons it and responds 504. 0 (the default) means no timeout. This " + - "bounds the request the client waits on; an in-flight bazel query may keep " + - "running in the background and still populate the per-SHA cache."], - defaultValue = "0") - var requestTimeoutSeconds: Long = 0 - - @CommandLine.Option( - names = ["--cacheDir"], - description = - ["Directory where generated hashes are cached per commit SHA. Persists across restarts."], - required = true, - converter = [NormalisingPathConverter::class]) - lateinit var cacheDir: Path - - @CommandLine.Option( - names = ["--cacheMaxAge"], - description = - [ - "Evict cached hashes not read or written within this window, so the cache does not " + - "grow without bound over time. Duration like 7d, 36h, 90m (units d/h/m/s). Unset " + - "means no age limit. Enforced by a background sweeper (see --cachePruneInterval)."], - converter = [DurationConverter::class]) - var cacheMaxAge: Duration? = null - - @CommandLine.Option( - names = ["--cacheMaxEntries"], - description = - [ - "Keep at most this many cached commit-SHA entries, evicting the least-recently-used " + - "first. Unset means no count limit."]) - var cacheMaxEntries: Int? = null - - @CommandLine.Option( - names = ["--cacheMaxSize"], - description = - [ - "Keep the cache's total on-disk size at or below this, evicting the " + - "least-recently-used entries first. Size like 10GB, 500MB, or a bare byte count " + - "(base 1024). Unset means no size limit."], - converter = [ByteSizeConverter::class]) - var cacheMaxSize: Long? = null - - @CommandLine.Option( - names = ["--cachePruneInterval"], - description = - [ - "How often the background sweeper enforces the --cacheMax* limits. Duration like 1h, " + - "30m. Defaults to 1h. No effect unless a --cacheMax* limit is set."], - converter = [DurationConverter::class], - defaultValue = "1h") - var cachePruneInterval: Duration = Duration.ofHours(1) - - @CommandLine.Option( - names = ["--s3Bucket"], - description = - [ - "S3 bucket used as a shared hash cache behind the local --cacheDir tier. Generated " + - "hashes are published to the bucket and local cache misses fall back to it, so " + - "replicas behind a load balancer share one cache. Credentials and region come " + - "from the AWS default provider chains (env vars, profile, IRSA, IMDS). Unset " + - "(the default) means local-disk caching only."]) - var s3Bucket: String? = null - - @CommandLine.Option( - names = ["--s3Prefix"], - description = - [ - "Key prefix for cache objects in --s3Bucket, e.g. 'bazel-diff/my-repo'. Defaults to " + - "no prefix (objects at the bucket root)."], - defaultValue = "") - var s3Prefix: String = "" - - @CommandLine.Option( - names = ["--s3Region"], - description = - [ - "AWS region of --s3Bucket. Defaults to the SDK's default region chain (env vars, " + - "profile, IMDS)."]) - var s3Region: String? = null - - @CommandLine.Option( - names = ["--s3Endpoint"], - description = - [ - "Custom S3 endpoint URL for S3-compatible stores (MinIO, LocalStack). Usually " + - "combined with --s3ForcePathStyle."]) - var s3Endpoint: URI? = null - - @CommandLine.Option( - names = ["--s3ForcePathStyle"], - negatable = true, - description = - [ - "Use path-style S3 addressing (bucket in the URL path, not the hostname), required " + - "by most S3-compatible stores. Defaults to false."]) - var s3ForcePathStyle = false - - @CommandLine.Option( - names = ["--warmupRevision"], - description = - [ - "Comma separated git revisions (branch/tag/SHA) whose hashes are generated and cached " + - "at startup, before the server reports healthy, so the first real request is warm " + - "and the Bazel analysis server is primed. Best-effort: a revision that fails to " + - "warm is logged and the server still becomes ready (serving it cold on demand). " + - "Increases time-to-healthy, so size deploy/health-check timeouts accordingly."], - converter = [CommaSeparatedValueConverter::class]) - var warmupRevisions: Set = emptySet() - - @CommandLine.Option( - names = ["--no-initial-fetch"], - description = - [ - "Skip the initial 'git fetch' before reporting healthy. Useful for local/offline testing."]) - var noInitialFetch = false - - @CommandLine.Option( - names = ["-so", "--bazelStartupOptions"], - description = - ["Additional space separated Bazel client startup options used when invoking Bazel"], - converter = [OptionsConverter::class]) - var bazelStartupOptions: List = emptyList() - - @CommandLine.Option( - names = ["-co", "--bazelCommandOptions"], - description = - ["Additional space separated Bazel command options used when invoking `bazel query`"], - converter = [OptionsConverter::class]) - var bazelCommandOptions: List = emptyList() - - @CommandLine.Option( - names = ["--cqueryCommandOptions"], - description = - [ - "Additional space separated Bazel command options used when invoking `bazel cquery`. No effect unless --useCquery is set."], - converter = [OptionsConverter::class]) - var cqueryCommandOptions: List = emptyList() - - @CommandLine.Option( - names = ["--useCquery"], - negatable = true, - description = ["If true, use cquery instead of query when generating dependency graphs."]) - var useCquery = false - - @CommandLine.Option( - names = ["--cqueryExpression"], - description = - ["Custom cquery expression to use instead of the default. No effect unless --useCquery."]) - var cqueryExpression: String? = null - - @CommandLine.Option( - names = ["-k", "--keep_going"], - negatable = true, - defaultValue = "false", - fallbackValue = "true", - description = ["Run `bazel query` with --keep_going. Defaults to false."]) - var keepGoing = false - - @CommandLine.Option( - names = ["--fineGrainedHashExternalRepos"], - description = - ["Comma separated list of external repos for which fine-grained hashes are computed."], - converter = [CommaSeparatedValueConverter::class]) - var fineGrainedHashExternalRepos: Set = emptySet() - - @CommandLine.Option( - names = ["--fineGrainedHashExternalReposFile"], - description = - [ - "A text file with a newline separated list of external repos. Mutually exclusive with --fineGrainedHashExternalRepos."]) - var fineGrainedHashExternalReposFile: File? = null - - @CommandLine.Option( - names = ["--ignoredRuleHashingAttributes"], - description = ["Attributes that should be ignored when hashing rule targets."], - converter = [CommaSeparatedValueConverter::class]) - var ignoredRuleHashingAttributes: Set = emptySet() - - @CommandLine.Option( - names = ["-s", "--seed-filepaths"], - description = - [ - "A text file with a newline separated list of filepaths used as a SHA256 seed for all targets."]) - var seedFilepaths: File? = null - - @CommandLine.Option( - names = ["--excludeExternalTargets"], - negatable = true, - description = ["If true, exclude external targets (do not query //external:all-targets)."]) - var excludeExternalTargets = false - - @CommandLine.Option( - names = ["--excludeTargetsQuery"], - description = - [ - "A Bazel query expression whose matched targets are excluded from the served hashes " + - "via the `except` operator, e.g. `manual`-tagged targets: " + - "--excludeTargetsQuery='attr(\"tags\", \"[\\[ ]manual[,\\]]\", //...)'."]) - var excludeTargetsQuery: String? = null - - @CommandLine.Option( - names = ["--trackDeps"], - negatable = true, - description = - [ - "Track dependency edges and persist them per commit SHA so build-graph distance " + - "metrics can be served via /impacted_targets_with_distances and the " + - "generate-hashes graph via /dependency_edges. Increases cache " + - "size and memory. Defaults to false."]) - var trackDeps = false - - // The background cache sweeper, when a --cacheMax* limit is configured. Held so the shutdown path - // can stop it; null when pruning is disabled or the backend manages its own retention. - private var cachePruner: CachePruner? = null - - override fun call(): Int { - org.koin.core.context.GlobalContext.stopKoin() - startKoin { - modules( - hasherModule( - workspacePath, - bazelPath, - null, - bazelStartupOptions, - bazelCommandOptions, - cqueryCommandOptions, - useCquery, - cqueryExpression, - keepGoing, - trackDeps, - fineGrainedHashExternalRepos, - fineGrainedHashExternalReposFile, - excludeExternalTargets, - excludeTargetsQuery, - ), - loggingModule(parent.verbose), - serialisationModule(), - ) - } - - return try { - val server = buildAndStartServer(createGitClient(), createStorage()) - awaitShutdown(server) - CommandLine.ExitCode.OK - } finally { - stopKoin() - } - } - - /** - * Builds the cache storage stack: the local `--cacheDir` tier, backed by the shared [remote] tier - * when one is configured. [remote] is injectable for tests; the default builds the S3 tier from - * the `--s3*` flags (null -- local-only -- when `--s3Bucket` is unset). - */ - fun createStorage(remote: HashCacheStorage? = createS3Storage()): HashCacheStorage { - val local = LocalDiskHashCacheStorage(cacheDir) - return if (remote == null) local else TieredHashCacheStorage(local, remote) - } - - /** The S3 remote cache tier from the `--s3*` flags, or null when `--s3Bucket` is unset. */ - fun createS3Storage(): S3HashCacheStorage? = - s3Bucket?.let { bucket -> - S3HashCacheStorage( - S3HashCacheStorage.buildClient(s3Region, s3Endpoint, s3ForcePathStyle), - bucket, - s3Prefix) - } - - /** Human-readable location of the shared remote cache tier for `/metrics`; null when unset. */ - fun remoteCacheLocation(): String? = - s3Bucket?.let { "s3://$it/${S3HashCacheStorage.normalizePrefix(s3Prefix)}" } - - /** - * Builds the [GitClient]. Git fetch/checkout operations shell out to the `git` binary at - * [gitPath], so a `git` binary must be available on the host. Overridable in tests so [call] can - * avoid shelling out during the initial-fetch handshake. - */ - open fun createGitClient(): GitClient = ProcessGitClient(workspacePath, gitPath) - - /** - * Wires the services, starts the HTTP server, and performs the initial git fetch + readiness - * handshake. Returns the started [BazelDiffServer]. Requires only a logging/serialisation Koin - * context to be active (the bazel-backed singletons are injected lazily and not touched until an - * `/impacted_targets` request arrives), which keeps this independently unit-testable. - */ - fun buildAndStartServer(gitClient: GitClient, storage: HashCacheStorage): BazelDiffServer { - val hashService = - HashService( - gitClient, - storage, - computeConfigFingerprint(), - loadSeedFilepaths(), - ignoredRuleHashingAttributes, - trackDeps) - val impactedTargetsService = - ImpactedTargetsService(gitClient, hashService, depsTracked = trackDeps) - - val ready = AtomicBoolean(false) - val metrics = - MetricsService( - version = VersionProvider().version.firstOrNull() ?: "unknown", - startedAtMillis = System.currentTimeMillis(), - gitEngine = "subprocess", - trackDeps = trackDeps, - cacheDir = cacheDir.toString(), - remoteCache = remoteCacheLocation(), - storage = storage, - readiness = { ready.get() }, - ) - val server = - BazelDiffServer(port, impactedTargetsService, requestTimeoutSeconds, metrics) { - ready.get() - } - server.start() - writePortFile(server) - performInitialFetch(gitClient, hashService, ready, server) - // Start sweeping only after warmup, so the immediate first pass evaluates an already-warm - // cache. - cachePruner = buildCachePruner(storage)?.also { it.start() } - return server - } - - /** - * The cache-pruning limits requested via the `--cacheMax*` flags (all-null = pruning disabled). - */ - fun cachePruneLimits(): CachePruneLimits = - CachePruneLimits(maxAge = cacheMaxAge, maxEntries = cacheMaxEntries, maxBytes = cacheMaxSize) - - /** - * Builds the background [CachePruner] for [storage], or null when no `--cacheMax*` limit is set - * or the backend manages its own retention. Only a [PrunableHashCacheStorage] can be swept - * in-process -- a remote backend (e.g. S3) would instead rely on a bucket lifecycle policy, so a - * limit set against a non-prunable backend is reported and ignored rather than silently - * pretended-to. - */ - fun buildCachePruner(storage: HashCacheStorage): CachePruner? { - val limits = cachePruneLimits() - if (!limits.hasAny) return null - if (storage !is PrunableHashCacheStorage) { - System.err.println( - "[Warn] --cacheMax* is set but the cache backend does not support in-process pruning; " + - "ignoring (rely on the backend's own retention policy instead)") - return null - } - return CachePruner(storage, limits, cachePruneInterval) - } - - /** - * Performs the startup git fetch (unless [noInitialFetch]) and flips [ready] to true once the - * clone is known good. On a fetch failure the server is left running but un-ready ("lame duck") - * so health checks report `503` and a load balancer removes the instance rather than us - * attempting a risky in-place repair (RFC issue #29). - */ - fun performInitialFetch( - gitClient: GitClient, - hashProvider: HashProvider, - ready: AtomicBoolean, - server: BazelDiffServer - ) { - if (noInitialFetch) { - warmUpCache(gitClient, hashProvider) - ready.set(true) - return - } - try { - gitClient.fetch() - // Warm up before flipping ready so the load balancer only routes to this instance once its - // configured baseline revisions are cached and the Bazel server is primed. - warmUpCache(gitClient, hashProvider) - ready.set(true) - System.err.println("[Info] initial git fetch complete; serving on port ${server.boundPort()}") - } catch (e: Exception) { - System.err.println( - "[Error] initial git fetch failed; server is lame-ducked (health will report NOT_READY): ${e.message}") - } - } - - /** - * Publishes the bound port to `--portFile`, if one was given. - * - * Written to a sibling temporary file and moved into place, so a reader that sees the path at all - * sees a complete number. That matters because the file doubles as a readiness signal for the - * listener: callers poll for it rather than guessing when the bind happened. - */ - fun writePortFile(server: BazelDiffServer) { - val destination = portFile ?: return - destination.toAbsolutePath().parent?.let { java.nio.file.Files.createDirectories(it) } - val temporary = - java.nio.file.Files.createTempFile( - destination.toAbsolutePath().parent, ".${destination.fileName}", ".tmp") - java.nio.file.Files.writeString(temporary, server.boundPort().toString()) - java.nio.file.Files.move( - temporary, destination, java.nio.file.StandardCopyOption.REPLACE_EXISTING) - } - - /** - * Best-effort priming of the hash cache (and, as a side effect, the Bazel analysis server) for - * each `--warmupRevision` before readiness is reported. A revision that fails to warm (bad ref, - * transient bazel error) is logged and skipped rather than failing the deploy: warmup is an - * optimization, not a correctness requirement, so the server still becomes ready and serves that - * revision cold on demand. Never throws, so a bad `--warmupRevision` cannot lame-duck the - * instance. - */ - fun warmUpCache(gitClient: GitClient, hashProvider: HashProvider) { - for (rev in warmupRevisions) { - try { - val sha = gitClient.resolveSha(rev) - System.err.println("[Info] warming hash cache for revision '$rev' ($sha)") - hashProvider.getHashes(sha) - System.err.println("[Info] warmed hash cache for revision '$rev' ($sha)") - } catch (e: Exception) { - System.err.println( - "[Warn] warmup for revision '$rev' failed; server will serve it cold on demand: ${e.message}") - } - } - } - - /** Reads the optional seed filepaths file into a set of paths. */ - fun loadSeedFilepaths(): Set = - seedFilepaths?.readLines()?.filter { it.isNotBlank() }?.map { File(it).toPath() }?.toSet() - ?: emptySet() - - /** - * Blocks until a JVM shutdown signal (or thread interrupt), stopping the server cleanly. - * - * [registerShutdownHook] and [await] default to the real JVM shutdown-hook + latch wait. Tests - * override this method (see [call]) or pass hooks so both the hook path and the interrupt path - * can be covered without hanging on a long-lived server. - */ - open fun awaitShutdown( - server: BazelDiffServer, - registerShutdownHook: (Thread) -> Unit = Runtime.getRuntime()::addShutdownHook, - await: (CountDownLatch) -> Unit = { it.await() }, - ) { - val latch = CountDownLatch(1) - registerShutdownHook( - Thread { - cachePruner?.stop() - server.stop(1) - latch.countDown() - }) - try { - await(latch) - } catch (e: InterruptedException) { - // Treated as a shutdown signal: stop serving and restore the interrupt flag. - cachePruner?.stop() - server.stop(1) - Thread.currentThread().interrupt() - } - } - - /** - * Short digest over the query-affecting flags, mixed into every cache key so hashes generated - * under one configuration are never served to a server started with a different one. The - * workspace SHA already determines file contents (including seed/ignored-attr file contents at - * that commit), so only the flag values themselves need to be captured here. - */ - fun computeConfigFingerprint(): String { - val canonical = buildString { - append("useCquery=").append(useCquery).append('\n') - append("cqueryExpression=").append(cqueryExpression ?: "").append('\n') - append("keepGoing=").append(keepGoing).append('\n') - append("excludeExternalTargets=").append(excludeExternalTargets).append('\n') - append("excludeTargetsQuery=").append(excludeTargetsQuery ?: "").append('\n') - append("bazelCommandOptions=").append(bazelCommandOptions.joinToString(" ")).append('\n') - append("cqueryCommandOptions=").append(cqueryCommandOptions.joinToString(" ")).append('\n') - append("bazelStartupOptions=").append(bazelStartupOptions.joinToString(" ")).append('\n') - append("fineGrainedHashExternalRepos=") - .append(fineGrainedHashExternalRepos.sorted().joinToString(",")) - .append('\n') - append("fineGrainedHashExternalReposFile=") - .append(fineGrainedHashExternalReposFile?.path ?: "") - .append('\n') - append("ignoredRuleHashingAttributes=") - .append(ignoredRuleHashingAttributes.sorted().joinToString(",")) - .append('\n') - // Distinct cache keys for deps-tracked vs deps-less runs: a deps-tracking server must never - // serve a deps-less cache entry (its distance queries would have no edges to traverse). - append("trackDeps=").append(trackDeps).append('\n') - append("version=").append(VersionProvider().version.firstOrNull() ?: "unknown").append('\n') - } - return sha256 { putBytes(canonical.toByteArray()) }.toHexString().take(12) - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt b/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt deleted file mode 100644 index d5222e06..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/VersionProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.bazel_diff.cli - -import java.io.BufferedReader -import java.io.InputStreamReader -import picocli.CommandLine.IVersionProvider - -class VersionProvider( - private val classLoader: ClassLoader = VersionProvider::class.java.classLoader -) : IVersionProvider { - override fun getVersion(): Array { - val inputStream = - classLoader.getResourceAsStream("cli/version") - ?: classLoader.getResourceAsStream("version") - ?: throw IllegalArgumentException( - "unknown version as version file not found in resources") - - val version = BufferedReader(InputStreamReader(inputStream)).use { it.readText().trim() } - return arrayOf(version) - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt deleted file mode 100644 index 8ba4b994..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/WarmupCommand.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.bazel_diff.cli - -import com.bazel_diff.interactor.FingerprintInteractor -import java.io.File -import picocli.CommandLine - -/** - * The Firecracker **record-side entrypoint**. See `docs/firecracker-snapshots.md` §4.1. - * - * `warmup` is `generate-hashes` for the base revision plus two metadata side effects: - * 1. writes the base hashes to a known path (`--base-hashes`, default `/snap/base_hashes.json`), - * 2. writes the fingerprint file (`--fingerprint-output`, default `/snap/fingerprint.json`). - * - * Crucially, it exits `0` **only** once `bazel query` has completed and the server is warm and - * quiesced — the host watches for this clean exit as the "safe to snapshot" signal. - * - * It extends [GenerateHashesCommand] so it inherits the exact same query-affecting flags; warmup is - * deliberately *generate-hashes plus metadata*, never a divergent query path. That guarantees the - * base hashes baked into the snapshot are byte-identical to what a cold `generate-hashes` would - * produce, and that the fingerprint reflects the flags actually used. - */ -@CommandLine.Command( - name = "warmup", - mixinStandardHelpOptions = true, - description = - [ - "Record-side entrypoint for Firecracker snapshots: runs generate-hashes for the base " + - "revision, writes base hashes + fingerprint to known paths, and exits 0 only once " + - "the Bazel server is warm (the host's 'safe to snapshot' signal)."], - versionProvider = VersionProvider::class) -open class WarmupCommand : GenerateHashesCommand() { - - @CommandLine.Option( - names = ["--base-hashes"], - description = ["Path to write the base hashes JSON. Default: /snap/base_hashes.json"], - defaultValue = "/snap/base_hashes.json") - lateinit var baseHashesPath: File - - @CommandLine.Option( - names = ["--fingerprint-output"], - description = ["Path to write the fingerprint JSON. Default: /snap/fingerprint.json"], - defaultValue = "/snap/fingerprint.json") - lateinit var fingerprintOutputPath: File - - override fun call(): Int { - // Route generate-hashes' output to the known base-hashes path. - outputPath = baseHashesPath - baseHashesPath.parentFile?.mkdirs() - - val genResult = runGenerateHashes() - if (genResult != CommandLine.ExitCode.OK) { - // Do not write the fingerprint or signal "safe to snapshot" on a failed warmup. - return genResult - } - - writeFingerprint() - return CommandLine.ExitCode.OK - } - - /** - * Invokes [GenerateHashesCommand.call]. Overridable in tests so [call] can be exercised without a - * real `bazel query`. - */ - open fun runGenerateHashes(): Int = super.call() - - /** - * Computes the fingerprint over the current flag set + workspace and writes it to - * [fingerprintOutputPath]. Split out of [call] so it is unit-testable without the bazel-backed - * `generate-hashes` run that [call] performs via `super.call()`. - */ - fun writeFingerprint() { - val flags = - FingerprintGatherer.canonicalizeFlags( - bazelStartupOptions = bazelStartupOptions, - bazelCommandOptions = bazelCommandOptions, - cqueryCommandOptions = cqueryCommandOptions, - useCquery = useCquery, - cqueryExpression = cqueryExpression, - includeTargetType = includeTargetType, - targetType = targetType, - fineGrainedHashExternalRepos = fineGrainedHashExternalRepos, - ignoredRuleHashingAttributes = ignoredRuleHashingAttributes, - excludeExternalTargets = excludeExternalTargets, - keepGoing = keepGoing, - ) - val inputs = - FingerprintGatherer.gather( - workspacePath = workspacePath, - bazelPath = bazelPath, - bazelDiffVersion = VersionProvider().version.firstOrNull() ?: "unknown", - flags = flags, - ) - val result = FingerprintInteractor().compute(inputs) - - fingerprintOutputPath.parentFile?.mkdirs() - fingerprintOutputPath.writeText(renderFingerprintJson(result, inputs.flags) + "\n") - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt deleted file mode 100644 index c3049764..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/ByteSizeConverter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.bazel_diff.cli.converter - -import picocli.CommandLine.ITypeConverter -import picocli.CommandLine.TypeConversionException - -/** - * Parses a human-friendly byte size such as `10GB`, `500MB`, `2g`, `1kb`, or a bare `1048576` - * (bytes when no unit is given). Units are base 1024 and case-insensitive: `b`, `k`/`kb`, `m`/`mb`, - * `g`/`gb`, `t`/`tb`. Returns the size in bytes. - */ -class ByteSizeConverter : ITypeConverter { - override fun convert(value: String): Long { - val match = - PATTERN.matchEntire(value.trim().lowercase()) - ?: throw TypeConversionException( - "invalid size '$value' (expected e.g. 1048576, 500mb, 10gb)") - val amount = match.groupValues[1].toLong() - val multiplier = - when (match.groupValues[2]) { - "", - "b" -> 1L - "k", - "kb" -> KB - "m", - "mb" -> KB * KB - "g", - "gb" -> KB * KB * KB - "t", - "tb" -> KB * KB * KB * KB - else -> throw TypeConversionException("invalid size unit in '$value'") - } - return amount * multiplier - } - - private companion object { - const val KB = 1024L - val PATTERN = Regex("(\\d+)\\s*([a-z]*)") - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/CommaSeparatedValueConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/CommaSeparatedValueConverter.kt deleted file mode 100644 index b81bff8b..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/CommaSeparatedValueConverter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.bazel_diff.cli.converter - -import picocli.CommandLine.ITypeConverter - -class CommaSeparatedValueConverter : ITypeConverter> { - override fun convert(value: String): Set { - return value.split(",").dropLastWhile { it.isEmpty() }.toSet() - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt deleted file mode 100644 index 6af20b77..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/DurationConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.bazel_diff.cli.converter - -import java.time.Duration -import picocli.CommandLine.ITypeConverter -import picocli.CommandLine.TypeConversionException - -/** - * Parses a human-friendly duration such as `7d`, `36h`, `90m`, `45s`, or a combination like - * `1d12h30m`. Recognised unit suffixes are `d` (days), `h` (hours), `m` (minutes) and `s` - * (seconds). The whole string must be made up of `` tokens; anything else is rejected - * so a typo fails fast rather than being silently truncated. - */ -class DurationConverter : ITypeConverter { - override fun convert(value: String): Duration { - val normalized = value.trim().lowercase() - val tokens = TOKEN.findAll(normalized).toList() - val consumed = tokens.sumOf { it.value.length } - if (tokens.isEmpty() || consumed != normalized.length) { - throw TypeConversionException( - "invalid duration '$value' (expected e.g. 7d, 36h, 90m, 45s, or 1d12h)") - } - var total = Duration.ZERO - for (token in tokens) { - val amount = token.groupValues[1].toLong() - total += - when (token.groupValues[2]) { - "d" -> Duration.ofDays(amount) - "h" -> Duration.ofHours(amount) - "m" -> Duration.ofMinutes(amount) - "s" -> Duration.ofSeconds(amount) - else -> throw TypeConversionException("invalid duration unit in '$value'") - } - } - return total - } - - private companion object { - val TOKEN = Regex("(\\d+)([dhms])") - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/ExplainFormatConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/ExplainFormatConverter.kt deleted file mode 100644 index 315d9279..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/ExplainFormatConverter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.bazel_diff.cli.converter - -import com.bazel_diff.interactor.ExplainFormat -import picocli.CommandLine.ITypeConverter -import picocli.CommandLine.TypeConversionException - -/** - * Parses `explain --format` case-insensitively, so `--format dot` works as well as `--format DOT`. - * - * picocli's built-in enum conversion is case-sensitive, which would reject the lower-case spelling - * everyone actually types (and the one used throughout the docs) for a Kotlin enum whose constants - * are conventionally upper-case. - */ -class ExplainFormatConverter : ITypeConverter { - override fun convert(value: String): ExplainFormat = - ExplainFormat.entries.firstOrNull { it.name.equals(value.trim(), ignoreCase = true) } - ?: throw TypeConversionException( - "invalid format '$value' (expected one of ${ - ExplainFormat.entries.joinToString(", ") { it.name.lowercase() } - })") -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt deleted file mode 100644 index 2626cb9b..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/NormalisingPathConverter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.bazel_diff.cli.converter - -import java.io.File -import java.nio.file.Path -import picocli.CommandLine.ITypeConverter - -class NormalisingPathConverter : ITypeConverter { - override fun convert(value: String): Path = File(value).toPath().normalize() -} diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt b/cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt deleted file mode 100644 index b151b3bf..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/cli/converter/OptionsConverter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.bazel_diff.cli.converter - -import picocli.CommandLine.ITypeConverter - -class OptionsConverter : ITypeConverter> { - override fun convert(value: String): List { - return value.split(" ").dropLastWhile { it.isEmpty() } - } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt b/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt deleted file mode 100644 index 557d4f9f..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.bazel_diff.di - -import com.bazel_diff.bazel.BazelClient -import com.bazel_diff.bazel.BazelModService -import com.bazel_diff.bazel.BazelQueryService -import com.bazel_diff.bazel.ModuleGraphParser -import com.bazel_diff.hash.* -import com.bazel_diff.io.ContentHashProvider -import com.bazel_diff.log.Logger -import com.bazel_diff.log.StderrLogger -import com.bazel_diff.process.Redirect -import com.bazel_diff.process.process -import com.google.gson.GsonBuilder -import java.io.File -import java.nio.file.Path -import java.nio.file.Paths -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import org.koin.core.module.Module -import org.koin.core.qualifier.named -import org.koin.dsl.module - -@OptIn(ExperimentalCoroutinesApi::class) -fun hasherModule( - workingDirectory: Path, - bazelPath: Path, - contentHashPath: File?, - startupOptions: List, - commandOptions: List, - cqueryOptions: List, - useCquery: Boolean, - cqueryExpression: String?, - keepGoing: Boolean, - trackDeps: Boolean, - fineGrainedHashExternalRepos: Set, - fineGrainedHashExternalReposFile: File?, - excludeExternalTargets: Boolean, - excludeTargetsQuery: String? = null, - alwaysAffectedTags: Set = emptySet(), -): Module = module { - if (fineGrainedHashExternalReposFile != null && fineGrainedHashExternalRepos.isNotEmpty()) { - System.err.println( - "Error: fineGrainedHashExternalReposFile and fineGrainedHashExternalRepos are mutually exclusive - please provide only one of them") - System.exit(1) - } - val userSuppliedFineGrainedHashExternalRepos = - fineGrainedHashExternalReposFile?.let { file -> - file.readLines().filter { it.isNotBlank() }.toSet() - } ?: fineGrainedHashExternalRepos - - // Auto-expand the user-supplied fine-grained set with any bzlmod module that transitively - // wraps one of those repos (issue #197). Without this, a main-repo target consuming - // `@middle_repo//:wrapped` -- itself an alias for `@inner_repo//:all_files` -- collapses - // `@middle_repo` into an opaque `//external:middle_repo` blob, so source changes inside - // `@inner_repo` never reach the main consumer. Adding the wrapper to the fine-grained set - // keeps `@middle_repo//:wrapped` as a real ruleInput whose `actual` attribute carries the - // chain down to the leaf. - val updatedFineGrainedHashExternalRepos = - expandFineGrainedHashExternalReposWithBzlmodDependents( - userSuppliedFineGrainedHashExternalRepos, - workingDirectory, - bazelPath, - startupOptions, - ) - - val cmd: MutableList = - ArrayList().apply { - add(bazelPath.toString()) - addAll(startupOptions) - add("info") - add("output_base") - } - val result = runBlocking { - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - workingDirectory = workingDirectory.toFile(), - stderr = Redirect.PRINT, - destroyForcibly = true, - ) - } - val outputPath = Paths.get(result.output.single()) - val debug = System.getProperty("DEBUG", "false").equals("true") - - // issue #401: when the user opts targets in via --alwaysAffectedTags, mint one per-invocation - // random sentinel and hand it to RuleHasher, which mixes it into those targets' hashes so a - // base/head diff always marks them impacted. Left empty (and never mixed) when the feature is - // unused, so every hash stays byte-for-byte identical to a run without the flag. - val alwaysAffectedSeed = - if (alwaysAffectedTags.isEmpty()) ByteArray(0) - else java.util.UUID.randomUUID().toString().toByteArray() - single { - BazelQueryService( - workingDirectory, - bazelPath, - startupOptions, - commandOptions, - cqueryOptions, - keepGoing, - debug) - } - single { BazelModService(workingDirectory, bazelPath, startupOptions, debug) } - single { - BazelClient( - useCquery, - cqueryExpression, - updatedFineGrainedHashExternalRepos, - excludeExternalTargets, - excludeTargetsQuery) - } - single { BuildGraphHasher(get()) } - single { TargetHasher() } - single { - RuleHasher( - useCquery, - trackDeps, - updatedFineGrainedHashExternalRepos, - alwaysAffectedTags, - alwaysAffectedSeed) - } - single { SourceFileHasherImpl(updatedFineGrainedHashExternalRepos) } - single { ExternalRepoResolver(workingDirectory, bazelPath, outputPath) } - single(named("working-directory")) { workingDirectory } - single(named("output-base")) { outputPath } - single { ContentHashProvider(contentHashPath) } -} - -/** - * Returns [userSupplied] together with every bzlmod module that transitively depends on a repo in - * [userSupplied]. Each entry is the apparent-name form prefixed with `@`, matching the format - * consumed by [BazelClient]/[com.bazel_diff.bazel.BazelRule] and the user-facing CLI flag. - * - * If `bazel mod graph --output=json` fails (e.g. bzlmod disabled) or the user supplied nothing, - * this is a no-op and returns [userSupplied] unchanged. This intentionally degrades gracefully for - * WORKSPACE-only setups where the bzlmod graph is unavailable -- the wrapped-repo bug (issue #197) - * is bzlmod-specific anyway. - */ -@OptIn(ExperimentalCoroutinesApi::class) -private fun expandFineGrainedHashExternalReposWithBzlmodDependents( - userSupplied: Set, - workingDirectory: Path, - bazelPath: Path, - startupOptions: List, -): Set { - if (userSupplied.isEmpty()) return userSupplied - - val cmd = - mutableListOf().apply { - add(bazelPath.toString()) - addAll(startupOptions) - add("mod") - add("graph") - add("--output=json") - } - val result = runBlocking { - process( - *cmd.toTypedArray(), - stdout = Redirect.CAPTURE, - stderr = Redirect.SILENT, - workingDirectory = workingDirectory.toFile(), - destroyForcibly = true, - ) - } - if (result.resultCode != 0) return userSupplied - val json = result.output.joinToString("\n").trim() - if (json.isEmpty()) return userSupplied - - val parser = ModuleGraphParser() - val graph = parser.parseModuleGraphEdges(json) - if (graph.edges.isEmpty()) return userSupplied - - // The user-supplied set uses the `@apparentName` form; strip leading `@`/`@@` to match the - // apparent names emitted by `bazel mod graph`. - val apparentTargets = - userSupplied - .map { it.removePrefix("@@").removePrefix("@") } - .filter { it.isNotEmpty() } - .toSet() - val dependents = - parser.findTransitiveDependents(graph.edges, apparentTargets, graph.rootApparentNames) - if (dependents.isEmpty()) return userSupplied - - return userSupplied + dependents.map { "@$it" } -} - -fun loggingModule(verbose: Boolean) = module { single { StderrLogger(verbose) } } - -fun serialisationModule() = module { - single { GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create() } -} diff --git a/cli/src/main/kotlin/com/bazel_diff/extensions/ByteArray.kt b/cli/src/main/kotlin/com/bazel_diff/extensions/ByteArray.kt deleted file mode 100644 index 0ec32589..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/extensions/ByteArray.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.bazel_diff.extensions - -import java.nio.charset.StandardCharsets - -fun ByteArray.toHexString(): String { - val hexChars = ByteArray(size * 2) - for (j in indices) { - val v = get(j).toInt() and 0xFF - hexChars[j * 2] = HEX_ARRAY[v ushr 4] - hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F] - } - return String(hexChars, StandardCharsets.UTF_8) -} - -private val HEX_ARRAY = "0123456789abcdef".toByteArray(StandardCharsets.US_ASCII) diff --git a/cli/src/main/kotlin/com/bazel_diff/extensions/ByteBuffer.kt b/cli/src/main/kotlin/com/bazel_diff/extensions/ByteBuffer.kt deleted file mode 100644 index 98effeda..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/extensions/ByteBuffer.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.bazel_diff.extensions - -import java.nio.Buffer -import java.nio.ByteBuffer - -/** Fix for NoSuchMethodError for JRE8 */ -fun ByteBuffer.compatClear() = ((this as Buffer).clear() as ByteBuffer) - -fun ByteBuffer.compatFlip() = ((this as Buffer).flip() as ByteBuffer) diff --git a/cli/src/main/kotlin/com/bazel_diff/extensions/HashingExtensions.kt b/cli/src/main/kotlin/com/bazel_diff/extensions/HashingExtensions.kt deleted file mode 100644 index 6a7ddfdb..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/extensions/HashingExtensions.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.bazel_diff.hash - -import com.bazel_diff.extensions.compatClear -import com.bazel_diff.extensions.compatFlip -import com.bazel_diff.io.ByteBufferPool -import com.google.common.hash.Hasher -import com.google.common.hash.Hashing -import java.io.BufferedInputStream -import java.io.File -import java.io.FileInputStream - -fun sha256(block: Hasher.() -> Unit): ByteArray { - val hasher = Hashing.sha256().newHasher() - hasher.apply(block) - return hasher.hash().asBytes().clone() -} - -fun Hasher.safePutBytes(block: ByteArray?) = block?.let { putBytes(it) } - -fun Hasher.putFile(file: File) { - BufferedInputStream(FileInputStream(file.absolutePath.toString())).use { stream -> - val buffer = pool.borrow() - val array = buffer!!.array() // Available for non-direct buffers - while (true) { - var length: Int - if (stream.read(array).also { length = it } == -1) break - buffer.compatFlip() - putBytes(array, 0, length) - buffer.compatClear() - } - pool.recycle(buffer) - } -} - -private val pool = ByteBufferPool(1024, 10240) // 10kb diff --git a/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt b/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt deleted file mode 100644 index b221dfbf..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/hash/BuildGraphHasher.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.bazel_diff.hash - -import com.bazel_diff.bazel.BazelClient -import com.bazel_diff.bazel.BazelModService -import com.bazel_diff.bazel.BazelRule -import com.bazel_diff.bazel.BazelSourceFileTarget -import com.bazel_diff.bazel.BazelTarget -import com.bazel_diff.extensions.toHexString -import com.bazel_diff.log.Logger -import com.google.common.collect.Sets -import java.nio.file.Path -import java.util.Calendar -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap -import java.util.concurrent.atomic.AtomicReference -import java.util.stream.Collectors -import kotlin.io.path.readBytes -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -/** - * Mutable per-run phase timings, filled in by [BuildGraphHasher.hashAllBazelTargetsAndSourcefiles] - * when the caller passes one. The phases run sequentially, in field order; [targetHashMillis] also - * covers deriving the seed and per-package `.bzl` seeds that target hashing consumes. - */ -class HasherPhaseTimings { - var bazelQueryMillis: Long = 0 - var sourceHashMillis: Long = 0 - var targetHashMillis: Long = 0 -} - -class BuildGraphHasher(private val bazelClient: BazelClient) : KoinComponent { - private val targetHasher: TargetHasher by inject() - private val sourceFileHasher: SourceFileHasher by inject() - private val bazelModService: BazelModService by inject() - private val logger: Logger by inject() - - fun hashAllBazelTargetsAndSourcefiles( - seedFilepaths: Set = emptySet(), - ignoredAttrs: Set = emptySet(), - modifiedFilepaths: Set = emptySet(), - timings: HasherPhaseTimings? = null - ): Map { - val hashInvocationContext = HashInvocationContext() - val (sourceDigests, allTargets) = - runBlocking { - val queryStartNanos = System.nanoTime() - val targetsTask = async(Dispatchers.IO) { bazelClient.queryAllTargets() } - val allTargets = targetsTask.await() - timings?.bazelQueryMillis = (System.nanoTime() - queryStartNanos) / 1_000_000 - val sourceTargets = - allTargets - .filter { it is BazelTarget.SourceFile } - .map { it as BazelTarget.SourceFile } - - val sourceDigestsFuture = - async(Dispatchers.IO) { - val sourceHashDurationEpoch = Calendar.getInstance().getTimeInMillis() - val sourceFileTargets = hashSourcefiles(sourceTargets, modifiedFilepaths) - val sourceHashDuration = - Calendar.getInstance().getTimeInMillis() - sourceHashDurationEpoch - timings?.sourceHashMillis = sourceHashDuration - logger.i { "Source file hashes calculated in $sourceHashDuration" } - sourceFileTargets - } - - Pair(sourceDigestsFuture.await(), allTargets) - } - val targetHashStartNanos = System.nanoTime() - val seedForFilepaths = runBlocking(Dispatchers.IO) { createSeedForFilepaths(seedFilepaths) } - // With macro instantiation stacks (`--proto:instantiation_stack`), RuleHasher.ruleBzlSeed - // attributes each `.bzl` to the rules a macro produced, so the coarse package seed (and its - // source-file over-invalidation) is dropped. Fall back to it when stacks are absent (#365). - val hasInstantiationStacks = - allTargets.any { it is BazelTarget.Rule && it.rule.instantiationStack.isNotEmpty() } - val packageBzlSeeds = - if (hasInstantiationStacks) emptyMap() - else createPackageBzlSeeds(allTargets, modifiedFilepaths) - val result = - hashAllTargets( - seedForFilepaths, - packageBzlSeeds, - sourceDigests, - allTargets, - ignoredAttrs, - modifiedFilepaths, - hashInvocationContext) - timings?.targetHashMillis = (System.nanoTime() - targetHashStartNanos) / 1_000_000 - return result - } - - private fun hashSourcefiles( - targets: List, - modifiedFilepaths: Set - ): ConcurrentMap { - val exception = AtomicReference(null) - val result: ConcurrentMap = - targets - .parallelStream() - .map { sourceFile: BazelTarget.SourceFile -> - val seed = sha256 { - safePutBytes(sourceFile.name.toByteArray()) - for (subinclude in sourceFile.subincludeList) { - safePutBytes(subinclude.toByteArray()) - } - } - try { - val sourceFileTarget = BazelSourceFileTarget(sourceFile.name, seed) - Pair( - sourceFileTarget.name, - sourceFileHasher.digest(sourceFileTarget, modifiedFilepaths)) - } catch (e: Exception) { - exception.set(e) - null - } - } - .filter { pair -> pair != null } - .collect( - Collectors.toConcurrentMap( - { pair -> pair!!.first }, - { pair -> pair!!.second }, - )) - - exception.get()?.let { throw it } - return result - } - - private fun hashAllTargets( - seedHash: ByteArray, - packageBzlSeeds: Map, - sourceDigests: ConcurrentMap, - allTargets: List, - ignoredAttrs: Set, - modifiedFilepaths: Set, - hashInvocationContext: HashInvocationContext - ): Map { - val ruleHashes: ConcurrentMap = ConcurrentHashMap() - val targetToRule: MutableMap = HashMap() - traverseGraph(allTargets, targetToRule) - - return allTargets - .parallelStream() - .map { target: BazelTarget -> - val targetDigest = - targetHasher.digest( - target, - targetToRule, - sourceDigests, - ruleHashes, - seedHash, - packageBzlSeeds, - ignoredAttrs, - modifiedFilepaths, - hashInvocationContext) - Pair( - target.name, - TargetHash( - target.javaClass.name.substringAfterLast('$'), - targetDigest.overallDigest.toHexString(), - targetDigest.directDigest.toHexString(), - targetDigest.deps, - )) - } - .filter { targetEntry: Pair? -> targetEntry != null } - .collect( - Collectors.toMap( - { obj: Pair -> obj.first }, - { obj: Pair -> obj.second }, - )) - } - - /** Traverses the list of targets and revisits the targets with yet-unknown generating rule */ - private fun traverseGraph( - allTargets: List, - targetToRule: MutableMap - ) { - var targetsToAnalyse: Set = Sets.newHashSet(allTargets) - while (!targetsToAnalyse.isEmpty()) { - val initialSize = targetsToAnalyse.size - val nextTargets: MutableSet = Sets.newHashSet() - for (target in targetsToAnalyse) { - val targetName = target.name - when (target) { - is BazelTarget.GeneratedFile -> { - targetToRule[target.generatingRuleName]?.let { targetToRule[targetName] = it } - ?: nextTargets.add(target) - } - is BazelTarget.Rule -> targetToRule[targetName] = target.rule - is BazelTarget.SourceFile -> continue - } - } - val newSize = nextTargets.size - if (newSize >= initialSize) { - throw RuntimeException("Not possible to traverse the build graph") - } - targetsToAnalyse = nextTargets - } - } - - private suspend fun createSeedForFilepaths(seedFilepaths: Set): ByteArray { - return sha256 { - // Include seed filepaths in hash - for (path in seedFilepaths) { - putBytes(path.readBytes()) - } - } - } - - /** - * Builds a per-package seed-hash contribution from the contents of every `.bzl` (and `.scl`) file - * that package's BUILD file loads, keyed by package label (e.g. `//pkg`, `//` for the root - * package). - * - * Background: Bazel pre-7 populated [Build.Rule.skylark_environment_hash_code] in the query - * proto, so any change to a `.bzl` file loaded by a rule's BUILD file naturally bubbled into that - * rule's hash. Bazel 7+ leaves that field empty, which is the root cause of issues - * [#259](https://github.com/Tinder/bazel-diff/issues/259) and - * [#227](https://github.com/Tinder/bazel-diff/issues/227): editing a macro body (e.g. adding - * `print()`) no longer invalidated any caller because the emitted rule attrs were identical. - * - * Fix: each package's BUILD `SourceFile` target carries a `subincludeList` (the - * `Build.SourceFile.subinclude` proto field) listing every `.bzl` that BUILD loaded. We - * softDigest each main-repo `.bzl` and roll the digests for a given package into a seed - * attributed to that package. Callers then mix only their own package's seed into each target's - * hash, so editing a `.bzl` re-hashes exactly the targets in packages that `load()` it -- not - * every target in the workspace ([#365](https://github.com/Tinder/bazel-diff/issues/365)). A - * package that loads no tracked `.bzl` has no entry here, so nothing is mixed in and its targets - * stay byte-for-byte stable. - * - * External-repo `.bzl` files (`@repo//...`, `@@canonical//...`) are skipped because - * [SourceFileHasher.softDigest] returns null for non-main-repo labels, which keeps the seed - * stable across BCR fetches that don't actually change repo contents. - */ - private fun createPackageBzlSeeds( - allTargets: List, - modifiedFilepaths: Set - ): Map { - // Union the loaded `.bzl` labels per package. Every source file in a package shares the same - // BUILD, but the `subinclude` field is populated on the BUILD's SourceFile target; group by - // package so we don't depend on which source file carries it. - val packageToLabels = sortedMapOf>() - for (target in allTargets) { - if (target !is BazelTarget.SourceFile || target.subincludeList.isEmpty()) continue - packageToLabels - .getOrPut(labelToPackage(target.sourceFileName)) { sortedSetOf() } - .addAll(target.subincludeList) - } - - val result = mutableMapOf() - for ((pkg, labels) in packageToLabels) { - val tracked = mutableListOf>() - for (label in labels) { - val digest = - sourceFileHasher.softDigest( - BazelSourceFileTarget(label, ByteArray(0)), modifiedFilepaths) - if (digest != null) tracked += label to digest - } - if (tracked.isEmpty()) continue - result[pkg] = sha256 { - for ((label, digest) in tracked) { - safePutBytes(label.toByteArray()) - safePutBytes(digest) - } - } - } - return result - } -} - -/** - * Returns the package portion of a Bazel label, i.e. everything before the target separator `:`. - * `//pkg:a` -> `//pkg`, `//:logo` -> `//`, `@@repo//pkg:a` -> `@@repo//pkg`. Labels without a `:` - * (not expected from `bazel query` output) are returned unchanged. - */ -fun labelToPackage(label: String): String { - val colon = label.lastIndexOf(':') - return if (colon >= 0) label.substring(0, colon) else label -} diff --git a/cli/src/main/kotlin/com/bazel_diff/hash/ExternalRepoResolver.kt b/cli/src/main/kotlin/com/bazel_diff/hash/ExternalRepoResolver.kt deleted file mode 100644 index 2dfcbe4a..00000000 --- a/cli/src/main/kotlin/com/bazel_diff/hash/ExternalRepoResolver.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.bazel_diff.hash - -import com.bazel_diff.log.Logger -import com.google.common.cache.CacheBuilder -import com.google.common.cache.CacheLoader -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.util.concurrent.TimeUnit -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class ExternalRepoResolver( - private val workingDirectory: Path, - private val bazelPath: Path, - private val outputBase: Path, -) : KoinComponent { - private val logger: Logger by inject() - - private val externalRoot: Path by lazy { outputBase.resolve("external") } - - private val cache = - CacheBuilder.newBuilder() - .build( - CacheLoader.from { repoName: String -> - externalRoot.resolve(repoName).let { - if (Files.exists(it)) { - return@from it - } - } - - externalRoot.resolve("$repoName+").let { - if (Files.exists(it)) { - return@from it - } - } - - resolveBzlModPath(repoName) - }) - - fun resolveExternalRepoRoot(repoName: String): Path { - return cache.get(repoName) - } - - private fun resolveBzlModPath(repoName: String): Path { - // Query result line should look something like "/external//some/bazel/target: