Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies. Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior. Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
98 lines
5.0 KiB
JavaScript
98 lines
5.0 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { createHash } from "node:crypto";
|
|
import { pathToFileURL } from "node:url";
|
|
import { buildService } from "./build-service.mjs";
|
|
|
|
const config = JSON.parse(await fs.readFile(process.argv[2], "utf8"));
|
|
const compiler = await import(pathToFileURL(config.compiler));
|
|
const contract = JSON.parse(await fs.readFile(config.buildContract, "utf8"));
|
|
compiler.checkPackageBuildContract(contract);
|
|
const pkg = contract.definition.package;
|
|
if (pkg.revision !== config.packageRevisionId) throw Error("Invalid package assembly context");
|
|
const output = process.env.out;
|
|
if (!output) throw Error("Package assembly requires an output directory");
|
|
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
const generatorDigest = digest(await fs.readFile(config.compiler));
|
|
const implementations = [],
|
|
components = [],
|
|
files = new Map(),
|
|
ids = new Set();
|
|
const recordFile = async (name, mediaType) => {
|
|
const bytes = await fs.readFile(path.join(output, compiler.artifactPath(name)));
|
|
return { path: name, bytes: bytes.length, digest: digest(bytes), mediaType };
|
|
};
|
|
await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true });
|
|
for (const root of config.groups) {
|
|
const manifest = compiler.parseArtifactGroup(
|
|
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
|
|
);
|
|
if (
|
|
manifest.packageRevisionId !== pkg.revision ||
|
|
manifest.bindingDigest !== contract.digest ||
|
|
JSON.stringify(manifest.versions) !== JSON.stringify(contract.definition.versions)
|
|
)
|
|
throw Error("Artifact group belongs to a different checked package");
|
|
const receipt = JSON.parse(await fs.readFile(path.join(root, "share/quixos/build-receipt.json"), "utf8"));
|
|
const expected = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
|
|
if (JSON.stringify(receipt) !== JSON.stringify(expected.receipt))
|
|
throw Error("Artifact group has an invalid build receipt");
|
|
const read = await compiler.artifactFileReader({ [pkg.revision]: root });
|
|
for (const artifact of [...manifest.implementations, ...(manifest.components ?? [])]) {
|
|
const key = `${artifact.compatibility === "org.quixos.react.esm/1" ? "module" : "implementation"}:${artifact.id}`;
|
|
if (ids.has(key)) throw Error("Duplicate artifact group export " + key);
|
|
ids.add(key);
|
|
for (const file of artifact.files) {
|
|
const bytes = await read(pkg.revision, file.path, file.bytes);
|
|
if (bytes.length !== file.bytes || digest(bytes) !== file.digest)
|
|
throw Error("Modified group asset " + file.path);
|
|
const previous = files.get(file.path);
|
|
if (previous && JSON.stringify(previous) !== JSON.stringify(file))
|
|
throw Error("Conflicting group asset " + file.path);
|
|
if (!previous) {
|
|
await fs.mkdir(path.dirname(path.join(output, file.path)), { recursive: true });
|
|
await fs.writeFile(path.join(output, file.path), bytes, { mode: file.path.startsWith("bin/") ? 0o755 : 0o644 });
|
|
files.set(file.path, file);
|
|
}
|
|
}
|
|
}
|
|
for (const [name, bytes] of expected.sources) {
|
|
const actual = await fs.readFile(path.join(root, name));
|
|
if (!Buffer.from(bytes).equals(actual)) throw Error("Modified group type witness " + name);
|
|
await fs.writeFile(path.join(output, name), bytes);
|
|
}
|
|
implementations.push(...manifest.implementations);
|
|
components.push(...(manifest.components ?? []));
|
|
}
|
|
if (implementations.filter((entry) => entry.compatibility === "quixos-service-v1").length > 1)
|
|
throw Error("A package can have only one process service owner");
|
|
if (!implementations.some((entry) => entry.compatibility === "quixos-service-v1"))
|
|
await buildService({
|
|
config: { ...config, service: {} },
|
|
compiler,
|
|
pkg,
|
|
output,
|
|
coreServer: path.join(config.sharedRuntime, "core.mjs"),
|
|
sharedFiles: [...files.values()].filter((file) => file.path.startsWith("share/quixos/shared/")),
|
|
implementations,
|
|
recordFile,
|
|
});
|
|
const manifest = compiler.parsePackageArtifacts({
|
|
schemaVersion: 1,
|
|
packageRevisionId: pkg.revision,
|
|
bindingDigest: contract.digest,
|
|
sourceDigest: config.sourceDigest,
|
|
versions: contract.definition.versions,
|
|
implementations,
|
|
...(components.length ? { components } : {}),
|
|
});
|
|
const { receipt, sources } = compiler.expectedPackageReceipt(contract, manifest, config.sourceDigest, generatorDigest);
|
|
for (const [name, bytes] of sources) {
|
|
const actual = await fs.readFile(path.join(output, name));
|
|
if (!Buffer.from(bytes).equals(actual)) throw Error("Assembly lacks checked generated source " + name);
|
|
}
|
|
await fs.writeFile(path.join(output, "share/quixos/package-artifacts.json"), JSON.stringify(manifest, null, 2));
|
|
await fs.writeFile(path.join(output, "share/quixos/build-receipt.json"), JSON.stringify(receipt, null, 2));
|
|
// Source selection is attested here; compilation groups retain their own receipt.
|
|
compiler.checkPackageCoverage({ ...pkg, bindingDigest: contract.digest, sourceDigest: config.sourceDigest }, manifest);
|