@rightkit/release 0.3.0 → 0.4.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 +25 -4
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +11 -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 +92 -37
- 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 +1 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- 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/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +15 -4
- package/publish-cargo.mjs +109 -22
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +45 -17
- package/release.mjs +69 -17
- package/rightkit-ownership.json +1 -1
- package/rightkit-versions.json +379 -189
- package/runtime-budgets.mjs +120 -16
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +160 -32
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
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";
|
|
@@ -77,7 +80,7 @@ const KEYS = Object.freeze({
|
|
|
77
80
|
resource: ["kind", "from", "to", "sha256", "machO", "clean", "platforms"],
|
|
78
81
|
command: ["kind", "name", "cmd", "args", "cwd", "output", "externalBin", "dest", "clean", "platforms"],
|
|
79
82
|
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"],
|
|
83
|
+
cmake: ["kind", "name", "repo", "commit", "target", "output", "cmakeArgs", "platformArgs", "generator", "platformGenerators", "architecture", "cmakeBin", "smoke", "sha256", "dest", "clean", "platforms"],
|
|
81
84
|
prebuilt: ["kind", "name", "from", "manifest", "dest", "platforms"],
|
|
82
85
|
});
|
|
83
86
|
const TRIPLES = Object.freeze({
|
|
@@ -190,6 +193,8 @@ function parseCatalogSpec(value, label, errors, kind) {
|
|
|
190
193
|
return undefined;
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
const CMAKE_GENERATOR = /^[A-Za-z0-9][A-Za-z0-9 .+-]{0,60}$/;
|
|
197
|
+
const CMAKE_ARCHITECTURE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
193
198
|
const CMAKE_DEFINE = /^-D[A-Za-z0-9_]+(?::[A-Z]+)?=[^\s]*$/;
|
|
194
199
|
const SOURCE_ARCHES = ["aarch64", "x86_64"];
|
|
195
200
|
|
|
@@ -205,8 +210,25 @@ function parseArchiveSource(value, label, errors) {
|
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
213
|
+
* A local ffmpeg source: `{ path, member? }`. `path` is relative to the app root and MAY step out of it (a vendored build such as
|
|
214
|
+
* ../vendor/ffmpeg-lgpl); it names the binary itself, or a directory holding `<member>` (or `bin/<member>`). Nothing is downloaded and no
|
|
215
|
+
* archive digest exists: the file's digest must still equal the APPROVED catalog row, exactly like a fetched member.
|
|
216
|
+
*/
|
|
217
|
+
function parseLocalSource(value, label, errors) {
|
|
218
|
+
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`);
|
|
219
|
+
const raw = value.path;
|
|
220
|
+
const normalized = typeof raw === "string" ? raw.replaceAll("\\", "/") : "";
|
|
221
|
+
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; }
|
|
222
|
+
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; }
|
|
223
|
+
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`);
|
|
224
|
+
return { path: normalized.replace(/\/+$/, ""), ...(typeof value.member === "string" ? { member: value.member } : {}) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const parseFfmpegSource = (value, label, errors) => (isObject(value) && "path" in value ? parseLocalSource(value, label, errors) : parseArchiveSource(value, label, errors));
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `ffmpeg.sources`: where the bytes of a catalog row come from (the catalog pins digests, not locations). A tool maps to one
|
|
231
|
+
* 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
232
|
*/
|
|
211
233
|
function parseFfmpegSources(raw, where, errors, { universal }) {
|
|
212
234
|
if (raw === undefined) return undefined;
|
|
@@ -215,12 +237,12 @@ function parseFfmpegSources(raw, where, errors, { universal }) {
|
|
|
215
237
|
for (const [tool, value] of Object.entries(raw)) {
|
|
216
238
|
const label = `${where}: sources.${tool}`;
|
|
217
239
|
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);
|
|
240
|
+
const perArch = isObject(value) && SOURCE_ARCHES.some((arch) => arch in value) && !("url" in value) && !("path" in value);
|
|
219
241
|
if (universal && !perArch) { errors.push(`${label} needs { aarch64, x86_64 } sources for universal-apple-darwin (each slice is a thin file)`); continue; }
|
|
220
242
|
if (perArch) {
|
|
221
243
|
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] =
|
|
244
|
+
out[tool] = Object.fromEntries(SOURCE_ARCHES.filter((arch) => arch in value).map((arch) => [arch, parseFfmpegSource(value[arch], `${label}.${arch}`, errors)]));
|
|
245
|
+
} else out[tool] = parseFfmpegSource(value, label, errors);
|
|
224
246
|
}
|
|
225
247
|
return out;
|
|
226
248
|
}
|
|
@@ -397,19 +419,40 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
397
419
|
else platformArgs[key] = flagList(list, `platformArgs.${key}`);
|
|
398
420
|
}
|
|
399
421
|
}
|
|
400
|
-
if (item.generator !== undefined && (typeof item.generator !== "string" ||
|
|
422
|
+
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"`);
|
|
423
|
+
const platformGenerators = {};
|
|
424
|
+
if (item.platformGenerators !== undefined) {
|
|
425
|
+
if (!isObject(item.platformGenerators)) errors.push(`${where}: platformGenerators must be an object keyed by mac, win or linux`);
|
|
426
|
+
else for (const [key, generator] of Object.entries(item.platformGenerators)) {
|
|
427
|
+
if (!PLATFORMS.has(key)) errors.push(`${where}: platformGenerators.${key}: platforms are ${[...PLATFORMS].join(", ")}`);
|
|
428
|
+
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"`);
|
|
429
|
+
else platformGenerators[key] = generator;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
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`);
|
|
433
|
+
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
434
|
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
435
|
sha(item.sha256, "sha256");
|
|
403
436
|
const dest = destOf(item.dest);
|
|
404
437
|
const clean = cleanOf();
|
|
405
438
|
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 } : {}) });
|
|
439
|
+
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
440
|
} else if (item.kind === "prebuilt") {
|
|
408
441
|
const name = item.name;
|
|
409
442
|
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
|
-
|
|
443
|
+
let from;
|
|
444
|
+
if (isObject(item.from)) {
|
|
445
|
+
// Per-arch inputs: each slice is verified against its own manifest entry; universal-apple-darwin lipo-merges them.
|
|
446
|
+
from = {};
|
|
447
|
+
for (const [arch, value] of Object.entries(item.from)) {
|
|
448
|
+
if (!SOURCE_ARCHES.includes(arch)) errors.push(`${where}: from.${arch}: arches are ${SOURCE_ARCHES.join(", ")}`);
|
|
449
|
+
else { const file = relativePath(value, `${where}: from.${arch}`, errors); if (file) from[arch] = file; }
|
|
450
|
+
}
|
|
451
|
+
if (!Object.keys(item.from).length) errors.push(`${where}: from must name at least one of ${SOURCE_ARCHES.join(", ")}`);
|
|
452
|
+
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)`);
|
|
453
|
+
} else from = item.from === undefined ? undefined : relativePath(item.from, `${where}: from`, errors);
|
|
411
454
|
const manifest = parsePrebuiltManifest(item.manifest, where, errors);
|
|
412
|
-
if (!skipped && t.arch === "universal") errors.push(`${where}:
|
|
455
|
+
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
456
|
const dest = destOf(item.dest);
|
|
414
457
|
if (!skipped) { if (dest) claim(dest, where); else if (typeof name === "string" && stageDir) claim(stagedClaim(name), where); }
|
|
415
458
|
Object.assign(entry, { name, ...(from ? { from } : {}), ...(manifest ? { manifest } : {}), ...(dest ? { dest } : {}) });
|
|
@@ -434,6 +477,17 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
434
477
|
}
|
|
435
478
|
|
|
436
479
|
const sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
480
|
+
/**
|
|
481
|
+
* Signature-independent catalog digest of `bytes` for `arch` (content-digest.mjs, port of
|
|
482
|
+
* rightkit-ffmpeg content_digest.rs). Catalog `contentSha256` rows pin THIS digest, never a
|
|
483
|
+
* whole-file SHA-256: signed & unsigned copies of one build agree. A malformed recognized
|
|
484
|
+
* format throws ContentDigestError (never a plain-hash fallback). Whole-file hashes elsewhere
|
|
485
|
+
* (archive/download pins, verifyStagedSidecars preSignSha256 signing evidence) stay separate.
|
|
486
|
+
*/
|
|
487
|
+
function catalogContentDigest(bytes, arch) {
|
|
488
|
+
const identity = contentIdentity(bytes);
|
|
489
|
+
return digestFor(identity, arch) ?? (identity.slices.length === 1 ? identity.slices[0].sha256 : null);
|
|
490
|
+
}
|
|
437
491
|
const profileDirectory = (profile) => (profile === "dev" || profile === "test" ? "debug" : profile === "bench" ? "release" : profile);
|
|
438
492
|
const tail = (text, lines = 12) => String(text ?? "").trim().split("\n").slice(-lines).join("\n");
|
|
439
493
|
|
|
@@ -673,10 +727,22 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
673
727
|
}
|
|
674
728
|
|
|
675
729
|
/** 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]; };
|
|
730
|
+
const ffmpegSourceFor = (sources, tool, arch) => { const spec = sources?.[tool]; return spec?.url || spec?.path ? spec : spec?.[arch]; };
|
|
731
|
+
|
|
732
|
+
/** The binary a local ffmpeg source names: the file itself, or `<dir>/<member>` / `<dir>/bin/<member>` (`.exe` added for a Windows target). */
|
|
733
|
+
function resolveLocalFfmpegSource(ctx, spec, tool) {
|
|
734
|
+
const { fs } = ctx;
|
|
735
|
+
const base = path.resolve(ctx.appRoot, spec.path);
|
|
736
|
+
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
|
|
737
|
+
const member = spec.member ?? `${tool}${ctx.windows ? ".exe" : ""}`;
|
|
738
|
+
const candidates = [path.join(base, member), path.join(base, "bin", member)];
|
|
739
|
+
const found = candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
740
|
+
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 });
|
|
741
|
+
return found;
|
|
742
|
+
}
|
|
677
743
|
|
|
678
744
|
/**
|
|
679
|
-
*
|
|
745
|
+
* 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
746
|
* member's digest checked against the APPROVED catalog row (so a swapped archive can never stage). A staged file that already
|
|
681
747
|
* matches its row is left alone.
|
|
682
748
|
*/
|
|
@@ -686,12 +752,19 @@ async function fetchFfmpegSources(entry, ctx, plan, arch) {
|
|
|
686
752
|
for (const file of plan.files) {
|
|
687
753
|
const spec = ffmpegSourceFor(entry.sources, file.tool, arch);
|
|
688
754
|
if (!spec) continue;
|
|
689
|
-
if (fs.existsSync(file.stagedPath) &&
|
|
755
|
+
if (fs.existsSync(file.stagedPath) && catalogContentDigest(fs.readFileSync(file.stagedPath), arch) === file.contentSha256) { ctx.log(`stage-sidecar: ${rel(ctx, file.stagedPath)} already staged`); continue; }
|
|
756
|
+
if (spec.path) {
|
|
757
|
+
const local = resolveLocalFfmpegSource(ctx, spec, file.tool);
|
|
758
|
+
const actual = catalogContentDigest(fs.readFileSync(local), arch);
|
|
759
|
+
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 });
|
|
760
|
+
copyInto(ctx, local, file.stagedPath, { executable: true });
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
690
763
|
const { cached } = await ensureCachedDownload(ctx, spec);
|
|
691
764
|
const scratch = ctx.makeTempDir();
|
|
692
765
|
try {
|
|
693
766
|
const member = await ctx.extractMember({ archive: cached, fileName: spec.member, destDir: scratch });
|
|
694
|
-
const actual =
|
|
767
|
+
const actual = catalogContentDigest(fs.readFileSync(member), arch);
|
|
695
768
|
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
769
|
copyInto(ctx, member, file.stagedPath, { executable: true });
|
|
697
770
|
} finally {
|
|
@@ -746,7 +819,9 @@ async function stageFfmpegSlice(entry, ctx, resolved, { config, t, writeNotices
|
|
|
746
819
|
const verified = verifyStagedSidecars(plan, { exists: fs.existsSync, stat: (file) => fs.statSync(file), hashFile: (file) => sha256Of(fs.readFileSync(file)) });
|
|
747
820
|
const notices = noticeFiles.map((file) => ({ name: path.basename(file), path: rel(ctx, file), sha256: sha256Of(fs.readFileSync(file)) }));
|
|
748
821
|
const rows = verified.map((file) => {
|
|
749
|
-
|
|
822
|
+
// Catalog rows pin the signature-independent content digest; preSignSha256 stays whole-file signing evidence.
|
|
823
|
+
const contentDigest = catalogContentDigest(fs.readFileSync(file.stagedPath), t.arch);
|
|
824
|
+
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
825
|
const probe = entry.probe ? probeFfmpegBinary(ctx, { file: file.stagedPath, tool: file.tool, target: t }) : undefined;
|
|
751
826
|
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
827
|
});
|
|
@@ -797,7 +872,7 @@ async function stageFfmpeg(entry, ctx) {
|
|
|
797
872
|
if (target.arch === "universal") return await stageFfmpegUniversal(entry, ctx, resolved);
|
|
798
873
|
return (await stageFfmpegSlice(entry, ctx, resolved, { config, t: target, writeNotices: true })).rows;
|
|
799
874
|
} catch (error) {
|
|
800
|
-
if (error instanceof StageSidecarError) throw error;
|
|
875
|
+
if (error instanceof StageSidecarError || error instanceof ContentDigestError) throw error;
|
|
801
876
|
return fail("STAGE_SIDECAR_FFMPEG_FAILED", `ffmpeg sidecar (${target.triple}): ${error.message}`, { cause: error, causeCode: error.code });
|
|
802
877
|
}
|
|
803
878
|
}
|
|
@@ -1041,6 +1116,32 @@ function checkoutPinnedSource(ctx, entry, srcDir) {
|
|
|
1041
1116
|
}
|
|
1042
1117
|
}
|
|
1043
1118
|
|
|
1119
|
+
/** Fixed Visual Studio Installer (vswhere) location under %ProgramFiles(x86)%, and the cmake Visual Studio bundles under an installationPath. */
|
|
1120
|
+
const VSWHERE_PARTS = ["Microsoft Visual Studio", "Installer", "vswhere.exe"];
|
|
1121
|
+
const VS_CMAKE_PARTS = ["Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe"];
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* 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
|
|
1125
|
+
* app root); off a Windows host plain `cmake`; on a Windows host `cmake` when `where cmake` finds one, else the Visual Studio-bundled
|
|
1126
|
+
* cmake that vswhere locates (installationPath\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe), else plain `cmake`
|
|
1127
|
+
* (which then fails with the usual spawn error naming it).
|
|
1128
|
+
*/
|
|
1129
|
+
function resolveCmakeBinary(ctx, entry) {
|
|
1130
|
+
if (entry.cmakeBin) return /[\\/]/.test(entry.cmakeBin) ? path.resolve(ctx.appRoot, entry.cmakeBin) : entry.cmakeBin;
|
|
1131
|
+
if (ctx.host.os !== "windows") return "cmake";
|
|
1132
|
+
const probe = ctx.run("where", ["cmake"], { cwd: ctx.appRoot, env: ctx.env });
|
|
1133
|
+
if (!probe?.error && probe?.status === 0 && String(probe.stdout ?? "").trim()) return "cmake";
|
|
1134
|
+
const programFiles = ctx.env["ProgramFiles(x86)"];
|
|
1135
|
+
if (typeof programFiles !== "string" || !programFiles) return "cmake";
|
|
1136
|
+
const vswhere = path.win32.join(programFiles, ...VSWHERE_PARTS);
|
|
1137
|
+
if (!ctx.fs.existsSync(vswhere)) return "cmake";
|
|
1138
|
+
const found = ctx.run(vswhere, ["-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.CMake.Project", "-property", "installationPath"], { cwd: ctx.appRoot, env: ctx.env });
|
|
1139
|
+
const installation = found?.error || found?.status !== 0 ? "" : String(found.stdout ?? "").split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
1140
|
+
if (!installation) return "cmake";
|
|
1141
|
+
const bundled = path.win32.join(installation, ...VS_CMAKE_PARTS);
|
|
1142
|
+
return ctx.fs.existsSync(bundled) ? bundled : "cmake";
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1044
1145
|
/**
|
|
1045
1146
|
* Build a pinned git commit with the build tool (no shell) and stage the result as <name>-<triple>[.exe]: fetch the commit, configure,
|
|
1046
1147
|
* build `target`, locate the executable, check it carries both archs for universal-apple-darwin, optionally pin its digest and
|
|
@@ -1049,6 +1150,8 @@ function checkoutPinnedSource(ctx, entry, srcDir) {
|
|
|
1049
1150
|
*/
|
|
1050
1151
|
function stageCmake(entry, ctx) {
|
|
1051
1152
|
const { appRoot, fs, run, env, target } = ctx;
|
|
1153
|
+
const cmake = resolveCmakeBinary(ctx, entry);
|
|
1154
|
+
const generator = entry.platformGenerators?.[target.platform] ?? entry.generator;
|
|
1052
1155
|
const root = path.resolve(appRoot, CMAKE_ROOT, entry.name);
|
|
1053
1156
|
const srcDir = path.join(root, "src");
|
|
1054
1157
|
const buildDir = path.join(root, "build");
|
|
@@ -1058,10 +1161,10 @@ function stageCmake(entry, ctx) {
|
|
|
1058
1161
|
const archFlag = target.platform !== "mac" ? undefined
|
|
1059
1162
|
: target.arch === "universal" ? "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64"
|
|
1060
1163
|
: `-DCMAKE_OSX_ARCHITECTURES=${target.arch === "aarch64" ? "arm64" : "x86_64"}`;
|
|
1061
|
-
const configure = ["-S", srcDir, "-B", buildDir, "-DCMAKE_BUILD_TYPE=Release", ...(
|
|
1164
|
+
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
1165
|
const step = (args, label) => {
|
|
1063
|
-
ctx.log(`stage-sidecar: cmake ${args.join(" ")}`);
|
|
1064
|
-
const result = run(
|
|
1166
|
+
ctx.log(`stage-sidecar: ${cmake} ${args.join(" ")}`);
|
|
1167
|
+
const result = run(cmake, args, { cwd: appRoot, env });
|
|
1065
1168
|
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
1169
|
};
|
|
1067
1170
|
step(configure, "configure");
|
|
@@ -1081,22 +1184,21 @@ function stageCmake(entry, ctx) {
|
|
|
1081
1184
|
const smoked = run(dest, entry.smoke, { cwd: appRoot, env });
|
|
1082
1185
|
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
1186
|
}
|
|
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 }];
|
|
1187
|
+
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
1188
|
}
|
|
1086
1189
|
|
|
1087
1190
|
/** `{platform}` in a prebuilt manifest entry: macos-aarch64, macos-x86_64, windows-x86_64, linux-x86_64, ... */
|
|
1088
1191
|
const prebuiltPlatformKey = (t) => `${t.os}-${t.arch}`;
|
|
1089
1192
|
|
|
1090
1193
|
/**
|
|
1091
|
-
* Verify
|
|
1092
|
-
* manifest file: its raw sha256, and when the manifest names an approval hash, the signing-independent approval digest
|
|
1194
|
+
* Verify one host-built binary `source` for `slice` (a parsed target: its os and arch name the manifest entry) against the digest in
|
|
1195
|
+
* an app manifest file: its raw sha256, and when the manifest names an approval hash, the signing-independent approval digest
|
|
1093
1196
|
* (`macho-codesign-strip-linkedit-v1` on macOS, `raw-sha256` elsewhere). Nothing is built or downloaded: a missing file or
|
|
1094
|
-
* a mismatch fails closed.
|
|
1197
|
+
* a mismatch fails closed. Returns { platformKey, actual, approval? }.
|
|
1095
1198
|
*/
|
|
1096
|
-
async function
|
|
1097
|
-
const { appRoot, fs
|
|
1098
|
-
const
|
|
1099
|
-
const platformKey = prebuiltPlatformKey(target);
|
|
1199
|
+
async function verifyPrebuilt(entry, ctx, source, slice) {
|
|
1200
|
+
const { appRoot, fs } = ctx;
|
|
1201
|
+
const platformKey = prebuiltPlatformKey(slice);
|
|
1100
1202
|
const manifestFile = path.resolve(appRoot, entry.manifest.file);
|
|
1101
1203
|
let manifest;
|
|
1102
1204
|
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 +1207,6 @@ async function stagePrebuilt(entry, ctx) {
|
|
|
1105
1207
|
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
1208
|
const pinned = node[entry.manifest.sha256];
|
|
1107
1209
|
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
1210
|
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
1211
|
const actual = sha256Of(fs.readFileSync(source));
|
|
1111
1212
|
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 +1214,40 @@ async function stagePrebuilt(entry, ctx) {
|
|
|
1113
1214
|
if (entry.manifest.approvalSha256) {
|
|
1114
1215
|
const expected = node[entry.manifest.approvalSha256];
|
|
1115
1216
|
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] : (
|
|
1217
|
+
const algorithm = entry.manifest.approvalAlgorithm ? node[entry.manifest.approvalAlgorithm] : (slice.os === "macos" ? APPROVAL_MACHO_STRIP_LINKEDIT_V1 : APPROVAL_RAW_SHA256);
|
|
1117
1218
|
if (typeof algorithm !== "string") fail("STAGE_SIDECAR_NOT_APPROVED", `${entry.manifest.file} ${platformKey}.${entry.manifest.approvalAlgorithm} does not name an approval hash algorithm`);
|
|
1118
1219
|
let digest;
|
|
1119
1220
|
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
1221
|
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
1222
|
approval = { algorithm, sha256: digest };
|
|
1122
1223
|
}
|
|
1224
|
+
return { platformKey, actual, ...(approval ? { approval } : {}) };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Verify a host-built binary (already staged at <stageDir>/<name>-<triple>, or copied from `from`) against the app manifest and stage it.
|
|
1229
|
+
* `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
|
|
1230
|
+
* staged `<stageDir>/<name>-<slice triple>`) is verified against its own manifest entry (macos-aarch64, macos-x86_64), then the two are
|
|
1231
|
+
* lipo-merged by the same helper the ffmpeg, onnxruntime and cmake kinds use into `<name>-universal-apple-darwin`; a thin merge result is refused.
|
|
1232
|
+
*/
|
|
1233
|
+
async function stagePrebuilt(entry, ctx) {
|
|
1234
|
+
const { fs, target, appRoot } = ctx;
|
|
1235
|
+
const dest = destFor(entry, ctx);
|
|
1236
|
+
if (target.arch === "universal") {
|
|
1237
|
+
const parts = [];
|
|
1238
|
+
for (const triple of SLICES) {
|
|
1239
|
+
const slice = parseTargetTriple(triple);
|
|
1240
|
+
const from = entry.from?.[slice.arch];
|
|
1241
|
+
const source = from ? path.resolve(appRoot, from) : path.resolve(appRoot, ctx.stageDir, stagedName(entry.name, slice));
|
|
1242
|
+
parts.push({ triple, source, ...(await verifyPrebuilt(entry, ctx, source, slice)) });
|
|
1243
|
+
}
|
|
1244
|
+
const merged = lipoMergeInto(ctx, parts.map((part) => part.source), dest, `prebuilt ${entry.name}`, { executable: true });
|
|
1245
|
+
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 } : {}) })) }];
|
|
1246
|
+
}
|
|
1247
|
+
const declared = isObject(entry.from) ? entry.from[target.arch] : entry.from;
|
|
1248
|
+
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 });
|
|
1249
|
+
const source = declared ? path.resolve(appRoot, declared) : dest;
|
|
1250
|
+
const { platformKey, actual, approval } = await verifyPrebuilt(entry, ctx, source, target);
|
|
1123
1251
|
let copied;
|
|
1124
1252
|
if (path.resolve(source) === path.resolve(dest)) {
|
|
1125
1253
|
if (!ctx.windows) fs.chmodSync(dest, 0o755);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The distribution an upload uses for one platform. An app that ships desktop and mobile has a different store per
|
|
2
|
+
// platform, so `targets.<platform>.distribution` wins; the top-level `distribution` is the single-store fallback.
|
|
3
|
+
export function targetDistribution(config, platform) {
|
|
4
|
+
return config?.targets?.[platform]?.distribution ?? config?.distribution;
|
|
5
|
+
}
|
package/upload-release.mjs
CHANGED
|
@@ -36,7 +36,12 @@ if (platform === "android") {
|
|
|
36
36
|
const { main } = await import("./android-play-upload.mjs");
|
|
37
37
|
process.exit(await main({ releaseId, configName, apply, dryRun }));
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// iOS: TestFlight through the App Store Connect build-upload API (in process, no key file), with its own gate and a dry run by default (ios-upload.mjs).
|
|
40
|
+
if (platform === "ios") {
|
|
41
|
+
const { main } = await import("./ios-upload.mjs");
|
|
42
|
+
process.exit(await main({ releaseId, configName, apply, dryRun }));
|
|
43
|
+
}
|
|
44
|
+
if (apply) fail("--apply is Android and iOS only; win and mac preview with --dry-run");
|
|
40
45
|
|
|
41
46
|
// Tier, primary checkout, clean tree, sealed commit, ownership evidence, provider and repository are checked by
|
|
42
47
|
// the gate that `right-release github --release` also uses; nothing below runs until it passes.
|
|
@@ -82,7 +87,7 @@ function writeJson(file, value) {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
function usage(code) {
|
|
85
|
-
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]\n right-release upload --platform android --release <sealed-id> [--config right-release.config.mjs] [--apply] (Play internal track; dry run unless --apply)");
|
|
90
|
+
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.mjs] [--repo owner/repo] [--dry-run]\n right-release upload --platform android --release <sealed-id> [--config right-release.config.mjs] [--apply] (Play internal track; dry run unless --apply)\n right-release upload --platform ios --release <sealed-id> [--config right-release.config.mjs] [--apply] (TestFlight; dry run unless --apply)");
|
|
86
91
|
process.exit(code);
|
|
87
92
|
}
|
|
88
93
|
|