Build composable Camino packages with explicit type and source dependencies
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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 }),
|
||||
);
|
||||
Reference in New Issue
Block a user