diff --git a/packages/codev-sandbox/script/build-vzhelper.ts b/packages/codev-sandbox/script/build-vzhelper.ts index 95a1ace699..273f9cedd8 100644 --- a/packages/codev-sandbox/script/build-vzhelper.ts +++ b/packages/codev-sandbox/script/build-vzhelper.ts @@ -14,12 +14,12 @@ const universal = process.argv.includes("--universal") const slices = await (async () => { if (!universal) { - await $`swift build -c release --package-path ${root}` + await swiftBuild([]) return [`${root}.build/release/codev-vmm`] } const triples = ["arm64-apple-macosx", "x86_64-apple-macosx"] for (const triple of triples) { - await $`swift build -c release --package-path ${root} --triple ${triple}` + await swiftBuild(["--triple", triple]) } return triples.map((triple) => `${root}.build/${triple}/release/codev-vmm`) })() @@ -31,3 +31,17 @@ else await $`lipo -create ${slices} -output ${root}dist/codev-vmm` // fat Mach-O, and lipo would strip a per-slice signature anyway. await $`codesign --force --sign - --entitlements ${root}vzhelper.entitlements ${root}dist/codev-vmm` console.log(`built ${root}dist/codev-vmm${universal ? " (universal)" : ""}`) + +// Swift 6.4 made the swiftbuild backend the default, and on Command Line +// Tools setups it fails before compiling anything ("Could not initialize +// build system ... Unknown error parsing property list"). The native backend +// still builds this package, so a failed build retries with it: toolchains +// whose default works never see the retry, and the flag stays out of the +// first attempt in case a later Swift drops it. +async function swiftBuild(extra: string[]) { + const args = ["build", "-c", "release", "--package-path", root, ...extra] + const first = await $`swift ${args}`.nothrow() + if (first.exitCode === 0) return + console.warn("swift build failed with the default build system; retrying with --build-system native") + await $`swift ${args} --build-system native` +} diff --git a/packages/codev-sandbox/src/vz/provider.test.ts b/packages/codev-sandbox/src/vz/provider.test.ts index 3b7b686743..503e8a1ec5 100644 --- a/packages/codev-sandbox/src/vz/provider.test.ts +++ b/packages/codev-sandbox/src/vz/provider.test.ts @@ -211,6 +211,18 @@ test("surfaces helper error events with the stderr tail", async () => { await rm(dir, { recursive: true, force: true }) }) +test("fails the start, instead of throwing, when the helper binary is missing", async () => { + const dir = await mkdtemp(join(tmpdir(), "codev-vz-")) + const helperPath = join(dir, "codev-vmm") + // The real spawn: a missing binary emits "error" and never "exit". + const { options, projectDir } = await preparedOptions(dir, FAKE_HELPER, { helperPath, spawnImpl: spawn }) + const provider = createVzProvider(options) + await expect(provider.start({ ...config, projectDir })).rejects.toThrow( + `sandbox VM helper is not installed (expected at ${helperPath})`, + ) + await rm(dir, { recursive: true, force: true }) +}) + test("times out when the helper never reaches running", async () => { const dir = await mkdtemp(join(tmpdir(), "codev-vz-")) const prepared = await preparedOptions(dir, SILENT_HELPER, { startTimeoutMs: 300 }) diff --git a/packages/codev-sandbox/src/vz/provider.ts b/packages/codev-sandbox/src/vz/provider.ts index e3cd349819..27b355c601 100644 --- a/packages/codev-sandbox/src/vz/provider.ts +++ b/packages/codev-sandbox/src/vz/provider.ts @@ -213,6 +213,17 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider { const running = new Promise<"running">((resolve) => { handlers.running = () => resolve("running") }) + // A helper that cannot be spawned (not built, deleted, not executable) + // emits "error" and never "exit". Unhandled, that event is an uncaught + // exception — in the desktop's main process, a crash dialog at launch + // when a sandbox autostarts — so it fails this start instead. + child.once("error", (err: NodeJS.ErrnoException) => { + handlers.error?.( + err.code === "ENOENT" + ? `sandbox VM helper is not installed (expected at ${options.helperPath})` + : `could not run the sandbox VM helper: ${err.message}`, + ) + }) child.stdout?.on( "data", createVzHelperEventReader((event: VzHelperEvent) => {