Skip to content

Commit 72db422

Browse files
committed
refactor(launcher): share Bun binary validation
1 parent e8f04a5 commit 72db422

6 files changed

Lines changed: 39 additions & 54 deletions

File tree

bin/ocx.mjs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
*/
1111
import { spawn, spawnSync } from "node:child_process";
1212
import { createRequire } from "node:module";
13-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
13+
import { existsSync, readFileSync, readdirSync } from "node:fs";
1414
import { homedir } from "node:os";
1515
import { dirname, join, resolve } from "node:path";
1616
import { fileURLToPath } from "node:url";
17+
import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";
1718
import { npmInvocation } from "../src/update/npm-invocation.mjs";
1819
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "../src/update/tray-update-plan.mjs";
1920

@@ -287,23 +288,8 @@ function bunBinDir() {
287288
return dirname(require.resolve("bun/package.json"));
288289
}
289290

290-
// The `bun` package ships a tiny ASCII placeholder at bin/bun.exe until its
291-
// postinstall downloads the real ~60MB binary. --ignore-scripts / pnpm leave
292-
// the ~450-byte stub in place, which is NOT executable (ENOEXEC). A size gate
293-
// cleanly distinguishes the stub from a real binary on every platform. Keep
294-
// this Node-safe copy aligned with src/lib/bun-runtime.ts: the launcher cannot
295-
// import Bun-native TypeScript until after it has resolved a Bun executable.
296-
const REAL_BUN_MIN_BYTES = 1_000_000;
297291
const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
298292

299-
function isRealBunBinary(path) {
300-
try {
301-
return existsSync(path) && statSync(path).size >= REAL_BUN_MIN_BYTES;
302-
} catch {
303-
return false;
304-
}
305-
}
306-
307293
function findBunBinary(bunDir) {
308294
// The npm `bun` package ships the binary as bin/bun.exe on every platform;
309295
// probe bin/bun too for forward compatibility.

src/lib/bun-binary-validator.d.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export declare const REAL_BUN_MIN_BYTES: number;
2+
3+
export declare function isRealBunBinary(path: string): boolean;

src/lib/bun-binary-validator.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { existsSync, statSync } from "node:fs";
2+
3+
// The `bun` package leaves a tiny ASCII placeholder at bin/bun.exe until its
4+
// postinstall downloads the real ~60MB binary. Keep the threshold and the
5+
// false-on-filesystem-error contract shared by the Node launcher and Bun code.
6+
export const REAL_BUN_MIN_BYTES = 1_000_000;
7+
8+
/**
9+
* @param {string} path
10+
* @returns {boolean}
11+
*/
12+
export function isRealBunBinary(path) {
13+
try {
14+
return existsSync(path) && statSync(path).size >= REAL_BUN_MIN_BYTES;
15+
} catch {
16+
return false;
17+
}
18+
}

src/lib/bun-runtime.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
* back to `process.execPath` (which is itself Bun when run via `bun src/cli/index.ts`).
1212
*/
1313
import { createRequire } from "node:module";
14-
import { existsSync, statSync } from "node:fs";
1514
import { dirname, join } from "node:path";
15+
import { isRealBunBinary } from "./bun-binary-validator.mjs";
16+
17+
export { isRealBunBinary };
1618

1719
const require = createRequire(import.meta.url);
1820

19-
// The `bun` package leaves a tiny ASCII placeholder at bin/bun.exe until its
20-
// postinstall downloads the real ~60MB binary; reject the stub by size so we
21-
// never bake a non-executable path into durable artifacts.
22-
const REAL_BUN_MIN_BYTES = 1_000_000;
2321
const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
2422

2523
export type DurableBunRuntime = {
@@ -28,19 +26,6 @@ export type DurableBunRuntime = {
2826
overrideEnv: typeof BUN_OVERRIDE_ENV;
2927
};
3028

31-
/**
32-
* True only for a real, downloaded Bun binary — not the ~450-byte ASCII
33-
* placeholder stub left by `--ignore-scripts` / pnpm. A size gate cleanly
34-
* separates the two on every platform (real binary is tens of MB).
35-
*/
36-
export function isRealBunBinary(path: string): boolean {
37-
try {
38-
return existsSync(path) && statSync(path).size >= REAL_BUN_MIN_BYTES;
39-
} catch {
40-
return false;
41-
}
42-
}
43-
4429
/**
4530
* Absolute path to the bundled Bun binary, or null if the `bun` dependency is
4631
* not installed/resolvable (or only the un-downloaded placeholder is present).

tests/ocx-launcher-runtime.test.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,6 @@ function removeTree(path: string): void {
177177

178178
async function effectiveRuntime(override: string): Promise<string> {
179179
const root = mkdtempSync(join(tmpdir(), "ocx-launcher-runtime-"));
180-
const opencodexHome = join(root, "opencodex");
181-
const codexHome = join(root, "codex");
182-
const grokHome = join(root, "grok");
183180
let port: number | null = null;
184181
let launcher: ChildProcess | null = null;
185182
let launcherPid: number | null = null;
@@ -189,22 +186,11 @@ async function effectiveRuntime(override: string): Promise<string> {
189186
let hasPrimaryError = false;
190187
let primaryError: unknown;
191188
try {
192-
mkdirSync(opencodexHome, { recursive: true });
193-
mkdirSync(codexHome, { recursive: true });
194-
mkdirSync(grokHome, { recursive: true });
195189
port = await freePort();
196190
launcher = spawn("node", [BIN_OCX, "start", "--port", String(port)], {
197191
stdio: "ignore",
198192
windowsHide: true,
199-
env: {
200-
...process.env,
201-
HOME: root,
202-
USERPROFILE: root,
203-
OPENCODEX_HOME: opencodexHome,
204-
CODEX_HOME: codexHome,
205-
GROK_HOME: grokHome,
206-
OPENCODEX_BUN_PATH: override,
207-
},
193+
env: isolatedLauncherEnv(root, override),
208194
});
209195
if (!launcher.pid) throw new Error("Node launcher has no process id");
210196
launcherPid = launcher.pid;

tests/ocx-launcher-source.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { join } from "node:path";
77
* cannot be imported by tests. Guard its Windows-critical invariants at the source level.
88
*/
99
const source = readFileSync(join(import.meta.dir, "..", "bin", "ocx.mjs"), "utf8");
10+
const runtimeSource = readFileSync(join(import.meta.dir, "..", "src", "lib", "bun-runtime.ts"), "utf8");
11+
const validatorSource = readFileSync(
12+
join(import.meta.dir, "..", "src", "lib", "bun-binary-validator.mjs"),
13+
"utf8",
14+
);
1015

1116
describe("ocx.mjs npm launcher (source invariants)", () => {
1217
test("Windows npm spawns use the trusted absolute invocation without shell lookup", () => {
@@ -51,14 +56,16 @@ describe("ocx.mjs npm launcher (source invariants)", () => {
5156
});
5257

5358
test("invalid Bun overrides warn safely and fall back without throwing", () => {
54-
expect(source).toContain("function isRealBunBinary(path) {");
55-
expect(source).toMatch(/function isRealBunBinary\(path\) \{[\s\S]*?try \{[\s\S]*?statSync\(path\)[\s\S]*?catch \{[\s\S]*?return false;/);
59+
expect(source).toContain('import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";');
5660
expect(source).toContain("is missing, unreadable, or not a complete Bun binary; falling back to the bundled runtime.");
5761
expect(source).not.toContain('${override} is missing, unreadable');
5862
});
5963

60-
test("documents why the Node launcher keeps a synchronized validator copy", () => {
61-
expect(source).toContain("this Node-safe copy aligned with src/lib/bun-runtime.ts");
62-
expect(source).toContain("cannot\n// import Bun-native TypeScript until after it has resolved a Bun executable");
64+
test("shares the Node-safe Bun binary validator across both runtime paths", () => {
65+
expect(source).toContain('import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";');
66+
expect(runtimeSource).toContain('import { isRealBunBinary } from "./bun-binary-validator.mjs";');
67+
expect(runtimeSource).toContain("export { isRealBunBinary };");
68+
expect(validatorSource).toContain("export const REAL_BUN_MIN_BYTES = 1_000_000;");
69+
expect(validatorSource).toMatch(/export function isRealBunBinary\(path\) \{[\s\S]*?try \{[\s\S]*?statSync\(path\)[\s\S]*?catch \{[\s\S]*?return false;/);
6370
});
6471
});

0 commit comments

Comments
 (0)