2026-09-22 13:51:32 -07:00
|
|
|
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));
|
2026-09-30 22:03:17 -07:00
|
|
|
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");
|
2026-09-22 13:51:32 -07:00
|
|
|
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) {
|
2026-09-30 22:03:17 -07:00
|
|
|
const manifest = compiler.parseArtifactGroup(
|
2026-09-22 13:51:32 -07:00
|
|
|
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
manifest.packageRevisionId !== pkg.revision ||
|
2026-09-30 22:03:17 -07:00
|
|
|
manifest.bindingDigest !== contract.digest ||
|
|
|
|
|
JSON.stringify(manifest.versions) !== JSON.stringify(contract.definition.versions)
|
2026-09-22 13:51:32 -07:00
|
|
|
)
|
|
|
|
|
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"));
|
2026-09-30 22:03:17 -07:00
|
|
|
const expected = compiler.expectedGroupReceipt(contract, manifest, generatorDigest);
|
2026-09-22 13:51:32 -07:00
|
|
|
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,
|
2026-09-30 22:03:17 -07:00
|
|
|
bindingDigest: contract.digest,
|
|
|
|
|
sourceDigest: config.sourceDigest,
|
|
|
|
|
versions: contract.definition.versions,
|
2026-09-22 13:51:32 -07:00
|
|
|
implementations,
|
|
|
|
|
...(components.length ? { components } : {}),
|
|
|
|
|
});
|
2026-09-30 22:03:17 -07:00
|
|
|
const { receipt, sources } = compiler.expectedPackageReceipt(contract, manifest, config.sourceDigest, generatorDigest);
|
2026-09-22 13:51:32 -07:00
|
|
|
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));
|
2026-09-30 22:03:17 -07:00
|
|
|
// Source selection is attested here; compilation groups retain their own receipt.
|
|
|
|
|
compiler.checkPackageCoverage({ ...pkg, bindingDigest: contract.digest, sourceDigest: config.sourceDigest }, manifest);
|