// 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"; import { createHash } from "node:crypto"; 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 = builtins.appendContext ${nixString(config.nixpkgsNarHash)} (builtins.fromJSON ${nixString(JSON.stringify(config.narHashContext))}); }; in { derivation = compiler.drvPath; yarn = compiler.yarn-freestanding.drvPath; source = toString compiler.src; metadata = (import ((/. + ${nixString(source)}) + "/source-qualification-metadata.nix")) compiler; }`; 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); } function summarized(value) { const text = JSON.stringify(value) ?? "undefined"; return text.length < 500 ? value : { bytes: Buffer.byteLength(text), sha256: createHash("sha256").update(text).digest("hex"), prefix: text.slice(0, 180), }; } function structuralDifferences(actual, expected, field = "", result = []) { if (JSON.stringify(actual) === JSON.stringify(expected)) return result; if ( Array.isArray(actual) && Array.isArray(expected) && [...actual, ...expected].every((x) => typeof x === "string") ) { result.push({ field, onlyActual: actual.filter((x) => !expected.includes(x)), onlyExpected: expected.filter((x) => !actual.includes(x)), actualCount: actual.length, expectedCount: expected.length, sequenceChanged: true, }); } else if ( actual && expected && typeof actual === "object" && typeof expected === "object" && !Array.isArray(actual) && !Array.isArray(expected) ) { for (const key of new Set([...Object.keys(actual), ...Object.keys(expected)])) structuralDifferences(actual[key], expected[key], field ? field + "." + key : key, result); } else { const difference = { field, actual: summarized(actual), expected: summarized(expected) }; if (typeof actual === "string" && typeof expected === "string" && Math.max(actual.length, expected.length) >= 500) { let offset = 0; while (offset < actual.length && offset < expected.length && actual[offset] === expected[offset]) offset++; difference.offset = offset; difference.actualExcerpt = actual.slice(Math.max(0, offset - 80), offset + 200); difference.expectedExcerpt = expected.slice(Math.max(0, offset - 80), offset + 200); } result.push(difference); } return result; } function baselineDifference(baseline) { const { metadata, ...identities } = baseline; console.log( "PROTOCOL_SOURCE_BASELINE: " + JSON.stringify({ actual: identities, expected: config.expected, expectedSource: config.expectedSource, evaluatorVersion: config.evaluatorVersion, pinnedNixVersion: config.pinnedNixVersion, narHashContext: config.narHashContext, }), ); console.log( "PROTOCOL_SOURCE_BASELINE_DIFF: " + JSON.stringify(structuralDifferences(metadata, config.expectedMetadata)).slice(0, 10000), ); } 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(); if (baseline.derivation !== config.expected) baselineDifference(baseline); 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", derivation: baseline.derivation, yarn: baseline.yarn, source: baseline.source, cases, metadataContext: config.narHashContext, evaluatorVersion: config.evaluatorVersion, pinnedNixVersion: config.pinnedNixVersion, nestedBuilds: false, guestExecution: false, }, null, 2, ) + "\n", ); } finally { await fs.rm(scratch, { recursive: true, force: true }); }