fix docker store seed target packages (#91547)

This commit is contained in:
Sally O'Malley
2026-06-08 23:38:46 -04:00
committed by GitHub
parent 84acb74a6a
commit c8a8152cd7
2 changed files with 203 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
// Lists production store packages from lockfile data.
// Lists current-target production packages for Docker's offline prune store seed.
import fs from "node:fs";
import path from "node:path";
import { parse } from "yaml";
@@ -6,6 +6,11 @@ import { parse } from "yaml";
const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
const roots = Array.isArray(parsed) ? parsed : [parsed];
const specs = new Set();
const target = {
cpu: process.arch,
libc: detectLibc(),
os: process.platform,
};
function packageSpec(name, version) {
if (!name || !version || typeof version !== "string") {
@@ -22,26 +27,72 @@ function packageSpec(name, version) {
return `${name}@${normalizedVersion}`;
}
function packageSpecFromLockfileKey(key) {
function detectLibc() {
if (process.platform !== "linux") {
return undefined;
}
const report = process.report?.getReport?.();
return report?.header?.glibcVersionRuntime ? "glibc" : "musl";
}
function matchesTargetSelector(selector, value) {
if (!Array.isArray(selector) || !value) {
return true;
}
const blocked = selector.some((entry) => entry === `!${value}`);
if (blocked) {
return false;
}
const allowed = selector.filter((entry) => typeof entry === "string" && !entry.startsWith("!"));
return allowed.length === 0 || allowed.includes(value);
}
function packageEntryForSpec(lockfile, spec) {
return lockfile?.packages?.[spec] ?? lockfile?.packages?.[`/${spec}`];
}
function normalizeLockfilePackageKey(key) {
if (typeof key !== "string") {
return undefined;
}
const normalizedKey = (key.startsWith("/") ? key.slice(1) : key).replace(/\(.+\)$/, "");
const separator = normalizedKey.lastIndexOf("@");
if (separator <= 0) {
return undefined;
}
return packageSpec(normalizedKey.slice(0, separator), normalizedKey.slice(separator + 1));
return (key.startsWith("/") ? key.slice(1) : key).replace(/\(.+\)$/, "");
}
function visitListNode(node) {
function snapshotForSpec(lockfile, spec) {
const snapshots = lockfile?.snapshots;
if (!snapshots) {
return undefined;
}
return (
snapshots[spec] ??
snapshots[`/${spec}`] ??
Object.entries(snapshots).find(([key]) => normalizeLockfilePackageKey(key) === spec)?.[1]
);
}
function packageSupportsTarget(lockfile, spec) {
const entry = packageEntryForSpec(lockfile, spec);
return (
matchesTargetSelector(entry?.os, target.os) &&
matchesTargetSelector(entry?.cpu, target.cpu) &&
matchesTargetSelector(entry?.libc, target.libc)
);
}
function addSpec(lockfile, spec) {
if (spec && packageSupportsTarget(lockfile, spec)) {
specs.add(spec);
}
}
function visitListNode(lockfile, node) {
for (const dep of Object.values(node.dependencies ?? {})) {
const name = dep.from || dep.name;
const spec = packageSpec(name, dep.version);
if (spec && dep.resolved?.startsWith("https://registry.npmjs.org/")) {
specs.add(spec);
addSpec(lockfile, spec);
}
visitListNode(dep);
visitListNode(lockfile, dep);
}
}
@@ -53,15 +104,6 @@ function readLockfile() {
return parse(fs.readFileSync(lockfilePath, "utf8"));
}
function addLockfilePackages(lockfile) {
for (const key of Object.keys(lockfile?.packages ?? {})) {
const spec = packageSpecFromLockfileKey(key);
if (spec) {
specs.add(spec);
}
}
}
function addSnapshotClosure(lockfile) {
const snapshots = lockfile?.snapshots;
const packages = lockfile?.packages;
@@ -76,26 +118,36 @@ function addSnapshotClosure(lockfile) {
continue;
}
visited.add(spec);
const snapshot = snapshots[spec];
const snapshot = snapshotForSpec(lockfile, spec);
if (!snapshot) {
continue;
}
for (const [name, version] of Object.entries(snapshot.dependencies ?? {})) {
const addDependencySpec = (name, version) => {
const depSpec = packageSpec(name, typeof version === "string" ? version : version?.version);
if (!depSpec || !packages[depSpec] || specs.has(depSpec)) {
continue;
if (
!depSpec ||
!packages[depSpec] ||
specs.has(depSpec) ||
!packageSupportsTarget(lockfile, depSpec)
) {
return;
}
specs.add(depSpec);
pending.push(depSpec);
};
for (const [name, version] of Object.entries(snapshot.dependencies ?? {})) {
addDependencySpec(name, version);
}
for (const [name, version] of Object.entries(snapshot.optionalDependencies ?? {})) {
addDependencySpec(name, version);
}
}
}
for (const root of roots) {
visitListNode(root);
}
const lockfile = readLockfile();
for (const root of roots) {
visitListNode(lockfile, root);
}
addSnapshotClosure(lockfile);
addLockfilePackages(lockfile);
process.stdout.write([...specs].toSorted((a, b) => a.localeCompare(b)).join("\n"));

View File

@@ -101,7 +101,66 @@ describe("list-prod-store-packages", () => {
expect(result.stdout).toBe("source-map-support@0.5.21\nsource-map@0.6.1");
});
it("adds lockfile packages missing from pnpm list output", () => {
it("adds target optional dependencies from peer-resolved lockfile snapshots", () => {
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
const platformPackages = [
["darwin", "arm64"],
["darwin", "x64"],
["linux", "arm64"],
["linux", "x64"],
["win32", "arm64"],
["win32", "x64"],
] as const;
writeFileSync(
join(cwd, "pnpm-lock.yaml"),
[
"lockfileVersion: '10.0'",
"",
"packages:",
" native-wrapper@1.0.0:",
" resolution: {integrity: sha512-test}",
...platformPackages.flatMap(([os, cpu]) => [
` native-wrapper-${os}-${cpu}@1.0.0:`,
" resolution: {integrity: sha512-test}",
` cpu: [${cpu}]`,
` os: [${os}]`,
]),
"",
"snapshots:",
" native-wrapper@1.0.0(peer@1.0.0):",
" optionalDependencies:",
...platformPackages.map(([os, cpu]) => ` native-wrapper-${os}-${cpu}: 1.0.0`),
...platformPackages.flatMap(([os, cpu]) => [
` native-wrapper-${os}-${cpu}@1.0.0:`,
" optional: true",
]),
"",
].join("\n"),
);
const result = runListProdStorePackages(
{
dependencies: {
nativeWrapper: {
from: "native-wrapper",
resolved: "https://registry.npmjs.org/native-wrapper/-/native-wrapper-1.0.0.tgz",
version: "1.0.0(peer@1.0.0)",
},
},
},
cwd,
);
expect(result.status).toBe(0);
const expectedPlatformPackage = [`native-wrapper-${process.platform}-${process.arch}@1.0.0`];
const supportedPlatformPackage = ["linux", "darwin", "win32"].includes(process.platform)
? expectedPlatformPackage
: [];
expect(result.stdout.split("\n").filter(Boolean)).toEqual(
["native-wrapper@1.0.0", ...supportedPlatformPackage].toSorted((a, b) => a.localeCompare(b)),
);
});
it("does not add unrelated lockfile packages missing from pnpm list output", () => {
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
writeFileSync(
join(cwd, "pnpm-lock.yaml"),
@@ -119,6 +178,68 @@ describe("list-prod-store-packages", () => {
const result = runListProdStorePackages({ dependencies: {} }, cwd);
expect(result.status).toBe(0);
expect(result.stdout).toBe("recma-jsx@1.0.1");
expect(result.stdout).toBe("");
});
it("only adds optional platform packages matching the current target", () => {
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
const platformPackages = [
["darwin", "arm64"],
["darwin", "x64"],
["linux", "arm64"],
["linux", "x64"],
["win32", "arm64"],
["win32", "x64"],
] as const;
const expectedPlatformPackage = platformPackages
.map(([os, cpu]) => `@zed-industries/codex-acp-${os}-${cpu}@0.15.0`)
.find(
(spec) => spec === `@zed-industries/codex-acp-${process.platform}-${process.arch}@0.15.0`,
);
writeFileSync(
join(cwd, "pnpm-lock.yaml"),
[
"lockfileVersion: '10.0'",
"",
"packages:",
" '@zed-industries/codex-acp@0.15.0':",
" resolution: {integrity: sha512-test}",
...platformPackages.flatMap(([os, cpu]) => [
` '@zed-industries/codex-acp-${os}-${cpu}@0.15.0':`,
" resolution: {integrity: sha512-test}",
` cpu: [${cpu}]`,
` os: [${os}]`,
]),
"",
"snapshots:",
" '@zed-industries/codex-acp@0.15.0':",
" optionalDependencies:",
...platformPackages.map(
([os, cpu]) => ` '@zed-industries/codex-acp-${os}-${cpu}': 0.15.0`,
),
...platformPackages.flatMap(([os, cpu]) => [
` '@zed-industries/codex-acp-${os}-${cpu}@0.15.0':`,
" optional: true",
]),
"",
].join("\n"),
);
const result = runListProdStorePackages(
{
dependencies: {
codexAcp: {
from: "@zed-industries/codex-acp",
resolved: "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.15.0.tgz",
version: "0.15.0",
},
},
},
cwd,
);
expect(result.status).toBe(0);
expect(result.stdout.split("\n").filter(Boolean)).toEqual(
[expectedPlatformPackage, "@zed-industries/codex-acp@0.15.0"].filter(Boolean),
);
});
});