From 214bb31e9af57646adfc5fef631dbdbabbdc99c7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 28 Aug 2026 07:22:09 -0500 Subject: [PATCH] fix(runtime): preserve executable link identity --- packages/cli/test/runtime-pack.test.ts | 22 ++++++++--- .../compiler/src/backend/runtime-pack.test.ts | 37 ++++++++++++++++++- packages/compiler/src/backend/runtime-pack.ts | 11 ++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/packages/cli/test/runtime-pack.test.ts b/packages/cli/test/runtime-pack.test.ts index 1db698c85..8bd62ef2c 100644 --- a/packages/cli/test/runtime-pack.test.ts +++ b/packages/cli/test/runtime-pack.test.ts @@ -2,7 +2,7 @@ import { execFile } from "node:child_process"; import { createRequire } from "node:module"; import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { release as osRelease, tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { promisify } from "node:util"; import { afterEach, describe, expect, test } from "vitest"; @@ -35,13 +35,17 @@ describe.runIf(supported)("precompiled runtime executable builds", () => { "", ].join("\n")); await chmod(wrapper, 0o755); - await execFileAsync(process.execPath, [ + const cliArgs = [ "--import", tsxLoader, cliEntry, "build", entry, "-o", output, - ], { + ]; + const env = { + ...process.env, + SCRIPTC_NO_CACHE: "1", + SCRIPTC_LINKER: wrapper, + }; + await execFileAsync(process.execPath, cliArgs, { env: { - ...process.env, - SCRIPTC_NO_CACHE: "1", - SCRIPTC_LINKER: wrapper, + ...env, }, }); const args = JSON.parse(await readFile(log, "utf8")) as string[]; @@ -53,5 +57,11 @@ describe.runIf(supported)("precompiled runtime executable builds", () => { expect(args.some((arg) => arg.includes("runtime-darwin-arm64/artifacts"))).toBe(false); await expect(execFileAsync(output, [], { encoding: "utf8" })) .resolves.toMatchObject({ stdout: "precompiled runtime\n" }); + const firstExecutable = await readFile(output); + const signature = await execFileAsync("codesign", ["-dvvv", output], { encoding: "utf8" }); + expect(signature.stderr).toContain(`Identifier=${basename(output)}`); + + await execFileAsync(process.execPath, cliArgs, { env }); + expect(await readFile(output)).toEqual(firstExecutable); }); }); diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 989e60f1d..245bc39cc 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { chmod, mkdtemp, mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { describe, expect, test } from "vitest"; import { compilerReleaseVersion } from "../library/sidecar.js"; import { snapshotNativeArtifactDependencies } from "./native-toolchain.js"; @@ -242,6 +242,41 @@ describe("runtime pack manifests", () => { expect(await readFile(runtimeObject, "utf8")).toBe("tampered"); }); + test("preserves the requested basename in the private linker output path", async () => { + const { root, packagePath } = await fixture(); + const programObject = join(root, "program.o"); + const output = join(root, "requested-name"); + const linker = join(root, "linker.mjs"); + await Promise.all([ + writeFile(programObject, "program object"), + writeFile(linker, [ + "#!/usr/bin/env node", + 'import { writeFileSync } from "node:fs";', + 'const outputIndex = process.argv.indexOf("-o");', + 'const output = process.argv[outputIndex + 1];', + 'writeFileSync(output, JSON.stringify(output));', + "", + ].join("\n")), + ]); + await chmod(linker, 0o755); + const plan = await createRuntimeLinkPlan({ + target: MACOS_ARM64_TARGET, + programObject, + outPath: output, + features: BASE, + ffi: null, + optimization: "release", + resolver: () => packagePath, + }); + + await linkRuntimePackExecutable(plan, { linker }); + + const privateOutput = JSON.parse(await readFile(output, "utf8")) as string; + expect(privateOutput).not.toBe(output); + expect(basename(privateOutput)).toBe(basename(output)); + expect(dirname(dirname(privateOutput))).toBe(dirname(output)); + }); + test("does not publish a cache proof from a stale program-object dependency snapshot", async () => { const { root, packagePath } = await fixture(); const programObject = join(root, "program.o"); diff --git a/packages/compiler/src/backend/runtime-pack.ts b/packages/compiler/src/backend/runtime-pack.ts index a6645a3fa..45802c14b 100644 --- a/packages/compiler/src/backend/runtime-pack.ts +++ b/packages/compiler/src/backend/runtime-pack.ts @@ -7,7 +7,6 @@ import { basename, dirname, join, resolve } from "node:path"; import { promisify } from "node:util"; import type { FfiProfile } from "../ffi/ffi-manifest.js"; import { compilerReleaseVersion } from "../library/sidecar.js"; -import { privateSiblingPath } from "./build-cache.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; import { CcCompileError, @@ -521,7 +520,13 @@ export async function linkRuntimePackExecutable( ): Promise { const linker = options.linker ?? process.env["SCRIPTC_LINKER"] ?? "clang"; const linkerPath = await resolveExecutable(linker); - const privateOut = privateSiblingPath(plan.outputPath, "runtime-pack-link"); + // ld64 derives an ad-hoc signature identifier from the output basename. + // Keep that basename caller-visible while a private sibling directory gives + // the link its own inode and preserves an atomic same-filesystem install. + const privateOutRoot = await mkdtemp( + join(dirname(plan.outputPath), ".scriptc-runtime-pack-link-"), + ); + const privateOut = join(privateOutRoot, basename(plan.outputPath)); let stagedRoot: string | null = null; try { const staged = await stageRuntimePackArtifacts(plan.runtimePack); @@ -583,7 +588,7 @@ export async function linkRuntimePackExecutable( ); } finally { await Promise.all([ - rm(privateOut, { force: true }).catch(() => undefined), + rm(privateOutRoot, { recursive: true, force: true }).catch(() => undefined), stagedRoot === null ? Promise.resolve() : rm(stagedRoot, { recursive: true, force: true }).catch(() => undefined),