@rightkit/release 0.2.172 → 0.2.174
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 +10 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +7 -3
- package/development-signing.mjs +135 -4
- package/development.mjs +27 -12
- package/in-place-install.mjs +32 -7
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +32 -8
- package/install-tauri-devid.mjs +231 -0
- package/inventory-check.mjs +1 -1
- package/mac-bundle-signing.mjs +143 -1
- package/mac-package-recipe.mjs +304 -0
- package/mac-signing-matrix.mjs +73 -6
- package/macos-signing-session.mjs +12 -2
- package/media-sidecar.mjs +1 -1
- package/package-recipe-targets.mjs +245 -0
- package/package.json +1 -1
- package/portable-development.mjs +9 -9
- package/project-type.mjs +1 -1
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release.mjs +27 -9
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-sync.mjs +1 -1
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +14 -5
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +73 -0
- package/stage-sidecar.mjs +839 -0
- package/swift-mac-dmg.mjs +2 -1
- package/win-package-recipe.mjs +321 -0
package/mac-bundle-signing.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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.
|
|
2
|
+
// (no entitlements unless declared per path), then the bundle itself, reusing the swift-mac-app inside-out discovery.
|
|
3
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { discoverNestedCode } from "./swift-mac-app.mjs";
|
|
@@ -31,3 +31,145 @@ export function expandMacSigningEntries(files, { isDirectory = (f) => statSync(f
|
|
|
31
31
|
}
|
|
32
32
|
return { nested: [...new Set(nested)], files };
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
36
|
+
// Declarative per-binary signing settings, shared by the dev installer (development-signing.mjs) and release nested code.
|
|
37
|
+
//
|
|
38
|
+
// identifiers: { "<bundle-relative path or glob>": "<--identifier value>" }
|
|
39
|
+
// identifierTemplate: "<bundleId>.runtime.<relpath>" (default identifier for the remaining plain Mach-O files)
|
|
40
|
+
// entitlements: { "<bundle-relative path or glob>": "<entitlements plist path>" }
|
|
41
|
+
//
|
|
42
|
+
// Paths are relative to the .app bundle with `/` separators (`Contents/MacOS/engine`); "." names the bundle itself.
|
|
43
|
+
// An exact key beats a glob; among globs the first declared wins. `*` and `?` stay inside one path segment, `**` crosses
|
|
44
|
+
// segments. Nothing declared means nothing changes: no `--identifier`, no entitlements (release nested code stays unentitled).
|
|
45
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/** A codesign `--identifier` value. Must not start with `-` (argv safety). */
|
|
48
|
+
export const SIGNING_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/; // same charset as release validation and the signing agent
|
|
49
|
+
const TEMPLATE_TOKENS = new Set(["bundleId", "relpath", "basename"]);
|
|
50
|
+
const BUNDLE_DIRECTORY = /\.(app|framework|xpc|appex|bundle)$/;
|
|
51
|
+
const sanitizePart = (value) => String(value).replace(/[^A-Za-z0-9.-]+/g, ".");
|
|
52
|
+
|
|
53
|
+
export const isGlobPattern = (pattern) => /[*?]/.test(pattern);
|
|
54
|
+
|
|
55
|
+
export function globToRegExp(glob) {
|
|
56
|
+
let out = "";
|
|
57
|
+
for (let i = 0; i < glob.length; i++) {
|
|
58
|
+
const c = glob[i];
|
|
59
|
+
if (c === "*") {
|
|
60
|
+
if (glob[i + 1] === "*") {
|
|
61
|
+
if (glob[i + 2] === "/") { out += "(?:.*/)?"; i += 2; } else { out += ".*"; i += 1; }
|
|
62
|
+
} else out += "[^/]*";
|
|
63
|
+
} else if (c === "?") out += "[^/]";
|
|
64
|
+
else out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
65
|
+
}
|
|
66
|
+
return new RegExp(`^${out}$`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Bundle-relative POSIX path of `file` inside `app`; "" for the bundle itself. */
|
|
70
|
+
export const bundleRelativePath = (app, file) => path.relative(app, file).split(path.sep).join("/");
|
|
71
|
+
|
|
72
|
+
export function validateBundleRelativeKey(key, label, reject) {
|
|
73
|
+
if (typeof key !== "string" || !key.trim() || key !== key.trim()) reject(`${label}: keys must be non-empty bundle-relative paths`);
|
|
74
|
+
if (key !== "." && (key.startsWith("/") || /^[A-Za-z]:/.test(key) || key.includes("\\") || key.split("/").includes(".."))) {
|
|
75
|
+
reject(`${label}: ${JSON.stringify(key)} must be a path inside the .app bundle (relative, no "..", "/" separators)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateSigningIdentifier(value, label, reject) {
|
|
80
|
+
if (typeof value !== "string" || !SIGNING_IDENTIFIER_PATTERN.test(value)) reject(`${label} must be a codesign identifier (letters, digits, "." "_" "-"; not starting with "-"), got ${JSON.stringify(value)}`);
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `<relpath>` and `<basename>` are the bundle-relative path / file name with runs outside [A-Za-z0-9.-] replaced by ".". */
|
|
85
|
+
export function expandIdentifierTemplate(template, { bundleId, relpath }) {
|
|
86
|
+
return template.replace(/<(bundleId|relpath|basename)>/g, (_, token) => {
|
|
87
|
+
if (token === "bundleId") {
|
|
88
|
+
if (!bundleId) throw fail("identifier template uses <bundleId> but the bundle identifier is unknown");
|
|
89
|
+
return bundleId;
|
|
90
|
+
}
|
|
91
|
+
return sanitizePart(token === "relpath" ? relpath : path.posix.basename(relpath));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateIdentifierTemplate(template, label, reject) {
|
|
96
|
+
if (typeof template !== "string" || !template) reject(`${label} must be a non-empty string such as "<bundleId>.runtime.<relpath>"`);
|
|
97
|
+
const tokens = [...template.matchAll(/<([^<>]*)>/g)].map((match) => match[1]);
|
|
98
|
+
if (tokens.some((token) => !TEMPLATE_TOKENS.has(token))) reject(`${label} may only use <bundleId>, <relpath> and <basename>`);
|
|
99
|
+
if (!tokens.includes("relpath") && !tokens.includes("basename")) reject(`${label} must contain <relpath> or <basename> so each binary gets its own identifier`);
|
|
100
|
+
const rest = template.replace(/<[^<>]*>/g, "");
|
|
101
|
+
if (rest.includes("<") || rest.includes(">")) reject(`${label} has an unbalanced "<" or ">"`);
|
|
102
|
+
validateSigningIdentifier(expandIdentifierTemplate(template, { bundleId: "a.b", relpath: "Contents/x" }), `${label} (expanded)`, reject);
|
|
103
|
+
return template;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Compile `{ pattern: value }` into lookup entries. `value(raw, label)` validates and transforms each value. */
|
|
107
|
+
export function compileBundlePathMap(map, label, { reject, value = (raw) => raw }) {
|
|
108
|
+
if (map === undefined) return [];
|
|
109
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) reject(`${label} must be an object mapping bundle-relative paths or globs to values`);
|
|
110
|
+
return Object.entries(map).map(([pattern, raw]) => {
|
|
111
|
+
validateBundleRelativeKey(pattern, label, reject);
|
|
112
|
+
return { pattern, regex: isGlobPattern(pattern) ? globToRegExp(pattern) : null, value: value(raw, `${label}[${JSON.stringify(pattern)}]`) };
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Exact key first, then the first matching glob. undefined when nothing matches. */
|
|
117
|
+
export function lookupBundlePath(entries, relative) {
|
|
118
|
+
const exact = entries.find((entry) => !entry.regex && entry.pattern === relative);
|
|
119
|
+
if (exact) return exact.value;
|
|
120
|
+
return entries.find((entry) => entry.regex?.test(relative))?.value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The `--identifier` for one path, or undefined (codesign then derives it as before). The map applies to anything,
|
|
125
|
+
* including "." for the bundle; the template applies only to plain Mach-O files (`plain`), never to bundles.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveBundleIdentifier({ relative, identifiers = [], template, bundleId, plain }) {
|
|
128
|
+
const explicit = lookupBundlePath(identifiers, relative || ".");
|
|
129
|
+
if (explicit !== undefined) return explicit;
|
|
130
|
+
if (!template || !plain || !relative) return undefined;
|
|
131
|
+
const expanded = expandIdentifierTemplate(template, { bundleId, relpath: relative });
|
|
132
|
+
if (!SIGNING_IDENTIFIER_PATTERN.test(expanded)) throw fail(`identifier template produced an invalid identifier for ${relative}: ${JSON.stringify(expanded)}`);
|
|
133
|
+
return expanded;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** CFBundleIdentifier of a bundle (XML plist only; undefined when unreadable). */
|
|
137
|
+
export function bundleIdentifierOf(app) {
|
|
138
|
+
try {
|
|
139
|
+
const plist = readFileSync(path.join(app, "Contents", "Info.plist"), "utf8");
|
|
140
|
+
return /<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/.exec(plist)?.[1];
|
|
141
|
+
} catch { return undefined; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Release nested-code settings from `sign.identifiers`, `sign.identifierTemplate` and a per-path `sign.entitlements` object.
|
|
146
|
+
* Returns Map<absolute nested file, { identifier?, entitlements?: absolute }> holding only files with something declared;
|
|
147
|
+
* every other file keeps today's behaviour (no entitlements, no --identifier). A string `sign.entitlements` is the
|
|
148
|
+
* app-level file and is ignored here. Paths in the map are resolved against `root`; a missing file is an error.
|
|
149
|
+
* The caller (release signing) adds the flags with `nestedSigningFlags`.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveNestedSigningSettings(nested, { app, sign = {}, bundleId = bundleIdentifierOf(app), root, exists = existsSync }) {
|
|
152
|
+
const reject = (message) => { throw fail(message); };
|
|
153
|
+
const identifiers = compileBundlePathMap(sign.identifiers, "sign.identifiers", { reject, value: (raw, label) => validateSigningIdentifier(raw, label, reject) });
|
|
154
|
+
const template = sign.identifierTemplate === undefined ? undefined : validateIdentifierTemplate(sign.identifierTemplate, "sign.identifierTemplate", reject);
|
|
155
|
+
const perPath = typeof sign.entitlements === "object" && sign.entitlements !== null && !Array.isArray(sign.entitlements) ? sign.entitlements : undefined;
|
|
156
|
+
const entitlements = compileBundlePathMap(perPath, "sign.entitlements", { reject, value: (raw, label) => {
|
|
157
|
+
if (typeof raw !== "string" || !raw.trim()) reject(`${label} must be an entitlements plist path`);
|
|
158
|
+
const file = path.resolve(root, raw);
|
|
159
|
+
if (!exists(file)) reject(`${label} entitlements file not found: ${file}`);
|
|
160
|
+
return file;
|
|
161
|
+
} });
|
|
162
|
+
const settings = new Map();
|
|
163
|
+
if (!identifiers.length && !template && !entitlements.length) return settings;
|
|
164
|
+
for (const file of nested) {
|
|
165
|
+
const relative = bundleRelativePath(app, file);
|
|
166
|
+
const identifier = resolveBundleIdentifier({ relative, identifiers, template, bundleId, plain: !BUNDLE_DIRECTORY.test(file) });
|
|
167
|
+
const entitlementsFile = lookupBundlePath(entitlements, relative);
|
|
168
|
+
if (identifier !== undefined || entitlementsFile !== undefined) {
|
|
169
|
+
settings.set(file, { ...(identifier !== undefined ? { identifier } : {}), ...(entitlementsFile !== undefined ? { entitlements: entitlementsFile } : {}) });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return settings;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const nestedSigningFlags = (settings) => [...(settings?.identifier ? ["--identifier", settings.identifier] : []), ...(settings?.entitlements ? ["--entitlements", settings.entitlements] : [])];
|
|
@@ -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/mac-signing-matrix.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// inside-out plan. It never runs codesign: sign-macos.mjs (via mac-sign-core.mjs) executes the plan.
|
|
12
12
|
import { existsSync, lstatSync, openSync, closeSync, readSync, realpathSync, statSync } from "node:fs";
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import { expandMacSigningEntries, mainExecutableName } from "./mac-bundle-signing.mjs";
|
|
14
|
+
import { expandMacSigningEntries, mainExecutableName, resolveNestedSigningSettings } from "./mac-bundle-signing.mjs";
|
|
15
15
|
|
|
16
16
|
const invalid = (message) => Object.assign(new Error(message), { code: "MAC_SIGN_CONFIG_INVALID" });
|
|
17
17
|
|
|
@@ -85,11 +85,57 @@ function validateNestedKey(key) {
|
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Per-path nested signing maps: `sign.identifiers`, `sign.identifierTemplate` and an object-valued `sign.entitlements`.
|
|
89
|
+
// Same strictness as the per-item settings above: reverse-DNS identifiers, contained relative plist paths that exist.
|
|
90
|
+
function validateNestedSigningMaps(sign, { root, errors, isFile }) {
|
|
91
|
+
const declared = {};
|
|
92
|
+
const check = (map, label, value) => {
|
|
93
|
+
if (!isPlainObject(map)) { errors.push(`${label} must be an object mapping bundle-relative paths or globs to values`); return; }
|
|
94
|
+
for (const [key, raw] of Object.entries(map)) {
|
|
95
|
+
const entry = `${label}[${JSON.stringify(key)}]`;
|
|
96
|
+
if (!key.trim() || key !== key.trim() || key.includes("\0") || key.includes("\\") || path.posix.isAbsolute(key) || /^[A-Za-z]:/.test(key) || key.split("/").includes("..")) {
|
|
97
|
+
errors.push(`${entry}: key must be a bundle-relative path (no "..", no absolute path, "/" separators)`);
|
|
98
|
+
}
|
|
99
|
+
value(raw, entry);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
if (sign.identifiers !== undefined) {
|
|
103
|
+
check(sign.identifiers, "sign.identifiers", (raw, entry) => {
|
|
104
|
+
if (typeof raw !== "string" || !IDENTIFIER_PATTERN.test(raw)) errors.push(`${entry} must be 1-255 characters of letters, digits, '.' and '-', starting with a letter or digit`);
|
|
105
|
+
});
|
|
106
|
+
declared.identifiers = sign.identifiers;
|
|
107
|
+
}
|
|
108
|
+
if (sign.identifierTemplate !== undefined) {
|
|
109
|
+
const template = sign.identifierTemplate;
|
|
110
|
+
if (typeof template !== "string" || !template) errors.push("sign.identifierTemplate must be a non-empty string such as \"<bundleId>.runtime.<relpath>\"");
|
|
111
|
+
else if (!/^[A-Za-z0-9.-]*$/.test(template.replace(/<[^<>]*>/g, ""))) errors.push("sign.identifierTemplate literal text may only contain letters, digits, '.' and '-' around <bundleId>, <relpath> and <basename>");
|
|
112
|
+
else declared.identifierTemplate = template;
|
|
113
|
+
}
|
|
114
|
+
if (isPlainObject(sign.entitlements)) {
|
|
115
|
+
check(sign.entitlements, "sign.entitlements", (raw, entry) => {
|
|
116
|
+
if (typeof raw !== "string" || !raw.trim()) { errors.push(`${entry} must be a non-empty path string`); return; }
|
|
117
|
+
if (path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw) || raw.includes("\0")) { errors.push(`${entry} must be relative to the release config directory: ${raw}`); return; }
|
|
118
|
+
const resolved = path.resolve(root, raw);
|
|
119
|
+
const relative = path.relative(root, resolved);
|
|
120
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) errors.push(`${entry} must stay inside the release config directory: ${raw}`);
|
|
121
|
+
else if (!isFile(resolved)) errors.push(`${entry} entitlements file not found: ${raw}`);
|
|
122
|
+
else if (!looksLikePlist(resolved)) errors.push(`${entry} is not a property list (expected an XML or binary plist): ${raw}`);
|
|
123
|
+
});
|
|
124
|
+
declared.entitlements = sign.entitlements;
|
|
125
|
+
}
|
|
126
|
+
if (errors.length || !Object.keys(declared).length) return undefined;
|
|
127
|
+
// Delegate key globs and template tokens to the shared resolver so release and development read them identically.
|
|
128
|
+
try { resolveNestedSigningSettings([], { app: "x.app", sign: declared, bundleId: "a.b", root, exists: () => true }); }
|
|
129
|
+
catch (error) { errors.push(error.message); return undefined; }
|
|
130
|
+
return declared;
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
/**
|
|
89
134
|
* Validate and normalize `sign` for a macOS target. Collects every problem, then throws one
|
|
90
|
-
* MAC_SIGN_CONFIG_INVALID error. Returns { entries, nestedCode } where
|
|
91
|
-
* entries:
|
|
92
|
-
* nestedCode:
|
|
135
|
+
* MAC_SIGN_CONFIG_INVALID error. Returns { entries, nestedCode, nestedSigning? } where
|
|
136
|
+
* entries: [{ file, identifier?, entitlements?: absolute, options: [] }] (file stays as declared)
|
|
137
|
+
* nestedCode: [{ key, identifier?, entitlements?: absolute, options: [] }]
|
|
138
|
+
* nestedSigning: { identifiers?, identifierTemplate?, entitlements? } as declared; present only when one is declared
|
|
93
139
|
* Injectable: isFile.
|
|
94
140
|
*/
|
|
95
141
|
export function validateMacSigningConfig(sign, { root, isFile = (file) => existsSync(file) && statSync(file).isFile() } = {}) {
|
|
@@ -136,8 +182,10 @@ export function validateMacSigningConfig(sign, { root, isFile = (file) => exists
|
|
|
136
182
|
}
|
|
137
183
|
}
|
|
138
184
|
}
|
|
185
|
+
const nestedSigning = validateNestedSigningMaps(sign, context);
|
|
186
|
+
if (nestedSigning && !entries.some((entry) => /\.app\/?$/.test(entry.file))) errors.push("sign.identifiers, sign.identifierTemplate and per-path sign.entitlements need a .app entry in sign.prePackageFiles to resolve their paths against");
|
|
139
187
|
if (errors.length) throw invalid(`invalid macOS signing configuration:\n${errors.map((line) => `- ${line}`).join("\n")}`);
|
|
140
|
-
return { entries, nestedCode };
|
|
188
|
+
return { entries, nestedCode, ...(nestedSigning ? { nestedSigning } : {}) };
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
/** True when an entry or nested item carries anything beyond the default (hardened runtime, no entitlements). */
|
|
@@ -154,7 +202,7 @@ const depth = (file) => file.split(path.sep).length;
|
|
|
154
202
|
* a nestedCode path is missing, is a symlink, escapes the bundle, or names the bundle's main executable.
|
|
155
203
|
* Injectable: expand, lstat, realpath, mainName.
|
|
156
204
|
*/
|
|
157
|
-
export function planMacSigning({ entries, nestedCode = [], expand = expandMacSigningEntries, lstat = lstatSync, realpath = realpathSync, mainName = mainExecutableName }) {
|
|
205
|
+
export function planMacSigning({ entries, nestedCode = [], nestedSigning, root, resolveSettings = resolveNestedSigningSettings, expand = expandMacSigningEntries, lstat = lstatSync, realpath = realpathSync, mainName = mainExecutableName }) {
|
|
158
206
|
const { nested: discovered } = expand(entries.map((entry) => entry.file));
|
|
159
207
|
const byPath = new Map(discovered.map((file) => [file, { file, options: [] }]));
|
|
160
208
|
if (nestedCode.length) {
|
|
@@ -174,6 +222,25 @@ export function planMacSigning({ entries, nestedCode = [], expand = expandMacSig
|
|
|
174
222
|
byPath.set(file, { file, ...(item.identifier ? { identifier: item.identifier } : {}), ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options });
|
|
175
223
|
}
|
|
176
224
|
}
|
|
225
|
+
// Per-path maps (sign.identifiers / identifierTemplate / per-path sign.entitlements) fill in only what a nestedCode item left
|
|
226
|
+
// unset. Nothing declared: this block is skipped and the items stay exactly as discovered.
|
|
227
|
+
if (nestedSigning && Object.keys(nestedSigning).length) {
|
|
228
|
+
const apps = entries.map((entry) => entry.file).filter((file) => /\.app\/?$/.test(file)).map((file) => file.replace(/\/+$/, ""));
|
|
229
|
+
for (const app of apps) {
|
|
230
|
+
const own = [...byPath.values()].filter((item) => item.file.startsWith(app + path.sep) || item.file.startsWith(`${app}/`));
|
|
231
|
+
let settings;
|
|
232
|
+
try { settings = resolveSettings(own.map((item) => item.file), { app, sign: nestedSigning, root: root ?? process.cwd() }); }
|
|
233
|
+
catch (error) { throw invalid(error.message); }
|
|
234
|
+
for (const item of own) {
|
|
235
|
+
const found = settings.get(item.file);
|
|
236
|
+
if (!found) continue;
|
|
237
|
+
const identifier = item.identifier ?? found.identifier;
|
|
238
|
+
if (identifier !== undefined && !IDENTIFIER_PATTERN.test(identifier)) throw invalid(`signing identifier for ${path.relative(app, item.file)} must be letters, digits, '.' and '-': ${JSON.stringify(identifier)}`);
|
|
239
|
+
const entitlements = item.entitlements ?? found.entitlements;
|
|
240
|
+
byPath.set(item.file, { ...item, ...(identifier !== undefined ? { identifier } : {}), ...(entitlements ? { entitlements } : {}) });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
177
244
|
const nested = [...byPath.values()].sort((a, b) => depth(b.file) - depth(a.file) || a.file.localeCompare(b.file));
|
|
178
245
|
return { nested, top: entries };
|
|
179
246
|
}
|
|
@@ -5,7 +5,7 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
5
5
|
|
|
6
6
|
import { requestSigningAgentSync, validateNotaryAgentReport } from "./signing-agent-client.mjs";
|
|
7
7
|
import { installSigningAgent, requireMacGuiSession } from "./signing-agent-install.mjs";
|
|
8
|
-
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within, NOTARIZATION_TIMEOUT_MS, MAX_NOTARIZATION_TIMEOUT_MS } from "./rightkit-signing-agent.mjs";
|
|
8
|
+
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within, NOTARIZATION_TIMEOUT_MS, MAX_NOTARIZATION_TIMEOUT_MS, AGENT_IDENTIFIER_PATTERN, CODESIGN_IDENTIFIER_SCHEMA } from "./rightkit-signing-agent.mjs";
|
|
9
9
|
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
10
10
|
|
|
11
11
|
export class MacSigningSessionError extends Error {
|
|
@@ -48,11 +48,13 @@ export function codesignAgentItems(commands) {
|
|
|
48
48
|
else if (option === "--timestamp" && item.timestamp === undefined) item.timestamp = true;
|
|
49
49
|
else if (option === "--options" && args[i + 1] === "runtime" && item.runtime === undefined) { item.runtime = true; i++; }
|
|
50
50
|
else if (option === "--sign" && item.identity === undefined) item.identity = args[++i];
|
|
51
|
+
else if (option === "--identifier" && item.identifier === undefined) item.identifier = args[++i];
|
|
51
52
|
else if (option === "--entitlements" && item.entitlements === undefined) item.entitlements = args[++i];
|
|
52
53
|
else if (option === "--preserve-metadata=entitlements" && item.preserveEntitlements === undefined) item.preserveEntitlements = true;
|
|
53
54
|
else throw new TypeError(`Unsupported agent codesign option: ${option}`);
|
|
54
55
|
}
|
|
55
56
|
item.path = args.at(-1);
|
|
57
|
+
if (item.identifier !== undefined && !AGENT_IDENTIFIER_PATTERN.test(item.identifier)) throw new TypeError("Agent signing requires a reverse-DNS codesign identifier");
|
|
56
58
|
if (!item.identity || item.identity === "-" || !path.isAbsolute(item.path ?? "")
|
|
57
59
|
|| (item.entitlements !== undefined && !path.isAbsolute(item.entitlements))) throw new TypeError("Agent signing requires real identity & absolute paths");
|
|
58
60
|
return item;
|
|
@@ -76,6 +78,8 @@ export function runMacGuiSigning({ kind, commands, files, cwd = process.cwd(), e
|
|
|
76
78
|
try {
|
|
77
79
|
if (kind === "codesign") {
|
|
78
80
|
plan.items = codesignAgentItems(commands);
|
|
81
|
+
// Only a request that carries a typed identifier uses schema 2; every other codesign request stays schema 1.
|
|
82
|
+
if (plan.items.some((item) => item.identifier !== undefined)) plan.schema = CODESIGN_IDENTIFIER_SCHEMA;
|
|
79
83
|
// Development signing generates entitlements in TMPDIR. Copy only those
|
|
80
84
|
// plist bytes into managed private staging, never broaden signing roots.
|
|
81
85
|
for (const item of plan.items) {
|
|
@@ -99,7 +103,13 @@ export function runMacGuiSigning({ kind, commands, files, cwd = process.cwd(), e
|
|
|
99
103
|
let report;
|
|
100
104
|
try { report = request(plan, clientOptions); }
|
|
101
105
|
catch (error) {
|
|
102
|
-
|
|
106
|
+
let missing = error.code === "MAC_SIGNING_AGENT_UNAVAILABLE";
|
|
107
|
+
// An installed agent that predates typed identifiers refuses schema 2: upgrade it once, never loop on a rejected request.
|
|
108
|
+
if (!missing && plan.schema === CODESIGN_IDENTIFIER_SCHEMA && error.code === "MAC_SIGNING_AGENT_PROTOCOL") {
|
|
109
|
+
const health = request({ schema: 1, type: "health" }, { home, timeoutMs: 1500 });
|
|
110
|
+
missing = !health.capabilities?.includes("codesign-identifier");
|
|
111
|
+
}
|
|
112
|
+
if (!missing) throw error;
|
|
103
113
|
requireMacGuiSession({ run, uid });
|
|
104
114
|
install({ home, username, uid, run, io, managedRoot: env.RIGHTKIT_MANAGED_ROOT });
|
|
105
115
|
report = request(plan, clientOptions); // One install attempt, one retry.
|
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",
|