@zackees/soldr 0.8.28 → 0.8.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/package.json +1 -1
- package/scripts/install.js +96 -31
- package/scripts/test-npm-package.js +270 -0
package/README.md
CHANGED
|
@@ -421,30 +421,29 @@ If you also have many separate test binaries, consider consolidating them under
|
|
|
421
421
|
|
|
422
422
|
## Architecture
|
|
423
423
|
|
|
424
|
-
**
|
|
424
|
+
**Internal workspace crates.** Five `publish = false` crates under `crates/`. The 2026-05 monocrate collapse was reversed by the #1490 split; soldr publishes no crates, so workspace membership has no external surface, and `soldr-cli` re-exports the others at their historical paths (`soldr_cli::core`, `soldr_cli::fetch`, …) so existing imports are unchanged.
|
|
425
425
|
|
|
426
426
|
```
|
|
427
427
|
soldr/
|
|
428
428
|
|-- crates/
|
|
429
|
-
|
|
|
430
|
-
|
|
|
431
|
-
|
|
|
432
|
-
|
|
|
433
|
-
|
|
|
434
|
-
| | `-- main.rs + cli/ # Mode detection, dispatch, cargo front door
|
|
435
|
-
| `-- tests/
|
|
429
|
+
| |-- soldr-core/ # Shared types, config, target resolution, wire schema
|
|
430
|
+
| |-- soldr-fetch/ # Binary resolution + download
|
|
431
|
+
| |-- soldr-cache/ # RUSTC_WRAPPER + daemon IPC + archive transport
|
|
432
|
+
| |-- soldr-daemon/ # Daemon runtime + embedded zccache service
|
|
433
|
+
| `-- soldr-cli/ # Facade + the `soldr` binary
|
|
436
434
|
|-- src/soldr/ # Python package (maturin bin bindings)
|
|
437
435
|
`-- tests/
|
|
438
436
|
```
|
|
439
437
|
|
|
440
|
-
|
|
|
438
|
+
| Crate | Role |
|
|
441
439
|
|---|---|
|
|
442
|
-
| `
|
|
443
|
-
| `
|
|
444
|
-
| `
|
|
445
|
-
| `
|
|
440
|
+
| `soldr-core` | Shared types, config (`~/.soldr/config.toml`), target-triple resolution (MSVC default on Windows), the daemon wire schema, error types, the `timed_test!` watchdog. No I/O beyond config files. |
|
|
441
|
+
| `soldr-fetch` | Binary resolution. `known_tools` registry, `trust` (SHA-256 pins + `SOLDR_TRUST_MODE` enforcement), rustup auto-bootstrap, resolution chain (local cache → repo lookup → GitHub Releases → extract). |
|
|
442
|
+
| `soldr-cache` | `RUSTC_WRAPPER` mode: hash inputs (blake3), check `~/.soldr/cache/`, daemon IPC (Unix socket / Windows named pipe), LRU eviction, `soldr save` / `soldr load` archive transport, auto-GC. |
|
|
443
|
+
| `soldr-daemon` | Daemon lifecycle (spawn/displacement/relocation), IPC server, wire codec, and the embedded zccache service. Depends on `soldr-core` + `soldr-cache`. |
|
|
444
|
+
| `soldr-cli` | Mode detection (chameleon dispatch), clap for built-ins, exec for tool fetch, cargo front door (`soldr cargo ...`), and the `[[bin]]` entry point. |
|
|
446
445
|
|
|
447
|
-
|
|
446
|
+
Dependency flow: every crate reaches into `core` for shared types; `fetch` and `cache` each consume `core`; `daemon` consumes `core` + `cache`; `cli` consumes all four. The re-exports are for internal consumers and tests — this is not a supported public Rust library API.
|
|
448
447
|
|
|
449
448
|
## Prior art
|
|
450
449
|
|
package/package.json
CHANGED
package/scripts/install.js
CHANGED
|
@@ -40,49 +40,97 @@ const TARGETS = {
|
|
|
40
40
|
// time based on `target.binary`.
|
|
41
41
|
const BUNDLED_BINARIES = zccacheContract.RELEASE_BUNDLED_BINARIES;
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
43
|
+
// The lowest glibc a host must have before the `-gnu` artifact is worth
|
|
44
|
+
// downloading (soldr#1060).
|
|
45
|
+
//
|
|
46
|
+
// Those artifacts are built natively on ubuntu-24.04, so they currently
|
|
47
|
+
// require GLIBC_2.39 — measured on the published v0.8.29 binaries, x86_64 and
|
|
48
|
+
// aarch64. On Debian 12 (glibc 2.36) the gnu binary dies with
|
|
49
|
+
// "version `GLIBC_2.39' not found" while the musl artifact from the same
|
|
50
|
+
// release runs fine, so "is this host glibc?" is the wrong question. The
|
|
51
|
+
// question is "is this host's glibc new enough for the binary we ship?".
|
|
52
|
+
//
|
|
53
|
+
// Kept in lockstep with the `--max-glibc` ceiling in release-auto.yml by a
|
|
54
|
+
// check in test-npm-package.js. When the release build is fixed to link
|
|
55
|
+
// against a 2.17 baseline that ceiling drops, and this must follow it down.
|
|
56
|
+
const MIN_GLIBC_FOR_GNU = "2.39";
|
|
57
|
+
|
|
58
|
+
function compareVersions(left, right) {
|
|
59
|
+
// Numeric, part by part. A lexical compare would rank "2.9" above "2.39"
|
|
60
|
+
// and wave through a host that cannot run the binary.
|
|
61
|
+
const a = String(left).split(".").map((p) => parseInt(p, 10) || 0);
|
|
62
|
+
const b = String(right).split(".").map((p) => parseInt(p, 10) || 0);
|
|
63
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
64
|
+
const diff = (a[i] || 0) - (b[i] || 0);
|
|
65
|
+
if (diff !== 0) {
|
|
66
|
+
return diff < 0 ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Decide which Linux artifact this host should download. Ordered probes:
|
|
73
|
+
//
|
|
74
|
+
// 1. A musl loader in /lib means the SYSTEM is musl, and that outranks
|
|
75
|
+
// whatever Node was linked against. This has to run first: a glibc Node
|
|
76
|
+
// on alpine (`apk add nodejs-current`) reports a perfectly good glibc
|
|
77
|
+
// version, so checking Node's header first would answer "gnu" and never
|
|
78
|
+
// consult the filesystem at all — which is precisely the case the probe
|
|
79
|
+
// was written for.
|
|
80
|
+
// 2. Node's reported runtime glibc. This is the only source that gives a
|
|
81
|
+
// VERSION, and gnu is chosen only at or above MIN_GLIBC_FOR_GNU.
|
|
82
|
+
// 3. Anything else → musl.
|
|
83
|
+
//
|
|
84
|
+
// musl is the safe end of every unknown because that artifact is verified
|
|
85
|
+
// statically linked before it is ever staged (release-auto.yml → "Verify musl
|
|
86
|
+
// binary is statically linked"), so it has no dynamic loader dependency and
|
|
87
|
+
// runs on glibc hosts too. The mistakes are not symmetric:
|
|
88
|
+
//
|
|
89
|
+
// pick musl, actually glibc → works, nothing to resolve
|
|
90
|
+
// pick gnu, actually musl → hard failure, "soldr: not found"
|
|
91
|
+
// pick gnu, glibc too old → hard failure, "GLIBC_2.39 not found"
|
|
92
|
+
//
|
|
93
|
+
// Only the first is recoverable.
|
|
94
|
+
//
|
|
95
|
+
// `probes` exists so the branches can be tested on any host; the defaults are
|
|
96
|
+
// the real detectors.
|
|
97
|
+
function detectLibc(platform = process.platform, probes = {}) {
|
|
56
98
|
if (platform !== "linux") {
|
|
57
99
|
return null;
|
|
58
100
|
}
|
|
101
|
+
const readHeader =
|
|
102
|
+
probes.readHeader ||
|
|
103
|
+
(() => process.report && process.report.getReport && process.report.getReport().header);
|
|
104
|
+
const listLib = probes.listLib || (() => fs.readdirSync("/lib"));
|
|
105
|
+
|
|
59
106
|
try {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
return "gnu";
|
|
63
|
-
}
|
|
64
|
-
if (header && Object.prototype.hasOwnProperty.call(header, "glibcVersionRuntime")) {
|
|
65
|
-
// Field present but empty / null → Node was built against musl.
|
|
107
|
+
const entries = listLib();
|
|
108
|
+
if (entries.some((name) => /^ld-musl-.+\.so\.1$/.test(name))) {
|
|
66
109
|
return "musl";
|
|
67
110
|
}
|
|
68
111
|
} catch (err) {
|
|
69
|
-
//
|
|
112
|
+
// /lib may not be readable in heavily sandboxed containers; fall through.
|
|
70
113
|
}
|
|
71
114
|
try {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
const header = readHeader();
|
|
116
|
+
const runtime = header && header.glibcVersionRuntime;
|
|
117
|
+
if (
|
|
118
|
+
typeof runtime === "string" &&
|
|
119
|
+
runtime.length > 0 &&
|
|
120
|
+
compareVersions(runtime, MIN_GLIBC_FOR_GNU) >= 0
|
|
121
|
+
) {
|
|
122
|
+
return "gnu";
|
|
75
123
|
}
|
|
76
124
|
} catch (err) {
|
|
77
|
-
//
|
|
125
|
+
// process.report can throw on locked-down environments; fall through.
|
|
78
126
|
}
|
|
79
|
-
return "
|
|
127
|
+
return "musl";
|
|
80
128
|
}
|
|
81
129
|
|
|
82
130
|
function platformTarget(platform = process.platform, arch = process.arch, libc = detectLibc(platform)) {
|
|
83
131
|
const key =
|
|
84
132
|
platform === "linux"
|
|
85
|
-
? `${platform}-${arch}-${libc || "
|
|
133
|
+
? `${platform}-${arch}-${libc || "musl"}`
|
|
86
134
|
: `${platform}-${arch}`;
|
|
87
135
|
const target = TARGETS[key];
|
|
88
136
|
if (!target) {
|
|
@@ -153,6 +201,24 @@ function checksumFor(checksumsText, filename) {
|
|
|
153
201
|
throw new Error(`checksum entry not found for ${filename}`);
|
|
154
202
|
}
|
|
155
203
|
|
|
204
|
+
// The integrity check for everything this package installs
|
|
205
|
+
// (docs/TRUST_BOUNDARIES.md). Extracted from `install()` so it can be
|
|
206
|
+
// tested: while it was inline, `checksumFor` was covered but the comparison
|
|
207
|
+
// itself was not, so deleting the mismatch branch would have disabled
|
|
208
|
+
// verification with every test still green.
|
|
209
|
+
//
|
|
210
|
+
// Throws rather than returning a boolean, because the only correct response
|
|
211
|
+
// to a mismatch is to stop, and a caller that forgot to check a returned
|
|
212
|
+
// false would install the archive anyway.
|
|
213
|
+
function verifyArchiveChecksum(archive, checksumsText, filename) {
|
|
214
|
+
const expected = checksumFor(checksumsText, filename);
|
|
215
|
+
const actual = crypto.createHash("sha256").update(archive).digest("hex");
|
|
216
|
+
if (actual !== expected) {
|
|
217
|
+
throw new Error(`checksum mismatch for ${filename}: expected ${expected}, got ${actual}`);
|
|
218
|
+
}
|
|
219
|
+
return actual;
|
|
220
|
+
}
|
|
221
|
+
|
|
156
222
|
function run(command, args, options = {}) {
|
|
157
223
|
const result = childProcess.spawnSync(command, args, {
|
|
158
224
|
stdio: "inherit",
|
|
@@ -232,11 +298,7 @@ async function install() {
|
|
|
232
298
|
download(checksumUrl).then((buffer) => buffer.toString("utf8")),
|
|
233
299
|
]);
|
|
234
300
|
|
|
235
|
-
|
|
236
|
-
const actual = crypto.createHash("sha256").update(archive).digest("hex");
|
|
237
|
-
if (actual !== expected) {
|
|
238
|
-
throw new Error(`checksum mismatch for ${filename}: expected ${expected}, got ${actual}`);
|
|
239
|
-
}
|
|
301
|
+
verifyArchiveChecksum(archive, checksums, filename);
|
|
240
302
|
|
|
241
303
|
const archivePath = path.join(tmp, filename);
|
|
242
304
|
const extractDir = path.join(tmp, "extract");
|
|
@@ -312,9 +374,12 @@ if (require.main === module) {
|
|
|
312
374
|
|
|
313
375
|
module.exports = {
|
|
314
376
|
ARCHIVE_EXT,
|
|
377
|
+
MIN_GLIBC_FOR_GNU,
|
|
378
|
+
compareVersions,
|
|
315
379
|
BUNDLED_BINARIES,
|
|
316
380
|
TARGETS,
|
|
317
381
|
checksumFor,
|
|
382
|
+
verifyArchiveChecksum,
|
|
318
383
|
detectLibc,
|
|
319
384
|
platformTarget,
|
|
320
385
|
releaseBaseUrl,
|
|
@@ -100,6 +100,12 @@ assert.strictEqual(
|
|
|
100
100
|
// Default (no libc arg) falls back to detectLibc; on most CI hosts that's
|
|
101
101
|
// gnu. We don't assert the triple here — just that the call resolves.
|
|
102
102
|
assert.ok(install.platformTarget("linux", "x64").triple.startsWith("x86_64-unknown-linux-"));
|
|
103
|
+
// An explicitly-unknown libc must take the runs-anywhere build, matching
|
|
104
|
+
// detectLibc's own unknown case rather than contradicting it.
|
|
105
|
+
assert.strictEqual(
|
|
106
|
+
install.platformTarget("linux", "x64", null).triple,
|
|
107
|
+
"x86_64-unknown-linux-musl",
|
|
108
|
+
);
|
|
103
109
|
assert.strictEqual(install.platformTarget("darwin", "x64").triple, "x86_64-apple-darwin");
|
|
104
110
|
assert.strictEqual(install.platformTarget("darwin", "arm64").triple, "aarch64-apple-darwin");
|
|
105
111
|
assert.strictEqual(install.platformTarget("win32", "x64").triple, "x86_64-pc-windows-msvc");
|
|
@@ -115,6 +121,193 @@ assert.strictEqual(install.detectLibc("win32"), null);
|
|
|
115
121
|
const linuxLibc = install.detectLibc("linux");
|
|
116
122
|
assert.ok(linuxLibc === "gnu" || linuxLibc === "musl", `unexpected libc: ${linuxLibc}`);
|
|
117
123
|
|
|
124
|
+
// detectLibc's branches, driven through the probe seam so they are
|
|
125
|
+
// exercised on every host rather than only on whichever libc CI runs.
|
|
126
|
+
const throwingProbe = () => {
|
|
127
|
+
throw new Error("probe unavailable");
|
|
128
|
+
};
|
|
129
|
+
const NEW_ENOUGH = install.MIN_GLIBC_FOR_GNU;
|
|
130
|
+
|
|
131
|
+
// Numeric version comparison. A lexical compare ranks "2.9" above "2.39"
|
|
132
|
+
// and would wave through a host that cannot run the binary.
|
|
133
|
+
assert.strictEqual(install.compareVersions("2.9", "2.39"), -1);
|
|
134
|
+
assert.strictEqual(install.compareVersions("2.39", "2.9"), 1);
|
|
135
|
+
assert.strictEqual(install.compareVersions("2.39", "2.39"), 0);
|
|
136
|
+
assert.strictEqual(install.compareVersions("2.40", "2.39"), 1);
|
|
137
|
+
assert.strictEqual(install.compareVersions("2.2.5", "2.14"), -1);
|
|
138
|
+
|
|
139
|
+
// 1. A glibc new enough for the shipped artifact takes the gnu build.
|
|
140
|
+
assert.strictEqual(
|
|
141
|
+
install.detectLibc("linux", {
|
|
142
|
+
readHeader: () => ({ glibcVersionRuntime: NEW_ENOUGH }),
|
|
143
|
+
listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
|
|
144
|
+
}),
|
|
145
|
+
"gnu",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// 2. A glibc that is too old must NOT take the gnu build. This is the live
|
|
149
|
+
// bug: Debian 12 reports 2.36, the published gnu binary requires
|
|
150
|
+
// GLIBC_2.39, and it dies with "version `GLIBC_2.39' not found" while the
|
|
151
|
+
// musl artifact from the same release runs fine.
|
|
152
|
+
assert.strictEqual(
|
|
153
|
+
install.detectLibc("linux", {
|
|
154
|
+
readHeader: () => ({ glibcVersionRuntime: "2.36" }),
|
|
155
|
+
listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
|
|
156
|
+
}),
|
|
157
|
+
"musl",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// 3. A musl SYSTEM wins over whatever Node was linked against. Node built
|
|
161
|
+
// against glibc on alpine (`apk add nodejs-current`) reports a perfectly
|
|
162
|
+
// good glibc version, so the filesystem probe has to be consulted FIRST --
|
|
163
|
+
// otherwise this answers "gnu" and never looks at /lib at all.
|
|
164
|
+
assert.strictEqual(
|
|
165
|
+
install.detectLibc("linux", {
|
|
166
|
+
readHeader: () => ({ glibcVersionRuntime: NEW_ENOUGH }),
|
|
167
|
+
listLib: () => ["ld-musl-x86_64.so.1"],
|
|
168
|
+
}),
|
|
169
|
+
"musl",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// 4. Real musl Node omits the property entirely (verified on node:22-alpine:
|
|
173
|
+
// hasOwnProperty is false, not present-and-empty).
|
|
174
|
+
assert.strictEqual(
|
|
175
|
+
install.detectLibc("linux", {
|
|
176
|
+
readHeader: () => ({}),
|
|
177
|
+
listLib: () => ["ld-musl-x86_64.so.1"],
|
|
178
|
+
}),
|
|
179
|
+
"musl",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// 5. Both probes unavailable (heavily sandboxed container) -> musl, the only
|
|
183
|
+
// artifact that runs without knowing anything about the host.
|
|
184
|
+
assert.strictEqual(
|
|
185
|
+
install.detectLibc("linux", { readHeader: throwingProbe, listLib: throwingProbe }),
|
|
186
|
+
"musl",
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// 6. A glibc host whose version cannot be read is also unknown: /lib says
|
|
190
|
+
// glibc but carries no version, so the floor cannot be confirmed.
|
|
191
|
+
assert.strictEqual(
|
|
192
|
+
install.detectLibc("linux", {
|
|
193
|
+
readHeader: throwingProbe,
|
|
194
|
+
listLib: () => ["ld-linux-x86-64.so.2", "libc.so.6"],
|
|
195
|
+
}),
|
|
196
|
+
"musl",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// The probe seam must not leak to other platforms: a non-Linux host still
|
|
200
|
+
// short-circuits to null before any probe runs.
|
|
201
|
+
assert.strictEqual(
|
|
202
|
+
install.detectLibc("darwin", { readHeader: throwingProbe, listLib: throwingProbe }),
|
|
203
|
+
null,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// MIN_GLIBC_FOR_GNU must track the ceiling release-auto.yml enforces on the
|
|
207
|
+
// gnu BINARIES. If the release build is fixed to link a 2.17 baseline and that
|
|
208
|
+
// ceiling drops, the installer must follow it down -- otherwise every glibc
|
|
209
|
+
// host below 2.39 keeps being sent to musl long after gnu would work.
|
|
210
|
+
//
|
|
211
|
+
// Anchored on the script name rather than on any `--max-glibc`. release-auto
|
|
212
|
+
// now passes that flag twice: 2.39 to verify_glibc_baseline.py for the
|
|
213
|
+
// binaries, and 2.17 to verify_wheel_glibc.py for the wheel contents. A bare
|
|
214
|
+
// match takes whichever appears first, so this was correct only by the order
|
|
215
|
+
// the steps happen to sit in. Reorder them and the lockstep would demand the
|
|
216
|
+
// installer drop to 2.17, routing glibc 2.17-2.38 hosts to a binary that needs
|
|
217
|
+
// 2.39 -- the bug #2081 fixed, reintroduced by its own guard.
|
|
218
|
+
function glibcCeilingsFor(workflowText, scriptName) {
|
|
219
|
+
const found = [];
|
|
220
|
+
const lines = workflowText.split(/\r?\n/);
|
|
221
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
222
|
+
if (!lines[i].includes(scriptName)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
// The invocation may be split across continuation lines, so look at the
|
|
226
|
+
// matching line and the few that follow it.
|
|
227
|
+
const window = lines.slice(i, i + 4).join("\n");
|
|
228
|
+
const match = window.match(/--max-glibc\s+([0-9][0-9.]*)/);
|
|
229
|
+
if (match) {
|
|
230
|
+
found.push(match[1]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return found;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function glibcCeilingFor(workflowText, scriptName) {
|
|
237
|
+
const found = glibcCeilingsFor(workflowText, scriptName);
|
|
238
|
+
if (found.length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
// release-auto invokes verify_glibc_baseline.py twice -- once pre-staging on
|
|
242
|
+
// the built binary, once post-staging across the whole bundle. Returning the
|
|
243
|
+
// first would let the two drift apart with the installer silently following
|
|
244
|
+
// only one of them, which is the same order-dependence this function was
|
|
245
|
+
// written to remove.
|
|
246
|
+
const distinct = [...new Set(found)];
|
|
247
|
+
assert.strictEqual(
|
|
248
|
+
distinct.length,
|
|
249
|
+
1,
|
|
250
|
+
`release-auto.yml passes conflicting --max-glibc values to ${scriptName}: ` +
|
|
251
|
+
`${found.join(", ")}. They gate the same artifacts and must agree.`,
|
|
252
|
+
);
|
|
253
|
+
return distinct[0];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Pin the anchoring itself: with the wheel invocation first, the binary
|
|
257
|
+
// ceiling must still resolve to the binary ceiling.
|
|
258
|
+
{
|
|
259
|
+
const reordered = [
|
|
260
|
+
"python3 .github/scripts/verify_wheel_glibc.py --max-glibc 2.17 dist/*.whl",
|
|
261
|
+
"python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 soldr",
|
|
262
|
+
].join("\n");
|
|
263
|
+
assert.strictEqual(
|
|
264
|
+
glibcCeilingFor(reordered, "verify_glibc_baseline.py"),
|
|
265
|
+
"2.39",
|
|
266
|
+
"the binary ceiling must not be confused with the wheel ceiling",
|
|
267
|
+
);
|
|
268
|
+
assert.strictEqual(
|
|
269
|
+
glibcCeilingFor(reordered, "verify_wheel_glibc.py"),
|
|
270
|
+
"2.17",
|
|
271
|
+
"the wheel ceiling must resolve independently",
|
|
272
|
+
);
|
|
273
|
+
assert.strictEqual(glibcCeilingFor(reordered, "not_a_script.py"), null);
|
|
274
|
+
|
|
275
|
+
// Two invocations of the same script that agree resolve to that value...
|
|
276
|
+
const agreeing = [
|
|
277
|
+
"python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 soldr",
|
|
278
|
+
"python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 bundle",
|
|
279
|
+
].join("\n");
|
|
280
|
+
assert.strictEqual(glibcCeilingFor(agreeing, "verify_glibc_baseline.py"), "2.39");
|
|
281
|
+
|
|
282
|
+
// ...and two that disagree are a hard error rather than a silent pick.
|
|
283
|
+
const conflicting = [
|
|
284
|
+
"python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.17 soldr",
|
|
285
|
+
"python3 .github/scripts/verify_glibc_baseline.py --max-glibc 2.39 bundle",
|
|
286
|
+
].join("\n");
|
|
287
|
+
assert.throws(
|
|
288
|
+
() => glibcCeilingFor(conflicting, "verify_glibc_baseline.py"),
|
|
289
|
+
/conflicting --max-glibc/,
|
|
290
|
+
"two ceilings for the same script must not be silently reconciled",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const releaseWorkflow = fs.readFileSync(
|
|
295
|
+
path.join(root, ".github", "workflows", "release-auto.yml"),
|
|
296
|
+
"utf8",
|
|
297
|
+
);
|
|
298
|
+
const binaryCeiling = glibcCeilingFor(releaseWorkflow, "verify_glibc_baseline.py");
|
|
299
|
+
assert(
|
|
300
|
+
binaryCeiling,
|
|
301
|
+
"release-auto.yml must pass --max-glibc to verify_glibc_baseline.py",
|
|
302
|
+
);
|
|
303
|
+
assert.strictEqual(
|
|
304
|
+
install.MIN_GLIBC_FOR_GNU,
|
|
305
|
+
binaryCeiling,
|
|
306
|
+
`install.js MIN_GLIBC_FOR_GNU (${install.MIN_GLIBC_FOR_GNU}) must match the ` +
|
|
307
|
+
`--max-glibc ceiling verify_glibc_baseline.py enforces in release-auto.yml ` +
|
|
308
|
+
`(${binaryCeiling})`,
|
|
309
|
+
);
|
|
310
|
+
|
|
118
311
|
assert.strictEqual(
|
|
119
312
|
install.checksumFor(
|
|
120
313
|
"abc123 soldr-v0.7.29-x86_64-unknown-linux-gnu.tar.zst\n",
|
|
@@ -123,6 +316,83 @@ assert.strictEqual(
|
|
|
123
316
|
"abc123",
|
|
124
317
|
);
|
|
125
318
|
|
|
319
|
+
// The integrity check for everything this package installs
|
|
320
|
+
// (docs/TRUST_BOUNDARIES.md). `checksumFor` was covered but the comparison
|
|
321
|
+
// that uses it was inline in install() and untested, so deleting the
|
|
322
|
+
// mismatch branch would have disabled verification with every test green.
|
|
323
|
+
const crypto = require("crypto");
|
|
324
|
+
const ARCHIVE_BYTES = Buffer.from("pretend this is a tar.zst");
|
|
325
|
+
const ARCHIVE_NAME = "soldr-v9.9.9-x86_64-unknown-linux-musl.tar.zst";
|
|
326
|
+
const ARCHIVE_SHA = crypto.createHash("sha256").update(ARCHIVE_BYTES).digest("hex");
|
|
327
|
+
const SUMS = `${ARCHIVE_SHA} ${ARCHIVE_NAME}
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
// A matching digest returns it rather than throwing.
|
|
331
|
+
assert.strictEqual(
|
|
332
|
+
install.verifyArchiveChecksum(ARCHIVE_BYTES, SUMS, ARCHIVE_NAME),
|
|
333
|
+
ARCHIVE_SHA,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// A tampered archive must throw. This is the case that had no coverage.
|
|
337
|
+
assert.throws(
|
|
338
|
+
() =>
|
|
339
|
+
install.verifyArchiveChecksum(
|
|
340
|
+
Buffer.concat([ARCHIVE_BYTES, Buffer.from("tampered")]),
|
|
341
|
+
SUMS,
|
|
342
|
+
ARCHIVE_NAME,
|
|
343
|
+
),
|
|
344
|
+
/checksum mismatch/,
|
|
345
|
+
"a tampered archive must be rejected",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// A digest for a DIFFERENT file must not be accepted for this one: the
|
|
349
|
+
// lookup is by exact filename, so a sums file listing only some other
|
|
350
|
+
// artifact fails closed rather than matching the first line it sees.
|
|
351
|
+
assert.throws(
|
|
352
|
+
() =>
|
|
353
|
+
install.verifyArchiveChecksum(
|
|
354
|
+
ARCHIVE_BYTES,
|
|
355
|
+
`${ARCHIVE_SHA} some-other-artifact.tar.zst
|
|
356
|
+
`,
|
|
357
|
+
ARCHIVE_NAME,
|
|
358
|
+
),
|
|
359
|
+
/checksum entry not found/,
|
|
360
|
+
"a sums file without this artifact must be rejected",
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// An empty sums file is not a free pass.
|
|
364
|
+
assert.throws(
|
|
365
|
+
() => install.verifyArchiveChecksum(ARCHIVE_BYTES, "", ARCHIVE_NAME),
|
|
366
|
+
/checksum entry not found/,
|
|
367
|
+
"an empty checksums file must be rejected",
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Real SHA256SUMS.txt files use two spaces and may carry CRLF; both must
|
|
371
|
+
// parse. Verified against the published v0.8.29 file, which is
|
|
372
|
+
// `<hash> <name>` with 16 entries covering every release asset.
|
|
373
|
+
assert.strictEqual(
|
|
374
|
+
install.verifyArchiveChecksum(
|
|
375
|
+
ARCHIVE_BYTES,
|
|
376
|
+
`deadbeef unrelated.whl
|
|
377
|
+
${ARCHIVE_SHA} ${ARCHIVE_NAME}
|
|
378
|
+
`,
|
|
379
|
+
ARCHIVE_NAME,
|
|
380
|
+
),
|
|
381
|
+
ARCHIVE_SHA,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Digests are compared case-insensitively on the manifest side: an
|
|
385
|
+
// uppercase entry names the same bytes.
|
|
386
|
+
assert.strictEqual(
|
|
387
|
+
install.verifyArchiveChecksum(
|
|
388
|
+
ARCHIVE_BYTES,
|
|
389
|
+
`${ARCHIVE_SHA.toUpperCase()} ${ARCHIVE_NAME}
|
|
390
|
+
`,
|
|
391
|
+
ARCHIVE_NAME,
|
|
392
|
+
),
|
|
393
|
+
ARCHIVE_SHA,
|
|
394
|
+
);
|
|
395
|
+
|
|
126
396
|
assert.strictEqual(install.ARCHIVE_EXT, "tar.zst");
|
|
127
397
|
assert.deepStrictEqual(
|
|
128
398
|
install.BUNDLED_BINARIES,
|