diff --git a/docker/feature-manifest.json b/docker/feature-manifest.json index 97099e22..2cdd1472 100644 --- a/docker/feature-manifest.json +++ b/docker/feature-manifest.json @@ -1,7 +1,10 @@ { "manifestVersion": 2, "imageVersion": "2.0.0", - "pythonVersion": { "amd64": "3.12", "arm64": "3.11" }, + "pythonVersion": { + "amd64": "3.12", + "arm64": "3.11" + }, "basePackages": [ "numpy==1.26.4", "Pillow==12.3.0", @@ -13,7 +16,8 @@ "scipy==1.12.0", "scikit-learn==1.4.2", "scikit-image==0.24.0", - "pandas==2.2.2" + "pandas==2.2.2", + "huggingface-hub==0.36.2" ], "bundleRepo": "deepsafe/feature-bundles", "bundles": { @@ -560,7 +564,7 @@ } }, "packages": { - "common": ["faster-whisper>=1.0.0"], + "common": ["faster-whisper>=1.0.0", "huggingface-hub[hf_xet]==0.36.2"], "amd64": [], "arm64": ["protobuf>=4.25.3,<5"] }, diff --git a/tests/unit/features/feature-manifest.test.ts b/tests/unit/features/feature-manifest.test.ts index 3acd3e28..8ffd13b1 100644 --- a/tests/unit/features/feature-manifest.test.ts +++ b/tests/unit/features/feature-manifest.test.ts @@ -75,6 +75,53 @@ describe("Feature manifest structure", () => { }); }); +describe("Feature manifest: cross-bundle dependency agreement", () => { + // #669: the arm64 transcription bundle baked huggingface_hub 1.22.0 (pulled + // transitively by unpinned faster-whisper at bundle-build time) while + // inpaint-hq's transformers needs hub <1.0. Bundles ship as diffs against + // the base venv, so last-writer-wins left 1.22.0 in the shared venv and the + // hq install failed verification on arm64. + function pinnedVersions(): Map> { + const byPackage = new Map>(); + for (const [bundleId, bundle] of Object.entries>(bundles)) { + const pkgs = bundle.packages as Record; + const specs = [...(pkgs.common ?? []), ...(pkgs.amd64 ?? []), ...(pkgs.arm64 ?? [])]; + for (const spec of specs) { + const m = /^([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?==(\S+)$/.exec(spec); + if (!m) continue; + const name = m[1].toLowerCase(); + const perBundle = byPackage.get(name) ?? new Map(); + perBundle.set(bundleId, m[2]); + byPackage.set(name, perBundle); + } + } + return byPackage; + } + + it("bundles that pin the same package agree on the version", () => { + for (const [pkg, byBundle] of pinnedVersions()) { + const versions = new Set(byBundle.values()); + expect( + versions.size, + `${pkg} is pinned to different versions across bundles: ${JSON.stringify([...byBundle.entries()])}`, + ).toBe(1); + } + }); + + it("huggingface-hub is constrained and every bundle pin matches the constraint", () => { + const constraints: string[] = manifest.constraints ?? []; + const hub = constraints.find((c) => c.toLowerCase().startsWith("huggingface-hub==")); + expect( + hub, + "constraints must pin huggingface-hub so no bundle rebuild can strand hub 1.x in the shared venv", + ).toBeDefined(); + const version = (hub ?? "").split("==")[1]; + for (const [bundleId, v] of pinnedVersions().get("huggingface-hub") ?? new Map()) { + expect(v, `${bundleId} pins huggingface-hub differently from the constraint`).toBe(version); + } + }); +}); + describe("Feature manifest: shared-venv numpy-1.x ABI closure lock", () => { // Legacy feature bundles still share one venv. A package that upgrades numpy // can strand scipy/scikit-learn/pandas wheels on the wrong ABI and break every