104 lines
5.1 KiB
JavaScript
104 lines
5.1 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 plan = JSON.parse(await fs.readFile(config.plan, "utf8"));
|
|
const pkg = plan.definition.packages.find((pkg) => pkg.revision === config.packageRevisionId);
|
|
if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) 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.parsePackageArtifacts(
|
|
JSON.parse(await fs.readFile(path.join(root, "share/quixos/package-artifacts.json"), "utf8")),
|
|
);
|
|
if (
|
|
manifest.packageRevisionId !== pkg.revision ||
|
|
manifest.bindingDigest !== pkg.bindingDigest ||
|
|
manifest.sourceDigest !== pkg.sourceDigest ||
|
|
JSON.stringify(manifest.versions) !== JSON.stringify(plan.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.expectedArtifactReceipt(plan, 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: pkg.bindingDigest,
|
|
sourceDigest: pkg.sourceDigest,
|
|
versions: plan.definition.versions,
|
|
implementations,
|
|
...(components.length ? { components } : {}),
|
|
});
|
|
const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, 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));
|
|
// Check package coverage with the same public validator used at installation.
|
|
const packagePlan = { definition: { ...plan.definition, packages: [pkg] }, digest: "" };
|
|
packagePlan.digest = compiler.hexDigest(packagePlan.definition);
|
|
await compiler.checkExecutionWorld(
|
|
packagePlan,
|
|
[manifest],
|
|
await compiler.artifactFileReader({ [pkg.revision]: output }),
|
|
);
|