63 lines
3.8 KiB
TypeScript
63 lines
3.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|||
|
|
import path from "node:path";
|
||
|
|
import {execFile as callback, spawn} from "node:child_process";
|
||
|
|
import {promisify} from "node:util";
|
||
|
|
import {fileURLToPath} from "node:url";
|
||
|
|
const execFile = promisify(callback);
|
||
|
|
const environment = () => ({...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", GIT_TERMINAL_PROMPT: "0"});
|
||
|
|
|
||
|
|
/** Checking snapshots jj, but never publishes or activates the working copy. */
|
||
|
|
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");
|
||
|
|
await execFile("git", ["diff", "--exit-code", "--no-ext-diff", "--no-textconv", commit, "--"], {cwd: root, env: environment()});
|
||
|
|
const {stdout} = await execFile("git", ["ls-files", "--others", "--exclude-standard", "-z"], {cwd: root});
|
||
|
|
if (stdout) throw new Error("Source contains files not captured by jj; inspect jj tracking before verification");
|
||
|
|
return commit;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Use Git's actual committed tree, never dirty overlays labelled as old pins. */
|
||
|
|
export async function checkoutCommit(root: string, commit: string, destination: string) {
|
||
|
|
await fs.mkdir(destination, {recursive: true});
|
||
|
|
const archive = await execFile("git", ["archive", "--format=tar", commit], {cwd: root, encoding: "buffer", maxBuffer: 128 * 1024 * 1024});
|
||
|
|
await new Promise<void>((resolve, reject) => {
|
||
|
|
const child = spawn("tar", ["-xf", "-", "-C", destination], {stdio: ["pipe", "ignore", "pipe"]});
|
||
|
|
let error = "";
|
||
|
|
child.stderr.on("data", chunk => {error += chunk;});
|
||
|
|
child.on("error", reject);
|
||
|
|
child.on("close", code => code === 0 ? resolve() : reject(new Error(`Cannot extract committed source: ${error}`)));
|
||
|
|
child.stdin.on("error", reject);
|
||
|
|
child.stdin.end(archive.stdout);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Shared by provisional checks, template publication and host activation.
|
||
|
|
* The Nix derivation is the cached check; its metadata is internal provenance,
|
||
|
|
* not a certificate authored or approved by the workspace agent.
|
||
|
|
*/
|
||
|
|
export async function buildCheckedPackage(source: string, schema: string, packageRevisionId: string): Promise<string> {
|
||
|
|
const generator = process.env.QUIXOS_CHECK_GENERATOR ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||
|
|
if (!/^\/nix\/store\/[^/]+$/.test(generator)) throw new Error("Run verification with the installed Quixos tooling (its exact Nix checker is required)");
|
||
|
|
const builder = path.join(generator, "share/checked-package.nix");
|
||
|
|
await fs.access(builder);
|
||
|
|
return await new Promise((resolve, reject) => {
|
||
|
|
const child = spawn("nix", ["build", "--impure", "--file", builder,
|
||
|
|
"--argstr", "source", source, "--argstr", "schema", schema,
|
||
|
|
"--argstr", "generator", generator, "--argstr", "packageRevisionId", packageRevisionId,
|
||
|
|
"--no-link", "--print-out-paths", "-L"], {env: environment(), stdio: ["ignore", "pipe", "inherit"]});
|
||
|
|
let output = "";
|
||
|
|
child.stdout.on("data", chunk => {output += chunk;});
|
||
|
|
child.on("error", reject);
|
||
|
|
child.on("close", code => {
|
||
|
|
const artifact = output.trim();
|
||
|
|
if (code !== 0 || !/^\/nix\/store\/[a-z0-9]{32}-[^\s/]+$/.test(artifact)) reject(new Error(`Checked Nix build failed (${code}); see build diagnostics above`));
|
||
|
|
else resolve(artifact);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|