@rightkit/release 0.6.29 → 0.6.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/build-helper-trace.mjs +9 -2
- package/build-helpers-command.mjs +65 -15
- package/package.json +1 -1
- package/rightkit-versions.json +2 -2
package/build-helper-trace.mjs
CHANGED
|
@@ -294,7 +294,7 @@ export function esloggerWritesByRoot(file, roots) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/** tracerpt XML from `file`, streamed by `</Event>`: Map(root pid -> written paths) for every pid in `roots`, kernel paths mapped by `devices`. */
|
|
297
|
-
export function etwWritesByRoot(file, roots, devices = {}) {
|
|
297
|
+
export function etwWritesByRoot(file, roots, devices = {}, { toolchainDirs = [] } = {}) {
|
|
298
298
|
const field = (ev, name) => new RegExp(`<Data Name="${name}">([^<]*)</Data>`).exec(ev)?.[1];
|
|
299
299
|
const id = (ev) => Number(/<EventID>(\d+)<\/EventID>/.exec(ev)?.[1]);
|
|
300
300
|
const pidOf = (ev) => Number(/ProcessID="(\d+)"/.exec(ev)?.[1]);
|
|
@@ -322,7 +322,14 @@ export function etwWritesByRoot(file, roots, devices = {}) {
|
|
|
322
322
|
const pid = Number(field(ev, "ProcessID"));
|
|
323
323
|
if (n === 1) {
|
|
324
324
|
const ppid = Number(field(ev, "ParentProcessID"));
|
|
325
|
-
|
|
325
|
+
// The MSVC linker starts its telemetry helper (vctip.exe, a .NET program) after a link: it writes the user profile and .NET's
|
|
326
|
+
// caches, belongs to Microsoft's toolchain rather than to any crate, and is left out of the build's tree with everything it starts.
|
|
327
|
+
// Only Microsoft's own copy, by path prefix (Program Files\Microsoft Visual Studio): a copy a build script places elsewhere stays in
|
|
328
|
+
// the tree, and placing one under Program Files is itself an outside write the trace fails.
|
|
329
|
+
const imagePath = String(toDos(field(ev, "ImageName")) ?? "").toLowerCase();
|
|
330
|
+
const msvcOwned = imagePath.endsWith("\\vctip.exe") && toolchainDirs.some((d) => imagePath.startsWith(`${String(d).toLowerCase().replace(/[\\/]+$/, "")}\\`));
|
|
331
|
+
if (msvcOwned) live.delete(pid);
|
|
332
|
+
else if (roots.has(pid) && ppid === runner) live.set(pid, pid);
|
|
326
333
|
else if (live.has(ppid)) live.set(pid, live.get(ppid));
|
|
327
334
|
else live.delete(pid);
|
|
328
335
|
} else if (n === 2) live.delete(pid);
|
|
@@ -84,6 +84,8 @@ export function cargoWorkspaces(root, { depth = 4, fsApi = fs } = {}) {
|
|
|
84
84
|
// in the workspace's own committed list (--record-repo). Default root: /Volumes/D/claude (Mac), D:\claude (Windows), the current
|
|
85
85
|
// directory elsewhere.
|
|
86
86
|
function traceAll(argv, { env, platform, log, err }) {
|
|
87
|
+
// A unit is judged once per run: a verdict recorded in one workspace (pass, fail or unbuildable) is never rebuilt in the next.
|
|
88
|
+
const judged = new Set();
|
|
87
89
|
const roots = argv.flatMap((a, i) => (a === "--root" && argv[i + 1] ? [path.resolve(argv[i + 1])] : []));
|
|
88
90
|
if (!roots.length) roots.push(platform === "darwin" ? "/Volumes/D/claude" : platform === "win32" ? "D:\\claude" : process.cwd());
|
|
89
91
|
let keepAlive = null;
|
|
@@ -103,7 +105,7 @@ function traceAll(argv, { env, platform, log, err }) {
|
|
|
103
105
|
const jobsAt = argv.indexOf("--jobs");
|
|
104
106
|
// The host list is what Class A reads on this host; a repository's committed list is written only on request (--record-repo), so
|
|
105
107
|
// tracing never edits a checkout another owner holds.
|
|
106
|
-
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 });
|
|
108
|
+
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, judged });
|
|
107
109
|
if (code !== 0) err(`right-release build-helpers trace-all: ${ws} ended with ${code}`);
|
|
108
110
|
worst = Math.max(worst, code);
|
|
109
111
|
}
|
|
@@ -112,7 +114,7 @@ function traceAll(argv, { env, platform, log, err }) {
|
|
|
112
114
|
return worst;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
export function main(argv = process.argv.slice(2), { env = process.env, platform = process.platform, run: runRaw = null, log = console.log, err = console.error } = {}) {
|
|
117
|
+
export function main(argv = process.argv.slice(2), { env = process.env, platform = process.platform, run: runRaw = null, log = console.log, err = console.error, judged = new Set() } = {}) {
|
|
116
118
|
if (argv[0] === "trace-all") return traceAll(argv.slice(1), { env, platform, log, err });
|
|
117
119
|
// GATE-CONTRACT GC-A11: the check's cargo and rustc calls compile nothing and run directly with the real toolchain (RustRight shims off PATH),
|
|
118
120
|
// writing to a scratch target, never through the broker.
|
|
@@ -123,10 +125,10 @@ export function main(argv = process.argv.slice(2), { env = process.env, platform
|
|
|
123
125
|
// 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
|
|
124
126
|
// ever goes through the RustRight shim (which refuses a build that redirects its output folder).
|
|
125
127
|
const directEnv = { ...env, [pathKey]: directPath };
|
|
126
|
-
try { return mainWith(argv, { env: directEnv, platform, run, log, err }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
|
|
128
|
+
try { return mainWith(argv, { env: directEnv, platform, run, log, err, judged }); } finally { fs.rmSync(scratchTarget, { recursive: true, force: true }); }
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
function mainWith(argv, { env, platform, run, log, err }) {
|
|
131
|
+
function mainWith(argv, { env, platform, run, log, err, judged = new Set(), confirm = null }) {
|
|
130
132
|
const [sub, ...rest] = argv;
|
|
131
133
|
const at = rest.indexOf("--manifest-path");
|
|
132
134
|
const manifestPath = at >= 0 ? rest[at + 1] : undefined;
|
|
@@ -161,18 +163,37 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
161
163
|
// target, and is judged only on the writes of that tree (eslogger and ETW events name their process). The cores are shared out so the
|
|
162
164
|
// builds do not fight over them.
|
|
163
165
|
const jobsAt = rest.indexOf("--jobs");
|
|
164
|
-
|
|
166
|
+
// A confirming pass builds one unit at a time, so nothing else of this run shares the machine with it.
|
|
167
|
+
const jobs = confirm ? 1 : Math.max(1, Number.parseInt(jobsAt >= 0 ? rest[jobsAt + 1] : "4", 10) || 4);
|
|
165
168
|
const perBuildJobs = String(Math.max(1, Math.floor(os.cpus().length / jobs)));
|
|
166
169
|
// A cross target builds with the provisioned zig C toolchain; without it those units wait (never recorded as unbuildable).
|
|
170
|
+
// The toolchain is proved first: a one-line C file compiled through the target's CC wrapper. A wrapper that cannot run (on the Dell a
|
|
171
|
+
// Node junction an elevated shell refused to traverse) failed every C crate, and 0.6.29 recorded those as unbuildable.
|
|
172
|
+
const workingCrossEnv = (target) => {
|
|
173
|
+
const cc = crossCompileEnv(target, undefined, platform);
|
|
174
|
+
if (!cc) return null;
|
|
175
|
+
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-cc-"));
|
|
176
|
+
try {
|
|
177
|
+
fs.writeFileSync(path.join(probeDir, "probe.c"), "int rk_probe(void) { return 0; }\n");
|
|
178
|
+
const zigCache = { ZIG_GLOBAL_CACHE_DIR: path.join(probeDir, "zig-global"), ZIG_LOCAL_CACHE_DIR: path.join(probeDir, "zig-local") };
|
|
179
|
+
// A .cmd wrapper runs through the shell on Windows, which joins arguments unquoted: every path is quoted there.
|
|
180
|
+
const q = (s) => (platform === "win32" ? `"${s}"` : s);
|
|
181
|
+
const r = run(q(cc[`CC_${target.replaceAll("-", "_")}`]), ["-c", q(path.join(probeDir, "probe.c")), "-o", q(path.join(probeDir, "probe.o"))], { encoding: "utf8", env: { ...env, ...zigCache }, shell: platform === "win32" });
|
|
182
|
+
if (r.status === 0 && fs.existsSync(path.join(probeDir, "probe.o"))) return cc;
|
|
183
|
+
err(`right-release build-helpers trace: the ${target} C compiler wrapper does not work (${String(r.error?.message ?? r.stderr ?? r.stdout ?? `exit ${r.status}`).trim().split(/\r?\n/)[0]}); its units are not traced (rerun scripts/ci/provision-zig.mjs)`);
|
|
184
|
+
return null;
|
|
185
|
+
} finally { fs.rmSync(probeDir, { recursive: true, force: true }); }
|
|
186
|
+
};
|
|
167
187
|
const crossEnv = new Map(); const waiting = [];
|
|
168
|
-
const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)].filter((unit) => {
|
|
188
|
+
const pending = [...verdict.untraced, ...verdict.failed.map((f) => f.unit)].filter((unit) => (confirm ? confirm.has(unit.key) : !judged.has(unit.key))).filter((unit) => {
|
|
169
189
|
if (unit.target === inv.host) return true;
|
|
170
|
-
if (!crossEnv.has(unit.target)) crossEnv.set(unit.target,
|
|
190
|
+
if (!crossEnv.has(unit.target)) crossEnv.set(unit.target, workingCrossEnv(unit.target));
|
|
171
191
|
if (crossEnv.get(unit.target)) return true;
|
|
172
192
|
waiting.push(unit); return false;
|
|
173
193
|
});
|
|
174
|
-
if (waiting.length) err(`right-release build-helpers trace: ${waiting.length} cross-target unit(s) not traced: the zig C toolchain is not provisioned (scripts/ci/provision-zig.mjs)`);
|
|
175
|
-
log(`right-release build-helpers trace: ${pending.length} unit(s), ${jobs} at a time`);
|
|
194
|
+
if (waiting.length) err(`right-release build-helpers trace: ${waiting.length} cross-target unit(s) not traced: the zig C toolchain is not provisioned or does not work (scripts/ci/provision-zig.mjs)`);
|
|
195
|
+
log(`right-release build-helpers trace: ${pending.length} unit(s)${confirm ? " to confirm" : ""}, ${jobs} at a time`);
|
|
196
|
+
const firstFails = new Map();
|
|
176
197
|
if (!pending.length) return 0;
|
|
177
198
|
const prepared = pending.map((unit) => {
|
|
178
199
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "rk-bht-"));
|
|
@@ -190,7 +211,10 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
190
211
|
// Built for the unit's own target, as the gate builds it: the host's, or a cross-check target with --target and zig.
|
|
191
212
|
const cross = unit.target !== inv.host;
|
|
192
213
|
const buildArgs = ["build", "--profile", "release", "--manifest-path", path.join(probe, "Cargo.toml"), "--target-dir", target, ...(cross ? ["--target", unit.target] : [])];
|
|
193
|
-
|
|
214
|
+
// zig (the cross C compiler) keeps its caches in this build's scratch folder: its default global cache is a host folder outside the
|
|
215
|
+
// allowed roots, and every crate with C code was failed for zig's own cache writes (ring, rustls, blake3, libsqlite3-sys on 0.6.29).
|
|
216
|
+
const zigCache = cross ? { ZIG_GLOBAL_CACHE_DIR: path.join(scratch, "zig-global"), ZIG_LOCAL_CACHE_DIR: path.join(scratch, "zig-local") } : {};
|
|
217
|
+
const buildEnv = { ...env, ...(cross ? crossEnv.get(unit.target) : {}), ...zigCache, CARGO_TARGET_DIR: target, CARGO_BUILD_JOBS: perBuildJobs };
|
|
194
218
|
const job = { cmd: "cargo", args: buildArgs, env: buildEnv };
|
|
195
219
|
job.label = `${unit.crate} ${unit.version}${cross ? ` (${unit.target})` : ""}`;
|
|
196
220
|
return { unit, scratch, target, job };
|
|
@@ -258,15 +282,25 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
258
282
|
const xml = path.join(sessionDir, "trace.xml");
|
|
259
283
|
const exported = run("tracerpt", [etl.file, "-o", xml, "-of", "XML", "-y"], { encoding: "utf8" });
|
|
260
284
|
if (exported.status !== 0) { err("right-release build-helpers trace: tracerpt could not export the ETW trace"); return 2; }
|
|
261
|
-
try { recorded = fs.statSync(xml).size > 0; if (recorded) byRoot = etwWritesByRoot(xml, roots, devices); } catch { /* no output */ }
|
|
285
|
+
try { recorded = fs.statSync(xml).size > 0; if (recorded) byRoot = etwWritesByRoot(xml, roots, devices, { toolchainDirs: [env.ProgramFiles, env["ProgramFiles(x86)"]].filter(Boolean).map((d) => path.win32.join(d, "Microsoft Visual Studio")) }); } catch { /* no output */ }
|
|
262
286
|
}
|
|
263
287
|
if (!recorded && roots.size) { err(`right-release build-helpers trace: the ${platform === "darwin" ? "eslogger" : "ETW"} session recorded nothing; nothing recorded`); return 2; }
|
|
264
288
|
prepared.forEach((b, k) => {
|
|
265
289
|
const r = results[k] ?? {};
|
|
266
290
|
if (!Number.isInteger(r.status)) { err(`right-release build-helpers trace: ${b.unit.crate} ${b.unit.version} did not finish (${r.error ?? "no result"}); not recorded`); return; }
|
|
291
|
+
if (r.status !== 0 && confirm) {
|
|
292
|
+
// It built in the first pass, so a failed rebuild is a passing problem (memory, a lock), not "unbuildable": the first FAIL stands.
|
|
293
|
+
const before = [...confirm.get(b.unit.key)];
|
|
294
|
+
judged.add(b.unit.key);
|
|
295
|
+
recordTrace(listFile, b.unit, { ok: false, outside: before });
|
|
296
|
+
if (recordRepo) recordTrace(repoFile, b.unit, { ok: false, outside: before });
|
|
297
|
+
log(`FAIL ${b.unit.crate} ${b.unit.version} (${b.unit.target}): the rebuild did not build; the first trace stands: wrote ${before.slice(0, 5).join(", ")}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
267
300
|
if (r.status !== 0) {
|
|
268
301
|
// It does not build here for this target, so nothing this host's gate runs builds it either: recorded once, skipped from then on.
|
|
269
302
|
const reason = String(r.error ?? `exit ${r.status}`);
|
|
303
|
+
judged.add(b.unit.key);
|
|
270
304
|
recordTrace(listFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
|
|
271
305
|
if (recordRepo) recordTrace(repoFile, b.unit, { ok: false, unbuildable: true, outside: [reason] });
|
|
272
306
|
log(`skip ${b.unit.crate} ${b.unit.version} (${b.unit.target}): does not build on this host (${reason.slice(0, 160)})`);
|
|
@@ -279,14 +313,30 @@ function mainWith(argv, { env, platform, run, log, err }) {
|
|
|
279
313
|
// the tracer under the real path, so comparing the link's path would fail every crate.
|
|
280
314
|
const real = (d) => { try { return fs.realpathSync.native(d); } catch { return d; } };
|
|
281
315
|
const roots = allowed.filter(Boolean).flatMap((d) => [d, real(d)]);
|
|
282
|
-
|
|
283
|
-
|
|
316
|
+
let verdictOf = judgeTrace(writes, { allowed: roots, platform, mustSee: real(b.target) });
|
|
317
|
+
// A FAIL must repeat (Dell, 2026-11-03: Windows' recorder pinned other programs' writes on a build now and then). The first FAIL is
|
|
318
|
+
// held and the unit is rebuilt alone; it fails only on the outside paths both builds wrote, or when either build went unobserved.
|
|
319
|
+
// Only Windows' recorder sees the whole machine, so only there can another program's write land on a build: on the Mac (eslogger
|
|
320
|
+
// follows the build's own processes) an outside write fails at once, and the rebuild only retries a build that went unobserved.
|
|
321
|
+
const unseen = (o) => o.startsWith("(the tracer recorded no write");
|
|
322
|
+
if (!verdictOf.ok && !confirm && (platform === "win32" || verdictOf.outside.every(unseen))) { firstFails.set(b.unit.key, new Set(verdictOf.outside)); log(`recheck ${b.unit.crate} ${b.unit.version} (${b.unit.target}): rebuilding it alone`); return; }
|
|
323
|
+
if (!verdictOf.ok && confirm) {
|
|
324
|
+
const before = confirm.get(b.unit.key);
|
|
325
|
+
// An unobserved first build proves nothing either way: then every outside write of the rebuild counts.
|
|
326
|
+
const firstUnseen = [...before].some(unseen);
|
|
327
|
+
const repeated = verdictOf.outside.filter((o) => !unseen(o) && (firstUnseen || before.has(o)));
|
|
328
|
+
const unobserved = verdictOf.outside.some(unseen);
|
|
329
|
+
verdictOf = repeated.length || unobserved ? { ok: false, outside: repeated.length ? repeated : verdictOf.outside } : { ok: true, outside: [] };
|
|
330
|
+
}
|
|
331
|
+
judged.add(b.unit.key);
|
|
332
|
+
recordTrace(listFile, b.unit, verdictOf);
|
|
284
333
|
// --record-repo also writes the verdict into the repository's committed list, for review and commit by the repository's owner.
|
|
285
|
-
if (recordRepo) recordTrace(repoFile, b.unit,
|
|
286
|
-
log(`${
|
|
334
|
+
if (recordRepo) recordTrace(repoFile, b.unit, verdictOf);
|
|
335
|
+
log(`${verdictOf.ok ? "pass" : "FAIL"} ${b.unit.crate} ${b.unit.version} (${b.unit.target})${verdictOf.ok ? "" : `: wrote ${verdictOf.outside.slice(0, 5).join(", ")}`}`);
|
|
287
336
|
});
|
|
288
337
|
for (const b of prepared) fs.rmSync(b.scratch, { recursive: true, force: true });
|
|
289
338
|
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
339
|
+
if (firstFails.size) return mainWith(argv, { env, platform, run, log, err, judged, confirm: firstFails });
|
|
290
340
|
return 0;
|
|
291
341
|
}
|
|
292
342
|
err("usage: right-release build-helpers check|trace [--manifest-path <Cargo.toml>] [--record-repo] [--jobs N] | trace-all [--root <dir>]... [--jobs N]");
|
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.30",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|
|
@@ -512,7 +512,7 @@
|
|
|
512
512
|
"rightkit-app-settings": "0.3.1",
|
|
513
513
|
"rightkit-apple-ops": "0.1.1",
|
|
514
514
|
"rightkit-approvals": "0.1.0",
|
|
515
|
-
"rightkit-asr": "0.
|
|
515
|
+
"rightkit-asr": "0.4.0",
|
|
516
516
|
"rightkit-audio": "0.3.1",
|
|
517
517
|
"rightkit-autoedit": "0.1.1",
|
|
518
518
|
"rightkit-automation": "0.6.4",
|