Skip to content

Commit ec9f55e

Browse files
committed
fix docker store seed target packages
1 parent 2c6bdc8 commit ec9f55e

2 files changed

Lines changed: 200 additions & 27 deletions

File tree

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
// Lists production store packages from lockfile data.
1+
// Lists current-target production packages for Docker's offline prune store seed.
22
import fs from "node:fs";
33
import path from "node:path";
44
import { parse } from "yaml";
55

66
const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
77
const roots = Array.isArray(parsed) ? parsed : [parsed];
88
const specs = new Set();
9+
const target = {
10+
cpu: process.arch,
11+
libc: detectLibc(),
12+
os: process.platform,
13+
};
914

1015
function packageSpec(name, version) {
1116
if (!name || !version || typeof version !== "string") {
@@ -22,26 +27,72 @@ function packageSpec(name, version) {
2227
return `${name}@${normalizedVersion}`;
2328
}
2429

25-
function packageSpecFromLockfileKey(key) {
30+
function detectLibc() {
31+
if (process.platform !== "linux") {
32+
return undefined;
33+
}
34+
const report = process.report?.getReport?.();
35+
return report?.header?.glibcVersionRuntime ? "glibc" : "musl";
36+
}
37+
38+
function matchesTargetSelector(selector, value) {
39+
if (!Array.isArray(selector) || !value) {
40+
return true;
41+
}
42+
const blocked = selector.some((entry) => entry === `!${value}`);
43+
if (blocked) {
44+
return false;
45+
}
46+
const allowed = selector.filter((entry) => typeof entry === "string" && !entry.startsWith("!"));
47+
return allowed.length === 0 || allowed.includes(value);
48+
}
49+
50+
function packageEntryForSpec(lockfile, spec) {
51+
return lockfile?.packages?.[spec] ?? lockfile?.packages?.[`/${spec}`];
52+
}
53+
54+
function normalizeLockfilePackageKey(key) {
2655
if (typeof key !== "string") {
2756
return undefined;
2857
}
29-
const normalizedKey = (key.startsWith("/") ? key.slice(1) : key).replace(/\(.+\)$/, "");
30-
const separator = normalizedKey.lastIndexOf("@");
31-
if (separator <= 0) {
58+
return (key.startsWith("/") ? key.slice(1) : key).replace(/\(.+\)$/, "");
59+
}
60+
61+
function snapshotForSpec(lockfile, spec) {
62+
const snapshots = lockfile?.snapshots;
63+
if (!snapshots) {
3264
return undefined;
3365
}
34-
return packageSpec(normalizedKey.slice(0, separator), normalizedKey.slice(separator + 1));
66+
return (
67+
snapshots[spec] ??
68+
snapshots[`/${spec}`] ??
69+
Object.entries(snapshots).find(([key]) => normalizeLockfilePackageKey(key) === spec)?.[1]
70+
);
3571
}
3672

37-
function visitListNode(node) {
73+
function packageSupportsTarget(lockfile, spec) {
74+
const entry = packageEntryForSpec(lockfile, spec);
75+
return (
76+
matchesTargetSelector(entry?.os, target.os) &&
77+
matchesTargetSelector(entry?.cpu, target.cpu) &&
78+
matchesTargetSelector(entry?.libc, target.libc)
79+
);
80+
}
81+
82+
function addSpec(lockfile, spec) {
83+
if (spec && packageSupportsTarget(lockfile, spec)) {
84+
specs.add(spec);
85+
}
86+
}
87+
88+
function visitListNode(lockfile, node) {
3889
for (const dep of Object.values(node.dependencies ?? {})) {
3990
const name = dep.from || dep.name;
4091
const spec = packageSpec(name, dep.version);
4192
if (spec && dep.resolved?.startsWith("https://registry.npmjs.org/")) {
42-
specs.add(spec);
93+
addSpec(lockfile, spec);
4394
}
44-
visitListNode(dep);
95+
visitListNode(lockfile, dep);
4596
}
4697
}
4798

@@ -53,15 +104,6 @@ function readLockfile() {
53104
return parse(fs.readFileSync(lockfilePath, "utf8"));
54105
}
55106

56-
function addLockfilePackages(lockfile) {
57-
for (const key of Object.keys(lockfile?.packages ?? {})) {
58-
const spec = packageSpecFromLockfileKey(key);
59-
if (spec) {
60-
specs.add(spec);
61-
}
62-
}
63-
}
64-
65107
function addSnapshotClosure(lockfile) {
66108
const snapshots = lockfile?.snapshots;
67109
const packages = lockfile?.packages;
@@ -76,26 +118,36 @@ function addSnapshotClosure(lockfile) {
76118
continue;
77119
}
78120
visited.add(spec);
79-
const snapshot = snapshots[spec];
121+
const snapshot = snapshotForSpec(lockfile, spec);
80122
if (!snapshot) {
81123
continue;
82124
}
83-
for (const [name, version] of Object.entries(snapshot.dependencies ?? {})) {
125+
const addDependencySpec = (name, version) => {
84126
const depSpec = packageSpec(name, typeof version === "string" ? version : version?.version);
85-
if (!depSpec || !packages[depSpec] || specs.has(depSpec)) {
86-
continue;
127+
if (
128+
!depSpec ||
129+
!packages[depSpec] ||
130+
specs.has(depSpec) ||
131+
!packageSupportsTarget(lockfile, depSpec)
132+
) {
133+
return;
87134
}
88135
specs.add(depSpec);
89136
pending.push(depSpec);
137+
};
138+
for (const [name, version] of Object.entries(snapshot.dependencies ?? {})) {
139+
addDependencySpec(name, version);
140+
}
141+
for (const [name, version] of Object.entries(snapshot.optionalDependencies ?? {})) {
142+
addDependencySpec(name, version);
90143
}
91144
}
92145
}
93146

147+
const lockfile = readLockfile();
94148
for (const root of roots) {
95-
visitListNode(root);
149+
visitListNode(lockfile, root);
96150
}
97-
const lockfile = readLockfile();
98151
addSnapshotClosure(lockfile);
99-
addLockfilePackages(lockfile);
100152

101153
process.stdout.write([...specs].toSorted((a, b) => a.localeCompare(b)).join("\n"));

test/scripts/list-prod-store-packages.test.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,66 @@ describe("list-prod-store-packages", () => {
101101
expect(result.stdout).toBe("source-map-support@0.5.21\nsource-map@0.6.1");
102102
});
103103

104-
it("adds lockfile packages missing from pnpm list output", () => {
104+
it("adds target optional dependencies from peer-resolved lockfile snapshots", () => {
105+
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
106+
const platformPackages = [
107+
["darwin", "arm64"],
108+
["darwin", "x64"],
109+
["linux", "arm64"],
110+
["linux", "x64"],
111+
["win32", "arm64"],
112+
["win32", "x64"],
113+
] as const;
114+
writeFileSync(
115+
join(cwd, "pnpm-lock.yaml"),
116+
[
117+
"lockfileVersion: '10.0'",
118+
"",
119+
"packages:",
120+
" native-wrapper@1.0.0:",
121+
" resolution: {integrity: sha512-test}",
122+
...platformPackages.flatMap(([os, cpu]) => [
123+
` native-wrapper-${os}-${cpu}@1.0.0:`,
124+
" resolution: {integrity: sha512-test}",
125+
` cpu: [${cpu}]`,
126+
` os: [${os}]`,
127+
]),
128+
"",
129+
"snapshots:",
130+
" native-wrapper@1.0.0(peer@1.0.0):",
131+
" optionalDependencies:",
132+
...platformPackages.map(([os, cpu]) => ` native-wrapper-${os}-${cpu}: 1.0.0`),
133+
...platformPackages.flatMap(([os, cpu]) => [
134+
` native-wrapper-${os}-${cpu}@1.0.0:`,
135+
" optional: true",
136+
]),
137+
"",
138+
].join("\n"),
139+
);
140+
const result = runListProdStorePackages(
141+
{
142+
dependencies: {
143+
nativeWrapper: {
144+
from: "native-wrapper",
145+
resolved: "https://registry.npmjs.org/native-wrapper/-/native-wrapper-1.0.0.tgz",
146+
version: "1.0.0(peer@1.0.0)",
147+
},
148+
},
149+
},
150+
cwd,
151+
);
152+
153+
expect(result.status).toBe(0);
154+
const expectedPlatformPackage = [`native-wrapper-${process.platform}-${process.arch}@1.0.0`];
155+
const supportedPlatformPackage = ["linux", "darwin", "win32"].includes(process.platform)
156+
? expectedPlatformPackage
157+
: [];
158+
expect(result.stdout.split("\n").filter(Boolean)).toEqual(
159+
["native-wrapper@1.0.0", ...supportedPlatformPackage].toSorted((a, b) => a.localeCompare(b)),
160+
);
161+
});
162+
163+
it("does not add unrelated lockfile packages missing from pnpm list output", () => {
105164
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
106165
writeFileSync(
107166
join(cwd, "pnpm-lock.yaml"),
@@ -119,6 +178,68 @@ describe("list-prod-store-packages", () => {
119178
const result = runListProdStorePackages({ dependencies: {} }, cwd);
120179

121180
expect(result.status).toBe(0);
122-
expect(result.stdout).toBe("recma-jsx@1.0.1");
181+
expect(result.stdout).toBe("");
182+
});
183+
184+
it("only adds optional platform packages matching the current target", () => {
185+
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
186+
const platformPackages = [
187+
["darwin", "arm64"],
188+
["darwin", "x64"],
189+
["linux", "arm64"],
190+
["linux", "x64"],
191+
["win32", "arm64"],
192+
["win32", "x64"],
193+
] as const;
194+
const expectedPlatformPackage = platformPackages
195+
.map(([os, cpu]) => `@zed-industries/codex-acp-${os}-${cpu}@0.15.0`)
196+
.find(
197+
(spec) => spec === `@zed-industries/codex-acp-${process.platform}-${process.arch}@0.15.0`,
198+
);
199+
writeFileSync(
200+
join(cwd, "pnpm-lock.yaml"),
201+
[
202+
"lockfileVersion: '10.0'",
203+
"",
204+
"packages:",
205+
" '@zed-industries/codex-acp@0.15.0':",
206+
" resolution: {integrity: sha512-test}",
207+
...platformPackages.flatMap(([os, cpu]) => [
208+
` '@zed-industries/codex-acp-${os}-${cpu}@0.15.0':`,
209+
" resolution: {integrity: sha512-test}",
210+
` cpu: [${cpu}]`,
211+
` os: [${os}]`,
212+
]),
213+
"",
214+
"snapshots:",
215+
" '@zed-industries/codex-acp@0.15.0':",
216+
" optionalDependencies:",
217+
...platformPackages.map(
218+
([os, cpu]) => ` '@zed-industries/codex-acp-${os}-${cpu}': 0.15.0`,
219+
),
220+
...platformPackages.flatMap(([os, cpu]) => [
221+
` '@zed-industries/codex-acp-${os}-${cpu}@0.15.0':`,
222+
" optional: true",
223+
]),
224+
"",
225+
].join("\n"),
226+
);
227+
const result = runListProdStorePackages(
228+
{
229+
dependencies: {
230+
codexAcp: {
231+
from: "@zed-industries/codex-acp",
232+
resolved: "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.15.0.tgz",
233+
version: "0.15.0",
234+
},
235+
},
236+
},
237+
cwd,
238+
);
239+
240+
expect(result.status).toBe(0);
241+
expect(result.stdout.split("\n").filter(Boolean)).toEqual(
242+
[expectedPlatformPackage, "@zed-industries/codex-acp@0.15.0"].filter(Boolean),
243+
);
123244
});
124245
});

0 commit comments

Comments
 (0)