@rightkit/release 0.2.165 → 0.2.173

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.
Files changed (86) hide show
  1. package/README.md +121 -4
  2. package/addon-command.mjs +6 -13
  3. package/android-aab-scan.mjs +265 -0
  4. package/android-dev.mjs +111 -0
  5. package/android-ndk.mjs +77 -0
  6. package/android-play-upload.mjs +262 -0
  7. package/android-release.mjs +344 -0
  8. package/android-test-fixtures.mjs +79 -0
  9. package/apple-profile-create.mjs +363 -0
  10. package/bin/darwin/rightkit-swap +0 -0
  11. package/bin/darwin/rightkit-swap.sign.json +6 -0
  12. package/build-release.mjs +73 -13
  13. package/cargo-guard.mjs +2 -0
  14. package/cargo-profile-contract.mjs +183 -0
  15. package/cargo-target.mjs +8 -0
  16. package/cli/right-release.mjs +16 -4
  17. package/committed-source.mjs +1 -1
  18. package/dev-artifact-refresh.mjs +176 -0
  19. package/dev-artifact-support.mjs +61 -0
  20. package/development-signing.mjs +71 -4
  21. package/development.mjs +93 -47
  22. package/direct-bootstrap.mjs +26 -41
  23. package/disk-command.mjs +298 -5
  24. package/disk-policy.json +7 -5
  25. package/disk-policy.mjs +105 -16
  26. package/disk-stable-root.mjs +19 -16
  27. package/engine-locator.mjs +116 -0
  28. package/external-bin.mjs +133 -0
  29. package/github-release.mjs +2 -2
  30. package/github-updater-release.mjs +154 -0
  31. package/hardeningscan.mjs +1 -1
  32. package/in-place-install.mjs +56 -0
  33. package/install-app-windows.mjs +63 -0
  34. package/install-app.mjs +53 -23
  35. package/install-tauri-devid.mjs +172 -0
  36. package/inventory-check.mjs +1 -1
  37. package/local-publish.mjs +11 -1
  38. package/mac-package-recipe.mjs +304 -0
  39. package/mac-signing-matrix.mjs +23 -11
  40. package/managed-cargo.mjs +6 -0
  41. package/media-sidecar-catalog.mjs +45 -0
  42. package/media-sidecar.mjs +306 -0
  43. package/ownership-approval-command.mjs +10 -44
  44. package/ownership-approval.mjs +3 -42
  45. package/ownership-catalog-config.json +2 -1
  46. package/ownership-catalog.mjs +4 -2
  47. package/ownership-command.mjs +7 -9
  48. package/ownership-dependency-allowlist.json +25 -1
  49. package/ownership-evidence.mjs +8 -5
  50. package/ownership-git-inventory.mjs +41 -7
  51. package/ownership-lockfiles.mjs +91 -8
  52. package/ownership-mandatory-set.json +36 -16
  53. package/ownership-report.mjs +5 -6
  54. package/ownership-scan-worker.mjs +2 -2
  55. package/ownership-verdict.mjs +10 -7
  56. package/ownership.mjs +47 -29
  57. package/package-recipe-targets.mjs +95 -0
  58. package/package.json +7 -4
  59. package/portable-development.mjs +32 -115
  60. package/preflight.mjs +4 -6
  61. package/project-type.mjs +1 -1
  62. package/publication-artifacts.mjs +34 -0
  63. package/publish-update.mjs +72 -11
  64. package/qa-adoption-exemptions.json +5 -0
  65. package/qa-build-features.mjs +7 -0
  66. package/qa-contract.mjs +182 -0
  67. package/qa-control-scan.mjs +3 -3
  68. package/release-qa-command.mjs +38 -12
  69. package/release-qa-installed.mjs +136 -0
  70. package/release-qa-receipt.mjs +23 -2
  71. package/release.mjs +285 -14
  72. package/rightkit-ownership.json +537 -5
  73. package/rightkit-sync.mjs +27 -1
  74. package/rightkit-versions.json +111 -63
  75. package/runtime-budget-classes.json +28 -0
  76. package/runtime-budgets.mjs +4 -1
  77. package/sign-candidate.mjs +161 -0
  78. package/sign-dev-artifact.mjs +198 -0
  79. package/stage-binary.mjs +60 -0
  80. package/stage-sidecar-command.mjs +41 -0
  81. package/stage-sidecar.mjs +424 -0
  82. package/swap-helper-bundle.mjs +105 -0
  83. package/swift-mac-dmg.mjs +2 -1
  84. package/templates/cargo-profiles.toml +28 -0
  85. package/upload-release.mjs +11 -1
  86. package/win-package-recipe.mjs +321 -0
@@ -0,0 +1,424 @@
1
+ // Declarative engine and sidecar staging for Tauri externalBin (bogusyogi/rightkit#345).
2
+ //
3
+ // Eight apps hand-rolled the same script: build an engine through `rightkit cargo`, find the executable, copy it to
4
+ // src-tauri/binaries/<name>-<target-triple>[.exe], stage ffmpeg, stage onnxruntime, copy model resources. This module is
5
+ // that script, driven by a `sidecars` list in right-release.config.mjs:
6
+ //
7
+ // sidecars: [
8
+ // { kind: "cargo-bin", package: "mailright-engine", bin?, name?, profile?: "release", features?, manifestPath?, sha256?, platforms? },
9
+ // { kind: "ffmpeg", variant?, tools?, notices: { dir, sourceOfferContact }, catalog, stage?, bundleDir? },
10
+ // { kind: "onnxruntime", version, catalog, resourceDir?: "src-tauri/resources/runtime", as?, licenseDir?, cacheDir? },
11
+ // { kind: "resource", from, to, sha256?, platforms? },
12
+ // ]
13
+ //
14
+ // It never signs (release.mjs signs staged files) and never calls bare cargo. Every effect goes through `deps`.
15
+ import { createHash } from "node:crypto";
16
+ import nodeFs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
20
+ import { resolveCommand } from "./development.mjs";
21
+ import {
22
+ planMediaSidecar, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
23
+ } from "./media-sidecar.mjs";
24
+
25
+ export const STAGE_SIDECAR_CONTRACT = "rightkit-stage-sidecar-v1";
26
+ export const STAGE_SIDECAR_RECEIPT_NAME = "stage-sidecar.json";
27
+ export const DEFAULT_STAGE_DIR = "src-tauri/binaries";
28
+ export const DEFAULT_ORT_RESOURCE_DIR = "src-tauri/resources/runtime";
29
+
30
+ export class StageSidecarError extends Error {
31
+ constructor(code, message, details = {}) {
32
+ super(message, details.cause ? { cause: details.cause } : undefined);
33
+ this.name = "StageSidecarError";
34
+ this.code = code;
35
+ const { cause: _cause, ...rest } = details;
36
+ this.details = rest;
37
+ }
38
+ }
39
+
40
+ const fail = (code, message, details) => { throw new StageSidecarError(code, message, details); };
41
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
42
+ const SHA256 = /^[0-9a-f]{64}$/;
43
+ const KINDS = new Set(["cargo-bin", "ffmpeg", "onnxruntime", "resource"]);
44
+ const PLATFORMS = new Set(["mac", "win", "linux"]);
45
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
46
+ const KEYS = Object.freeze({
47
+ "cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "platforms"],
48
+ ffmpeg: ["kind", "variant", "tools", "notices", "catalog", "stage", "bundleDir", "platforms"],
49
+ onnxruntime: ["kind", "version", "catalog", "resourceDir", "as", "licenseDir", "cacheDir", "platforms"],
50
+ resource: ["kind", "from", "to", "sha256", "platforms"],
51
+ });
52
+ const TRIPLES = Object.freeze({
53
+ "aarch64-apple-darwin": { platform: "mac", os: "macos", arch: "aarch64" },
54
+ "x86_64-apple-darwin": { platform: "mac", os: "macos", arch: "x86_64" },
55
+ "x86_64-pc-windows-msvc": { platform: "win", os: "windows", arch: "x86_64" },
56
+ "aarch64-pc-windows-msvc": { platform: "win", os: "windows", arch: "aarch64" },
57
+ "x86_64-unknown-linux-gnu": { platform: "linux", os: "linux", arch: "x86_64" },
58
+ "aarch64-unknown-linux-gnu": { platform: "linux", os: "linux", arch: "aarch64" },
59
+ });
60
+
61
+ /** Parse a Rust target triple into { triple, platform, os, arch }. universal-apple-darwin is refused (stage per arch, lipo is the app's). */
62
+ export function parseTargetTriple(target) {
63
+ if (target === "universal-apple-darwin") fail("STAGE_SIDECAR_TARGET_UNSUPPORTED", "universal-apple-darwin is not staged here: stage aarch64-apple-darwin and x86_64-apple-darwin separately and merge with lipo");
64
+ const row = typeof target === "string" ? TRIPLES[target] : undefined;
65
+ if (!row) fail("STAGE_SIDECAR_TARGET_UNSUPPORTED", `unsupported target triple ${JSON.stringify(target)} (${Object.keys(TRIPLES).join(", ")})`);
66
+ return { triple: target, ...row };
67
+ }
68
+
69
+ /** `--target`, then TAURI_ENV_TARGET_TRIPLE, then the host triple from `rightkit rustc -vV`. */
70
+ export function resolveTargetTriple({ target, env = process.env, run = defaultRun, cwd = process.cwd() } = {}) {
71
+ if (target) return target;
72
+ if (env.TAURI_ENV_TARGET_TRIPLE) return env.TAURI_ENV_TARGET_TRIPLE;
73
+ const result = run("rightkit", ["rustc", "-vV"], { cwd, env });
74
+ const host = result?.stdout?.match(/^host:\s*(.+)$/m)?.[1]?.trim();
75
+ if (!host || result.status !== 0) fail("STAGE_SIDECAR_TARGET_UNKNOWN", "cannot determine the host triple (rightkit rustc -vV failed); pass a target");
76
+ return host;
77
+ }
78
+
79
+ function relativePath(value, label, errors) {
80
+ if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
81
+ const normalized = value.replaceAll("\\", "/");
82
+ if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) { errors.push(`${label} must be relative and stay inside the app: ${value}`); return undefined; }
83
+ return normalized.replace(/\/+$/, "");
84
+ }
85
+
86
+ /**
87
+ * Validate and normalise the manifest. `raw` is the `sidecars` array, or { sidecars, stageDir?, receiptPath? }.
88
+ * Every problem is reported at once as STAGE_SIDECAR_CONFIG_INVALID. Entries whose `platforms` excludes the target are
89
+ * kept (shape-checked) with skipped: true.
90
+ */
91
+ export function validateSidecarManifest(raw, { target } = {}) {
92
+ const t = parseTargetTriple(target);
93
+ const errors = [];
94
+ const wrapper = Array.isArray(raw) ? { sidecars: raw } : raw;
95
+ if (!isObject(wrapper)) fail("STAGE_SIDECAR_CONFIG_INVALID", "sidecars must be an array (or { sidecars, stageDir?, receiptPath? })");
96
+ for (const key of Object.keys(wrapper)) if (!["sidecars", "stageDir", "receiptPath"].includes(key)) errors.push(`unknown key ${JSON.stringify(key)}`);
97
+ if (!Array.isArray(wrapper.sidecars) || !wrapper.sidecars.length) errors.push("sidecars must be a non-empty array");
98
+ const stageDir = relativePath(wrapper.stageDir ?? DEFAULT_STAGE_DIR, "stageDir", errors);
99
+ const receiptPath = wrapper.receiptPath === undefined
100
+ ? (stageDir ? `${stageDir}/${STAGE_SIDECAR_RECEIPT_NAME}` : undefined)
101
+ : relativePath(wrapper.receiptPath, "receiptPath", errors);
102
+ const entries = [];
103
+ const outputs = new Map();
104
+ const claim = (output, label) => {
105
+ const key = output.toLowerCase();
106
+ if (outputs.has(key)) errors.push(`${label} writes ${output}, which ${outputs.get(key)} already writes`);
107
+ else outputs.set(key, label);
108
+ };
109
+ for (const [index, item] of (Array.isArray(wrapper.sidecars) ? wrapper.sidecars : []).entries()) {
110
+ const label = `sidecars[${index}]`;
111
+ if (!isObject(item) || !KINDS.has(item.kind)) { errors.push(`${label}.kind must be one of ${[...KINDS].join(", ")}`); continue; }
112
+ const where = `${label} (${item.kind})`;
113
+ for (const key of Object.keys(item)) if (!KEYS[item.kind].includes(key)) errors.push(`${where}: unknown key ${JSON.stringify(key)} (allowed: ${KEYS[item.kind].join(", ")})`);
114
+ let platforms;
115
+ if (item.platforms !== undefined) {
116
+ if (!Array.isArray(item.platforms) || !item.platforms.length || item.platforms.some((p) => !PLATFORMS.has(p))) errors.push(`${where}: platforms must be a non-empty list drawn from ${[...PLATFORMS].join(", ")}`);
117
+ else platforms = [...item.platforms];
118
+ }
119
+ const skipped = platforms ? !platforms.includes(t.platform) : false;
120
+ const sha = (value, name) => {
121
+ if (value !== undefined && !SHA256.test(value ?? "")) errors.push(`${where}: ${name} must be a lowercase 64-hex SHA-256`);
122
+ };
123
+ const entry = { kind: item.kind, index, skipped, ...(platforms ? { platforms } : {}) };
124
+ if (item.kind === "cargo-bin") {
125
+ if (typeof item.package !== "string" || !NAME.test(item.package)) errors.push(`${where}: package must be a Cargo package name`);
126
+ const bin = item.bin ?? item.package;
127
+ if (typeof bin !== "string" || !NAME.test(bin)) errors.push(`${where}: bin must be a binary target name`);
128
+ const name = item.name ?? bin;
129
+ if (typeof name !== "string" || !NAME.test(name) || /\.exe$/i.test(name) || name !== path.basename(name)) errors.push(`${where}: name must be a bare sidecar name (no path, no .exe, no target triple)`);
130
+ const profile = item.profile ?? "release";
131
+ if (typeof profile !== "string" || !/^[a-z][a-z0-9_-]*$/.test(profile)) errors.push(`${where}: profile must be a Cargo profile name`);
132
+ const features = item.features ?? [];
133
+ if (!Array.isArray(features) || features.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_/-]+$/.test(f))) errors.push(`${where}: features must be a list of feature names (package/feature allowed)`);
134
+ const manifestPath = item.manifestPath === undefined ? undefined : relativePath(item.manifestPath, `${where}: manifestPath`, errors);
135
+ sha(item.sha256, "sha256");
136
+ if (typeof name === "string" && stageDir && !skipped) claim(stagedFileName(name, t.os, t.arch), where);
137
+ Object.assign(entry, { package: item.package, bin, name, profile, features: [...features], ...(manifestPath ? { manifestPath } : {}), ...(item.sha256 ? { sha256: item.sha256 } : {}) });
138
+ } else if (item.kind === "ffmpeg") {
139
+ const { kind: _kind, platforms: _platforms, ...rest } = item;
140
+ if (rest.stage !== undefined && isObject(rest.stage) && rest.stage.cmd === "cargo") errors.push(`${where}: stage.cmd must not be bare cargo; use { cmd: "rightkit", args: ["cargo", ...] }`);
141
+ if (!skipped) {
142
+ try {
143
+ const config = validateMediaSidecarConfig({ ...rest, arch: t.arch, stageDir }, { platform: t.platform, hostArch: t.arch });
144
+ for (const tool of config.tools) claim(stagedFileName(tool, t.os, t.arch), where);
145
+ entry.media = config;
146
+ } catch (error) { errors.push(...String(error.message).split("\n").filter((line) => line.startsWith("- ")).map((line) => `${where}: ${line.slice(2)}`).concat(error.message.startsWith("invalid") ? [] : [`${where}: ${error.message}`])); }
147
+ }
148
+ } else if (item.kind === "onnxruntime") {
149
+ if (typeof item.version !== "string" || !/^\d+\.\d+\.\d+$/.test(item.version)) errors.push(`${where}: version must be x.y.z`);
150
+ const resourceDir = relativePath(item.resourceDir ?? DEFAULT_ORT_RESOURCE_DIR, `${where}: resourceDir`, errors);
151
+ const licenseDir = relativePath(item.licenseDir ?? (resourceDir ? `${resourceDir}/LICENSES` : "x"), `${where}: licenseDir`, errors);
152
+ const cacheDir = relativePath(item.cacheDir ?? ".cache/onnxruntime", `${where}: cacheDir`, errors);
153
+ const catalog = item.catalog === undefined ? undefined : relativePath(item.catalog, `${where}: catalog`, errors);
154
+ if (item.as !== undefined && (typeof item.as !== "string" || !NAME.test(item.as))) errors.push(`${where}: as must be a bare file name`);
155
+ Object.assign(entry, { version: item.version, resourceDir, licenseDir, cacheDir, ...(catalog ? { catalog } : {}), ...(item.as ? { as: item.as } : {}) });
156
+ } else {
157
+ const from = relativePath(item.from, `${where}: from`, errors);
158
+ const to = relativePath(item.to, `${where}: to`, errors);
159
+ sha(item.sha256, "sha256");
160
+ if (to && !skipped) claim(to, where);
161
+ Object.assign(entry, { from, to, ...(item.sha256 ? { sha256: item.sha256 } : {}) });
162
+ }
163
+ entries.push(entry);
164
+ }
165
+ if (errors.length) fail("STAGE_SIDECAR_CONFIG_INVALID", `invalid sidecars configuration:\n${errors.map((line) => `- ${line}`).join("\n")}`, { errors });
166
+ return { target: t, stageDir, receiptPath, entries };
167
+ }
168
+
169
+ const sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
170
+ const profileDirectory = (profile) => (profile === "dev" || profile === "test" ? "debug" : profile === "bench" ? "release" : profile);
171
+ const tail = (text, lines = 12) => String(text ?? "").trim().split("\n").slice(-lines).join("\n");
172
+
173
+ /** Default process runner: `rightkit` resolves through the installed runtime client (as the apps' scripts did). */
174
+ export function defaultRun(command, args, { cwd, env = process.env } = {}) {
175
+ const launch = command === "rightkit" ? resolveCommand("rightkit", args, { root: cwd, env }) : { command, args };
176
+ return nodeSpawnSync(launch.command, launch.args, {
177
+ cwd, env: { ...env, ...(launch.env ?? {}) }, encoding: "utf8", maxBuffer: 512 * 1024 * 1024, shell: false, windowsHide: true, stdio: ["inherit", "pipe", "inherit"],
178
+ });
179
+ }
180
+
181
+ /** Default archive member extraction (bsdtar reads tgz and zip/nupkg): the one member whose basename is `fileName`. */
182
+ export function defaultExtractMember({ archive, fileName, destDir, run = defaultRun }) {
183
+ const listing = run("tar", ["-tf", archive], { cwd: path.dirname(archive) });
184
+ if (listing.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot list ${archive}`);
185
+ const members = String(listing.stdout).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && path.posix.basename(line) === fileName);
186
+ if (members.length !== 1) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${archive} has ${members.length} members named ${fileName}, expected exactly one`);
187
+ const extracted = run("tar", ["-xf", archive, "-C", destDir, members[0]], { cwd: path.dirname(archive) });
188
+ if (extracted.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot extract ${members[0]} from ${archive}`);
189
+ return path.join(destDir, ...members[0].split("/"));
190
+ }
191
+
192
+ async function defaultFetchBytes(url) {
193
+ if (!url.startsWith("https://")) fail("STAGE_SIDECAR_CONFIG_INVALID", `refusing a non-https archive URL: ${url}`);
194
+ const response = await fetch(url);
195
+ if (!response.ok) fail("STAGE_SIDECAR_DOWNLOAD_FAILED", `GET ${url} failed with ${response.status}`);
196
+ return Buffer.from(await response.arrayBuffer());
197
+ }
198
+
199
+ function readJsonCatalog(entryCatalog, appRoot, fs, what) {
200
+ const file = path.resolve(appRoot, entryCatalog);
201
+ let bytes;
202
+ try { bytes = fs.readFileSync(file); } catch (error) { fail("STAGE_SIDECAR_CATALOG_UNREADABLE", `cannot read the ${what} catalog ${file}`, { cause: error }); }
203
+ try { return { catalog: JSON.parse(String(bytes)), catalogBytes: bytes, crateDir: path.resolve(path.dirname(file), "..") }; } catch (error) { fail("STAGE_SIDECAR_CATALOG_UNREADABLE", `the ${what} catalog ${file} is not JSON`, { cause: error }); }
204
+ }
205
+
206
+ function artifactFromStream(stdout, bin) {
207
+ let found;
208
+ for (const line of String(stdout ?? "").split("\n")) {
209
+ if (!line.startsWith("{")) continue;
210
+ try {
211
+ const message = JSON.parse(line);
212
+ if (message.reason === "compiler-artifact" && message.executable && message.target?.name === bin && (message.target.kind ?? ["bin"]).includes("bin")) found = message.executable;
213
+ } catch { /* not a cargo message */ }
214
+ }
215
+ return found;
216
+ }
217
+
218
+ /** Candidate output paths, in lookup order: the explicit-target layout (broker) first, then the plain layout. */
219
+ export function builtBinaryCandidates({ targetDir, triple, profile, bin, windows }) {
220
+ const file = windows ? `${bin}.exe` : bin;
221
+ const dir = profileDirectory(profile);
222
+ return [path.join(targetDir, triple, dir, file), path.join(targetDir, dir, file)];
223
+ }
224
+
225
+ function copyInto(ctx, source, dest, { executable }) {
226
+ const { fs, windows } = ctx;
227
+ const sourceSha256 = sha256Of(fs.readFileSync(source));
228
+ let changed = true;
229
+ if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === sourceSha256) changed = false;
230
+ if (changed) {
231
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
232
+ const temporary = `${dest}.tmp-${ctx.pid}`;
233
+ fs.copyFileSync(source, temporary);
234
+ if (executable && !windows) fs.chmodSync(temporary, 0o755);
235
+ fs.renameSync(temporary, dest);
236
+ }
237
+ const sha256 = sha256Of(fs.readFileSync(dest));
238
+ if (sha256 !== sourceSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `staged ${dest} differs from its source ${source}`, { expected: sourceSha256, actual: sha256 });
239
+ return { sha256, sizeBytes: fs.statSync(dest).size, changed };
240
+ }
241
+
242
+ const rel = (ctx, file) => path.relative(ctx.appRoot, file).replaceAll("\\", "/");
243
+
244
+ function stageCargoBin(entry, ctx) {
245
+ const { target, run, appRoot, env, fs } = ctx;
246
+ const args = ["cargo", "build", "--locked", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : []),
247
+ "-p", entry.package, "--bin", entry.bin, "--target", target.triple, "--profile", entry.profile,
248
+ ...(entry.features.length ? ["--features", entry.features.join(",")] : []), "--message-format=json-render-diagnostics"];
249
+ ctx.log(`stage-sidecar: rightkit ${args.join(" ")}`);
250
+ const built = run("rightkit", args, { cwd: appRoot, env });
251
+ if (built?.error || built?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `rightkit cargo build of ${entry.package} failed (${built?.error?.message ?? `exit ${built?.status}`})`, { package: entry.package, stderr: tail(built?.stderr) });
252
+ let source = artifactFromStream(built.stdout, entry.bin);
253
+ if (!source || !fs.existsSync(source)) {
254
+ const meta = run("rightkit", ["cargo", "metadata", "--format-version", "1", "--no-deps", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : [])], { cwd: appRoot, env });
255
+ let targetDir;
256
+ try { targetDir = JSON.parse(meta.stdout).target_directory; } catch { /* refused below */ }
257
+ if (meta.status !== 0 || typeof targetDir !== "string" || !path.isAbsolute(targetDir)) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and its target directory cannot be read`, { bin: entry.bin });
258
+ const candidates = builtBinaryCandidates({ targetDir, triple: target.triple, profile: entry.profile, bin: entry.bin, windows: ctx.windows });
259
+ source = candidates.find((file) => fs.existsSync(file));
260
+ if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build did not produce ${entry.bin} (looked in ${candidates.join(", ")})`, { bin: entry.bin, candidates });
261
+ }
262
+ const sourceSha256 = sha256Of(fs.readFileSync(source));
263
+ if (entry.sha256 && entry.sha256 !== sourceSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.bin} built to ${sourceSha256}, expected ${entry.sha256}`, { expected: entry.sha256, actual: sourceSha256 });
264
+ const dest = path.resolve(appRoot, ctx.stageDir, stagedFileName(entry.name, target.os, target.arch));
265
+ const copied = copyInto(ctx, source, dest, { executable: true });
266
+ return [{ kind: "cargo-bin", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: true }];
267
+ }
268
+
269
+ async function stageFfmpeg(entry, ctx) {
270
+ const { appRoot, run, env, fs, target } = ctx;
271
+ const config = entry.media;
272
+ try {
273
+ let resolved = await ctx.loadMediaCatalog({ config, appRoot, entry });
274
+ if (!resolved) {
275
+ if (!config.catalog) fail("STAGE_SIDECAR_CATALOG_UNREADABLE", "ffmpeg needs a catalog path (the rightkit-ffmpeg artifacts.json) or deps.loadMediaCatalog");
276
+ resolved = readJsonCatalog(config.catalog, appRoot, fs, "rightkit-ffmpeg");
277
+ }
278
+ const plan = planMediaSidecar({ config, catalog: resolved.catalog, crateDir: resolved.crateDir, crateVersion: resolved.crateVersion, catalogBytes: resolved.catalogBytes, root: appRoot });
279
+ if (config.stage) {
280
+ ctx.log(`stage-sidecar: ${config.stage.cmd} ${(config.stage.args ?? []).join(" ")}`);
281
+ const staged = run(config.stage.cmd, config.stage.args ?? [], { cwd: appRoot, env });
282
+ if (staged?.error || staged?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `ffmpeg stage command failed (${staged?.error?.message ?? `exit ${staged?.status}`})`);
283
+ }
284
+ const noticeFiles = writeMediaSidecarNotices(plan, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync });
285
+ const verified = verifyStagedSidecars(plan, { exists: fs.existsSync, stat: (file) => fs.statSync(file), hashFile: (file) => sha256Of(fs.readFileSync(file)) });
286
+ const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
287
+ return verified.map((file) => {
288
+ if (file.preSignSha256 !== file.contentSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${file.tool} staged as ${file.preSignSha256}, the approved catalog row ${file.artifactId} says ${file.contentSha256}`, { expected: file.contentSha256, actual: file.preSignSha256 });
289
+ return { kind: "ffmpeg", name: file.tool, path: rel(ctx, file.stagedPath), sha256: file.preSignSha256, sizeBytes: file.preSignSizeBytes, changed: null, catalogRow: file.artifactId, version: file.version, catalogContentSha256: file.contentSha256, externalBin: true, license: "LGPL-2.1-or-later", notices };
290
+ });
291
+ } catch (error) {
292
+ if (error instanceof StageSidecarError) throw error;
293
+ return fail("STAGE_SIDECAR_FFMPEG_FAILED", `ffmpeg sidecar (${target.triple}): ${error.message}`, { cause: error, causeCode: error.code });
294
+ }
295
+ }
296
+
297
+ async function stageOnnxRuntime(entry, ctx) {
298
+ const { appRoot, fs, target } = ctx;
299
+ const loaded = await (ctx.loadNativeCatalog({ entry, appRoot }) ?? (entry.catalog ? readJsonCatalog(entry.catalog, appRoot, fs, "native-libraries") : undefined));
300
+ if (!loaded) fail("STAGE_SIDECAR_CATALOG_UNREADABLE", "onnxruntime needs a catalog path (the rightkit-ffmpeg native-libraries.json) or deps.loadNativeCatalog");
301
+ const rows = (loaded.catalog?.libraries ?? []).filter((row) => row.library === "onnxruntime" && row.version === entry.version && row.os === target.os && row.arch === target.arch);
302
+ const where = `onnxruntime ${entry.version} ${target.os}/${target.arch}`;
303
+ if (rows.length === 0) fail("STAGE_SIDECAR_NOT_APPROVED", `the native catalog has no ${where} row`);
304
+ if (rows.length > 1) fail("STAGE_SIDECAR_NOT_APPROVED", `the native catalog has ${rows.length} ${where} rows`);
305
+ const [row] = rows;
306
+ if (!["approved", "pinned"].includes(row.status)) fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} is ${row.status}, not approved or pinned`);
307
+ for (const key of ["sha256", "artifactFileName"]) if (typeof row[key] !== "string" || !row[key]) fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} has no ${key}`);
308
+ if (!SHA256.test(row.sha256) || !SHA256.test(row.archive?.sha256 ?? "") || typeof row.archive?.url !== "string") fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} lacks a valid library digest or archive url and digest`);
309
+ if (!Array.isArray(row.noticeFiles) || !row.noticeFiles.length) fail("STAGE_SIDECAR_NOTICE_MISSING", `the ${where} row ${row.id} lists no notice files; the licence must ship with the library`);
310
+ const crateDir = loaded.crateDir;
311
+ const notices = row.noticeFiles.map((file) => {
312
+ const normalized = String(file).replaceAll("\\", "/");
313
+ if (path.isAbsolute(file) || normalized.split("/").includes("..")) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file path escapes the catalog crate: ${file}`);
314
+ return { source: path.join(crateDir, ...normalized.split("/")), dest: path.resolve(appRoot, entry.licenseDir, path.posix.basename(normalized)) };
315
+ });
316
+ for (const { source } of notices) if (!fs.existsSync(source)) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file not found: ${source}`);
317
+
318
+ const outName = entry.as ?? row.artifactFileName;
319
+ const dest = path.resolve(appRoot, entry.resourceDir, outName);
320
+ if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === row.sha256) {
321
+ ctx.log(`stage-sidecar: ${rel(ctx, dest)} already staged`);
322
+ } else {
323
+ const archive = path.resolve(appRoot, entry.cacheDir, path.posix.basename(new URL(row.archive.url).pathname));
324
+ if (!(fs.existsSync(archive) && sha256Of(fs.readFileSync(archive)) === row.archive.sha256)) {
325
+ const bytes = await ctx.fetchBytes(row.archive.url);
326
+ const actual = sha256Of(bytes);
327
+ if (actual !== row.archive.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${row.archive.url} downloaded as ${actual}, the catalog pins ${row.archive.sha256}; nothing staged`, { expected: row.archive.sha256, actual });
328
+ fs.mkdirSync(path.dirname(archive), { recursive: true });
329
+ fs.writeFileSync(archive, bytes);
330
+ }
331
+ const scratch = ctx.makeTempDir();
332
+ try {
333
+ const library = await ctx.extractMember({ archive, fileName: row.artifactFileName, destDir: scratch });
334
+ const actual = sha256Of(fs.readFileSync(library));
335
+ if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${row.artifactFileName} extracted as ${actual}, the catalog pins ${row.sha256}; nothing staged`, { expected: row.sha256, actual });
336
+ copyInto(ctx, library, dest, { executable: false });
337
+ } finally {
338
+ ctx.removeDir(scratch);
339
+ }
340
+ }
341
+ const noticeOut = notices.map(({ source, dest: out }) => {
342
+ fs.mkdirSync(path.dirname(out), { recursive: true });
343
+ fs.copyFileSync(source, out);
344
+ return { name: path.basename(out), path: rel(ctx, out), sha256: sha256Of(fs.readFileSync(out)) };
345
+ });
346
+ return [{ kind: "onnxruntime", name: outName, path: rel(ctx, dest), sha256: row.sha256, sizeBytes: fs.statSync(dest).size, changed: null, catalogRow: row.id, version: row.version, archiveSha256: row.archive.sha256, license: row.licenseSpdx ?? null, notices: noticeOut, externalBin: false }];
347
+ }
348
+
349
+ function stageResource(entry, ctx) {
350
+ const { appRoot, fs } = ctx;
351
+ const source = path.resolve(appRoot, entry.from);
352
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) fail("STAGE_SIDECAR_RESOURCE_MISSING", `resource ${entry.from} is not a file (directories are not staged; list each file)`, { from: entry.from });
353
+ const actual = sha256Of(fs.readFileSync(source));
354
+ if (entry.sha256 && entry.sha256 !== actual) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.from} is ${actual}, expected ${entry.sha256}`, { expected: entry.sha256, actual });
355
+ const dest = path.resolve(appRoot, entry.to);
356
+ const copied = copyInto(ctx, source, dest, { executable: false });
357
+ return [{ kind: "resource", name: path.basename(dest), path: rel(ctx, dest), ...copied, sourceSha256: actual, from: entry.from, externalBin: false }];
358
+ }
359
+
360
+ /**
361
+ * Stage every configured sidecar for `target` (a Rust triple) and write the staging receipt.
362
+ * Returns { receipt, receiptPath, files, externalBinNames }. `deps` (all optional) injects every effect:
363
+ * fs, run, fetchBytes, extractMember, loadMediaCatalog, loadNativeCatalog, makeTempDir, removeDir, now, pid, log, env.
364
+ */
365
+ export async function stageSidecars({ appRoot, manifest, target, deps = {} } = {}) {
366
+ if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("STAGE_SIDECAR_CONFIG_INVALID", "appRoot must be an absolute path");
367
+ const plan = validateSidecarManifest(manifest, { target });
368
+ const fs = deps.fs ?? nodeFs;
369
+ const env = deps.env ?? process.env;
370
+ const run = deps.run ?? defaultRun;
371
+ const ctx = {
372
+ appRoot, target: plan.target, stageDir: plan.stageDir, fs, env, run,
373
+ windows: plan.target.os === "windows",
374
+ pid: deps.pid ?? process.pid,
375
+ log: deps.log ?? (() => {}),
376
+ fetchBytes: deps.fetchBytes ?? defaultFetchBytes,
377
+ extractMember: deps.extractMember ?? ((args) => defaultExtractMember({ ...args, run })),
378
+ loadMediaCatalog: deps.loadMediaCatalog ?? (() => undefined),
379
+ loadNativeCatalog: deps.loadNativeCatalog ?? (() => undefined),
380
+ makeTempDir: deps.makeTempDir ?? (() => nodeFs.mkdtempSync(path.join(os.tmpdir(), "rightkit-stage-"))),
381
+ removeDir: deps.removeDir ?? ((dir) => nodeFs.rmSync(dir, { recursive: true, force: true })),
382
+ };
383
+ const files = [];
384
+ const skipped = [];
385
+ for (const entry of plan.entries) {
386
+ if (entry.skipped) { skipped.push({ kind: entry.kind, index: entry.index, platforms: entry.platforms }); continue; }
387
+ const staged = entry.kind === "cargo-bin" ? stageCargoBin(entry, ctx)
388
+ : entry.kind === "ffmpeg" ? await stageFfmpeg(entry, ctx)
389
+ : entry.kind === "onnxruntime" ? await stageOnnxRuntime(entry, ctx)
390
+ : stageResource(entry, ctx);
391
+ files.push(...staged);
392
+ }
393
+ const receipt = {
394
+ schema: 1,
395
+ contract: STAGE_SIDECAR_CONTRACT,
396
+ target: plan.target.triple,
397
+ os: plan.target.os,
398
+ arch: plan.target.arch,
399
+ stagedAt: (deps.now ?? (() => new Date().toISOString()))(),
400
+ files,
401
+ skipped,
402
+ };
403
+ validateStageSidecarReceipt(receipt, { target: plan.target.triple });
404
+ const receiptFile = path.resolve(appRoot, plan.receiptPath);
405
+ fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
406
+ fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
407
+ return { receipt, receiptPath: receiptFile, files, externalBinNames: files.filter((file) => file.externalBin).map((file) => file.name) };
408
+ }
409
+
410
+ /** Structural check of a staging receipt; `expected` may pin target and names. */
411
+ export function validateStageSidecarReceipt(receipt, expected = {}) {
412
+ const bad = (message) => fail("STAGE_SIDECAR_RECEIPT_INVALID", `stage sidecar receipt: ${message}`);
413
+ if (!isObject(receipt) || receipt.schema !== 1 || receipt.contract !== STAGE_SIDECAR_CONTRACT) bad("unrecognized contract");
414
+ if (!Array.isArray(receipt.files)) bad("no files array");
415
+ for (const file of receipt.files) {
416
+ if (typeof file.name !== "string" || !file.name || typeof file.path !== "string" || !file.path) bad("file without a name or path");
417
+ if (!SHA256.test(file.sha256 ?? "")) bad(`${file.name}.sha256 must be a SHA-256`);
418
+ if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes <= 0) bad(`${file.name}.sizeBytes must be positive`);
419
+ }
420
+ if (new Set(receipt.files.map((file) => file.path)).size !== receipt.files.length) bad("duplicate staged path");
421
+ if (expected.target && receipt.target !== expected.target) bad(`target ${receipt.target} is not ${expected.target}`);
422
+ if (expected.names && JSON.stringify([...expected.names].sort()) !== JSON.stringify(receipt.files.filter((file) => file.externalBin).map((file) => file.name).sort())) bad("externalBin names differ from the configuration");
423
+ return receipt;
424
+ }
@@ -0,0 +1,105 @@
1
+ // The Developer-ID-signed `bin/darwin/rightkit-swap` that @rightkit/release ships (Orthic-Labs/rightkit#258).
2
+ // rightkit-swap is the bin target of crates/rightkit-fs (renamex_np RENAME_SWAP). CI has no Apple signing secrets, so
3
+ // @rightkit/release is published through `right-release publish npm --local` on the Mac, which calls
4
+ // `prepareSwapHelper` before packing. install-app calls `assertShippedSwapHelperSigned` before it trusts the helper.
5
+ import crypto from "node:crypto";
6
+ import nodeFs from "node:fs";
7
+ import path from "node:path";
8
+ import { execFileSync as nodeExecFileSync, spawnSync as nodeSpawnSync } from "./background-process.mjs";
9
+ import { signMacosEntries } from "./mac-sign-core.mjs";
10
+ import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
11
+ import { runMacCodesignCommands } from "./macos-signing-session.mjs";
12
+
13
+ export const SWAP_HELPER_PACKAGE = "@rightkit/release";
14
+ export const SWAP_HELPER_BIN = ["bin", "darwin", "rightkit-swap"];
15
+ export const SWAP_HELPER_RECEIPT = ["bin", "darwin", "rightkit-swap.sign.json"];
16
+ // rightkit-swap is gated on the crate feature "fs" (crates/rightkit-fs/Cargo.toml [[bin]] required-features).
17
+ export const SWAP_HELPER_CARGO_ARGS = ["build", "--release", "-p", "rightkit-fs", "--features", "fs", "--bin", "rightkit-swap"];
18
+
19
+ /** Typed error. Codes: SWAP_HELPER_BUILD_FAILED, SWAP_HELPER_SIGN_FAILED, SWAP_HELPER_MISSING, SWAP_HELPER_UNSIGNED. */
20
+ export class SwapHelperError extends Error {
21
+ constructor(code, message) { super(message); this.name = "SwapHelperError"; this.code = code; }
22
+ }
23
+
24
+ const helperPath = (packageDir) => path.join(packageDir, ...SWAP_HELPER_BIN);
25
+ const receiptPath = (packageDir) => path.join(packageDir, ...SWAP_HELPER_RECEIPT);
26
+ const sha256 = (fs, file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
27
+ const tail = (text, lines = 30) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
28
+
29
+ /** True when `codesign --verify --strict` accepts the file. Only meaningful on macOS. */
30
+ export function isSwapHelperSigned(file, { execFileSync = nodeExecFileSync } = {}) {
31
+ try { execFileSync("codesign", ["--verify", "--strict", file], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); return true; }
32
+ catch { return false; }
33
+ }
34
+
35
+ /**
36
+ * Local-pack step. darwin: build through the managed cargo wrapper (`run("cargo", ...)` is `rightkit cargo` in
37
+ * local-publish), copy to <packageDir>/bin/darwin/rightkit-swap, Developer ID sign with the shared identity
38
+ * resolver and `macCodesignArgs` (--options runtime --timestamp), verify strictly, write a sha256 receipt.
39
+ * Anywhere else: refuse unless the helper is present and its receipt matches its bytes. Fails closed with SwapHelperError.
40
+ */
41
+ export function prepareSwapHelper({
42
+ workspaceRoot, packageDir = path.join(workspaceRoot, "packages", "release"), platform = process.platform, run, env = process.env, fs = nodeFs,
43
+ resolveIdentity = resolveMacosDeveloperIdIdentity,
44
+ signOne = (codesignArgs) => runMacCodesignCommands([codesignArgs])[0],
45
+ // codesign --verify / -dvv write to stderr: return both streams (as sign-dev-artifact defaultInspect does).
46
+ inspect = (cmd, args) => {
47
+ const result = nodeSpawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
48
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
49
+ if (result.error || result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} failed: ${output.trim() || result.error?.message}`);
50
+ return output;
51
+ },
52
+ log = () => {},
53
+ }) {
54
+ const dest = helperPath(packageDir);
55
+ if (platform !== "darwin") {
56
+ let receipt = null;
57
+ try { receipt = JSON.parse(fs.readFileSync(receiptPath(packageDir), "utf8")); } catch { /* refused below */ }
58
+ if (!fs.existsSync(dest) || receipt?.schema !== 1 || receipt.sha256 !== sha256(fs, dest)) {
59
+ throw new SwapHelperError("SWAP_HELPER_MISSING", `${SWAP_HELPER_PACKAGE} must ship a signed ${SWAP_HELPER_BIN.join("/")}; it is absent or unsigned here (${platform}). Run \`right-release publish npm --package ${SWAP_HELPER_PACKAGE} --local\` on the Mac, where the Developer ID identity lives.`);
60
+ }
61
+ return { file: dest, signed: true, built: false, identity: receipt.identity ?? null };
62
+ }
63
+ const fail = (code, message) => { throw new SwapHelperError(code, message); };
64
+ const built = run("cargo", SWAP_HELPER_CARGO_ARGS, { cwd: workspaceRoot, env });
65
+ if (built.error || built.status !== 0) fail("SWAP_HELPER_BUILD_FAILED", `rightkit cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} failed (${built.error?.message ?? `exit ${built.status}`})\n${tail(`${built.stdout ?? ""}\n${built.stderr ?? ""}`)}`);
66
+ const meta = run("cargo", ["metadata", "--format-version", "1", "--no-deps"], { cwd: workspaceRoot, env });
67
+ let targetDir;
68
+ try { targetDir = JSON.parse(meta.stdout).target_directory; } catch { /* refused below */ }
69
+ if (meta.status !== 0 || typeof targetDir !== "string" || !path.isAbsolute(targetDir)) fail("SWAP_HELPER_BUILD_FAILED", "cannot read the Cargo target directory to find rightkit-swap");
70
+ // The RustRight broker builds with an explicit --target triple (target/<triple>/release); plain cargo uses target/release.
71
+ const triple = `${process.arch === "arm64" ? "aarch64" : "x86_64"}-apple-darwin`;
72
+ const candidates = [path.join(targetDir, triple, "release", "rightkit-swap"), path.join(targetDir, "release", "rightkit-swap")];
73
+ const built_ = candidates.find((file) => fs.existsSync(file));
74
+ if (!built_) fail("SWAP_HELPER_BUILD_FAILED", `the build did not produce rightkit-swap (looked in ${candidates.join(", ")})`);
75
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
76
+ fs.rmSync(dest, { force: true });
77
+ fs.copyFileSync(built_, dest);
78
+ fs.chmodSync(dest, 0o755);
79
+ let identity; let records;
80
+ try {
81
+ identity = resolveIdentity({ env });
82
+ // Same Developer ID Application identity, same argv (--force --options runtime --timestamp --sign) and the same strict verify as sign-macos.
83
+ records = signMacosEntries([{ file: dest, options: [], hardenedRuntime: true }], { identity, signOne, inspect });
84
+ } catch (error) {
85
+ fs.rmSync(dest, { force: true });
86
+ fail("SWAP_HELPER_SIGN_FAILED", `cannot Developer-ID sign ${SWAP_HELPER_BIN.join("/")}: ${error.message}`);
87
+ }
88
+ fs.writeFileSync(receiptPath(packageDir), `${JSON.stringify({ schema: 1, sha256: sha256(fs, dest), identity, codesign: records[0]?.codesign ?? "strict-valid" }, null, 2)}\n`);
89
+ log(`[right-release] signed ${SWAP_HELPER_BIN.join("/")} with ${identity}`);
90
+ return { file: dest, signed: true, built: true, identity };
91
+ }
92
+
93
+ /**
94
+ * Runtime guard (install-app, macOS). A shipped helper that exists must be signed; an unsigned one is a typed error.
95
+ * A helper that is absent is NOT an error here: install-app keeps its documented journaled two-rename fallback and
96
+ * emits `swap-fallback` / `replaced {swap}` so the mode that ran is always logged.
97
+ * @returns {{ present: boolean, file: string|null }}
98
+ */
99
+ export function assertShippedSwapHelperSigned({ file, fs = nodeFs, execFileSync = nodeExecFileSync } = {}) {
100
+ if (!file || !fs.existsSync(file)) return { present: false, file: file ?? null };
101
+ if (!isSwapHelperSigned(file, { execFileSync })) {
102
+ throw new SwapHelperError("SWAP_HELPER_UNSIGNED", `${file} is not validly code-signed, so the atomic swap helper cannot be trusted. Reinstall @rightkit/release from a build published with \`right-release publish npm --package ${SWAP_HELPER_PACKAGE} --local\` on the Mac (Developer ID signed), or set RIGHTKIT_SWAP to a signed helper.`);
103
+ }
104
+ return { present: true, file };
105
+ }
package/swift-mac-dmg.mjs CHANGED
@@ -26,7 +26,8 @@ export function swiftMacDmgConfigError({ target, projectType, appName = "app" })
26
26
 
27
27
  /** Plan for the automatic DMG, or null when the app config builds its own (package step, dmg: false, or no appBeforePackage). */
28
28
  export function swiftMacDmgPlan({ target, projectType, appName = "app", resolve = (p) => path.resolve(p) }) {
29
- if (projectType !== "swiftpm" || !target?.notarize?.appBeforePackage || target.dmg === false || target.package?.cmd) return null;
29
+ // SwiftPM apps, and Tauri apps whose targets.mac.macPackage expanded (package-recipe-targets.mjs, #345).
30
+ if ((projectType !== "swiftpm" && target?.recipe?.kind !== "macPackage") || !target?.notarize?.appBeforePackage || target.dmg === false || target.package?.cmd) return null;
30
31
  const d = target.dmg ?? {};
31
32
  return {
32
33
  app: resolve(target.notarize.app),
@@ -0,0 +1,28 @@
1
+ # RightKit standard Cargo profiles for every app workspace (Adrian, 2026-10-16; Astra-reviewed option C).
2
+ # One development profile: Cargo's built-in `dev`, used by build, test, check, clippy and `right-release dev`.
3
+ # `release-iterate` is retired. Panic behaviour is the same in every app: unwind.
4
+ # Copy these tables verbatim into the app's workspace-root Cargo.toml; the RightKit ownership scanner
5
+ # flags any deviation. Keep the app's existing [profile.release] optimization keys, but set panic = "unwind".
6
+
7
+ [profile.dev]
8
+ opt-level = 1
9
+ debug = "line-tables-only"
10
+ strip = "none"
11
+ debug-assertions = true
12
+ overflow-checks = true
13
+ lto = "off"
14
+ panic = "unwind"
15
+ incremental = true
16
+ codegen-units = 256
17
+ rpath = false
18
+
19
+ # Dependencies: non-incremental so sccache can cache them across apps.
20
+ [profile.dev.package."*"]
21
+ incremental = false
22
+ codegen-units = 16
23
+
24
+ # No test-profile overrides: test inherits dev.
25
+ # The retired release-iterate profile must not appear: delete it and its package overrides.
26
+
27
+ [profile.release]
28
+ panic = "unwind"
@@ -14,6 +14,7 @@ let tier = "";
14
14
  let repo = "";
15
15
  let configName = "right-release.config.mjs";
16
16
  let dryRun = false;
17
+ let apply = false;
17
18
 
18
19
  for (let index = 0; index < args.length; index += 1) {
19
20
  const arg = args[index];
@@ -24,10 +25,19 @@ for (let index = 0; index < args.length; index += 1) {
24
25
  else if (arg === "--repo") repo = args[++index];
25
26
  else if (arg === "--config") configName = args[++index];
26
27
  else if (arg === "--dry-run") dryRun = true;
28
+ else if (arg === "--apply") apply = true;
27
29
  else if (arg === "-h" || arg === "--help") usage(0);
28
30
  else fail(`unknown argument: ${arg}`);
29
31
  }
30
32
 
33
+ // Android: the Google Play internal track, with its own gate and a dry run by default (android-play-upload.mjs). No tier: the
34
+ // internal track has no patch/update feed.
35
+ if (platform === "android") {
36
+ const { main } = await import("./android-play-upload.mjs");
37
+ process.exit(await main({ releaseId, configName, apply, dryRun }));
38
+ }
39
+ if (apply) fail("--apply is Android only; win and mac preview with --dry-run");
40
+
31
41
  // Tier, primary checkout, clean tree, sealed commit, ownership evidence, provider and repository are checked by
32
42
  // the gate that `right-release github --release` also uses; nothing below runs until it passes.
33
43
  let authority;
@@ -72,7 +82,7 @@ function writeJson(file, value) {
72
82
  }
73
83
 
74
84
  function usage(code) {
75
- console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]");
85
+ console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]\n right-release upload --platform android --release <sealed-id> [--config right-release.config.mjs] [--apply] (Play internal track; dry run unless --apply)");
76
86
  process.exit(code);
77
87
  }
78
88