@rightkit/release 0.6.20 → 0.6.22
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/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +67 -18
- package/carried-cargo.mjs +2 -2
- package/deterministic-scans.mjs +7 -3
- package/gate-stages.mjs +10 -5
- package/package-recipe-targets.mjs +12 -2
- package/package.json +1 -1
- package/rightkit-versions.json +1 -1
- package/snapshot-broker.mjs +27 -3
- package/snapshot-check.mjs +4 -1
- package/snapshot-request.mjs +32 -16
- package/stage-ledger.mjs +5 -2
- package/stage-sidecar-command.mjs +21 -6
- package/stage-sidecar.mjs +43 -2
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -1608,12 +1608,14 @@ async function runStages(o, args, root, git, io) {
|
|
|
1608
1608
|
// `main --all` compiles. A broker host's stage-sidecar child reaches Cargo through the stage's carrier (carried-cargo.mjs).
|
|
1609
1609
|
// right-release runs in appRoot (nested apps), so the repository-relative config is handed over absolute (Astra, second pass).
|
|
1610
1610
|
// On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
//
|
|
1611
|
+
// rightkit#1331: the declared compile inputs that are the app's web build output are produced in the snapshot before ANY compile reads them,
|
|
1612
|
+
// the sidecar builds included: the broker admits a compile only when every declared input is in the snapshot (HeardRight Mac 2026-11-02:
|
|
1613
|
+
// stage-sidecar's cargo build REJECTED "snapshot admission: tauri-app-next/dist: No such file or directory").
|
|
1614
1614
|
if (ok && ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1615
|
-
// The Windows package-identity MSIX a Tauri app bundles (its release config's windows.identity), produced in the snapshot on Windows only.
|
|
1615
|
+
// The Windows package-identity MSIX a Tauri app bundles (its release config's windows.identity), produced in the snapshot on Windows only, for the same reason.
|
|
1616
1616
|
if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1617
|
+
if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1618
|
+
else for (const config of ok ? sidecarConfigs : []) if (!await rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
|
|
1617
1619
|
let compiledDirs = []; let compiledBins = null; let qualifyOwed = [];
|
|
1618
1620
|
const reportOutputs = (dirs, bins = null) => { compiledDirs = [...dirs]; compiledBins = bins; };
|
|
1619
1621
|
if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, classAEarlier: true, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs, onQualifyOwed: (list) => { qualifyOwed = list; } } : {}) })))({ reportOutputs })) === 0;
|
|
@@ -1636,7 +1638,17 @@ async function runStages(o, args, root, git, io) {
|
|
|
1636
1638
|
// compile was already passed (a separately invoked `--to package` re-stages after the sync, so it never meets a missing externalBin).
|
|
1637
1639
|
// right-release then takes its identity from the selected commit and the verified snapshot (release-source-identity.mjs, row 7.04),
|
|
1638
1640
|
// never the checkout's HEAD, status or branch; its outputs, state and the sealed directory stay in the checkout's `.right-release`.
|
|
1639
|
-
|
|
1641
|
+
// The declared web build and Windows identity inputs first, as in the compile stage: the sidecar builds are broker-admitted only when every declared input is in the snapshot.
|
|
1642
|
+
ok = true;
|
|
1643
|
+
if (ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1644
|
+
if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1645
|
+
if (ok && ds) {
|
|
1646
|
+
// The package triple: a Mac recipe with `stageSidecars` builds for its own triple (universal-apple-darwin by default), so the gate stages that triple here, under its own
|
|
1647
|
+
// producer carrier, and the recipe's prepare step (`stage-sidecar --target <triple>`) then finds it current and changes nothing.
|
|
1648
|
+
const packageConfigs = sidecarConfigsOf();
|
|
1649
|
+
const packageTarget = await packageSidecarTarget({ o, ds, configs: packageConfigs });
|
|
1650
|
+
ok = await stageSidecarsOnce({ o, ds, root, git, configs: packageConfigs, appDir, cli, commonDir, commit, tree, stage, executor, target: packageTarget });
|
|
1651
|
+
}
|
|
1640
1652
|
// Source QA (right-release qa --stage source) exists for mac and win only; a mobile package is the signed build alone.
|
|
1641
1653
|
// Row 7.04c (R-T1, 7.09): the snapshot holds no Cargo target. On a direct host the checkout's output directory is resolved here, in the primary
|
|
1642
1654
|
// checkout, and handed to right-release absolute: it binds every manifest's target for this stage, and the sealed manifest must record it back
|
|
@@ -1938,6 +1950,16 @@ async function restagedSidecars({ o, ds, prior, now }) {
|
|
|
1938
1950
|
return out;
|
|
1939
1951
|
}
|
|
1940
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* The --target the package stage stages sidecars for: `package` on a Mac host, which right-release stage-sidecar resolves itself from the
|
|
1955
|
+
* snapshot's config (the Mac package recipe's own triple, e.g. universal-apple-darwin), so this gate never imports app code; null elsewhere (host).
|
|
1956
|
+
*/
|
|
1957
|
+
export async function packageSidecarTarget({ o, ds, configs = [] }) {
|
|
1958
|
+
if (o.platform !== "darwin" || !ds) return null;
|
|
1959
|
+
// Read as text, never imported (as committedSidecarConfigs does): only a config whose Mac package recipe stages sidecars gets a package-triple staging.
|
|
1960
|
+
return configs.some((config) => { try { return /\bstageSidecars\b/.test(o.readFile(path.join(ds.path, config))); } catch { return false; } }) ? "package" : null;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1941
1963
|
/** The release configs of `commit` (under `appDir`) that declare `sidecars`, read from `base` (the snapshot on the snapshot route): the commit's config decides, never the checkout's. */
|
|
1942
1964
|
export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
|
|
1943
1965
|
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
@@ -1951,9 +1973,14 @@ export function committedSidecarConfigs({ o, git, commit, appDir = ".", base })
|
|
|
1951
1973
|
* step's additions must equal the files it reported plus its receipt (anything else is an unattributed write, RK_CHECK_ERROR). The
|
|
1952
1974
|
* staged files become immutable `staged` ledger entries. Returns true when nothing is owed or staging passed.
|
|
1953
1975
|
*/
|
|
1954
|
-
export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
1955
|
-
|
|
1956
|
-
|
|
1976
|
+
export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:", target = null }) {
|
|
1977
|
+
// Once per door run PER TARGET TRIPLE: `target` null is the host (the compile stage: stage-sidecar's own default, no `--target` passed), anything else is the triple the
|
|
1978
|
+
// package recipe stages for (universal-apple-darwin on a Mac recipe with `stageSidecars`), passed to stage-sidecar as `--target`. stage-sidecar is a no-op for a target whose
|
|
1979
|
+
// receipt and files are already current, so the recipe's own prepare step afterwards changes nothing.
|
|
1980
|
+
const staged = ds.sidecarsStaged instanceof Set ? ds.sidecarsStaged : (ds.sidecarsStaged = new Set());
|
|
1981
|
+
const key = target ?? "host";
|
|
1982
|
+
if (staged.has(key)) return true;
|
|
1983
|
+
staged.add(key);
|
|
1957
1984
|
if (!configs.length) return true;
|
|
1958
1985
|
const { attributionProblems, reportedPaths } = await import("../stage-ledger.mjs");
|
|
1959
1986
|
const source = sourceSplitOf({ root, ds, appDir });
|
|
@@ -1961,13 +1988,13 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1961
1988
|
for (const config of configs) {
|
|
1962
1989
|
const before = ds.ledger?.entries ?? [];
|
|
1963
1990
|
// The stage-sidecar child's Cargo calls (its builds and metadata queries) are carried on a broker host (carried-cargo.mjs).
|
|
1964
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
1991
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
1965
1992
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing was staged`); return false; }
|
|
1966
1993
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
1967
1994
|
let r;
|
|
1968
1995
|
try {
|
|
1969
|
-
o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
|
|
1970
|
-
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
1996
|
+
o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)}${target ? ` --target ${target}` : ""} (from the snapshot ${ds.path})`);
|
|
1997
|
+
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), ...(target ? ["--target", target] : []), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
1971
1998
|
} finally { endRun(run); }
|
|
1972
1999
|
const carriedOk = await carrier.finish({ stage });
|
|
1973
2000
|
if (r?.stderr) o.err(String(r.stderr).trimEnd());
|
|
@@ -1983,7 +2010,7 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1983
2010
|
const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
|
|
1984
2011
|
const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`).replaceAll("\\", "/");
|
|
1985
2012
|
const receiptAbs = typeof result.receiptPath === "string" ? path.relative(ds.path, result.receiptPath).split(path.sep).join("/") : null;
|
|
1986
|
-
const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs })];
|
|
2013
|
+
const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs, removable: before.filter((e) => e.kind === "staged" && e.producer?.stage === "sidecar" && e.producer?.step === config).map((e) => e.path) /* only THIS config's earlier staged files (Oracle: another config's build must never delete them unreported) */ })];
|
|
1987
2014
|
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar ${config}: ${p}`); return false; }
|
|
1988
2015
|
const priorPaths = new Set(before.map((e) => e.path));
|
|
1989
2016
|
const priorByPath = new Map(before.map((e) => [e.path, e]));
|
|
@@ -2042,7 +2069,7 @@ export async function stageWebBuildOnce({ o, ds, root, git = null, appDir, cli,
|
|
|
2042
2069
|
for (const config of configs) {
|
|
2043
2070
|
if (!absent().length) break;
|
|
2044
2071
|
const before = ds.ledger?.entries ?? [];
|
|
2045
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2072
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
2046
2073
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the web build did not run`); return false; }
|
|
2047
2074
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2048
2075
|
let r;
|
|
@@ -2094,7 +2121,7 @@ export async function stageWindowsIdentityOnce({ o, ds, root, git = null, appDir
|
|
|
2094
2121
|
const producer = path.join(path.dirname(cli), "..", "windows-identity.mjs");
|
|
2095
2122
|
for (const config of configs) {
|
|
2096
2123
|
const before = ds.ledger?.entries ?? [];
|
|
2097
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2124
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
2098
2125
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the Windows identity package did not build`); return false; }
|
|
2099
2126
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2100
2127
|
let r;
|
|
@@ -2725,7 +2752,7 @@ async function snapshotCargoWorkspaces({ git, ds, root }) {
|
|
|
2725
2752
|
* child's environment; `finish` runs after the child ended, fails on any call that was refused, unverified or unfinished and on any
|
|
2726
2753
|
* receipt the gate cannot verify again itself, and removes the carrier. A direct executor, or no snapshot, has nothing to carry.
|
|
2727
2754
|
*/
|
|
2728
|
-
async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:" }) {
|
|
2755
|
+
async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:", producer = false }) {
|
|
2729
2756
|
const none = { env: {}, carried: false, refusal: null, async finish() { return true; }, cleanup() {} };
|
|
2730
2757
|
if (!ds || executor?.kind !== "broker") return none;
|
|
2731
2758
|
let base = null;
|
|
@@ -2734,6 +2761,15 @@ async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "ri
|
|
|
2734
2761
|
const carried = await import("../carried-cargo.mjs");
|
|
2735
2762
|
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
2736
2763
|
const sb = await carrierBroker({ o, root, ds, executor });
|
|
2764
|
+
// Only a PRODUCER carrier (the gate's own web-build, windows-identity and stage-sidecar steps, which write declared inputs by design) takes the gate's digest of the
|
|
2765
|
+
// declared inputs at stage open (absent included); the end-of-stage ledger re-verify then accepts any receipt state for an input that digest says the step changed.
|
|
2766
|
+
// Every other carrier passes none: each receipt, adapter receipts included, must equal the stage-end digest of every declared input (strict), so a child that swaps
|
|
2767
|
+
// an input around an admission and restores it is refused. A malformed declaration or an unreadable input is the refusal below, as before.
|
|
2768
|
+
let openInputDigests = null;
|
|
2769
|
+
if (producer) {
|
|
2770
|
+
const { readDeclaredInputs: readInputsOf, declaredInputDigestsOf: digestInputsOf } = await import("../snapshot-request.mjs");
|
|
2771
|
+
openInputDigests = digestInputsOf(ds.path, readInputsOf(ds.path), { allowAbsent: true });
|
|
2772
|
+
}
|
|
2737
2773
|
// Installing the carrier map sends nothing, so it records nothing: the child's own uncarried probes (`rustc -vV`, `--version`) never
|
|
2738
2774
|
// reach the broker under a snapshot, and each carried request records its sub-handle just before it is sent (snapshot-broker.mjs).
|
|
2739
2775
|
const checkoutDirs = await snapshotCargoWorkspaces({ git, ds, root });
|
|
@@ -2752,7 +2788,7 @@ async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "ri
|
|
|
2752
2788
|
finished = true;
|
|
2753
2789
|
let problems;
|
|
2754
2790
|
try {
|
|
2755
|
-
problems = await installed.problems(sb);
|
|
2791
|
+
problems = await installed.problems(sb, openInputDigests ? { openInputDigests } : {});
|
|
2756
2792
|
// The output directories the stage's verified receipts name: the package stage's sealed manifest must record only these.
|
|
2757
2793
|
for (const dir of installed.outputDirs?.() ?? []) ((ds.carriedByStage ??= {})[at] ??= new Set()).add(dir);
|
|
2758
2794
|
// And by workspace, for the next stage's children: a receipt already names the output directory, so none asks Cargo for it.
|
|
@@ -2868,6 +2904,11 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
|
|
|
2868
2904
|
// app's own prepare step may stage them too; the ledger's attribution check, not an environment marker, is what keeps that single and exact.
|
|
2869
2905
|
if (ds) {
|
|
2870
2906
|
const sidecarConfigs = committedSidecarConfigs({ o, git, commit, appDir, base: ds.path });
|
|
2907
|
+
// The declared web build and Windows identity inputs first: the sidecar builds are broker-admitted only when every declared input is in the snapshot.
|
|
2908
|
+
const rr = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
|
|
2909
|
+
if (!(await stageWebBuildOnce({ o, ds, root, git, appDir, cli: rr, commonDir, commit, tree, stage: to, executor, tag })) || !(await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli: rr, commonDir, commit, tree, stage: to, executor, tag }))) {
|
|
2910
|
+
o.err(`${tag} --to ${to} stopped: the declared compile inputs could not be produced; nothing compiled`); return 1;
|
|
2911
|
+
}
|
|
2871
2912
|
const staged = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli: fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url)), commonDir, commit, tree, stage: to, executor, tag });
|
|
2872
2913
|
if (!staged) { o.err(`${tag} --to ${to} stopped: the declared sidecars could not be staged; nothing compiled`); return 1; }
|
|
2873
2914
|
}
|
|
@@ -4044,12 +4085,20 @@ export async function main(argv, io = {}) {
|
|
|
4044
4085
|
const unitByName = new Map(rust.units.map((u) => [u.name, u]));
|
|
4045
4086
|
const known = new Set(unitByName.keys());
|
|
4046
4087
|
const builtBins = {};
|
|
4047
|
-
|
|
4088
|
+
// Where Cargo put them: `<dir>/debug` on a direct host, `<dir>/<triple>/debug` on a broker host, whose jobs build with --target <triple> (the receipt
|
|
4089
|
+
// names it, `targetTriple`). A broker row with no triple records nothing for its directory: a guessed path could vouch for a stale binary.
|
|
4090
|
+
const addBins = (dir, unit, profile) => {
|
|
4091
|
+
if (!dir || !profile || !unit?.testLinksBins || !unit.bins?.length) return;
|
|
4092
|
+
const prior = builtBins[dir];
|
|
4093
|
+
if (prior && prior.profile !== profile) { builtBins[dir] = { profile: null, bins: [] }; return; }
|
|
4094
|
+
builtBins[dir] = { profile, bins: [...new Set([...(prior?.bins ?? []), ...unit.bins])].sort() };
|
|
4095
|
+
};
|
|
4048
4096
|
for (const r of scanResult.scans) {
|
|
4049
4097
|
// A row answered from the scan cache compiled nothing in this run: the bytes in the target are not vouched for by it.
|
|
4050
4098
|
if (r.status !== "pass" || r.cached === true || !/^test:rust(?:-serial)?:/.test(String(r.id))) continue;
|
|
4051
4099
|
const unit = unitByName.get(rustUnitOfScanId(r.id, known));
|
|
4052
|
-
|
|
4100
|
+
if (snapshotBroker) addBins(r.snapshotEvidence?.outputDir, unit, r.snapshotEvidence?.targetTriple ? `${r.snapshotEvidence.targetTriple}/debug` : null);
|
|
4101
|
+
else addBins(targetDirs?.[unit?.cargoDir ?? "."], unit, "debug");
|
|
4053
4102
|
}
|
|
4054
4103
|
const outputWorkspaces = (snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir) : Object.values(targetDirs ?? {})).filter(Boolean).map((dir) => [dir]);
|
|
4055
4104
|
o.onCompileOutputs?.(outputWorkspaces.map(([dir]) => dir), builtBins);
|
package/carried-cargo.mjs
CHANGED
|
@@ -136,10 +136,10 @@ function finish(ledgerDir, dir, workspaces = {}) {
|
|
|
136
136
|
const checkoutRoots = Object.values(workspaces);
|
|
137
137
|
return {
|
|
138
138
|
/** Every call of the stage that is not a completed, verified success, and every verified call the gate cannot verify again itself. */
|
|
139
|
-
async problems(sb) {
|
|
139
|
+
async problems(sb, opts = {}) {
|
|
140
140
|
const { entries, problems } = readLedger(ledgerDir, UNSCOPED);
|
|
141
141
|
const out = [...problems];
|
|
142
|
-
if (sb) out.push(...await verifyBrokerLedger(entries, sb));
|
|
142
|
+
if (sb) out.push(...await verifyBrokerLedger(entries, sb, opts));
|
|
143
143
|
return out;
|
|
144
144
|
},
|
|
145
145
|
/** The verified calls' output directories, in call order (the receipts'; an adapter job's nested children's included). */
|
package/deterministic-scans.mjs
CHANGED
|
@@ -1348,7 +1348,9 @@ export function planScans({
|
|
|
1348
1348
|
// A package that depends on another workspace package is therefore checked through a generated project that maps each sibling's
|
|
1349
1349
|
// exported entry points to its committed sources (TSC_PROJECT, built by the runner); every other package keeps its own tsconfig.
|
|
1350
1350
|
// `--noEmit` stays on the command line as well as in the project: the class A launch guard judges a tsc launch by its argv alone.
|
|
1351
|
-
|
|
1351
|
+
// Every workspace package is a possible sibling, whatever subset this plan checks (a publication plans only its units, and their
|
|
1352
|
+
// siblings' sources must still be mapped: publish scans of @rightkit/workspace-ui could not resolve @rightkit/mail-ui/*).
|
|
1353
|
+
const workspaceDirs = [...new Set([...(npmMap ? [...npmMap.values()].map((u) => u.dir) : []), ...listDir(path.join(liveRoot, "packages")).map((n) => `packages/${n}`)])].sort();
|
|
1352
1354
|
const manifestOf = (dir) => { try { return JSON.parse(readFile(path.join(liveRoot, dir, "package.json")).replace(/^/, "")); } catch { return null; } };
|
|
1353
1355
|
const workspaceNames = new Map(workspaceDirs.map((d) => [manifestOf(d)?.name, d]).filter(([n]) => typeof n === "string"));
|
|
1354
1356
|
const usesWorkspaceSibling = (dir) => {
|
|
@@ -2596,7 +2598,9 @@ export async function runScans(plan, options = {}) {
|
|
|
2596
2598
|
// generated outside the checked tree and removed after the run, so the check reads nothing a commit lacks and writes nothing.
|
|
2597
2599
|
let tscProject = null;
|
|
2598
2600
|
if (args.includes(TSC_PROJECT)) {
|
|
2599
|
-
|
|
2601
|
+
// The siblings are mapped in the same tree tsc runs in (a publication scans a content checkout and a live checkout of
|
|
2602
|
+
// one verified commit; mixing them puts the package outside the project's rootDir, TS6059).
|
|
2603
|
+
try { tscProject = writeTscProject({ contentRoot: rootDir(s.root), packageDir: path.join(rootDir(s.root), s.cwd ?? ""), workspace: s.tscWorkspace ?? [] }); }
|
|
2600
2604
|
catch (e) { advisory?.cleanup?.(); return { ...row, status: "fail", code: "TSC_PROJECT", detail: `${s.id}: the check project could not be built from the committed workspace: ${e.message}` }; }
|
|
2601
2605
|
args = args.map((a) => (a === TSC_PROJECT ? tscProject.file : a));
|
|
2602
2606
|
}
|
|
@@ -2755,7 +2759,7 @@ export async function runScans(plan, options = {}) {
|
|
|
2755
2759
|
const ref = readRequestRef(path.join(carrier.refDir, "request.json"));
|
|
2756
2760
|
const receipt = await deps.snapshotBroker.receiptOf(ref.requestId);
|
|
2757
2761
|
const { evidence } = verifySnapshotReceipt(receipt, { pins: deps.snapshotBroker.pins, jobId: ref.requestId, carrier: carrier.made.carrier, argv: carrier.cargoArgv, workspace: ref.workspace, observedEnv: observedEnvOf({ ...deps.env, ...s.env, ...(extraEnv ?? {}) }, deps.platform ?? process.platform), checkout: deps.snapshotBroker.checkout, outputRoots: deps.snapshotBroker.outputRoots, platform: deps.snapshotBroker.platform ?? deps.platform ?? process.platform });
|
|
2758
|
-
row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
|
|
2762
|
+
row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", targetTriple: typeof receipt?.targetTriple === "string" && /^[A-Za-z0-9_.]+(?:-[A-Za-z0-9_.]+){1,4}$/.test(receipt.targetTriple) ? receipt.targetTriple : null, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
|
|
2759
2763
|
} catch (e) {
|
|
2760
2764
|
status = "fail"; code = "SNAPSHOT_EVIDENCE"; detail = `${s.id}: the broker's snapshot evidence did not verify (${e.code ?? "error"}: ${e.message})`;
|
|
2761
2765
|
}
|
package/gate-stages.mjs
CHANGED
|
@@ -234,10 +234,11 @@ export function compileKeyOf(env = {}) {
|
|
|
234
234
|
export const COMPILE_PROFILE_DIR = "debug";
|
|
235
235
|
|
|
236
236
|
/** How `binaries` was chosen: only the bin targets the compile built in that directory (a consumer requires this marker). */
|
|
237
|
-
export const BUILT_BINARIES_SCHEMA = "built-bin-targets/
|
|
237
|
+
export const BUILT_BINARIES_SCHEMA = "built-bin-targets/2";
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* The sha256 of each binary this compile built (rustright#209 item 5): for every output directory, the files `<dir
|
|
240
|
+
* The sha256 of each binary this compile built (rustright#209 item 5): for every output directory, the files `<dir>/<profile>/<bin>[.exe]` (`debug`, or
|
|
241
|
+
* `<triple>/debug` for a broker build with --target; each row's `path` names it, schema built-bin-targets/2) of the
|
|
241
242
|
* bin targets the compile built there (`builtBins`, { [dir]: [bin target name] }: the units whose scans ran `cargo build -p <crate>` in that
|
|
242
243
|
* directory's workspace, so each binary was rebuilt or proven fresh by Cargo for this commit). Any other file in `debug/`, a stale binary of a
|
|
243
244
|
* removed or renamed target or of a package this run did not build, is never recorded. A census proves nothing once truncated (a broker
|
|
@@ -247,12 +248,16 @@ export const BUILT_BINARIES_SCHEMA = "built-bin-targets/1";
|
|
|
247
248
|
export function compiledBinaries(builtBins = {}, { platform = process.platform, stat = (f) => fs.statSync(f), readFile = (f) => fs.readFileSync(f) } = {}) {
|
|
248
249
|
const out = [];
|
|
249
250
|
for (const dir of Object.keys(builtBins ?? {}).filter((d) => typeof d === "string" && d).sort()) {
|
|
250
|
-
|
|
251
|
+
// { profile, bins }: profile is the directory under `dir` Cargo wrote them to (`debug`, or `<triple>/debug` for a --target build); a bare array is `debug`.
|
|
252
|
+
const entry = Array.isArray(builtBins[dir]) ? { profile: COMPILE_PROFILE_DIR, bins: builtBins[dir] } : builtBins[dir] ?? {};
|
|
253
|
+
if (typeof entry.profile !== "string" || !/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)?$/.test(entry.profile) || entry.profile.split("/").some((c) => c === "." || c === "..")) continue;
|
|
254
|
+
for (const bin of [...new Set(entry.bins ?? [])].sort()) {
|
|
251
255
|
const name = `${bin}${platform === "win32" ? ".exe" : ""}`;
|
|
252
|
-
const
|
|
256
|
+
const rel = `${entry.profile}/${name}`;
|
|
257
|
+
const file = path.join(dir, ...rel.split("/"));
|
|
253
258
|
let info = null;
|
|
254
259
|
try { info = stat(file); } catch { info = null; }
|
|
255
|
-
out.push(info?.isFile?.() ? { dir, name, size: info.size, sha256: sha256(readFile(file)) } : { dir, name, size: null, sha256: null });
|
|
260
|
+
out.push(info?.isFile?.() ? { dir, path: rel, name, size: info.size, sha256: sha256(readFile(file)) } : { dir, path: rel, name, size: null, sha256: null });
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
return out;
|
|
@@ -272,6 +272,16 @@ function stageSidecarsSpec(value) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/** The triple a `targets.mac.macPackage` spec builds for: `triple`, else this host's arch when `universal: false`, else universal-apple-darwin. */
|
|
276
|
+
export function macPackageTriple(spec) {
|
|
277
|
+
return spec.triple ?? (spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** True when the macPackage spec stages its declared sidecars itself (`stageSidecars: true | { config?, receipt? }`, the recipe's first prepare step). */
|
|
281
|
+
export function macPackageStagesSidecars(spec) {
|
|
282
|
+
return spec.stageSidecars !== undefined && spec.stageSidecars !== false;
|
|
283
|
+
}
|
|
284
|
+
|
|
275
285
|
export function expandMacPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
|
|
276
286
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
277
287
|
const spec = target.macPackage;
|
|
@@ -291,7 +301,7 @@ export function expandMacPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
291
301
|
if (spec.updater !== undefined && spec.updater !== true && spec.updater !== false && typeof spec.updater !== "string") fail("targets.mac.macPackage.updater must be true or an app-relative .tar.gz path");
|
|
292
302
|
if (typeof spec.updater === "string" && !/\.tar\.gz$/i.test(relativeInside(spec.updater, "targets.mac.macPackage.updater"))) fail("targets.mac.macPackage.updater must end in .tar.gz");
|
|
293
303
|
// Tauri's universal-apple-darwin target builds both slices and lipos them itself.
|
|
294
|
-
const triple =
|
|
304
|
+
const triple = macPackageTriple(spec);
|
|
295
305
|
if (spec.triple !== undefined) {
|
|
296
306
|
const wrong = [...MAC_TRIPLES, "universal-apple-darwin"].find((other) => other !== triple && target.notarize.app.replaceAll("\\", "/").split("/").includes(other));
|
|
297
307
|
if (wrong) fail(`targets.mac.macPackage.triple is ${triple} but notarize.app is under ${wrong}; the built .app lives in the ${triple} directory`);
|
|
@@ -301,7 +311,7 @@ export function expandMacPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
301
311
|
const sidecars = stageSidecarsSpec(spec.stageSidecars);
|
|
302
312
|
if (sidecars) {
|
|
303
313
|
// First prepare step: stage the declared sidecars for this triple (universal: both slices, lipo, <name>-universal-apple-darwin).
|
|
304
|
-
extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
|
|
314
|
+
extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, "--if-stale", ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
|
|
305
315
|
}
|
|
306
316
|
let helpers;
|
|
307
317
|
if (spec.nativeHelpers !== undefined) {
|
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.22",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|
package/snapshot-broker.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { removeCarrier, sweepSnapshotRequests } from "./carrier-census.mjs";
|
|
|
27
27
|
import { DEFAULT_RETRY_WINDOW_MS, RETRYABLE_OUTCOME_CODES, clientGuardMs, diagnoseControlReply, drainFrames, operationDeadlineEnv, operationOutcomeInOutput, overCapProblem, refusalInCapturedOutput, retryPauseMs, sleepSync } from "./broker-wire.mjs";
|
|
28
28
|
import { OWNERSHIP_ENV, OWNERSHIP_FILE_ENV, markCarriedSend, markCarriedUnsent } from "./ownership-mark.mjs";
|
|
29
29
|
import {
|
|
30
|
-
MANAGED_RUST_ENVIRONMENT_KEYS, SNAPSHOT_REQUEST_SCHEMA, SnapshotRequestError, argvSha256, isCallHandleOf, lockedArgv, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
|
|
30
|
+
MANAGED_RUST_ENVIRONMENT_KEYS, SNAPSHOT_REQUEST_SCHEMA, SnapshotRequestError, argvSha256, declaredInputDigestsOf, isCallHandleOf, lockedArgv, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
|
|
31
31
|
} from "./snapshot-request.mjs";
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -424,6 +424,13 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
|
|
|
424
424
|
const seen = operationOutcomeInOutput(`${out.stdout}\n${out.stderr}`) ?? (r.error?.code === "ETIMEDOUT" ? { code: "operation-deadline", deadlineMs: null, requestId: ref?.requestId ?? null, cancelConfirmed: null, terminalState: null, guard: true } : null);
|
|
425
425
|
if (seen) outcome = { status: "unverified", code: seen.code, message: seen.code === "operation-deadline" ? `the operation deadline${seen.deadlineMs ? ` of ${seen.deadlineMs} ms` : ""} passed${seen.guard ? " and the client was stopped by the gate's outer guard" : ""}; request ${seen.requestId ?? ref?.requestId ?? "unknown"}: cancelConfirmed: ${seen.cancelConfirmed ?? "unknown"}, terminal state: ${seen.terminalState ?? "not observed"}; the job is neither abandoned nor resubmitted and ownership stays UNKNOWN until every sub-handle is REAPED` : seen.code === "broker-busy" ? "every broker connection was busy (RK_BROKER_BUSY: a wait, not an outage); nothing was admitted" : "the broker was unavailable (RK_BROKER_UNAVAILABLE)", ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
|
|
426
426
|
}
|
|
427
|
+
// The client's own account of a call that never got a request reference (it refused the carrier, or the broker refused the request before
|
|
428
|
+
// accepting it): its `rustright: <reason>` lines, which capture mode would otherwise drop (HeardRight Mac package 2026-11-02: exit 125 with
|
|
429
|
+
// no reason in the gate log). A bare `rustright: request <id>` line is not a reason.
|
|
430
|
+
if (outcome.status === "unverified" && capture && ref === null) {
|
|
431
|
+
const said = out.stderr.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^rustright(?:[: ]|-)/.test(line) && !/^rustright: request [0-9a-f-]+$/.test(line));
|
|
432
|
+
if (said.length) outcome = { ...outcome, message: `${outcome.message}; the RustRight client said (exit ${r.status ?? "none"}): ${said.slice(-3).join(" | ").slice(0, 600)}` };
|
|
433
|
+
}
|
|
427
434
|
if (outcome.status === "unverified" && outcome.code === "operation-deadline") {
|
|
428
435
|
const settled = reconcileAfterCancel({ section, callHandle: made.carrier.ownership.handle, childEnv });
|
|
429
436
|
outcome = { ...outcome, message: `${outcome.message}; ${settled.note}`, reaped: settled.reaped, subHandle: settled.subHandle };
|
|
@@ -691,9 +698,26 @@ export const runBrokerCargo = (call) => carryBrokerCargo(call).status;
|
|
|
691
698
|
* again now, verifies against THIS gate's commit, snapshot, ownership handle and pins, and its carrier was used once: a nonce or a
|
|
692
699
|
* request id that appears twice in the run (`seen`) is a replay. Returns the problems; none means every call verified.
|
|
693
700
|
*/
|
|
694
|
-
export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set(), handles: new Set() } } = {}) {
|
|
701
|
+
export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set(), handles: new Set() }, openInputDigests = null } = {}) {
|
|
695
702
|
seen.handles ??= new Set();
|
|
696
703
|
const problems = [];
|
|
704
|
+
// `openInputDigests` is supplied ONLY by a producer carrier (the gate's own web-build, windows-identity and stage-sidecar steps, which write declared inputs by design):
|
|
705
|
+
// the inputs whose digest at stage open (the gate's own, allowAbsent) differs from the digest at stage end, computed once, are then "produced" and accept any well-formed
|
|
706
|
+
// receipt entry. Without it (every other carrier) nothing is produced: every receipt, cargo AND adapter, must equal the stage-end digest input by input, so an input a child
|
|
707
|
+
// swapped around an admission and restored is refused. closeSnapshotStage's attribution check is what makes "produced" safe. A failure to digest ends the verification with that problem.
|
|
708
|
+
let inputPolicy = null;
|
|
709
|
+
const ledgered = entries.some((e) => e.mode === "broker" && e.status === "verified");
|
|
710
|
+
if (ledgered) {
|
|
711
|
+
try {
|
|
712
|
+
const end = declaredInputDigestsOf(sb.snapshot.path, readDeclaredInputs(sb.snapshot.path), { platform: sb.platform, allowAbsent: true });
|
|
713
|
+
const produced = new Set();
|
|
714
|
+
if (Array.isArray(openInputDigests)) {
|
|
715
|
+
if (openInputDigests.length !== end.length) throw new SnapshotRequestError("ledger", "the stage-open digest of the declared inputs does not match the declaration");
|
|
716
|
+
end.forEach((d, i) => { if (openInputDigests[i]?.path !== d.path) throw new SnapshotRequestError("ledger", "the stage-open digest of the declared inputs is not in declaration order"); if (canonicalJson(openInputDigests[i]) !== canonicalJson(d)) produced.add(d.path); });
|
|
717
|
+
}
|
|
718
|
+
inputPolicy = { produced, current: end };
|
|
719
|
+
} catch (error) { return [`the gate cannot digest the snapshot's declared inputs at the end of the stage (${error.code ?? "error"}: ${error.message})`]; }
|
|
720
|
+
}
|
|
697
721
|
for (const e of entries) {
|
|
698
722
|
if (e.mode !== "broker" || e.status !== "verified") continue;
|
|
699
723
|
const adapter = typeof e.adapter === "string" && e.adapter ? e.adapter : null;
|
|
@@ -726,7 +750,7 @@ export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set
|
|
|
726
750
|
ownership: { handle: e.ownershipHandle }, primaryCwd: e.checkoutCwd, argvSha256: argvSha256(e.argv), nonce: e.nonce,
|
|
727
751
|
};
|
|
728
752
|
const receipt = await sb.receiptOf(e.requestId);
|
|
729
|
-
const verified = verifySnapshotReceipt(receipt, { pins: sb.pins, jobId: e.requestId, carrier, argv: e.argv, workspace: e.workspace, observedEnv: e.observedEnv ?? {}, adapter, adapterEnv: e.adapterEnv ?? {}, checkout: sb.checkout, outputRoots: sb.outputRoots, platform: sb.platform });
|
|
753
|
+
const verified = verifySnapshotReceipt(receipt, { pins: sb.pins, jobId: e.requestId, carrier, argv: e.argv, workspace: e.workspace, observedEnv: e.observedEnv ?? {}, adapter, adapterEnv: e.adapterEnv ?? {}, checkout: sb.checkout, outputRoots: sb.outputRoots, platform: sb.platform, inputPolicy });
|
|
730
754
|
if (adapter !== null) {
|
|
731
755
|
// The children the ledger records (the bundle root and the stage's output directories are read from them) are the signed ones, and no more.
|
|
732
756
|
if (canonicalJson(e.nestedChildren ?? null) !== canonicalJson(verified.nestedChildren)) throw new SnapshotRequestError("nested", "the nested children the call recorded are not the ones the broker's receipt signs");
|
package/snapshot-check.mjs
CHANGED
|
@@ -305,7 +305,10 @@ export function installSnapshotDependencies({ checkout, snapshot, commit, tree,
|
|
|
305
305
|
const ms = now() - started;
|
|
306
306
|
if (failed(r) || /No projects matched the filters/.test(`${r?.stdout ?? ""}\n${r?.stderr ?? ""}`)) {
|
|
307
307
|
const out = `${r?.stdout ?? ""}\n${r?.stderr ?? ""}`;
|
|
308
|
-
|
|
308
|
+
// pnpm 12 prints the code on its own line and wraps the tarball URL ("from https://\n registry.npmjs.org/..."): the URL is rejoined first,
|
|
309
|
+
// and the code and its URL may be a few lines apart (Dell, 2026-11-02: the specific message never fired; only progress lines showed).
|
|
310
|
+
const joined = out.replace(/(https?:\/\/)\s+/g, "$1");
|
|
311
|
+
const miss = /ERR_PNPM_NO_OFFLINE_TARBALL[\s\S]{0,600}?(https?:\/\/\S+?\/-\/(\S+?)\.tgz)/.exec(joined);
|
|
309
312
|
const message = miss
|
|
310
313
|
? `root ${root}: ${miss[2]} is missing from the store ${storeDir} (offline install, no network fallback); install the checkout's dependencies so the store holds it, then run the check again`
|
|
311
314
|
: `root ${root}: offline dependency install failed (store ${storeDir}): ${tail(r) || r?.error?.message || `exit ${r?.status}`}`;
|
package/snapshot-request.mjs
CHANGED
|
@@ -356,9 +356,9 @@ export function assertSuppliedInputs(snapshotRoot, supplied) {
|
|
|
356
356
|
* anywhere, a special file, or a limit breach is a refusal. The name is the input's path components joined by `/` on EVERY platform (GB-49: RustRight hashes
|
|
357
357
|
* `/` on Windows too, so a `\` in a declaration or a platform separator never reaches the hash), UTF-8, no Unicode normalisation.
|
|
358
358
|
*
|
|
359
|
-
* `allowAbsent`
|
|
360
|
-
*
|
|
361
|
-
*
|
|
359
|
+
* `allowAbsent` makes a declared input that does not exist in the snapshot (ENOENT/ENOTDIR on a component; a link is still refused) the entry
|
|
360
|
+
* `{ path, absent: true }` (no `sha256`, 0 files, 0 bytes), the wire form RustRight signs. The broker admits every request with an absent input
|
|
361
|
+
* (a compile is refused at exit only if an input CHANGED while it ran, rustright d378d32), so the receipt check digests with it for every class.
|
|
362
362
|
*/
|
|
363
363
|
export function declaredInputDigestsOf(snapshotRoot, inputs, { allowAbsent = false } = {}) {
|
|
364
364
|
let files = 0; let bytes = 0;
|
|
@@ -528,7 +528,8 @@ export function verifyNestedChildren(evidence, receipt, { snapshotPath, checkout
|
|
|
528
528
|
* Beyond the signature, the job, the nonce and the request digest, the receipt must carry (item 7 row 7.22):
|
|
529
529
|
* - signed SUCCEEDED with exit 0, and signed snapshotEvidence equal to the receipt's copies (commit, ownership handle, request id,
|
|
530
530
|
* snapshot path, `treeAtStart` = `treeAtExit` = the expected tree);
|
|
531
|
-
* - `declaredInputDigests`
|
|
531
|
+
* - `declaredInputDigests` well formed (the declaration's paths in order, each `{path, sha256}` or `{path, absent:true}`), and for a cargo request equal to the gate's own
|
|
532
|
+
* digest of the snapshot's declared inputs read now (absent included; an adapter request skips the comparison, it may produce an input); `inputPolicy` replaces that digest (see below);
|
|
532
533
|
* - `outputDir` absolute, outside the snapshot and the checkout, and a managed generation target under one of `outputRoots`;
|
|
533
534
|
* - signed stageEvidence (freshness, attribution, toolchain) equal to the receipt's;
|
|
534
535
|
* - for a compile, freshness.causeEvidence (rustright.cause-evidence.v1) coherent with its census and with a verdict of `explained`: refused as `evidence` when missing,
|
|
@@ -536,7 +537,7 @@ export function verifyNestedChildren(evidence, receipt, { snapshotPath, checkout
|
|
|
536
537
|
* - timingsMs, exit, diagnosticsPath, brokerVersion and executor present with their types; the retired `verdict` and `refreshed` are refused (GB-30: Class A and its verdict reuse are the gate's).
|
|
537
538
|
* `checkout` and `outputRoots` are required: without them nothing can be said about where the outputs went, so it refuses.
|
|
538
539
|
*/
|
|
539
|
-
export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, workspace, observedEnv = {}, adapter = null, adapterEnv = {}, checkout, outputRoots, platform = process.platform, canonicalPath = (p) => fs.realpathSync.native(p), digestInputs = declaredInputDigestsOf, readInputs = readDeclaredInputs }) {
|
|
540
|
+
export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, workspace, observedEnv = {}, adapter = null, adapterEnv = {}, checkout, outputRoots, platform = process.platform, canonicalPath = (p) => fs.realpathSync.native(p), digestInputs = declaredInputDigestsOf, readInputs = readDeclaredInputs, inputPolicy = null }) {
|
|
540
541
|
const requestDigest = requestDigestOf(snapshotRequestCore({ argv, workspace, observedEnv, carrier, adapter, adapterEnv }));
|
|
541
542
|
const payload = verifyProvenance(receipt, pins, { jobId, nonce: carrier.nonce, requestDigest });
|
|
542
543
|
// The signed outcome, not the client's exit code: a passing row needs the broker to have signed SUCCEEDED with exit 0 and no
|
|
@@ -563,21 +564,36 @@ export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, wor
|
|
|
563
564
|
commit: carrier.commit, tree: carrier.snapshot.expectedTree, jobId, handle: carrier.ownership.handle, snapshotPath: carrier.snapshot.path,
|
|
564
565
|
samePath: (a, b) => { try { return canonicalPath(a) === canonicalPath(b); } catch { return false; } },
|
|
565
566
|
});
|
|
566
|
-
// Declared inputs
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
567
|
+
// Declared inputs. The broker admits every snapshot request with a declared input absent and records it as `{ path, absent: true }` in the signed
|
|
568
|
+
// snapshotEvidence.declaredInputDigests (declaration order); a compile is refused at exit if any declared input changed while it ran, an absent one
|
|
569
|
+
// appearing included (rustright d378d32). So an absent entry is evidence on any request, and the receipt is judged per class:
|
|
570
|
+
// - a cargo request (adapter === null): the receipt's digests equal the gate's current digest of the snapshot's inputs (the child copies the next
|
|
571
|
+
// file only after the call returns, and the broker refused a compile whose inputs changed during it);
|
|
572
|
+
// - an adapter request: the digest comparison is skipped (the job may be the producer of an input), but the receipt's entries must be well formed,
|
|
573
|
+
// the declaration's paths in declaration order, each exactly `{ path, sha256 }` or exactly `{ path, absent: true }`;
|
|
574
|
+
// - with `inputPolicy` ({ produced: Set<path>, current: digest array }) the gate's own digest is replaced: an input whose path is in `produced` accepts any
|
|
575
|
+
// well-formed entry (absent or any sha256); every other input must equal `current`'s entry for that path (the swap-around-admission refusal).
|
|
576
|
+
let gateInputs; let gateDigests = null;
|
|
575
577
|
try {
|
|
576
578
|
gateInputs = readInputs(carrier.snapshot.path);
|
|
577
|
-
gateDigests = digestInputs(carrier.snapshot.path, gateInputs, { platform, allowAbsent:
|
|
579
|
+
if (!inputPolicy && adapter === null) gateDigests = digestInputs(carrier.snapshot.path, gateInputs, { platform, allowAbsent: true });
|
|
578
580
|
} catch (e) { fail("evidence", `the gate cannot digest the snapshot's declared inputs (${e.message})`); }
|
|
579
581
|
if (!same(gateInputs, carrier.declaredInputs?.inputs)) fail("evidence", "the snapshot's declared inputs are not the ones this request carried (declaredInputs differs from the snapshot declaration)");
|
|
580
|
-
|
|
582
|
+
const signedInputs = evidence.declaredInputDigests;
|
|
583
|
+
const wellFormed = Array.isArray(signedInputs) && signedInputs.length === gateInputs.length && signedInputs.every((d, i) => {
|
|
584
|
+
if (!isObject(d) || d.path !== gateInputs[i]) return false;
|
|
585
|
+
const keys = Object.keys(d).sort().join();
|
|
586
|
+
return (keys === "path,sha256" && typeof d.sha256 === "string" && /^[0-9a-f]{64}$/.test(d.sha256)) || (keys === "absent,path" && d.absent === true);
|
|
587
|
+
});
|
|
588
|
+
if (!wellFormed) fail("evidence", "the signed declaredInputDigests are not the snapshot's declared inputs in declaration order, each exactly { path, sha256 } or { path, absent: true }");
|
|
589
|
+
if (inputPolicy) {
|
|
590
|
+
const { produced, current } = inputPolicy;
|
|
591
|
+
if (!(produced instanceof Set) || !Array.isArray(current) || current.length !== gateInputs.length) fail("evidence", "the gate's input policy is not { produced: Set, current: one digest per declared input }");
|
|
592
|
+
signedInputs.forEach((d, i) => {
|
|
593
|
+
if (produced.has(gateInputs[i])) return;
|
|
594
|
+
if (!same(d, current[i])) fail("evidence", `the signed declaredInputDigests entry for ${gateInputs[i]} is not the gate's own digest of that input at the end of the stage, and the stage did not change it (an input the receipt records absent must be absent, and the reverse)`);
|
|
595
|
+
});
|
|
596
|
+
} else if (adapter === null && !same(signedInputs, gateDigests)) fail("evidence", "the signed declaredInputDigests are not the gate's own digest of the snapshot's declared inputs (an input the receipt records absent must be absent in the snapshot, and the reverse)");
|
|
581
597
|
// Output directory: where the outputs went must be the broker's managed generation target, never under the snapshot or the checkout.
|
|
582
598
|
const out = evidence.outputDir;
|
|
583
599
|
if (typeof out !== "string" || !path.isAbsolute(out) && !path.win32.isAbsolute(out)) fail("evidence", "the signed evidence names no absolute output directory");
|
package/stage-ledger.mjs
CHANGED
|
@@ -137,12 +137,15 @@ export const sameEntries = (x, y) => { const d = diffEntries(x, y); return !d.ad
|
|
|
137
137
|
* The attribution diff of a staging step: every path the step added must be one it reported (or its receipt), and every path it
|
|
138
138
|
* reported must exist. Anything else is an unattributed write. `reported` and `receipt` are snapshot-relative posix paths.
|
|
139
139
|
*/
|
|
140
|
-
export function attributionProblems({ before, after, reported, receipt }) {
|
|
140
|
+
export function attributionProblems({ before, after, reported, receipt, removable = [] }) {
|
|
141
141
|
const d = diffEntries(before, after);
|
|
142
142
|
const allowed = new Set([...reported, ...(receipt ? [receipt] : [])]);
|
|
143
|
+
// A step re-staging another target may replace a file an earlier staging step staged (a CHANGED path it reports is allowed above) and may remove one (a path in `removable`,
|
|
144
|
+
// the staged sidecar outputs of the step's own earlier staging, which a `wipeStageDir` re-stage deletes); nothing else may be removed.
|
|
145
|
+
const removableSet = new Set(removable);
|
|
143
146
|
const problems = [];
|
|
144
147
|
for (const p of [...d.added, ...d.changed]) if (!allowed.has(p)) problems.push(`${p} was written by the staging step but it did not report it`);
|
|
145
|
-
for (const p of d.removed) problems.push(`${p} was removed by the staging step`);
|
|
148
|
+
for (const p of d.removed) if (!removableSet.has(p)) problems.push(`${p} was removed by the staging step`);
|
|
146
149
|
const present = new Set(after.map((e) => e.path));
|
|
147
150
|
for (const p of allowed) if (!present.has(p)) problems.push(`${p} was reported by the staging step but is not in the snapshot as a ledger entry`);
|
|
148
151
|
return problems;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `right-release stage-sidecar [--config right-release.config.json] [--target <triple
|
|
2
|
+
// `right-release stage-sidecar [--config right-release.config.json] [--target <triple>|package] [--if-stale] [--check] [--json]` (bogusyogi/rightkit#345):
|
|
3
3
|
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming. `--check` validates
|
|
4
4
|
// the manifest and prints the plan without building, downloading, copying or deleting anything (for template gates).
|
|
5
5
|
import path from "node:path";
|
|
@@ -9,7 +9,7 @@ import { planSidecarStaging, resolveTargetTriple, stageSidecars } from "./stage-
|
|
|
9
9
|
import { readSourceSplit } from "./source-split.mjs";
|
|
10
10
|
|
|
11
11
|
export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
12
|
-
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false };
|
|
12
|
+
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false, ifStale: false };
|
|
13
13
|
for (let i = 0; i < argv.length; i += 1) {
|
|
14
14
|
const arg = argv[i];
|
|
15
15
|
if (arg === "--config" || arg === "--target") {
|
|
@@ -19,6 +19,7 @@ export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
|
19
19
|
i += 1;
|
|
20
20
|
} else if (arg === "--json") opts.json = true;
|
|
21
21
|
else if (arg === "--check") opts.check = true;
|
|
22
|
+
else if (arg === "--if-stale") opts.ifStale = true;
|
|
22
23
|
else throw new Error(`unknown stage-sidecar argument: ${arg}`);
|
|
23
24
|
}
|
|
24
25
|
return opts;
|
|
@@ -57,15 +58,23 @@ async function main() {
|
|
|
57
58
|
const config = await importReleaseConfig(configPath, { lenient: true });
|
|
58
59
|
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
59
60
|
if (opts.check) {
|
|
60
|
-
const target = opts.target ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
61
|
+
const target = (opts.target === "package" ? undefined : opts.target) ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
61
62
|
const plan = planSidecarStaging({ manifest: config.sidecars, target });
|
|
62
63
|
if (opts.json) console.log(JSON.stringify(plan, null, 2));
|
|
63
64
|
else for (const line of formatSidecarPlan(plan)) console.log(line);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
// `--target package` (the gate's package stage): the triple the config's Mac package recipe stages for (its first prepare step's own
|
|
68
|
+
// --target), decided here in the child that already loaded the config, so the gate never imports app code; otherwise the host's.
|
|
69
|
+
let wanted = opts.target;
|
|
70
|
+
if (wanted === "package") {
|
|
71
|
+
const { macPackageStagesSidecars, macPackageTriple } = await import("./package-recipe-targets.mjs");
|
|
72
|
+
const mac = process.platform === "darwin" ? config?.targets?.mac?.macPackage : undefined;
|
|
73
|
+
wanted = mac && macPackageStagesSidecars(mac) ? macPackageTriple(mac) : undefined;
|
|
74
|
+
}
|
|
75
|
+
const target = resolveTargetTriple({ target: wanted, cwd: appRoot });
|
|
67
76
|
// Warnings (a never-run row, a catalog row that is not staged) go to stderr; stdout stays the staged-file list or the --json document.
|
|
68
|
-
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, ...(split ? { cacheRoot: split.checkoutAppRoot } : {}), deps: { warn: (line) => console.error(line) } });
|
|
77
|
+
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, ifStale: opts.ifStale, ...(split ? { cacheRoot: split.checkoutAppRoot } : {}), deps: { warn: (line) => console.error(line) } });
|
|
69
78
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
70
79
|
else {
|
|
71
80
|
for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
|
|
@@ -74,5 +83,11 @@ async function main() {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
77
|
-
main().catch((error) => {
|
|
86
|
+
main().catch((error) => {
|
|
87
|
+
console.error(`right-release stage-sidecar: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
88
|
+
// The failed tool's own last words (a captured rustright or cargo stderr), which the message alone does not carry.
|
|
89
|
+
const tail = String(error.details?.stderr ?? "").trim().split(/\r?\n/).slice(-8).join("\n");
|
|
90
|
+
if (tail) console.error(`right-release stage-sidecar: its stderr ended:\n${tail}`);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
});
|
|
78
93
|
}
|
package/stage-sidecar.mjs
CHANGED
|
@@ -1936,12 +1936,11 @@ export function planSidecarStaging({ manifest, target, env = process.env } = {})
|
|
|
1936
1936
|
* fs (existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, chmodSync, statSync, readdirSync, rmSync), run, fetch (https downloads of `remote`), fetchBytes, extractMember, loadMediaCatalog, loadNativeCatalog, makeTempDir, removeDir, now, pid, log (progress), warn (warnings; defaults to log), env.
|
|
1937
1937
|
* The API is quiet by default; the `right-release stage-sidecar` command passes `warn` so the CLI prints warnings on stderr.
|
|
1938
1938
|
*/
|
|
1939
|
-
export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps = {} } = {}) {
|
|
1939
|
+
export async function stageSidecars({ appRoot, manifest, target, cacheRoot, ifStale = false, deps = {} } = {}) {
|
|
1940
1940
|
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("STAGE_SIDECAR_CONFIG_INVALID", "appRoot must be an absolute path");
|
|
1941
1941
|
if (cacheRoot !== undefined && (typeof cacheRoot !== "string" || !path.isAbsolute(cacheRoot))) fail("STAGE_SIDECAR_CONFIG_INVALID", "cacheRoot must be an absolute path");
|
|
1942
1942
|
const env = deps.env ?? process.env;
|
|
1943
1943
|
const plan = validateSidecarManifest(manifest, { target, mode: resolveBuildMode(env) });
|
|
1944
|
-
assertNotNested(plan, env);
|
|
1945
1944
|
// A source split (source-split.mjs; rightkit-gate's snapshot route): `appRoot` is the snapshot of the commit, where the declared
|
|
1946
1945
|
// sources are read and where the staged files and the receipt go (the Tauri build script reads them there). `cacheRoot` is the
|
|
1947
1946
|
// checkout's app directory, which keeps every cache, archive, slice and build tree right-release owns (`.right-release/...`) and
|
|
@@ -1969,6 +1968,14 @@ export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps
|
|
|
1969
1968
|
makeTempDir: deps.makeTempDir ?? (() => nodeFs.mkdtempSync(path.join(os.tmpdir(), "rightkit-stage-"))),
|
|
1970
1969
|
removeDir: deps.removeDir ?? ((dir) => nodeFs.rmSync(dir, { recursive: true, force: true })),
|
|
1971
1970
|
};
|
|
1971
|
+
// Already staged: when the receipt at the plan's receipt path is for this manifest and target and every file it names is on disk with its recorded sha256, nothing is built,
|
|
1972
|
+
// downloaded, copied or written (the gate staged the package triple first, rightkit-gate stageSidecarsOnce, so a recipe's own prepare step changes nothing). Judged before the
|
|
1973
|
+
// nested-broker refusal, which guards only a step that would build.
|
|
1974
|
+
// Opt-in (`ifStale`, `stage-sidecar --if-stale`): only a Mac package recipe's own prepare step asks for it. The check is structural (no source hash), so every
|
|
1975
|
+
// other caller, the gate's staging step included, builds as before.
|
|
1976
|
+
const current = ifStale ? currentStageReceipt(plan, ctx) : null;
|
|
1977
|
+
if (current) return { receipt: current, receiptPath: path.resolve(appRoot, plan.receiptPath), files: current.files, externalBinNames: current.files.filter((file) => file.externalBin).map((file) => file.name), unchanged: true };
|
|
1978
|
+
assertNotNested(plan, env);
|
|
1972
1979
|
await claimNativeOutputs(plan, ctx);
|
|
1973
1980
|
const files = [];
|
|
1974
1981
|
const skipped = [];
|
|
@@ -2009,6 +2016,40 @@ export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps
|
|
|
2009
2016
|
return { receipt, receiptPath: receiptFile, files, externalBinNames: files.filter((file) => file.externalBin).map((file) => file.name) };
|
|
2010
2017
|
}
|
|
2011
2018
|
|
|
2019
|
+
/**
|
|
2020
|
+
* The receipt at the plan's receipt path when staging is already current, else null: a valid receipt of this target whose skipped rows are the plan's, that has a file of the
|
|
2021
|
+
* same kind and name for every named row the plan stages, and whose every file is on disk, a regular file, with its recorded sha256 (and, for a plan that wipes its stage
|
|
2022
|
+
* directory, nothing else in that directory). A receipt that cannot be read or does not match is not current; nothing is ever written.
|
|
2023
|
+
*/
|
|
2024
|
+
function currentStageReceipt(plan, ctx) {
|
|
2025
|
+
const { fs, appRoot } = ctx;
|
|
2026
|
+
try {
|
|
2027
|
+
const receiptFile = path.resolve(appRoot, plan.receiptPath);
|
|
2028
|
+
const receipt = validateStageSidecarReceipt(JSON.parse(fs.readFileSync(receiptFile, "utf8")), { target: plan.target.triple });
|
|
2029
|
+
const planned = plan.entries.filter((entry) => !entry.skipped);
|
|
2030
|
+
const skipped = plan.entries.filter((entry) => entry.skipped).map((entry) => ({ kind: entry.kind, index: entry.index, platforms: entry.platforms }));
|
|
2031
|
+
if (JSON.stringify(skipped) !== JSON.stringify(receipt.skipped ?? [])) return null;
|
|
2032
|
+
if (receipt.files.length < planned.length) return null;
|
|
2033
|
+
for (const entry of planned) if (typeof entry.name === "string" && ["cargo-bin", "command", "cmake", "prebuilt", "remote"].includes(entry.kind) && !receipt.files.some((file) => file.kind === entry.kind && file.name === entry.name)) return null;
|
|
2034
|
+
const named = new Set();
|
|
2035
|
+
for (const file of receipt.files) {
|
|
2036
|
+
const full = path.resolve(appRoot, file.path);
|
|
2037
|
+
const meta = fs.statSync(full);
|
|
2038
|
+
if (!meta.isFile() || sha256Of(fs.readFileSync(full)) !== file.sha256) return null;
|
|
2039
|
+
named.add(full.toLowerCase());
|
|
2040
|
+
}
|
|
2041
|
+
if (plan.wipeStageDir) {
|
|
2042
|
+
named.add(receiptFile.toLowerCase());
|
|
2043
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).every((item) => {
|
|
2044
|
+
const full = path.join(dir, item.name);
|
|
2045
|
+
return item.isDirectory() ? walk(full) : named.has(path.resolve(full).toLowerCase());
|
|
2046
|
+
});
|
|
2047
|
+
if (!walk(path.resolve(appRoot, plan.stageDir))) return null;
|
|
2048
|
+
}
|
|
2049
|
+
return receipt;
|
|
2050
|
+
} catch { return null; }
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2012
2053
|
/** Structural check of a staging receipt; `expected` may pin target and names. */
|
|
2013
2054
|
export function validateStageSidecarReceipt(receipt, expected = {}) {
|
|
2014
2055
|
const bad = (message) => fail("STAGE_SIDECAR_RECEIPT_INVALID", `stage sidecar receipt: ${message}`);
|