feat: reviewed libvips 1.3.4 supplement, and the native proof on sharp 0.35.5
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
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
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1788d17f0a
commit
afaf3a853c
@@ -7,6 +7,6 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ekklesia/module-sdk": "workspace:*",
|
||||
"sharp": "0.35.4"
|
||||
"sharp": "0.35.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const MIT = "MIT License\n\nCopyright (c) 2026 Example Author\n\nPermission is h
|
||||
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.4.node"),
|
||||
path.join(path.dirname(sharp), "@img", "sharp-linux-x64", "lib", "sharp-linux-x64-0.35.5.node"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ 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.6");
|
||||
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");
|
||||
|
||||
@@ -28,7 +28,7 @@ const execFileAsync = promisify(execFile);
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repositoryRoot = path.resolve(packageRoot, "..", "..");
|
||||
const LIBVIPS_DIGEST =
|
||||
"ekklesia-content-v1:sha256:50647b0b1e0b99943f24e4a1580650c0d8fe7744779b0f16271dcd2d59a7678c";
|
||||
"ekklesia-content-v1:sha256:2927fe11493461fa34847191d0b9b1c6441007c3e99f306e5708cb07fb0a5e59";
|
||||
const CLOSURE = [
|
||||
"@img/colour",
|
||||
"@img/sharp-libvips-linux-x64",
|
||||
@@ -139,18 +139,18 @@ describe.runIf(onTargetHost)("the linux-x64-glibc target, proven with sharp", ()
|
||||
expect(at, name).toBeGreaterThan(0);
|
||||
expect(notices.slice(at, at + 400), name).toMatch(/Used in: (?:backend, )?node_modules/u);
|
||||
}
|
||||
const libvips = notices.indexOf("\n@img/sharp-libvips-linux-x64 1.3.3\n");
|
||||
const nextEntry = notices.indexOf("\n@img/sharp-linux-x64 0.35.4\n");
|
||||
const libvips = notices.indexOf("\n@img/sharp-libvips-linux-x64 1.3.4\n");
|
||||
const nextEntry = notices.indexOf("\n@img/sharp-linux-x64 0.35.5\n");
|
||||
const section = notices.slice(libvips, nextEntry);
|
||||
// The declared licence is the package's own, unchanged.
|
||||
expect(section).toContain("License: LGPL-3.0-or-later\n");
|
||||
expect(section).toContain("[Reviewed license supplement]");
|
||||
expect(section).toContain(LIBVIPS_DIGEST);
|
||||
expect(section).toContain("not legal review");
|
||||
expect(section).toContain("[libvips 8.18.6: LICENSE]");
|
||||
expect(section).toContain("[@img/sharp-libvips-linux-x64 1.3.3: GNU LGPL version 3]");
|
||||
expect(section).toContain("[libvips 8.18.7: LICENSE]");
|
||||
expect(section).toContain("[@img/sharp-libvips-linux-x64 1.3.4: GNU LGPL version 3]");
|
||||
// Other packages carry their own shipped licence text, with no supplement.
|
||||
const sharp = notices.slice(notices.indexOf("\nsharp 0.35.4\n"));
|
||||
const sharp = notices.slice(notices.indexOf("\nsharp 0.35.5\n"));
|
||||
expect(sharp).toContain("[LICENSE]");
|
||||
expect(sharp).not.toContain("[Reviewed license supplement]");
|
||||
});
|
||||
@@ -226,10 +226,10 @@ describe.runIf(onTargetHost)("the linux-x64-glibc target, proven with sharp", ()
|
||||
}
|
||||
const native = outcome.mapped.filter((file) => /\.(?:node|so)(?:\.|$)/u.test(file));
|
||||
expect(native).toContain(
|
||||
`${extractTo}/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64-0.35.4.node`,
|
||||
`${extractTo}/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64-0.35.5.node`,
|
||||
);
|
||||
expect(native).toContain(
|
||||
`${extractTo}/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.18.6`,
|
||||
`${extractTo}/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.18.7`,
|
||||
);
|
||||
// Everything else is the host's own system libraries; nothing from the workspace.
|
||||
for (const file of native) {
|
||||
|
||||
@@ -228,24 +228,60 @@ describe("merging a supplement into THIRD_PARTY_NOTICES", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the reviewed libvips supplement", () => {
|
||||
it("loads intact, and pins exactly the installed @img/[email protected]", async () => {
|
||||
const supplements = await loadLicenseSupplements(DEFAULT_SUPPLEMENTS_DIRECTORY);
|
||||
expect([...supplements.keys()]).toEqual(["@img/sharp-libvips-linux-x64"]);
|
||||
const [libvips] = supplements.get("@img/sharp-libvips-linux-x64") ?? [];
|
||||
expect(libvips).toMatchObject({ license: "LGPL-3.0-or-later", version: "1.3.3" });
|
||||
describe("the reviewed libvips supplements", () => {
|
||||
// Each reviewed supplement against the real package it names, as installed: 1.3.4 with sharp
|
||||
// 0.35.5 (the native proof's), and 1.3.3 with sharp 0.35.4, installed for this test alone as
|
||||
// `sharp-0.35.4`. A package built with either sharp matches exactly the supplement reviewed for its
|
||||
// libvips, and never the other.
|
||||
const INSTALLED = [
|
||||
{ libvips: "8.18.6", sharp: "sharp-0.35.4", version: "1.3.3" },
|
||||
{ libvips: "8.18.7", sharp: "sharp", version: "1.3.4" },
|
||||
] as const;
|
||||
|
||||
const sharp = await realpath(await installedPackageFile("sharp"));
|
||||
async function installedLibvips(sharp: string) {
|
||||
const root = await realpath(await installedPackageFile(sharp));
|
||||
const target = NATIVE_TARGETS.get("linux-x64-glibc");
|
||||
if (target === undefined) throw new Error("target");
|
||||
const closure = await nativeClosure([{ name: "sharp", root: sharp }], target);
|
||||
const closure = await nativeClosure([{ name: "sharp", root }], target);
|
||||
const installed = closure.find((entry) => entry.name === "@img/sharp-libvips-linux-x64");
|
||||
expect(installed && contentDigest(installed.files)).toBe(libvips?.contentDigest);
|
||||
if (installed === undefined) throw new Error("libvips is not in sharp's closure");
|
||||
return installed;
|
||||
}
|
||||
|
||||
it("loads both intact, one per reviewed version, with distinct content digests", async () => {
|
||||
const supplements = await loadLicenseSupplements(DEFAULT_SUPPLEMENTS_DIRECTORY);
|
||||
expect([...supplements.keys()]).toEqual(["@img/sharp-libvips-linux-x64"]);
|
||||
const libvips = supplements.get("@img/sharp-libvips-linux-x64") ?? [];
|
||||
expect(libvips.map((supplement) => supplement.version)).toEqual(["1.3.3", "1.3.4"]);
|
||||
expect(new Set(libvips.map((supplement) => supplement.contentDigest)).size).toBe(2);
|
||||
for (const supplement of libvips) {
|
||||
expect(supplement.license).toBe("LGPL-3.0-or-later");
|
||||
}
|
||||
});
|
||||
|
||||
it.each(
|
||||
INSTALLED,
|
||||
)("pins exactly the installed @img/sharp-libvips-linux-x64@$version, and only it", async ({
|
||||
libvips: library,
|
||||
sharp,
|
||||
version,
|
||||
}) => {
|
||||
const supplements = await loadLicenseSupplements(DEFAULT_SUPPLEMENTS_DIRECTORY);
|
||||
const installed = await installedLibvips(sharp);
|
||||
expect(installed.version).toBe(version);
|
||||
const digest = contentDigest(installed.files);
|
||||
// The packager's selection: the same name, the same version, and the same content digest.
|
||||
const matching = (supplements.get(installed.name) ?? []).filter(
|
||||
(supplement) =>
|
||||
supplement.version === installed.version && supplement.contentDigest === digest,
|
||||
);
|
||||
expect(matching.map((supplement) => supplement.version)).toEqual([version]);
|
||||
const [libvips] = matching;
|
||||
|
||||
// Every library built into the binary is covered, with material from a recorded source, and the
|
||||
// open determination is stated rather than resolved.
|
||||
const names = new Set(libvips?.components.map((component) => component.name));
|
||||
for (const library of [
|
||||
for (const name of [
|
||||
"libvips",
|
||||
"glib",
|
||||
"gvdb",
|
||||
@@ -255,13 +291,52 @@ describe("the reviewed libvips supplement", () => {
|
||||
"mozjpeg",
|
||||
"zlib-ng",
|
||||
]) {
|
||||
expect(names.has(library), library).toBe(true);
|
||||
expect(names.has(name), name).toBe(true);
|
||||
}
|
||||
expect(libvips?.components.find((component) => component.name === "libvips")?.version).toBe(
|
||||
library,
|
||||
);
|
||||
for (const component of libvips?.components ?? []) {
|
||||
expect(component.material.length, component.name).toBeGreaterThan(0);
|
||||
expect(component.source, component.name).toMatch(/https:\/\//u);
|
||||
}
|
||||
expect(libvips?.statement).toContain("open determination");
|
||||
expect(libvips?.statement).toContain(`lib/libvips-cpp.so.${library}`);
|
||||
expect(names.has("proxy-libintl")).toBe(false);
|
||||
});
|
||||
|
||||
it("records each version's own upstream statement for cairo, and adds a standard text only where none ships", async () => {
|
||||
const supplements = await loadLicenseSupplements(DEFAULT_SUPPLEMENTS_DIRECTORY);
|
||||
const byVersion = new Map(
|
||||
(supplements.get("@img/sharp-libvips-linux-x64") ?? []).map((supplement) => [
|
||||
supplement.version,
|
||||
supplement,
|
||||
]),
|
||||
);
|
||||
const cairo = (version: string) =>
|
||||
byVersion.get(version)?.components.find((component) => component.name === "cairo");
|
||||
// sharp-libvips v1.3.3 stated MPL 2.0, which cairo does not ship, so its standard text was added.
|
||||
expect(cairo("1.3.3")?.license).toBe("Mozilla Public License 2.0");
|
||||
expect(cairo("1.3.3")?.material.map((material) => material.title)).toContain(
|
||||
"MPL-2.0 (the version sharp-libvips states cairo is used under; standard text from https://www.mozilla.org/MPL/2.0/)",
|
||||
);
|
||||
// v1.3.4 states MPL 1.1, whose text cairo ships itself; nothing is added.
|
||||
expect(cairo("1.3.4")?.license).toBe("Mozilla Public License 1.1");
|
||||
expect(cairo("1.3.4")?.material.map((material) => material.title)).toEqual([
|
||||
"COPYING",
|
||||
"COPYING-LGPL-2.1",
|
||||
"COPYING-MPL-1.1",
|
||||
]);
|
||||
// selectors ships no licence file in either, and carries the standard MPL 2.0 text in both.
|
||||
for (const version of ["1.3.3", "1.3.4"]) {
|
||||
const selectors = byVersion
|
||||
.get(version)
|
||||
?.components.find((component) => component.name === "selectors");
|
||||
expect(selectors?.license, version).toBe("MPL-2.0");
|
||||
expect(
|
||||
selectors?.material.map((material) => material.title),
|
||||
version,
|
||||
).toEqual(["MPL-2.0 (standard text; the crate ships no licence file)"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user