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 }), );