#!/usr/bin/env node import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; import { acquireCorrespondingSources, buildCorrespondingSource, CorrespondingSourceError, verifyRedistribution, } from "./corresponding-source.js"; /** * `ekklesia-corresponding-source` — corresponding-source artifacts for native packages (ADR 0055). * * ekklesia-corresponding-source acquire (--package | --recipe @) --cache * Downloads every recorded source file the package's covered native dependencies (or the named * recipe) need into the verified cache. The only subcommand that uses the network. * ekklesia-corresponding-source build --package --cache --out * Offline. Writes -corresponding-source.tar.gz, its .sha256, .redistribution.json, * and .source-notice.txt. * ekklesia-corresponding-source verify --package [--source --metadata ] * The public-distribution gate. Exits 0 only when the package is approved. */ const USAGE = [ "Usage:", " ekklesia-corresponding-source acquire (--package | --recipe @) --cache ", " ekklesia-corresponding-source build --package --cache --out ", " ekklesia-corresponding-source verify --package [--source --metadata ]", ].join("\n"); const OPTIONS: Readonly> = Object.freeze({ acquire: ["--package", "--recipe", "--cache"], build: ["--package", "--cache", "--out"], verify: ["--package", "--source", "--metadata"], }); const REQUIRED: Readonly> = Object.freeze({ acquire: ["--cache"], build: ["--package", "--cache", "--out"], verify: ["--package"], }); class ArgumentsError extends Error {} export function parseCorrespondingSourceArguments(argv: readonly string[]): { readonly command: "acquire" | "build" | "verify"; readonly values: ReadonlyMap; } { const [command, ...rest] = argv; const allowed = command === undefined ? undefined : OPTIONS[command]; if (allowed === undefined) { throw new ArgumentsError(USAGE); } const values = new Map(); for (let index = 0; index < rest.length; index += 2) { const option = rest[index] ?? ""; const value = rest[index + 1]; if ( !allowed.includes(option) || value === undefined || value.startsWith("--") || values.has(option) ) { throw new ArgumentsError(USAGE); } values.set(option, option === "--recipe" ? value : path.resolve(value)); } if (command === "acquire" && values.has("--package") === values.has("--recipe")) { throw new ArgumentsError(USAGE); } if ((REQUIRED[command as string] ?? []).some((option) => !values.has(option))) { throw new ArgumentsError(USAGE); } return { command: command as "acquire" | "build" | "verify", values }; } async function main(): Promise { try { const { command, values } = parseCorrespondingSourceArguments(process.argv.slice(2)); const packageArchivePath = values.get("--package") ?? ""; if (command === "acquire") { const recipe = values.get("--recipe"); const at = recipe?.lastIndexOf("@") ?? -1; if (recipe !== undefined && at <= 0) { throw new ArgumentsError(USAGE); } const result = await acquireCorrespondingSources({ cacheDirectory: values.get("--cache") ?? "", ...(recipe === undefined ? { packageArchivePath } : { recipes: [{ name: recipe.slice(0, at), version: recipe.slice(at + 1) }] }), }); process.stdout.write( `Source cache holds all ${result.files} recorded files (${result.bytes} bytes); fetched ${result.fetched}.\n`, ); } else if (command === "build") { const result = await buildCorrespondingSource({ cacheDirectory: values.get("--cache") ?? "", outDirectory: values.get("--out") ?? "", packageArchivePath, }); if (!result.required) { process.stdout.write( "The package redistributes no covered native code: no artifact is needed.\n", ); } else { process.stdout.write( `Wrote ${path.basename(result.sourceArchivePath ?? "")}, ${result.sourceBytes} bytes\nsha256 ${result.sourceSha256}\n`, ); } } else { const metadataPath = values.get("--metadata"); const sourceArchivePath = values.get("--source"); const approval = await verifyRedistribution({ packageArchivePath, ...(metadataPath === undefined ? {} : { metadataPath }), ...(sourceArchivePath === undefined ? {} : { sourceArchivePath }), }); process.stdout.write( approval.correspondingSource === undefined ? `Approved: ${approval.package.file} redistributes no covered native code.\n` : `Approved: ${approval.package.file} with ${approval.correspondingSource.file} (sha256 ${approval.correspondingSource.sha256}).\n`, ); } return 0; } catch (error) { if (error instanceof ArgumentsError) { process.stderr.write(`${error.message}\n`); return 64; } if (error instanceof CorrespondingSourceError) { process.stderr.write( `${error.code}: ${error.message}${error.subject ? ` (${error.subject})` : ""}\n`, ); return 1; } process.stderr.write("ekklesia-corresponding-source failed unexpectedly.\n"); return 1; } } function isDirectExecution(): boolean { const entrypoint = process.argv[1]; // Run through the committed launcher, which imports this file, or directly. return ( entrypoint !== undefined && (import.meta.url === pathToFileURL(path.resolve(entrypoint)).href || path.basename(entrypoint) === "ekklesia-corresponding-source.mjs" || path.basename(entrypoint) === "ekklesia-corresponding-source") ); } if (isDirectExecution()) { process.exitCode = await main(); }