@rightkit/release 0.2.141 → 0.2.142
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 +4 -0
- package/build-release.mjs +1 -1
- package/mac-bundle-signing.mjs +33 -0
- package/macos-file-evidence.mjs +27 -0
- package/package.json +1 -1
- package/release.mjs +25 -3
- package/rightkit-versions.json +8 -6
- package/sign-macos.mjs +2 -24
- package/swift-mac-dmg.mjs +52 -0
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ Build a SwiftPM app with `right-release build --platform mac|win` (add `--config
|
|
|
72
72
|
|
|
73
73
|
Set `targets.mac.notarize.appBeforePackage: true` (with `notarize.app` naming the `.app`) to notarize and staple the app itself right after signing and before the package step copies it into the DMG, so an app copied out of the DMG passes `stapler validate` offline. The DMG named by `notarize.file` is still notarized and stapled afterward; the app ticket receipt is `macos-app-notarization.json`. Without the flag, behavior is unchanged.
|
|
74
74
|
|
|
75
|
+
For `projectType: "swiftpm"` on mac with `appBeforePackage: true` and no `targets.mac.package`, right-release builds the DMG itself (same `dmg-build.mjs` as Tauri) at the package step, after the `.app` is signed, notarized and stapled: it runs `xcrun stapler validate` on the app (refusing to pack an unstapled one), builds the DMG at `notarize.file` (must end `.dmg`), Developer ID signs it (receipt `macos-dmg-signing.json`), then notarizes and staples the DMG as before. Options go in `targets.mac.dmg`: `{ volname, background, iconSize, appPosition, applicationsPosition, window, volumeIcon, out }` (`volname` defaults to the app name). A prePackage hook must no longer build the DMG: preflight fails when its command or arguments mention `dmg` or `hdiutil` (heuristic), because that DMG would carry the unstapled app. Keep a custom DMG flow with `targets.mac.dmg: false` or a `targets.mac.package` step (both run after the staple and skip the automatic build).
|
|
76
|
+
|
|
77
|
+
`sign.prePackageFiles` entries may be `.app` bundles (directories). Nested code (Mach-O files, frameworks, XPC services, plug-ins) is signed first, inside-out and without entitlements, then the bundle is signed; the receipt records a tree digest of the bundle. Any other directory is rejected with a clear error.
|
|
78
|
+
|
|
75
79
|
## Known issues
|
|
76
80
|
|
|
77
81
|
- `smol-toml` is pinned exactly. A consumer that adds another `smol-toml` gets a second copy (npm audit 2026-10-08, release finding).
|
package/build-release.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
|
48
48
|
// and test-only edits and would force a cold Rust rebuild of every app each time.
|
|
49
49
|
// Cached target directories must not outlive changes to these signing helpers.
|
|
50
50
|
|
|
51
|
-
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs", "msix-build.mjs", "msix-plan.mjs"];
|
|
51
|
+
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "swift-mac-dmg.mjs", "mac-bundle-signing.mjs", "macos-file-evidence.mjs", "swift-mac-app.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs", "msix-build.mjs", "msix-plan.mjs"];
|
|
52
52
|
const PIPELINE_FINGERPRINT = createHash("sha256")
|
|
53
53
|
.update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
|
|
54
54
|
.digest("hex")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Directory entries (.app bundles) in `sign.prePackageFiles`: sign nested code inside-out first
|
|
2
|
+
// (no entitlements), then the bundle itself, reusing the swift-mac-app inside-out discovery.
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { discoverNestedCode } from "./swift-mac-app.mjs";
|
|
6
|
+
|
|
7
|
+
const fail = (message) => Object.assign(new Error(message), { code: "MAC_SIGN_ENTRY_INVALID" });
|
|
8
|
+
|
|
9
|
+
function mainExecutableName(app) {
|
|
10
|
+
try {
|
|
11
|
+
const plist = readFileSync(path.join(app, "Contents", "Info.plist"), "utf8");
|
|
12
|
+
const match = /<key>CFBundleExecutable<\/key>\s*<string>([^<]+)<\/string>/.exec(plist);
|
|
13
|
+
if (match) return match[1];
|
|
14
|
+
} catch { /* binary plist or unreadable: fall back to the bundle name */ }
|
|
15
|
+
return path.basename(app, ".app");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split signing entries into nested code (deepest first, signed without entitlements) and the entries
|
|
20
|
+
* themselves. A directory must be a `.app` bundle; anything else is rejected with a clear error.
|
|
21
|
+
* Injectable: isDirectory, discover, exists.
|
|
22
|
+
*/
|
|
23
|
+
export function expandMacSigningEntries(files, { isDirectory = (f) => statSync(f).isDirectory(), exists = existsSync, discover = discoverNestedCode, mainName = mainExecutableName } = {}) {
|
|
24
|
+
const nested = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
if (!isDirectory(file)) continue;
|
|
27
|
+
if (!file.endsWith(".app") || !exists(path.join(file, "Contents", "Info.plist"))) {
|
|
28
|
+
throw fail(`sign.prePackageFiles entry is a directory but not a .app bundle with Contents/Info.plist: ${file}`);
|
|
29
|
+
}
|
|
30
|
+
nested.push(...discover(file, mainName(file)));
|
|
31
|
+
}
|
|
32
|
+
return { nested: [...new Set(nested)], files };
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Content evidence for a macOS signing entry: a plain file hashes its bytes; a directory (.app bundle)
|
|
2
|
+
// hashes a deterministic tree digest. Shared by sign-macos.mjs (writes the receipt) and release.mjs (verifies it).
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { lstatSync, readdirSync, readFileSync, readlinkSync, statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export function fileEvidence(file) {
|
|
8
|
+
if (statSync(file).isDirectory()) {
|
|
9
|
+
const digest = createHash("sha256");
|
|
10
|
+
let sizeBytes = 0;
|
|
11
|
+
const visit = (directory) => {
|
|
12
|
+
for (const name of readdirSync(directory).sort()) {
|
|
13
|
+
const entry = path.join(directory, name);
|
|
14
|
+
const info = lstatSync(entry);
|
|
15
|
+
const type = info.isSymbolicLink() ? "link" : info.isDirectory() ? "directory" : "file";
|
|
16
|
+
const bytes = type === "link" ? Buffer.from(readlinkSync(entry)) : type === "file" ? readFileSync(entry) : Buffer.alloc(0);
|
|
17
|
+
digest.update(`${path.relative(file, entry).split(path.sep).join("/")}\0${type}\0${bytes.length}\0`).update(bytes);
|
|
18
|
+
sizeBytes += bytes.length;
|
|
19
|
+
if (type === "directory") visit(entry);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
visit(file);
|
|
23
|
+
return { sha256: digest.digest("hex"), sizeBytes, kind: "bundle-tree-v1" };
|
|
24
|
+
}
|
|
25
|
+
const bytes = readFileSync(file);
|
|
26
|
+
return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
|
|
27
|
+
}
|
package/package.json
CHANGED
package/release.mjs
CHANGED
|
@@ -29,6 +29,9 @@ import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
|
29
29
|
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
30
30
|
import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
31
31
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
32
|
+
import { fileEvidence } from "./macos-file-evidence.mjs";
|
|
33
|
+
import { expandMacSigningEntries } from "./mac-bundle-signing.mjs";
|
|
34
|
+
import { buildStapledSwiftMacDmg, swiftMacDmgConfigError, swiftMacDmgPlan } from "./swift-mac-dmg.mjs";
|
|
32
35
|
import { runConfiguredChecks } from "./release-checks.mjs";
|
|
33
36
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
34
37
|
import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
@@ -157,6 +160,8 @@ if (opts.platform === "mac") {
|
|
|
157
160
|
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
|
|
158
161
|
if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
|
|
159
162
|
if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
|
|
163
|
+
const swiftDmgError = swiftMacDmgConfigError({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? "app" });
|
|
164
|
+
if (swiftDmgError) fail(swiftDmgError);
|
|
160
165
|
}
|
|
161
166
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
162
167
|
/**
|
|
@@ -329,6 +334,14 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
329
334
|
}
|
|
330
335
|
}
|
|
331
336
|
|
|
337
|
+
if (opts.platform === "mac" && runMacPhase("package")) {
|
|
338
|
+
// SwiftPM + appBeforePackage: the .app is stapled by now, so right-release packs it into the DMG itself.
|
|
339
|
+
const swiftDmg = swiftMacDmgPlan({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? path.basename(root), resolve: (p) => resolveReleaseArtifactPath(root, p) });
|
|
340
|
+
if (swiftDmg) {
|
|
341
|
+
if (opts.dryRun) console.log(`dry-run: build DMG ${swiftDmg.out} from stapled app ${swiftDmg.app}, then sign it`);
|
|
342
|
+
else await buildStapledSwiftMacDmg(swiftDmg, { remove: (file) => rmSync(file, { force: true }), signDmg: (dmg) => signMacos([dmg], root, "dmg-signing") });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
332
345
|
if (command && (opts.platform !== "mac" || runMacPhase("package"))) await runCommand(command, root);
|
|
333
346
|
|
|
334
347
|
for (const rel of target.artifacts ?? []) {
|
|
@@ -858,8 +871,17 @@ function windowsReceiptPath(root, phase) {
|
|
|
858
871
|
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
859
872
|
}
|
|
860
873
|
|
|
861
|
-
async function signMacos(files, root) {
|
|
862
|
-
const receipt = macosReceiptPath(root, "signing");
|
|
874
|
+
async function signMacos(files, root, phase = "signing") {
|
|
875
|
+
const receipt = phase === "signing" ? macosReceiptPath(root, "signing") : path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
876
|
+
// Directory entries (.app bundles): nested code first, no entitlements, then the bundle itself.
|
|
877
|
+
let nested = [];
|
|
878
|
+
try { ({ nested } = opts.dryRun ? { nested: [] } : expandMacSigningEntries(files)); } catch (error) { fail(error.message); }
|
|
879
|
+
if (nested.length) {
|
|
880
|
+
const nestedReceipt = path.resolve(path.dirname(receipt), `macos-${phase}-nested.json`);
|
|
881
|
+
if (existsSync(nestedReceipt)) unlinkSync(nestedReceipt);
|
|
882
|
+
console.log(`right-release: signing ${nested.length} nested code item(s) inside-out`);
|
|
883
|
+
await run("node", [SIGN_MACOS, "--receipt", nestedReceipt, ...nested], root);
|
|
884
|
+
}
|
|
863
885
|
const args = [SIGN_MACOS];
|
|
864
886
|
if (opts.dryRun) args.push("--dry-run");
|
|
865
887
|
args.push("--receipt", receipt, ...files);
|
|
@@ -911,7 +933,7 @@ function verifyMacosSigningReceipt(receipt, files) {
|
|
|
911
933
|
try { expectedIdentity = resolveMacosDeveloperIdIdentity(); } catch (error) { fail(error.message); }
|
|
912
934
|
const expected = files.map((file) => path.resolve(file)).sort();
|
|
913
935
|
const actual = Array.isArray(evidence.files) ? evidence.files.map((item) => path.resolve(item.file)).sort() : [];
|
|
914
|
-
const hashes = new Map(files.map((file) => [path.resolve(file),
|
|
936
|
+
const hashes = new Map(files.map((file) => [path.resolve(file), fileEvidence(file).sha256]));
|
|
915
937
|
if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || !expectedIdentity || evidence.identity !== expectedIdentity
|
|
916
938
|
|| JSON.stringify(expected) !== JSON.stringify(actual)
|
|
917
939
|
|| evidence.files.some((item) => item.codesign !== "strict-valid" || item.identity !== expectedIdentity || item.timestampPresent !== true || item.after?.sha256 !== hashes.get(path.resolve(item.file)))) fail(`invalid macOS Developer ID signing receipt evidence: ${receipt}`);
|
package/rightkit-versions.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@rightkit/app-shell": "0.3.0",
|
|
17
17
|
"@rightkit/ax": "0.2.4",
|
|
18
18
|
"@rightkit/doc-render": "0.1.4",
|
|
19
|
-
"@rightkit/git": "0.2.
|
|
19
|
+
"@rightkit/git": "0.2.33",
|
|
20
20
|
"@rightkit/hooks": "0.1.2",
|
|
21
21
|
"@rightkit/i18n": "0.1.0",
|
|
22
22
|
"@rightkit/legal": "0.3.1",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@rightkit/platform-ui": "0.2.0",
|
|
30
30
|
"@rightkit/qa": "0.4.0",
|
|
31
31
|
"@rightkit/qa-chrome": "0.1.3",
|
|
32
|
-
"@rightkit/release": "0.2.
|
|
32
|
+
"@rightkit/release": "0.2.142",
|
|
33
33
|
"@rightkit/settings-ui": "0.3.0",
|
|
34
34
|
"@rightkit/shell": "0.1.0",
|
|
35
35
|
"@rightkit/tauri": "0.2.0",
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"0.2.28",
|
|
74
74
|
"0.2.29",
|
|
75
75
|
"0.2.30",
|
|
76
|
-
"0.2.31"
|
|
76
|
+
"0.2.31",
|
|
77
|
+
"0.2.32"
|
|
77
78
|
],
|
|
78
79
|
"@rightkit/legal-ui": [
|
|
79
80
|
"0.1.0",
|
|
@@ -172,7 +173,8 @@
|
|
|
172
173
|
"0.2.137",
|
|
173
174
|
"0.2.138",
|
|
174
175
|
"0.2.139",
|
|
175
|
-
"0.2.140"
|
|
176
|
+
"0.2.140",
|
|
177
|
+
"0.2.141"
|
|
176
178
|
],
|
|
177
179
|
"@rightkit/qa": [
|
|
178
180
|
"0.1.0",
|
|
@@ -371,7 +373,7 @@
|
|
|
371
373
|
"@rightkit/app-shell": "0.3.0",
|
|
372
374
|
"@rightkit/ax": "0.2.4",
|
|
373
375
|
"@rightkit/doc-render": "0.1.4",
|
|
374
|
-
"@rightkit/git": "0.2.
|
|
376
|
+
"@rightkit/git": "0.2.33",
|
|
375
377
|
"@rightkit/hooks": "0.1.2",
|
|
376
378
|
"@rightkit/i18n": "0.1.0",
|
|
377
379
|
"@rightkit/legal": "0.3.1",
|
|
@@ -384,7 +386,7 @@
|
|
|
384
386
|
"@rightkit/platform-ui": "0.2.0",
|
|
385
387
|
"@rightkit/qa": "0.4.0",
|
|
386
388
|
"@rightkit/qa-chrome": "0.1.3",
|
|
387
|
-
"@rightkit/release": "0.2.
|
|
389
|
+
"@rightkit/release": "0.2.142",
|
|
388
390
|
"@rightkit/settings-ui": "0.3.0",
|
|
389
391
|
"@rightkit/shell": "0.1.0",
|
|
390
392
|
"@rightkit/tauri": "0.2.0",
|
package/sign-macos.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
import { fileEvidence } from "./macos-file-evidence.mjs";
|
|
6
6
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
7
7
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
8
8
|
import { runMacCodesignCommands } from "./macos-signing-session.mjs";
|
|
@@ -56,28 +56,6 @@ const absoluteReceipt = path.resolve(receiptPath);
|
|
|
56
56
|
mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
|
|
57
57
|
writeFileSync(absoluteReceipt, `${JSON.stringify({ schema: 1, signingContract: MACOS_PORTABLE_SIGNING_CONTRACT, identity, files: evidence }, null, 2)}\n`);
|
|
58
58
|
|
|
59
|
-
function fileEvidence(file) {
|
|
60
|
-
if (statSync(file).isDirectory()) {
|
|
61
|
-
const digest = createHash("sha256");
|
|
62
|
-
let sizeBytes = 0;
|
|
63
|
-
function visit(directory) {
|
|
64
|
-
for (const name of readdirSync(directory).sort()) {
|
|
65
|
-
const entry = path.join(directory, name);
|
|
66
|
-
const info = lstatSync(entry);
|
|
67
|
-
const type = info.isSymbolicLink() ? "link" : info.isDirectory() ? "directory" : "file";
|
|
68
|
-
const bytes = type === "link" ? Buffer.from(readlinkSync(entry)) : type === "file" ? readFileSync(entry) : Buffer.alloc(0);
|
|
69
|
-
digest.update(`${path.relative(file, entry).split(path.sep).join("/")}\0${type}\0${bytes.length}\0`).update(bytes);
|
|
70
|
-
sizeBytes += bytes.length;
|
|
71
|
-
if (type === "directory") visit(entry);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
visit(file);
|
|
75
|
-
return { sha256: digest.digest("hex"), sizeBytes, kind: "bundle-tree-v1" };
|
|
76
|
-
}
|
|
77
|
-
const bytes = readFileSync(file);
|
|
78
|
-
return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
59
|
function run(cmd, runArgs) {
|
|
82
60
|
const result = spawnSync(cmd, runArgs, { encoding: "utf8" });
|
|
83
61
|
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SwiftPM macOS DMG ordering. With `notarize.appBeforePackage` the .app is notarized and stapled first;
|
|
2
|
+
// right-release then builds the DMG itself (dmg-build.mjs, as Tauri does) so it packs the stapled app,
|
|
3
|
+
// signs it, and the existing archive notarization staples the DMG.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { buildDmg } from "./dmg-build.mjs";
|
|
7
|
+
|
|
8
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
9
|
+
const DMG_HOOK = /dmg|hdiutil/i;
|
|
10
|
+
|
|
11
|
+
/** True when the prePackage hook looks like it builds a DMG (heuristic on command and arguments). */
|
|
12
|
+
export const prePackageBuildsDmg = (prePackage) => Boolean(prePackage?.cmd) && DMG_HOOK.test([prePackage.cmd, ...(prePackage.args ?? [])].join(" "));
|
|
13
|
+
|
|
14
|
+
/** Config error for swiftpm mac targets, or null. */
|
|
15
|
+
export function swiftMacDmgConfigError({ target, projectType, appName = "app" }) {
|
|
16
|
+
if (projectType !== "swiftpm" || !target?.notarize?.appBeforePackage) return null;
|
|
17
|
+
if (prePackageBuildsDmg(target.prePackage)) {
|
|
18
|
+
return `${appName} mac prePackage builds a DMG while notarize.appBeforePackage is on, so the DMG would pack the unstapled .app. Drop DMG creation from prePackage: right-release builds the DMG after the .app is notarized and stapled (configure targets.mac.dmg for volname/background), or set targets.mac.dmg: false and build it in targets.mac.package.`;
|
|
19
|
+
}
|
|
20
|
+
if (target.dmg !== false && !target.package?.cmd && !String(target.notarize.file ?? "").endsWith(".dmg")) {
|
|
21
|
+
return `${appName} mac notarize.file must be a .dmg when right-release builds the DMG (notarize.appBeforePackage with no targets.mac.package)`;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Plan for the automatic DMG, or null when the app config builds its own (package step, dmg: false, or no appBeforePackage). */
|
|
27
|
+
export function swiftMacDmgPlan({ target, projectType, appName = "app", resolve = (p) => path.resolve(p) }) {
|
|
28
|
+
if (projectType !== "swiftpm" || !target?.notarize?.appBeforePackage || target.dmg === false || target.package?.cmd) return null;
|
|
29
|
+
const d = target.dmg ?? {};
|
|
30
|
+
return {
|
|
31
|
+
app: resolve(target.notarize.app),
|
|
32
|
+
out: resolve(d.out ?? target.notarize.file),
|
|
33
|
+
options: { volname: d.volname ?? appName, background: d.background && resolve(d.background), iconSize: d.iconSize, appPosition: d.appPosition,
|
|
34
|
+
applicationsPosition: d.applicationsPosition, window: d.window, volumeIcon: d.volumeIcon && resolve(d.volumeIcon) },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const definedOnly = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the DMG from the stapled app, then sign it. Refuses to pack an app whose notarization ticket is not
|
|
42
|
+
* stapled. Injectable: run (stapler), build, signDmg, log.
|
|
43
|
+
*/
|
|
44
|
+
export async function buildStapledSwiftMacDmg(plan, { run = spawnSync, build = buildDmg, signDmg, log = console.log, remove = () => {} } = {}) {
|
|
45
|
+
const probe = run("xcrun", ["stapler", "validate", plan.app], { encoding: "utf8" });
|
|
46
|
+
if (probe.error || probe.status !== 0) throw fail("SWIFT_DMG_APP_NOT_STAPLED", `refusing to build the DMG: ${plan.app} is not stapled (xcrun stapler validate failed). Notarize and staple the app before packaging.`);
|
|
47
|
+
remove(plan.out);
|
|
48
|
+
log(`right-release: building macOS DMG from stapled app ${plan.app}`);
|
|
49
|
+
const built = build({ app: plan.app, out: plan.out, ...definedOnly(plan.options) });
|
|
50
|
+
if (signDmg) await signDmg(plan.out);
|
|
51
|
+
return built;
|
|
52
|
+
}
|