@rightkit/release 0.6.8 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/unit-gate.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// RightKit's own units use the same door, but never reuse mutable unit receipts as evidence.
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { localBuildGranted } from "./execution-policy.mjs";
|
|
7
|
+
import { readCargoPackage } from "./crate-graph.mjs";
|
|
8
|
+
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
9
|
+
import { endRun, startRun, stagesTo } from "./gate-stages.mjs";
|
|
10
|
+
import { qualificationNonce } from "./provenance-verify.mjs";
|
|
11
|
+
import { assignJobs, describeCommand, expectedStageCommands } from "./stage-commands.mjs";
|
|
12
|
+
|
|
13
|
+
const GH = process.platform === "win32" ? "C:\\Program Files\\GitHub CLI\\gh.exe" : "/opt/homebrew/bin/gh";
|
|
14
|
+
const defaultRun = (cmd, args, options = {}) => nodeSpawnSync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...options });
|
|
15
|
+
const defaultReadJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The gate-owned release of one RightKit unit. Runs only on a broker-managed host and only after this process's check, compile,
|
|
19
|
+
* package and qualify stages: (1) the unit's local pre-flight through the broker (rk-pipeline preflight: the compile rows the dispatch
|
|
20
|
+
* adapter admits), (2) the broker-admitted dispatch of publish.yml (rk-pipeline dispatch -> rustright run rightkit-dispatch; the
|
|
21
|
+
* broker's request id is the run's correlation token), (3) rk-pipeline watch --wait: the exact run (sha, attempt, event, workflow,
|
|
22
|
+
* token), then its publish-direct upload run, then the registry, (4) the upload run's publication receipt (the sealed bytes the
|
|
23
|
+
* registry serves: published && registryVerified) read back against the registry itself. A queued dispatch, a missing upload run,
|
|
24
|
+
* an ambiguous receipt or differing registry bytes is a failure; nothing is retried or re-dispatched here (reconcile rk-pipeline's
|
|
25
|
+
* state by hand). The local bundle's digest is reported beside the hosted one, never equated with it.
|
|
26
|
+
*/
|
|
27
|
+
export const QUALIFICATION_SCHEMA = "rightkit.unit-qualification/1";
|
|
28
|
+
/** The stages whose broker jobs the hosted admission authenticates (their signed receipts ride in the qualification), and how many
|
|
29
|
+
* receipts one stage may carry (every command of the stage needs its own; the dispatch input is capped at 65535 characters in all and
|
|
30
|
+
* each signed envelope is ~3 KB, which writeQualification checks). */
|
|
31
|
+
export const ATTESTED_STAGES = ["compile", "package"];
|
|
32
|
+
export const MAX_STAGE_RECEIPTS = 64;
|
|
33
|
+
const MAX_QUALIFICATION_BYTES = 58_000;
|
|
34
|
+
|
|
35
|
+
/** `rustright status <id>`: the broker's own record of one job, reduced to what a verifier needs (id + signed envelope). A job the
|
|
36
|
+
* broker cannot report, or one without a signed envelope, fails the stage: an unauthenticated qualification is worth nothing. */
|
|
37
|
+
/** The commands each stage must have run for this unit, from the same builders the stages use, over the checkout's own config. */
|
|
38
|
+
export function defaultStageCommands({ unit, root, platform, readJson = defaultReadJson }) {
|
|
39
|
+
const manifest = unit.kind === "npm" ? readJson(path.join(root, "packages", unit.name.slice("@rightkit/".length), "package.json")) : undefined;
|
|
40
|
+
return expectedStageCommands({ kind: unit.kind, unit: unit.name, config: readJson(path.join(root, "ci", "gate.config.json")), manifest, platform });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readBrokerReceipt(id, { run = defaultRun } = {}) {
|
|
44
|
+
const r = run("rustright", ["status", id], { encoding: "utf8", timeout: 60_000 });
|
|
45
|
+
if (r.error || r.status !== 0) throw new Error(`the broker cannot report job ${id} (rustright status failed)`);
|
|
46
|
+
let doc;
|
|
47
|
+
try { doc = JSON.parse(r.stdout); } catch { throw new Error(`the broker's status of job ${id} is not JSON`); }
|
|
48
|
+
const receipt = doc?.receipt ?? doc;
|
|
49
|
+
if (receipt?.id !== id || !receipt.provenance?.payload || !receipt.provenance.signature) throw new Error(`job ${id} has no signed provenance receipt`);
|
|
50
|
+
return { id, provenance: receipt.provenance };
|
|
51
|
+
}
|
|
52
|
+
/** `<bundle>/qualification.json`: the local stages that passed and the one artifact sha256 they bound, for the dispatcher. */
|
|
53
|
+
export function writeQualification({ report, artifact, unit, commit, tree, runtime, executor, write = fs.writeFileSync }) {
|
|
54
|
+
for (const stage of ["check", "compile", "package", "qualify"]) if (report.stages?.[stage]?.ok !== true) throw new Error(`release needs stage ${stage} ok before it runs`);
|
|
55
|
+
if (!artifact?.artifactFile || !/^[0-9a-f]{64}$/.test(artifact.artifactSha256 ?? "")) throw new Error("release needs the qualified artifact");
|
|
56
|
+
const receipt = { schema: QUALIFICATION_SCHEMA, unit: { kind: unit.kind, name: unit.name, version: unit.version }, commit, tree, runtime, executor: executor?.kind ?? null,
|
|
57
|
+
stages: Object.fromEntries(["check", "compile", "package", "qualify"].map((s) => [s, { ...report.stages[s] }])), artifactSha256: artifact.artifactSha256, writtenAt: new Date().toISOString() };
|
|
58
|
+
// The broker's signed receipts for the compile and package stages are what the hosted admission verifies; without them the
|
|
59
|
+
// booleans above are only this process's say-so.
|
|
60
|
+
for (const stage of ATTESTED_STAGES) {
|
|
61
|
+
const list = report.stages[stage].receipts;
|
|
62
|
+
if (!Array.isArray(list) || list.length < 1 || list.length > MAX_STAGE_RECEIPTS) throw new Error(`release needs 1 to ${MAX_STAGE_RECEIPTS} signed broker receipts of the ${stage} stage (found ${Array.isArray(list) ? list.length : "none"})`);
|
|
63
|
+
}
|
|
64
|
+
if (JSON.stringify(receipt).length > MAX_QUALIFICATION_BYTES) throw new Error("the qualification receipt with its broker receipts exceeds the workflow dispatch input limit");
|
|
65
|
+
const file = path.join(path.dirname(artifact.artifactFile), "qualification.json");
|
|
66
|
+
write(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
67
|
+
return file;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const requireBrokerHost = (executor) => { if (executor?.kind !== "broker") throw new Error("unit release runs only on a broker-managed host (the hosted qualification is dispatched by the RustRight rightkit-dispatch adapter); a direct host has no gate-owned release route, so no upload was started"); };
|
|
71
|
+
|
|
72
|
+
export async function releaseUnit({ unit, root, commit, executor, env, artifact, qualification, platform = process.platform, run = defaultRun, readJson = defaultReadJson, out = console.log, scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch", registryBytes = defaultRegistryBytes }) {
|
|
73
|
+
requireBrokerHost(executor);
|
|
74
|
+
if (typeof qualification !== "string" || !fs.existsSync(qualification)) throw new Error("release needs the unit qualification receipt (written by the gate after the qualify stage); nothing is dispatched without it");
|
|
75
|
+
const registry = unit.kind === "npm" ? "npm" : "crates";
|
|
76
|
+
const pipeline = path.join(root, "scripts", "ci", "rk-pipeline.mjs");
|
|
77
|
+
const stateFile = platform === "win32" ? "D:\\scratch\\rk-pipeline\\state.json" : "/Volumes/D/scratch/rk-pipeline/state.json";
|
|
78
|
+
const step = (label, args, { accept = [0], acceptWhen = null } = {}) => {
|
|
79
|
+
out(`rightkit-gate: [release] ${label}`);
|
|
80
|
+
const r = run(process.execPath, [pipeline, ...args], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
81
|
+
const accepted = r.status === 0 || (accept.includes(r.status) && (!acceptWhen || acceptWhen(r)));
|
|
82
|
+
if (!accepted) {
|
|
83
|
+
const said = `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`.split("\n").map((l) => l.trim()).filter((l) => l && !l.includes("note:")).slice(-4).join(" | ");
|
|
84
|
+
throw new Error(`${label} failed (exit ${r.status}): ${said.slice(0, 600)}`);
|
|
85
|
+
}
|
|
86
|
+
return r;
|
|
87
|
+
};
|
|
88
|
+
const findDispatch = () => {
|
|
89
|
+
const found = Object.values(readJson(stateFile).dispatches ?? {}).filter((d) => d.unit === unit.name && d.sha === commit && d.registry === registry);
|
|
90
|
+
if (found.length > 1) throw new Error(`${found.length} dispatch records for ${unit.name} at ${commit.slice(0, 12)} in rk-pipeline state; reconcile before any retry`);
|
|
91
|
+
return found[0];
|
|
92
|
+
};
|
|
93
|
+
// The pipeline canary (rk-release-canary: the candidate's `right-release doctor` against every app on both hosts, versus the
|
|
94
|
+
// registry's version) runs before the pre-flight. rk-pipeline exits 2 both for "no pipeline canary for this unit" and for any
|
|
95
|
+
// internal error, so 2 is accepted only when it says the former (reviewer, 2026-10-27); a canary error never reads as no canary.
|
|
96
|
+
step(`pipeline canary of ${unit.name} (doctor on every app, both hosts, candidate vs registry)`, ["canary", unit.name],
|
|
97
|
+
{ accept: [2], acceptWhen: (r) => /no pipeline canary for this unit/.test(`${r.stdout ?? ""}\n${r.stderr ?? ""}`) });
|
|
98
|
+
step(`local pre-flight of ${unit.name} through the broker`, ["preflight", unit.name, "--registry", registry]);
|
|
99
|
+
step(`broker-admitted hosted qualification dispatch of ${unit.name} at ${commit.slice(0, 12)}`, ["dispatch", unit.name, "--registry", registry, "--qualification", qualification]);
|
|
100
|
+
const dispatched = findDispatch();
|
|
101
|
+
if (!dispatched?.runId || !dispatched.runAttempt) throw new Error(`no dispatch record with a run for ${unit.name} at ${commit.slice(0, 12)}; nothing is claimed (reconcile rk-pipeline state before any retry)`);
|
|
102
|
+
step(`waiting for publish.yml run ${dispatched.runId}, its publish-direct upload run and the registry`, ["watch", "--wait"]);
|
|
103
|
+
const done = findDispatch();
|
|
104
|
+
if (done?.status !== "published" || done.version !== unit.version || !done.uploadRunId) throw new Error(`${unit.name}: the watch ended without a published record (${done?.status ?? "none"}${done?.conclusion ? `: ${done.conclusion}` : ""}); nothing is claimed`);
|
|
105
|
+
const receipt = readPublicationReceipt({ uploadRunId: done.uploadRunId, runId: done.runId, runAttempt: done.runAttempt, run, env, scratch });
|
|
106
|
+
// The receipt is written by the publish-direct UPLOAD run (its runId/runAttempt are that run's: upload.mjs records GITHUB_RUN_ID);
|
|
107
|
+
// the qualification run it consumed is its `candidate` (reviewed-candidate.mjs prepareQualified: id qualified-<run>-<attempt>,
|
|
108
|
+
// runId, runAttempt, sourceSha). Both are bound to what rk-pipeline dispatched and watched, plus commit, registry and repository.
|
|
109
|
+
const candidate = receipt.candidate ?? {};
|
|
110
|
+
const bound = String(receipt.runId) === String(done.uploadRunId)
|
|
111
|
+
&& receipt.commit === commit && receipt.registry === registry && receipt.repository === "bogusyogi/rightkit" && receipt.dryRun !== true
|
|
112
|
+
&& candidate.source === "qualified" && String(candidate.runId) === String(done.runId) && String(candidate.runAttempt) === String(done.runAttempt) && candidate.sourceSha === commit;
|
|
113
|
+
if (!bound) {
|
|
114
|
+
throw new Error(`${unit.name}@${unit.version}: the publication receipt of upload run ${done.uploadRunId} is not that run's receipt for qualification run ${done.runId} attempt ${done.runAttempt} of ${commit.slice(0, 12)} (${registry}); nothing is claimed`);
|
|
115
|
+
}
|
|
116
|
+
const rows = (receipt.receipts ?? []).filter((r) => r.unit === unit.name && r.version === unit.version);
|
|
117
|
+
if (rows.length !== 1) throw new Error(`${unit.name}@${unit.version}: the publication receipt holds ${rows.length} row(s) for this unit; exactly one is required`);
|
|
118
|
+
const [row] = rows;
|
|
119
|
+
if (row.published !== true || row.registryVerified !== true || row.outcome !== "published" || !/^[0-9a-f]{64}$/.test(row.sha256 ?? "")) throw new Error(`${unit.name}@${unit.version}: the publication receipt of upload run ${done.uploadRunId} records no verified publication (${row.outcome})`);
|
|
120
|
+
// Read the registry back independently of the upload run: npm's dist.shasum (sha1) and the served tarball's sha256 (the receipt
|
|
121
|
+
// row carries sha256 + sha1, not the sha512 integrity); crates: the sparse index cksum (sha256).
|
|
122
|
+
const served = await registryBytes({ unit, registry, run, env, expected: row, scratch });
|
|
123
|
+
const same = registry === "npm" ? served.shasum === row.sha1 && served.sha256 === row.sha256 : served.cksum === row.sha256;
|
|
124
|
+
if (!same) throw new Error(`${unit.name}@${unit.version}: the registry serves different bytes from the upload receipt (served sha256 ${served.sha256 ?? served.cksum ?? "?"}, receipt ${row.sha256})`);
|
|
125
|
+
return {
|
|
126
|
+
registry, runId: done.runId, runAttempt: done.runAttempt, requestId: done.requestId ?? null, uploadRunId: done.uploadRunId,
|
|
127
|
+
hostedSha256: row.sha256, hostedSha1: row.sha1 ?? null, registryVerified: true,
|
|
128
|
+
localArtifactSha256: artifact?.artifactSha256 ?? null, hostedEqualsLocal: artifact ? artifact.artifactSha256 === row.sha256 : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The one `publication-receipt-*` artifact of the upload run, downloaded and parsed. More or fewer than one is ambiguous. */
|
|
133
|
+
export function readPublicationReceipt({ uploadRunId, runId, runAttempt, run, env, scratch }) {
|
|
134
|
+
const listed = run(GH, ["api", `repos/bogusyogi/rightkit/actions/runs/${uploadRunId}/artifacts`], { env, encoding: "utf8" });
|
|
135
|
+
if (listed.status !== 0) throw new Error(`cannot list the artifacts of upload run ${uploadRunId}`);
|
|
136
|
+
// publish-direct names its receipt `publication-receipt-<qualification run id>-<its attempt>-<upload attempt>`.
|
|
137
|
+
const pattern = new RegExp(`^publication-receipt-${runId}-${runAttempt}-\\d+$`);
|
|
138
|
+
const names = (JSON.parse(listed.stdout).artifacts ?? []).map((a) => a.name).filter((n) => pattern.test(n));
|
|
139
|
+
if (names.length !== 1) throw new Error(`upload run ${uploadRunId} has ${names.length} publication receipt artifact(s) for run ${runId} attempt ${runAttempt}; exactly one is required`);
|
|
140
|
+
const dir = path.join(scratch, "rk-unit-receipts", `${uploadRunId}-${Date.now()}`);
|
|
141
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
const fetched = run(GH, ["run", "download", String(uploadRunId), "-R", "bogusyogi/rightkit", "--name", names[0], "--dir", dir], { env, encoding: "utf8" });
|
|
144
|
+
if (fetched.status !== 0) throw new Error(`cannot download ${names[0]} of upload run ${uploadRunId}`);
|
|
145
|
+
const file = path.join(dir, "publication-receipt.json");
|
|
146
|
+
if (!fs.existsSync(file)) throw new Error(`${names[0]} holds no publication-receipt.json`);
|
|
147
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What the registry serves for the unit's version: npm dist.integrity + dist.shasum; crates the sparse index cksum. */
|
|
151
|
+
export async function defaultRegistryBytes({ unit, registry, run, env, expected, scratch }) {
|
|
152
|
+
if (registry === "npm") {
|
|
153
|
+
const r = run("npm", ["view", `${unit.name}@${unit.version}`, "dist.shasum", "--json", "--prefer-online"], { env, encoding: "utf8" });
|
|
154
|
+
if (r.status !== 0) throw new Error(`npm view ${unit.name}@${unit.version} failed`);
|
|
155
|
+
const shasum = JSON.parse(r.stdout);
|
|
156
|
+
// The served tarball itself, hashed here: `npm pack` downloads exactly what the registry serves for that version.
|
|
157
|
+
const dir = path.join(scratch, "rk-unit-readback", `${unit.name.replace("/", "__")}-${unit.version}-${Date.now()}`);
|
|
158
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
159
|
+
const packed = run("npm", ["pack", `${unit.name}@${unit.version}`, "--pack-destination", dir, "--ignore-scripts"], { env, encoding: "utf8", cwd: dir });
|
|
160
|
+
if (packed.status !== 0) throw new Error(`npm pack ${unit.name}@${unit.version} failed`);
|
|
161
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".tgz"));
|
|
162
|
+
if (files.length !== 1) throw new Error(`npm pack of ${unit.name}@${unit.version} produced ${files.length} tarball(s)`);
|
|
163
|
+
return { shasum: typeof shasum === "string" ? shasum : shasum?.["dist.shasum"], sha256: hash(fs.readFileSync(path.join(dir, files[0]))) };
|
|
164
|
+
}
|
|
165
|
+
// The sparse index must serve exactly the receipt's sha256 (verifyCargoRegistry throws PUBLICATION_REGISTRY_MISMATCH otherwise).
|
|
166
|
+
const { verifyCargoRegistry } = await import("./cargo-registry-readback.mjs");
|
|
167
|
+
await verifyCargoRegistry({ name: unit.name, version: unit.version, sha256: expected.sha256 });
|
|
168
|
+
return { cksum: expected.sha256 };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
const hash = (data) => createHash("sha256").update(data).digest("hex");
|
|
173
|
+
|
|
174
|
+
export function resolveUnit(selector, root, { granted = localBuildGranted, readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8")), readCargo = readCargoPackage } = {}) {
|
|
175
|
+
const match = /^(npm):(@rightkit\/[a-z0-9-]+)$|^(cargo):(rightkit-[a-z0-9-]+)$/.exec(selector ?? "");
|
|
176
|
+
if (!match || !granted(root)) throw new Error("unit needs npm:@rightkit/<name> or cargo:rightkit-<name> in the locally granted RightKit repository");
|
|
177
|
+
const kind = match[1] ?? match[3], name = match[2] ?? match[4];
|
|
178
|
+
const dir = path.join(root, kind === "npm" ? "packages" : "crates", kind === "npm" ? name.slice("@rightkit/".length) : name);
|
|
179
|
+
const manifestFile = path.join(dir, kind === "npm" ? "package.json" : "Cargo.toml");
|
|
180
|
+
if (!fs.realpathSync(manifestFile).startsWith(`${fs.realpathSync(root)}${path.sep}`)) throw new Error("unit manifest escapes the repository");
|
|
181
|
+
const manifest = kind === "npm" ? readJson(path.join(dir, "package.json")) : readCargo(dir);
|
|
182
|
+
if (manifest.name !== name || manifest.private === true || manifest.manifest?.package?.publish === false || typeof manifest.version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version)) throw new Error(`invalid canonical manifest for ${selector}`);
|
|
183
|
+
return { kind, name, version: manifest.version };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `ci/known-red.json` hosts by `process.platform`; any other platform (Linux) has no known-red table. */
|
|
187
|
+
const KNOWN_RED_HOST = { darwin: "macos", win32: "windows" };
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The ci/known-red.json entry that bars this unit from a compile, or null. A unit listed for the host platform is not compiled,
|
|
191
|
+
* packaged or qualified there; one that is headed to release is barred when it is listed for either host (a known-red unit is never
|
|
192
|
+
* published, AGENTS.md; scripts/ci/publish.mjs refuses it the same way). An entry is keyed by the crate name or `rust:<crate>`, the
|
|
193
|
+
* unit's directory or `npm:<directory>` (the ids scripts/ci/gate.mjs and publish.mjs use). A missing file lists nothing; an unreadable
|
|
194
|
+
* one refuses (a table that cannot be read is not an empty table).
|
|
195
|
+
*/
|
|
196
|
+
export function knownRedEntry({ unit, root, platform, toRelease = false, readText = (file) => fs.readFileSync(file, "utf8") }) {
|
|
197
|
+
let table;
|
|
198
|
+
try { table = JSON.parse(readText(path.join(root, "ci", "known-red.json"))); }
|
|
199
|
+
catch (error) { if (error?.code === "ENOENT") return null; throw new Error(`ci/known-red.json cannot be read (${error?.message}); a known-red table that cannot be read is not an empty one`); }
|
|
200
|
+
const dir = unit.kind === "npm" ? `packages/${unit.name.slice("@rightkit/".length)}` : `crates/${unit.name}`;
|
|
201
|
+
const keys = [unit.name, `rust:${unit.name}`, `${unit.kind}:${unit.name}`, dir, `npm:${dir}`];
|
|
202
|
+
const hosts = toRelease ? Object.keys(table ?? {}) : [KNOWN_RED_HOST[platform]].filter(Boolean);
|
|
203
|
+
for (const host of hosts) {
|
|
204
|
+
const key = keys.find((k) => Object.hasOwn(table?.[host] ?? {}, k));
|
|
205
|
+
if (key !== undefined) {
|
|
206
|
+
const reason = table[host][key];
|
|
207
|
+
return { host, key, reason: typeof reason === "string" ? reason : JSON.stringify(reason) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** One invocation reruns all owed stages; reports are never read as admission evidence. */
|
|
214
|
+
/**
|
|
215
|
+
* `source` ({ checkout, commit, tree }) marks a run built from the door's snapshot of that commit (row 7.06a): `root` is then the
|
|
216
|
+
* snapshot, `commonDir` the checkout's, and the stages take their identity from the selection (local-publish.mjs sourceGit). `stageGuard`
|
|
217
|
+
* ({ start(stage), end(stage) } returning true or false) is the door's ledger verify around each stage (7.08); `envExtra` carries the
|
|
218
|
+
* Cargo output pin. Without `source` every behavior is the checkout route's, unchanged.
|
|
219
|
+
*/
|
|
220
|
+
export async function runUnitGate({ selector, to, root, commonDir, commit, tree, executor, env, platform, check, source = null, stageGuard = null, envExtra = {}, loadOwner = () => import("./local-publish.mjs"), out = console.log, err = console.error, releaser = null, resolve = resolveUnit, writeReport, fetchReceipt = readBrokerReceipt, stageCommands = defaultStageCommands, readKnownRed }) {
|
|
221
|
+
const stages = stagesTo(to);
|
|
222
|
+
if (!stages) { err(`rightkit-gate: invalid unit stage ${to}`); return 2; }
|
|
223
|
+
let unit;
|
|
224
|
+
try { unit = resolve(selector, root); } catch (error) { err(`rightkit-gate: BLOCKED: ${error.message}`); return 1; }
|
|
225
|
+
// Before any compile (so --to compile|package|qualify|release; --to check compiles nothing): a unit recorded red is refused, not built.
|
|
226
|
+
if (stages.includes("compile")) {
|
|
227
|
+
try {
|
|
228
|
+
const red = knownRedEntry({ unit, root, platform, toRelease: stages.includes("release"), ...(readKnownRed ? { readText: readKnownRed } : {}) });
|
|
229
|
+
if (red) { err(`rightkit-gate: BLOCKED: ${selector} is listed in ci/known-red.json for ${red.host} (${red.key}: ${red.reason}); a known-red unit is not compiled, packaged, qualified or published. Fix the defect and remove the entry first`); return 1; }
|
|
230
|
+
} catch (error) { err(`rightkit-gate: BLOCKED: ${error.message}`); return 1; }
|
|
231
|
+
}
|
|
232
|
+
const scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch";
|
|
233
|
+
const bundle = path.join(scratch, `rk-unit-${randomBytes(12).toString("hex")}`);
|
|
234
|
+
const argv = ["--local", unit.kind === "npm" ? "--package" : "--crate", unit.name, "--workspace", root, "--qualify-only", "--bundle", bundle];
|
|
235
|
+
const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
236
|
+
const runtimeFiles = [...fs.readdirSync(runtimeDir).filter((name) => name.endsWith(".mjs")).sort().map((name) => path.join(runtimeDir, name)), path.join(runtimeDir, "bin", "rightkit-gate.mjs")];
|
|
237
|
+
const runtime = hash(runtimeFiles.map((file) => `${path.relative(runtimeDir, file)}\0${hash(fs.readFileSync(file))}`).join("\n"));
|
|
238
|
+
const report = { schema: "rightkit.unit-gate-report/1", unit, commit, tree, runtime, executor: executor.kind, stages: {} };
|
|
239
|
+
const save = () => { if (writeReport) writeReport(report); };
|
|
240
|
+
let state, owner, artifact, currentStage, checkTier = null;
|
|
241
|
+
// A run headed to release has its compile and package broker jobs sent with a nonce bound to this commit, tree, unit and stage
|
|
242
|
+
// (RIGHTKIT_ATTEST_NONCE), so each job's signed receipt can be verified hosted for exactly that and nothing else.
|
|
243
|
+
const attested = stages.includes("release") && executor?.kind === "broker";
|
|
244
|
+
const stageEnv = async (stage, action) => {
|
|
245
|
+
const run = startRun({ commonDir, commit, tree, stage });
|
|
246
|
+
const nonce = attested && ATTESTED_STAGES.includes(stage) ? { RIGHTKIT_ATTEST_NONCE: qualificationNonce({ commit, tree, unit: `${unit.kind}:${unit.name}`, stage }) } : {};
|
|
247
|
+
try { return await action({ ...env, ...envExtra, ...nonce, ...run.env }); } finally { endRun(run); }
|
|
248
|
+
};
|
|
249
|
+
const receipts = {};
|
|
250
|
+
const collectReceipts = (stage, ids) => {
|
|
251
|
+
if (!attested || !ATTESTED_STAGES.includes(stage)) return;
|
|
252
|
+
// Every job of the stage is fetched; the stage owes a signed receipt for EVERY command it runs (hosted admission requires the same),
|
|
253
|
+
// so a missing one fails here, before any dispatch, naming it. Jobs that are none of the stage's commands are not carried.
|
|
254
|
+
const fetched = [...new Set(ids ?? [])].map((id) => fetchReceipt(id));
|
|
255
|
+
const { assigned, missing } = assignJobs(stageCommands({ unit, root, platform })[stage], fetched.map((r) => ({ id: r.id, payload: r.provenance.payload })));
|
|
256
|
+
if (missing.length) throw new Error(`the ${stage} stage's ${describeCommand(missing[0])} has no broker job receipt (${missing.length} required command(s) missing)`);
|
|
257
|
+
receipts[stage] = fetched.filter((r) => assigned.has(r.id));
|
|
258
|
+
};
|
|
259
|
+
const verifyArtifact = (result) => {
|
|
260
|
+
if (!result || result.kind !== unit.kind || result.name !== unit.name || result.version !== unit.version || result.commit !== commit || !/^[0-9a-f]{64}$/.test(result.artifactSha256 ?? "") || hash(fs.readFileSync(result.artifactFile)) !== result.artifactSha256) throw new Error("unit artifact identity or digest differs from the completed stage");
|
|
261
|
+
};
|
|
262
|
+
try {
|
|
263
|
+
for (const stage of stages) {
|
|
264
|
+
currentStage = stage;
|
|
265
|
+
if (stageGuard && !(await stageGuard.start(stage))) throw new Error(`RK_CHECK_ERROR: the snapshot is not the commit's bytes plus the ledger before the ${stage} stage`);
|
|
266
|
+
// A unit run headed to qualify or release owes the release tier: it runs here, before the compile, and qualify does not run it again.
|
|
267
|
+
if (stage === "check") { checkTier = stages.includes("qualify") ? "release" : "compile"; if (await check(checkTier) !== 0) throw new Error(`class A check (${checkTier} tier) failed`); }
|
|
268
|
+
else if (stage === "compile") {
|
|
269
|
+
owner = await loadOwner();
|
|
270
|
+
state = await stageEnv(stage, (runEnv) => owner.compileLocalUnit(unit.kind, argv, { env: runEnv, ...(source ? { source } : {}) }));
|
|
271
|
+
collectReceipts(stage, owner.localUnitCompileRequests?.(state));
|
|
272
|
+
} else if (stage === "package") {
|
|
273
|
+
artifact = await stageEnv(stage, (runEnv) => owner.packageLocalUnit(state, { env: runEnv }));
|
|
274
|
+
verifyArtifact(artifact);
|
|
275
|
+
collectReceipts(stage, artifact.brokerRequests);
|
|
276
|
+
} else if (stage === "qualify") {
|
|
277
|
+
if (checkTier !== "release" && await check("release") !== 0) throw new Error("release-tier check failed");
|
|
278
|
+
const qualified = await stageEnv(stage, (runEnv) => owner.qualifyLocalUnit(state, { env: runEnv }));
|
|
279
|
+
verifyArtifact(qualified);
|
|
280
|
+
if (!qualified.qualifiedOnly || !qualified.evidencePath || qualified.artifactSha256 !== artifact.artifactSha256) throw new Error("qualification did not bind the packaged artifact");
|
|
281
|
+
artifact = qualified;
|
|
282
|
+
}
|
|
283
|
+
let released = null;
|
|
284
|
+
if (stage === "release") {
|
|
285
|
+
// The qualification receipt the dispatcher carries to publish.yml: the hosted run is bound to these bytes and stages.
|
|
286
|
+
requireBrokerHost(executor); // a direct host has no broker receipts to carry: refuse for that reason, not for the missing receipts
|
|
287
|
+
const qualification = writeQualification({ report, artifact, unit, commit, tree, runtime, executor });
|
|
288
|
+
released = await stageEnv(stage, (runEnv) => (releaser ?? releaseUnit)({ unit, root, commit, executor, env: runEnv, artifact, qualification, platform, out }));
|
|
289
|
+
}
|
|
290
|
+
if (stageGuard && !(await stageGuard.end(stage))) throw new Error(`RK_CHECK_ERROR: the ${stage} stage's result covers no known bytes (a snapshot byte changed, or a write is outside the ledger)`);
|
|
291
|
+
report.stages[stage] = { ok: true, ...(stage === "package" || stage === "qualify" ? { artifactFile: artifact.artifactFile, artifactSha256: artifact.artifactSha256 } : {}), ...(receipts[stage] ? { receipts: receipts[stage] } : {}), ...(released ?? {}) };
|
|
292
|
+
save();
|
|
293
|
+
out(`rightkit-gate: [${stage}] ${unit.kind}:${unit.name} passed`);
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (currentStage) { report.stages[currentStage] = { ok: false }; save(); }
|
|
298
|
+
err(`rightkit-gate: unit ${selector} stopped: ${error.message}`);
|
|
299
|
+
return 1;
|
|
300
|
+
} finally { if (state && owner) await owner.closeLocalUnit(state); }
|
|
301
|
+
}
|
package/web-build.mjs
CHANGED
|
@@ -12,10 +12,18 @@
|
|
|
12
12
|
//
|
|
13
13
|
// A step is `{ cmd | node, args?, cwd?, env?, nodeFlags?, nodeOptions? }` (see command-spec.mjs). `--dry-run` prints the
|
|
14
14
|
// resolved commands and runs nothing. Exit code: 0, or the failing step's exit code.
|
|
15
|
+
//
|
|
16
|
+
// The door (gate-stages.mjs requireDoorRun) is checked before the config is imported: a .mjs config is code, so even a dry run
|
|
17
|
+
// runs only under a live door run. The exported runner checks it again, so no in-process caller spawns a step around it.
|
|
18
|
+
import path from "node:path";
|
|
15
19
|
import { pathToFileURL } from "node:url";
|
|
16
20
|
import { spawnSync } from "./background-process.mjs";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
17
23
|
import { CommandSpecError, loadAppConfig, resolveInvocation, validateStep } from "./command-spec.mjs";
|
|
18
24
|
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
25
|
+
import { requireDoorRun } from "./gate-stages.mjs";
|
|
26
|
+
import { doorContextFor } from "./source-split.mjs";
|
|
19
27
|
|
|
20
28
|
export function parseWebBuildArgs(argv) {
|
|
21
29
|
const opts = { config: defaultReleaseConfigFile(), dryRun: false };
|
|
@@ -41,23 +49,49 @@ export function planWebBuild(config) {
|
|
|
41
49
|
return steps.map((step, index) => validateStep(step, `web.build[${index}]`));
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The door refusal for a web build of the app at `appRoot`, or null under a live door run. Outside a git repository nothing can be
|
|
54
|
+
* bound to a commit: refused. `spawnSyncFn` runs git; `requireDoor` is gate-stages.mjs requireDoorRun.
|
|
55
|
+
*/
|
|
56
|
+
export function webBuildDoorRefusal({ appRoot, env = process.env, spawnSyncFn = spawnSync, requireDoor = requireDoorRun }) {
|
|
57
|
+
// Inside a source split (the door's snapshot of a commit) the git context is the door run's checkout and commit, not the cwd's.
|
|
58
|
+
let context;
|
|
59
|
+
try { context = doorContextFor({ cwd: appRoot, env, run: spawnSyncFn }); } catch (error) { return `right-release web-build: ${error.code ?? "RK_SOURCE_SPLIT"}: ${error.message}`; }
|
|
60
|
+
if (!context) return "right-release web-build runs only inside a git repository, under a door run";
|
|
61
|
+
return requireDoor({ env, commonDir: context.commonDir, commit: context.commit, command: "right-release web-build" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function doorError(refusal) { const error = new Error(refusal); error.code = "RK_DOOR"; return error; }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run the steps in order under a live door run; `spawnSyncFn`, `requireDoor` and `log` are injectable. Returns the exit code. The
|
|
68
|
+
* door is checked here as well as in main: this is the only place a step is spawned.
|
|
69
|
+
*/
|
|
70
|
+
export function runWebBuild(steps, { appRoot, env = process.env, dryRun = false, spawnSyncFn = spawnSync, requireDoor = requireDoorRun, log = console.log, door = buildLaunch } = {}) {
|
|
71
|
+
const refusal = webBuildDoorRefusal({ appRoot, env, spawnSyncFn, requireDoor });
|
|
72
|
+
if (refusal) throw doorError(refusal);
|
|
46
73
|
for (const step of steps) {
|
|
47
74
|
const invocation = resolveInvocation(step, { appRoot, baseEnv: env });
|
|
48
75
|
log(`${dryRun ? "[dry-run] " : ""}${[invocation.cmd, ...invocation.args].join(" ")}`);
|
|
49
76
|
if (dryRun) continue;
|
|
50
|
-
|
|
77
|
+
// A web build is a build: through the one door (build-launch.mjs), a broker job on a managed host.
|
|
78
|
+
const target = door(invocation.cmd, invocation.args, { env: invocation.env, cwd: invocation.cwd, exists: existsSync });
|
|
79
|
+
const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: invocation.env, stdio: "inherit" });
|
|
51
80
|
if (result.error) throw result.error;
|
|
52
81
|
if (result.status !== 0) return result.status ?? 1;
|
|
53
82
|
}
|
|
54
83
|
return 0;
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
export async function main(argv = process.argv.slice(2)) {
|
|
86
|
+
export async function main(argv = process.argv.slice(2), { env = process.env, requireDoor = requireDoorRun, spawnSyncFn = spawnSync, loadConfig = loadAppConfig, log = console.log } = {}) {
|
|
58
87
|
const opts = parseWebBuildArgs(argv);
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
// A web build is a compile: like right-release build, the direct entry runs only under a door run (rightkit-gate --to compile runs
|
|
89
|
+
// class A first). The door is checked before the config is imported, since a .mjs config is code; a dry run is no exception.
|
|
90
|
+
const appRoot = path.dirname(path.resolve(opts.config));
|
|
91
|
+
const refusal = webBuildDoorRefusal({ appRoot, env, spawnSyncFn, requireDoor });
|
|
92
|
+
if (refusal) throw doorError(refusal);
|
|
93
|
+
const { config } = await loadConfig(opts.config);
|
|
94
|
+
return runWebBuild(planWebBuild(config), { appRoot, env, dryRun: opts.dryRun, spawnSyncFn, requireDoor, log });
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
package/win-package-recipe.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import path from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { spawnSync } from "./background-process.mjs";
|
|
22
22
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
23
|
+
import { resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
|
|
23
24
|
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
24
25
|
import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
25
26
|
|
|
@@ -198,7 +199,9 @@ export function defaultWinPackageDeps({ appRoot, target, env }) {
|
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
/** Where a produced artifact really is: the declared path, else the managed artifact directory (same rule as release.mjs). */
|
|
201
|
-
function locate(declared, { env, fs }) {
|
|
202
|
+
function locate(declared, { env, fs, split = false }) {
|
|
203
|
+
// A door split (row 7.04c, R-T2): the declared path is already the bound target's; there is no artifact-directory fallback for it.
|
|
204
|
+
if (split) return declared;
|
|
202
205
|
if (fs.existsSync(declared)) return declared;
|
|
203
206
|
const managed = env.RIGHT_RELEASE_ARTIFACT_DIR;
|
|
204
207
|
if (managed) {
|
|
@@ -219,6 +222,15 @@ export function runWinPackageRecipe({ appRoot, config = {}, target, deps = {} }
|
|
|
219
222
|
const base = defaultWinPackageDeps({ appRoot: plan.appRoot, target, env });
|
|
220
223
|
const d = { ...base, ...deps, env, fs: { ...base.fs, ...(deps.fs ?? {}) } };
|
|
221
224
|
const { fs } = d;
|
|
225
|
+
// A door split has no target directory in its snapshot: the plan's `<app>/src-tauri/target/...` paths mean the bound target (R-T1, R-T3), where the
|
|
226
|
+
// raw EXE is deleted before the build, signed in place, bundled and verified. Outside a split this is the identity.
|
|
227
|
+
const artifactPaths = d.artifactPaths ?? splitArtifactContext({ cwd: plan.appRoot, env, target });
|
|
228
|
+
const split = artifactPaths.mode === "split";
|
|
229
|
+
const placed = (declared) => {
|
|
230
|
+
if (!split) return declared;
|
|
231
|
+
const relative = path.relative(plan.appRoot, declared).replaceAll("\\", "/");
|
|
232
|
+
return resolveArtifactPath(relative.startsWith("..") || path.isAbsolute(relative) ? declared : relative, artifactPaths);
|
|
233
|
+
};
|
|
222
234
|
const dry = d.dryRun === true;
|
|
223
235
|
const phases = [];
|
|
224
236
|
const phase = (name, work) => {
|
|
@@ -244,15 +256,16 @@ export function runWinPackageRecipe({ appRoot, config = {}, target, deps = {} }
|
|
|
244
256
|
|
|
245
257
|
if (d.platform !== "win32" && !dry) throw new WinPackageRecipeError("build", `the Windows package recipe must run on Windows, not ${d.platform}`);
|
|
246
258
|
|
|
247
|
-
|
|
259
|
+
const rawExe = placed(plan.rawExe);
|
|
260
|
+
let raw = rawExe;
|
|
248
261
|
const sidecars = [...plan.sidecars];
|
|
249
262
|
|
|
250
263
|
phase("build", () => {
|
|
251
|
-
if (!dry) fs.rmSync(
|
|
264
|
+
if (!dry) fs.rmSync(rawExe, { force: true });
|
|
252
265
|
tauri(["build", "--no-bundle", "--target", plan.triple], quiet);
|
|
253
266
|
if (dry) return;
|
|
254
|
-
raw = locate(
|
|
255
|
-
if (!fs.existsSync(raw)) throw new Error(`tauri build did not produce the raw EXE: ${
|
|
267
|
+
raw = locate(rawExe, { env, fs, split });
|
|
268
|
+
if (!fs.existsSync(raw)) throw new Error(`tauri build did not produce the raw EXE: ${rawExe}`);
|
|
256
269
|
for (const sidecar of sidecars) if (!fs.existsSync(sidecar)) throw new Error(`declared sidecar does not exist: ${sidecar}`);
|
|
257
270
|
});
|
|
258
271
|
|
|
@@ -270,14 +283,14 @@ export function runWinPackageRecipe({ appRoot, config = {}, target, deps = {} }
|
|
|
270
283
|
return { sha256: byFile.get(path.resolve(raw).toLowerCase()), sidecars: Object.fromEntries(sidecars.map((file) => [file, byFile.get(path.resolve(file).toLowerCase())])) };
|
|
271
284
|
});
|
|
272
285
|
|
|
273
|
-
const installerBase = plan.installerFile;
|
|
286
|
+
const installerBase = placed(plan.installerFile);
|
|
274
287
|
let installer = installerBase;
|
|
275
288
|
phase("bundle", () => {
|
|
276
289
|
if (!dry) for (const stale of [installerBase, `${installerBase}.sig`, `${installerBase}.zip`, `${installerBase}.zip.sig`]) fs.rmSync(stale, { force: true });
|
|
277
290
|
const override = { bundle: { createUpdaterArtifacts: false, windows: { nsis: { installMode: plan.perMachine ? "perMachine" : "currentUser" } } } };
|
|
278
291
|
tauri(["bundle", "--bundles", "nsis", "--target", plan.triple, "--config", JSON.stringify(override)], quiet);
|
|
279
292
|
if (dry) return;
|
|
280
|
-
installer = locate(installerBase, { env, fs });
|
|
293
|
+
installer = locate(installerBase, { env, fs, split });
|
|
281
294
|
if (!fs.existsSync(installer)) throw new Error(`tauri bundle did not produce the installer: ${installerBase}`);
|
|
282
295
|
});
|
|
283
296
|
|