mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Fix linker cache invalidation and warnings
This commit is contained in:
@@ -8,6 +8,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs";
|
||||
import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
||||
|
||||
// Node 24 can persist V8's compiled module bytecode. scriptc's CLI imports
|
||||
@@ -109,7 +110,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
let nativeEnvironment: string | null;
|
||||
try {
|
||||
nativeEnvironment = helperObjectRoute
|
||||
? startup.executableLinkerEnvironmentFingerprint()
|
||||
? await startup.executableLinkerEnvironmentFingerprint()
|
||||
: await startup.executableNativeEnvironmentFingerprint();
|
||||
} catch {
|
||||
nativeEnvironment = null;
|
||||
@@ -134,6 +135,14 @@ async function tryFastPath(): Promise<number | null> {
|
||||
nodeVersion: process.version,
|
||||
});
|
||||
if (hit === null) return null;
|
||||
if (shouldWarnLegacyCExecutable({
|
||||
executable: true,
|
||||
fromC: false,
|
||||
backend,
|
||||
sanitize: values.sanitize,
|
||||
})) {
|
||||
process.stderr.write(LEGACY_C_EXECUTABLE_WARNING);
|
||||
}
|
||||
if (hit.native.llvmRefusal !== undefined) {
|
||||
process.stderr.write(`scriptc: backend c (llvm refused: ${hit.native.llvmRefusal})\n`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
|
||||
const executable = {
|
||||
executable: true,
|
||||
fromC: false,
|
||||
backend: undefined,
|
||||
sanitize: false,
|
||||
};
|
||||
|
||||
test("warns only for generated legacy C executables on runtime-pack hosts", () => {
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang" }, true)).toBe(true);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, fromC: true }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, backend: "c" }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, sanitize: true }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable({ ...executable, executable: false }, { SCRIPTC_CC: "clang" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang", SCRIPTC_TARGET: "wasm32-wasi" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "" }, true)).toBe(false);
|
||||
expect(shouldWarnLegacyCExecutable(executable, { SCRIPTC_CC: "clang" }, false)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs";
|
||||
|
||||
export const LEGACY_C_EXECUTABLE_WARNING =
|
||||
"scriptc: warning: SCRIPTC_CC selects the deprecated legacy C executable path on macOS arm64; " +
|
||||
"unset it to use the bundled LLVM helper/runtime pack, or use SCRIPTC_LINKER to select the platform linker driver\n";
|
||||
|
||||
export function shouldWarnLegacyCExecutable(options: {
|
||||
executable: boolean;
|
||||
fromC: boolean;
|
||||
backend: "c" | "llvm" | undefined;
|
||||
sanitize: boolean;
|
||||
}, env: NodeJS.ProcessEnv = process.env, runtimePackHost = hostSupportsRuntimePack()): boolean {
|
||||
return options.executable && !options.fromC &&
|
||||
options.backend !== "c" && !options.sanitize &&
|
||||
(env["SCRIPTC_TARGET"] ?? "") === "" &&
|
||||
runtimePackHost &&
|
||||
env["SCRIPTC_CC"] !== undefined && env["SCRIPTC_CC"] !== "";
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { analyze, buildTargetPlatform, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler";
|
||||
import { hostSupportsRuntimePack } from "../scripts/runtime-pack-host.mjs";
|
||||
import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
|
||||
import { resolveOutputOptions } from "./output-options.js";
|
||||
import { selectOutputPaths } from "./paths.js";
|
||||
import { CLI_OPTIONS, USAGE } from "./usage.js";
|
||||
@@ -263,15 +263,13 @@ async function main(): Promise<number> {
|
||||
// SCRIPTC_CC remains a migration escape hatch for explicit C, sanitizer,
|
||||
// and comparison builds. The normal LLVM executable route is controlled by
|
||||
// SCRIPTC_LINKER, which receives objects and archives only.
|
||||
if (
|
||||
output.outputKind === "exe" && values.backend !== "c" && !values.sanitize &&
|
||||
(process.env["SCRIPTC_TARGET"] ?? "") === "" && hostSupportsRuntimePack() &&
|
||||
process.env["SCRIPTC_CC"] !== undefined && process.env["SCRIPTC_CC"] !== ""
|
||||
) {
|
||||
process.stderr.write(
|
||||
"scriptc: warning: SCRIPTC_CC selects the deprecated legacy C executable path on macOS arm64; " +
|
||||
"unset it to use the bundled LLVM helper/runtime pack, or use SCRIPTC_LINKER to select the platform linker driver\n",
|
||||
);
|
||||
if (shouldWarnLegacyCExecutable({
|
||||
executable: output.outputKind === "exe",
|
||||
fromC: values["from-c"],
|
||||
backend: values.backend,
|
||||
sanitize: values.sanitize,
|
||||
})) {
|
||||
process.stderr.write(LEGACY_C_EXECUTABLE_WARNING);
|
||||
}
|
||||
|
||||
let nativeLinkInfo: object | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { release as osRelease, tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
@@ -8,6 +8,8 @@ import { expect, test } from "vitest";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const bootstrap = join(repoRoot, "packages/cli/dist/bootstrap.js");
|
||||
const runtimePackHost = process.platform === "darwin" && process.arch === "arm64" &&
|
||||
Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) >= 24;
|
||||
|
||||
test("bootstrap serves version and help without loading the compiler graph", async () => {
|
||||
const preloadDir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-preload-"));
|
||||
@@ -92,3 +94,38 @@ test("bootstrap exact builds use the routed cache and source edits fall through"
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
test.skipIf(!runtimePackHost)(
|
||||
"bootstrap cache hits retain the legacy C executable warning",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-legacy-warning-"));
|
||||
const cacheRoot = join(dir, "cache");
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "program");
|
||||
const env = {
|
||||
...process.env,
|
||||
SCRIPTC_CACHE_DIR: cacheRoot,
|
||||
SCRIPTC_CC: "clang",
|
||||
SCRIPTC_TIMING: "1",
|
||||
};
|
||||
delete env.SCRIPTC_NO_CACHE;
|
||||
const build = () => execFileAsync(
|
||||
process.execPath,
|
||||
[bootstrap, "build", entry, "-o", outPath],
|
||||
{ env, maxBuffer: 4 * 1024 * 1024 },
|
||||
);
|
||||
try {
|
||||
await writeFile(entry, 'console.log("legacy warning");\n');
|
||||
const first = await build();
|
||||
expect(first.stderr).toContain("deprecated legacy C executable path");
|
||||
expect(first.stderr).toContain("scriptc lowering");
|
||||
|
||||
const cached = await build();
|
||||
expect(cached.stderr).toContain("deprecated legacy C executable path");
|
||||
expect(cached.stderr).not.toContain("scriptc lowering");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
@@ -102,7 +102,7 @@ test("--from-c forwards the dev optimization posture", async () => {
|
||||
"}",
|
||||
"",
|
||||
].join("\n"));
|
||||
await execFileAsync(
|
||||
const result = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
@@ -116,8 +116,12 @@ test("--from-c forwards the dev optimization posture", async () => {
|
||||
"-o",
|
||||
outPath,
|
||||
],
|
||||
{ maxBuffer: 4 * 1024 * 1024 },
|
||||
{
|
||||
env: { ...process.env, SCRIPTC_CC: "clang" },
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
expect(result.stderr).not.toContain("deprecated legacy C executable path");
|
||||
await expect(execFileAsync(outPath)).resolves.toMatchObject({ stdout: "" });
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* runtime C as part of linking.
|
||||
*/
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, realpathSync, statSync } from "node:fs";
|
||||
import { mkdtemp, rename, rm, stat } from "node:fs/promises";
|
||||
import { delimiter } from "node:path";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
nativeArtifactDependenciesStillMatch,
|
||||
parseLinkTraceFiles,
|
||||
subprocessFailureDetail,
|
||||
toolchainEnvironmentCachePolicy,
|
||||
toolchainEnvironmentFingerprint,
|
||||
type NativeArtifactDependency,
|
||||
} from "./native-toolchain.js";
|
||||
@@ -35,8 +36,10 @@ export function resolvePlatformLinker(env: NodeJS.ProcessEnv = process.env): str
|
||||
* must still invalidate an early executable when a new `clang` appears ahead
|
||||
* of an unchanged PATH entry; relying on the string "clang" would restore a
|
||||
* binary past a wrapper that injects link inputs. */
|
||||
function platformLinkerIdentity(env: NodeJS.ProcessEnv): string {
|
||||
const linker = resolvePlatformLinker(env);
|
||||
function platformLinkerIdentity(
|
||||
env: NodeJS.ProcessEnv,
|
||||
linker: string = resolvePlatformLinker(env),
|
||||
): string {
|
||||
const hasSeparator = linker.includes("/") || linker.includes("\\");
|
||||
const pathEntries = hasSeparator
|
||||
? [""]
|
||||
@@ -69,13 +72,34 @@ function platformLinkerIdentity(env: NodeJS.ProcessEnv): string {
|
||||
* linker's resolved SDK/CRT inputs before it restores an executable. This
|
||||
* deliberately performs no synthetic C compilation: the linker receives
|
||||
* only the helper-produced program object and runtime-pack artifacts. */
|
||||
export function executableLinkerEnvironmentFingerprint(
|
||||
export async function executableLinkerEnvironmentFingerprint(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const linker = resolvePlatformLinker(env);
|
||||
const linkerIdentity = platformLinkerIdentity(env);
|
||||
let effectiveDriverIdentity: string;
|
||||
try {
|
||||
const selected = await execFileAsync(linker, ["-print-prog-name=clang"], {
|
||||
env,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
const selectedDriver = selected.stdout.trim();
|
||||
if (selectedDriver === "") throw new Error("linker driver reported no effective clang");
|
||||
effectiveDriverIdentity = platformLinkerIdentity(env, selectedDriver);
|
||||
} catch {
|
||||
// A driver that cannot expose its effective clang may still link correctly,
|
||||
// but the trusted default cannot safely address an existing early
|
||||
// executable entry. Explicit drivers never publish complete executables,
|
||||
// so their own file identity remains a sufficient stable partial-cache key.
|
||||
effectiveDriverIdentity = env["SCRIPTC_LINKER"] === undefined
|
||||
? `<unavailable:${linker}:${randomUUID()}>`
|
||||
: `<unavailable:${linker}>`;
|
||||
}
|
||||
const hash = createHash("sha256")
|
||||
.update("executable-linker-environment-v1\0")
|
||||
.update("executable-linker-environment-v2\0")
|
||||
.update(toolchainEnvironmentFingerprint(env)).update("\0")
|
||||
.update(platformLinkerIdentity(env)).update("\0");
|
||||
.update(linkerIdentity).update("\0")
|
||||
.update(effectiveDriverIdentity).update("\0");
|
||||
for (const name of ["PATH", "SCRIPTC_FETCH_CURL", "SCRIPTC_TEST_RUNTIME_SRC_DIR"] as const) {
|
||||
const value = env[name];
|
||||
hash.update(name).update(value === undefined ? "\0unset\0" : "\0set\0").update(value ?? "").update("\0");
|
||||
@@ -92,7 +116,9 @@ export function platformLinkerSupportsPersistentCache(
|
||||
// The Apple system shim is a stable front door to the active SDK/linker;
|
||||
// linkNativeExecutable snapshots the selected transitive inputs before it
|
||||
// publishes the final cache entry. A PATH wrapper is intentionally opaque.
|
||||
return env["SCRIPTC_LINKER"] === undefined && platformLinkerIdentity(env).startsWith("/usr/bin/clang\0");
|
||||
return env["SCRIPTC_LINKER"] === undefined &&
|
||||
toolchainEnvironmentCachePolicy(env).completeArtifacts &&
|
||||
platformLinkerIdentity(env).startsWith("/usr/bin/clang\0");
|
||||
}
|
||||
|
||||
async function snapshotDependencies(paths: readonly string[]): Promise<NativeArtifactDependency[]> {
|
||||
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
type RuntimePackManifest,
|
||||
} from "./runtime-pack.js";
|
||||
import { createNativeLinkPlan } from "./link-plan.js";
|
||||
import { linkNativeExecutable, platformLinkerSupportsPersistentCache, resolvePlatformLinker } from "./linker.js";
|
||||
import {
|
||||
executableLinkerEnvironmentFingerprint,
|
||||
linkNativeExecutable,
|
||||
platformLinkerSupportsPersistentCache,
|
||||
resolvePlatformLinker,
|
||||
} from "./linker.js";
|
||||
import { MACOS_ARM64_TARGET } from "./targets.js";
|
||||
|
||||
const VERSION = compilerReleaseVersion();
|
||||
@@ -127,6 +132,36 @@ describe("runtime pack manifests", () => {
|
||||
expect(resolvePlatformLinker({})).toBe("clang");
|
||||
expect(resolvePlatformLinker({ SCRIPTC_LINKER: "ld-driver" })).toBe("ld-driver");
|
||||
expect(platformLinkerSupportsPersistentCache({ SCRIPTC_LINKER: "wrapper" })).toBe(false);
|
||||
expect(platformLinkerSupportsPersistentCache({ LIBRARY_PATH: "/mutable" })).toBe(false);
|
||||
expect(platformLinkerSupportsPersistentCache({ SDKROOT: "/mutable" })).toBe(false);
|
||||
});
|
||||
|
||||
test("the linker environment follows the effective clang behind a stable driver", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scriptc-linker-environment-"));
|
||||
const linker = join(root, "linker.mjs");
|
||||
const selectedOne = join(root, "selected-one");
|
||||
const selectedTwo = join(root, "selected-two");
|
||||
await Promise.all([
|
||||
writeFile(selectedOne, "one"),
|
||||
writeFile(selectedTwo, "two"),
|
||||
writeFile(linker, [
|
||||
"#!/bin/sh",
|
||||
'test "$1" = "-print-prog-name=clang" || exit 2',
|
||||
'printf "%s\\n" "$SCRIPTC_TEST_SELECTED_CLANG"',
|
||||
"",
|
||||
].join("\n")),
|
||||
]);
|
||||
await chmod(linker, 0o755);
|
||||
|
||||
const first = await executableLinkerEnvironmentFingerprint({
|
||||
SCRIPTC_LINKER: linker,
|
||||
SCRIPTC_TEST_SELECTED_CLANG: selectedOne,
|
||||
});
|
||||
const second = await executableLinkerEnvironmentFingerprint({
|
||||
SCRIPTC_LINKER: linker,
|
||||
SCRIPTC_TEST_SELECTED_CLANG: selectedTwo,
|
||||
});
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
test("feature implications and predicates are deterministic", () => {
|
||||
|
||||
@@ -1359,7 +1359,7 @@ async function compileTracked(
|
||||
: (process.env["SCRIPTC_CC"] ?? "clang"),
|
||||
],
|
||||
nativeEnvironment: helperObjectRoute
|
||||
? executableLinkerEnvironmentFingerprint()
|
||||
? await executableLinkerEnvironmentFingerprint()
|
||||
: await executableNativeEnvironmentFingerprint(),
|
||||
nodeVersion: process.version,
|
||||
implementation: implementation.digest,
|
||||
|
||||
Reference in New Issue
Block a user