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", +)