@rightkit/release 0.6.26 → 0.6.27
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/build-helper-trace.mjs +86 -3
- package/build-helpers-command.mjs +117 -51
- package/package.json +1 -1
- package/rightkit-versions.json +1 -1
package/build-helper-trace.mjs
CHANGED
|
@@ -23,8 +23,9 @@ const CRATES_IO = new Set(["registry+https://github.com/rust-lang/crates.io-inde
|
|
|
23
23
|
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
24
24
|
|
|
25
25
|
/** The unit key: sha256 of the canonical {crate, version, checksum, features (sorted), target, toolchain}. */
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// No toolchain in the key (Adrian, 2026-11-03, rightkit#1473: "why would a rust upgrade void it"): a Rust upgrade never voids a trace list.
|
|
27
|
+
export function buildHelperKey({ crate, version, checksum, features = [], target }) {
|
|
28
|
+
return sha256(JSON.stringify({ crate, version, checksum, features: [...features].sort(), target }));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** Checksums of crates.io packages from a Cargo.lock text: Map "name version" -> checksum. */
|
|
@@ -93,8 +94,16 @@ export function recordTrace(file, unit, verdict, { now = () => new Date().toISOS
|
|
|
93
94
|
*/
|
|
94
95
|
export function checkBuildHelpers(units, list) {
|
|
95
96
|
const untraced = []; const failed = [];
|
|
97
|
+
// Entries recorded under the older key (which held the toolchain) are re-keyed from their own fields; a pass wins over a fail.
|
|
98
|
+
const byKey = new Map();
|
|
99
|
+
for (const entry of Object.values(list?.units ?? {})) {
|
|
100
|
+
if (!entry || typeof entry !== "object") continue;
|
|
101
|
+
const key = buildHelperKey(entry);
|
|
102
|
+
const had = byKey.get(key);
|
|
103
|
+
if (!had || (had.verdict !== "pass" && entry.verdict === "pass") || !TRUSTED_TRACERS.includes(had.tracer)) byKey.set(key, entry);
|
|
104
|
+
}
|
|
96
105
|
for (const u of units) {
|
|
97
|
-
const entry =
|
|
106
|
+
const entry = byKey.get(u.key);
|
|
98
107
|
if (!entry || !u.checksum || !TRUSTED_TRACERS.includes(entry.tracer)) untraced.push(u);
|
|
99
108
|
else if (entry.verdict !== "pass") failed.push({ unit: u, outside: entry.outside ?? [] });
|
|
100
109
|
}
|
|
@@ -262,3 +271,77 @@ export function readStraceFiles(prefix, { fsApi = fs } = {}) {
|
|
|
262
271
|
const dir = path.dirname(prefix); const base = path.basename(prefix);
|
|
263
272
|
return fsApi.readdirSync(dir).filter((n) => n.startsWith(`${base}.`)).map((n) => fsApi.readFileSync(path.join(dir, n), "utf8")).join("\n");
|
|
264
273
|
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
/** Calls `fn(record)` for every record of `file` separated by `sep`, reading 8 MiB at a time (a whole-run trace can pass Node's string limit). */
|
|
277
|
+
export function forEachRecord(file, sep, fn, { fsApi = fs } = {}) {
|
|
278
|
+
const fd = fsApi.openSync(file, "r"); const buf = Buffer.alloc(8 << 20); let rest = "";
|
|
279
|
+
try {
|
|
280
|
+
for (let n = fsApi.readSync(fd, buf, 0, buf.length, null); n > 0; n = fsApi.readSync(fd, buf, 0, buf.length, null)) {
|
|
281
|
+
const parts = (rest + buf.toString("utf8", 0, n)).split(sep);
|
|
282
|
+
rest = parts.pop();
|
|
283
|
+
for (const part of parts) fn(part);
|
|
284
|
+
}
|
|
285
|
+
if (rest) fn(rest);
|
|
286
|
+
} finally { fsApi.closeSync(fd); }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The root (one of `roots`) a pid descends from, through `parent`, or null. */
|
|
290
|
+
function rootFinder(parent, roots) {
|
|
291
|
+
const memo = new Map();
|
|
292
|
+
return (pid) => {
|
|
293
|
+
if (!Number.isInteger(pid)) return null;
|
|
294
|
+
if (memo.has(pid)) return memo.get(pid);
|
|
295
|
+
let found = null;
|
|
296
|
+
for (let p = pid, hops = 0; Number.isInteger(p) && hops < 64; p = parent.get(p), hops += 1) if (roots.has(p)) { found = p; break; }
|
|
297
|
+
memo.set(pid, found);
|
|
298
|
+
return found;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** eslogger JSON lines from `file`, streamed: Map(root pid -> written paths) for every pid in `roots` (a Set), in two passes. */
|
|
303
|
+
export function esloggerWritesByRoot(file, roots) {
|
|
304
|
+
const parent = new Map();
|
|
305
|
+
const parse = (line) => { if (!line.trim().startsWith("{")) return null; try { return JSON.parse(line); } catch { return null; } };
|
|
306
|
+
forEachRecord(file, "\n", (line) => {
|
|
307
|
+
const e = parse(line); if (!e) return;
|
|
308
|
+
const pid = e?.process?.audit_token?.pid; const ppid = e?.process?.ppid ?? e?.process?.original_ppid;
|
|
309
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid) && !parent.has(pid)) parent.set(pid, ppid);
|
|
310
|
+
const child = e?.event?.fork?.child?.audit_token?.pid;
|
|
311
|
+
if (Number.isInteger(child) && Number.isInteger(pid)) parent.set(child, pid);
|
|
312
|
+
});
|
|
313
|
+
const rootOf = rootFinder(parent, roots); const out = new Map([...roots].map((r) => [r, new Set()]));
|
|
314
|
+
forEachRecord(file, "\n", (line) => {
|
|
315
|
+
const e = parse(line); if (!e) return;
|
|
316
|
+
const root = rootOf(e?.process?.audit_token?.pid); if (root === null) return;
|
|
317
|
+
for (const w of parseEsloggerWrites(line, { rootPid: e.process.audit_token.pid })) out.get(root).add(w);
|
|
318
|
+
});
|
|
319
|
+
return new Map([...out].map(([r, s]) => [r, [...s]]));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** tracerpt XML from `file`, streamed by `</Event>`: Map(root pid -> written paths) for every pid in `roots`, kernel paths mapped by `devices`. */
|
|
323
|
+
export function etwWritesByRoot(file, roots, devices = {}) {
|
|
324
|
+
const field = (ev, name) => new RegExp(`<Data Name="${name}">([^<]*)</Data>`).exec(ev)?.[1];
|
|
325
|
+
const id = (ev) => Number(/<EventID>(\d+)<\/EventID>/.exec(ev)?.[1]);
|
|
326
|
+
const pidOf = (ev) => Number(/ProcessID="(\d+)"/.exec(ev)?.[1]);
|
|
327
|
+
const provider = (ev) => /Provider Name="([^"]+)"/.exec(ev)?.[1] ?? "";
|
|
328
|
+
const parent = new Map();
|
|
329
|
+
forEachRecord(file, "</Event>", (ev) => {
|
|
330
|
+
if (provider(ev) === "Microsoft-Windows-Kernel-Process" && id(ev) === 1) parent.set(Number(field(ev, "ProcessID")), Number(field(ev, "ParentProcessID")));
|
|
331
|
+
});
|
|
332
|
+
const rootOf = rootFinder(parent, roots); const out = new Map([...roots].map((r) => [r, new Set()]));
|
|
333
|
+
const toDos = (p) => { if (!p) return null; for (const [dev, letter] of Object.entries(devices)) if (p.toLowerCase().startsWith(dev.toLowerCase())) return letter + p.slice(dev.length); return p; };
|
|
334
|
+
const names = new Map();
|
|
335
|
+
forEachRecord(file, "</Event>", (ev) => {
|
|
336
|
+
if (provider(ev) !== "Microsoft-Windows-Kernel-File") return;
|
|
337
|
+
const n = id(ev); const fileObject = field(ev, "FileObject");
|
|
338
|
+
if (n === 12) names.set(fileObject, field(ev, "FileName"));
|
|
339
|
+
const root = rootOf(pidOf(ev)); if (root === null) return;
|
|
340
|
+
const add = (p) => { const d = toDos(p); if (d) out.get(root).add(d); };
|
|
341
|
+
if (n === 12) { const disposition = (Number(field(ev, "CreateOptions") ?? 0) >>> 24) & 0xff; if (disposition !== 1) add(field(ev, "FileName")); }
|
|
342
|
+
else if (n === 30) add(field(ev, "FileName"));
|
|
343
|
+
else if (n === 16) add(names.get(fileObject));
|
|
344
|
+
else if (n === 26 || n === 27) add(field(ev, "FilePath") ?? field(ev, "FileName"));
|
|
345
|
+
});
|
|
346
|
+
return new Map([...out].map(([r, s]) => [r, [...s]]));
|
|
347
|
+
}
|
|
@@ -13,7 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { buildHelperRefusal, buildHelperUnits, checkBuildHelpers, hostListPath,
|
|
16
|
+
import { buildHelperRefusal, buildHelperUnits, checkBuildHelpers, hostListPath, esloggerWritesByRoot, etwWritesByRoot, judgeTrace, mergeLists, parseStraceWrites, readStraceFiles, readHostList, recordTrace, repoListPath } from "./build-helper-trace.mjs";
|
|
17
17
|
import { resolveManagedRoots } from "./managed-roots.mjs";
|
|
18
18
|
import { directCargoPath } from "./managed-cargo.mjs";
|
|
19
19
|
|
|
@@ -27,6 +27,22 @@ function hostFacts(run = spawnSync) {
|
|
|
27
27
|
return { target: host, toolchain: `rustc ${release}${commit ? ` (${commit.slice(0, 9)})` : ""}` };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Runs the trace builds, `argv[3]` at a time, starting the next whenever one finishes: argv[1] = jobs JSON [{cmd, args, env}], argv[2] = results
|
|
31
|
+
// file [{pid, status, error}] in job order.
|
|
32
|
+
const BATCH_RUNNER = `const fs = require("node:fs"); const { spawn } = require("node:child_process");
|
|
33
|
+
const jobs = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const limit = Math.max(1, Number(process.argv[3]) || 4);
|
|
34
|
+
const results = new Array(jobs.length); let next = 0; let done = 0;
|
|
35
|
+
const start = (i) => new Promise((resolve) => {
|
|
36
|
+
const j = jobs[i]; let child;
|
|
37
|
+
try { child = spawn(j.cmd, j.args, { env: j.env, stdio: ["ignore", "ignore", "pipe"] }); } catch (e) { results[i] = { status: -1, error: e.message }; resolve(); return; }
|
|
38
|
+
let tail = ""; child.stderr.on("data", (d) => { tail = (tail + d).slice(-2000); });
|
|
39
|
+
const finish = (r) => { if (results[i]) return; results[i] = r; done += 1; process.stdout.write(" [" + done + "/" + jobs.length + "] " + j.label + (r.status === 0 ? " built" : " failed") + "\\n"); resolve(); };
|
|
40
|
+
child.on("error", (e) => finish({ pid: child.pid, status: -1, error: e.message }));
|
|
41
|
+
child.on("close", (status) => finish({ pid: child.pid, status, ...(status ? { error: tail.trim().split("\\n").slice(-2).join(" | ") } : {}) }));
|
|
42
|
+
});
|
|
43
|
+
const worker = async () => { while (next < jobs.length) { const i = next++; await start(i); } };
|
|
44
|
+
Promise.all(Array.from({ length: Math.min(limit, jobs.length) }, worker)).then(() => fs.writeFileSync(process.argv[2], JSON.stringify(results)));`;
|
|
45
|
+
|
|
30
46
|
export function inventory({ manifestPath, run = spawnSync, readFile = (f) => fs.readFileSync(f, "utf8") }) {
|
|
31
47
|
const args = ["metadata", "--format-version", "1", "--locked", ...(manifestPath ? ["--manifest-path", manifestPath] : [])];
|
|
32
48
|
const r = run("cargo", args, { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
@@ -72,12 +88,15 @@ function traceAll(argv, { env, platform, log, err }) {
|
|
|
72
88
|
try {
|
|
73
89
|
for (const ws of workspaces) {
|
|
74
90
|
log(`--- ${ws}`);
|
|
75
|
-
const
|
|
91
|
+
const jobsAt = argv.indexOf("--jobs");
|
|
92
|
+
// The host list is what Class A reads on this host; a repository's committed list is written only on request (--record-repo), so
|
|
93
|
+
// tracing never edits a checkout another owner holds.
|
|
94
|
+
const code = main(["trace", "--manifest-path", path.join(ws, "Cargo.toml"), ...(argv.includes("--record-repo") ? ["--record-repo"] : []), ...(jobsAt >= 0 ? ["--jobs", argv[jobsAt + 1]] : [])], { env, platform, log, err });
|
|
76
95
|
if (code !== 0) err(`right-release build-helpers trace-all: ${ws} ended with ${code}`);
|
|
77
96
|
worst = Math.max(worst, code);
|
|
78
97
|
}
|
|
79
98
|
} finally { try { keepAlive?.kill(); } catch { /* already gone */ } }
|
|
80
|
-
log(`right-release build-helpers trace-all: done; commit each workspace's .rightkit/build-helper-trace.json`);
|
|
99
|
+
log(`right-release build-helpers trace-all: done${argv.includes("--record-repo") ? "; commit each workspace's .rightkit/build-helper-trace.json" : ""}`);
|
|
81
100
|
return worst;
|
|
82
101
|
}
|
|
83
102
|
|
|
@@ -89,7 +108,10 @@ export function main(argv = process.argv.slice(2), { env = process.env, platform
|
|
|
89
108
|
const directPath = directCargoPath({ env: { ...env, PATH: env[pathKey] }, platform, gateWrappers: true }).path;
|
|
90
109
|
const scratchTarget = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bh-target-"));
|
|
91
110
|
const run = runRaw ?? ((cmd, args, opts = {}) => spawnSync(cmd, args, { ...opts, env: { ...env, ...(opts.env ?? {}), [pathKey]: directPath, CARGO_TARGET_DIR: opts.env?.CARGO_TARGET_DIR ?? scratchTarget } }));
|
|
92
|
-
|
|
111
|
+
// The parallel trace builds are spawned by the batch runner with the job's own env: that env carries the direct PATH too, so no build
|
|
112
|
+
// ever goes through the RustRight shim (which refuses a build that redirects its output folder).
|
|
113
|
+
const directEnv = { ...env, [pathKey]: directPath };
|
|
114
|
+
try { return mainWith(argv, { env: directEnv, platform, run, log, err }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function mainWith(argv, { env, platform, run, log, err }) {
|
|
@@ -126,57 +148,101 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
126
148
|
}
|
|
127
149
|
const recordRepo = rest.includes("--record-repo");
|
|
128
150
|
const cargoHome = env.CARGO_HOME ?? path.join(os.homedir(), ".cargo");
|
|
129
|
-
|
|
151
|
+
// Parallel: `--jobs N` builds at once (default 4). Each build is its own process tree, rooted at its own cargo, in its own scratch
|
|
152
|
+
// target, and is judged only on the writes of that tree (eslogger and ETW events name their process; strace -ff writes one file per
|
|
153
|
+
// process of its own run). The cores are shared out so the builds do not fight over them.
|
|
154
|
+
const jobsAt = rest.indexOf("--jobs");
|
|
155
|
+
const jobs = Math.max(1, Number.parseInt(jobsAt >= 0 ? rest[jobsAt + 1] : "4", 10) || 4);
|
|
156
|
+
const perBuildJobs = String(Math.max(1, Math.floor(os.cpus().length / jobs)));
|
|
157
|
+
const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)];
|
|
158
|
+
log(`right-release build-helpers trace: ${pending.length} unit(s), ${jobs} at a time`);
|
|
159
|
+
if (!pending.length) return 0;
|
|
160
|
+
const prepared = pending.map((unit) => {
|
|
130
161
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-"));
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// --record-repo also writes the verdict into the repository's committed list, for review and commit by the repository's owner.
|
|
173
|
-
if (recordRepo) recordTrace(repoFile, unit, judged);
|
|
174
|
-
log(`${judged.ok ? "pass" : "FAIL"} ${unit.crate} ${unit.version}${judged.ok ? "" : `: wrote ${judged.outside.slice(0, 5).join(", ")}`}`);
|
|
175
|
-
fs.rmSync(scratch, { recursive: true, force: true });
|
|
162
|
+
const target = path.join(scratch, "target");
|
|
163
|
+
// The unit is built on its own: a throwaway probe package in the scratch folder depends on exactly this version with exactly its
|
|
164
|
+
// resolved features (`cargo build -p <crate>@<ver>` panics for a crate the workspace uses only as a build dependency, which most
|
|
165
|
+
// build helpers are). The workspace's Cargo.lock seeds the probe so its other crates resolve to the same versions.
|
|
166
|
+
const probe = path.join(scratch, "probe");
|
|
167
|
+
fs.mkdirSync(path.join(probe, "src"), { recursive: true });
|
|
168
|
+
const features = unit.features.filter((f) => f !== "default");
|
|
169
|
+
fs.writeFileSync(path.join(probe, "Cargo.toml"), `[package]\nname = "rk-build-helper-probe"\nversion = "0.0.0"\nedition = "2021"\npublish = false\n\n[dependencies]\n${JSON.stringify(unit.crate)} = { version = "=${unit.version}", default-features = ${unit.features.includes("default")}, features = ${JSON.stringify(features)} }\n\n[workspace]\n`);
|
|
170
|
+
fs.writeFileSync(path.join(probe, "src", "lib.rs"), "");
|
|
171
|
+
try { fs.copyFileSync(path.join(inv.metadata.workspace_root, "Cargo.lock"), path.join(probe, "Cargo.lock")); } catch { /* the probe resolves on its own */ }
|
|
172
|
+
// Release profile, like every other build (GATE-CONTRACT GC-A15): no dev or debug build anywhere, a throwaway probe included.
|
|
173
|
+
const buildArgs = ["build", "--profile", "release", "--manifest-path", path.join(probe, "Cargo.toml"), "--target-dir", target];
|
|
174
|
+
const buildEnv = { ...env, CARGO_TARGET_DIR: target, CARGO_BUILD_JOBS: perBuildJobs };
|
|
175
|
+
const straceOut = path.join(scratch, "strace");
|
|
176
|
+
const job = platform === "linux"
|
|
177
|
+
? { cmd: "strace", args: ["-ff", "-qq", "-o", straceOut, "-e", "trace=open,openat,openat2,creat,rename,renameat,renameat2,unlink,unlinkat,mkdir,mkdirat,rmdir,link,linkat,symlink,symlinkat,truncate,chmod,fchmodat,chown,fchownat,lchown,utimensat", "cargo", ...buildArgs], env: buildEnv }
|
|
178
|
+
: { cmd: "cargo", args: buildArgs, env: buildEnv };
|
|
179
|
+
job.label = `${unit.crate} ${unit.version}`;
|
|
180
|
+
return { unit, scratch, target, straceOut, job };
|
|
181
|
+
});
|
|
182
|
+
const sessionDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-session-"));
|
|
183
|
+
const traceFile = path.join(sessionDir, "trace.txt");
|
|
184
|
+
// One tracer session for the whole run (macOS eslogger, Windows ETW); Linux traces per build through strace.
|
|
185
|
+
let stopTracer = () => {};
|
|
186
|
+
let etl = null;
|
|
187
|
+
if (platform === "darwin") {
|
|
188
|
+
const tracer = run("sh", ["-c", `sudo -n eslogger fork exec create rename unlink link truncate open close clone copyfile exchangedata > '${traceFile}' 2>'${traceFile}.err' & echo $!`], { encoding: "utf8" });
|
|
189
|
+
const tracerPid = String(tracer.stdout ?? "").trim();
|
|
190
|
+
run("sleep", ["2"]);
|
|
191
|
+
stopTracer = () => { run("sleep", ["1"]); if (tracerPid) { run("sudo", ["-n", "pkill", "-INT", "-P", tracerPid]); run("kill", [tracerPid]); run("sleep", ["1"]); } };
|
|
192
|
+
} else if (platform === "win32") {
|
|
193
|
+
// One ETW session with both kernel providers, each with an explicit level (0x5): a separate process-only session without a level
|
|
194
|
+
// recorded nothing on the Dell (proc.etl 0 MB), so no write could be tied to its build. Tested on the Dell: process starts (with
|
|
195
|
+
// ParentProcessID) and file writes both recorded.
|
|
196
|
+
etl = { file: path.join(sessionDir, "trace.etl") };
|
|
197
|
+
const providers = path.join(sessionDir, "providers.txt");
|
|
198
|
+
fs.writeFileSync(providers, '"Microsoft-Windows-Kernel-Process" 0x10 0x5\r\n"Microsoft-Windows-Kernel-File" 0x1E90 0x5\r\n');
|
|
199
|
+
run("logman", ["stop", "rk-bht", "-ets"]);
|
|
200
|
+
const started = run("logman", ["start", "rk-bht", "-pf", providers, "-o", etl.file, "-ets"], { encoding: "utf8" });
|
|
201
|
+
if (started.status !== 0) { err(`right-release build-helpers trace: logman could not start the ETW session: ${String(started.stdout ?? "").trim()}`); return 2; }
|
|
202
|
+
stopTracer = () => { run("logman", ["stop", "rk-bht", "-ets"]); };
|
|
176
203
|
}
|
|
204
|
+
// The runner keeps `jobs` builds running and starts the next whenever one finishes; each build's pid is its tree's root.
|
|
205
|
+
const jobsFile = path.join(sessionDir, "jobs.json"); const resultFile = path.join(sessionDir, "results.json");
|
|
206
|
+
fs.writeFileSync(jobsFile, JSON.stringify(prepared.map((b) => b.job)));
|
|
207
|
+
let runner;
|
|
208
|
+
try { runner = run(process.execPath, ["-e", BATCH_RUNNER, jobsFile, resultFile, String(jobs)], { encoding: "utf8", env, stdio: ["ignore", "inherit", "inherit"] }); }
|
|
209
|
+
finally { stopTracer(); }
|
|
210
|
+
let results = [];
|
|
211
|
+
try { results = JSON.parse(fs.readFileSync(resultFile, "utf8")); } catch { err(`right-release build-helpers trace: the build runner wrote no results (${runner?.error?.message ?? `exit ${runner?.status}`}); nothing recorded`); return 2; }
|
|
212
|
+
// The whole-run recording is streamed, never read into one string, and attributed to every build's tree in one pass.
|
|
213
|
+
const roots = new Set(results.filter((r) => r?.status === 0 && Number.isInteger(r.pid)).map((r) => r.pid));
|
|
214
|
+
let byRoot = new Map(); let recorded = false;
|
|
215
|
+
if (platform === "darwin") { try { recorded = fs.statSync(traceFile).size > 0; if (recorded) byRoot = esloggerWritesByRoot(traceFile, roots); } catch { /* no output */ } }
|
|
216
|
+
if (platform === "win32") {
|
|
217
|
+
const xml = path.join(sessionDir, "trace.xml");
|
|
218
|
+
const exported = run("tracerpt", [etl.file, "-o", xml, "-of", "XML", "-y"], { encoding: "utf8" });
|
|
219
|
+
if (exported.status !== 0) { err("right-release build-helpers trace: tracerpt could not export the ETW trace"); return 2; }
|
|
220
|
+
try { recorded = fs.statSync(xml).size > 0; if (recorded) byRoot = etwWritesByRoot(xml, roots, devices); } catch { /* no output */ }
|
|
221
|
+
}
|
|
222
|
+
if (platform !== "linux" && !recorded && roots.size) { err(`right-release build-helpers trace: the ${platform === "darwin" ? "eslogger" : "ETW"} session recorded nothing; nothing recorded`); return 2; }
|
|
223
|
+
prepared.forEach((b, k) => {
|
|
224
|
+
const r = results[k] ?? {};
|
|
225
|
+
if (r.status !== 0 || !Number.isInteger(r.pid)) { err(`right-release build-helpers trace: ${b.unit.crate} ${b.unit.version} did not build (${r.error ?? `exit ${r.status}`}); not recorded`); return; }
|
|
226
|
+
let writes;
|
|
227
|
+
if (platform === "linux") { let text = ""; try { text = readStraceFiles(b.straceOut); } catch { /* none */ } writes = parseStraceWrites(text, { cwd: path.dirname(path.resolve(manifestPath ?? "Cargo.toml")) }); }
|
|
228
|
+
else writes = byRoot.get(r.pid) ?? [];
|
|
229
|
+
const allowed = platform === "win32" ? [b.scratch, cargoHome, os.tmpdir(), env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, "Temp") : null]
|
|
230
|
+
: [b.scratch, cargoHome, os.tmpdir(), "/private/var/folders", "/dev", "/proc/self"];
|
|
231
|
+
// Allowed folders by their real path: a linked CARGO_HOME (~/.cargo -> /Volumes/D/...) or temp (/var -> /private/var) is reported by
|
|
232
|
+
// the tracer under the real path, so comparing the link's path would fail every crate.
|
|
233
|
+
const real = (d) => { try { return fs.realpathSync.native(d); } catch { return d; } };
|
|
234
|
+
const roots = allowed.filter(Boolean).flatMap((d) => [d, real(d)]);
|
|
235
|
+
const judged = judgeTrace(writes, { allowed: roots, platform, mustSee: real(b.target) });
|
|
236
|
+
recordTrace(listFile, b.unit, judged);
|
|
237
|
+
// --record-repo also writes the verdict into the repository's committed list, for review and commit by the repository's owner.
|
|
238
|
+
if (recordRepo) recordTrace(repoFile, b.unit, judged);
|
|
239
|
+
log(`${judged.ok ? "pass" : "FAIL"} ${b.unit.crate} ${b.unit.version}${judged.ok ? "" : `: wrote ${judged.outside.slice(0, 5).join(", ")}`}`);
|
|
240
|
+
});
|
|
241
|
+
for (const b of prepared) fs.rmSync(b.scratch, { recursive: true, force: true });
|
|
242
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
177
243
|
return 0;
|
|
178
244
|
}
|
|
179
|
-
err("usage: right-release build-helpers check|trace [--manifest-path <Cargo.toml>] [--record-repo] | trace-all [--root <dir>]...");
|
|
245
|
+
err("usage: right-release build-helpers check|trace [--manifest-path <Cargo.toml>] [--record-repo] [--jobs N] | trace-all [--root <dir>]... [--jobs N]");
|
|
180
246
|
return 2;
|
|
181
247
|
}
|
|
182
248
|
|
package/package.json
CHANGED
package/rightkit-versions.json
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@rightkit/qa": "0.6.2",
|
|
50
50
|
"@rightkit/qa-chrome": "0.1.12",
|
|
51
51
|
"@rightkit/react-shell": "0.7.2",
|
|
52
|
-
"@rightkit/release": "0.6.
|
|
52
|
+
"@rightkit/release": "0.6.27",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|