mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Fix persistent cache invalidation
This commit is contained in:
@@ -2420,6 +2420,54 @@ test("frontend-generated same-output builds no-op only while output and dependen
|
||||
}
|
||||
});
|
||||
|
||||
test("fresh processes preserve output-local dependency coverage after source edits", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-local-artifact-process-"));
|
||||
scratch.push(dir);
|
||||
const cacheRoot = join(dir, "cache");
|
||||
const cPath = join(dir, "program.c");
|
||||
const outPath = join(dir, "program");
|
||||
const helperPath = join(dir, "compile.mjs");
|
||||
const childEnv: NodeJS.ProcessEnv = { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot };
|
||||
delete childEnv["SCRIPTC_NO_CACHE"];
|
||||
delete childEnv["SCRIPTC_TEST_STABLE_TOOLCHAIN"];
|
||||
delete childEnv["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"];
|
||||
|
||||
await writeFile(
|
||||
helperPath,
|
||||
`import { compileC } from ${JSON.stringify(new URL("./cc.ts", import.meta.url).href)};\n` +
|
||||
`await compileC({ cPath: process.argv[2], outPath: process.argv[3], cacheIdentity: "scriptc-generated-v1" });\n`,
|
||||
);
|
||||
const compileInFreshProcess = (): void => {
|
||||
execFileSync(process.execPath, ["--import", "tsx", helperPath, cPath, outPath], {
|
||||
env: childEnv,
|
||||
stdio: "pipe",
|
||||
});
|
||||
};
|
||||
const stampPath = join(
|
||||
cacheRoot,
|
||||
"local",
|
||||
createHash("sha256").update(outPath).digest("hex"),
|
||||
);
|
||||
const dependencyPaths = async (): Promise<string[]> => {
|
||||
const stamp = JSON.parse(await readFile(stampPath, "utf8")) as {
|
||||
dependencies: { path: string }[];
|
||||
};
|
||||
return stamp.dependencies.map((dependency) => dependency.path);
|
||||
};
|
||||
|
||||
await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("one"); return 0; }\n');
|
||||
compileInFreshProcess();
|
||||
const firstPaths = await dependencyPaths();
|
||||
expect(firstPaths.length).toBeGreaterThan(2);
|
||||
|
||||
// The metadata fingerprints are restored from cache files in this second
|
||||
// Node process; their in-memory fingerprint-to-path map starts empty.
|
||||
await writeFile(cPath, '#include <stdio.h>\nint main(void) { puts("two"); return 0; }\n');
|
||||
compileInFreshProcess();
|
||||
expect(execFileSync(outPath, { encoding: "utf8" }).trim()).toBe("two");
|
||||
expect(await dependencyPaths()).toEqual(firstPaths);
|
||||
});
|
||||
|
||||
test("native metadata snapshots survive source edits and repair after tampering", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-native-metadata-"));
|
||||
scratch.push(dir);
|
||||
|
||||
@@ -4469,6 +4469,7 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
: effectiveLinkInvocationArgs.filter((arg) => arg !== `-L${curlStubDir}`);
|
||||
let implicitLinker: string | null = null;
|
||||
let preBuildDependencies: LocalArtifactDependency[] | null = null;
|
||||
let localArtifactDependencyPaths: string[] | null = null;
|
||||
let linkMetadataStamp: NativeMetadataStamp | null = null;
|
||||
const linkMetadataKey =
|
||||
root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
|
||||
@@ -4531,6 +4532,18 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
fingerprintDependencyPaths.get(implicitLinker!) ?? []),
|
||||
...(fingerprintDependencyPaths.get(programDependencies) ?? []),
|
||||
]);
|
||||
if (localArtifact !== null) {
|
||||
// Native metadata stamps persist their dependency paths across CLI
|
||||
// processes; fingerprintDependencyPaths deliberately does not. Build
|
||||
// the output-local stamp from this complete validated snapshot so a
|
||||
// source edit in a fresh process cannot replace it with only the two
|
||||
// process-local fallback paths.
|
||||
localArtifactDependencyPaths = [
|
||||
localArtifact.compilerPath,
|
||||
dirname(resolve(opts.cPath)),
|
||||
...preBuildDependencies.map((dependency) => dependency.path),
|
||||
];
|
||||
}
|
||||
} catch {
|
||||
// Program-header discovery and linker tracing are both required for a
|
||||
// complete hit. Runtime objects remain safely cacheable if either probe
|
||||
@@ -4590,17 +4603,12 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
// cache entry populated by a less restrictive shell must not widen access.
|
||||
await chmod(tmpOut, 0o777 & ~process.umask());
|
||||
await rename(tmpOut, opts.outPath);
|
||||
if (localArtifact !== null) {
|
||||
if (localArtifact !== null && localArtifactDependencyPaths !== null) {
|
||||
await publishLocalArtifactStamp(
|
||||
localArtifact.stampPath,
|
||||
opts.outPath,
|
||||
localArtifact.key,
|
||||
[
|
||||
localArtifact.compilerPath,
|
||||
dirname(resolve(opts.cPath)),
|
||||
...(fingerprintDependencyPaths.get(implicitToolchain!) ?? []),
|
||||
...(fingerprintDependencyPaths.get(implicitLinker!) ?? []),
|
||||
],
|
||||
localArtifactDependencyPaths,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
return; // hit: the program/runtime payload compile and link were skipped
|
||||
@@ -4706,17 +4714,17 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
/* publishing is best-effort */
|
||||
}
|
||||
}
|
||||
if (localArtifact !== null && cacheCompleteArtifact && cacheInputsStable) {
|
||||
if (
|
||||
localArtifact !== null &&
|
||||
localArtifactDependencyPaths !== null &&
|
||||
cacheCompleteArtifact &&
|
||||
cacheInputsStable
|
||||
) {
|
||||
await publishLocalArtifactStamp(
|
||||
localArtifact.stampPath,
|
||||
opts.outPath,
|
||||
localArtifact.key,
|
||||
[
|
||||
localArtifact.compilerPath,
|
||||
dirname(resolve(opts.cPath)),
|
||||
...(fingerprintDependencyPaths.get(implicitToolchain!) ?? []),
|
||||
...(fingerprintDependencyPaths.get(implicitLinker!) ?? []),
|
||||
],
|
||||
localArtifactDependencyPaths,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -686,9 +686,11 @@ function genChannels(
|
||||
* fallback for it. */
|
||||
const MAP_TYPE_MAX_DEPTH = 64;
|
||||
let mapTypeDepth = 0;
|
||||
/** Bumped whenever mapType resolves a type through CONTEXT-SENSITIVE hooks
|
||||
* (a generic body's type parameter, a mixin instantiation) — mappings that
|
||||
* make the same ts.Type answer differently across instantiation contexts.
|
||||
/** Bumped whenever mapType resolves a type through CONTEXT-SENSITIVE or
|
||||
* collection-sensitive hooks (a generic body's type parameter, a mixin
|
||||
* instantiation, a generic class instance) — mappings that can make the same
|
||||
* ts.Type answer differently across instantiation contexts or collection
|
||||
* phases.
|
||||
* The recursive-shape machinery keys shape identity by checker type, which
|
||||
* is sound only for context-FREE mappings: a recursive frame that observes
|
||||
* a bump between entry and exit stays fenced (recursive generic-open types
|
||||
@@ -1190,7 +1192,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer
|
||||
// hook owns the instance table (monomorphization by flow).
|
||||
if (classDecl.typeParameters) {
|
||||
return ctx.genericClassInstance ? ctx.genericClassInstance(classDecl, widened) : null;
|
||||
const instance = ctx.genericClassInstance
|
||||
? ctx.genericClassInstance(classDecl, widened)
|
||||
: null;
|
||||
// Before the generic declaration's collection turn, the hook can only
|
||||
// return its family shell; after collection the same checker type names
|
||||
// a concrete registered instance. Never memoize either phase's answer.
|
||||
if (instance !== null) contextResolutions++;
|
||||
return instance;
|
||||
}
|
||||
return { kind: "object", className: classNamer(classDecl) };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// Generic classes, monomorphized per instantiation: fields, methods,
|
||||
// accessors, inference and explicit type arguments, instanceof against the
|
||||
// generic name (one runtime class in Node — the family interval here).
|
||||
// The early reference pins collection-order independence: its checker type is
|
||||
// first mapped before Box's family has collected, then must remap to Box%N.
|
||||
class EarlyHolder {
|
||||
readonly box: Box<number>;
|
||||
constructor(box: Box<number>) {
|
||||
this.box = box;
|
||||
}
|
||||
read(): number {
|
||||
return this.box.get();
|
||||
}
|
||||
}
|
||||
|
||||
class Box<T> {
|
||||
v: T;
|
||||
constructor(v: T) {
|
||||
@@ -20,6 +32,7 @@ class Box<T> {
|
||||
const a = new Box(41); // inferred Box<number>
|
||||
a.set(a.get() + 1);
|
||||
console.log(a.get(), a.describe());
|
||||
console.log(new EarlyHolder(a).read());
|
||||
|
||||
const b = new Box<string>("hi"); // explicit
|
||||
console.log(b.get().toUpperCase(), b.describe());
|
||||
|
||||
Reference in New Issue
Block a user