CI / verify (pull_request) Successful in 1m3s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / package-release (pull_request) Successful in 36s
CI / package-publication (pull_request) Canceled after 0s
CI / deploy-smoke (same-origin) (pull_request) Canceled after 0s
CI / deploy-smoke (split) (pull_request) Canceled after 0s
CI / image-build (pull_request) Canceled after 30s
Ekklesia may publicly distribute a native Module Package containing LGPL or MPL components only beside a version-specific corresponding-source artifact. The artifact carries the exact source, patches, build definitions, and rebuild and relink instructions for those components. ADR 0055 (Accepted) records the policy: project compliance policy, not legal certification. It decides the item ADR 0052 deferred, and extends ADR 0049's distributable-release gate. Module Package v1, packageFormat 1, and both libvips supplements are unchanged. The mechanism is generic and lives in @ekklesia/module-pack: - Source recipes: reviewed data under corresponding-sources/<package>/<version>/. recipe.json names the exact package (name, version, content digest) and the exact supplement it was prepared with, and lists every component with its role, licence, licence material, and relationship to the binary. Every source file and patch is recorded by origin, size, and SHA-256; documents are hashed too, and every exclusion has a reason. Loading is strict and refuses anything unlisted. - Acquisition (the only network step) fetches each recorded file once into a cache keyed by SHA-256. It uses node:https with exact headers and no decoding, retries transient failures, and stores only bytes that match. - Construction is offline. From a built package it finds the covered native dependencies (by declared licence, or by a supplement component's licence), verifies every byte, and writes four sibling artifacts: the deterministic <package>-corresponding-source.tar.gz and its .sha256; <package>.redistribution.json, linking package, dependencies, supplement, recipe, and source by digest; and <package>.source-notice.txt, the release notice. - The public-distribution gate (verifyRedistribution, `ekklesia-corresponding-source verify`) approves only when the recipe, supplement, canonical archive, manifest, and metadata identify exactly these files. It refuses bundled copyleft code, and approves packages with no covered native code without an artifact. packModule is untouched. First complete proof: @img/[email protected] (sharp 0.35.5), from the reviewed supplement and provenance. - 383 components and 387 source files, about 208 MB: the sharp-libvips v1.3.4 build definitions, the 27 library archives, the five build-time patches, the 353 crates librsvg's resolved Cargo.lock records, and rust-src for nightly-2026-09-27 (rustc 75a75c3e0), which vendors std's dependencies. - BUILDING.md and two helpers point sharp-libvips's unchanged build logic at those files. - Followed in a Rocky Linux 8 container with networking disabled, they rebuilt libvips-cpp.so.8.18.7, byte-identically across two builds. Swapped into a verified, extracted native package, the rebuilt library was the one loaded, and sharp ran. - The native proof's source artifact, native-proof-0.1.0-linux-x64-glibc-corresponding-source.tar.gz, is 207242785 bytes, sha256 cc405492032b04a5391ab84e64f0e5e50f78e571ef7818204782c25a03eb50b7. It is byte-identical from unrelated directories and from a cache acquired fresh from upstream. Tests: - corresponding-source.test.ts runs in CI: synthetic inputs, every negative case, acquisition with retries and refusal, and determinism. - corresponding-source-native.test.ts checks the real recipe everywhere. With a verified cache (`pnpm test:corresponding-source`, which acquires once) it builds the real artifact twice, gates it, fails it closed, and runs sharp from the package. Docs: the module guide's distribution section, the README and architecture notes, and an ADR 0052 cross-reference. No Signage change, version bump, publication, or tag. Co-Authored-By: Claude Opus 5.5 <[email protected]>
156 lines
6.0 KiB
JavaScript
156 lines
6.0 KiB
JavaScript
#!/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 <module package> | --recipe <name>@<version>) --cache <directory>
|
|
* 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 <module package> --cache <directory> --out <directory>
|
|
* Offline. Writes <package>-corresponding-source.tar.gz, its .sha256, <package>.redistribution.json,
|
|
* and <package>.source-notice.txt.
|
|
* ekklesia-corresponding-source verify --package <module package> [--source <archive> --metadata <json>]
|
|
* The public-distribution gate. Exits 0 only when the package is approved.
|
|
*/
|
|
|
|
const USAGE = [
|
|
"Usage:",
|
|
" ekklesia-corresponding-source acquire (--package <archive> | --recipe <name>@<version>) --cache <directory>",
|
|
" ekklesia-corresponding-source build --package <archive> --cache <directory> --out <directory>",
|
|
" ekklesia-corresponding-source verify --package <archive> [--source <archive> --metadata <file>]",
|
|
].join("\n");
|
|
|
|
const OPTIONS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
acquire: ["--package", "--recipe", "--cache"],
|
|
build: ["--package", "--cache", "--out"],
|
|
verify: ["--package", "--source", "--metadata"],
|
|
});
|
|
const REQUIRED: Readonly<Record<string, readonly string[]>> = 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<string, string>;
|
|
} {
|
|
const [command, ...rest] = argv;
|
|
const allowed = command === undefined ? undefined : OPTIONS[command];
|
|
if (allowed === undefined) {
|
|
throw new ArgumentsError(USAGE);
|
|
}
|
|
const values = new Map<string, string>();
|
|
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<number> {
|
|
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();
|
|
}
|