@rightkit/release 0.2.172 → 0.2.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +2 -0
- package/development.mjs +16 -2
- package/install-tauri-devid.mjs +172 -0
- package/inventory-check.mjs +1 -1
- package/mac-package-recipe.mjs +304 -0
- package/media-sidecar.mjs +1 -1
- package/package-recipe-targets.mjs +95 -0
- package/package.json +1 -1
- package/project-type.mjs +1 -1
- package/release.mjs +4 -1
- package/rightkit-sync.mjs +1 -1
- package/rightkit-versions.json +5 -4
- package/stage-sidecar-command.mjs +41 -0
- package/stage-sidecar.mjs +424 -0
- package/swift-mac-dmg.mjs +2 -1
- package/win-package-recipe.mjs +321 -0
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"sha256": "
|
|
3
|
+
"sha256": "c6b47bc1767d45fc9a72a401a258d5f645a0c54185f2bf2b6c51634cff7c8d6b",
|
|
4
4
|
"identity": "Developer ID Application: Adrian D'souza (6KLGD3LLKF)",
|
|
5
5
|
"codesign": "strict-valid"
|
|
6
6
|
}
|
package/cli/right-release.mjs
CHANGED
|
@@ -21,6 +21,7 @@ const commands = new Map([
|
|
|
21
21
|
["sign-dev-artifact", "sign-dev-artifact.mjs"],
|
|
22
22
|
["sign-candidate", "sign-candidate.mjs"],
|
|
23
23
|
["engine-locate", "engine-locator.mjs"],
|
|
24
|
+
["stage-sidecar", "stage-sidecar-command.mjs"],
|
|
24
25
|
["signing-agent", "signing-agent-command.mjs"],
|
|
25
26
|
["create-mac-updater", "create-mac-updater.mjs"],
|
|
26
27
|
["mirror-root-artifact", "mirror-root-artifact.mjs"],
|
|
@@ -181,6 +182,7 @@ function printHelp() {
|
|
|
181
182
|
console.log(`right-release <command> [options]
|
|
182
183
|
|
|
183
184
|
Commands:
|
|
185
|
+
stage-sidecar [--config <file>] [--target <triple>] [--json] Stage declarative sidecars into src-tauri/binaries (Tauri externalBin naming)
|
|
184
186
|
dmg build --app <.app> --out <dmg> --volname <name> [--background <png>] [--spec <appdmg.json>]
|
|
185
187
|
[--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h] [--window-position x,y]
|
|
186
188
|
Build branded UDZO image without Finder
|
package/development.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { previousPathOf, stagingPathOf, swapInPlace } from "./in-place-install.m
|
|
|
13
13
|
import { stageDevelopmentBinaries, validateStageEntries } from "./stage-binary.mjs";
|
|
14
14
|
import { assertStableDesignatedRequirement, readBundleExecutable, requestedCiAdhocSigning, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
15
15
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
16
|
+
import { installTauriDevId, resolveTauriDevIdDescriptor, TAURI_DEVID_KIND } from "./install-tauri-devid.mjs";
|
|
16
17
|
import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
|
|
17
18
|
|
|
18
19
|
// One standard Cargo profile for every app (templates/cargo-profiles.toml): Cargo's built-in `dev`.
|
|
@@ -70,6 +71,10 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
|
|
|
70
71
|
for (const key of ["sourceDirectory", "entrypoint"]) {
|
|
71
72
|
if (typeof descriptor[key] !== "string" || !descriptor[key] || path.isAbsolute(descriptor[key]) || path.win32.isAbsolute(descriptor[key]) || descriptor[key].split(/[\\/]/).includes("..")) throw new Error(`portable development ${key} must be a contained relative path`);
|
|
72
73
|
}
|
|
74
|
+
} else if (target.install?.kind === TAURI_DEVID_KIND) {
|
|
75
|
+
if (platform !== "mac") throw new Error(`the ${TAURI_DEVID_KIND} development installer is for --platform mac`);
|
|
76
|
+
resolveTauriDevIdDescriptor(target.install, { appRoot: root });
|
|
77
|
+
if (!target.install.targetTriple && !target.install.bundleDirectory) throw new Error(`development.targets.${platform}.install requires targetTriple (or bundleDirectory) to find the built .app`);
|
|
73
78
|
} else if (target.install?.kind === "adb") {
|
|
74
79
|
if (platform !== "android") throw new Error("the adb development installer is for --platform android");
|
|
75
80
|
} else if (target.install) throw new Error(`unsupported development installer kind: ${target.install.kind}`);
|
|
@@ -94,6 +99,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
94
99
|
const childEnv = codesignEnvironment(developmentEnvironment(env));
|
|
95
100
|
// One lease spans every managed request this run makes, so a broker cutover cannot land between them.
|
|
96
101
|
const lease = acquireManagedRunLease({ command: `right-release dev --platform ${platform}`, env });
|
|
102
|
+
let releaseLease = true;
|
|
97
103
|
try {
|
|
98
104
|
const startedMs = Date.now();
|
|
99
105
|
// Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
|
|
@@ -104,9 +110,17 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
104
110
|
const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform }) : [];
|
|
105
111
|
for (const file of staged) console.log(`right-release dev: staged ${file}`);
|
|
106
112
|
if (buildOnly || !target.install) return { built: true, installed: false, ...(staged.length ? { staged } : {}) };
|
|
113
|
+
if (target.install.kind === TAURI_DEVID_KIND) {
|
|
114
|
+
// installTauriDevId is async: the lease is held until it settles, so a broker cutover cannot land mid-install.
|
|
115
|
+
const built = resolveTauriArtifact(target.install, { root, platform, env: childEnv });
|
|
116
|
+
releaseLease = false;
|
|
117
|
+
return installTauriDevId({ appRoot: root, config: target.install, built, env: childEnv })
|
|
118
|
+
.then((outcome) => ({ built: true, installed: true, ...(staged.length ? { staged } : {}), devid: outcome }))
|
|
119
|
+
.finally(() => lease.release());
|
|
120
|
+
}
|
|
107
121
|
const signing = install(target.install, { root, platform, config, env: childEnv, run, sinceMs: startedMs, serial });
|
|
108
122
|
return { built: true, installed: true, ...(staged.length ? { staged } : {}), ...(platform === "mac" && target.install.kind === "tauri" ? { signing } : {}) };
|
|
109
|
-
} finally { lease.release(); }
|
|
123
|
+
} finally { if (releaseLease) lease.release(); }
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
export function assertNativeDevelopmentHost(platform, host = process.platform) {
|
|
@@ -341,7 +355,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
341
355
|
if (pathKey) process.env[pathKey] = safeEnv[pathKey] ?? process.env[pathKey];
|
|
342
356
|
const loaded = await import(pathToFileURL(absoluteConfig));
|
|
343
357
|
const config = loaded.default ?? loaded.config ?? loaded;
|
|
344
|
-
const result = runDevelopment({ root: path.dirname(absoluteConfig), platform, config, dryRun, buildOnly, serial });
|
|
358
|
+
const result = await runDevelopment({ root: path.dirname(absoluteConfig), platform, config, dryRun, buildOnly, serial });
|
|
345
359
|
if (dryRun) console.log(JSON.stringify(result.plan, null, 2));
|
|
346
360
|
return result;
|
|
347
361
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `install.kind: "tauri-devid"`: install a freshly built Tauri .app at ONE fixed path, signed with the stable
|
|
2
|
+
// Developer ID, so macOS privacy grants (TCC) and Keychain code requirements survive every rebuild.
|
|
3
|
+
//
|
|
4
|
+
// Pipeline (installTauriDevId): copy the built .app to a private staging copy -> sign nested code, then the app,
|
|
5
|
+
// with the stable Developer ID (signMacosDevelopmentBundle) -> assert the designated requirement equals the
|
|
6
|
+
// installed copy's (assertStableDesignatedRequirement) -> install at the fixed path keeping `<dest>.previous`
|
|
7
|
+
// (installApp, dev mode) -> verify the installed copy; any verify failure restores `.previous`.
|
|
8
|
+
// resetTauriDevId restores `.previous` or removes the install, only when asked.
|
|
9
|
+
//
|
|
10
|
+
// Every external call (copy, sign, run, install, file system) is injectable through `deps`; the defaults are the
|
|
11
|
+
// real RightKit pieces. macOS paths are POSIX, so this module uses path.posix throughout.
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { spawnSync } from "./background-process.mjs";
|
|
17
|
+
import { assertStableDesignatedRequirement, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
18
|
+
import { installApp } from "./install-app.mjs";
|
|
19
|
+
import { previousPathOf } from "./in-place-install.mjs";
|
|
20
|
+
|
|
21
|
+
export const TAURI_DEVID_KIND = "tauri-devid";
|
|
22
|
+
|
|
23
|
+
export class TauriDevIdError extends Error {
|
|
24
|
+
constructor(code, message, details = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "TauriDevIdError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
Object.assign(this, details);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const P = path.posix;
|
|
33
|
+
const absolute = (appRoot, value) => (P.isAbsolute(value) ? P.normalize(value) : P.join(appRoot, value));
|
|
34
|
+
const text = (result) => `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`.trim();
|
|
35
|
+
|
|
36
|
+
/** Real implementations of every external call. Tests replace any of them. */
|
|
37
|
+
export function defaultDeps() {
|
|
38
|
+
return {
|
|
39
|
+
run: spawnSync,
|
|
40
|
+
sign: signMacosDevelopmentBundle,
|
|
41
|
+
assertRequirement: assertStableDesignatedRequirement,
|
|
42
|
+
installApp,
|
|
43
|
+
exists: existsSync,
|
|
44
|
+
makeTemp: () => mkdtempSync(P.join(os.tmpdir(), "rightkit-tauri-devid-")),
|
|
45
|
+
copyApp: (from, to) => cpSync(from, to, { recursive: true, verbatimSymlinks: true }),
|
|
46
|
+
rename: renameSync,
|
|
47
|
+
remove: (target) => rmSync(target, { recursive: true, force: true }),
|
|
48
|
+
readFile: readFileSync,
|
|
49
|
+
log: (line) => console.log(line),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `tauri-devid` descriptor, validated. Accepts the config root or the descriptor itself. */
|
|
54
|
+
export function resolveTauriDevIdDescriptor(config, { appRoot = "" } = {}) {
|
|
55
|
+
const descriptor = config?.development?.targets?.mac?.install ?? config;
|
|
56
|
+
if (!descriptor || descriptor.kind !== TAURI_DEVID_KIND) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `development.targets.mac.install.kind must be "${TAURI_DEVID_KIND}"`);
|
|
57
|
+
if (typeof descriptor.productName !== "string" || !descriptor.productName) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.productName is required");
|
|
58
|
+
if (typeof descriptor.destination !== "string" || !P.isAbsolute(descriptor.destination) || !descriptor.destination.endsWith(".app") || descriptor.destination.split("/").includes("..")) {
|
|
59
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.destination must be an absolute /…/<Product>.app path (the one fixed install path, for example /Applications/<Product>.app)");
|
|
60
|
+
}
|
|
61
|
+
if (descriptor.signing !== undefined) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `install.signing is not allowed for ${TAURI_DEVID_KIND}: ad hoc signing changes every build and resets privacy grants`);
|
|
62
|
+
const binaries = descriptor.verifyBinaries ?? [];
|
|
63
|
+
if (!Array.isArray(binaries) || binaries.some((item) => typeof item !== "string" || !item || P.isAbsolute(item) || item.split("/").includes(".."))) {
|
|
64
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.verifyBinaries must be relative paths inside the app, for example Contents/MacOS/<name>");
|
|
65
|
+
}
|
|
66
|
+
return { ...descriptor, destination: P.normalize(descriptor.destination), verifyBinaries: binaries, appRoot };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const builtPathOf = (built, appRoot) => {
|
|
70
|
+
const value = typeof built === "string" ? built : built?.app;
|
|
71
|
+
if (typeof value !== "string" || !value.replace(/\/+$/, "").endsWith(".app")) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", "built must be the path of the built .app (a string, or { app })");
|
|
72
|
+
return absolute(appRoot, value.replace(/\/+$/, ""));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const digestOf = (deps, file) => createHash("sha256").update(deps.readFile(file)).digest("hex");
|
|
76
|
+
|
|
77
|
+
/** Put `.previous` back at the fixed path. Renames only; the bad copy is removed last. */
|
|
78
|
+
function restorePrevious(destination, deps) {
|
|
79
|
+
const previous = previousPathOf(destination);
|
|
80
|
+
if (!deps.exists(previous)) throw new TauriDevIdError("TAURI_DEVID_NO_PREVIOUS", `no rollback copy at ${previous}`);
|
|
81
|
+
const aside = `${destination}.reset`;
|
|
82
|
+
deps.remove(aside);
|
|
83
|
+
const hadCurrent = deps.exists(destination);
|
|
84
|
+
if (hadCurrent) deps.rename(destination, aside);
|
|
85
|
+
try { deps.rename(previous, destination); }
|
|
86
|
+
catch (error) { if (hadCurrent) deps.rename(aside, destination); throw error; }
|
|
87
|
+
deps.remove(aside);
|
|
88
|
+
return { destination, restoredFrom: previous };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verifyInstalled({ appRoot, destination, staged, expectedRequirement, binaries, env, deps }) {
|
|
92
|
+
const strict = deps.run("codesign", ["--verify", "--strict", "--deep", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
93
|
+
if (strict.error || strict.status !== 0) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} failed codesign --verify --strict --deep: ${strict.error?.message ?? text(strict)}`);
|
|
94
|
+
const display = deps.run("codesign", ["--display", "-r-", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
95
|
+
const actual = /^designated => (.+)$/m.exec(text(display))?.[1]?.trim();
|
|
96
|
+
if (display.status !== 0 || actual !== expectedRequirement) {
|
|
97
|
+
throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} designated requirement ${JSON.stringify(actual)} is not the signed ${JSON.stringify(expectedRequirement)}`, { installedRequirement: actual, designatedRequirement: expectedRequirement });
|
|
98
|
+
}
|
|
99
|
+
for (const binary of binaries) {
|
|
100
|
+
let want; let got;
|
|
101
|
+
try { want = digestOf(deps, P.join(staged, binary)); got = digestOf(deps, P.join(destination, binary)); }
|
|
102
|
+
catch (error) { throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `cannot read ${binary} to verify the install: ${error.message}`); }
|
|
103
|
+
if (want !== got) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${binary} does not match the signed staged copy`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {{appRoot: string, config: object, built: string|{app: string}, deps?: object, env?: object}} options
|
|
109
|
+
* `config` is the app's release config (development.targets.mac.install is the `tauri-devid` descriptor).
|
|
110
|
+
* @returns {Promise<{installed: string, previous: string|null, designatedRequirement: string, identity: string, teamIdentifier: string, signing: object}>}
|
|
111
|
+
*/
|
|
112
|
+
export async function installTauriDevId({ appRoot, config, built, deps = {}, env = {} }) {
|
|
113
|
+
const d = { ...defaultDeps(), ...deps };
|
|
114
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot });
|
|
115
|
+
const destination = descriptor.destination;
|
|
116
|
+
const source = builtPathOf(built, appRoot);
|
|
117
|
+
if (!d.exists(source)) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", `built app not found: ${source}`);
|
|
118
|
+
const name = P.basename(destination);
|
|
119
|
+
const tempRoot = d.makeTemp();
|
|
120
|
+
// The staging copy carries the final bundle name so installApp installs <dest dir>/<name> without renaming.
|
|
121
|
+
const staged = P.join(tempRoot, name);
|
|
122
|
+
try {
|
|
123
|
+
d.copyApp(source, staged);
|
|
124
|
+
// 1. Sign: nested code first, then the app, with the stable Developer ID (development-signing orders it deepest first).
|
|
125
|
+
const signing = await d.sign({ root: appRoot, bundle: staged, descriptor, config, env });
|
|
126
|
+
if (signing.adhoc || !signing.designatedRequirement) throw new TauriDevIdError("TAURI_DEVID_SIGNING_UNSTABLE", `signing produced no stable Developer ID designated requirement for ${staged}; refusing to install`);
|
|
127
|
+
// 2. Same identity as the installed copy, or fail: a changed designated requirement resets every privacy grant.
|
|
128
|
+
if (d.exists(destination)) await d.assertRequirement({ root: appRoot, installed: destination, expected: signing.designatedRequirement, env, run: d.run });
|
|
129
|
+
// 3. Install at the fixed path; the replaced copy stays as <dest>.previous.
|
|
130
|
+
const outcome = await d.installApp({ app: staged, dev: true, dest: P.dirname(destination), appName: name, bundleId: descriptor.bundleId ?? null, relaunch: null }, { env });
|
|
131
|
+
// 4. Verify the installed copy; a failure puts .previous back (or removes a first install) so the path is never left broken.
|
|
132
|
+
try {
|
|
133
|
+
verifyInstalled({ appRoot, destination, staged, expectedRequirement: signing.designatedRequirement, binaries: descriptor.verifyBinaries, env, deps: d });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
let rolledBack = "none";
|
|
136
|
+
if (outcome?.previous && d.exists(previousPathOf(destination))) { restorePrevious(destination, d); rolledBack = "restored-previous"; }
|
|
137
|
+
else { d.remove(destination); rolledBack = "removed-first-install"; }
|
|
138
|
+
error.rolledBack = rolledBack;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
d.log(`right-release dev: installed ${destination}; identity=${signing.identity}; TeamIdentifier=${signing.teamIdentifier}; designated=${signing.designatedRequirement}`);
|
|
142
|
+
return { installed: destination, previous: outcome?.previous ?? null, designatedRequirement: signing.designatedRequirement, identity: signing.identity, teamIdentifier: signing.teamIdentifier, signing };
|
|
143
|
+
} finally { d.remove(tempRoot); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Deliberate reset of a fixed install. mode "restore" (default) makes `.previous` the install again; mode "remove"
|
|
148
|
+
* deletes the install and its `.previous` (and, with `resetPrivacy`, runs `tccutil reset All <bundleId>`).
|
|
149
|
+
* Quit the app first; nothing here kills processes.
|
|
150
|
+
*/
|
|
151
|
+
export async function resetTauriDevId({ appRoot, config, mode = "restore", resetPrivacy = false, deps = {}, env = {} }) {
|
|
152
|
+
const d = { ...defaultDeps(), ...deps };
|
|
153
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot });
|
|
154
|
+
const destination = descriptor.destination;
|
|
155
|
+
if (mode === "restore") {
|
|
156
|
+
const result = restorePrevious(destination, d);
|
|
157
|
+
d.log(`right-release dev: restored ${destination} from ${result.restoredFrom}`);
|
|
158
|
+
return { mode, ...result };
|
|
159
|
+
}
|
|
160
|
+
if (mode !== "remove") throw new TauriDevIdError("TAURI_DEVID_RESET_MODE_INVALID", `mode must be "restore" or "remove", got ${JSON.stringify(mode)}`);
|
|
161
|
+
d.remove(destination);
|
|
162
|
+
d.remove(previousPathOf(destination));
|
|
163
|
+
let privacyReset = false;
|
|
164
|
+
if (resetPrivacy) {
|
|
165
|
+
if (!descriptor.bundleId) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "resetPrivacy requires install.bundleId");
|
|
166
|
+
const reset = d.run("tccutil", ["reset", "All", descriptor.bundleId], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
167
|
+
if (reset.error || reset.status !== 0) throw new TauriDevIdError("TAURI_DEVID_RESET_FAILED", `tccutil reset failed: ${reset.error?.message ?? text(reset)}`);
|
|
168
|
+
privacyReset = true;
|
|
169
|
+
}
|
|
170
|
+
d.log(`right-release dev: removed ${destination}${privacyReset ? " and reset its privacy grants" : ""}`);
|
|
171
|
+
return { mode, destination, removed: true, privacyReset };
|
|
172
|
+
}
|
package/inventory-check.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
|
24
24
|
const CONCURRENCY = 8;
|
|
25
25
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
26
|
const CRATES_API_INTERVAL_MS = 1000;
|
|
27
|
-
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/
|
|
27
|
+
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/rightkit)";
|
|
28
28
|
const EXCLUDED_PREFIX = "build-control-";
|
|
29
29
|
const OWNER_PROBE_CRATE = "rightkit-fs";
|
|
30
30
|
const EXTRA_ACCOUNT_CRATES = Object.freeze([]);
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Declarative macOS packaging recipe (bogusyogi/rightkit#345). A mac target that sets `macPackage` and has no
|
|
2
|
+
// `prePackage` / `package` hook scripts is packaged by this module instead of a per-app mac-dmg.mjs:
|
|
3
|
+
//
|
|
4
|
+
// 1. universal check every Mach-O (main executable + nested) must hold x86_64 and arm64 (macPackage.universal)
|
|
5
|
+
// 2. sign nested code Developer ID, hardened runtime, no entitlements, deepest first
|
|
6
|
+
// 3. sign the app Developer ID, hardened runtime, optional entitlements
|
|
7
|
+
// 4. build the DMG branded, from the signed app (dmg-build)
|
|
8
|
+
// 5. sign the DMG Developer ID with a secure timestamp (no hardened runtime, as for a disk image)
|
|
9
|
+
// 6. notarize + staple the DMG and the app (macos-notarization), when macPackage.notarize
|
|
10
|
+
// 7. updater tarball tar.gz of the stapled app's Contents + the updater signature, when macPackage.updater
|
|
11
|
+
//
|
|
12
|
+
// Every external effect is injectable through `deps`; nothing here reads the host unless a default is used.
|
|
13
|
+
// Fails closed: every problem throws MacPackageRecipeError, and a failure after the DMG exists removes the
|
|
14
|
+
// half-processed DMG / updater files so a later seal cannot pick up an artifact that skipped a step.
|
|
15
|
+
import * as nodeFs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { buildDmg } from "./dmg-build.mjs";
|
|
18
|
+
import { spawnSync } from "./background-process.mjs";
|
|
19
|
+
import { signMacosEntries } from "./mac-sign-core.mjs";
|
|
20
|
+
import { mainExecutableName } from "./mac-bundle-signing.mjs";
|
|
21
|
+
import { notarizeMacosArtifact } from "./macos-notarization.mjs";
|
|
22
|
+
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
23
|
+
import { runMacCodesignCommands, signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
24
|
+
import { discoverNestedCode } from "./swift-mac-app.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Typed error. Codes: MAC_PACKAGE_CONFIG_INVALID, MAC_PACKAGE_HOST_UNSUPPORTED, MAC_PACKAGE_INPUT_MISSING,
|
|
28
|
+
* MAC_PACKAGE_UNIVERSAL_FAILED, MAC_PACKAGE_SIGN_FAILED, MAC_PACKAGE_DMG_FAILED, MAC_PACKAGE_NOTARIZE_FAILED,
|
|
29
|
+
* MAC_PACKAGE_UPDATER_FAILED.
|
|
30
|
+
*/
|
|
31
|
+
export class MacPackageRecipeError extends Error {
|
|
32
|
+
constructor(code, message, options) { super(message, options); this.name = "MacPackageRecipeError"; this.code = code; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const MAC_PACKAGE_STEPS = Object.freeze(["universal", "sign-nested", "sign-app", "dmg", "sign-dmg", "notarize", "updater"]);
|
|
36
|
+
const UNIVERSAL_ARCHS = ["x86_64", "arm64"];
|
|
37
|
+
const MACPACKAGE_KEYS = ["universal", "dmg", "notarize", "updater", "nested", "entitlements"];
|
|
38
|
+
const DMG_KEYS = ["name", "background", "windowSize", "iconPositions"];
|
|
39
|
+
const ICON_KEYS = ["app", "applications"];
|
|
40
|
+
|
|
41
|
+
const invalid = (message) => new MacPackageRecipeError("MAC_PACKAGE_CONFIG_INVALID", `macPackage: ${message}`);
|
|
42
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
const rejectUnknown = (object, allowed, label) => {
|
|
44
|
+
const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
|
|
45
|
+
if (unknown.length) throw invalid(`${label} has unknown key(s): ${unknown.join(", ")} (allowed: ${allowed.join(", ")})`);
|
|
46
|
+
};
|
|
47
|
+
const integerPair = (value, label, min) => {
|
|
48
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some((n) => !Number.isInteger(n) || n < min || n > 32767)) {
|
|
49
|
+
throw invalid(`${label} must be [x, y] integers ${min}..32767`);
|
|
50
|
+
}
|
|
51
|
+
return [...value];
|
|
52
|
+
};
|
|
53
|
+
const requireBoolean = (value, label) => {
|
|
54
|
+
if (typeof value !== "boolean") throw invalid(`${label} is required and must be true or false`);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A bundle-relative glob: forward slashes only, no `..`, no absolute path, only `*`, `**` and `?` wildcards. */
|
|
59
|
+
function validateGlob(pattern, label) {
|
|
60
|
+
if (typeof pattern !== "string" || !pattern.trim()) throw invalid(`${label} must be a non-empty string`);
|
|
61
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern) || pattern.includes("\\")) throw invalid(`${label} must be a bundle-relative path with forward slashes: ${pattern}`);
|
|
62
|
+
if (/[[\]{}()!]/.test(pattern)) throw invalid(`${label} supports only *, ** and ? wildcards: ${pattern}`);
|
|
63
|
+
const segments = pattern.split("/");
|
|
64
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw invalid(`${label} must not contain empty, '.' or '..' segments: ${pattern}`);
|
|
65
|
+
if (segments.some((segment) => segment.includes("**") && segment !== "**")) throw invalid(`${label}: '**' must be a whole path segment: ${pattern}`);
|
|
66
|
+
return pattern;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Strictly validate and normalize a `macPackage` value. Unknown keys, wrong types and missing fields throw. */
|
|
70
|
+
export function validateMacPackageConfig(macPackage) {
|
|
71
|
+
if (!isPlainObject(macPackage)) throw invalid("must be an object");
|
|
72
|
+
rejectUnknown(macPackage, MACPACKAGE_KEYS, "macPackage");
|
|
73
|
+
const universal = macPackage.universal === undefined ? false : requireBoolean(macPackage.universal, "universal");
|
|
74
|
+
const notarize = requireBoolean(macPackage.notarize, "notarize");
|
|
75
|
+
const updater = requireBoolean(macPackage.updater, "updater");
|
|
76
|
+
const dmg = macPackage.dmg;
|
|
77
|
+
if (!isPlainObject(dmg)) throw invalid("dmg is required and must be an object");
|
|
78
|
+
rejectUnknown(dmg, DMG_KEYS, "dmg");
|
|
79
|
+
if (typeof dmg.name !== "string" || !dmg.name.trim() || /[\0\r\n/:]/.test(dmg.name) || Buffer.byteLength(dmg.name) > 255) {
|
|
80
|
+
throw invalid("dmg.name must be a non-empty volume name without NUL, CR, LF, '/' or ':'");
|
|
81
|
+
}
|
|
82
|
+
const normalized = { name: dmg.name };
|
|
83
|
+
if (dmg.background !== undefined) {
|
|
84
|
+
if (typeof dmg.background !== "string" || !/\.(png|tiff?)$/i.test(dmg.background)) throw invalid("dmg.background must be a path to a PNG or TIFF file");
|
|
85
|
+
normalized.background = dmg.background;
|
|
86
|
+
}
|
|
87
|
+
if (dmg.windowSize !== undefined) normalized.windowSize = integerPair(dmg.windowSize, "dmg.windowSize", 1);
|
|
88
|
+
if (dmg.iconPositions !== undefined) {
|
|
89
|
+
if (!isPlainObject(dmg.iconPositions)) throw invalid("dmg.iconPositions must be an object with app and/or applications");
|
|
90
|
+
rejectUnknown(dmg.iconPositions, ICON_KEYS, "dmg.iconPositions");
|
|
91
|
+
normalized.iconPositions = Object.fromEntries(Object.entries(dmg.iconPositions).map(([key, value]) => [key, integerPair(value, `dmg.iconPositions.${key}`, 0)]));
|
|
92
|
+
}
|
|
93
|
+
let nested = [];
|
|
94
|
+
if (macPackage.nested !== undefined) {
|
|
95
|
+
if (!Array.isArray(macPackage.nested) || !macPackage.nested.length) throw invalid("nested must be a non-empty array of bundle-relative globs");
|
|
96
|
+
nested = macPackage.nested.map((pattern, index) => validateGlob(pattern, `nested[${index}]`));
|
|
97
|
+
if (new Set(nested).size !== nested.length) throw invalid("nested globs must be unique");
|
|
98
|
+
}
|
|
99
|
+
if (macPackage.entitlements !== undefined && (typeof macPackage.entitlements !== "string" || !macPackage.entitlements.trim())) throw invalid("entitlements must be a path string");
|
|
100
|
+
return { universal, dmg: normalized, notarize, updater, nested, ...(macPackage.entitlements ? { entitlements: macPackage.entitlements } : {}) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate the mac target around `macPackage`: the declarative recipe and the hook scripts are mutually exclusive, and the
|
|
105
|
+
* app / DMG paths come from the existing `notarize.app` and `notarize.file` fields so there is one source of truth.
|
|
106
|
+
*/
|
|
107
|
+
export function validateMacPackageTarget(target) {
|
|
108
|
+
if (!isPlainObject(target)) throw invalid("the mac target must be an object");
|
|
109
|
+
if (target.prePackage !== undefined || target.package !== undefined) throw invalid("a target with macPackage must not also declare prePackage or package; delete the hook scripts");
|
|
110
|
+
if (target.dmg !== undefined) throw invalid("a target with macPackage must not also declare targets.mac.dmg; use macPackage.dmg");
|
|
111
|
+
const recipe = validateMacPackageConfig(target.macPackage);
|
|
112
|
+
const app = target.notarize?.app;
|
|
113
|
+
const file = target.notarize?.file;
|
|
114
|
+
if (typeof app !== "string" || !/\.app\/?$/.test(app)) throw invalid("notarize.app must name the .app bundle the recipe signs and packs");
|
|
115
|
+
if (typeof file !== "string" || !/\.dmg$/i.test(file)) throw invalid("notarize.file must name the .dmg the recipe produces");
|
|
116
|
+
return { recipe, app, dmgFile: file };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Bundle-relative glob to RegExp; `**` spans path segments, `*` and `?` stay inside one. */
|
|
120
|
+
export function globToRegExp(pattern) {
|
|
121
|
+
const segments = pattern.split("/");
|
|
122
|
+
const body = segments.map((segment, index) => {
|
|
123
|
+
const last = index === segments.length - 1;
|
|
124
|
+
if (segment === "**") return last ? ".+" : "(?:[^/]+/)*";
|
|
125
|
+
return `${segment.replace(/[.+^$|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}${last ? "" : "/"}`;
|
|
126
|
+
}).join("");
|
|
127
|
+
return new RegExp(`^${body}$`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function listTree(root, fs) {
|
|
131
|
+
const found = [];
|
|
132
|
+
const walk = (dir, prefix) => {
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
135
|
+
found.push(relative);
|
|
136
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) walk(path.join(dir, entry.name), relative);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(root, "");
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const depth = (file) => file.split(/[\\/]/).length;
|
|
144
|
+
const tail = (text, lines = 20) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
|
|
145
|
+
|
|
146
|
+
const defaultInspect = (cmd, args) => {
|
|
147
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
148
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
149
|
+
if (result.error || result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} failed: ${output.trim() || result.error?.message}`);
|
|
150
|
+
return output;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run the declarative mac package recipe. Returns { steps, app, dmg, updater, receipt, signing } where `steps` lists the
|
|
155
|
+
* steps that actually ran, in order. Throws MacPackageRecipeError on the first failure.
|
|
156
|
+
*
|
|
157
|
+
* deps (all optional): platform, env, fs, resolveArtifact(root, p), resolveIdentity({env}), signOne(codesignArgs),
|
|
158
|
+
* inspect(cmd, args) -> output, evidence(file), hashFile(file), discover(app, mainName), mainName(app),
|
|
159
|
+
* listFiles(dir) -> bundle-relative posix paths, buildDmg(options), notarize(options), run(cmd, args, opts),
|
|
160
|
+
* signUpdater(files, options), receiptPath, log(event).
|
|
161
|
+
*/
|
|
162
|
+
export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} }) {
|
|
163
|
+
const { recipe, app: declaredApp, dmgFile } = validateMacPackageTarget(target);
|
|
164
|
+
const {
|
|
165
|
+
platform = process.platform, env = process.env, fs = nodeFs,
|
|
166
|
+
resolveArtifact = (root, p) => path.resolve(root, p),
|
|
167
|
+
resolveIdentity = resolveMacosDeveloperIdIdentity,
|
|
168
|
+
signOne = (codesignArgs) => runMacCodesignCommands([codesignArgs], { env })[0],
|
|
169
|
+
inspect = defaultInspect, evidence, hashFile,
|
|
170
|
+
discover = discoverNestedCode, mainName = mainExecutableName,
|
|
171
|
+
listFiles = (dir) => listTree(dir, fs),
|
|
172
|
+
buildDmg: build = buildDmg, notarize = notarizeMacosArtifact,
|
|
173
|
+
run = spawnSync, signUpdater = (files, options) => signMacUpdaterFiles(files, options),
|
|
174
|
+
log = () => {},
|
|
175
|
+
} = deps;
|
|
176
|
+
const appName = config.app ?? path.basename(appRoot ?? ".");
|
|
177
|
+
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) throw invalid("appRoot must be an absolute path");
|
|
178
|
+
if (platform !== "darwin") throw new MacPackageRecipeError("MAC_PACKAGE_HOST_UNSUPPORTED", `macPackage signs and notarizes on macOS only (host: ${platform})`);
|
|
179
|
+
|
|
180
|
+
const app = path.resolve(resolveArtifact(appRoot, declaredApp));
|
|
181
|
+
const dmg = path.resolve(resolveArtifact(appRoot, dmgFile));
|
|
182
|
+
const updater = `${app}.tar.gz`;
|
|
183
|
+
const receiptPath = deps.receiptPath ?? path.join(path.resolve(env.RIGHT_RELEASE_STATE_ROOT ?? path.join(appRoot, ".right-release"), "receipts"), `${appName}-mac-notarization.json`);
|
|
184
|
+
const missing = (what, file) => new MacPackageRecipeError("MAC_PACKAGE_INPUT_MISSING", `${what} not found: ${file}`);
|
|
185
|
+
const wrap = (code, step, error) => (error instanceof MacPackageRecipeError ? error
|
|
186
|
+
: new MacPackageRecipeError(code, `macPackage ${step} failed: ${error?.message ?? error}`, { cause: error }));
|
|
187
|
+
|
|
188
|
+
// Resolve every input before any effect so a typo cannot leave a half-signed app.
|
|
189
|
+
if (!fs.existsSync(app)) throw missing("app bundle", app);
|
|
190
|
+
const infoPlist = path.join(app, "Contents", "Info.plist");
|
|
191
|
+
if (!fs.existsSync(infoPlist)) throw missing("Contents/Info.plist in the app bundle", infoPlist);
|
|
192
|
+
const entitlements = recipe.entitlements ? path.resolve(appRoot, recipe.entitlements) : undefined;
|
|
193
|
+
if (entitlements && !fs.existsSync(entitlements)) throw missing("entitlements file", entitlements);
|
|
194
|
+
const background = recipe.dmg.background ? path.resolve(appRoot, recipe.dmg.background) : undefined;
|
|
195
|
+
if (background && !fs.existsSync(background)) throw missing("dmg.background file", background);
|
|
196
|
+
|
|
197
|
+
const executable = mainName(app);
|
|
198
|
+
const mainBinary = path.join(app, "Contents", "MacOS", executable);
|
|
199
|
+
const nestedSet = new Set(discover(app, executable).map((file) => path.resolve(file)));
|
|
200
|
+
if (recipe.nested.length) {
|
|
201
|
+
const tree = listFiles(app);
|
|
202
|
+
for (const pattern of recipe.nested) {
|
|
203
|
+
const matcher = globToRegExp(pattern);
|
|
204
|
+
const hits = tree.filter((relative) => matcher.test(relative));
|
|
205
|
+
if (!hits.length) throw new MacPackageRecipeError("MAC_PACKAGE_INPUT_MISSING", `macPackage.nested glob matched nothing inside ${path.basename(app)}: ${pattern}`);
|
|
206
|
+
for (const hit of hits) nestedSet.add(path.resolve(app, hit));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
nestedSet.delete(mainBinary);
|
|
210
|
+
nestedSet.delete(app);
|
|
211
|
+
// Inside-out: a path nested deeper is always signed before the code that contains it.
|
|
212
|
+
const nested = [...nestedSet].sort((a, b) => depth(b) - depth(a));
|
|
213
|
+
|
|
214
|
+
const steps = [];
|
|
215
|
+
const record = (step) => { steps.push(step); log({ event: "mac-package-step", step, app: appName }); };
|
|
216
|
+
const signOptions = { signOne, inspect, ...(evidence ? { evidence } : {}), ...(hashFile ? { hashFile } : {}) };
|
|
217
|
+
let signing = [];
|
|
218
|
+
let receipt;
|
|
219
|
+
let updaterFiles = [];
|
|
220
|
+
try {
|
|
221
|
+
// 1) universal: verify every Mach-O holds both architectures. A thin slice makes the whole app Intel- or
|
|
222
|
+
// Apple-Silicon-only, which compiles, signs and notarizes fine and then fails on half the users' Macs.
|
|
223
|
+
if (recipe.universal) {
|
|
224
|
+
const archsOf = (file) => String(inspect("lipo", ["-archs", file])).trim().split(/\s+/);
|
|
225
|
+
const assertFat = (file) => {
|
|
226
|
+
const archs = archsOf(file);
|
|
227
|
+
const lacking = UNIVERSAL_ARCHS.filter((arch) => !archs.includes(arch));
|
|
228
|
+
if (lacking.length) throw new Error(`${path.relative(app, file)} is not universal (has: ${archs.join(" ")}; missing: ${lacking.join(" ")})`);
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
assertFat(mainBinary); // an unreadable main executable is a failure, not a skip
|
|
232
|
+
for (const file of nested) {
|
|
233
|
+
let isFile = false;
|
|
234
|
+
try { isFile = fs.statSync(file).isFile(); } catch { /* absent: signing reports it */ }
|
|
235
|
+
if (!isFile) continue; // code bundles are covered by the Mach-O files inside them
|
|
236
|
+
try { archsOf(file); } catch { continue; } // lipo rejects non-Mach-O files (scripts, resources matched by a glob)
|
|
237
|
+
assertFat(file);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) { throw wrap("MAC_PACKAGE_UNIVERSAL_FAILED", "universal check", error); }
|
|
240
|
+
record("universal");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2-3) sign nested code, then the app. One identity for the whole run.
|
|
244
|
+
let identity;
|
|
245
|
+
try { identity = resolveIdentity({ env }); } catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "identity lookup", error); }
|
|
246
|
+
if (typeof identity !== "string" || !identity.trim()) throw new MacPackageRecipeError("MAC_PACKAGE_SIGN_FAILED", "macPackage signing identity is empty");
|
|
247
|
+
if (nested.length) {
|
|
248
|
+
try { signing.push(...signMacosEntries(nested.map((file) => ({ file, hardenedRuntime: true, options: [] })), { identity, ...signOptions })); }
|
|
249
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "nested code signing", error); }
|
|
250
|
+
record("sign-nested");
|
|
251
|
+
}
|
|
252
|
+
try { signing.push(...signMacosEntries([{ file: app, hardenedRuntime: true, options: [], ...(entitlements ? { entitlements } : {}) }], { identity, ...signOptions })); }
|
|
253
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "app signing", error); }
|
|
254
|
+
record("sign-app");
|
|
255
|
+
|
|
256
|
+
// 4) branded DMG from the signed app.
|
|
257
|
+
fs.rmSync(dmg, { force: true });
|
|
258
|
+
try {
|
|
259
|
+
build({
|
|
260
|
+
app, out: dmg, volname: recipe.dmg.name,
|
|
261
|
+
...(background ? { background } : {}),
|
|
262
|
+
...(recipe.dmg.windowSize ? { window: recipe.dmg.windowSize } : {}),
|
|
263
|
+
...(recipe.dmg.iconPositions?.app ? { appPosition: recipe.dmg.iconPositions.app } : {}),
|
|
264
|
+
...(recipe.dmg.iconPositions?.applications ? { applicationsPosition: recipe.dmg.iconPositions.applications } : {}),
|
|
265
|
+
});
|
|
266
|
+
} catch (error) { throw wrap("MAC_PACKAGE_DMG_FAILED", "DMG build", error); }
|
|
267
|
+
if (!fs.existsSync(dmg)) throw new MacPackageRecipeError("MAC_PACKAGE_DMG_FAILED", `DMG build reported success but ${dmg} does not exist`);
|
|
268
|
+
record("dmg");
|
|
269
|
+
|
|
270
|
+
// 5) sign the DMG: secure timestamp, no hardened runtime (a disk image has none).
|
|
271
|
+
try { signing.push(...signMacosEntries([{ file: dmg, hardenedRuntime: false, options: [] }], { identity, ...signOptions })); }
|
|
272
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "DMG signing", error); }
|
|
273
|
+
record("sign-dmg");
|
|
274
|
+
|
|
275
|
+
// 6) notarize + staple. The DMG submission covers the app inside it; the app is stapled too so the updater tarball carries the ticket.
|
|
276
|
+
if (recipe.notarize) {
|
|
277
|
+
try { receipt = notarize({ archive: dmg, app, receiptPath, env, run }); }
|
|
278
|
+
catch (error) { throw wrap("MAC_PACKAGE_NOTARIZE_FAILED", "notarization", error); }
|
|
279
|
+
if (!receipt || receipt.notarization?.status !== "Accepted") throw new MacPackageRecipeError("MAC_PACKAGE_NOTARIZE_FAILED", `macPackage notarization did not report Accepted for ${dmg}`);
|
|
280
|
+
record("notarize");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 7) updater tarball from the final (stapled) app, then its signature.
|
|
284
|
+
if (recipe.updater) {
|
|
285
|
+
updaterFiles = [updater, `${updater}.sig`];
|
|
286
|
+
for (const file of updaterFiles) fs.rmSync(file, { force: true });
|
|
287
|
+
// bsdtar otherwise serializes extended attributes as AppleDouble `._*` entries, which Tauri's extractor writes
|
|
288
|
+
// into the bundle and invalidates the seal. The explicit App.app/ directory entry is omitted for the same extractor.
|
|
289
|
+
let packed;
|
|
290
|
+
try { packed = run("tar", ["-czf", updater, "-C", path.dirname(app), path.join(path.basename(app), "Contents")], { cwd: appRoot, env: { ...env, COPYFILE_DISABLE: "1" }, encoding: "utf8", stdio: "pipe" }); }
|
|
291
|
+
catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater tar", error); }
|
|
292
|
+
if (!packed || packed.error || packed.status !== 0) throw new MacPackageRecipeError("MAC_PACKAGE_UPDATER_FAILED", `macPackage updater tar failed: ${packed?.error?.message ?? (tail(packed?.stderr) || packed?.status)}`);
|
|
293
|
+
try { signUpdater([updater], { cwd: appRoot, env }); }
|
|
294
|
+
catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater signing", error); }
|
|
295
|
+
for (const file of updaterFiles) if (!fs.existsSync(file)) throw missing("updater artifact", file);
|
|
296
|
+
record("updater");
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// Fail closed: never leave a DMG or updater that skipped a step where the seal can find it.
|
|
300
|
+
for (const file of [dmg, ...updaterFiles]) { try { fs.rmSync(file, { force: true }); } catch { /* best effort */ } }
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return { steps, app, dmg, updater: recipe.updater ? updater : null, receipt: receipt ?? null, receiptPath: receipt ? receiptPath : null, signing };
|
|
304
|
+
}
|
package/media-sidecar.mjs
CHANGED
|
@@ -187,7 +187,7 @@ export function sourceOfferText({ rows, contact, crateVersion }) {
|
|
|
187
187
|
"The licence text and build notes are in this folder. You may replace these programs with your own build of the same source.",
|
|
188
188
|
"",
|
|
189
189
|
`Source code and build recipe: ${rows[0].attribution.sourceUrl}`,
|
|
190
|
-
`Build recipes, pinned sources and notes: https://github.com/
|
|
190
|
+
`Build recipes, pinned sources and notes: https://github.com/bogusyogi/rightkit/tree/main/crates/rightkit-ffmpeg${crateVersion ? ` (crate rightkit-ffmpeg ${crateVersion}, published on crates.io)` : ""}`,
|
|
191
191
|
`To request the corresponding source on physical media, or if a link is unavailable, write to: ${contact}`,
|
|
192
192
|
"",
|
|
193
193
|
"Programs shipped",
|