@rightkit/release 0.6.7 → 0.6.10
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 +35 -10
- package/admission-files.mjs +1 -1
- package/app-check.mjs +94 -20
- 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 +957 -83
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +2 -1
- package/cargo-contract.mjs +3 -1
- package/cargo-target-shim.mjs +166 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +3 -1
- package/codesign-shim.mjs +11 -1
- package/deterministic-scans.mjs +807 -56
- package/development-signing.mjs +4 -1
- package/development.mjs +110 -20
- package/door-policy.mjs +61 -6
- package/door.mjs +23 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +15 -5
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/ios-dev.mjs +70 -3
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +161 -26
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/native-helpers.mjs +355 -0
- package/package-recipe-targets.mjs +52 -7
- package/package.json +5 -3
- package/pipeline-receipts.mjs +302 -302
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +8 -6
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +73 -5
- package/release-config.mjs +1 -1
- package/release.mjs +15 -5
- package/rightkit-versions.json +13 -7
- package/snapshot-check.mjs +306 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +161 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +856 -0
- package/stage-commands.mjs +60 -0
- package/stage-sidecar.mjs +57 -7
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +259 -0
- package/web-build.mjs +38 -6
package/project-type.mjs
CHANGED
|
@@ -41,5 +41,5 @@ export function targetUsesMsix(target) {
|
|
|
41
41
|
return Boolean(target?.msix) || target?.signingContract === "windows-msix-signed-v1";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
export const packageCommandOptional = (target, projectType) => targetUsesMsix(target) || Boolean(target?.winPackage || target?.macPackage) || (projectType === "swiftpm" && Boolean(target?.prePackage?.cmd));
|
|
44
|
+
/** Expanded recipes must come from the validator, which strips caller-supplied recipe markers. */
|
|
45
|
+
export const packageCommandOptional = (target, projectType, { expanded = false } = {}) => targetUsesMsix(target) || Boolean(target?.winPackage || target?.macPackage) || (expanded && target?.recipe?.kind === "macPackage") || (projectType === "swiftpm" && Boolean(target?.prePackage?.cmd));
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Verifies a RustRight broker job receipt's signed provenance envelope in pure JS (node:crypto only), exactly as
|
|
2
|
+
// rustright-protocol's attestation.rs `verify_receipt` does, so a hosted runner with no broker can authenticate the receipts a local
|
|
3
|
+
// gate run embedded in its qualification. Never trusts a value from the receipt that the caller can pin instead.
|
|
4
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export const PROVENANCE_SCHEMA = "rustright.provenance.v1";
|
|
7
|
+
export const PROVENANCE_DOMAIN = "rustright-provenance-v1";
|
|
8
|
+
export const QUALIFICATION_NONCE_DOMAIN = "rightkit-qualification-v1";
|
|
9
|
+
|
|
10
|
+
export class ProvenanceError extends Error {
|
|
11
|
+
constructor(code, message) { super(message); this.name = "ProvenanceError"; this.code = code; }
|
|
12
|
+
}
|
|
13
|
+
const refuse = (code, message) => { throw new ProvenanceError(code, message); };
|
|
14
|
+
|
|
15
|
+
const sha256Hex = (data) => createHash("sha256").update(data).digest("hex");
|
|
16
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
17
|
+
|
|
18
|
+
function writeString(text, out) {
|
|
19
|
+
let s = '"';
|
|
20
|
+
for (const ch of text) {
|
|
21
|
+
const code = ch.codePointAt(0);
|
|
22
|
+
if (ch === '"') s += '\\"';
|
|
23
|
+
else if (ch === "\\") s += "\\\\";
|
|
24
|
+
else if (code < 0x20) s += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
25
|
+
else s += ch;
|
|
26
|
+
}
|
|
27
|
+
out.push(`${s}"`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeCanonical(value, out) {
|
|
31
|
+
if (value === null) out.push("null");
|
|
32
|
+
else if (typeof value === "boolean") out.push(value ? "true" : "false");
|
|
33
|
+
else if (typeof value === "number") {
|
|
34
|
+
// Integers only: serde_json prints floats with its own algorithm, and an integer beyond 2^53 was already corrupted by JSON.parse.
|
|
35
|
+
// Either would make a signature fail for the wrong reason, so refuse loudly instead.
|
|
36
|
+
if (!Number.isSafeInteger(value)) refuse("canonical", "canonical JSON takes only safe integers");
|
|
37
|
+
out.push(String(value));
|
|
38
|
+
} else if (typeof value === "string") writeString(value, out);
|
|
39
|
+
else if (Array.isArray(value)) {
|
|
40
|
+
out.push("[");
|
|
41
|
+
value.forEach((item, index) => { if (index > 0) out.push(","); writeCanonical(item, out); });
|
|
42
|
+
out.push("]");
|
|
43
|
+
} else if (isRecord(value)) {
|
|
44
|
+
// Rust sorts by the UTF-8 bytes of the key; JavaScript's default sort is by UTF-16 code unit and differs above U+FFFF.
|
|
45
|
+
const keys = Object.keys(value).sort((a, b) => Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8")));
|
|
46
|
+
out.push("{");
|
|
47
|
+
keys.forEach((key, index) => { if (index > 0) out.push(","); writeString(key, out); out.push(":"); writeCanonical(value[key], out); });
|
|
48
|
+
out.push("}");
|
|
49
|
+
} else refuse("canonical", `canonical JSON cannot carry a ${typeof value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Canonical JSON: keys sorted by byte order, no whitespace, strings escape only `"`, `\` and control characters < 0x20 (\u00xx lower-case). */
|
|
53
|
+
export function canonicalJson(value) {
|
|
54
|
+
const out = [];
|
|
55
|
+
writeCanonical(value, out);
|
|
56
|
+
return out.join("");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The exact bytes the broker signs for a payload. */
|
|
60
|
+
export function signedBytes(payload) {
|
|
61
|
+
return Buffer.from(`${PROVENANCE_DOMAIN}\n${canonicalJson(payload)}`, "utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
65
|
+
const keyFromSec1 = (publicKeyHex) => {
|
|
66
|
+
if (typeof publicKeyHex !== "string" || !HEX.test(publicKeyHex)) refuse("pin", "pinned public key is not hex");
|
|
67
|
+
const bytes = Buffer.from(publicKeyHex, "hex");
|
|
68
|
+
if (bytes.length !== 65 || bytes[0] !== 0x04) refuse("pin", "pinned public key is not an uncompressed SEC1 P-256 point");
|
|
69
|
+
try {
|
|
70
|
+
return createPublicKey({ key: { kty: "EC", crv: "P-256", x: bytes.subarray(1, 33).toString("base64url"), y: bytes.subarray(33, 65).toString("base64url") }, format: "jwk" });
|
|
71
|
+
} catch { return refuse("pin", "pinned public key is not a P-256 point"); }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Verifies receipt.provenance against pins the caller holds (never from the receipt) and what this invocation expects. Every check
|
|
76
|
+
* fails closed (throws ProvenanceError with a stable `code`) and returns the signed payload on success.
|
|
77
|
+
* pins: { publicKey (SEC1 uncompressed hex), keyId, host, brokerDigest }
|
|
78
|
+
* expected: { jobId, nonce, requestDigest? } requestDigest is checked only when the caller can recompute it.
|
|
79
|
+
*/
|
|
80
|
+
export function verifyProvenance(receipt, pins, expected) {
|
|
81
|
+
const envelope = receipt?.provenance;
|
|
82
|
+
if (!isRecord(envelope)) refuse("legacy", "the receipt has no provenance envelope");
|
|
83
|
+
if (envelope.schema !== PROVENANCE_SCHEMA) refuse("schema", "unknown provenance schema");
|
|
84
|
+
const payload = envelope.payload;
|
|
85
|
+
if (!isRecord(payload)) refuse("malformed", "payload is missing");
|
|
86
|
+
const signature = envelope.signature;
|
|
87
|
+
if (!isRecord(signature)) refuse("malformed", "signature is missing");
|
|
88
|
+
if (signature.alg !== "ES256") refuse("alg", "signature algorithm must be ES256");
|
|
89
|
+
if (!isRecord(pins) || typeof pins.publicKey !== "string" || !HEX.test(pins.publicKey)) refuse("pin", "pinned public key is not hex");
|
|
90
|
+
if (sha256Hex(Buffer.from(pins.publicKey, "hex")) !== pins.keyId) refuse("pin", "pinned key id does not match the pinned public key");
|
|
91
|
+
if (signature.keyId !== pins.keyId || payload.keyId !== pins.keyId) refuse("key", "the receipt was not signed by the pinned key");
|
|
92
|
+
const key = keyFromSec1(pins.publicKey);
|
|
93
|
+
if (typeof signature.value !== "string" || !/^[0-9a-fA-F]+$/.test(signature.value)) refuse("signature", "signature is not hex");
|
|
94
|
+
if (signature.value.length !== 128) refuse("signature", "signature is not 64 bytes r||s");
|
|
95
|
+
let good = false;
|
|
96
|
+
try { good = verify("sha256", signedBytes(payload), { key, dsaEncoding: "ieee-p1363" }, Buffer.from(signature.value, "hex")); } catch (error) { if (error instanceof ProvenanceError) throw error; }
|
|
97
|
+
if (!good) refuse("signature", "signature does not verify");
|
|
98
|
+
if (payload.host !== pins.host) refuse("host", "signed for another host");
|
|
99
|
+
if (payload.broker?.brokerDigest !== pins.brokerDigest) refuse("runtime", "signed by another broker runtime");
|
|
100
|
+
if (typeof expected?.jobId !== "string" || payload.jobId !== expected.jobId || receipt.id !== expected.jobId) refuse("job", "the receipt is for another job");
|
|
101
|
+
if (typeof expected.nonce !== "string" || payload.nonce !== expected.nonce) refuse("nonce", "the nonce is not the one this invocation sent");
|
|
102
|
+
if (expected.requestDigest !== undefined && payload.requestDigest !== expected.requestDigest) refuse("request", "the request digest is not the one this invocation computed");
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The nonce a unit gate run sets (RIGHTKIT_ATTEST_NONCE) for the broker jobs of one stage; the hosted admission recomputes it from
|
|
107
|
+
* what it knows of the dispatched commit, so a receipt of another commit, tree, unit or stage cannot match. 64 lowercase hex. */
|
|
108
|
+
export function qualificationNonce({ commit, tree, unit, stage }) {
|
|
109
|
+
return sha256Hex(`${QUALIFICATION_NONCE_DOMAIN}\n${commit}\n${tree}\n${unit}\n${stage}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Broker request ids the client prints (`rustright: request <id>`) in one command's output. */
|
|
113
|
+
export function brokerRequestIds(text) {
|
|
114
|
+
return [...String(text ?? "").matchAll(/^rustright: request ([0-9a-f-]{36})/gm)].map((m) => m[1]);
|
|
115
|
+
}
|
package/publish-cargo.mjs
CHANGED
|
@@ -335,10 +335,10 @@ function fileViolations(relative, bytes, records) {
|
|
|
335
335
|
* (the repository gate) the reviewed records of `<repoRoot>/.rightkit/secret-scan-records.json` apply and each violation is a
|
|
336
336
|
* `<repo-relative path>[:<line>]: <reason>` line; `repoRoot` (default `crateDir`) is the root those paths and the records are relative to.
|
|
337
337
|
*/
|
|
338
|
-
export function scanCrateForSecrets(crateDir, { envTemplates = false, repoRoot = crateDir } = {}) {
|
|
339
|
-
if (envTemplates) return scanRepositoryForSecrets(crateDir, repoRoot);
|
|
338
|
+
export function scanCrateForSecrets(crateDir, { envTemplates = false, repoRoot = crateDir, skip = null } = {}) {
|
|
339
|
+
if (envTemplates) return scanRepositoryForSecrets(crateDir, repoRoot, skip);
|
|
340
340
|
const violations = [];
|
|
341
|
-
for (const file of walkFiles(crateDir)) {
|
|
341
|
+
for (const file of walkFiles(crateDir, skip)) {
|
|
342
342
|
const relative = path.relative(crateDir, file).replaceAll("\\", "/");
|
|
343
343
|
if (FORBIDDEN_PACKAGE_PATH.test(relative)) {
|
|
344
344
|
violations.push(relative);
|
|
@@ -350,10 +350,10 @@ export function scanCrateForSecrets(crateDir, { envTemplates = false, repoRoot =
|
|
|
350
350
|
return [...new Set(violations)].sort();
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
function scanRepositoryForSecrets(dir, repoRoot) {
|
|
353
|
+
function scanRepositoryForSecrets(dir, repoRoot, skip = null) {
|
|
354
354
|
const { records = new Map(), problems = [] } = readSecretScanRecords(repoRoot);
|
|
355
355
|
const violations = [...problems];
|
|
356
|
-
for (const file of walkFiles(dir)) {
|
|
356
|
+
for (const file of walkFiles(dir, skip)) {
|
|
357
357
|
const inDir = path.relative(dir, file).replaceAll("\\", "/");
|
|
358
358
|
const relative = path.relative(repoRoot, file).replaceAll("\\", "/");
|
|
359
359
|
if (neverRecordable(inDir)) {
|
|
@@ -521,7 +521,8 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
521
521
|
return publication ?? { name: options.crate, version: cargoVersion, dryRun: true };
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
|
|
524
|
+
// `skip(absolutePath)`: a caller's exclusion (the gate's git-ignored paths of a live tree); a skipped directory is not descended.
|
|
525
|
+
function walkFiles(root, skip = null) {
|
|
525
526
|
const files = [];
|
|
526
527
|
const pending = [root];
|
|
527
528
|
while (pending.length) {
|
|
@@ -529,6 +530,7 @@ function walkFiles(root) {
|
|
|
529
530
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
530
531
|
if (entry.isDirectory() && [".git", "target", "node_modules"].includes(entry.name)) continue;
|
|
531
532
|
const absolute = path.join(current, entry.name);
|
|
533
|
+
if (skip && skip(absolute)) continue;
|
|
532
534
|
if (entry.isDirectory()) pending.push(absolute);
|
|
533
535
|
else if (entry.isFile()) files.push(absolute);
|
|
534
536
|
}
|
package/qa-admission-policy.json
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"apps": {},
|
|
4
|
-
"approvedRunners": [
|
|
4
|
+
"approvedRunners": [
|
|
5
|
+
{
|
|
6
|
+
"sha256": "3c11788ecb51459cde33c6d98dfad936a6c4c4c409f1d11ef327b0f0781f2986",
|
|
7
|
+
"version": "0.9.1",
|
|
8
|
+
"platform": "win",
|
|
9
|
+
"set_by": "adrian",
|
|
10
|
+
"since": "2026-10-27",
|
|
11
|
+
"note": "Dell runner, Adrian's go 2026-10-28, sha256 3c11788e…2986"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
5
14
|
}
|
package/recipe-build.mjs
CHANGED
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
// requires the planned target directory before and after it (a temporary bridge until the broker exposes artifacts).
|
|
31
31
|
import { existsSync as nodeExistsSync, readFileSync, readdirSync as nodeReaddirSync, rmSync as nodeRmSync, statSync as nodeStatSync } from "node:fs";
|
|
32
32
|
import path from "node:path";
|
|
33
|
-
import { pathToFileURL } from "node:url";
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
34
|
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
35
35
|
import { resolveTargetRoot as cargoResolveTargetRoot } from "./cargo-target.mjs";
|
|
36
|
+
import { buildAndEmbedNativeHelpers, validateNativeHelpers } from "./native-helpers.mjs";
|
|
36
37
|
import { assertTargetDirUnchanged, removeFreshSources, stageArtifacts, verifySameContent } from "./managed-target-stage.mjs";
|
|
37
38
|
|
|
38
39
|
export const RECIPE_BUILD_PLAN_VERSION = 1;
|
|
@@ -50,7 +51,10 @@ export class RecipeBuildError extends Error {
|
|
|
50
51
|
const fail = (code, message, details) => { throw new RecipeBuildError(code, message, details); };
|
|
51
52
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
52
53
|
const STEP_KEYS = new Set(["phase", "cmd", "args", "cwd", "env"]);
|
|
53
|
-
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability", "stage"]);
|
|
54
|
+
const PLAN_KEYS = new Set(["version", "steps", "cleanup", "stability", "stage", "verifySidecars", "nativeHelpers"]);
|
|
55
|
+
const VERIFY_SIDECARS_KEYS = new Set(["receipt", "target"]);
|
|
56
|
+
const NATIVE_HELPERS_KEYS = new Set(["app", "helpers", "version", "forceArch", "configuration"]);
|
|
57
|
+
const UNIVERSAL_TRIPLE = "universal-apple-darwin";
|
|
54
58
|
const STAGE_KEYS = new Set(["pre", "post", "verify", "resolve"]);
|
|
55
59
|
const STAGE_ENTRY_KEYS = new Set(["from", "to", "fresh"]);
|
|
56
60
|
const CLEANUP_KEYS = new Set(["remove", "apps", "dmgVolumes"]);
|
|
@@ -154,6 +158,29 @@ function validateStage(value, errors) {
|
|
|
154
158
|
return out;
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
function validateVerifySidecars(value, errors) {
|
|
162
|
+
if (value === undefined) return undefined;
|
|
163
|
+
if (!isObject(value)) { errors.push("verifySidecars must be { receipt, target }"); return undefined; }
|
|
164
|
+
unknownKeys(value, VERIFY_SIDECARS_KEYS, "verifySidecars", errors);
|
|
165
|
+
if (value.target !== UNIVERSAL_TRIPLE) errors.push(`verifySidecars.target must be ${UNIVERSAL_TRIPLE}`);
|
|
166
|
+
const receipt = insideApp(value.receipt, "verifySidecars.receipt", errors, { strict: true });
|
|
167
|
+
return receipt ? { receipt, target: UNIVERSAL_TRIPLE } : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateNativeHelpersSection(value, errors) {
|
|
171
|
+
if (value === undefined) return undefined;
|
|
172
|
+
if (!isObject(value)) { errors.push("nativeHelpers must be { app, helpers, version?, forceArch?, configuration? }"); return undefined; }
|
|
173
|
+
unknownKeys(value, NATIVE_HELPERS_KEYS, "nativeHelpers", errors);
|
|
174
|
+
const app = insideApp(value.app, "nativeHelpers.app", errors, { strict: true });
|
|
175
|
+
if (app && !app.replace(/\/+$/, "").endsWith(".app")) errors.push("nativeHelpers.app must name the .app bundle");
|
|
176
|
+
let helpers;
|
|
177
|
+
try { helpers = validateNativeHelpers(value.helpers, { label: "nativeHelpers.helpers" }); } catch (error) { errors.push(...(error.details?.errors ?? [error.message])); }
|
|
178
|
+
if (value.forceArch !== undefined && !["arm64", "x86_64"].includes(value.forceArch)) errors.push("nativeHelpers.forceArch must be arm64 or x86_64");
|
|
179
|
+
if (value.configuration !== undefined && !["release", "debug"].includes(value.configuration)) errors.push("nativeHelpers.configuration must be release or debug");
|
|
180
|
+
if (value.version !== undefined && (typeof value.version !== "string" || !/^\d+(\.\d+){0,2}$/.test(value.version))) errors.push("nativeHelpers.version must be 1-3 numeric parts");
|
|
181
|
+
return app && helpers ? { app, helpers, ...(value.version === undefined ? {} : { version: value.version }), ...(value.forceArch ? { forceArch: value.forceArch } : {}), ...(value.configuration ? { configuration: value.configuration } : {}) } : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
/** Validate a plan strictly; every problem is reported at once as RECIPE_BUILD_PLAN_INVALID. Returns the normalized plan. */
|
|
158
185
|
export function validateRecipePlan(plan) {
|
|
159
186
|
const errors = [];
|
|
@@ -184,10 +211,14 @@ export function validateRecipePlan(plan) {
|
|
|
184
211
|
const cleanup = validateCleanup(plan.cleanup, errors);
|
|
185
212
|
const stability = validateStability(plan.stability, errors);
|
|
186
213
|
const stage = validateStage(plan.stage, errors);
|
|
214
|
+
const verifySidecars = validateVerifySidecars(plan.verifySidecars, errors);
|
|
215
|
+
const nativeHelpers = validateNativeHelpersSection(plan.nativeHelpers, errors);
|
|
187
216
|
if (errors.length) fail("RECIPE_BUILD_PLAN_INVALID", `invalid recipe plan:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
|
|
188
|
-
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage ? { stage } : {}) };
|
|
217
|
+
return { version: RECIPE_BUILD_PLAN_VERSION, steps, ...(cleanup ? { cleanup } : {}), ...(stability ? { stability } : {}), ...(stage ? { stage } : {}), ...(verifySidecars ? { verifySidecars } : {}), ...(nativeHelpers ? { nativeHelpers } : {}) };
|
|
189
218
|
}
|
|
190
219
|
|
|
220
|
+
/** This package's own right-release entry: the door's RightKit, whatever the app's node_modules holds. */
|
|
221
|
+
export const OWN_RIGHT_RELEASE = fileURLToPath(new URL("./cli/right-release.mjs", import.meta.url));
|
|
191
222
|
// cmd.exe would reinterpret these in a shell launch; refuse rather than escape.
|
|
192
223
|
const SHELL_UNSAFE = /[&|<>^%"!\r\n]/;
|
|
193
224
|
|
|
@@ -196,7 +227,11 @@ const SHELL_UNSAFE = /[&|<>^%"!\r\n]/;
|
|
|
196
227
|
* release runs under pnpm). On Windows a non-.exe command is a .cmd shim that only a shell can start, so it is allowed
|
|
197
228
|
* only when no argument could be reinterpreted by that shell.
|
|
198
229
|
*/
|
|
199
|
-
export function resolveLaunch({ cmd, args = [], platform = process.platform, env = process.env, execPath = process.execPath }) {
|
|
230
|
+
export function resolveLaunch({ cmd, args = [], platform = process.platform, env = process.env, execPath = process.execPath, ownRightRelease = OWN_RIGHT_RELEASE }) {
|
|
231
|
+
// A step's `right-release ...` (also `pnpm exec right-release ...` and `pnpm right-release ...`) runs THIS package's own CLI, never the
|
|
232
|
+
// app's node_modules copy that `pnpm exec` would put first on PATH (the same rule as development.mjs resolveCommand).
|
|
233
|
+
if (cmd === "right-release") return { command: execPath, args: [ownRightRelease, ...args], shell: false };
|
|
234
|
+
if (cmd === "pnpm" && ((args[0] === "exec" && args[1] === "right-release") || args[0] === "right-release")) return { command: execPath, args: [ownRightRelease, ...args.slice(args[0] === "exec" ? 2 : 1)], shell: false };
|
|
200
235
|
if (cmd === "pnpm" && env.npm_execpath) return { command: execPath, args: [env.npm_execpath, ...args], shell: false };
|
|
201
236
|
if (platform === "win32" && !/\.(?:exe|com)$/i.test(cmd)) {
|
|
202
237
|
if ([cmd, ...args].some((value) => SHELL_UNSAFE.test(value))) fail("RECIPE_BUILD_LAUNCH_UNSAFE", `${cmd} needs a shell on Windows and its arguments contain characters a shell would reinterpret; run the release under pnpm (npm_execpath) or name an .exe`, { cmd });
|
|
@@ -330,6 +365,7 @@ export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform =
|
|
|
330
365
|
const cleaned = runRecipeCleanup(plan.cleanup, { appRoot, platform, log, spawnSync, ...(fs ? { fs } : {}) });
|
|
331
366
|
let stable = [];
|
|
332
367
|
let staged = [];
|
|
368
|
+
let nativeHelpers = null;
|
|
333
369
|
const stagePost = plan.stage?.post ?? [];
|
|
334
370
|
const resolveEntry = (entry, appKey) => ({ ...entry, [appKey]: path.resolve(appRoot, entry[appKey]) });
|
|
335
371
|
for (const [index, step] of plan.steps.entries()) {
|
|
@@ -342,6 +378,7 @@ export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform =
|
|
|
342
378
|
// Immediately before the step that embeds the real EXE: it must be exactly the signed in-app copy.
|
|
343
379
|
verifyReal("after the copy-back, before the build step");
|
|
344
380
|
}
|
|
381
|
+
if (step.phase === "build" && plan.verifySidecars) verifyUniversalSidecars(plan.verifySidecars, { appRoot, spawnSync, env, log, ...(fs?.readFileSync ? { readFile: fs.readFileSync } : {}) });
|
|
345
382
|
const launch = resolveLaunch({ cmd: step.cmd, args: step.args, platform, env });
|
|
346
383
|
const cwd = path.resolve(appRoot, step.cwd ?? ".");
|
|
347
384
|
log(`recipe-build: [${step.phase}] ${step.cmd} ${step.args.join(" ")}`.trimEnd());
|
|
@@ -355,8 +392,39 @@ export function runRecipeBuild(rawPlan, { appRoot, env = process.env, platform =
|
|
|
355
392
|
if (step.phase === "build" && stagePost.length) waitForStableArtifacts({ ...(plan.stability ?? {}), files: stagePost.map((entry) => entry.from).filter((file) => (fs?.existsSync ?? nodeExistsSync)(file)) }, { appRoot, log, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}), ...(statSync ? { statSync } : {}) });
|
|
356
393
|
if (step.phase === "build" && stagePost.length) staged.push(...stageArtifacts(stagePost.map((entry) => resolveEntry(entry, "to")), { ...(fs ? { fs } : {}), log }));
|
|
357
394
|
if (step.phase === "build") stable = waitForStableArtifacts(plan.stability, { appRoot, log, ...(sleep ? { sleep } : {}), ...(now ? { now } : {}), ...(statSync ? { statSync } : {}) });
|
|
395
|
+
// Native Swift helpers are built and embedded into the built .app AFTER the app exists and BEFORE postBuild and signing.
|
|
396
|
+
if (step.phase === "build" && plan.nativeHelpers) {
|
|
397
|
+
const section = plan.nativeHelpers;
|
|
398
|
+
nativeHelpers = buildAndEmbedNativeHelpers({ appRoot, app: path.resolve(appRoot, section.app), helpers: section.helpers, version: section.version, forceArch: section.forceArch, configuration: section.configuration ?? "release", env, spawn: spawnSync, log, ...(fs ? { fs } : {}) });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { steps: done, cleaned, stable, staged, nativeHelpers };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* After the prepare steps of a universal macOS build: the stage-sidecar receipt must be for universal-apple-darwin and every
|
|
406
|
+
* externalBin file it lists must be named `<name>-universal-apple-darwin` (the name Tauri's universal build looks for) and hold
|
|
407
|
+
* exactly arm64 and x86_64 (`lipo -archs`). Anything else throws RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL before the Tauri build starts.
|
|
408
|
+
* Options: appRoot, spawnSync, readFile, env, log.
|
|
409
|
+
*/
|
|
410
|
+
export function verifyUniversalSidecars(section, { appRoot, spawnSync = nodeSpawnSync, readFile = readFileSync, env = process.env, log = () => {} } = {}) {
|
|
411
|
+
const receiptFile = path.resolve(appRoot, section.receipt);
|
|
412
|
+
let receipt;
|
|
413
|
+
try { receipt = JSON.parse(readFile(receiptFile, "utf8")); } catch (error) { fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `cannot read the stage-sidecar receipt ${section.receipt} (${error.message}); stageSidecars must run before the build`); }
|
|
414
|
+
if (!isObject(receipt) || !Array.isArray(receipt.files)) fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `${section.receipt} is not a stage-sidecar receipt`);
|
|
415
|
+
if (receipt.target !== section.target) fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `${section.receipt} was staged for ${receipt.target}, not ${section.target}`);
|
|
416
|
+
const checked = [];
|
|
417
|
+
for (const file of receipt.files.filter((item) => item?.externalBin === true)) {
|
|
418
|
+
if (typeof file.path !== "string" || !file.path.endsWith(`-${UNIVERSAL_TRIPLE}`)) fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `externalBin ${file.name} is staged as ${file.path}, not <name>-${UNIVERSAL_TRIPLE}`);
|
|
419
|
+
const absolute = path.resolve(appRoot, file.path);
|
|
420
|
+
if (path.relative(appRoot, absolute).startsWith("..")) fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `externalBin ${file.name} path escapes the app: ${file.path}`);
|
|
421
|
+
const result = spawnSync("lipo", ["-archs", absolute], { cwd: appRoot, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
422
|
+
const archs = String(result?.stdout ?? "").trim().split(/\s+/).filter(Boolean).sort();
|
|
423
|
+
if (result?.error || result?.status !== 0 || archs.join(" ") !== "arm64 x86_64") fail("RECIPE_BUILD_SIDECAR_NOT_UNIVERSAL", `externalBin ${file.path} must hold exactly arm64 and x86_64, lipo -archs reports ${JSON.stringify(archs.join(" "))}${result?.error ? ` (${result.error.message})` : ""}`, { file: file.path, archs });
|
|
424
|
+
checked.push(file.path);
|
|
358
425
|
}
|
|
359
|
-
|
|
426
|
+
log(`recipe-build: universal sidecars verified (${checked.length} externalBin file(s))`);
|
|
427
|
+
return checked;
|
|
360
428
|
}
|
|
361
429
|
|
|
362
430
|
export function parseRecipeBuildArgs(argv) {
|
package/release-config.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export const LEGACY_EXECUTABLE_EXTENSIONS = Object.freeze([".mjs", ".js", ".cjs"
|
|
|
41
41
|
export const TOP_LEVEL_KEYS = Object.freeze([
|
|
42
42
|
"$schema", "$comment", "schema", "app", "version", "channel", "distribution", "packageManager", "workdir", "targets", "development",
|
|
43
43
|
"deps", "checks", "buildInputs", "hostedWorkflows", "devtools", "releaseSwitches", "qa", "nativeAssembly", "legal", "web", "tests",
|
|
44
|
-
"sidecars", "releaseDependencies", "appStoreConnect", "storage", "cargoProfileExempt", "runtimeArtifacts",
|
|
44
|
+
"sidecars", "assembly", "releaseDependencies", "appStoreConnect", "storage", "cargoProfileExempt", "runtimeArtifacts",
|
|
45
45
|
]);
|
|
46
46
|
export const TEMPLATE_TOKENS = Object.freeze(["app", "version", "home", "localAppData"]);
|
|
47
47
|
|
package/release.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
|
|
|
3
3
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { buildLaunch } from "./build-launch.mjs";
|
|
6
7
|
import { createRequire } from "node:module";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -608,7 +609,7 @@ if (problems.length || prebuildFindings.length) {
|
|
|
608
609
|
const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
|
|
609
610
|
const mode = "package";
|
|
610
611
|
const command = target.package;
|
|
611
|
-
if (!command && opts.platform !== "android" && opts.platform !== "ios" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }))) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
|
|
612
|
+
if (!command && opts.platform !== "android" && opts.platform !== "ios" && !packageCommandOptional(target, resolveProjectType({ appRoot: root, config, target }), { expanded: true })) fail(`${config.app ?? "app"} has no ${opts.platform} ${mode} command`);
|
|
612
613
|
if (opts.upload && !target.publish && !target.upload) {
|
|
613
614
|
fail(`${config.app ?? "app"} has no ${opts.platform} upload config`);
|
|
614
615
|
}
|
|
@@ -1141,14 +1142,16 @@ function sweepStaleRustArtifacts() {
|
|
|
1141
1142
|
}
|
|
1142
1143
|
|
|
1143
1144
|
async function runInstall(pm, cwd) {
|
|
1144
|
-
if (pm === "pnpm")
|
|
1145
|
-
else if (pm === "npm")
|
|
1145
|
+
if (pm === "pnpm") { const t = buildLaunch("pnpm", ["install", "--frozen-lockfile"], { cwd, exists: existsSync }); await run(t.command, t.args, cwd, t.env ?? {}, { install: true }); }
|
|
1146
|
+
else if (pm === "npm") { const t = buildLaunch("npm", ["ci"], { cwd, exists: existsSync }); await run(t.command, t.args, cwd, t.env ?? {}, { install: true }); }
|
|
1146
1147
|
else fail(`unsupported package manager: ${pm}`);
|
|
1147
1148
|
}
|
|
1148
1149
|
|
|
1149
1150
|
async function runPackageScript(pm, script, cwd) {
|
|
1150
1151
|
if (script === false) return;
|
|
1151
|
-
|
|
1152
|
+
// A package script (a check or a web build) goes through the one door (build-launch.mjs): a broker job on a managed host.
|
|
1153
|
+
const target = buildLaunch(pm, ["run", script], { cwd, exists: existsSync });
|
|
1154
|
+
await run(target.command, target.args, cwd, target.env ?? {});
|
|
1152
1155
|
}
|
|
1153
1156
|
|
|
1154
1157
|
// A hook may be one command object or an ordered array of them (e.g. create-mac-updater then lsclean).
|
|
@@ -1162,9 +1165,16 @@ async function runCommands(value, root, options) {
|
|
|
1162
1165
|
for (const command of commandList(value)) await runCommand(command, root, options);
|
|
1163
1166
|
}
|
|
1164
1167
|
|
|
1168
|
+
// Every configured hook (prePackage, the package command and hook, sidecar stages, gradle, the finalizer, postSign, publish) is
|
|
1169
|
+
// arbitrary code that can build, so it goes through the one door (build-launch.mjs): this package's own right-release steps run as
|
|
1170
|
+
// they are (their builds come back through the door), node, pnpm, cargo and rightkit-qa are broker jobs, and anything else is refused
|
|
1171
|
+
// on a broker-managed host (rightkit#1241).
|
|
1165
1172
|
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
1166
1173
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
1167
|
-
|
|
1174
|
+
const commandEnvironment = { ...(await commandEnv(command, root)), ...env };
|
|
1175
|
+
const target = buildLaunch(command.cmd, command.args ?? [], { env: { ...process.env, ...commandEnvironment }, cwd, exists: existsSync });
|
|
1176
|
+
// A direct (Linux) launch carries PATH without RustRight shim directories (build-launch `env`).
|
|
1177
|
+
return run(target.command, target.args, cwd, { ...commandEnvironment, ...(target.env ?? {}) }, {
|
|
1168
1178
|
...command,
|
|
1169
1179
|
captureStdout,
|
|
1170
1180
|
timeoutMs: commandTimeoutMs(command),
|
package/rightkit-versions.json
CHANGED
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"@rightkit/logs": "0.1.6",
|
|
27
27
|
"@rightkit/media-ui": "0.3.4",
|
|
28
28
|
"@rightkit/onboarding": "0.6.3",
|
|
29
|
-
"@rightkit/outdated": "0.
|
|
29
|
+
"@rightkit/outdated": "0.3.0",
|
|
30
30
|
"@rightkit/pdf-ui": "0.2.0",
|
|
31
31
|
"@rightkit/platform-ui": "0.4.0",
|
|
32
32
|
"@rightkit/qa": "0.6.1",
|
|
33
33
|
"@rightkit/qa-chrome": "0.1.12",
|
|
34
34
|
"@rightkit/react-shell": "0.7.2",
|
|
35
|
-
"@rightkit/release": "0.6.
|
|
35
|
+
"@rightkit/release": "0.6.10",
|
|
36
36
|
"@rightkit/settings-ui": "0.7.0",
|
|
37
37
|
"@rightkit/tauri": "0.3.0",
|
|
38
38
|
"@rightkit/tauri-shell": "0.1.3",
|
|
@@ -374,7 +374,9 @@
|
|
|
374
374
|
"0.6.3",
|
|
375
375
|
"0.6.4",
|
|
376
376
|
"0.6.5",
|
|
377
|
-
"0.6.6"
|
|
377
|
+
"0.6.6",
|
|
378
|
+
"0.6.7",
|
|
379
|
+
"0.6.8"
|
|
378
380
|
],
|
|
379
381
|
"@rightkit/settings-ui": [
|
|
380
382
|
"0.1.0",
|
|
@@ -424,6 +426,9 @@
|
|
|
424
426
|
],
|
|
425
427
|
"@rightkit/updates": [
|
|
426
428
|
"0.2.4"
|
|
429
|
+
],
|
|
430
|
+
"@rightkit/outdated": [
|
|
431
|
+
"0.2.0"
|
|
427
432
|
]
|
|
428
433
|
},
|
|
429
434
|
"cargo": {
|
|
@@ -440,6 +445,7 @@
|
|
|
440
445
|
"rightkit-pdf": "0.1.0",
|
|
441
446
|
"rightkit-playback": "0.1.0",
|
|
442
447
|
"rightkit-process": "0.1.0",
|
|
448
|
+
"rightkit-qa": "0.9.1",
|
|
443
449
|
"rightkit-search": "0.1.0",
|
|
444
450
|
"rightkit-tauri": "0.1.0"
|
|
445
451
|
},
|
|
@@ -454,7 +460,7 @@
|
|
|
454
460
|
"rightkit-browser": "0.3.6",
|
|
455
461
|
"rightkit-cache": "0.2.3",
|
|
456
462
|
"rightkit-channel-memory": "0.2.1",
|
|
457
|
-
"rightkit-child-process": "0.5.
|
|
463
|
+
"rightkit-child-process": "0.5.7",
|
|
458
464
|
"rightkit-compose": "0.2.0",
|
|
459
465
|
"rightkit-contracts": "0.1.7",
|
|
460
466
|
"rightkit-crash": "0.3.6",
|
|
@@ -490,7 +496,6 @@
|
|
|
490
496
|
"rightkit-pdf": "0.3.3",
|
|
491
497
|
"rightkit-platform-rules": "0.1.0",
|
|
492
498
|
"rightkit-playback": "0.3.0",
|
|
493
|
-
"rightkit-qa": "0.9.1",
|
|
494
499
|
"rightkit-qa-driver": "0.7.0",
|
|
495
500
|
"rightkit-record": "0.3.3",
|
|
496
501
|
"rightkit-review": "0.4.0",
|
|
@@ -579,7 +584,7 @@
|
|
|
579
584
|
"rightkit-platform-rules": "0.1.0",
|
|
580
585
|
"rightkit-playback": "0.3.0",
|
|
581
586
|
"rightkit-process": "0.4.0",
|
|
582
|
-
"rightkit-qa": "0.9.
|
|
587
|
+
"rightkit-qa": "0.9.1",
|
|
583
588
|
"rightkit-qa-driver": "0.7.0",
|
|
584
589
|
"rightkit-qts": "0.2.6",
|
|
585
590
|
"rightkit-qts-ggml": "0.1.2",
|
|
@@ -985,7 +990,8 @@
|
|
|
985
990
|
"0.5.2",
|
|
986
991
|
"0.5.3",
|
|
987
992
|
"0.5.4",
|
|
988
|
-
"0.5.5"
|
|
993
|
+
"0.5.5",
|
|
994
|
+
"0.5.6"
|
|
989
995
|
],
|
|
990
996
|
"rightkit-hotkeys": [
|
|
991
997
|
"0.1.0",
|