Files
Ekklesia/packages/module-pack/tests/native-closure.test.ts
ekklesiaandClaude Opus 5.5 afaf3a853c
CI / verify (pull_request) Successful in 1m4s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / package-release (pull_request) Successful in 34s
CI / image-build (pull_request) Successful in 30s
CI / deploy-smoke (same-origin) (pull_request) Successful in 47s
CI / deploy-smoke (split) (pull_request) Successful in 1m28s
CI / package-publication (pull_request) Successful in 50s
feat: reviewed libvips 1.3.4 supplement, and the native proof on sharp 0.35.5
sharp 0.35.4 carries a production-relevant librsvg advisory (GHSA-wq5f-xc86-pv6w). Its fix, sharp
0.35.5, loads @img/sharp-libvips-linux-x64 1.3.4, which ships no licence material either. This adds
the reviewed supplement for that exact package (ADR 0052) and moves the packager's native proof to
sharp 0.35.5, so a later 0.2.1 release can package it on linux-x64-glibc. Nothing in module-pack's
code changes: supplement selection is already by exact name, version, and content digest.

- Supplement: license-supplements/@img/sharp-libvips-linux-x64/1.3.4/
  - content digest ekklesia-content-v1:sha256:2927fe11493461fa34847191d0b9b1c6441007c3e99f306e5708cb07fb0a5e59;
  - 190 components and 146 hashed texts;
  - assembled with the same method as 1.3.3's, from sharp-libvips v1.3.4's own inputs. That method
    reproduces the 1.3.3 supplement byte for byte.
  - PROVENANCE.md records:
    - the 27 upstream archives and five build-time patches, by sha256;
    - the librsvg 2.63.2 crate reproduction (cargo 1.99.0; Cargo.lock 5723e14c…);
    - rustc 75a75c3e…;
    - every change from 1.3.3.
  - Notable: sharp-libvips v1.3.4 now states cairo is used under MPL 1.1, not MPL 2.0. cairo ships
    that text itself, so the standard MPL 2.0 text is no longer added for cairo; selectors still
    carries it.
  - No legal conclusion: the LGPL and MPL source-availability determination stays open, as for 1.3.3.
- The 1.3.3 supplement is unchanged, and still proven: module-pack keeps sharp 0.35.4 as a test-only
  devDependency alias (`sharp-0.35.4`). Each supplement is pinned against its real installed
  libvips, and each matches only its own.
- Native proof fixture: sharp 0.35.4 → 0.35.5. The closure has the same shape: sharp, @img/colour,
  detect-libc, semver, and @img/sharp-linux-x64 0.35.5 with @img/sharp-libvips-linux-x64 1.3.4.
  The runtime read from the binaries is unchanged: Node-API 9, glibc >= 2.28, x86-64.
- Docs: the module guide's note on available supplements.

No version bump, no publication, no tag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 21:34:35 -05:00

280 lines
10 KiB
TypeScript

import { readFile, symlink } from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { describeElf, ElfFormatError } from "../src/elf.js";
import { type ModulePackErrorCode, packModule } from "../src/index.js";
import {
cleanupFixtures,
type FakePackage,
type FixtureModuleOptions,
fixtureModule,
installedPackageFile,
SOURCE_REVISION,
scratchDirectory,
} from "./support/fixture-module.js";
import { fileEntries, readTarGz } from "./support/tar.js";
/**
* The native closure rules (ADR 0049), with fake packages built around the real sharp addon's bytes,
* so each rule can be broken on its own: which packages are copied, which are left out, and what
* fails closed.
*/
afterEach(cleanupFixtures);
const MIT = "MIT License\n\nCopyright (c) 2026 Example Author\n\nPermission is hereby granted.\n";
async function sharpAddon(): Promise<Buffer> {
const sharp = await installedPackageFile("sharp");
return readFile(
path.join(path.dirname(sharp), "@img", "sharp-linux-x64", "lib", "sharp-linux-x64-0.35.5.node"),
);
}
function wrapper(extra: Record<string, unknown> = {}): FakePackage {
return {
files: { "index.js": "export default 1;\n", LICENSE: MIT, "index.d.ts": "export {};\n" },
name: "fake-native",
packageJson: {
dependencies: { "fake-helper": "1.0.0" },
devDependencies: { "fake-devtool": "1.0.0" },
optionalDependencies: {
"fake-native-darwin-arm64": "1.0.0",
"fake-native-linux-x64-gnu": "1.0.0",
"fake-native-linux-x64-musl": "1.0.0",
},
...extra,
},
};
}
async function platformPackages(addon: Uint8Array): Promise<FakePackage[]> {
return [
{
files: { "lib/fake.node": addon, LICENSE: MIT, "index.js": "module.exports = 1;\n" },
name: "fake-native-linux-x64-gnu",
packageJson: { cpu: ["x64"], libc: ["glibc"], os: ["linux"], type: "commonjs" },
},
{
files: { "lib/fake.node": addon, LICENSE: MIT },
name: "fake-native-darwin-arm64",
packageJson: { cpu: ["arm64"], os: ["darwin"] },
},
{
files: { "lib/fake.node": addon, LICENSE: MIT },
name: "fake-native-linux-x64-musl",
packageJson: { cpu: ["x64"], libc: ["musl"], os: ["linux"] },
},
{ files: { "index.js": "export default 2;\n", LICENSE: MIT }, name: "fake-helper" },
{ files: { "index.js": "export default 3;\n", LICENSE: MIT }, name: "fake-devtool" },
];
}
const importsFakeNative =
'import value from "fake-native";\nexport default { id: "example", version: "0.1.0", register() { return value; } };\n';
async function packNative(options: FixtureModuleOptions, target = "linux-x64-glibc") {
const moduleDirectory = await fixtureModule(options);
const outDirectory = await scratchDirectory("ekklesia-native-closure-out-");
const result = await packModule({
licenseFile: path.join(moduleDirectory, "LICENSE"),
moduleDirectory,
nodeRange: ">=24.11.0 <25",
outDirectory,
sourceRevision: SOURCE_REVISION,
target,
});
return { archive: await readFile(result.archivePath), result };
}
async function refusal(
options: FixtureModuleOptions,
code: ModulePackErrorCode,
target = "linux-x64-glibc",
): Promise<void> {
await expect(packNative(options, target)).rejects.toMatchObject({ code });
}
describe("the native closure", () => {
it("copies the native package, its production dependencies, and only the matching platform package", async () => {
const { archive, result } = await packNative({
fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))],
serverSource: importsFakeNative,
});
const files = [...fileEntries(archive).keys()].map((name) => name.replace(/^example\//u, ""));
const copied = new Set(
files.filter((name) => name.startsWith("node_modules/")).map((name) => name.split("/")[1]),
);
expect([...copied].sort()).toEqual(["fake-helper", "fake-native", "fake-native-linux-x64-gnu"]);
// Every copied file is a real file; declarations never ship.
expect(
readTarGz(archive).every((entry) => entry.typeflag === "0" || entry.typeflag === "5"),
).toBe(true);
expect(files).not.toContain("node_modules/fake-native/index.d.ts");
expect(files).toContain("node_modules/fake-native-linux-x64-gnu/lib/fake.node");
const metadata = JSON.parse(
fileEntries(archive).get("example/ekklesia.package.json")?.toString("utf8") ?? "{}",
);
// The sharp addon alone: Node-API 9, and GLIBC_2.17 is the newest symbol version it needs.
expect(metadata.runtime).toEqual({
libcVersion: ">=2.17.0",
node: ">=24.11.0 <25",
nodeApi: 9,
});
expect(result.target).toBe("linux-x64-glibc");
const notices = fileEntries(archive).get("example/THIRD_PARTY_NOTICES")?.toString("utf8") ?? "";
expect(notices).toContain(
"fake-native-linux-x64-gnu 1.0.0\nLicense: MIT\nUsed in: node_modules",
);
expect(notices).not.toContain("fake-devtool");
expect(notices).not.toContain("fake-native-darwin-arm64");
});
it("refuses a required dependency that is not installed", async () => {
const packages = (await platformPackages(await sharpAddon())).filter(
(fake) => fake.name !== "fake-helper",
);
await refusal(
{ fakePackages: [wrapper(), ...packages], serverSource: importsFakeNative },
"MODULE_PACK_NATIVE_DEPENDENCY_MISSING",
);
});
it("refuses a required dependency built for another platform", async () => {
await refusal(
{
fakePackages: [
wrapper({ dependencies: { "fake-helper": "1.0.0", "fake-required-darwin": "1.0.0" } }),
...(await platformPackages(await sharpAddon())),
{
files: { "index.js": "export default 4;\n", LICENSE: MIT },
name: "fake-required-darwin",
packageJson: { os: ["darwin"] },
},
],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_TARGET_MISMATCH",
);
});
it("refuses two versions of one package in the flat layout", async () => {
const addon = await sharpAddon();
const packages = await platformPackages(addon);
const platform = packages.find((fake) => fake.name === "fake-native-linux-x64-gnu");
const conflicting: FakePackage = {
...(platform as FakePackage),
files: {
...(platform?.files ?? {}),
"node_modules/fake-helper/package.json": JSON.stringify({
license: "MIT",
name: "fake-helper",
version: "2.0.0",
}),
"node_modules/fake-helper/LICENSE": MIT,
},
packageJson: { ...(platform?.packageJson ?? {}), dependencies: { "fake-helper": "2.0.0" } },
};
await refusal(
{
fakePackages: [wrapper(), conflicting, ...packages.filter((fake) => fake !== platform)],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_LAYOUT_CONFLICT",
);
});
it("refuses an addon built for another machine, or one that is not an ELF object", async () => {
const foreign = Buffer.from(await sharpAddon());
foreign.writeUInt16LE(183, 18); // EM_AARCH64
await refusal(
{
fakePackages: [wrapper(), ...(await platformPackages(foreign))],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_TARGET_MISMATCH",
);
await refusal(
{
fakePackages: [wrapper(), ...(await platformPackages(Buffer.from("not an addon")))],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_TARGET_MISMATCH",
);
});
it("refuses a link inside a native package", async () => {
const moduleDirectory = await fixtureModule({
fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))],
serverSource: importsFakeNative,
});
await symlink(
"/etc/hostname",
path.join(moduleDirectory, "node_modules", "fake-native-linux-x64-gnu", "escape"),
);
await expect(
packModule({
licenseFile: path.join(moduleDirectory, "LICENSE"),
moduleDirectory,
nodeRange: ">=24.11.0 <25",
outDirectory: await scratchDirectory(),
sourceRevision: SOURCE_REVISION,
target: "linux-x64-glibc",
}),
).rejects.toMatchObject({ code: "MODULE_PACK_NATIVE_PACKAGE_INVALID" });
});
it("refuses a native dependency that does not support the package's Node.js range", async () => {
await refusal(
{
fakePackages: [
wrapper({ engines: { node: ">=99" } }),
...(await platformPackages(await sharpAddon())),
],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_NODE_INCOMPATIBLE",
);
});
it("refuses a native target for a module with no native addon", async () => {
await refusal({}, "MODULE_PACK_NATIVE_TARGET_UNNEEDED");
});
it("refuses targets it does not produce", async () => {
for (const target of ["linux-arm64-glibc", "linux-x64-musl", "darwin-arm64", "win32-x64"]) {
await refusal({}, "MODULE_PACK_TARGET_UNSUPPORTED", target);
}
});
it("still refuses the same native package in an any-target package", async () => {
await refusal(
{
fakePackages: [wrapper(), ...(await platformPackages(await sharpAddon()))],
serverSource: importsFakeNative,
},
"MODULE_PACK_NATIVE_DEPENDENCY",
"any",
);
});
});
describe("reading an ELF object", () => {
it("reads the machine, needed libraries, symbol versions, and declared Node-API version", async () => {
const description = describeElf(await sharpAddon());
expect(description.machine).toBe(62);
expect(description.needed).toContain("libvips-cpp.so.8.18.7");
expect(description.exported.has("napi_register_module_v1")).toBe(true);
expect(description.nodeApiModuleVersion).toBe(9);
expect(description.requiredVersions).toContain("GLIBC_2.17");
});
it("refuses what it cannot read", () => {
expect(() => describeElf(Buffer.from("\u007fELF"))).toThrow(ElfFormatError);
const thirtyTwoBit = Buffer.alloc(64);
Buffer.from([0x7f, 0x45, 0x4c, 0x46, 1, 1]).copy(thirtyTwoBit);
expect(() => describeElf(thirtyTwoBit)).toThrow(ElfFormatError);
});
});