@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.
@@ -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
- export function buildHelperKey({ crate, version, checksum, features = [], target, toolchain }) {
27
- return sha256(JSON.stringify({ crate, version, checksum, features: [...features].sort(), target, toolchain }));
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 = list?.units?.[u.key];
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, judgeTrace, mergeLists, parseEsloggerWrites, parseEtwWrites, parseStraceWrites, readStraceFiles, readHostList, recordTrace, repoListPath } from "./build-helper-trace.mjs";
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 code = main(["trace", "--manifest-path", path.join(ws, "Cargo.toml"), "--record-repo"], { env, platform, log, err });
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
- try { return mainWith(argv, { env, platform, run, log, err }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
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
- for (const unit of [...verdict.untraced, ...verdict.failed.map((f) => f.unit)]) {
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 traceFile = path.join(scratch, "trace.txt");
132
- const buildArgs = ["build", "-p", `${unit.crate}@${unit.version}`, "--locked", "--target-dir", path.join(scratch, "target"), ...(manifestPath ? ["--manifest-path", manifestPath] : [])];
133
- const buildEnv = { ...env, CARGO_TARGET_DIR: path.join(scratch, "target") };
134
- let build;
135
- if (platform === "win32") {
136
- const etlFile = path.join(scratch, "file.etl"); const etlProc = path.join(scratch, "proc.etl");
137
- run("logman", ["stop", "rk-bht-file", "-ets"]); run("logman", ["stop", "rk-bht-proc", "-ets"]);
138
- const startProc = run("logman", ["start", "rk-bht-proc", "-p", "Microsoft-Windows-Kernel-Process", "0x10", "-o", etlProc, "-ets"], { encoding: "utf8" });
139
- const startFile = run("logman", ["start", "rk-bht-file", "-p", "Microsoft-Windows-Kernel-File", "0x1E90", "-o", etlFile, "-ets"], { encoding: "utf8" });
140
- if (startProc.status !== 0 || startFile.status !== 0) { err(`right-release build-helpers trace: logman could not start the ETW sessions: ${String(startFile.stdout ?? startProc.stdout ?? "").trim()}`); return 2; }
141
- try { build = run("cargo", buildArgs, { encoding: "utf8", env: buildEnv }); }
142
- finally { run("logman", ["stop", "rk-bht-file", "-ets"]); run("logman", ["stop", "rk-bht-proc", "-ets"]); }
143
- const xml = path.join(scratch, "trace.xml");
144
- const exported = run("tracerpt", [etlProc, etlFile, "-o", xml, "-of", "XML", "-y"], { encoding: "utf8" });
145
- if (exported.status !== 0) { err("right-release build-helpers trace: tracerpt could not export the ETW trace"); return 2; }
146
- fs.writeFileSync(traceFile, fs.readFileSync(xml, "utf8"));
147
- } else if (platform === "linux") {
148
- // strace follows every child (-f) and records the file calls; it needs no root for the agent's own processes.
149
- // -ff: one output file per process, so no system call is split across <unfinished>/<resumed> lines.
150
- build = run("strace", ["-ff", "-qq", "-o", traceFile, "-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], { encoding: "utf8", env: buildEnv });
151
- if (build.error?.code === "ENOENT") { err("right-release build-helpers trace: strace is not installed on this host"); return 2; }
152
- } else {
153
- // Endpoint Security (eslogger) names every event's process and its parent, so the whole tree under this process is traced: build
154
- // scripts and everything they start (sh, cc, cmake, perl), whatever the name. Only the logger is root (`sudo -n`: the credential
155
- // `trace-all` cached with one `sudo -v`); the build runs as the user. eslogger needs Full Disk Access for the terminal running it.
156
- 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" });
157
- const tracerPid = String(tracer.stdout ?? "").trim();
158
- run("sleep", ["2"]);
159
- build = run("cargo", buildArgs, { encoding: "utf8", env: buildEnv });
160
- run("sleep", ["1"]);
161
- if (tracerPid) { run("sudo", ["-n", "pkill", "-INT", "-P", tracerPid]); run("kill", [tracerPid]); run("sleep", ["1"]); }
162
- }
163
- if (build.error || build.status !== 0) { err(`right-release build-helpers trace: ${unit.crate} ${unit.version} did not build (${build.error?.message ?? `exit ${build.status}`}); not recorded`); continue; }
164
- let text = ""; try { text = platform === "linux" ? readStraceFiles(traceFile) : fs.readFileSync(traceFile, "utf8"); } catch { /* no trace output */ }
165
- if (!text.trim()) { err(`right-release build-helpers trace: ${platform === "linux" ? "strace" : platform === "darwin" ? "eslogger (it needs sudo and Full Disk Access for this terminal)" : "ETW"} recorded nothing; nothing recorded`); return 2; }
166
- const writes = platform === "win32" ? parseEtwWrites(text, { rootPid: process.pid, devices })
167
- : platform === "linux" ? parseStraceWrites(text, { cwd: path.dirname(path.resolve(manifestPath ?? "Cargo.toml")) }) : parseEsloggerWrites(text, { rootPid: process.pid });
168
- const allowed = platform === "win32" ? [path.join(scratch, "target"), cargoHome, os.tmpdir(), env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, "Temp") : null]
169
- : [path.join(scratch, "target"), cargoHome, os.tmpdir(), "/private/var/folders", "/dev", "/proc/self"];
170
- const judged = judgeTrace(writes, { allowed, platform, mustSee: path.join(scratch, "target") });
171
- recordTrace(listFile, unit, judged);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -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.26",
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",