Instrument and improve incremental workspace checks
Profile source capture, Nix evaluation/realization, resource/query checking, and implementation builds without putting timings into immutable outputs. Keep modular repositories; bound independent capture work, snapshot jj once per observation, and retain final concurrent-edit and Git completeness checks. Compose exact resource/closure/frontend/toolchain checks as memoized Nix derivations. Preserve whole-workspace validation and the full compiler path when no checked dependency is supplied. Share canonical Nixpkgs by content, index workspace build contexts once, and narrow lock/driver/compiler inputs with conservative complete-source fallback. Batch React witness checks only when distinct source closures cannot share ambient declarations; otherwise run the original independent TypeScript checks. Seed local authoring toolchains with exact canonical fetch identities when they match the selected inputs; retain store-path inputs for overrides. Add generic correctness checks rather than permanent Reader timing/content fixtures. Verify actual Reader edits and rejection/recovery in a private workbench, preserving the original user's workbench and runtime. Query checking currently caches at resource/closure boundaries, not individual query semantic dependency boundaries. Template helper pins and reproducible fixture snapshots follow in a separate publication change.
This commit is contained in:
@@ -13,13 +13,14 @@ export const checkerIdentity = () =>
|
||||
export async function snapshotCommit(root: string): Promise<string> {
|
||||
const run = async (...args: string[]) =>
|
||||
(await execFile("jj", args, { cwd: root, env: environment() })).stdout.trim();
|
||||
await run("status");
|
||||
// jj resolve --list exits 1 on a clean revision. Query structured revision
|
||||
// metadata instead of depending on diagnostic wording or swallowing errors.
|
||||
if ((await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "conflict")) !== "false")
|
||||
throw new Error("Resolve source conflicts before verification");
|
||||
const commit = await run("--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id");
|
||||
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit)) throw new Error("Verification requires an exact jj commit");
|
||||
// A single normal log snapshots the working copy and observes both values
|
||||
// from that exact revision. Separate status/log processes add no evidence.
|
||||
const [conflict, commit] = (await run("log", "--no-graph", "-r", "@", "-T", 'conflict ++ "\\n" ++ commit_id')).split(
|
||||
"\n",
|
||||
);
|
||||
if (conflict !== "false") throw new Error("Resolve source conflicts before verification");
|
||||
if (!commit || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))
|
||||
throw new Error("Verification requires an exact jj commit");
|
||||
await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], {
|
||||
cwd: root,
|
||||
env: environment(),
|
||||
@@ -110,6 +111,7 @@ export async function buildImmutableCandidate(
|
||||
kind: "workspace" | "interface" | "package" | "build",
|
||||
logFile: string,
|
||||
contractOnly = false,
|
||||
observe?: (phase: "nixEvaluationMs" | "nixBuildMs", ms: number) => void,
|
||||
): Promise<string> {
|
||||
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(source.commit))
|
||||
throw new Error("An immutable candidate requires an exact commit");
|
||||
@@ -126,16 +128,52 @@ export async function buildImmutableCandidate(
|
||||
log.once("open", () => resolve());
|
||||
log.once("error", reject);
|
||||
});
|
||||
return await new Promise((resolve, reject) => {
|
||||
let output = "",
|
||||
tail = "",
|
||||
failure: Error | undefined;
|
||||
const child = spawn(
|
||||
"nix",
|
||||
[
|
||||
"build",
|
||||
"--impure",
|
||||
"--file",
|
||||
const run = async (executable: string, args: string[]) =>
|
||||
await new Promise<string>((resolve, reject) => {
|
||||
let output = "",
|
||||
tail = "",
|
||||
failure: Error | undefined;
|
||||
const child = spawn(executable, args, { env: environment(), stdio: ["ignore", "pipe", "pipe"] });
|
||||
const failedLog = (error: Error) => {
|
||||
failure = error;
|
||||
child.kill();
|
||||
};
|
||||
log.on("error", failedLog);
|
||||
child.stdout.on("data", (chunk) => {
|
||||
output += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
log.write(chunk);
|
||||
tail = (tail + String(chunk)).slice(-6000);
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
failure = error;
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
log.off("error", failedLog);
|
||||
if (failure) reject(failure);
|
||||
else if (code !== 0) reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
|
||||
else resolve(output.trim());
|
||||
});
|
||||
});
|
||||
const timed = async (phase: "nixEvaluationMs" | "nixBuildMs", action: () => Promise<string>) => {
|
||||
const started = performance.now();
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
observe?.(phase, Math.round(performance.now() - started));
|
||||
}
|
||||
};
|
||||
let gcDirectory: string | undefined;
|
||||
try {
|
||||
// Evaluation includes import-from-derivation builds (locks and contracts).
|
||||
// Build the resulting derivation directly, without evaluating the DAG twice.
|
||||
// Keep the derivation alive across the two processes, including concurrent
|
||||
// GC. This temporary root is removed on success and on verification failure.
|
||||
gcDirectory = await fs.mkdtemp(path.join(path.dirname(logFile), ".gc-root-"));
|
||||
const gcRoot = path.join(gcDirectory, "candidate");
|
||||
const derivation = await timed("nixEvaluationMs", async () => {
|
||||
await run("nix-instantiate", [
|
||||
builder,
|
||||
"--argstr",
|
||||
"repository",
|
||||
@@ -152,33 +190,22 @@ export async function buildImmutableCandidate(
|
||||
"--arg",
|
||||
"contractOnly",
|
||||
contractOnly ? "true" : "false",
|
||||
"--no-link",
|
||||
"--print-out-paths",
|
||||
"-L",
|
||||
],
|
||||
{ env: environment(), stdio: ["ignore", "pipe", "pipe"] },
|
||||
"--add-root",
|
||||
gcRoot,
|
||||
"--indirect",
|
||||
]);
|
||||
return await fs.readlink(gcRoot);
|
||||
});
|
||||
if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+\.drv$/.test(derivation))
|
||||
throw new Error("Nix returned an invalid candidate derivation");
|
||||
const artifact = await timed("nixBuildMs", () =>
|
||||
run("nix", ["build", derivation + "^*", "--no-link", "--print-out-paths", "-L"]),
|
||||
);
|
||||
log.on("error", (error) => {
|
||||
failure = error;
|
||||
child.kill();
|
||||
});
|
||||
child.stdout.on("data", (chunk) => {
|
||||
output += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
log.write(chunk);
|
||||
tail = (tail + String(chunk)).slice(-6000);
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
failure = error;
|
||||
});
|
||||
child.on("close", (code) =>
|
||||
log.end(() => {
|
||||
if (failure) reject(failure);
|
||||
else if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(output.trim()))
|
||||
reject(new Error(`Candidate Nix check failed (${code}). Full log: ${logFile}\n${tail}`));
|
||||
else resolve(output.trim());
|
||||
}),
|
||||
);
|
||||
});
|
||||
if (!/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact))
|
||||
throw new Error("Nix returned an invalid candidate artifact");
|
||||
return artifact;
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => log.end(resolve));
|
||||
if (gcDirectory) await fs.rm(gcDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user