Repository navigation
fix(launcher): scope an inherited transpiler cache to the Bun version it starts #7362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /** | ||
| * Scope an inherited Bun runtime transpiler cache to the Bun version this launcher starts. | ||
| * | ||
| * Bun keys `.pile` entries by source-content hash alone and stamps them with a format | ||
| * version that changes between releases (1.4.2 writes 28, 1.4.3 writes 34). A reader that | ||
| * finds the other format deletes and rewrites the entry. When one explicit cache directory | ||
| * is shared by two Bun versions, each start of one evicts what the other just wrote, so | ||
| * neither ever runs warm. That is the CI shape: the test runner (`testRunnerBun`) and the | ||
| * bundled runtime this launcher starts share the shard's `BUN_RUNTIME_TRANSPILER_CACHE_PATH`, | ||
| * and every launcher spawn re-transpiled its whole CLI graph from nothing. | ||
| * | ||
| * A per-version subdirectory keeps each runtime's entries stable. Bun creates the directory. | ||
| * A disabled cache ("0" or empty), an unset one, or an unknown version is left as given. | ||
| * Windows environment names are case-insensitive (Bun's lookup too) while this object is not, | ||
| * so on win32 every spelling of the name is matched and replaced by one scoped entry. | ||
| */ | ||
| export const TRANSPILER_CACHE_ENV = "BUN_RUNTIME_TRANSPILER_CACHE_PATH"; | ||
|
|
||
| const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?$/; | ||
|
|
||
| /** | ||
| * @param {Record<string, string | undefined>} env | ||
| * @param {string | null | undefined} runtimeVersion | ||
| * @param {(...parts: string[]) => string} join | ||
| * @param {NodeJS.Platform} [platform] | ||
| * @returns {Record<string, string | undefined>} | ||
| */ | ||
| export function scopeTranspilerCacheToRuntime(env, runtimeVersion, join, platform = process.platform) { | ||
| const wanted = TRANSPILER_CACHE_ENV.toLowerCase(); | ||
| const names = platform === "win32" | ||
| ? Object.keys(env).filter(name => name.toLowerCase() === wanted) | ||
| : Object.hasOwn(env, TRANSPILER_CACHE_ENV) ? [TRANSPILER_CACHE_ENV] : []; | ||
| const name = names.includes(TRANSPILER_CACHE_ENV) ? TRANSPILER_CACHE_ENV : names[0]; | ||
| const cache = name === undefined ? undefined : env[name]; | ||
| if (typeof cache !== "string" || cache === "" || cache === "0") return env; | ||
| if (typeof runtimeVersion !== "string" || !VERSION.test(runtimeVersion)) return env; | ||
| const scoped = { ...env }; | ||
| for (const variant of names) delete scoped[variant]; | ||
| scoped[TRANSPILER_CACHE_ENV] = join(cache, `bun-${runtimeVersion}`); | ||
| return scoped; | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { afterEach, describe, expect, test } from "bun:test"; | ||
| import { spawnSync } from "node:child_process"; | ||
| import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { scopeTranspilerCacheToRuntime, TRANSPILER_CACHE_ENV } from "../../src/lib/bun-transpiler-cache-scope.mjs"; | ||
| import { removeTreeWithRetry } from "../helpers/remove-tree"; | ||
| import { repoPath, repoRoot } from "../helpers/repo-root"; | ||
| import { COLD_SPAWN_BUDGET_MS } from "../helpers/test-budget"; | ||
|
|
||
| const roots: string[] = []; | ||
| afterEach(() => { for (const root of roots.splice(0)) removeTreeWithRetry(root); }); | ||
|
|
||
| describe("launcher transpiler cache scope", () => { | ||
| test("an explicit cache is scoped to the started runtime's version", () => { | ||
| const env = { [TRANSPILER_CACHE_ENV]: "/cache", OTHER: "kept" }; | ||
| expect(scopeTranspilerCacheToRuntime(env, "1.4.2", join)).toEqual({ [TRANSPILER_CACHE_ENV]: join("/cache", "bun-1.4.2"), OTHER: "kept" }); | ||
| expect(env[TRANSPILER_CACHE_ENV]).toBe("/cache"); | ||
| }); | ||
|
|
||
| test("a disabled, unset or unknown-version cache is left as given", () => { | ||
| for (const value of ["0", "", undefined]) { | ||
| const env = { [TRANSPILER_CACHE_ENV]: value }; | ||
| expect(scopeTranspilerCacheToRuntime(env, "1.4.2", join)).toBe(env); | ||
| } | ||
| const env = { [TRANSPILER_CACHE_ENV]: "/cache" }; | ||
| for (const version of [null, undefined, "", "../1.4.2", "1.4", "latest"]) { | ||
| expect(scopeTranspilerCacheToRuntime(env, version, join)).toBe(env); | ||
| } | ||
| }); | ||
|
|
||
| test("Windows matches every spelling of the name and leaves one scoped entry", () => { | ||
| const env = { Bun_Runtime_Transpiler_Cache_Path: "C:\\cache", OTHER: "kept" }; | ||
| expect(scopeTranspilerCacheToRuntime(env, "1.4.2", join, "win32")) | ||
| .toEqual({ [TRANSPILER_CACHE_ENV]: join("C:\\cache", "bun-1.4.2"), OTHER: "kept" }); | ||
| const both = { [TRANSPILER_CACHE_ENV]: "C:\\exact", bun_runtime_transpiler_cache_path: "C:\\other" }; | ||
| expect(scopeTranspilerCacheToRuntime(both, "1.4.2", join, "win32")) | ||
| .toEqual({ [TRANSPILER_CACHE_ENV]: join("C:\\exact", "bun-1.4.2") }); | ||
| // POSIX names are case-sensitive: Bun never reads the lower-case spelling there. | ||
| const posix = { bun_runtime_transpiler_cache_path: "/cache" }; | ||
| expect(scopeTranspilerCacheToRuntime(posix, "1.4.2", join, "linux")).toBe(posix); | ||
| }); | ||
|
|
||
| // CI shares one cache between the test-runner Bun and the bundled Bun the launcher starts. | ||
| // Their .pile formats differ and each deletes the other's entries, so before this scope every | ||
| // launcher child re-transpiled its CLI graph cold (15 s spawnSync timeouts on Windows). | ||
| test.skipIf(spawnSync("node", ["--version"], { windowsHide: true }).status !== 0)( | ||
| "the published launcher writes its runtime's entries beside the runner's, never over them", () => { | ||
| const cache = mkdtempSync(join(tmpdir(), "ocx-transpiler-scope-")); | ||
| roots.push(cache); | ||
| const bundled = JSON.parse(readFileSync(repoPath("node_modules", "bun", "package.json"), "utf8")).version as string; | ||
| const result = spawnSync("node", [repoPath("bin", "ocx.mjs"), "--version"], { | ||
| cwd: repoRoot(), encoding: "utf8", timeout: COLD_SPAWN_BUDGET_MS - 5_000, windowsHide: true, | ||
| env: { ...process.env, [TRANSPILER_CACHE_ENV]: cache, OPENCODEX_BUN_PATH: "" }, | ||
| }); | ||
| expect(result.error).toBeUndefined(); | ||
| expect(result.status, result.stderr).toBe(0); | ||
| expect(readdirSync(cache)).toEqual([`bun-${bundled}`]); | ||
| expect(existsSync(join(cache, `bun-${bundled}`)) && readdirSync(join(cache, `bun-${bundled}`)).length > 0).toBe(true); | ||
| }, COLD_SPAWN_BUDGET_MS); | ||
| }); | ||
|
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the bundled runtime is unavailable and an accepted PATH Bun reports a version with build metadata such as
1.4.0+build.1,findPathBunreturns that version (andtests/cli/ocx-launcher-runtime.test.tsexplicitly requires it), but this regex rejects it. The launcher consequently leaves the shared cache unscoped, recreating the cross-version eviction and Windows timeout behavior this change is intended to fix; align this validation with the PATH resolver's accepted stable-version grammar.Useful? React with 👍 / 👎.