@rightkit/release 0.2.174 → 0.3.0
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 +178 -1
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +29 -3
- package/command-spec.mjs +80 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/icon-pack.mjs +117 -0
- package/init-command.mjs +116 -0
- package/install-app.mjs +36 -3
- package/local-publish.mjs +317 -29
- package/macho-approval-hash.mjs +181 -0
- package/managed-cargo.mjs +49 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +69 -21
- package/npm-license-notices.mjs +159 -0
- package/ownership-dependency-allowlist.json +1 -0
- package/ownership.mjs +37 -4
- package/package-recipe-targets.mjs +15 -1
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/publication-gate.mjs +70 -3
- package/publish-cargo.mjs +26 -7
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-switch-scan.mjs +17 -7
- package/release.mjs +75 -4
- package/rightkit-ownership.json +46 -2
- package/rightkit-versions.json +5 -4
- package/runtime-budgets.mjs +14 -7
- package/stage-sidecar.mjs +505 -68
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/web-build.mjs +67 -0
package/stage-sidecar.mjs
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// { kind: "onnxruntime", version, catalog, resourceDir?: "src-tauri/resources/runtime", as?, licenseDir?, cacheDir? },
|
|
11
11
|
// { kind: "resource", from, to, sha256?, machO?, clean?, platforms? }, // `from` may be a directory (copied recursively, every file hashed)
|
|
12
12
|
// { kind: "command", name, cmd, args?, cwd?, output, externalBin: true | (false with dest), dest?, clean?, platforms? },
|
|
13
|
-
// { kind: "remote", name, url (https), sha256, dest | to, size?, clean?, platforms? },
|
|
13
|
+
// { kind: "remote", name, url (https), sha256, dest | to, size?, extract?, memberSha256?, executable?, clean?, platforms? },
|
|
14
|
+
// { kind: "cmake", name, repo (https), commit (40 hex), target?, output?, cmakeArgs?, platformArgs?, generator?, smoke?, sha256?, dest?, clean?, platforms? },
|
|
15
|
+
// { kind: "prebuilt", name, from?, manifest: { file, entry, sha256?, approvalSha256?, approvalAlgorithm? }, dest?, platforms? },
|
|
14
16
|
// ]
|
|
15
17
|
//
|
|
16
18
|
// `dest` (cargo-bin, command, remote) is a contained app-relative path (never the app root, the stage dir, the receipt, the cache, .git, src, src-tauri, node_modules or package.json; outputs may not overlap) used instead of <stageDir>/<name>-<triple>; the file is
|
|
@@ -20,7 +22,9 @@
|
|
|
20
22
|
// `catalog` is a relative path, an absolute path, { crate: "rightkit-ffmpeg", file? } (resolved through `rightkit cargo
|
|
21
23
|
// metadata`) or { npm: "<pkg>/<file>" }. The wrapper form { sidecars, profiles: { dev, release } } sets the default cargo
|
|
22
24
|
// profile pair; RIGHTKIT_BUILD_MODE (dev|release, default release) selects. The target `universal-apple-darwin` builds
|
|
23
|
-
// both slices of a cargo-bin and lipo-creates `<name>-universal-apple-darwin`.
|
|
25
|
+
// both slices of a cargo-bin and lipo-creates `<name>-universal-apple-darwin`. `ffmpeg` and `onnxruntime` do the same from their
|
|
26
|
+
// per-arch catalog rows (each slice staged under .right-release/slices/<triple>, then lipo-merged), a `cmake` entry builds with
|
|
27
|
+
// CMAKE_OSX_ARCHITECTURES="arm64;x86_64".
|
|
24
28
|
//
|
|
25
29
|
// It never signs (release.mjs signs staged files) and never calls bare cargo. Every effect goes through `deps`.
|
|
26
30
|
import { createHash } from "node:crypto";
|
|
@@ -30,8 +34,9 @@ import path from "node:path";
|
|
|
30
34
|
import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
|
|
31
35
|
import { resolveCommand } from "./development.mjs";
|
|
32
36
|
import { managedHost } from "./managed-cargo.mjs";
|
|
37
|
+
import { APPROVAL_MACHO_STRIP_LINKEDIT_V1, APPROVAL_RAW_SHA256, approvalDigest } from "./macho-approval-hash.mjs";
|
|
33
38
|
import {
|
|
34
|
-
planMediaSidecar, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
|
|
39
|
+
planMediaSidecar, sourceOfferText, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
|
|
35
40
|
} from "./media-sidecar.mjs";
|
|
36
41
|
|
|
37
42
|
export const STAGE_SIDECAR_CONTRACT = "rightkit-stage-sidecar-v1";
|
|
@@ -52,9 +57,13 @@ export class StageSidecarError extends Error {
|
|
|
52
57
|
const fail = (code, message, details) => { throw new StageSidecarError(code, message, details); };
|
|
53
58
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
54
59
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
55
|
-
const KINDS = new Set(["cargo-bin", "ffmpeg", "onnxruntime", "resource", "command", "remote"]);
|
|
60
|
+
const KINDS = new Set(["cargo-bin", "ffmpeg", "onnxruntime", "resource", "command", "remote", "cmake", "prebuilt"]);
|
|
56
61
|
export const NESTED_BROKER_CODE = "STAGE_SIDECAR_NESTED_BROKER";
|
|
57
62
|
export const REMOTE_CACHE_DIR = ".right-release/cache/remote";
|
|
63
|
+
/** Per-arch staging for a universal ffmpeg or onnxruntime entry: `<SLICE_ROOT>/<triple>/...`, merged with lipo afterwards. */
|
|
64
|
+
export const SLICE_ROOT = ".right-release/slices";
|
|
65
|
+
/** Pinned-commit source checkouts and build trees of `cmake` entries: `<CMAKE_ROOT>/<name>/{src,build}` (fixed paths, in place). */
|
|
66
|
+
export const CMAKE_ROOT = ".right-release/cmake";
|
|
58
67
|
const UNIVERSAL = "universal-apple-darwin";
|
|
59
68
|
const SLICES = Object.freeze(["aarch64-apple-darwin", "x86_64-apple-darwin"]);
|
|
60
69
|
const MODES = new Set(["dev", "release"]);
|
|
@@ -63,11 +72,13 @@ const PLATFORMS = new Set(["mac", "win", "linux"]);
|
|
|
63
72
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
64
73
|
const KEYS = Object.freeze({
|
|
65
74
|
"cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
|
|
66
|
-
ffmpeg: ["kind", "variant", "tools", "notices", "catalog", "stage", "bundleDir", "platforms"],
|
|
75
|
+
ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "sources", "probe", "platforms"],
|
|
67
76
|
onnxruntime: ["kind", "version", "catalog", "resourceDir", "as", "licenseDir", "cacheDir", "platforms"],
|
|
68
77
|
resource: ["kind", "from", "to", "sha256", "machO", "clean", "platforms"],
|
|
69
78
|
command: ["kind", "name", "cmd", "args", "cwd", "output", "externalBin", "dest", "clean", "platforms"],
|
|
70
|
-
remote: ["kind", "name", "url", "sha256", "dest", "to", "size", "clean", "platforms"],
|
|
79
|
+
remote: ["kind", "name", "url", "sha256", "dest", "to", "size", "extract", "memberSha256", "executable", "clean", "platforms"],
|
|
80
|
+
cmake: ["kind", "name", "repo", "commit", "target", "output", "cmakeArgs", "platformArgs", "generator", "smoke", "sha256", "dest", "clean", "platforms"],
|
|
81
|
+
prebuilt: ["kind", "name", "from", "manifest", "dest", "platforms"],
|
|
71
82
|
});
|
|
72
83
|
const TRIPLES = Object.freeze({
|
|
73
84
|
"aarch64-apple-darwin": { platform: "mac", os: "macos", arch: "aarch64" },
|
|
@@ -179,6 +190,57 @@ function parseCatalogSpec(value, label, errors, kind) {
|
|
|
179
190
|
return undefined;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
const CMAKE_DEFINE = /^-D[A-Za-z0-9_]+(?::[A-Z]+)?=[^\s]*$/;
|
|
194
|
+
const SOURCE_ARCHES = ["aarch64", "x86_64"];
|
|
195
|
+
|
|
196
|
+
/** One pinned https archive: { url, sha256, member, size? }. Problems are pushed to `errors`. */
|
|
197
|
+
function parseArchiveSource(value, label, errors) {
|
|
198
|
+
if (!isObject(value)) { errors.push(`${label} must be { url, sha256, member, size? }`); return undefined; }
|
|
199
|
+
for (const key of Object.keys(value)) if (!["url", "sha256", "member", "size"].includes(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)}`);
|
|
200
|
+
if (typeof value.url !== "string" || !/^https:\/\/[^\s/]+\/\S*$/.test(value.url)) errors.push(`${label}.url must be an https:// URL (no other scheme is fetched)`);
|
|
201
|
+
if (typeof value.sha256 !== "string" || !SHA256.test(value.sha256)) errors.push(`${label}.sha256 is required and must be a lowercase 64-hex SHA-256 of the archive`);
|
|
202
|
+
if (typeof value.member !== "string" || !NAME.test(value.member)) errors.push(`${label}.member must be the bare file name of one archive member (for example ffmpeg or ffmpeg.exe)`);
|
|
203
|
+
if (value.size !== undefined && (!Number.isSafeInteger(value.size) || value.size <= 0)) errors.push(`${label}.size must be a positive integer (bytes)`);
|
|
204
|
+
return { url: value.url, sha256: value.sha256, member: value.member, ...(value.size ? { size: value.size } : {}) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `ffmpeg.sources`: where to download the bytes of a catalog row (the catalog pins digests, not URLs). A tool maps to one
|
|
209
|
+
* archive source, or to `{ aarch64: source, x86_64: source }`; a universal target needs the per-arch form for every tool.
|
|
210
|
+
*/
|
|
211
|
+
function parseFfmpegSources(raw, where, errors, { universal }) {
|
|
212
|
+
if (raw === undefined) return undefined;
|
|
213
|
+
if (!isObject(raw) || !Object.keys(raw).length) { errors.push(`${where}: sources must be an object mapping ffmpeg or ffprobe to an archive source`); return undefined; }
|
|
214
|
+
const out = {};
|
|
215
|
+
for (const [tool, value] of Object.entries(raw)) {
|
|
216
|
+
const label = `${where}: sources.${tool}`;
|
|
217
|
+
if (!["ffmpeg", "ffprobe"].includes(tool)) { errors.push(`${label}: not ffmpeg or ffprobe`); continue; }
|
|
218
|
+
const perArch = isObject(value) && SOURCE_ARCHES.some((arch) => arch in value) && !("url" in value);
|
|
219
|
+
if (universal && !perArch) { errors.push(`${label} needs { aarch64, x86_64 } sources for universal-apple-darwin (each slice is a thin file)`); continue; }
|
|
220
|
+
if (perArch) {
|
|
221
|
+
for (const key of Object.keys(value)) if (!SOURCE_ARCHES.includes(key)) errors.push(`${label}: unknown arch ${JSON.stringify(key)}`);
|
|
222
|
+
out[tool] = Object.fromEntries(SOURCE_ARCHES.filter((arch) => arch in value).map((arch) => [arch, parseArchiveSource(value[arch], `${label}.${arch}`, errors)]));
|
|
223
|
+
} else out[tool] = parseArchiveSource(value, label, errors);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* `prebuilt.manifest`: { file, entry, sha256?, approvalSha256?, approvalAlgorithm? }. `file` is a JSON file inside the app, `entry`
|
|
230
|
+
* a dot path to the object for this target (`{platform}` becomes macos-aarch64, windows-x86_64 and so on), and the three keys name
|
|
231
|
+
* the fields inside it (defaults: sha256 -> "sha256"; approval fields only when named).
|
|
232
|
+
*/
|
|
233
|
+
function parsePrebuiltManifest(raw, where, errors) {
|
|
234
|
+
if (!isObject(raw)) { errors.push(`${where}: manifest must be { file, entry, sha256?, approvalSha256?, approvalAlgorithm? }`); return undefined; }
|
|
235
|
+
for (const key of Object.keys(raw)) if (!["file", "entry", "sha256", "approvalSha256", "approvalAlgorithm"].includes(key)) errors.push(`${where}: manifest: unknown key ${JSON.stringify(key)}`);
|
|
236
|
+
const file = relativePath(raw.file, `${where}: manifest.file`, errors);
|
|
237
|
+
if (typeof raw.entry !== "string" || !/^[A-Za-z0-9_{}.-]+$/.test(raw.entry) || raw.entry.split(".").some((part) => part === "")) errors.push(`${where}: manifest.entry must be a dot path such as binaries.{platform}`);
|
|
238
|
+
for (const key of ["sha256", "approvalSha256", "approvalAlgorithm"]) if (raw[key] !== undefined && (typeof raw[key] !== "string" || !/^[A-Za-z0-9_-]+$/.test(raw[key]))) errors.push(`${where}: manifest.${key} must be a field name`);
|
|
239
|
+
if (raw.approvalAlgorithm !== undefined && raw.approvalSha256 === undefined) errors.push(`${where}: manifest.approvalAlgorithm needs approvalSha256`);
|
|
240
|
+
if (!file) return undefined;
|
|
241
|
+
return { file, entry: raw.entry, sha256: raw.sha256 ?? "sha256", ...(raw.approvalSha256 ? { approvalSha256: raw.approvalSha256 } : {}), ...(raw.approvalAlgorithm ? { approvalAlgorithm: raw.approvalAlgorithm } : {}) };
|
|
242
|
+
}
|
|
243
|
+
|
|
182
244
|
/**
|
|
183
245
|
* Validate and normalise the manifest. `raw` is the `sidecars` array, or { sidecars, stageDir?, receiptPath?, profiles? }.
|
|
184
246
|
* `mode` (dev | release, default release) selects the profile of a { dev, release } pair.
|
|
@@ -247,17 +309,33 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
247
309
|
if (!skipped) { if (dest) claim(dest, where); else if (typeof name === "string" && stageDir) claim(stagedClaim(name), where); }
|
|
248
310
|
Object.assign(entry, { package: item.package, bin, name, profile, features: [...features], ...(manifestPath ? { manifestPath } : {}), ...(item.sha256 ? { sha256: item.sha256 } : {}), ...(dest ? { dest } : {}), ...(clean ? { clean } : {}) });
|
|
249
311
|
} else if (item.kind === "ffmpeg") {
|
|
250
|
-
const { kind: _kind, platforms: _platforms, catalog: rawCatalog, ...rest } = item;
|
|
312
|
+
const { kind: _kind, platforms: _platforms, catalog: rawCatalog, sources: rawSources, probe: rawProbe, ...rest } = item;
|
|
251
313
|
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", ...] }`);
|
|
252
314
|
const catalogSpec = parseCatalogSpec(rawCatalog, `${where}: catalog`, errors, "ffmpeg");
|
|
253
315
|
if (catalogSpec) entry.catalogSpec = catalogSpec;
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
316
|
+
if (rawProbe !== undefined && typeof rawProbe !== "boolean") errors.push(`${where}: probe must be a boolean`);
|
|
317
|
+
if (rawProbe === true) entry.probe = true;
|
|
318
|
+
const sources = parseFfmpegSources(rawSources, where, errors, { universal: t.arch === "universal" });
|
|
319
|
+
if (sources) entry.sources = sources;
|
|
320
|
+
if (!skipped) {
|
|
321
|
+
const mediaErrors = (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}`]));
|
|
322
|
+
if (t.arch === "universal") {
|
|
323
|
+
// One staging pass per arch slice (under SLICE_ROOT), then a lipo merge into <stageDir>/<tool>-universal-apple-darwin.
|
|
324
|
+
try {
|
|
325
|
+
entry.slices = SLICES.map((triple) => {
|
|
326
|
+
const slice = parseTargetTriple(triple);
|
|
327
|
+
return { triple, arch: slice.arch, media: validateMediaSidecarConfig({ ...rest, arch: slice.arch, stageDir: `${SLICE_ROOT}/${triple}` }, { platform: t.platform, hostArch: slice.arch }) };
|
|
328
|
+
});
|
|
329
|
+
entry.media = entry.slices[0].media;
|
|
330
|
+
for (const tool of entry.media.tools) claim(stagedClaim(tool), where);
|
|
331
|
+
} catch (error) { mediaErrors(error); }
|
|
332
|
+
} else {
|
|
333
|
+
try {
|
|
334
|
+
const config = validateMediaSidecarConfig({ ...rest, arch: t.arch, stageDir }, { platform: t.platform, hostArch: t.arch });
|
|
335
|
+
for (const tool of config.tools) claim(`${stageDir}/${stagedFileName(tool, t.os, t.arch)}`, where);
|
|
336
|
+
entry.media = config;
|
|
337
|
+
} catch (error) { mediaErrors(error); }
|
|
338
|
+
}
|
|
261
339
|
}
|
|
262
340
|
} else if (item.kind === "onnxruntime") {
|
|
263
341
|
if (typeof item.version !== "string" || !/^\d+\.\d+\.\d+$/.test(item.version)) errors.push(`${where}: version must be x.y.z`);
|
|
@@ -266,7 +344,6 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
266
344
|
const cacheDir = relativePath(item.cacheDir ?? ".cache/onnxruntime", `${where}: cacheDir`, errors);
|
|
267
345
|
const catalogSpec = parseCatalogSpec(item.catalog, `${where}: catalog`, errors, "onnxruntime");
|
|
268
346
|
if (item.as !== undefined && (typeof item.as !== "string" || !NAME.test(item.as))) errors.push(`${where}: as must be a bare file name`);
|
|
269
|
-
if (!skipped && t.arch === "universal") errors.push(`${where}: the catalog has no universal row; stage a lipo-merged file with a "command" or "resource" entry (machO: true) instead`);
|
|
270
347
|
Object.assign(entry, { version: item.version, resourceDir, licenseDir, cacheDir, ...(catalogSpec ? { catalogSpec } : {}), ...(item.as ? { as: item.as } : {}) });
|
|
271
348
|
} else if (item.kind === "command") {
|
|
272
349
|
const name = item.name;
|
|
@@ -291,9 +368,51 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
291
368
|
if (item.dest === undefined && item.to === undefined) errors.push(`${where}: dest (or to) is required`);
|
|
292
369
|
const dest = destOf(item.dest ?? item.to, item.dest === undefined ? "to" : "dest");
|
|
293
370
|
if (item.size !== undefined && (!Number.isSafeInteger(item.size) || item.size <= 0)) errors.push(`${where}: size must be a positive integer (bytes)`);
|
|
371
|
+
if (item.extract !== undefined && (typeof item.extract !== "string" || !NAME.test(item.extract))) errors.push(`${where}: extract must be the bare file name of one archive member (no path)`);
|
|
372
|
+
if (item.memberSha256 !== undefined && (item.extract === undefined || !SHA256.test(item.memberSha256 ?? ""))) errors.push(`${where}: memberSha256 must be a lowercase 64-hex SHA-256 and needs extract`);
|
|
373
|
+
if (item.executable !== undefined && typeof item.executable !== "boolean") errors.push(`${where}: executable must be a boolean`);
|
|
294
374
|
const clean = cleanOf();
|
|
295
375
|
if (dest && !skipped) claim(dest, where);
|
|
296
|
-
Object.assign(entry, { name, url: item.url, sha256: item.sha256, dest, ...(item.size ? { size: item.size } : {}), ...(clean ? { clean } : {}) });
|
|
376
|
+
Object.assign(entry, { name, url: item.url, sha256: item.sha256, dest, ...(item.size ? { size: item.size } : {}), ...(item.extract ? { extract: item.extract } : {}), ...(item.memberSha256 ? { memberSha256: item.memberSha256 } : {}), ...(item.executable ? { executable: true } : {}), ...(clean ? { clean } : {}) });
|
|
377
|
+
} else if (item.kind === "cmake") {
|
|
378
|
+
const name = item.name;
|
|
379
|
+
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)`);
|
|
380
|
+
if (typeof item.repo !== "string" || !/^https:\/\/[^\s/]+\/\S*$/.test(item.repo)) errors.push(`${where}: repo must be an https:// URL (no other scheme is fetched)`);
|
|
381
|
+
if (typeof item.commit !== "string" || !/^[0-9a-f]{40}$/.test(item.commit)) errors.push(`${where}: commit must be a full lowercase 40-hex commit id (a branch or tag is not a pin)`);
|
|
382
|
+
const target = item.target ?? name;
|
|
383
|
+
if (typeof target !== "string" || !NAME.test(target)) errors.push(`${where}: target must be a build target name`);
|
|
384
|
+
let output;
|
|
385
|
+
if (item.output !== undefined) output = relativePath(item.output, `${where}: output`, errors);
|
|
386
|
+
const flagList = (value, label) => {
|
|
387
|
+
if (value === undefined) return [];
|
|
388
|
+
if (!Array.isArray(value) || value.some((arg) => typeof arg !== "string" || !CMAKE_DEFINE.test(arg))) { errors.push(`${where}: ${label} must be a list of -DNAME=value definitions (no shell, no other flags)`); return []; }
|
|
389
|
+
return [...value];
|
|
390
|
+
};
|
|
391
|
+
const cmakeArgs = flagList(item.cmakeArgs, "cmakeArgs");
|
|
392
|
+
const platformArgs = {};
|
|
393
|
+
if (item.platformArgs !== undefined) {
|
|
394
|
+
if (!isObject(item.platformArgs)) errors.push(`${where}: platformArgs must be an object keyed by mac, win or linux`);
|
|
395
|
+
else for (const [key, list] of Object.entries(item.platformArgs)) {
|
|
396
|
+
if (!PLATFORMS.has(key)) errors.push(`${where}: platformArgs.${key}: platforms are ${[...PLATFORMS].join(", ")}`);
|
|
397
|
+
else platformArgs[key] = flagList(list, `platformArgs.${key}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (item.generator !== undefined && (typeof item.generator !== "string" || !/^[A-Za-z0-9][A-Za-z0-9 .+-]{0,60}$/.test(item.generator))) errors.push(`${where}: generator must be a generator name such as "Ninja"`);
|
|
401
|
+
if (item.smoke !== undefined && (!Array.isArray(item.smoke) || item.smoke.some((arg) => typeof arg !== "string"))) errors.push(`${where}: smoke must be a list of argument strings`);
|
|
402
|
+
sha(item.sha256, "sha256");
|
|
403
|
+
const dest = destOf(item.dest);
|
|
404
|
+
const clean = cleanOf();
|
|
405
|
+
if (!skipped) { if (dest) claim(dest, where); else if (typeof name === "string" && stageDir) claim(stagedClaim(name), where); }
|
|
406
|
+
Object.assign(entry, { name, repo: item.repo, commit: item.commit, target, ...(output ? { output } : {}), cmakeArgs, platformArgs, ...(item.generator ? { generator: item.generator } : {}), ...(item.smoke ? { smoke: [...item.smoke] } : {}), ...(item.sha256 ? { sha256: item.sha256 } : {}), ...(dest ? { dest } : {}), ...(clean ? { clean } : {}) });
|
|
407
|
+
} else if (item.kind === "prebuilt") {
|
|
408
|
+
const name = item.name;
|
|
409
|
+
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)`);
|
|
410
|
+
const from = item.from === undefined ? undefined : relativePath(item.from, `${where}: from`, errors);
|
|
411
|
+
const manifest = parsePrebuiltManifest(item.manifest, where, errors);
|
|
412
|
+
if (!skipped && t.arch === "universal") errors.push(`${where}: a prebuilt binary is verified per architecture; there is no universal manifest entry`);
|
|
413
|
+
const dest = destOf(item.dest);
|
|
414
|
+
if (!skipped) { if (dest) claim(dest, where); else if (typeof name === "string" && stageDir) claim(stagedClaim(name), where); }
|
|
415
|
+
Object.assign(entry, { name, ...(from ? { from } : {}), ...(manifest ? { manifest } : {}), ...(dest ? { dest } : {}) });
|
|
297
416
|
} else {
|
|
298
417
|
const from = relativePath(item.from, `${where}: from`, errors);
|
|
299
418
|
const to = relativePath(item.to, `${where}: to`, errors);
|
|
@@ -465,6 +584,37 @@ function requireUniversalFile(ctx, file, label) {
|
|
|
465
584
|
if (!(archs.includes("arm64") && archs.includes("x86_64"))) fail("STAGE_SIDECAR_NOT_UNIVERSAL", `${label} must contain arm64 and x86_64 for universal-apple-darwin, lipo -archs reports ${JSON.stringify(archs.join(" "))}`, { file, archs });
|
|
466
585
|
}
|
|
467
586
|
|
|
587
|
+
/**
|
|
588
|
+
* lipo-create `slices` into `dest` (a universal Mach-O): temp file, both archs required, written only when the bytes
|
|
589
|
+
* changed (an identical file is not rewritten, so Tauri's rerun-if-changed is not triggered needlessly).
|
|
590
|
+
*/
|
|
591
|
+
function lipoMergeInto(ctx, slices, dest, label, { executable = false } = {}) {
|
|
592
|
+
const { fs, run, appRoot, env } = ctx;
|
|
593
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
594
|
+
const temporary = `${dest}.tmp-${ctx.pid}`;
|
|
595
|
+
const args = ["-create", "-output", temporary, ...slices];
|
|
596
|
+
ctx.log(`stage-sidecar: lipo ${args.join(" ")}`);
|
|
597
|
+
const merged = run("lipo", args, { cwd: appRoot, env });
|
|
598
|
+
if (merged?.error || merged?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `lipo -create of ${label} failed (${merged?.error?.message ?? `exit ${merged?.status}`})`, { stderr: tail(merged?.stderr) });
|
|
599
|
+
requireUniversalFile(ctx, temporary, `${label} after lipo`);
|
|
600
|
+
const sha256 = sha256Of(fs.readFileSync(temporary));
|
|
601
|
+
const changed = !(fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === sha256);
|
|
602
|
+
if (executable) fs.chmodSync(temporary, 0o755);
|
|
603
|
+
if (changed) fs.renameSync(temporary, dest);
|
|
604
|
+
else {
|
|
605
|
+
fs.rmSync(temporary, { force: true });
|
|
606
|
+
if (executable) fs.chmodSync(dest, 0o755);
|
|
607
|
+
}
|
|
608
|
+
return { sha256, sizeBytes: fs.statSync(dest).size, changed };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Can a binary built for `t` run on this host (so a probe or smoke run is meaningful)? Cross-target builds are not run. */
|
|
612
|
+
function canRunTarget(ctx, t) {
|
|
613
|
+
const host = ctx.host;
|
|
614
|
+
if (t.arch === "universal") return host.os === "macos";
|
|
615
|
+
return t.os === host.os && t.arch === host.arch;
|
|
616
|
+
}
|
|
617
|
+
|
|
468
618
|
const destFor = (entry, ctx) => (entry.dest ? path.resolve(ctx.appRoot, entry.dest) : path.resolve(ctx.appRoot, ctx.stageDir, stagedName(entry.name, ctx.target)));
|
|
469
619
|
|
|
470
620
|
function stageCargoBin(entry, ctx) {
|
|
@@ -522,8 +672,121 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
522
672
|
return { source, sourceSha256 };
|
|
523
673
|
}
|
|
524
674
|
|
|
675
|
+
/** The archive source for one tool at one arch: a single source, or the per-arch form. */
|
|
676
|
+
const ffmpegSourceFor = (sources, tool, arch) => { const spec = sources?.[tool]; return spec?.url ? spec : spec?.[arch]; };
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Download the bytes of the catalog rows that `ffmpeg.sources` names: an archive pinned by digest, one member unpacked, the
|
|
680
|
+
* member's digest checked against the APPROVED catalog row (so a swapped archive can never stage). A staged file that already
|
|
681
|
+
* matches its row is left alone.
|
|
682
|
+
*/
|
|
683
|
+
async function fetchFfmpegSources(entry, ctx, plan, arch) {
|
|
684
|
+
if (!entry.sources) return;
|
|
685
|
+
const { fs } = ctx;
|
|
686
|
+
for (const file of plan.files) {
|
|
687
|
+
const spec = ffmpegSourceFor(entry.sources, file.tool, arch);
|
|
688
|
+
if (!spec) continue;
|
|
689
|
+
if (fs.existsSync(file.stagedPath) && sha256Of(fs.readFileSync(file.stagedPath)) === file.contentSha256) { ctx.log(`stage-sidecar: ${rel(ctx, file.stagedPath)} already staged`); continue; }
|
|
690
|
+
const { cached } = await ensureCachedDownload(ctx, spec);
|
|
691
|
+
const scratch = ctx.makeTempDir();
|
|
692
|
+
try {
|
|
693
|
+
const member = await ctx.extractMember({ archive: cached, fileName: spec.member, destDir: scratch });
|
|
694
|
+
const actual = sha256Of(fs.readFileSync(member));
|
|
695
|
+
if (actual !== file.contentSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${spec.member} from ${spec.url} is ${actual}, the approved catalog row ${file.artifactId} says ${file.contentSha256}; nothing staged`, { expected: file.contentSha256, actual });
|
|
696
|
+
copyInto(ctx, member, file.stagedPath, { executable: true });
|
|
697
|
+
} finally {
|
|
698
|
+
ctx.removeDir(scratch);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Optional (`probe: true`) LGPL and encoder probes of a staged ffmpeg or ffprobe, restored from the apps' own stage scripts: the
|
|
705
|
+
* `configuration:` line of `-version` must say --disable-gpl and --disable-nonfree and must not enable GPL, nonfree or the GPL
|
|
706
|
+
* encoders; ffmpeg's `-encoders` list must not carry libx264, libx265, libxvid or libfdk_aac. A binary built for another
|
|
707
|
+
* architecture or OS than this host is not run: the row says `skipped-cross-target`. Returns "passed" or "skipped-cross-target".
|
|
708
|
+
*/
|
|
709
|
+
function probeFfmpegBinary(ctx, { file, tool, target }) {
|
|
710
|
+
if (!canRunTarget(ctx, target)) return "skipped-cross-target";
|
|
711
|
+
const text = (result, what) => {
|
|
712
|
+
if (result?.error || result?.status !== 0) fail("STAGE_SIDECAR_FFMPEG_PROBE_FAILED", `${tool} ${what} failed (${result?.error?.message ?? `exit ${result?.status}`})`, { file });
|
|
713
|
+
return String(result.stdout ?? "");
|
|
714
|
+
};
|
|
715
|
+
const version = text(ctx.run(file, ["-version"], { cwd: ctx.appRoot, env: ctx.env }), "-version");
|
|
716
|
+
const configuration = /^configuration:(.*)$/m.exec(version)?.[1] ?? "";
|
|
717
|
+
for (const flag of ["--disable-gpl", "--disable-nonfree"]) if (!configuration.includes(flag)) fail("STAGE_SIDECAR_FFMPEG_PROBE_FAILED", `${tool} was not built with ${flag}; the LGPL redistribution terms do not hold`, { file });
|
|
718
|
+
const enabled = /--enable-(gpl|nonfree|libx264|libx265|libxvid|libfdk-aac)\b/.exec(configuration);
|
|
719
|
+
if (enabled) fail("STAGE_SIDECAR_FFMPEG_PROBE_FAILED", `${tool} was built with --enable-${enabled[1]}; it cannot ship under the LGPL`, { file });
|
|
720
|
+
if (tool === "ffmpeg") {
|
|
721
|
+
const encoders = text(ctx.run(file, ["-hide_banner", "-encoders"], { cwd: ctx.appRoot, env: ctx.env }), "-encoders");
|
|
722
|
+
const gpl = /\b(libx264|libx264rgb|libx265|libxvid|libfdk_aac)\b/.exec(encoders);
|
|
723
|
+
if (gpl) fail("STAGE_SIDECAR_FFMPEG_PROBE_FAILED", `ffmpeg lists the ${gpl[1]} encoder; it cannot ship under the LGPL`, { file });
|
|
724
|
+
}
|
|
725
|
+
return "passed";
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Stage one architecture of an ffmpeg entry: plan the catalog rows, fetch declared sources, run the optional stage command,
|
|
730
|
+
* write the notices (unless a universal merge writes them once for every slice), verify each staged file against its row.
|
|
731
|
+
* Returns { plan, rows }; each row is a receipt row.
|
|
732
|
+
*/
|
|
733
|
+
async function stageFfmpegSlice(entry, ctx, resolved, { config, t, writeNotices }) {
|
|
734
|
+
const { appRoot, run, env, fs } = ctx;
|
|
735
|
+
const plan = planMediaSidecar({ config, catalog: resolved.catalog, crateDir: resolved.crateDir, crateVersion: resolved.crateVersion, catalogBytes: resolved.catalogBytes, root: appRoot });
|
|
736
|
+
await fetchFfmpegSources(entry, ctx, plan, t.arch);
|
|
737
|
+
if (config.stage) {
|
|
738
|
+
const universal = ctx.target.arch === "universal";
|
|
739
|
+
// A universal entry stages once per slice: `{arch}`, `{triple}` and `{stageDir}` in the stage command's arguments name the slice.
|
|
740
|
+
const args = (config.stage.args ?? []).map((arg) => (universal ? String(arg).replaceAll("{arch}", t.arch).replaceAll("{triple}", t.triple).replaceAll("{stageDir}", config.stageDir) : arg));
|
|
741
|
+
ctx.log(`stage-sidecar: ${config.stage.cmd} ${args.join(" ")}`);
|
|
742
|
+
const staged = run(config.stage.cmd, args, { cwd: appRoot, env });
|
|
743
|
+
if (staged?.error || staged?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `ffmpeg stage command failed (${staged?.error?.message ?? `exit ${staged?.status}`})`);
|
|
744
|
+
}
|
|
745
|
+
const noticeFiles = writeNotices ? writeMediaSidecarNotices(plan, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync }) : [];
|
|
746
|
+
const verified = verifyStagedSidecars(plan, { exists: fs.existsSync, stat: (file) => fs.statSync(file), hashFile: (file) => sha256Of(fs.readFileSync(file)) });
|
|
747
|
+
const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
|
|
748
|
+
const rows = verified.map((file) => {
|
|
749
|
+
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 });
|
|
750
|
+
const probe = entry.probe ? probeFfmpegBinary(ctx, { file: file.stagedPath, tool: file.tool, target: t }) : undefined;
|
|
751
|
+
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, ...(file.variant ? { variant: file.variant } : {}), externalBin: true, license: "LGPL-2.1-or-later", notices, ...(probe ? { probe } : {}) };
|
|
752
|
+
});
|
|
753
|
+
return { plan, rows };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** universal-apple-darwin: stage each arch slice under SLICE_ROOT, then lipo-merge every tool into <stageDir>/<tool>-universal-apple-darwin. */
|
|
757
|
+
async function stageFfmpegUniversal(entry, ctx, resolved) {
|
|
758
|
+
const { appRoot, fs } = ctx;
|
|
759
|
+
const results = [];
|
|
760
|
+
for (const slice of entry.slices) {
|
|
761
|
+
results.push({ slice, ...(await stageFfmpegSlice(entry, ctx, resolved, { config: slice.media, t: parseTargetTriple(slice.triple), writeNotices: false })) });
|
|
762
|
+
}
|
|
763
|
+
// One notice set and one source offer that names the rows of every slice (a per-slice offer would overwrite the other).
|
|
764
|
+
const noticeByDest = new Map();
|
|
765
|
+
for (const { plan } of results) for (const notice of plan.notices) noticeByDest.set(notice.dest, notice);
|
|
766
|
+
const rowsById = new Map();
|
|
767
|
+
for (const { plan } of results) for (const row of plan.rows) rowsById.set(row.id, row);
|
|
768
|
+
const combined = {
|
|
769
|
+
notices: [...noticeByDest.values()],
|
|
770
|
+
offerPath: results[0].plan.offerPath,
|
|
771
|
+
offer: sourceOfferText({ rows: [...rowsById.values()], contact: entry.media.notices.sourceOfferContact, crateVersion: resolved.crateVersion }),
|
|
772
|
+
};
|
|
773
|
+
const noticeFiles = writeMediaSidecarNotices(combined, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync });
|
|
774
|
+
const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
|
|
775
|
+
return entry.media.tools.map((tool) => {
|
|
776
|
+
const parts = results.map(({ slice, rows }) => ({ slice, row: rows.find((row) => row.name === tool) }));
|
|
777
|
+
const dest = path.resolve(appRoot, ctx.stageDir, stagedName(tool, ctx.target));
|
|
778
|
+
const merged = lipoMergeInto(ctx, parts.map(({ row }) => path.resolve(appRoot, row.path)), dest, `ffmpeg sidecar ${tool}`, { executable: true });
|
|
779
|
+
const probe = entry.probe ? probeFfmpegBinary(ctx, { file: dest, tool, target: ctx.target }) : undefined;
|
|
780
|
+
return {
|
|
781
|
+
kind: "ffmpeg", name: tool, path: rel(ctx, dest), ...merged, sourceSha256: merged.sha256, catalogRow: parts.map(({ row }) => row.catalogRow).join("+"), version: parts[0].row.version,
|
|
782
|
+
externalBin: true, license: "LGPL-2.1-or-later", notices, ...(probe ? { probe } : {}),
|
|
783
|
+
slices: parts.map(({ slice, row }) => ({ triple: slice.triple, sha256: row.sha256, catalogRow: row.catalogRow, catalogContentSha256: row.catalogContentSha256 })),
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
525
788
|
async function stageFfmpeg(entry, ctx) {
|
|
526
|
-
const { appRoot,
|
|
789
|
+
const { appRoot, fs, target } = ctx;
|
|
527
790
|
const config = entry.media;
|
|
528
791
|
try {
|
|
529
792
|
let resolved = await ctx.loadMediaCatalog({ config, appRoot, entry });
|
|
@@ -531,31 +794,22 @@ async function stageFfmpeg(entry, ctx) {
|
|
|
531
794
|
if (!entry.catalogSpec) fail("STAGE_SIDECAR_CATALOG_UNREADABLE", "ffmpeg needs a catalog (the rightkit-ffmpeg artifacts.json: a path, { crate } or { npm }) or deps.loadMediaCatalog");
|
|
532
795
|
resolved = readJsonCatalog(resolveCatalogFile(entry.catalogSpec, ctx, "ffmpeg"), appRoot, fs, "rightkit-ffmpeg");
|
|
533
796
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
ctx.log(`stage-sidecar: ${config.stage.cmd} ${(config.stage.args ?? []).join(" ")}`);
|
|
537
|
-
const staged = run(config.stage.cmd, config.stage.args ?? [], { cwd: appRoot, env });
|
|
538
|
-
if (staged?.error || staged?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `ffmpeg stage command failed (${staged?.error?.message ?? `exit ${staged?.status}`})`);
|
|
539
|
-
}
|
|
540
|
-
const noticeFiles = writeMediaSidecarNotices(plan, { exists: fs.existsSync, mkdir: fs.mkdirSync, copy: fs.copyFileSync, write: fs.writeFileSync });
|
|
541
|
-
const verified = verifyStagedSidecars(plan, { exists: fs.existsSync, stat: (file) => fs.statSync(file), hashFile: (file) => sha256Of(fs.readFileSync(file)) });
|
|
542
|
-
const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
|
|
543
|
-
return verified.map((file) => {
|
|
544
|
-
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 });
|
|
545
|
-
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 };
|
|
546
|
-
});
|
|
797
|
+
if (target.arch === "universal") return await stageFfmpegUniversal(entry, ctx, resolved);
|
|
798
|
+
return (await stageFfmpegSlice(entry, ctx, resolved, { config, t: target, writeNotices: true })).rows;
|
|
547
799
|
} catch (error) {
|
|
548
800
|
if (error instanceof StageSidecarError) throw error;
|
|
549
801
|
return fail("STAGE_SIDECAR_FFMPEG_FAILED", `ffmpeg sidecar (${target.triple}): ${error.message}`, { cause: error, causeCode: error.code });
|
|
550
802
|
}
|
|
551
803
|
}
|
|
552
804
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
805
|
+
/**
|
|
806
|
+
* Stage one architecture of onnxruntime: the approved or pinned catalog row for `t`, its archive (digest-checked), the library
|
|
807
|
+
* (digest-checked) into `destDir` (default the entry's resourceDir), and the notices. Returns { row, dest, outName, notices }.
|
|
808
|
+
*/
|
|
809
|
+
async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
|
|
810
|
+
const { appRoot, fs } = ctx;
|
|
811
|
+
const rows = (loaded.catalog?.libraries ?? []).filter((row) => row.library === "onnxruntime" && row.version === entry.version && row.os === t.os && row.arch === t.arch);
|
|
812
|
+
const where = `onnxruntime ${entry.version} ${t.os}/${t.arch}`;
|
|
559
813
|
if (rows.length === 0) fail("STAGE_SIDECAR_NOT_APPROVED", `the native catalog has no ${where} row`);
|
|
560
814
|
if (rows.length > 1) fail("STAGE_SIDECAR_NOT_APPROVED", `the native catalog has ${rows.length} ${where} rows`);
|
|
561
815
|
const [row] = rows;
|
|
@@ -572,7 +826,7 @@ async function stageOnnxRuntime(entry, ctx) {
|
|
|
572
826
|
for (const { source } of notices) if (!fs.existsSync(source)) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file not found: ${source}`);
|
|
573
827
|
|
|
574
828
|
const outName = entry.as ?? row.artifactFileName;
|
|
575
|
-
const dest = path.resolve(appRoot, entry.resourceDir, outName);
|
|
829
|
+
const dest = path.resolve(appRoot, destDir ?? entry.resourceDir, outName);
|
|
576
830
|
if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === row.sha256) {
|
|
577
831
|
ctx.log(`stage-sidecar: ${rel(ctx, dest)} already staged`);
|
|
578
832
|
} else {
|
|
@@ -599,7 +853,30 @@ async function stageOnnxRuntime(entry, ctx) {
|
|
|
599
853
|
fs.copyFileSync(source, out);
|
|
600
854
|
return { name: path.basename(out), path: rel(ctx, out), sha256: sha256Of(fs.readFileSync(out)) };
|
|
601
855
|
});
|
|
602
|
-
return
|
|
856
|
+
return { row, dest, outName, notices: noticeOut };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function stageOnnxRuntime(entry, ctx) {
|
|
860
|
+
const { appRoot, fs, target } = ctx;
|
|
861
|
+
const loaded = await (ctx.loadNativeCatalog({ entry, appRoot }) ?? (entry.catalogSpec ? readJsonCatalog(resolveCatalogFile(entry.catalogSpec, ctx, "onnxruntime"), appRoot, fs, "native-libraries") : undefined));
|
|
862
|
+
if (!loaded) fail("STAGE_SIDECAR_CATALOG_UNREADABLE", "onnxruntime needs a catalog path (the rightkit-ffmpeg native-libraries.json) or deps.loadNativeCatalog");
|
|
863
|
+
if (target.arch !== "universal") {
|
|
864
|
+
const { row, dest, outName, notices } = await stageOnnxRuntimeSlice(entry, ctx, loaded, target);
|
|
865
|
+
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, externalBin: false }];
|
|
866
|
+
}
|
|
867
|
+
// universal-apple-darwin: each arch row is staged thin under SLICE_ROOT, then merged into the entry's resource file.
|
|
868
|
+
const slices = [];
|
|
869
|
+
for (const triple of SLICES) slices.push({ triple, ...(await stageOnnxRuntimeSlice(entry, ctx, loaded, parseTargetTriple(triple), { destDir: `${SLICE_ROOT}/${triple}` })) });
|
|
870
|
+
const names = [...new Set(slices.map((slice) => slice.outName))];
|
|
871
|
+
if (names.length !== 1) fail("STAGE_SIDECAR_NOT_APPROVED", `the arch rows name different library files (${names.join(", ")}); set "as" so one universal file name is staged`);
|
|
872
|
+
const dest = path.resolve(appRoot, entry.resourceDir, names[0]);
|
|
873
|
+
const merged = lipoMergeInto(ctx, slices.map((slice) => slice.dest), dest, `onnxruntime ${entry.version}`);
|
|
874
|
+
const notices = [...new Map(slices.flatMap((slice) => slice.notices).map((notice) => [notice.path, notice])).values()];
|
|
875
|
+
return [{
|
|
876
|
+
kind: "onnxruntime", name: names[0], path: rel(ctx, dest), ...merged, sourceSha256: merged.sha256, catalogRow: slices.map((slice) => slice.row.id).join("+"), version: entry.version,
|
|
877
|
+
license: slices[0].row.licenseSpdx ?? null, notices, externalBin: false,
|
|
878
|
+
slices: slices.map((slice) => ({ triple: slice.triple, sha256: slice.row.sha256, catalogRow: slice.row.id, archiveSha256: slice.row.archive.sha256 })),
|
|
879
|
+
}];
|
|
603
880
|
}
|
|
604
881
|
|
|
605
882
|
/** Every file under `dir`, sorted, as paths relative to it (forward slashes). */
|
|
@@ -664,36 +941,191 @@ function stageCommand(entry, ctx) {
|
|
|
664
941
|
}
|
|
665
942
|
|
|
666
943
|
/**
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
944
|
+
* Download `{ url, sha256, size? }` into the content-addressed cache (`.right-release/cache/remote/<sha256>`). The bytes are
|
|
945
|
+
* hashed before anything is written: the cache entry exists only from verified bytes. A mismatch (or a non-https redirect)
|
|
946
|
+
* fails closed with nothing staged. Returns { cached, fromCache }.
|
|
947
|
+
*/
|
|
948
|
+
async function ensureCachedDownload(ctx, { url, sha256, size }) {
|
|
949
|
+
const { appRoot, fs } = ctx;
|
|
950
|
+
const cached = path.resolve(appRoot, REMOTE_CACHE_DIR, sha256);
|
|
951
|
+
if (fs.existsSync(cached) && sha256Of(fs.readFileSync(cached)) === sha256) return { cached, fromCache: true };
|
|
952
|
+
ctx.log(`stage-sidecar: GET ${url}`);
|
|
953
|
+
let response;
|
|
954
|
+
try { response = await ctx.fetch(url, { redirect: "follow" }); } catch (error) { fail("STAGE_SIDECAR_DOWNLOAD_FAILED", `GET ${url} failed: ${error.message}`, { cause: error, url }); }
|
|
955
|
+
if (!response?.ok) fail("STAGE_SIDECAR_DOWNLOAD_FAILED", `GET ${url} failed with ${response?.status}`, { url });
|
|
956
|
+
if (typeof response.url === "string" && response.url && !response.url.startsWith("https://")) fail("STAGE_SIDECAR_DOWNLOAD_FAILED", `${url} redirected to a non-https URL: ${response.url}`, { url });
|
|
957
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
958
|
+
if (size !== undefined && bytes.length !== size) fail("STAGE_SIDECAR_HASH_MISMATCH", `${url} is ${bytes.length} bytes, expected ${size}; nothing staged`, { expectedSize: size, actualSize: bytes.length });
|
|
959
|
+
const actual = sha256Of(bytes);
|
|
960
|
+
if (actual !== sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${url} downloaded as ${actual}, expected ${sha256}; nothing staged`, { expected: sha256, actual });
|
|
961
|
+
fs.mkdirSync(path.dirname(cached), { recursive: true });
|
|
962
|
+
const temporary = `${cached}.tmp-${ctx.pid}`;
|
|
963
|
+
fs.writeFileSync(temporary, bytes);
|
|
964
|
+
fs.renameSync(temporary, cached);
|
|
965
|
+
return { cached, fromCache: false };
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* A pinned https download. `dest` is copied only from the verified cache entry. With `extract` the download is an archive
|
|
970
|
+
* (zip, tgz): the one member whose base name is `extract` is unpacked (digest-checked against `memberSha256` when given) and
|
|
971
|
+
* only that member is staged, so no `command` entry or shell is needed to unpack it.
|
|
670
972
|
*/
|
|
671
973
|
async function stageRemote(entry, ctx) {
|
|
672
974
|
const { appRoot, fs } = ctx;
|
|
673
975
|
const dest = path.resolve(appRoot, entry.dest);
|
|
674
|
-
const cached =
|
|
675
|
-
let
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
976
|
+
const { cached, fromCache } = await ensureCachedDownload(ctx, entry);
|
|
977
|
+
let source = cached;
|
|
978
|
+
let scratch;
|
|
979
|
+
try {
|
|
980
|
+
let memberSha256;
|
|
981
|
+
if (entry.extract) {
|
|
982
|
+
scratch = ctx.makeTempDir();
|
|
983
|
+
source = await ctx.extractMember({ archive: cached, fileName: entry.extract, destDir: scratch });
|
|
984
|
+
memberSha256 = sha256Of(fs.readFileSync(source));
|
|
985
|
+
if (entry.memberSha256 && memberSha256 !== entry.memberSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.extract} from ${entry.url} is ${memberSha256}, expected ${entry.memberSha256}; nothing staged`, { expected: entry.memberSha256, actual: memberSha256 });
|
|
986
|
+
}
|
|
987
|
+
// Download and verify first: a failed fetch or a hash mismatch must never leave a cleaned destination behind.
|
|
988
|
+
cleanDest(ctx, entry, dest);
|
|
989
|
+
const copied = copyInto(ctx, source, dest, { executable: entry.executable === true });
|
|
990
|
+
const expected = entry.extract ? entry.memberSha256 : entry.sha256;
|
|
991
|
+
if (expected && copied.sha256 !== expected) fail("STAGE_SIDECAR_HASH_MISMATCH", `staged ${entry.dest} is ${copied.sha256}, expected ${expected}`, { expected, actual: copied.sha256 });
|
|
992
|
+
return [{ kind: "remote", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256: memberSha256 ?? entry.sha256, url: entry.url, cached: fromCache, externalBin: false, ...(entry.extract ? { archiveSha256: entry.sha256, member: entry.extract } : {}) }];
|
|
993
|
+
} finally {
|
|
994
|
+
if (scratch) ctx.removeDir(scratch);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Bring the fixed source checkout `srcDir` to the pinned commit: `git init` once, then fetch exactly that commit (git verifies the
|
|
1000
|
+
* object hash, so the tree is pinned with no repackable tarball) and detach onto it. A checkout already at the commit is reused;
|
|
1001
|
+
* one with local edits is refused rather than built.
|
|
1002
|
+
*/
|
|
1003
|
+
function checkoutPinnedSource(ctx, entry, srcDir) {
|
|
1004
|
+
const { fs, run, env } = ctx;
|
|
1005
|
+
const git = (args) => {
|
|
1006
|
+
const result = run("git", args, { cwd: srcDir, env });
|
|
1007
|
+
if (result?.error || result?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `git ${args[0]} for ${entry.name} failed (${result?.error?.message ?? `exit ${result?.status}`})`, { name: entry.name, stderr: tail(result?.stderr) });
|
|
1008
|
+
return result;
|
|
1009
|
+
};
|
|
1010
|
+
const head = () => { const result = run("git", ["rev-parse", "HEAD"], { cwd: srcDir, env }); return result?.status === 0 ? String(result.stdout ?? "").trim() : ""; };
|
|
1011
|
+
if (!fs.existsSync(path.join(srcDir, ".git"))) {
|
|
1012
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
1013
|
+
git(["init", "-q"]);
|
|
1014
|
+
git(["remote", "add", "origin", entry.repo]);
|
|
1015
|
+
} else git(["remote", "set-url", "origin", entry.repo]);
|
|
1016
|
+
// Cleanliness is judged BEFORE any destructive source operation: tracked edits, untracked files and ignored files are all
|
|
1017
|
+
// build inputs (CMake globbing), so any of them refuses the build and nothing is checked out over them.
|
|
1018
|
+
const assertClean = (when) => {
|
|
1019
|
+
const dirty = run("git", ["status", "--porcelain", "--ignored"], { cwd: srcDir, env });
|
|
1020
|
+
if (dirty?.status !== 0 || String(dirty.stdout ?? "").trim()) fail("STAGE_SIDECAR_SOURCE_MISMATCH", `${entry.name} source checkout ${srcDir} has local, untracked or ignored files ${when}; refusing to build a tree that is not the pinned commit`, { name: entry.name, status: tail(dirty?.stdout) });
|
|
1021
|
+
};
|
|
1022
|
+
assertClean("before checkout");
|
|
1023
|
+
if (head() !== entry.commit) {
|
|
1024
|
+
ctx.log(`stage-sidecar: git fetch ${entry.repo} ${entry.commit}`);
|
|
1025
|
+
git(["fetch", "--depth", "1", "origin", entry.commit]);
|
|
1026
|
+
git(["checkout", "-q", "--force", "--detach", "FETCH_HEAD"]);
|
|
1027
|
+
const actual = head();
|
|
1028
|
+
if (actual !== entry.commit) fail("STAGE_SIDECAR_SOURCE_MISMATCH", `${entry.name} checkout is ${actual || "unreadable"}, the pin is ${entry.commit}`, { expected: entry.commit, actual });
|
|
1029
|
+
}
|
|
1030
|
+
assertClean("after checkout");
|
|
1031
|
+
if (fs.existsSync(path.join(srcDir, ".gitmodules"))) {
|
|
1032
|
+
// Submodule commits are pinned by the superproject tree: bring them to exactly those commits, then require every one to match
|
|
1033
|
+
// (no `+` moved, `-` missing or `U` conflicted prefix) and be clean, so no unpinned submodule source reaches the build.
|
|
1034
|
+
git(["submodule", "update", "--init", "--recursive", "--checkout"]);
|
|
1035
|
+
const modules = git(["submodule", "status", "--recursive"]);
|
|
1036
|
+
const bad = String(modules.stdout ?? "").split("\n").filter((line) => /^[-+U]/.test(line));
|
|
1037
|
+
if (bad.length) fail("STAGE_SIDECAR_SOURCE_MISMATCH", `${entry.name} submodules are not at their pinned commits: ${bad.join("; ")}`, { name: entry.name });
|
|
1038
|
+
const inner = git(["submodule", "foreach", "--recursive", "git status --porcelain --ignored"]);
|
|
1039
|
+
if (String(inner.stdout ?? "").split("\n").some((line) => line.trim() && !/^Entering /.test(line))) fail("STAGE_SIDECAR_SOURCE_MISMATCH", `${entry.name} submodule checkout has local, untracked or ignored files; refusing to build`, { name: entry.name });
|
|
1040
|
+
assertClean("after submodule update");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Build a pinned git commit with the build tool (no shell) and stage the result as <name>-<triple>[.exe]: fetch the commit, configure,
|
|
1046
|
+
* build `target`, locate the executable, check it carries both archs for universal-apple-darwin, optionally pin its digest and
|
|
1047
|
+
* smoke-run it. Per-platform flags (for example Metal on macOS) come from `platformArgs`; the source and build trees live at the
|
|
1048
|
+
* fixed `.right-release/cmake/<name>/{src,build}`.
|
|
1049
|
+
*/
|
|
1050
|
+
function stageCmake(entry, ctx) {
|
|
1051
|
+
const { appRoot, fs, run, env, target } = ctx;
|
|
1052
|
+
const root = path.resolve(appRoot, CMAKE_ROOT, entry.name);
|
|
1053
|
+
const srcDir = path.join(root, "src");
|
|
1054
|
+
const buildDir = path.join(root, "build");
|
|
1055
|
+
const dest = destFor(entry, ctx);
|
|
693
1056
|
cleanDest(ctx, entry, dest);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1057
|
+
checkoutPinnedSource(ctx, entry, srcDir);
|
|
1058
|
+
const archFlag = target.platform !== "mac" ? undefined
|
|
1059
|
+
: target.arch === "universal" ? "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64"
|
|
1060
|
+
: `-DCMAKE_OSX_ARCHITECTURES=${target.arch === "aarch64" ? "arm64" : "x86_64"}`;
|
|
1061
|
+
const configure = ["-S", srcDir, "-B", buildDir, "-DCMAKE_BUILD_TYPE=Release", ...(entry.generator ? ["-G", entry.generator] : []), ...(archFlag ? [archFlag] : []), ...(entry.platformArgs[target.platform] ?? []), ...entry.cmakeArgs];
|
|
1062
|
+
const step = (args, label) => {
|
|
1063
|
+
ctx.log(`stage-sidecar: cmake ${args.join(" ")}`);
|
|
1064
|
+
const result = run("cmake", args, { cwd: appRoot, env });
|
|
1065
|
+
if (result?.error || result?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `cmake ${label} of ${entry.name} failed (${result?.error?.message ?? `exit ${result?.status}`})`, { name: entry.name, stderr: tail(result?.stderr) });
|
|
1066
|
+
};
|
|
1067
|
+
step(configure, "configure");
|
|
1068
|
+
step(["--build", buildDir, "--config", "Release", "--target", entry.target, "--parallel"], "build");
|
|
1069
|
+
const exe = ctx.windows ? ".exe" : "";
|
|
1070
|
+
const output = entry.output ?? `bin/${entry.target}`;
|
|
1071
|
+
const dir = path.posix.dirname(output);
|
|
1072
|
+
const file = `${path.posix.basename(output)}${exe}`;
|
|
1073
|
+
const candidates = [...new Set([path.join(buildDir, dir, file), path.join(buildDir, dir, "Release", file), path.join(buildDir, "Release", file)])];
|
|
1074
|
+
const source = candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
1075
|
+
if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build of ${entry.name} did not produce ${output} (looked in ${candidates.join(", ")})`, { name: entry.name, candidates });
|
|
1076
|
+
if (target.arch === "universal") requireUniversalFile(ctx, source, `cmake build ${entry.name}`);
|
|
1077
|
+
const sourceSha256 = sha256Of(fs.readFileSync(source));
|
|
1078
|
+
if (entry.sha256 && entry.sha256 !== sourceSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.name} built to ${sourceSha256}, expected ${entry.sha256}`, { expected: entry.sha256, actual: sourceSha256 });
|
|
1079
|
+
const copied = copyInto(ctx, source, dest, { executable: true });
|
|
1080
|
+
if (entry.smoke && canRunTarget(ctx, target)) {
|
|
1081
|
+
const smoked = run(dest, entry.smoke, { cwd: appRoot, env });
|
|
1082
|
+
if (smoked?.error || smoked?.status !== 0) fail("STAGE_SIDECAR_BUILD_FAILED", `${entry.name} does not run (${entry.smoke.join(" ")}): ${smoked?.error?.message ?? `exit ${smoked?.status}`}`, { name: entry.name });
|
|
1083
|
+
}
|
|
1084
|
+
return [{ kind: "cmake", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256, repo: entry.repo, commit: entry.commit, cmakeTarget: entry.target, flags: configure.filter((arg) => arg.startsWith("-D")), externalBin: !entry.dest }];
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** `{platform}` in a prebuilt manifest entry: macos-aarch64, macos-x86_64, windows-x86_64, linux-x86_64, ... */
|
|
1088
|
+
const prebuiltPlatformKey = (t) => `${t.os}-${t.arch}`;
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Verify a host-built binary (already staged at <stageDir>/<name>-<triple>, or copied from `from`) against the digest in an app
|
|
1092
|
+
* manifest file: its raw sha256, and when the manifest names an approval hash, the signing-independent approval digest
|
|
1093
|
+
* (`macho-codesign-strip-linkedit-v1` on macOS, `raw-sha256` elsewhere). Nothing is built or downloaded: a missing file or
|
|
1094
|
+
* a mismatch fails closed.
|
|
1095
|
+
*/
|
|
1096
|
+
async function stagePrebuilt(entry, ctx) {
|
|
1097
|
+
const { appRoot, fs, target } = ctx;
|
|
1098
|
+
const dest = destFor(entry, ctx);
|
|
1099
|
+
const platformKey = prebuiltPlatformKey(target);
|
|
1100
|
+
const manifestFile = path.resolve(appRoot, entry.manifest.file);
|
|
1101
|
+
let manifest;
|
|
1102
|
+
try { manifest = JSON.parse(String(fs.readFileSync(manifestFile))); } catch (error) { fail("STAGE_SIDECAR_CATALOG_UNREADABLE", `cannot read the prebuilt manifest ${manifestFile} as JSON`, { cause: error }); }
|
|
1103
|
+
let node = manifest;
|
|
1104
|
+
for (const part of entry.manifest.entry.replaceAll("{platform}", platformKey).split(".")) node = isObject(node) ? node[part] : undefined;
|
|
1105
|
+
if (!isObject(node)) fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} has no ${entry.manifest.entry.replaceAll("{platform}", platformKey)} entry for ${entry.name} on ${platformKey}`);
|
|
1106
|
+
const pinned = node[entry.manifest.sha256];
|
|
1107
|
+
if (typeof pinned !== "string" || !SHA256.test(pinned)) fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} ${platformKey}.${entry.manifest.sha256} is not a lowercase 64-hex SHA-256`);
|
|
1108
|
+
const source = entry.from ? path.resolve(appRoot, entry.from) : dest;
|
|
1109
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `prebuilt ${entry.name} for ${platformKey} is missing: ${source}. Build it on its native host first; packaging never substitutes a stock binary.`, { name: entry.name, source });
|
|
1110
|
+
const actual = sha256Of(fs.readFileSync(source));
|
|
1111
|
+
if (actual !== pinned) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.name} integrity failure: ${source} is ${actual}, ${entry.manifest.file} pins ${pinned}`, { expected: pinned, actual });
|
|
1112
|
+
let approval;
|
|
1113
|
+
if (entry.manifest.approvalSha256) {
|
|
1114
|
+
const expected = node[entry.manifest.approvalSha256];
|
|
1115
|
+
if (typeof expected !== "string" || !SHA256.test(expected)) fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} ${platformKey}.${entry.manifest.approvalSha256} is not a lowercase 64-hex SHA-256`);
|
|
1116
|
+
const algorithm = entry.manifest.approvalAlgorithm ? node[entry.manifest.approvalAlgorithm] : (target.os === "macos" ? APPROVAL_MACHO_STRIP_LINKEDIT_V1 : APPROVAL_RAW_SHA256);
|
|
1117
|
+
if (typeof algorithm !== "string") fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} ${platformKey}.${entry.manifest.approvalAlgorithm} does not name an approval hash algorithm`);
|
|
1118
|
+
let digest;
|
|
1119
|
+
try { digest = await ctx.approvalDigest(source, algorithm); } catch (error) { fail("STAGE_SIDECAR_APPROVAL_FAILED", `${entry.name} approval hash (${algorithm}): ${error.message}`, { cause: error, causeCode: error.code }); }
|
|
1120
|
+
if (digest !== expected) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.name} signing-independent integrity failure: ${algorithm} is ${digest}, ${entry.manifest.file} pins ${expected}`, { expected, actual: digest });
|
|
1121
|
+
approval = { algorithm, sha256: digest };
|
|
1122
|
+
}
|
|
1123
|
+
let copied;
|
|
1124
|
+
if (path.resolve(source) === path.resolve(dest)) {
|
|
1125
|
+
if (!ctx.windows) fs.chmodSync(dest, 0o755);
|
|
1126
|
+
copied = { sha256: actual, sizeBytes: fs.statSync(dest).size, changed: false };
|
|
1127
|
+
} else copied = copyInto(ctx, source, dest, { executable: true });
|
|
1128
|
+
return [{ kind: "prebuilt", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256: actual, manifest: entry.manifest.file, platform: platformKey, ...(approval ? { approvalAlgorithm: approval.algorithm, approvalSha256: approval.sha256 } : {}), externalBin: !entry.dest }];
|
|
697
1129
|
}
|
|
698
1130
|
|
|
699
1131
|
/**
|
|
@@ -706,7 +1138,7 @@ function wipeStaleStageDir(ctx, plan, produced) {
|
|
|
706
1138
|
let listing;
|
|
707
1139
|
try { listing = [...ctx.fs.readdirSync(dir)]; } catch { return []; }
|
|
708
1140
|
const keep = new Set([...produced, path.resolve(ctx.appRoot, plan.receiptPath)].map((file) => path.resolve(file).toLowerCase()));
|
|
709
|
-
const names = plan.entries.flatMap((entry) => (entry.kind === "ffmpeg" ? (entry.media?.tools ?? []) : entry.kind === "cargo-bin" || entry.kind === "command" ? [entry.name] : []));
|
|
1141
|
+
const names = plan.entries.flatMap((entry) => (entry.kind === "ffmpeg" ? (entry.media?.tools ?? []) : entry.kind === "cargo-bin" || entry.kind === "command" || entry.kind === "cmake" || entry.kind === "prebuilt" ? [entry.name] : []));
|
|
710
1142
|
const exact = new Set(names.flatMap((name) => [`${name}-${ctx.target.triple}`, `${name}-${ctx.target.triple}.exe`]));
|
|
711
1143
|
const removed = [];
|
|
712
1144
|
for (const name of listing.sort()) {
|
|
@@ -750,17 +1182,18 @@ export function planSidecarStaging({ manifest, target, env = process.env } = {})
|
|
|
750
1182
|
const plan = validateSidecarManifest(manifest, { target, mode: resolveBuildMode(env) });
|
|
751
1183
|
const t = plan.target;
|
|
752
1184
|
const outputsOf = (entry) => {
|
|
753
|
-
if (
|
|
1185
|
+
if (["cargo-bin", "command", "cmake", "prebuilt"].includes(entry.kind)) return [entry.dest ?? `${plan.stageDir}/${stagedName(entry.name, t)}`];
|
|
754
1186
|
if (entry.kind === "remote") return [entry.dest];
|
|
755
1187
|
if (entry.kind === "resource") return [entry.to];
|
|
756
1188
|
if (entry.kind === "onnxruntime") return [`${entry.resourceDir}/${entry.as ?? "<catalog artifactFileName>"}`];
|
|
757
|
-
return (entry.media?.tools ?? []).map((tool) => `${plan.stageDir}/${
|
|
1189
|
+
return (entry.media?.tools ?? []).map((tool) => `${plan.stageDir}/${stagedName(tool, t)}`);
|
|
758
1190
|
};
|
|
759
1191
|
const entries = plan.entries.map((entry) => ({
|
|
760
1192
|
index: entry.index, kind: entry.kind, skipped: entry.skipped, ...(entry.platforms ? { platforms: entry.platforms } : {}),
|
|
761
|
-
...(entry.skipped ? {} : { outputs: outputsOf(entry), externalBin:
|
|
1193
|
+
...(entry.skipped ? {} : { outputs: outputsOf(entry), externalBin: ["cargo-bin", "command", "cmake", "prebuilt"].includes(entry.kind) ? !entry.dest : entry.kind === "ffmpeg" }),
|
|
762
1194
|
...(entry.kind === "cargo-bin" ? { package: entry.package, bin: entry.bin, profile: entry.profile } : {}),
|
|
763
1195
|
...(entry.kind === "remote" ? { url: entry.url } : {}),
|
|
1196
|
+
...(entry.kind === "cmake" ? { repo: entry.repo, commit: entry.commit } : {}),
|
|
764
1197
|
...(entry.clean ? { clean: true } : {}),
|
|
765
1198
|
}));
|
|
766
1199
|
return { target: t.triple, platform: t.platform, mode: resolveBuildMode(env), stageDir: plan.stageDir, receiptPath: plan.receiptPath, wipeStageDir: plan.wipeStageDir, nestedBrokerRisk: needsBroker(plan) && isNestedBrokerContext(env), entries };
|
|
@@ -782,6 +1215,8 @@ export async function stageSidecars({ appRoot, manifest, target, deps = {} } = {
|
|
|
782
1215
|
appRoot, target: plan.target, stageDir: plan.stageDir, fs, env, run, protectedPaths: protectedOutputPaths(plan),
|
|
783
1216
|
windows: plan.target.os === "windows",
|
|
784
1217
|
pid: deps.pid ?? process.pid,
|
|
1218
|
+
host: deps.host ?? { os: { darwin: "macos", win32: "windows", linux: "linux" }[process.platform] ?? process.platform, arch: { arm64: "aarch64", x64: "x86_64" }[process.arch] ?? process.arch },
|
|
1219
|
+
approvalDigest: deps.approvalDigest ?? ((file, algorithm) => approvalDigest(file, algorithm)),
|
|
785
1220
|
log: deps.log ?? (() => {}),
|
|
786
1221
|
fetchBytes: deps.fetchBytes ?? defaultFetchBytes,
|
|
787
1222
|
fetch: deps.fetch ?? ((...args) => globalThis.fetch(...args)),
|
|
@@ -800,7 +1235,9 @@ export async function stageSidecars({ appRoot, manifest, target, deps = {} } = {
|
|
|
800
1235
|
: entry.kind === "onnxruntime" ? await stageOnnxRuntime(entry, ctx)
|
|
801
1236
|
: entry.kind === "command" ? stageCommand(entry, ctx)
|
|
802
1237
|
: entry.kind === "remote" ? await stageRemote(entry, ctx)
|
|
803
|
-
:
|
|
1238
|
+
: entry.kind === "cmake" ? stageCmake(entry, ctx)
|
|
1239
|
+
: entry.kind === "prebuilt" ? await stagePrebuilt(entry, ctx)
|
|
1240
|
+
: stageResource(entry, ctx);
|
|
804
1241
|
files.push(...staged);
|
|
805
1242
|
}
|
|
806
1243
|
const wiped = plan.wipeStageDir ? wipeStaleStageDir(ctx, plan, files.map((file) => path.resolve(appRoot, file.path))) : [];
|