@rightkit/release 0.3.0 → 0.4.1
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 +74 -10
- package/addon-command.mjs +3 -4
- package/adoption-evidence.mjs +650 -0
- package/android-play-upload.mjs +2 -1
- package/apple-profile-create.mjs +113 -10
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/binary-license-notices.mjs +161 -0
- package/build-release.mjs +138 -47
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +21 -2
- package/codesign-environment.mjs +2 -2
- package/content-digest.mjs +209 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/github-updater-release.mjs +70 -7
- package/in-place-install.mjs +17 -14
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +21 -40
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +222 -53
- package/managed-cargo.mjs +8 -0
- package/mirror-root-artifact.mjs +5 -1
- package/native-cargo-layout.mjs +47 -4
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +3 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.json +117 -67
- package/ownership-mandatory-set.mjs +141 -21
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +49 -7
- package/package-recipe-targets.mjs +41 -1
- package/package.json +1 -1
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/preflight.mjs +14 -7
- package/production-mode.mjs +42 -0
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +117 -25
- package/publish-swift.mjs +12 -3
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +8 -0
- package/release-config.mjs +330 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +88 -21
- package/release.mjs +82 -20
- package/rightkit-ownership.json +1 -1
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +428 -194
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +248 -47
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +207 -40
- package/swap-helper-bundle.mjs +7 -3
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
- package/bin/darwin/rightkit-swap.sign.json +0 -6
package/sign-updater.mjs
CHANGED
|
@@ -1,68 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
6
|
-
import { refuseManagedEnvSecrets
|
|
6
|
+
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
7
|
+
import { execWithUpdaterCredential } from "./protected-credentials.mjs";
|
|
8
|
+
import { resolvePnpmCli } from "./development.mjs";
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const files = [];
|
|
10
|
-
let dryRun = false;
|
|
11
|
-
let keyFile = process.env.RIGHT_UPDATER_KEY_FILE || "";
|
|
12
|
-
for (let i = 0; i < args.length; i++) {
|
|
13
|
-
if (args[i] === "--file") files.push(path.resolve(args[++i]));
|
|
14
|
-
else if (args[i] === "--key-file") keyFile = args[++i] ?? "";
|
|
15
|
-
else if (args[i] === "--dry-run") dryRun = true;
|
|
16
|
-
else fail(`unknown argument: ${args[i]}`);
|
|
17
|
-
}
|
|
18
|
-
if (!files.length) fail("at least one --file is required");
|
|
10
|
+
export const UPDATER_SIGNING_SECRET = "updater-signing-key";
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
export function runSignUpdater(args, {
|
|
13
|
+
env = process.env, platform = process.platform, cwd = process.cwd(),
|
|
14
|
+
execCredential = execWithUpdaterCredential, resolvePnpm = resolvePnpmCli,
|
|
15
|
+
signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, log = console.log,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const files = [];
|
|
18
|
+
let dryRun = false;
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
if (args[i] === "--file") {
|
|
21
|
+
if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--file requires an artifact path");
|
|
22
|
+
files.push(path.resolve(cwd, args[++i]));
|
|
23
|
+
} else if (args[i] === "--key-file") throw new Error("--key-file is removed: updater keys come from the OS credential provider");
|
|
24
|
+
else if (args[i] === "--dry-run") dryRun = true;
|
|
25
|
+
else throw new Error(`unknown argument: ${args[i]}`);
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
27
|
+
if (!files.length) throw new Error("at least one --file is required");
|
|
28
|
+
if (!dryRun) {
|
|
29
|
+
refuseManagedEnvSecrets(env, ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]);
|
|
30
|
+
for (const key of Object.keys(env)) {
|
|
31
|
+
if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase()) && env[key]) {
|
|
32
|
+
throw new Error("updater signing key/password must come from the OS credential provider");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const file of files) if (!exists(file)) throw new Error(`updater artifact not found: ${file}`);
|
|
36
|
+
}
|
|
37
|
+
const signingEnv = { ...env };
|
|
38
|
+
for (const key of Object.keys(signingEnv)) {
|
|
39
|
+
if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase())) delete signingEnv[key];
|
|
40
|
+
}
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
|
|
43
|
+
if (dryRun) continue;
|
|
44
|
+
remove(`${file}.sig`, { force: true });
|
|
45
|
+
if (platform === "darwin") continue;
|
|
46
|
+
const result = execCredential(process.execPath,
|
|
47
|
+
[resolvePnpm({ root: cwd, env: signingEnv }), "exec", "tauri", "signer", "sign", file],
|
|
48
|
+
{ cwd, env: signingEnv });
|
|
49
|
+
if (result.error || result.status !== 0) {
|
|
50
|
+
const error = new Error(`updater credential provider/signing failed (exit ${result.status ?? "unavailable"})`);
|
|
51
|
+
error.exitCode = result.status ?? 1;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!dryRun && platform === "darwin") signMac(files, { env: signingEnv, cwd });
|
|
56
|
+
return { files, dryRun };
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
60
|
+
try { runSignUpdater(process.argv.slice(2)); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`sign-updater: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
63
|
+
process.exitCode = error.exitCode ?? 1;
|
|
64
|
+
}
|
|
68
65
|
}
|
package/source-gate.mjs
CHANGED
|
@@ -30,11 +30,18 @@ export function stripOwnLockRecords(status) {
|
|
|
30
30
|
return kept.length ? `${kept.join("\0")}\0` : "";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Path of one porcelain v1 record ("XY path"). Tolerates a record whose leading status column was
|
|
34
|
+
* trimmed away by a caller ("M path"), so the first path never loses its first character (#782). */
|
|
35
|
+
function porcelainPath(entry) {
|
|
36
|
+
const m = /^([ MADRCU?!]{2} |[MADRCU?!] )/.exec(entry);
|
|
37
|
+
return m ? entry.slice(m[0].length) : entry;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
export function assertCleanSource({ status, commandId = "release" }) {
|
|
34
41
|
if (typeof status !== "string") throw new Error("release source status is required");
|
|
35
42
|
status = stripOwnLockRecords(status);
|
|
36
43
|
if (status.length !== 0) {
|
|
37
|
-
const files = status.split("\0").filter(Boolean).map(
|
|
44
|
+
const files = status.split("\0").filter(Boolean).map(porcelainPath).slice(0, 10);
|
|
38
45
|
throw new Error(`${commandId} requires a clean working tree; commit or stash first:\n${files.map((file) => `- ${file}`).join("\n")}`);
|
|
39
46
|
}
|
|
40
47
|
}
|
package/stage-sidecar.mjs
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
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
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
|
|
14
|
+
// { kind: "cmake", name, repo (https), commit (40 hex), target?, output?, cmakeArgs?, platformArgs?, generator?, platformGenerators?, architecture?, cmakeBin?, smoke?, sha256?, dest?, clean?, platforms? },
|
|
15
|
+
// { kind: "prebuilt", name, from? (a path, or { aarch64, x86_64 } for universal-apple-darwin), manifest: { file, entry, sha256?, approvalSha256?, approvalAlgorithm? }, dest?, platforms? },
|
|
16
16
|
// ]
|
|
17
17
|
//
|
|
18
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
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
// profile pair; RIGHTKIT_BUILD_MODE (dev|release, default release) selects. The target `universal-apple-darwin` builds
|
|
25
25
|
// both slices of a cargo-bin and lipo-creates `<name>-universal-apple-darwin`. `ffmpeg` and `onnxruntime` do the same from their
|
|
26
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".
|
|
27
|
+
// CMAKE_OSX_ARCHITECTURES="arm64;x86_64". A `prebuilt` entry does it from per-arch host-built inputs (`from: { aarch64, x86_64 }`, or the
|
|
28
|
+
// staged `<name>-<slice triple>` files), each verified against its own manifest entry and then lipo-merged. An `ffmpeg` source may be a
|
|
29
|
+
// local path (`sources.ffmpeg: { path: "../vendor/ffmpeg-lgpl" }`) instead of a pinned https archive.
|
|
28
30
|
//
|
|
29
31
|
// It never signs (release.mjs signs staged files) and never calls bare cargo. Every effect goes through `deps`.
|
|
30
32
|
import { createHash } from "node:crypto";
|
|
@@ -38,6 +40,7 @@ import { APPROVAL_MACHO_STRIP_LINKEDIT_V1, APPROVAL_RAW_SHA256, approvalDigest }
|
|
|
38
40
|
import {
|
|
39
41
|
planMediaSidecar, sourceOfferText, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
|
|
40
42
|
} from "./media-sidecar.mjs";
|
|
43
|
+
import { ContentDigestError, contentIdentity, digestFor } from "./content-digest.mjs";
|
|
41
44
|
|
|
42
45
|
export const STAGE_SIDECAR_CONTRACT = "rightkit-stage-sidecar-v1";
|
|
43
46
|
export const STAGE_SIDECAR_RECEIPT_NAME = "stage-sidecar.json";
|
|
@@ -70,6 +73,28 @@ const MODES = new Set(["dev", "release"]);
|
|
|
70
73
|
const PROFILE = /^[a-z][a-z0-9_-]*$/;
|
|
71
74
|
const PLATFORMS = new Set(["mac", "win", "linux"]);
|
|
72
75
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
76
|
+
/** One path segment of an archive member path: no glob metacharacters, no separators, no drive colon. */
|
|
77
|
+
const MEMBER_SEGMENT = /^[A-Za-z0-9_.+@~-]+$/;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalize and guard a requested archive member. A bare file name (no `/`) selects the one member with that base name; a path
|
|
81
|
+
* (contains `/`) selects the one member with exactly that archive path. A leading `./` is stripped. Absolute paths, drive
|
|
82
|
+
* prefixes, backslashes, empty, `.` and `..` segments and glob metacharacters are refused. Returns { value, byPath } or
|
|
83
|
+
* { error }. The member never influences where the extracted file is written.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeArchiveMember(member) {
|
|
86
|
+
if (typeof member !== "string" || !member) return { error: "must be a non-empty string" };
|
|
87
|
+
if (member.includes("\\")) return { error: "must not contain a backslash" };
|
|
88
|
+
const value = member.startsWith("./") ? member.slice(2) : member;
|
|
89
|
+
if (value.startsWith("/")) return { error: "must not be an absolute path" };
|
|
90
|
+
if (/^[A-Za-z]:/.test(value)) return { error: "must not start with a drive prefix" };
|
|
91
|
+
const segments = value.split("/");
|
|
92
|
+
if (segments.some((segment) => segment === "")) return { error: "must not contain an empty segment" };
|
|
93
|
+
if (segments.some((segment) => segment === "." || segment === "..")) return { error: "must not contain a '.' or '..' segment" };
|
|
94
|
+
// A bare file name keeps today's matching (any characters but the refused ones above); a path is held to a strict segment alphabet so it can never act as a tar glob.
|
|
95
|
+
if (segments.length > 1 && !segments.every((segment) => MEMBER_SEGMENT.test(segment))) return { error: "has a segment with characters outside A-Z a-z 0-9 _ . + @ ~ -" };
|
|
96
|
+
return { value, byPath: segments.length > 1 };
|
|
97
|
+
}
|
|
73
98
|
const KEYS = Object.freeze({
|
|
74
99
|
"cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
|
|
75
100
|
ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "sources", "probe", "platforms"],
|
|
@@ -77,7 +102,7 @@ const KEYS = Object.freeze({
|
|
|
77
102
|
resource: ["kind", "from", "to", "sha256", "machO", "clean", "platforms"],
|
|
78
103
|
command: ["kind", "name", "cmd", "args", "cwd", "output", "externalBin", "dest", "clean", "platforms"],
|
|
79
104
|
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"],
|
|
105
|
+
cmake: ["kind", "name", "repo", "commit", "target", "output", "cmakeArgs", "platformArgs", "generator", "platformGenerators", "architecture", "cmakeBin", "smoke", "sha256", "dest", "clean", "platforms"],
|
|
81
106
|
prebuilt: ["kind", "name", "from", "manifest", "dest", "platforms"],
|
|
82
107
|
});
|
|
83
108
|
const TRIPLES = Object.freeze({
|
|
@@ -190,6 +215,8 @@ function parseCatalogSpec(value, label, errors, kind) {
|
|
|
190
215
|
return undefined;
|
|
191
216
|
}
|
|
192
217
|
|
|
218
|
+
const CMAKE_GENERATOR = /^[A-Za-z0-9][A-Za-z0-9 .+-]{0,60}$/;
|
|
219
|
+
const CMAKE_ARCHITECTURE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
193
220
|
const CMAKE_DEFINE = /^-D[A-Za-z0-9_]+(?::[A-Z]+)?=[^\s]*$/;
|
|
194
221
|
const SOURCE_ARCHES = ["aarch64", "x86_64"];
|
|
195
222
|
|
|
@@ -205,8 +232,25 @@ function parseArchiveSource(value, label, errors) {
|
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
235
|
+
* A local ffmpeg source: `{ path, member? }`. `path` is relative to the app root and MAY step out of it (a vendored build such as
|
|
236
|
+
* ../vendor/ffmpeg-lgpl); it names the binary itself, or a directory holding `<member>` (or `bin/<member>`). Nothing is downloaded and no
|
|
237
|
+
* archive digest exists: the file's digest must still equal the APPROVED catalog row, exactly like a fetched member.
|
|
238
|
+
*/
|
|
239
|
+
function parseLocalSource(value, label, errors) {
|
|
240
|
+
for (const key of Object.keys(value)) if (!["path", "member"].includes(key)) errors.push(`${label}: unknown key ${JSON.stringify(key)} for a local { path, member? } source`);
|
|
241
|
+
const raw = value.path;
|
|
242
|
+
const normalized = typeof raw === "string" ? raw.replaceAll("\\", "/") : "";
|
|
243
|
+
if (typeof raw !== "string" || !raw.trim() || path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw) || /^[a-z][a-z0-9+.-]*:\/\//i.test(normalized)) { errors.push(`${label}.path must be a relative path (it may start with ../); a URL belongs in { url, sha256, member }`); return undefined; }
|
|
244
|
+
if (!normalized.split("/").some((segment) => segment !== "" && segment !== "." && segment !== "..")) { errors.push(`${label}.path must name a file or directory, not the app root or a bare parent: ${raw}`); return undefined; }
|
|
245
|
+
if (value.member !== undefined && (typeof value.member !== "string" || !NAME.test(value.member))) errors.push(`${label}.member must be the bare file name of the binary inside the directory`);
|
|
246
|
+
return { path: normalized.replace(/\/+$/, ""), ...(typeof value.member === "string" ? { member: value.member } : {}) };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const parseFfmpegSource = (value, label, errors) => (isObject(value) && "path" in value ? parseLocalSource(value, label, errors) : parseArchiveSource(value, label, errors));
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `ffmpeg.sources`: where the bytes of a catalog row come from (the catalog pins digests, not locations). A tool maps to one
|
|
253
|
+
* source (a pinned https archive, or a local path), or to `{ aarch64: source, x86_64: source }`; a universal target needs the per-arch form for every tool.
|
|
210
254
|
*/
|
|
211
255
|
function parseFfmpegSources(raw, where, errors, { universal }) {
|
|
212
256
|
if (raw === undefined) return undefined;
|
|
@@ -215,12 +259,12 @@ function parseFfmpegSources(raw, where, errors, { universal }) {
|
|
|
215
259
|
for (const [tool, value] of Object.entries(raw)) {
|
|
216
260
|
const label = `${where}: sources.${tool}`;
|
|
217
261
|
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);
|
|
262
|
+
const perArch = isObject(value) && SOURCE_ARCHES.some((arch) => arch in value) && !("url" in value) && !("path" in value);
|
|
219
263
|
if (universal && !perArch) { errors.push(`${label} needs { aarch64, x86_64 } sources for universal-apple-darwin (each slice is a thin file)`); continue; }
|
|
220
264
|
if (perArch) {
|
|
221
265
|
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,
|
|
223
|
-
} else out[tool] =
|
|
266
|
+
out[tool] = Object.fromEntries(SOURCE_ARCHES.filter((arch) => arch in value).map((arch) => [arch, parseFfmpegSource(value[arch], `${label}.${arch}`, errors)]));
|
|
267
|
+
} else out[tool] = parseFfmpegSource(value, label, errors);
|
|
224
268
|
}
|
|
225
269
|
return out;
|
|
226
270
|
}
|
|
@@ -368,7 +412,7 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
368
412
|
if (item.dest === undefined && item.to === undefined) errors.push(`${where}: dest (or to) is required`);
|
|
369
413
|
const dest = destOf(item.dest ?? item.to, item.dest === undefined ? "to" : "dest");
|
|
370
414
|
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
|
|
415
|
+
if (item.extract !== undefined) { const member = normalizeArchiveMember(item.extract); if (member.error || (!member.byPath && !NAME.test(member.value))) errors.push(`${where}: extract must be the bare file name of one archive member or its archive path (posix, no leading /, no .. segment): ${member.error ?? "invalid file name"}`); }
|
|
372
416
|
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
417
|
if (item.executable !== undefined && typeof item.executable !== "boolean") errors.push(`${where}: executable must be a boolean`);
|
|
374
418
|
const clean = cleanOf();
|
|
@@ -397,19 +441,40 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
397
441
|
else platformArgs[key] = flagList(list, `platformArgs.${key}`);
|
|
398
442
|
}
|
|
399
443
|
}
|
|
400
|
-
if (item.generator !== undefined && (typeof item.generator !== "string" ||
|
|
444
|
+
if (item.generator !== undefined && (typeof item.generator !== "string" || !CMAKE_GENERATOR.test(item.generator))) errors.push(`${where}: generator must be a generator name such as "Ninja"`);
|
|
445
|
+
const platformGenerators = {};
|
|
446
|
+
if (item.platformGenerators !== undefined) {
|
|
447
|
+
if (!isObject(item.platformGenerators)) errors.push(`${where}: platformGenerators must be an object keyed by mac, win or linux`);
|
|
448
|
+
else for (const [key, generator] of Object.entries(item.platformGenerators)) {
|
|
449
|
+
if (!PLATFORMS.has(key)) errors.push(`${where}: platformGenerators.${key}: platforms are ${[...PLATFORMS].join(", ")}`);
|
|
450
|
+
else if (typeof generator !== "string" || !CMAKE_GENERATOR.test(generator)) errors.push(`${where}: platformGenerators.${key} must be a generator name such as "Visual Studio 17 2022"`);
|
|
451
|
+
else platformGenerators[key] = generator;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (item.architecture !== undefined && (typeof item.architecture !== "string" || !CMAKE_ARCHITECTURE.test(item.architecture))) errors.push(`${where}: architecture must be a cmake -A platform such as x64, Win32 or ARM64`);
|
|
455
|
+
if (item.cmakeBin !== undefined && (typeof item.cmakeBin !== "string" || !item.cmakeBin.trim() || /[\0\r\n]/.test(item.cmakeBin))) errors.push(`${where}: cmakeBin must be a cmake executable name or path`);
|
|
401
456
|
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
457
|
sha(item.sha256, "sha256");
|
|
403
458
|
const dest = destOf(item.dest);
|
|
404
459
|
const clean = cleanOf();
|
|
405
460
|
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 } : {}) });
|
|
461
|
+
Object.assign(entry, { name, repo: item.repo, commit: item.commit, target, ...(output ? { output } : {}), cmakeArgs, platformArgs, ...(item.generator ? { generator: item.generator } : {}), ...(Object.keys(platformGenerators).length ? { platformGenerators } : {}), ...(item.architecture ? { architecture: item.architecture } : {}), ...(item.cmakeBin ? { cmakeBin: item.cmakeBin.trim() } : {}), ...(item.smoke ? { smoke: [...item.smoke] } : {}), ...(item.sha256 ? { sha256: item.sha256 } : {}), ...(dest ? { dest } : {}), ...(clean ? { clean } : {}) });
|
|
407
462
|
} else if (item.kind === "prebuilt") {
|
|
408
463
|
const name = item.name;
|
|
409
464
|
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
|
-
|
|
465
|
+
let from;
|
|
466
|
+
if (isObject(item.from)) {
|
|
467
|
+
// Per-arch inputs: each slice is verified against its own manifest entry; universal-apple-darwin lipo-merges them.
|
|
468
|
+
from = {};
|
|
469
|
+
for (const [arch, value] of Object.entries(item.from)) {
|
|
470
|
+
if (!SOURCE_ARCHES.includes(arch)) errors.push(`${where}: from.${arch}: arches are ${SOURCE_ARCHES.join(", ")}`);
|
|
471
|
+
else { const file = relativePath(value, `${where}: from.${arch}`, errors); if (file) from[arch] = file; }
|
|
472
|
+
}
|
|
473
|
+
if (!Object.keys(item.from).length) errors.push(`${where}: from must name at least one of ${SOURCE_ARCHES.join(", ")}`);
|
|
474
|
+
if (!skipped && t.arch === "universal" && !SOURCE_ARCHES.every((arch) => arch in item.from)) errors.push(`${where}: from needs both aarch64 and x86_64 for universal-apple-darwin (or omit from to use the staged <name>-<slice triple> files)`);
|
|
475
|
+
} else from = item.from === undefined ? undefined : relativePath(item.from, `${where}: from`, errors);
|
|
411
476
|
const manifest = parsePrebuiltManifest(item.manifest, where, errors);
|
|
412
|
-
if (!skipped && t.arch === "universal") errors.push(`${where}:
|
|
477
|
+
if (!skipped && t.arch === "universal" && typeof item.from === "string") errors.push(`${where}: from must be { aarch64, x86_64 } for universal-apple-darwin (each slice is verified against its own manifest entry, then lipo-merged)`);
|
|
413
478
|
const dest = destOf(item.dest);
|
|
414
479
|
if (!skipped) { if (dest) claim(dest, where); else if (typeof name === "string" && stageDir) claim(stagedClaim(name), where); }
|
|
415
480
|
Object.assign(entry, { name, ...(from ? { from } : {}), ...(manifest ? { manifest } : {}), ...(dest ? { dest } : {}) });
|
|
@@ -434,6 +499,17 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
434
499
|
}
|
|
435
500
|
|
|
436
501
|
const sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
502
|
+
/**
|
|
503
|
+
* Signature-independent catalog digest of `bytes` for `arch` (content-digest.mjs, port of
|
|
504
|
+
* rightkit-ffmpeg content_digest.rs). Catalog `contentSha256` rows pin THIS digest, never a
|
|
505
|
+
* whole-file SHA-256: signed & unsigned copies of one build agree. A malformed recognized
|
|
506
|
+
* format throws ContentDigestError (never a plain-hash fallback). Whole-file hashes elsewhere
|
|
507
|
+
* (archive/download pins, verifyStagedSidecars preSignSha256 signing evidence) stay separate.
|
|
508
|
+
*/
|
|
509
|
+
function catalogContentDigest(bytes, arch) {
|
|
510
|
+
const identity = contentIdentity(bytes);
|
|
511
|
+
return digestFor(identity, arch) ?? (identity.slices.length === 1 ? identity.slices[0].sha256 : null);
|
|
512
|
+
}
|
|
437
513
|
const profileDirectory = (profile) => (profile === "dev" || profile === "test" ? "debug" : profile === "bench" ? "release" : profile);
|
|
438
514
|
const tail = (text, lines = 12) => String(text ?? "").trim().split("\n").slice(-lines).join("\n");
|
|
439
515
|
|
|
@@ -445,15 +521,28 @@ export function defaultRun(command, args, { cwd, env = process.env } = {}) {
|
|
|
445
521
|
});
|
|
446
522
|
}
|
|
447
523
|
|
|
448
|
-
/**
|
|
524
|
+
/**
|
|
525
|
+
* Default archive member extraction (bsdtar reads tgz and zip/nupkg). A bare `fileName` selects the one member whose basename it is;
|
|
526
|
+
* a `fileName` containing `/` selects the one member whose full archive path equals it (case-sensitive, leading `./` ignored on both
|
|
527
|
+
* sides), so archives with the same base name in several directories (ORT DirectML nupkgs) can be addressed. The returned file is
|
|
528
|
+
* always under `destDir`; callers copy it to their own declared output name, so the member path never decides where anything is written.
|
|
529
|
+
*/
|
|
449
530
|
export function defaultExtractMember({ archive, fileName, destDir, run = defaultRun }) {
|
|
531
|
+
const wanted = normalizeArchiveMember(fileName);
|
|
532
|
+
if (wanted.error) fail("STAGE_SIDECAR_EXTRACT_FAILED", `archive member ${JSON.stringify(fileName)} ${wanted.error}`);
|
|
450
533
|
const listing = run("tar", ["-tf", archive], { cwd: path.dirname(archive) });
|
|
451
534
|
if (listing.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot list ${archive}`);
|
|
452
|
-
const
|
|
453
|
-
|
|
535
|
+
const lines = String(listing.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
536
|
+
const members = wanted.byPath
|
|
537
|
+
? lines.filter((line) => (line.startsWith("./") ? line.slice(2) : line) === wanted.value)
|
|
538
|
+
: lines.filter((line) => path.posix.basename(line) === wanted.value);
|
|
539
|
+
if (members.length !== 1) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${archive} has ${members.length} members ${wanted.byPath ? "at the path" : "named"} ${wanted.value}, expected exactly one`);
|
|
454
540
|
const extracted = run("tar", ["-xf", archive, "-C", destDir, members[0]], { cwd: path.dirname(archive) });
|
|
455
541
|
if (extracted.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot extract ${members[0]} from ${archive}`);
|
|
456
|
-
|
|
542
|
+
const out = path.join(destDir, ...members[0].split("/"));
|
|
543
|
+
const inside = path.relative(path.resolve(destDir), path.resolve(out));
|
|
544
|
+
if (!inside || inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) fail("STAGE_SIDECAR_EXTRACT_FAILED", `member ${members[0]} of ${archive} resolves outside the extraction directory`);
|
|
545
|
+
return out;
|
|
457
546
|
}
|
|
458
547
|
|
|
459
548
|
async function defaultFetchBytes(url) {
|
|
@@ -673,10 +762,22 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
673
762
|
}
|
|
674
763
|
|
|
675
764
|
/** 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]; };
|
|
765
|
+
const ffmpegSourceFor = (sources, tool, arch) => { const spec = sources?.[tool]; return spec?.url || spec?.path ? spec : spec?.[arch]; };
|
|
766
|
+
|
|
767
|
+
/** The binary a local ffmpeg source names: the file itself, or `<dir>/<member>` / `<dir>/bin/<member>` (`.exe` added for a Windows target). */
|
|
768
|
+
function resolveLocalFfmpegSource(ctx, spec, tool) {
|
|
769
|
+
const { fs } = ctx;
|
|
770
|
+
const base = path.resolve(ctx.appRoot, spec.path);
|
|
771
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
|
|
772
|
+
const member = spec.member ?? `${tool}${ctx.windows ? ".exe" : ""}`;
|
|
773
|
+
const candidates = [path.join(base, member), path.join(base, "bin", member)];
|
|
774
|
+
const found = candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
775
|
+
if (!found) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the local ${tool} source ${spec.path} has no ${member} (looked in ${candidates.join(", ")}); vendor the build first, nothing is downloaded for a path source`, { tool, candidates });
|
|
776
|
+
return found;
|
|
777
|
+
}
|
|
677
778
|
|
|
678
779
|
/**
|
|
679
|
-
*
|
|
780
|
+
* Fetch the bytes of the catalog rows that `ffmpeg.sources` names (a local `path` is copied, never downloaded): an archive pinned by digest, one member unpacked, the
|
|
680
781
|
* member's digest checked against the APPROVED catalog row (so a swapped archive can never stage). A staged file that already
|
|
681
782
|
* matches its row is left alone.
|
|
682
783
|
*/
|
|
@@ -686,12 +787,19 @@ async function fetchFfmpegSources(entry, ctx, plan, arch) {
|
|
|
686
787
|
for (const file of plan.files) {
|
|
687
788
|
const spec = ffmpegSourceFor(entry.sources, file.tool, arch);
|
|
688
789
|
if (!spec) continue;
|
|
689
|
-
if (fs.existsSync(file.stagedPath) &&
|
|
790
|
+
if (fs.existsSync(file.stagedPath) && catalogContentDigest(fs.readFileSync(file.stagedPath), arch) === file.contentSha256) { ctx.log(`stage-sidecar: ${rel(ctx, file.stagedPath)} already staged`); continue; }
|
|
791
|
+
if (spec.path) {
|
|
792
|
+
const local = resolveLocalFfmpegSource(ctx, spec, file.tool);
|
|
793
|
+
const actual = catalogContentDigest(fs.readFileSync(local), arch);
|
|
794
|
+
if (actual !== file.contentSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${local} is ${actual}, the approved catalog row ${file.artifactId} says ${file.contentSha256}; nothing staged`, { expected: file.contentSha256, actual });
|
|
795
|
+
copyInto(ctx, local, file.stagedPath, { executable: true });
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
690
798
|
const { cached } = await ensureCachedDownload(ctx, spec);
|
|
691
799
|
const scratch = ctx.makeTempDir();
|
|
692
800
|
try {
|
|
693
801
|
const member = await ctx.extractMember({ archive: cached, fileName: spec.member, destDir: scratch });
|
|
694
|
-
const actual =
|
|
802
|
+
const actual = catalogContentDigest(fs.readFileSync(member), arch);
|
|
695
803
|
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
804
|
copyInto(ctx, member, file.stagedPath, { executable: true });
|
|
697
805
|
} finally {
|
|
@@ -746,7 +854,9 @@ async function stageFfmpegSlice(entry, ctx, resolved, { config, t, writeNotices
|
|
|
746
854
|
const verified = verifyStagedSidecars(plan, { exists: fs.existsSync, stat: (file) => fs.statSync(file), hashFile: (file) => sha256Of(fs.readFileSync(file)) });
|
|
747
855
|
const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
|
|
748
856
|
const rows = verified.map((file) => {
|
|
749
|
-
|
|
857
|
+
// Catalog rows pin the signature-independent content digest; preSignSha256 stays whole-file signing evidence.
|
|
858
|
+
const contentDigest = catalogContentDigest(fs.readFileSync(file.stagedPath), t.arch);
|
|
859
|
+
if (contentDigest !== file.contentSha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${file.tool} staged with content digest ${contentDigest}, the approved catalog row ${file.artifactId} says ${file.contentSha256}`, { expected: file.contentSha256, actual: contentDigest });
|
|
750
860
|
const probe = entry.probe ? probeFfmpegBinary(ctx, { file: file.stagedPath, tool: file.tool, target: t }) : undefined;
|
|
751
861
|
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
862
|
});
|
|
@@ -797,7 +907,7 @@ async function stageFfmpeg(entry, ctx) {
|
|
|
797
907
|
if (target.arch === "universal") return await stageFfmpegUniversal(entry, ctx, resolved);
|
|
798
908
|
return (await stageFfmpegSlice(entry, ctx, resolved, { config, t: target, writeNotices: true })).rows;
|
|
799
909
|
} catch (error) {
|
|
800
|
-
if (error instanceof StageSidecarError) throw error;
|
|
910
|
+
if (error instanceof StageSidecarError || error instanceof ContentDigestError) throw error;
|
|
801
911
|
return fail("STAGE_SIDECAR_FFMPEG_FAILED", `ffmpeg sidecar (${target.triple}): ${error.message}`, { cause: error, causeCode: error.code });
|
|
802
912
|
}
|
|
803
913
|
}
|
|
@@ -825,6 +935,10 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
|
|
|
825
935
|
});
|
|
826
936
|
for (const { source } of notices) if (!fs.existsSync(source)) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file not found: ${source}`);
|
|
827
937
|
|
|
938
|
+
// The archive member is the catalog's archive.member (an archive path) when present, else the library file name; the output name never comes from it.
|
|
939
|
+
const member = row.archive.member ?? row.artifactFileName;
|
|
940
|
+
const memberCheck = normalizeArchiveMember(member);
|
|
941
|
+
if (memberCheck.error) fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} has an invalid archive member ${JSON.stringify(member)}: ${memberCheck.error}`);
|
|
828
942
|
const outName = entry.as ?? row.artifactFileName;
|
|
829
943
|
const dest = path.resolve(appRoot, destDir ?? entry.resourceDir, outName);
|
|
830
944
|
if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === row.sha256) {
|
|
@@ -840,9 +954,9 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
|
|
|
840
954
|
}
|
|
841
955
|
const scratch = ctx.makeTempDir();
|
|
842
956
|
try {
|
|
843
|
-
const library = await ctx.extractMember({ archive, fileName:
|
|
957
|
+
const library = await ctx.extractMember({ archive, fileName: member, destDir: scratch });
|
|
844
958
|
const actual = sha256Of(fs.readFileSync(library));
|
|
845
|
-
if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${
|
|
959
|
+
if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${member} extracted as ${actual}, the catalog pins ${row.sha256}; nothing staged`, { expected: row.sha256, actual });
|
|
846
960
|
copyInto(ctx, library, dest, { executable: false });
|
|
847
961
|
} finally {
|
|
848
962
|
ctx.removeDir(scratch);
|
|
@@ -967,7 +1081,7 @@ async function ensureCachedDownload(ctx, { url, sha256, size }) {
|
|
|
967
1081
|
|
|
968
1082
|
/**
|
|
969
1083
|
* 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
|
|
1084
|
+
* (zip, tgz): the one member whose base name is `extract` (or, when `extract` contains `/`, whose archive path equals it) is unpacked (digest-checked against `memberSha256` when given) and
|
|
971
1085
|
* only that member is staged, so no `command` entry or shell is needed to unpack it.
|
|
972
1086
|
*/
|
|
973
1087
|
async function stageRemote(entry, ctx) {
|
|
@@ -1041,6 +1155,32 @@ function checkoutPinnedSource(ctx, entry, srcDir) {
|
|
|
1041
1155
|
}
|
|
1042
1156
|
}
|
|
1043
1157
|
|
|
1158
|
+
/** Fixed Visual Studio Installer (vswhere) location under %ProgramFiles(x86)%, and the cmake Visual Studio bundles under an installationPath. */
|
|
1159
|
+
const VSWHERE_PARTS = ["Microsoft Visual Studio", "Installer", "vswhere.exe"];
|
|
1160
|
+
const VS_CMAKE_PARTS = ["Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe"];
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Which cmake to run. Order: the entry's `cmakeBin` (a bare name is looked up on PATH; a path with separators is resolved against the
|
|
1164
|
+
* app root); off a Windows host plain `cmake`; on a Windows host `cmake` when `where cmake` finds one, else the Visual Studio-bundled
|
|
1165
|
+
* cmake that vswhere locates (installationPath\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe), else plain `cmake`
|
|
1166
|
+
* (which then fails with the usual spawn error naming it).
|
|
1167
|
+
*/
|
|
1168
|
+
function resolveCmakeBinary(ctx, entry) {
|
|
1169
|
+
if (entry.cmakeBin) return /[\\/]/.test(entry.cmakeBin) ? path.resolve(ctx.appRoot, entry.cmakeBin) : entry.cmakeBin;
|
|
1170
|
+
if (ctx.host.os !== "windows") return "cmake";
|
|
1171
|
+
const probe = ctx.run("where", ["cmake"], { cwd: ctx.appRoot, env: ctx.env });
|
|
1172
|
+
if (!probe?.error && probe?.status === 0 && String(probe.stdout ?? "").trim()) return "cmake";
|
|
1173
|
+
const programFiles = ctx.env["ProgramFiles(x86)"];
|
|
1174
|
+
if (typeof programFiles !== "string" || !programFiles) return "cmake";
|
|
1175
|
+
const vswhere = path.win32.join(programFiles, ...VSWHERE_PARTS);
|
|
1176
|
+
if (!ctx.fs.existsSync(vswhere)) return "cmake";
|
|
1177
|
+
const found = ctx.run(vswhere, ["-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.CMake.Project", "-property", "installationPath"], { cwd: ctx.appRoot, env: ctx.env });
|
|
1178
|
+
const installation = found?.error || found?.status !== 0 ? "" : String(found.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
1179
|
+
if (!installation) return "cmake";
|
|
1180
|
+
const bundled = path.win32.join(installation, ...VS_CMAKE_PARTS);
|
|
1181
|
+
return ctx.fs.existsSync(bundled) ? bundled : "cmake";
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1044
1184
|
/**
|
|
1045
1185
|
* Build a pinned git commit with the build tool (no shell) and stage the result as <name>-<triple>[.exe]: fetch the commit, configure,
|
|
1046
1186
|
* build `target`, locate the executable, check it carries both archs for universal-apple-darwin, optionally pin its digest and
|
|
@@ -1049,6 +1189,8 @@ function checkoutPinnedSource(ctx, entry, srcDir) {
|
|
|
1049
1189
|
*/
|
|
1050
1190
|
function stageCmake(entry, ctx) {
|
|
1051
1191
|
const { appRoot, fs, run, env, target } = ctx;
|
|
1192
|
+
const cmake = resolveCmakeBinary(ctx, entry);
|
|
1193
|
+
const generator = entry.platformGenerators?.[target.platform] ?? entry.generator;
|
|
1052
1194
|
const root = path.resolve(appRoot, CMAKE_ROOT, entry.name);
|
|
1053
1195
|
const srcDir = path.join(root, "src");
|
|
1054
1196
|
const buildDir = path.join(root, "build");
|
|
@@ -1058,10 +1200,10 @@ function stageCmake(entry, ctx) {
|
|
|
1058
1200
|
const archFlag = target.platform !== "mac" ? undefined
|
|
1059
1201
|
: target.arch === "universal" ? "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64"
|
|
1060
1202
|
: `-DCMAKE_OSX_ARCHITECTURES=${target.arch === "aarch64" ? "arm64" : "x86_64"}`;
|
|
1061
|
-
const configure = ["-S", srcDir, "-B", buildDir, "-DCMAKE_BUILD_TYPE=Release", ...(
|
|
1203
|
+
const configure = ["-S", srcDir, "-B", buildDir, "-DCMAKE_BUILD_TYPE=Release", ...(generator ? ["-G", generator] : []), ...(entry.architecture ? ["-A", entry.architecture] : []), ...(archFlag ? [archFlag] : []), ...(entry.platformArgs[target.platform] ?? []), ...entry.cmakeArgs];
|
|
1062
1204
|
const step = (args, label) => {
|
|
1063
|
-
ctx.log(`stage-sidecar: cmake ${args.join(" ")}`);
|
|
1064
|
-
const result = run(
|
|
1205
|
+
ctx.log(`stage-sidecar: ${cmake} ${args.join(" ")}`);
|
|
1206
|
+
const result = run(cmake, args, { cwd: appRoot, env });
|
|
1065
1207
|
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
1208
|
};
|
|
1067
1209
|
step(configure, "configure");
|
|
@@ -1081,22 +1223,21 @@ function stageCmake(entry, ctx) {
|
|
|
1081
1223
|
const smoked = run(dest, entry.smoke, { cwd: appRoot, env });
|
|
1082
1224
|
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
1225
|
}
|
|
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 }];
|
|
1226
|
+
return [{ kind: "cmake", name: entry.name, path: rel(ctx, dest), ...copied, sourceSha256, repo: entry.repo, commit: entry.commit, cmakeTarget: entry.target, ...(cmake !== "cmake" ? { cmakeBin: cmake } : {}), ...(generator ? { generator } : {}), flags: configure.filter((arg) => arg.startsWith("-D")), externalBin: !entry.dest }];
|
|
1085
1227
|
}
|
|
1086
1228
|
|
|
1087
1229
|
/** `{platform}` in a prebuilt manifest entry: macos-aarch64, macos-x86_64, windows-x86_64, linux-x86_64, ... */
|
|
1088
1230
|
const prebuiltPlatformKey = (t) => `${t.os}-${t.arch}`;
|
|
1089
1231
|
|
|
1090
1232
|
/**
|
|
1091
|
-
* Verify
|
|
1092
|
-
* manifest file: its raw sha256, and when the manifest names an approval hash, the signing-independent approval digest
|
|
1233
|
+
* Verify one host-built binary `source` for `slice` (a parsed target: its os and arch name the manifest entry) against the digest in
|
|
1234
|
+
* an app manifest file: its raw sha256, and when the manifest names an approval hash, the signing-independent approval digest
|
|
1093
1235
|
* (`macho-codesign-strip-linkedit-v1` on macOS, `raw-sha256` elsewhere). Nothing is built or downloaded: a missing file or
|
|
1094
|
-
* a mismatch fails closed.
|
|
1236
|
+
* a mismatch fails closed. Returns { platformKey, actual, approval? }.
|
|
1095
1237
|
*/
|
|
1096
|
-
async function
|
|
1097
|
-
const { appRoot, fs
|
|
1098
|
-
const
|
|
1099
|
-
const platformKey = prebuiltPlatformKey(target);
|
|
1238
|
+
async function verifyPrebuilt(entry, ctx, source, slice) {
|
|
1239
|
+
const { appRoot, fs } = ctx;
|
|
1240
|
+
const platformKey = prebuiltPlatformKey(slice);
|
|
1100
1241
|
const manifestFile = path.resolve(appRoot, entry.manifest.file);
|
|
1101
1242
|
let manifest;
|
|
1102
1243
|
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 }); }
|
|
@@ -1105,7 +1246,6 @@ async function stagePrebuilt(entry, ctx) {
|
|
|
1105
1246
|
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
1247
|
const pinned = node[entry.manifest.sha256];
|
|
1107
1248
|
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
1249
|
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
1250
|
const actual = sha256Of(fs.readFileSync(source));
|
|
1111
1251
|
if (actual !== pinned) fail("STAGE_SIDECAR_HASH_MISMATCH", `${entry.name} integrity failure: ${source} is ${actual}, ${entry.manifest.file} pins ${pinned}`, { expected: pinned, actual });
|
|
@@ -1113,13 +1253,40 @@ async function stagePrebuilt(entry, ctx) {
|
|
|
1113
1253
|
if (entry.manifest.approvalSha256) {
|
|
1114
1254
|
const expected = node[entry.manifest.approvalSha256];
|
|
1115
1255
|
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] : (
|
|
1256
|
+
const algorithm = entry.manifest.approvalAlgorithm ? node[entry.manifest.approvalAlgorithm] : (slice.os === "macos" ? APPROVAL_MACHO_STRIP_LINKEDIT_V1 : APPROVAL_RAW_SHA256);
|
|
1117
1257
|
if (typeof algorithm !== "string") fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} ${platformKey}.${entry.manifest.approvalAlgorithm} does not name an approval hash algorithm`);
|
|
1118
1258
|
let digest;
|
|
1119
1259
|
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
1260
|
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
1261
|
approval = { algorithm, sha256: digest };
|
|
1122
1262
|
}
|
|
1263
|
+
return { platformKey, actual, ...(approval ? { approval } : {}) };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Verify a host-built binary (already staged at <stageDir>/<name>-<triple>, or copied from `from`) against the app manifest and stage it.
|
|
1268
|
+
* `from` is a path, or `{ aarch64, x86_64 }` (the entry for this arch is used). `universal-apple-darwin`: each slice (its `from.<arch>`, else the
|
|
1269
|
+
* staged `<stageDir>/<name>-<slice triple>`) is verified against its own manifest entry (macos-aarch64, macos-x86_64), then the two are
|
|
1270
|
+
* lipo-merged by the same helper the ffmpeg, onnxruntime and cmake kinds use into `<name>-universal-apple-darwin`; a thin merge result is refused.
|
|
1271
|
+
*/
|
|
1272
|
+
async function stagePrebuilt(entry, ctx) {
|
|
1273
|
+
const { fs, target, appRoot } = ctx;
|
|
1274
|
+
const dest = destFor(entry, ctx);
|
|
1275
|
+
if (target.arch === "universal") {
|
|
1276
|
+
const parts = [];
|
|
1277
|
+
for (const triple of SLICES) {
|
|
1278
|
+
const slice = parseTargetTriple(triple);
|
|
1279
|
+
const from = entry.from?.[slice.arch];
|
|
1280
|
+
const source = from ? path.resolve(appRoot, from) : path.resolve(appRoot, ctx.stageDir, stagedName(entry.name, slice));
|
|
1281
|
+
parts.push({ triple, source, ...(await verifyPrebuilt(entry, ctx, source, slice)) });
|
|
1282
|
+
}
|
|
1283
|
+
const merged = lipoMergeInto(ctx, parts.map((part) => part.source), dest, `prebuilt ${entry.name}`, { executable: true });
|
|
1284
|
+
return [{ kind: "prebuilt", name: entry.name, path: rel(ctx, dest), ...merged, sourceSha256: merged.sha256, manifest: entry.manifest.file, platform: "macos-universal", externalBin: !entry.dest, slices: parts.map((part) => ({ triple: part.triple, sha256: part.actual, platform: part.platformKey, ...(part.approval ? { approvalAlgorithm: part.approval.algorithm, approvalSha256: part.approval.sha256 } : {}) })) }];
|
|
1285
|
+
}
|
|
1286
|
+
const declared = isObject(entry.from) ? entry.from[target.arch] : entry.from;
|
|
1287
|
+
if (isObject(entry.from) && !declared) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `prebuilt ${entry.name} declares from for ${Object.keys(entry.from).join(", ")}, not ${target.arch}`, { name: entry.name });
|
|
1288
|
+
const source = declared ? path.resolve(appRoot, declared) : dest;
|
|
1289
|
+
const { platformKey, actual, approval } = await verifyPrebuilt(entry, ctx, source, target);
|
|
1123
1290
|
let copied;
|
|
1124
1291
|
if (path.resolve(source) === path.resolve(dest)) {
|
|
1125
1292
|
if (!ctx.windows) fs.chmodSync(dest, 0o755);
|