From 29dc56c1a35e6449c8a08e63f348eca7ae87a2c7 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 27 Jul 2026 15:47:45 +0200 Subject: [PATCH] IJAI-382 declare the AIR docs gate's bun to Bazel; an unrunnable gate now fails The architecture and agent-catalog gates ran docs/scripts/*.ts through community/tools/bun.cmd, which downloads bun into a per-user cache. ADR 0008 accepted the consequence - "the gate on TC is best-effort by design: where bun cannot run, model validation coverage degrades to skipped tests" - and a gate that may skip eventually does, indistinguishably from a green one. MODULE.bazel now fetches the six bun release archives with http_archive, pinned to the same version and checksums as bun.cmd, and @community//build:bun selects the host's. air-plugin-tests_test declares it in data and passes its $(rlocationpath) as -Dair.docs.bun.path; AirDocsRuntime resolves that through runfiles and runs the binary directly, so neither the wrapper, its cache, nor the network is consulted. Outside Bazel the wrapper is still the entry point, since there is no runfiles tree to resolve against. GateResult.Unavailable and the catalog test's infrastructure assumptions are now hard failures. Verified both directions: with ~/Library/Caches/.../monorepo-tools/bun moved aside the target still passes, and pointing the property at a missing file reports FAILED naming the data dependency instead of SKIPPED. The archives are mirrored on packages.jetbrains.team because bazel_downloader.cfg routes github.com through cache-redirector.jetbrains.com, which does not serve the oven-sh/bun repository. BunToolConsistencyTest fails if the MODULE.bazel and bun.cmd pins ever drift. bun.cmd itself is unchanged and remains the entry point for BT, the ij-proxy MCP server, render-guides and pnpm run check. air-plugin-tests_test stays external for the other reason - AirRepoScan walks the working tree and plugins/air is 129 Bazel packages - which NON_SANDBOXED_BASELINE now records. No behavior change outside tests, so no spec update was needed. (cherry picked from commit 3c1ba7e7cca24aaeb68e290cdadfaba4b07a4f0d) GitOrigin-RevId: 380c87ec3940392eb9b24b53bfa479277b815d70 --- MODULE.bazel | 39 +++++++++++ build/BUILD.bazel | 49 +++++++++++++ build/BUILD.bun.bazel | 17 +++++ tools/tests/testSrc/BunToolConsistencyTest.kt | 68 +++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 build/BUILD.bun.bazel create mode 100644 tools/tests/testSrc/BunToolConsistencyTest.kt diff --git a/MODULE.bazel b/MODULE.bazel index ed983e65811b..2a97684af8f0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -124,6 +124,45 @@ override_repo( remote_java_tools_windows = "remote_java_tools_windows_with_vc_redist", ) +# bun as a declared Bazel input, so a test that needs the JS runtime does not depend on +# `tools/bun.cmd` having populated its per-user cache — the wrapper downloads on demand, which a +# sandboxed test cannot do (`sandbox = True` implies `block-network`). See //build:bun for the +# host-selected alias, and plugins/air/docs/decisions/0016-bazel-provided-bun.md for why the AIR +# architecture gate needs it. +# +# The version and checksums must stay in sync with `tools/bun.cmd`; `BunToolConsistencyTest` enforces +# that. The checksums are the upstream release archives', which is what `sha256` here wants too. +BUN_VERSION = "1.3.14" + +# A mirror of the GitHub release, byte-identical: `bazel_downloader.cfg` routes github.com through +# cache-redirector.jetbrains.com, and the redirector does not serve the `oven-sh/bun` repo. +BUN_BASE_URL = "https://packages.jetbrains.team/files/p/ij/intellij-build-dependencies/bun" + +BUN_PLATFORMS = { + "linux_x64": "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f", + "linux_aarch64": "a27ffb63a8310375836e0d6f668ae17fa8d8d18b88c37c821c65331973a19a3b", + "windows_x64": "0a0620930b6675d7ba440e81f4e0e00d3cfbe096c4b140d3fff02205e9e18922", + "windows_aarch64": "89841f5a57f2348b67ec0839b718f4bf4ea7d07c371c9ba4b77b6c790f918953", + "darwin_x64": "4183df3374623e5bab315c547cfa0974533cd457d86b73b639f7a87974cd6633", + "darwin_aarch64": "d8b96221828ad6f97ac7ac0ab7e95872341af763001e8803e8267652c2652620", +} + +[ + http_archive( + name = "bun_%s" % platform, + build_file = "//build:BUILD.bun.bazel", + sha256 = sha256, + # the release zip nests everything under a directory named after the archive + strip_prefix = "bun-%s" % platform.replace("_", "-"), + url = "{base}/{version}/bun-{platform}.zip".format( + base = BUN_BASE_URL, + platform = platform.replace("_", "-"), + version = BUN_VERSION, + ), + ) + for platform, sha256 in BUN_PLATFORMS.items() +] + # Register the extension that runs jps-to-bazel converter and provides target lists for dev-build. # This creates the @jps_dynamic_deps_community repository. use_repo( diff --git a/build/BUILD.bazel b/build/BUILD.bazel index cb75646aec6b..f870f0bde90d 100644 --- a/build/BUILD.bazel +++ b/build/BUILD.bazel @@ -27,6 +27,55 @@ jps_to_bazel_targets_json( visibility = ["//visibility:public"], ) +[ + config_setting( + name = "host_%s" % name, + constraint_values = constraints, + ) + for name, constraints in { + "darwin_aarch64": [ + "@platforms//os:macos", + "@platforms//cpu:arm64", + ], + "darwin_x64": [ + "@platforms//os:macos", + "@platforms//cpu:x86_64", + ], + "linux_aarch64": [ + "@platforms//os:linux", + "@platforms//cpu:arm64", + ], + "linux_x64": [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + "windows_aarch64": [ + "@platforms//os:windows", + "@platforms//cpu:arm64", + ], + "windows_x64": [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], + }.items() +] + +# The bun runtime for the current host, declared in //:MODULE.bazel. Depend on this from `data` and +# resolve it with BazelTestUtil.getFileFromBazelRuntime instead of running `tools/bun.cmd`, which +# needs its per-user cache or the network — neither of which a sandboxed test has. +alias( + name = "bun", + actual = select({ + ":host_darwin_aarch64": "@bun_darwin_aarch64//:binary", + ":host_darwin_x64": "@bun_darwin_x64//:binary", + ":host_linux_aarch64": "@bun_linux_aarch64//:binary", + ":host_linux_x64": "@bun_linux_x64//:binary", + ":host_windows_aarch64": "@bun_windows_aarch64//:binary", + ":host_windows_x64": "@bun_windows_x64//:binary", + }), + visibility = ["//visibility:public"], +) + # Can't name the target 'installers' since it triggers elevation in Windows java_binary( name = "i_build_target", diff --git a/build/BUILD.bun.bazel b/build/BUILD.bun.bazel new file mode 100644 index 000000000000..7d0518dd2b96 --- /dev/null +++ b/build/BUILD.bun.bazel @@ -0,0 +1,17 @@ +# Build file for the `bun_*` archives declared in //:MODULE.bazel. +# The archives are the same GitHub release zips `tools/bun.cmd` pins, with the top-level +# `bun-` directory stripped, so the runtime is a single binary at the root. +# +# Not named `bun`: the extracted binary is itself `bun`, and a rule sharing a source file's name +# makes `:bun` self-referential — Bazel reports it as a dependency cycle. +filegroup( + name = "binary", + srcs = glob( + [ + "bun", + "bun.exe", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/tools/tests/testSrc/BunToolConsistencyTest.kt b/tools/tests/testSrc/BunToolConsistencyTest.kt new file mode 100644 index 000000000000..c783594d6549 --- /dev/null +++ b/tools/tests/testSrc/BunToolConsistencyTest.kt @@ -0,0 +1,68 @@ +package com.intellij.tools.cmd + +import com.intellij.openapi.application.PathManager +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.readText + +/** + * `tools/bun.cmd` and the `bun_*` archives in `MODULE.bazel` must pin the same bun. + * + * They are two independent entry points to the same runtime: the wrapper serves everything outside + * Bazel (`BT`, the ij-proxy MCP server, `pnpm run check`), while the archives serve Bazel tests that + * cannot download anything. If the two pins drift, the same script runs on two different bun versions + * depending on who invoked it, which is the kind of difference that only shows up as an + * unreproducible test failure. + */ +@Timeout(1, unit = TimeUnit.MINUTES) +class BunToolConsistencyTest { + @Test + fun moduleBazelPinsTheSameVersionAsTheWrapper() { + val declared = Regex("""^BUN_VERSION = "([^"]+)"""", RegexOption.MULTILINE).find(moduleBazel.readText()) + assertThat(requireNotNull(declared) { "$moduleBazel declares no BUN_VERSION" }.groupValues[1]) + .describedAs( + "MODULE.bazel BUN_VERSION must match TOOL_VERSION in tools/bun.cmd; update both, and refresh " + + "BUN_PLATFORMS with the new archive checksums" + ) + .isEqualTo(CmdToolTestUtil.parseToolVersion("bun.cmd")) + } + + @Test + fun moduleBazelPinsTheSameChecksumsAsTheWrapper() { + // `sha256` in an http_archive and TOOL_CHECKSUM_* in the wrapper both hash the release archive, + // so they are comparable directly. + val wrapper = CmdToolTestUtil.resolveToolsDir().resolve("bun.cmd").readText() + val expected = WRAPPER_CHECKSUM_VARIABLES.mapValues { (_, variable) -> + val match = Regex("""^export $variable="([0-9a-f]{64})"""", RegexOption.MULTILINE).find(wrapper) + requireNotNull(match) { "tools/bun.cmd declares no $variable" }.groupValues[1] + } + + assertThat(starlarkPlatformChecksums()) + .describedAs("MODULE.bazel BUN_PLATFORMS must match the TOOL_CHECKSUM_* values in tools/bun.cmd") + .isEqualTo(expected) + } + + /** `BUN_PLATFORMS` as declared in `MODULE.bazel`, keyed by the platform used in the archive name. */ + private fun starlarkPlatformChecksums(): Map { + val block = Regex("""BUN_PLATFORMS = \{(.*?)}""", RegexOption.DOT_MATCHES_ALL).find(moduleBazel.readText()) + requireNotNull(block) { "$moduleBazel declares no BUN_PLATFORMS" } + return Regex(""""([a-z0-9_]+)":\s*"([0-9a-f]{64})"""").findAll(block.groupValues[1]) + .associate { it.groupValues[1] to it.groupValues[2] } + } + + private val moduleBazel: Path + get() = Path.of(PathManager.getCommunityHomePath()).resolve("MODULE.bazel") +} + +/** MODULE.bazel platform key to the wrapper variable holding that platform's archive checksum. */ +private val WRAPPER_CHECKSUM_VARIABLES = mapOf( + "linux_x64" to "TOOL_CHECKSUM_LINUX_X64", + "linux_aarch64" to "TOOL_CHECKSUM_LINUX_ARM64", + "windows_x64" to "TOOL_CHECKSUM_WINDOWS_X64", + "windows_aarch64" to "TOOL_CHECKSUM_WINDOWS_ARM64", + "darwin_x64" to "TOOL_CHECKSUM_MACOS_X64", + "darwin_aarch64" to "TOOL_CHECKSUM_MACOS_ARM64", +)