@rightkit/release 0.2.172 → 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.
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +2 -0
- package/development.mjs +16 -2
- package/install-tauri-devid.mjs +172 -0
- package/inventory-check.mjs +1 -1
- package/mac-package-recipe.mjs +304 -0
- package/media-sidecar.mjs +1 -1
- package/package-recipe-targets.mjs +95 -0
- package/package.json +1 -1
- package/project-type.mjs +1 -1
- package/release.mjs +4 -1
- package/rightkit-sync.mjs +1 -1
- package/rightkit-versions.json +5 -4
- package/stage-sidecar-command.mjs +41 -0
- package/stage-sidecar.mjs +424 -0
- package/swift-mac-dmg.mjs +2 -1
- 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
|
+
}
|
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
|
-
|
|
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),
|