@rightkit/release 0.6.12 → 0.6.14
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 +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -335
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +15 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/app-closure.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Executing scans follow the selected unit's committed dependency closure. File-only scans stay repository-wide.
|
|
2
|
+
// A Cargo scan (metadata, package list, deny, audit) executes cargo, through the broker on a broker host: it is an executing scan,
|
|
3
|
+
// never a file-only one, and follows the Rust closure (no Rust unit in the closure, no Cargo scan).
|
|
4
|
+
// Unknown local dependency edges refuse; they never silently omit checks or authorize every repository unit.
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { parse } from "smol-toml";
|
|
8
|
+
import { digestScanSet } from "./scan-digest.mjs";
|
|
9
|
+
|
|
10
|
+
const posix = (p) => String(p).replaceAll("\\", "/");
|
|
11
|
+
const within = (dir, base) => base === "." || dir === base || dir.startsWith(`${base}/`);
|
|
12
|
+
const manifestAt = (dir, file) => path.posix.join(dir, file);
|
|
13
|
+
const problem = (message) => new Error(`RK_SCOPE_CLOSURE: ${message}`);
|
|
14
|
+
const tableNames = ["dependencies", "dev-dependencies", "build-dependencies"];
|
|
15
|
+
const dependencyTables = (m) => [m, ...Object.values(m.target ?? {})].flatMap((holder) => tableNames.map((name) => holder?.[name] ?? {}));
|
|
16
|
+
const toml = (source, file = "Cargo.toml") => { try { return parse(String(source)); } catch (e) { throw problem(`${file} cannot be parsed: ${e.message}`); } };
|
|
17
|
+
const localDir = (base, relative) => {
|
|
18
|
+
if (typeof relative !== "string" || !relative || path.posix.isAbsolute(posix(relative)) || /^[A-Za-z]:/.test(relative)) throw problem(`invalid local dependency path ${JSON.stringify(relative)} in ${base}`);
|
|
19
|
+
const dir = path.posix.normalize(path.posix.join(base, posix(relative)));
|
|
20
|
+
if (dir === ".." || dir.startsWith("../")) throw problem(`dependency ${relative} in ${base} leaves the committed repository`);
|
|
21
|
+
return dir;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function manifestDependencyFacts(source) {
|
|
25
|
+
const m = toml(source); const paths = []; let inherits = false;
|
|
26
|
+
for (const table of dependencyTables(m)) for (const spec of Object.values(table)) {
|
|
27
|
+
if (spec && typeof spec === "object") {
|
|
28
|
+
if (spec.path !== undefined) { if (typeof spec.path !== "string") throw problem("dependency path must be a string"); paths.push(spec.path); }
|
|
29
|
+
if (spec.workspace === true) inherits = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { paths, inherits };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Source-only workspace membership for discovery. No Cargo process is needed for unrelated workspaces. */
|
|
36
|
+
export function cargoMetadataFromManifests({ manifests, readFile }) {
|
|
37
|
+
const docs = new Map();
|
|
38
|
+
for (const file of manifests) {
|
|
39
|
+
let source;
|
|
40
|
+
try { source = readFile(file); } catch (e) { throw problem(`${file} cannot be read: ${e.message}`); }
|
|
41
|
+
docs.set(path.posix.dirname(file), toml(source, file));
|
|
42
|
+
}
|
|
43
|
+
const glob = (pattern, value) => {
|
|
44
|
+
let re = "";
|
|
45
|
+
const p = posix(pattern).replace(/^\.\//, "").replace(/\/$/, "");
|
|
46
|
+
for (let i = 0; i < p.length; i += 1) {
|
|
47
|
+
if (p[i] === "*") { if (p[i + 1] === "*") { re += ".*"; i += 1; } else re += "[^/]*"; }
|
|
48
|
+
else if (p[i] === "?") re += "[^/]";
|
|
49
|
+
else re += p[i].replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
50
|
+
}
|
|
51
|
+
return new RegExp(`^${re}$`).test(value);
|
|
52
|
+
};
|
|
53
|
+
const owner = (dir) => {
|
|
54
|
+
const m = docs.get(dir);
|
|
55
|
+
if (m?.workspace) return dir;
|
|
56
|
+
if (typeof m?.package?.workspace === "string") {
|
|
57
|
+
const ws = localDir(dir, m.package.workspace);
|
|
58
|
+
if (!docs.get(ws)?.workspace) throw problem(`${dir}/Cargo.toml names unknown workspace ${ws}`);
|
|
59
|
+
return ws;
|
|
60
|
+
}
|
|
61
|
+
for (let at = path.posix.dirname(dir); at !== dir;) {
|
|
62
|
+
const ws = docs.get(at)?.workspace;
|
|
63
|
+
if (ws) {
|
|
64
|
+
const rel = path.posix.relative(at, dir);
|
|
65
|
+
if (!(ws.exclude ?? []).some((p) => glob(p, rel))) {
|
|
66
|
+
// Cargo automatically includes path dependencies under the workspace root. An exact owner is confirmed by Cargo
|
|
67
|
+
// for every selected workspace before scans; unrelated workspaces need only their source inventory for file-only checks.
|
|
68
|
+
if ((ws.members ?? []).some((p) => glob(p, rel))) return at;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (at === ".") break;
|
|
72
|
+
at = path.posix.dirname(at);
|
|
73
|
+
}
|
|
74
|
+
return dir;
|
|
75
|
+
};
|
|
76
|
+
return (dir) => {
|
|
77
|
+
if (!docs.has(dir)) throw problem(`unknown Cargo manifest ${dir}/Cargo.toml`);
|
|
78
|
+
const cargoDir = owner(dir);
|
|
79
|
+
const members = [...docs].filter(([d, m]) => m.package && owner(d) === cargoDir).map(([d, m]) => {
|
|
80
|
+
if (typeof m.package.name !== "string" || !m.package.name) throw problem(`${d}/Cargo.toml has no package name`);
|
|
81
|
+
return { name: m.package.name, dir: d, manifest: manifestAt(d, "Cargo.toml"), publish: !(Array.isArray(m.package.publish) && m.package.publish.length === 0) };
|
|
82
|
+
});
|
|
83
|
+
return { cargoDir, members };
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** npm/Rust unit names plus local dependency directories. readFile takes a repository-relative committed path. */
|
|
88
|
+
export function closureOf({ scopeDirs, npmUnits = [], rustUnits = [], readFile }) {
|
|
89
|
+
const dirs = [...new Set(scopeDirs.map((d) => posix(d).replace(/^\.\//, "").replace(/\/$/, "") || "."))].sort();
|
|
90
|
+
const npm = new Set(); const rust = new Set(); const dependencyDirs = new Set();
|
|
91
|
+
const read = (dir, file, parser) => {
|
|
92
|
+
const at = manifestAt(dir, file);
|
|
93
|
+
try { return parser(String(readFile(at)).replace(/^\uFEFF/, "")); }
|
|
94
|
+
catch (e) { throw problem(`${at} cannot resolve dependency closure: ${e.message}`); }
|
|
95
|
+
};
|
|
96
|
+
const npmDocs = new Map();
|
|
97
|
+
const npmManifest = (u) => { if (!npmDocs.has(u.dir)) npmDocs.set(u.dir, read(u.dir, "package.json", JSON.parse)); return npmDocs.get(u.dir); };
|
|
98
|
+
const byName = new Map(); const npmByDir = new Map(npmUnits.map((u) => [u.dir, u]));
|
|
99
|
+
for (const u of npmUnits) { const m = npmManifest(u); if (m.name) byName.set(`${u.root ?? "."}\0${m.name}`, u); }
|
|
100
|
+
const stack = npmUnits.filter((u) => dirs.some((d) => within(u.dir, d)));
|
|
101
|
+
for (const u of stack) npm.add(u.name);
|
|
102
|
+
while (stack.length) {
|
|
103
|
+
const u = stack.pop(); dependencyDirs.add(u.dir);
|
|
104
|
+
const m = npmManifest(u);
|
|
105
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) for (const [name, spec] of Object.entries(m[section] ?? {})) {
|
|
106
|
+
if (typeof spec !== "string") throw problem(`${u.dir}/package.json dependency ${name} is not a string`);
|
|
107
|
+
let dep = byName.get(`${u.root ?? "."}\0${name}`);
|
|
108
|
+
if (/^(?:file|link):/.test(spec)) dep = npmByDir.get(localDir(u.dir, spec.slice(spec.indexOf(":") + 1)));
|
|
109
|
+
else if (/^workspace:\.\.?\//.test(spec)) dep = npmByDir.get(localDir(u.dir, spec.slice("workspace:".length)));
|
|
110
|
+
else if (spec.startsWith("workspace:")) {
|
|
111
|
+
const alias = /^workspace:([^@~^*/][^@]*)@/.exec(spec);
|
|
112
|
+
if (alias) dep = byName.get(`${u.root ?? "."}\0${alias[1]}`);
|
|
113
|
+
}
|
|
114
|
+
if (!dep && /^(?:workspace|file|link):/.test(spec)) throw problem(`${u.dir}/package.json dependency ${name} (${spec}) has no committed npm unit`);
|
|
115
|
+
if (dep && !npm.has(dep.name)) { npm.add(dep.name); stack.push(dep); }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const byDir = new Map(rustUnits.map((u) => [u.dir, u]));
|
|
119
|
+
// A local npm dependency can own native Cargo sources too. Its executing checks & output binding share that same closure.
|
|
120
|
+
const rustScopes = [...new Set([...dirs, ...dependencyDirs])];
|
|
121
|
+
const queue = rustUnits.filter((u) => rustScopes.some((d) => within(u.dir, d)));
|
|
122
|
+
for (const u of queue) rust.add(u.name);
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
while (queue.length) {
|
|
125
|
+
const u = queue.pop();
|
|
126
|
+
if (seen.has(u.dir)) continue; seen.add(u.dir); dependencyDirs.add(u.dir);
|
|
127
|
+
const m = read(u.dir, "Cargo.toml", parse);
|
|
128
|
+
const workspaceDir = u.cargoDir ?? ".";
|
|
129
|
+
let workspace;
|
|
130
|
+
const workspaceManifest = () => workspace ??= (workspaceDir === u.dir ? m : read(workspaceDir, "Cargo.toml", parse));
|
|
131
|
+
const local = (base, spec, name) => {
|
|
132
|
+
if (!spec || typeof spec !== "object" || spec.path === undefined) return;
|
|
133
|
+
const dir = localDir(base, spec.path); dependencyDirs.add(dir);
|
|
134
|
+
const dep = byDir.get(dir);
|
|
135
|
+
if (!dep) {
|
|
136
|
+
// Excluded/vendor crates are valid dependency sources, not standalone scan units. Read their committed manifests &
|
|
137
|
+
// follow their own edges; missing/unreadable paths still refuse before execution.
|
|
138
|
+
const vendored = read(dir, "Cargo.toml", parse);
|
|
139
|
+
if (typeof vendored.package?.name !== "string") throw problem(`${u.dir}/Cargo.toml local dependency ${name} at ${dir} has no committed package`);
|
|
140
|
+
queue.push({ dir, cargoDir: vendored.workspace ? dir : workspaceDir, name: null });
|
|
141
|
+
} else if (!rust.has(dep.name)) { rust.add(dep.name); queue.push(dep); }
|
|
142
|
+
};
|
|
143
|
+
for (const table of dependencyTables(m)) for (const [name, spec] of Object.entries(table)) {
|
|
144
|
+
if (spec && typeof spec === "object" && spec.workspace === true) {
|
|
145
|
+
const inherited = workspaceManifest().workspace?.dependencies?.[name];
|
|
146
|
+
if (inherited === undefined) throw problem(`${u.dir}/Cargo.toml dependency ${name} has no [workspace.dependencies] entry in ${workspaceDir}`);
|
|
147
|
+
local(workspaceDir, inherited, name);
|
|
148
|
+
} else local(u.dir, spec, name);
|
|
149
|
+
}
|
|
150
|
+
// Workspace source overrides are compile inputs too, including renamed & target-qualified dependencies.
|
|
151
|
+
const ws = workspaceManifest();
|
|
152
|
+
for (const table of [...Object.values(ws.patch ?? {}), ws.replace ?? {}]) for (const [name, spec] of Object.entries(table)) local(workspaceDir, spec, name);
|
|
153
|
+
}
|
|
154
|
+
if (!npm.size && !rust.size) throw problem(`scope ${dirs.join(", ")} contains no committed npm or Rust unit`);
|
|
155
|
+
return { npm, rust, dirs, dependencyDirs: [...dependencyDirs].sort(), widened: [] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The Class A Cargo scans deterministic-scans.mjs plans: `cargo-metadata|cargo-deny|cargo-audit[:<workspace label>]` run in the Cargo workspace
|
|
159
|
+
// named by the scan's `cwd` (none = the repository root workspace); `cargo-package-list:<unit name>` runs for one unit. The workspace label
|
|
160
|
+
// in an id is not always the directory (a crate-per-workspace layout labels by crate), so the workspace is read from `cwd`, never the id.
|
|
161
|
+
const CARGO_WORKSPACE_SCAN = /^cargo-(?:metadata|deny|audit)(?::.+)?$/;
|
|
162
|
+
const CARGO_PACKAGE_LIST = /^cargo-package-list:(.+)$/;
|
|
163
|
+
const cleanWorkspace = (dir) => posix(dir ?? ".").replace(/^\.\//, "").replace(/\/+$/, "") || ".";
|
|
164
|
+
|
|
165
|
+
/** A Class A scan that runs cargo (resolves metadata, lists a package, or runs cargo-deny / cargo-audit). */
|
|
166
|
+
export const isCargoScan = (s) => CARGO_WORKSPACE_SCAN.test(String(s.id)) || CARGO_PACKAGE_LIST.test(String(s.id));
|
|
167
|
+
|
|
168
|
+
/** A scan that runs repository code or cargo: tests, executing rows (tsc, eslint, biome, generators), the suite API contract, and every Cargo scan. */
|
|
169
|
+
export const isExecutingScan = (s) => s.executes === true || s.kind === "test" || s.id === "suite-api" || isCargoScan(s);
|
|
170
|
+
|
|
171
|
+
/** Keep file-only checks; filter every executing unit check (Cargo scans included) & refuse unclassified repository execution. */
|
|
172
|
+
export function scopeScans(scans, closure, { rustUnits = [] } = {}) {
|
|
173
|
+
const names = rustUnits.map((u) => u.name).sort((a, b) => b.length - a.length);
|
|
174
|
+
// The Cargo workspaces that hold a unit of the closure: a workspace-level Cargo scan (metadata, deny, audit) runs only for these.
|
|
175
|
+
const closureWorkspaces = new Set(rustUnits.filter((u) => closure.rust.has(u.name)).map((u) => cleanWorkspace(u.cargoDir)));
|
|
176
|
+
const kept = []; const dropped = [];
|
|
177
|
+
for (const s of scans) {
|
|
178
|
+
const npm = /^(?:tsc|eslint|biome|test:npm):(.+)$/.exec(s.id);
|
|
179
|
+
const rust = /^(?:clippy|check|check-features|check-dependent|doc|test:rust|test:rust-serial):(.+)$/.exec(s.id);
|
|
180
|
+
const cargoList = CARGO_PACKAGE_LIST.exec(s.id);
|
|
181
|
+
let include = true;
|
|
182
|
+
if (npm) include = closure.npm.has(npm[1]);
|
|
183
|
+
else if (cargoList) {
|
|
184
|
+
if (!names.includes(cargoList[1])) throw problem(`scan ${s.id} has no known Rust owner`);
|
|
185
|
+
include = closure.rust.has(cargoList[1]);
|
|
186
|
+
}
|
|
187
|
+
else if (CARGO_WORKSPACE_SCAN.test(s.id)) include = closureWorkspaces.has(cleanWorkspace(s.cwd));
|
|
188
|
+
else if (/^cargo-/.test(s.id)) throw problem(`executing scan ${s.id} has no exact scope owner`);
|
|
189
|
+
else if (Array.isArray(s.batch?.units)) {
|
|
190
|
+
const selected = s.batch.units.filter((n) => closure.rust.has(n));
|
|
191
|
+
if (selected.length && selected.length !== s.batch.units.length && s.class !== "B") throw problem(`mixed executing batch ${s.id} cannot be scoped exactly`);
|
|
192
|
+
// --scope accepts Class A snapshot checks only. A mixed Class B batch never runs in this route; retain no partial command
|
|
193
|
+
// that could compile unrelated units. Scoped B execution is refused by the gate before planning.
|
|
194
|
+
include = selected.length === s.batch.units.length;
|
|
195
|
+
}
|
|
196
|
+
else if (rust) { const name = names.find((n) => rust[1] === n || rust[1].startsWith(`${n}:`)); if (!name) throw problem(`scan ${s.id} has no known Rust owner`); include = closure.rust.has(name); }
|
|
197
|
+
else if (s.class === "B" || ["suite-api", "generated-docs", "test:scripts-ci"].includes(s.id)) include = closure.dirs.includes(".");
|
|
198
|
+
else if (s.executes === true || s.kind === "test") {
|
|
199
|
+
if (closure.dirs.includes(".")) include = true;
|
|
200
|
+
else if (typeof s.cwd === "string") include = closure.dependencyDirs.includes(s.cwd);
|
|
201
|
+
else throw problem(`executing scan ${s.id} has no exact scope owner`);
|
|
202
|
+
}
|
|
203
|
+
const row = include && Array.isArray(s.tscWorkspace) ? { ...s, tscWorkspace: s.tscWorkspace.filter((d) => closure.dependencyDirs.includes(d)) } : s;
|
|
204
|
+
(include ? kept : dropped).push(row);
|
|
205
|
+
}
|
|
206
|
+
return { scans: kept, dropped };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Scope identity also binds scan selection so prior Class A evidence cannot admit another scope. */
|
|
210
|
+
export function scopedPlan(plan, scans, closure) {
|
|
211
|
+
const digest = (rows) => createHash("sha256").update(JSON.stringify(rows)).digest("hex");
|
|
212
|
+
const scopeDigest = digest({ dirs: closure.dirs, npm: [...closure.npm].sort(), rust: [...closure.rust].sort(), dependencyDirs: closure.dependencyDirs });
|
|
213
|
+
return { ...plan, scans, scopeDigest, deferred: plan.deferred.filter((id) => scans.some((s) => s.id === id)), inputDigest: digest([plan.inputDigest, scopeDigest]), scanSetDigest: digestScanSet(scans), classDigests: { A: digestScanSet(scans.filter((s) => s.class === "A")), B: digestScanSet(scans.filter((s) => s.class === "B")) } };
|
|
214
|
+
}
|
package/assemble-app-command.mjs
CHANGED
|
@@ -12,13 +12,16 @@
|
|
|
12
12
|
// assemble-app --platform win --package
|
|
13
13
|
// makensis from assembly.win.nsis over the staged payload, then Authenticode-signs the installer (sign-windows).
|
|
14
14
|
import { spawnSync } from "./background-process.mjs";
|
|
15
|
-
import nodeFs, { realpathSync } from "node:fs";
|
|
15
|
+
import nodeFs, { existsSync, realpathSync } from "node:fs";
|
|
16
|
+
import { BUILD_LAUNCH_REFUSED, buildLaunch } from "./build-launch.mjs";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
import { assembleMacBundle, assembleWindowsPayload, AssembledAppError, loadStageReceipt, resolveAssembly, resolveNestedApp, verifyMacBundle, verifyReleaseBinding, verifyWindowsPayload, writeReleaseBinding } from "./assembled-app.mjs";
|
|
19
20
|
import { buildOuterApp, outerProductPath, resolveAssemblyStorage } from "./install-assembled-app.mjs";
|
|
20
21
|
|
|
21
22
|
const SIGN_WINDOWS = fileURLToPath(new URL("./sign-windows.mjs", import.meta.url));
|
|
23
|
+
/** This package's own right-release entry: it comes back through the door for its own children (development.mjs OWN_RIGHT_RELEASE). */
|
|
24
|
+
const OWN_RIGHT_RELEASE = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
22
25
|
const refuse = (code, message) => { throw new AssembledAppError(code, message); };
|
|
23
26
|
|
|
24
27
|
export function parseAssembleArgs(argv) {
|
|
@@ -45,13 +48,27 @@ export function parseAssembleArgs(argv) {
|
|
|
45
48
|
return opts;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
/**
|
|
52
|
+
* GB-59 (GATE commit/broker entry; BR20): every compile-capable prepare step goes through the one build door (build-launch.mjs), like a dev build step: on a
|
|
53
|
+
* broker host it is a carried or adapter job with the step's declared env (a refusal comes before anything is spawned), anywhere else the plain command.
|
|
54
|
+
* This package's own `right-release` keeps its process order (its children come back through the door). Trusted release orchestration stays session-side.
|
|
55
|
+
*/
|
|
56
|
+
function runPrepare({ root, spec, env, run, resolveCommand, log, door = buildLaunch, exists = existsSync }) {
|
|
49
57
|
for (const command of spec.prepare.release) {
|
|
50
|
-
const
|
|
58
|
+
const args = (command.args ?? []).map(String);
|
|
51
59
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
60
|
+
const ownReleaseStep = (args[0] === "exec" && args[1] === "right-release") || args[0] === "right-release";
|
|
61
|
+
const resolved = command.cmd === "pnpm" && !ownReleaseStep ? { command: "pnpm", args } : resolveCommand(command.cmd, args, { root, env });
|
|
62
|
+
const own = resolved.command === process.execPath && resolved.args[0] === OWN_RIGHT_RELEASE;
|
|
63
|
+
let launch = resolved;
|
|
64
|
+
if (!own) {
|
|
65
|
+
try { launch = { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, stepEnv: command.env, cwd, exists }) }; }
|
|
66
|
+
catch (error) { if (error?.code === BUILD_LAUNCH_REFUSED) refuse("ASSEMBLY_BUILD_REFUSED", `prepare command refused before it started: ${error.message}`); throw error; }
|
|
67
|
+
}
|
|
68
|
+
log(`assemble-app: prepare ${command.cmd} ${args.join(" ")}`);
|
|
69
|
+
const baseEnv = launch.env && launch.env !== resolved.env ? { ...(resolved.env ?? env), ...launch.env } : (launch.env ?? env);
|
|
70
|
+
const result = run(launch.command, launch.args, { cwd, env: { ...baseEnv, ...(command.env ?? {}) }, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
|
|
71
|
+
if (result.error || result.status !== 0) refuse("ASSEMBLY_BUILD_FAILED", `prepare command failed (${result.status ?? result.error?.message}): ${command.cmd} ${args.join(" ")}`);
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
@@ -112,7 +129,7 @@ export async function assembleApp({ root, config, opts, env = process.env, deps
|
|
|
112
129
|
return { installer: output };
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
if (opts.build) runPrepare({ root, spec, env, run: d.run, resolveCommand: d.resolveCommand, log: d.log });
|
|
132
|
+
if (opts.build) runPrepare({ root, spec, env, run: d.run, resolveCommand: d.resolveCommand, log: d.log, ...(d.door ? { door: d.door } : {}), ...(d.exists ? { exists: d.exists } : {}) });
|
|
116
133
|
const receipt = loadStageReceipt({ root, spec, profile, fs: d.fs });
|
|
117
134
|
if (spec.platform === "mac") {
|
|
118
135
|
const storage = d.storage({ root, profile, env, run: d.run });
|
package/atomic-write.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The one durable replace-write for authoritative result files (GB-26; the gate's stage journal, the protected QA receipts): the bytes go to a temp
|
|
2
|
+
// file beside the destination, are fsynced, and replace the destination with one rename, then the directory is fsynced. A crash, a full disk or a
|
|
3
|
+
// failing step leaves the previous valid file untouched and the new content wholly present or wholly absent, never a truncated mix; the temp file is
|
|
4
|
+
// removed on every failure path and the failure is typed (RK_ATOMIC_WRITE, with the step that failed). The replaced file keeps its permission bits.
|
|
5
|
+
// A LEAF module: node builtins only.
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import nodeFs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
export const ATOMIC_WRITE_FAILED = "RK_ATOMIC_WRITE";
|
|
11
|
+
|
|
12
|
+
export class AtomicWriteError extends Error {
|
|
13
|
+
constructor(step, file, cause) {
|
|
14
|
+
super(`atomic write of ${file} failed at ${step}: ${cause?.message ?? cause}`);
|
|
15
|
+
this.name = "AtomicWriteError";
|
|
16
|
+
this.code = ATOMIC_WRITE_FAILED;
|
|
17
|
+
this.step = step;
|
|
18
|
+
this.file = file;
|
|
19
|
+
if (cause) this.cause = cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Writes `data` (string or Buffer) to `file` atomically. `fs` is a seam for fault injection (the node:fs functions it uses). */
|
|
24
|
+
export function writeFileAtomic(file, data, { mode, fs = nodeFs } = {}) {
|
|
25
|
+
const dir = path.dirname(file);
|
|
26
|
+
const temp = path.join(dir, `.${path.basename(file)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
27
|
+
let step = "mkdir";
|
|
28
|
+
let fd;
|
|
29
|
+
let created = false;
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
let keep = mode;
|
|
33
|
+
if (keep === undefined) { try { keep = fs.statSync(file).mode & 0o777; } catch { keep = undefined; } }
|
|
34
|
+
step = "open";
|
|
35
|
+
fd = fs.openSync(temp, "wx", keep ?? 0o644);
|
|
36
|
+
created = true;
|
|
37
|
+
step = "write";
|
|
38
|
+
const bytes = Buffer.isBuffer(data) ? data : Buffer.from(String(data), "utf8");
|
|
39
|
+
let at = 0;
|
|
40
|
+
while (at < bytes.length) at += fs.writeSync(fd, bytes, at, bytes.length - at);
|
|
41
|
+
step = "fsync";
|
|
42
|
+
fs.fsyncSync(fd);
|
|
43
|
+
step = "close";
|
|
44
|
+
const closing = fd; fd = undefined;
|
|
45
|
+
fs.closeSync(closing);
|
|
46
|
+
step = "rename";
|
|
47
|
+
fs.renameSync(temp, file);
|
|
48
|
+
created = false;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* the original failure is the one reported */ } }
|
|
51
|
+
if (created) { try { fs.rmSync(temp, { force: true }); } catch { /* a leftover temp is never read as the destination */ } }
|
|
52
|
+
throw error instanceof AtomicWriteError ? error : new AtomicWriteError(step, file, error);
|
|
53
|
+
}
|
|
54
|
+
// The rename is done: the new content is the file. Making the directory entry durable is best effort (not every platform can open a directory).
|
|
55
|
+
let dirFd;
|
|
56
|
+
try { dirFd = fs.openSync(dir, "r"); fs.fsyncSync(dirFd); } catch { /* POSIX only; the replace itself already happened */ } finally { if (dirFd !== undefined) { try { fs.closeSync(dirFd); } catch { /* closed */ } } }
|
|
57
|
+
}
|
package/bin/darwin/rightkit-swap
CHANGED
|
File without changes
|