import { buildComponents } from "./build-components.mjs"; /** Trusted Nix build driver. No package-defined build hooks run between generation and witnessing. */ import fs from "node:fs/promises"; import path from "node:path"; import { createHash } from "node:crypto"; import { builtinModules } from "node:module"; import { execFileSync } from "node:child_process"; import { pathToFileURL } from "node:url"; 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((p) => p.revision === config.packageRevisionId); if (!pkg || compiler.hexDigest(plan.definition) !== plan.digest) throw Error("Invalid checked execution plan/package"); const output = process.env.out; if (!output) throw Error("Missing Nix output"); const digest = (bytes) => createHash("sha256").update(bytes).digest("hex"); const generatorDigest = digest(await fs.readFile(config.compiler)); const runtime = config.portable || config.remote ? JSON.parse(await fs.readFile(path.join(config.sharedRuntime, "runtime.json"), "utf8")) : { files: [] }; const relative = (p) => compiler.artifactPath(p); const implementations = []; const generated = new Map(); const recordFile = async (p, mediaType) => { const bytes = await fs.readFile(path.join(output, relative(p))); return { path: p, digest: digest(bytes), bytes: bytes.length, mediaType }; }; const copyRuntime = async () => { await fs.mkdir(path.join(output, "share/quixos/shared"), { recursive: true }); for (const file of runtime.files) { relative(file.path); const bytes = await fs.readFile(path.join(config.sharedRuntime, file.path)); if (bytes.length !== file.bytes || digest(bytes) !== file.digest) throw Error("Shared runtime mismatch"); await fs.writeFile(path.join(output, "share/quixos/shared", file.path), bytes); } }; await copyRuntime(); const sharedFiles = await Promise.all( runtime.files.map((f) => recordFile(`share/quixos/shared/${f.path}`, f.mediaType)), ); const coreFile = sharedFiles.find((f) => f.path.endsWith("/core.mjs")); const coreURL = coreFile ? compiler.contentArtifactUrl(coreFile) : null; const coreServer = path.join(config.sharedRuntime, "core.mjs"); await fs.mkdir(path.join(output, "share/quixos/generated"), { recursive: true }); const portable = config.portable; for (const target of Object.keys(portable?.entries ?? {})) if (!["browser", "server"].includes(target)) throw Error(`Unsupported portable entry target ${target}`); for (const settings of [portable, config.remote].filter(Boolean)) if (!/^[$A-Z_a-z][$\w]*$/.test(settings.registryExport)) throw Error("Registry export must be a JavaScript identifier"); const targets = portable ? [...new Set([...portable.targets, "server"])] : []; const builds = targets.map((target) => ({ target, mode: target, settings: portable })); if (config.remote) builds.push({ target: "server", mode: "remote", settings: config.remote }); for (const { target, mode, settings } of builds) { if (!["browser", "server"].includes(target)) throw Error(`Unsupported JS build target ${target}`); const work = path.resolve(`work-${mode}`); await fs.cp(config.source, work, { recursive: true, dereference: false }); const writable = async (directory) => { await fs.chmod(directory, 0o755); for (const item of await fs.readdir(directory, { withFileTypes: true })) { const file = path.join(directory, item.name); if (item.isSymbolicLink()) throw Error("Authored package source cannot contain symlinks"); if (item.isDirectory()) await writable(file); else await fs.chmod(file, 0o644); } }; await writable(work); const modules = path.join(work, "node_modules"); await fs.mkdir(path.join(modules, "@quixos"), { recursive: true }); if (config.nodeModules) { for (const item of await fs.readdir(config.nodeModules, { withFileTypes: true })) { if (item.name === "@quixos") throw Error("Application dependencies cannot substitute the checked SDK"); await fs.symlink(path.join(config.nodeModules, item.name), path.join(modules, item.name)); } } await fs.symlink(config.core, path.join(modules, "@quixos/camino-replica-core")); await fs.writeFile(path.join(work, "package.json"), JSON.stringify({ type: "module" })); const bindingPath = relative(settings.bindingOutput); const source = mode === "remote" ? compiler.generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision) : compiler.generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }); await fs.mkdir(path.dirname(path.join(work, bindingPath)), { recursive: true }); await fs.writeFile(path.join(work, bindingPath), source); generated.set(mode, { file: path.join(work, bindingPath), source }); await fs.writeFile(path.join(output, "share/quixos/generated", `${mode}.ts`), source); const entry = relative(settings.entries?.[target] ?? settings.entry); execFileSync( config.node, [ config.tsc, "--noEmit", "--strict", "--skipLibCheck", "false", "--target", "es2024", "--lib", "es2024,dom,esnext.disposable", "--module", "nodenext", "--moduleResolution", "nodenext", entry, ], { cwd: work, stdio: "inherit" }, ); const bundlePath = mode === "remote" ? "share/quixos/remote/server/module.mjs" : `share/quixos/portable/${target}/module.mjs`; await fs.mkdir(path.dirname(path.join(output, bundlePath)), { recursive: true }); const coreImport = target === "browser" ? coreURL : coreServer; const metadata = path.join(work, "bundle-meta.json"); execFileSync( config.esbuild, [ entry, "--bundle", `--platform=${target === "browser" ? "browser" : "node"}`, "--format=esm", "--minify-whitespace", "--target=es2024", `--outfile=${path.join(output, bundlePath)}`, `--metafile=${metadata}`, `--alias:@quixos/camino-replica-core=${coreImport}`, `--external:${coreImport}`, ], { cwd: work, stdio: "inherit" }, ); const meta = JSON.parse(await fs.readFile(metadata, "utf8")); const built = Object.values(meta.outputs).find((o) => o.entryPoint); if (!built?.exports.includes(settings.registryExport)) throw Error("Bundle lacks the declared registry export"); // Portable modules must contain their ordinary source dependencies and share only the checked SDK. if ( Object.values(meta.outputs).some((o) => o.imports.some( (i) => i.external && i.path !== coreImport && !(target === "server" && builtinModules.includes(i.path.replace(/^node:/, ""))), ), ) ) throw Error("Unexpected external import in portable module"); const exports = pkg.exports .filter((e) => mode === "remote" ? e.execution.kind === "remote" : e.execution.kind !== "remote" && e.execution.targets.includes(target), ) .map((e) => e.id); const body = { id: `${mode}-module`, target, compatibility: "quixos-js-module-v1", path: bundlePath, ports: "quixos-transaction-ports-v1", registryExport: settings.registryExport, exports, files: [await recordFile(bundlePath, "text/javascript"), ...sharedFiles], }; implementations.push({ ...body, contentDigest: compiler.implementationDigest(body) }); } const components = await buildComponents({ config, compiler, plan, pkg, output, recordFile, generated }); if (pkg.exports.length || config.service.entry) { await fs.mkdir(path.join(output, "bin"), { recursive: true }); await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true }); let serviceEntry; if (config.service.entry) { if (!/\.m?js$/.test(config.service.entry)) throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging"); serviceEntry = path.join(config.source, relative(config.service.entry)); } else { const modules = implementations.filter((implementation) => implementation.target === "server"); if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id)))) throw Error("Generated service does not cover every package export; provide a custom service"); if (!modules.length) throw Error("Generated service requires a checked registry"); serviceEntry = path.resolve("service-entry.mjs"); const imports = modules .map( (module, index) => `import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`, ) .join("\n"); await fs.writeFile( serviceEntry, imports + `\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)}; const registries = [${modules.map((_, index) => "registry" + index).join(",")}]; const owners = new Map(); for (const registry of registries) for (const id of registry.exports) { if (owners.has(id)) throw Error("Duplicate registry export " + id); owners.set(id, registry); } servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) { const registry = owners.get(request.exportId); if (!registry) throw Error("Unknown export"); return registry.invoke(request, channel, context); }});\n`, ); } const serviceModule = "share/quixos/service/main.mjs"; execFileSync( config.esbuild, [ serviceEntry, "--bundle", "--platform=node", "--format=esm", "--minify-whitespace", "--target=es2024", `--external:${coreServer}`, `--outfile=${path.join(output, serviceModule)}`, ], { stdio: "inherit" }, ); await fs.writeFile( path.join(output, "bin/service"), `#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`, { mode: 0o755 }, ); const serviceBody = { id: "service", target: "server", compatibility: "quixos-service-v1", path: "bin/service", ports: "quixos-transaction-ports-v1", exports: pkg.exports.map((e) => e.id), files: [ await recordFile("bin/service", "text/x-shellscript"), await recordFile(serviceModule, "text/javascript"), ...sharedFiles, ], }; implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) }); } const manifest = compiler.parsePackageArtifacts({ schemaVersion: 1, packageRevisionId: pkg.revision, bindingDigest: pkg.bindingDigest, sourceDigest: pkg.sourceDigest, versions: plan.definition.versions, implementations, ...(components.length ? { components } : {}), }); for (const { file, source } of generated.values()) if ((await fs.readFile(file, "utf8")) !== source) throw Error("Generated bindings changed during build"); const { receipt, sources } = compiler.expectedArtifactReceipt(plan, manifest, generatorDigest); for (const [file, bytes] of sources) if (!Buffer.from(bytes).equals(await fs.readFile(path.join(output, file)))) throw Error("Receipt generation differs from compiled bindings"); 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));