// Evaluate the production compiler constructor; its checked build is a prerequisite. import assert from "node:assert/strict"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; const config = JSON.parse(await fs.readFile(process.argv[2], "utf8")); const output = process.argv[3]; const scratch = await fs.mkdtemp(path.join(os.tmpdir(), "protocol-source-")); const source = path.join(scratch, "source"); const nixString = (value) => JSON.stringify(value).replaceAll("${", "\\${"); async function writable(directory) { await fs.chmod(directory, 0o700); for (const item of await fs.readdir(directory, { withFileTypes: true })) { const file = path.join(directory, item.name); if (item.isDirectory()) await writable(file); else { const info = await fs.stat(file); await fs.chmod(file, info.mode & 0o111 ? 0o700 : 0o600); } } } function identity() { const expression = `let pkgs = import ${nixString(config.nixpkgs)} { system = ${nixString(config.system)}; }; compiler = import (${nixString(source)} + "/default.nix") { inherit pkgs; nixpkgsNarHash = ${nixString(config.nixpkgsNarHash)}; }; in { derivation = compiler.drvPath; yarn = compiler.yarn-freestanding.drvPath; source = toString compiler.src; }`; const result = spawnSync( "nix", [ "--store", "local?root=" + process.env.QUIXOS_PROTOCOL_EVALUATION_STORE, "eval", "--offline", "--impure", "--json", "--expr", expression, ], { encoding: "utf8", timeout: 30000, maxBuffer: 1048576 }, ); assert.equal(result.status, 0, result.error?.message ?? result.stderr); return JSON.parse(result.stdout); } async function edit(file, mutate, check) { const before = await fs.readFile(file, "utf8"); try { await fs.writeFile(file, mutate(before)); await check(); } finally { await fs.writeFile(file, before); } } const cases = []; async function record(name, check) { await check(); cases.push(name); console.log("PROTOCOL_SOURCE_INPUT: " + name); } try { await fs.cp(config.source, source, { recursive: true }); await writable(source); const baseline = identity(); await record("real production compiler and generated Yarn launcher identities", async () => { assert.equal(baseline.derivation, config.expected); assert.equal(baseline.yarn, config.yarn); }); for (const file of ["README.md", "flake.nix", "default.nix", "yarn-project.nix"]) { await record(file + " commentary preserves compiler identity", () => edit( path.join(source, file), (text) => text + (file.endsWith(".nix") ? "\n# source qualification\n" : "\nSource qualification.\n"), async () => assert.deepEqual(identity(), baseline), ), ); } await record("new documentation preserves compiler identity", async () => { const file = path.join(source, "NOTES.md"); try { await fs.writeFile(file, "qualification\n"); assert.deepEqual(identity(), baseline); } finally { await fs.rm(file, { force: true }); } }); async function compilerOnly() { const changed = identity(); assert.notEqual(changed.derivation, baseline.derivation); assert.equal(changed.yarn, baseline.yarn); } for (const file of [ "src/capability-language/cli.ts", "test/emit-managed.ts", "grammar/QuixosCapability.g4", "proto/quixos/refs.proto", "nix/check-package.mjs", "package.json", "yarn.lock", ".yarnrc.yml", ".yarn/plugins/yarn-plugin-nixify.cjs", "tsconfig.json", "tsconfig.build.json", "tsconfig.test.json", ]) { await record(file + " remains a compiler input", () => edit(path.join(source, file), (text) => text + "\n", compilerOnly), ); } for (const file of ["src/future.ts", "test/future.ts", "nix/future.nix"]) { await record(file + " is included without a file inventory update", async () => { const target = path.join(source, file); try { await fs.writeFile(target, "qualification\n"); await compilerOnly(); } finally { await fs.rm(target, { force: true }); } }); } await record("executable recipe changes invalidate compiler identity", () => edit( path.join(source, "default.nix"), (text) => { assert(text.includes("runHook preBuild")); return text.replace("runHook preBuild", "runHook preBuild\n echo qualification"); }, compilerOnly, ), ); await record("selected generated archive pin remains a compiler input", () => edit( path.join(source, "yarn-project.nix"), (text) => { const changedHash = "sha512-" + Buffer.alloc(64, 0x42).toString("base64"); const pattern = /("@types\/node@npm:[^\n]+hash = ")[^"]+(";)/; assert(pattern.test(text)); return text.replace(pattern, (_, before, after) => before + changedHash + after); }, async () => { await compilerOnly(); assert.equal(identity().source, baseline.source); }, ), ); await record("installed Nixpkgs trust identity remains a compiler input", async () => { const previous = config.nixpkgsNarHash; try { config.nixpkgsNarHash = "sha256-" + Buffer.alloc(32, 0x42).toString("base64"); assert.notEqual(config.nixpkgsNarHash, previous); await compilerOnly(); assert.equal(identity().source, baseline.source); } finally { config.nixpkgsNarHash = previous; } }); assert.deepEqual(identity(), baseline, "Every private mutation was restored"); await fs.mkdir(output, { recursive: true }); await fs.writeFile( path.join(output, "protocol-source.json"), JSON.stringify( { codec: "quixos-protocol-source-qualification/1", ...baseline, cases, nestedBuilds: false, guestExecution: false, }, null, 2, ) + "\n", ); } finally { await fs.rm(scratch, { recursive: true, force: true }); }