apple build --platform macos: --tauri takes tauri.<name>.conf.json (tauri.appstore.conf.json found by itself), entitlements from --entitlements / apple.macos.entitlements / the merged config, Developer-ID-only keys refused or warned

This commit is contained in:
CRTkafa 2026-11-03 23:40:06 +07:00
parent d88ed3ec55
commit 68467ffe9b
7 changed files with 312 additions and 21 deletions

View file

@ -1,6 +1,6 @@
// Mac App Store build of a Tauri app: `tauri build --bundles app`, signed for the Mac App Store, then productbuild to a .pkg.
// storebound apple build --platform macos --tauri <src-tauri> [--path-free] [--target universal-apple-darwin]
// [--version x.y.z] [--build-number N] [--profile <file.provisionprofile>] [--out build/macos] [--unsigned] [--dry] [--json]
// [--entitlements <plist>] [--allow-entitlements] [--version x.y.z] [--build-number N] [--profile <file.provisionprofile>] [--out build/macos] [--unsigned] [--dry] [--json]
// Steps: Tauri project -> version / build number (next free one on App Store Connect, every platform) -> signing
// identities from the keychain ("Apple Distribution" or "3rd Party Mac Developer Application"; "Mac Installer
// Distribution" or "3rd Party Mac Developer Installer") -> Mac App Store provisioning profile (--profile, installed, or
@ -27,7 +27,7 @@ import { prebuildFor, prebuildsDone, runPrebuild } from '../lib/web-assets';
import { preCheckCommand } from '../lib/pre-check';
import { pump } from '../lib/runner';
import {
appProblems, entitlementKeysOf, entitlementsXml, findBuiltApp, findTauri, leakNeedles, masEntitlements, type MasProfile, masSteps, pathFreeEnv, pickMasIdentities,
appProblems, entitlementKeysOf, entitlementsSource, entitlementsXml, findBuiltApp, findTauri, leakNeedles, masEntitlementProblems, masEntitlements, type MasProfile, masSteps, pathFreeEnv, pickMasIdentities,
pickMasProfiles, pkgProblems, parsePkgSignature, readAppInfo, scanPathLeaks, tauriOverlay, categoryId,
} from '../lib/mas';
import { readdirSync } from 'node:fs';
@ -167,17 +167,31 @@ export async function run(ctx: Ctx, platform: string = process.platform): Promis
const prebuild = prebuildFor(config, 'apple') ?? null;
const prebuildSkip = !prebuild ? undefined : a.has('no-prebuild') ? '--no-prebuild' : prebuildsDone().has(prebuild) ? 'already ran in this flow run' : undefined;
const appCheck = config.apple?.macAppCheck?.trim() || null;
const existingEnt = tauri.entitlements && existsSync(tauri.entitlements) ? (parsePlist(readFileSync(tauri.entitlements)) as Record<string, PlistValue>) : undefined;
// entitlements: --entitlements, apple.macos.entitlements, the merged Tauri config (the first one that is set)
const entFlag = a.get('entitlements'), entCfg = config.apple?.macos?.entitlements;
const entSrc = entitlementsSource({ flag: entFlag ? resolve(entFlag) : undefined, config: entCfg ? resolve(entCfg) : undefined, tauri: tauri.entitlements });
let existingEnt: Record<string, PlistValue> | undefined;
if (entSrc) {
if (existsSync(entSrc.file)) existingEnt = parsePlist(readFileSync(entSrc.file)) as Record<string, PlistValue>;
else if (entSrc.source !== 'tauri config bundle.macOS.entitlements') throw new Error(`${entSrc.source}: ${entSrc.file} does not exist`);
else warn(TAG, `${entSrc.source}: ${relative(root, entSrc.file)} does not exist, using the sandbox defaults only`);
}
const ent = masEntitlements({ teamId: team, bundleId, existing: existingEnt });
const plan = { platform: 'MAC_OS', tauri: relative(root, tauri.confPath), target, version, build: buildNo, teamId: teamId, unsigned, pathFree, appIdentity, installerIdentity,
profile: profile ? basenameOf(profile) : null, entitlements: Object.keys(ent), tauriOverlay: overlay, env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, k === 'RUSTFLAGS' ? v.replaceAll(home, '~') : v])),
const entCheck = masEntitlementProblems(ent);
const allowEnt = a.has('allow-entitlements');
const plan = { platform: 'MAC_OS', tauri: relative(root, tauri.confPath), tauriOverlayFile: tauri.overlayPath ? relative(root, tauri.overlayPath) : null, target, version, build: buildNo, teamId: teamId, unsigned, pathFree, appIdentity, installerIdentity,
profile: profile ? basenameOf(profile) : null, entitlements: Object.keys(ent), entitlementsSource: entSrc ? { source: entSrc.source, file: relative(root, entSrc.file) } : null,
entitlementProblems: entCheck, tauriOverlay: overlay, env: Object.fromEntries(Object.entries(env).map(([k, v]) => [k, k === 'RUSTFLAGS' ? v.replaceAll(home, '~') : v])),
prebuild, ...(prebuildSkip ? { prebuildSkip } : {}), appCheck,
steps: [showCmd(steps.tauri, null), showCmd(steps.codesignVerify, null), showCmd(steps.productbuild, null), showCmd(steps.pkgCheck, null)], out: relative(root, out) || '.', pkg: relative(root, pkg) };
if (dry) {
if (json) { printJson(plan); return; }
log(TAG, `Tauri ${tauri.productName} (${bundleId}) ${version} (${buildNo}) for ${target}, ${unsigned ? 'UNSIGNED' : 'Mac App Store signing'}${pathFree ? ', path-free' : ''}`);
log(TAG, identityLine({ platform, appIdentity, installerIdentity, profile: plan.profile }));
log(TAG, `entitlements: ${plan.entitlements.join(', ')}`);
if (tauri.overlayPath) log(TAG, `Tauri overlay ${plan.tauriOverlayFile}${tauri.overlayAuto ? ' (found next to tauri.conf.json)' : ''}: merged over tauri.conf.json, passed to tauri build --config`);
log(TAG, `entitlements from ${entSrc?.source ?? 'the sandbox defaults'}${entSrc ? ` (${plan.entitlementsSource!.file})` : ''}: ${plan.entitlements.join(', ')}`);
for (const e of entCheck.errors) warn(TAG, `entitlement ${e}${allowEnt ? '' : ' (the real build stops here; --allow-entitlements builds anyway)'}`);
for (const w of entCheck.warnings) warn(TAG, `entitlement ${w}`);
if (pathFree) log(TAG, `RUSTFLAGS: ${plan.env.RUSTFLAGS}`);
if (prebuild) log(TAG, prebuildSkip ? `prebuild skipped (${prebuildSkip}): ${prebuild}` : `would run the prebuild: ${prebuild}`);
for (const s of plan.steps) log(TAG, `would run: ${s}`);
@ -185,6 +199,11 @@ export async function run(ctx: Ctx, platform: string = process.platform): Promis
done(TAG, 'macOS build planned (dry run)');
return;
}
if (tauri.overlayPath) log(TAG, `Tauri overlay ${plan.tauriOverlayFile}${tauri.overlayAuto ? ' (found next to tauri.conf.json)' : ''}: merged over tauri.conf.json, passed to tauri build --config`);
log(TAG, `entitlements from ${entSrc?.source ?? 'the sandbox defaults'}${entSrc ? ` (${plan.entitlementsSource!.file})` : ''}: ${plan.entitlements.join(', ')}`);
for (const w of entCheck.warnings) warn(TAG, `entitlement ${w}`);
for (const e of entCheck.errors) if (allowEnt) warn(TAG, `entitlement ${e} (--allow-entitlements)`);
if (entCheck.errors.length && !allowEnt) throw new Error(`the entitlements (from ${entSrc?.source ?? 'the sandbox defaults'}) do not belong in a Mac App Store build:\n${entCheck.errors.map(x => ` ${x}`).join('\n')}\n Point the build at the App Store plist: --entitlements <plist>, apple.macos.entitlements, or a tauri.appstore.conf.json overlay next to tauri.conf.json (--allow-entitlements builds anyway).`);
if (!mac) throw new Error('apple build --platform macos needs macOS (tauri build, codesign, productbuild)');
const remote = remoteBuildSession(process.env, run1(['launchctl', 'managername'], 5000).out.trim());
if (remote && !unsigned && !a.has('allow-ssh')) throw new Error(`${sshBuildRefusal(remote, { unlockFlag: false })}
@ -205,7 +224,7 @@ export async function run(ctx: Ctx, platform: string = process.platform): Promis
const entOut = unsigned ? null : entitlementKeysOf(run1(['codesign', '-d', '--entitlements', ':-', appDir]).out);
const leaks = pathFree || a.has('scan-paths') ? scanPathLeaks(appDir, leakNeedles(home, user, root)) : [];
const checks = appProblems({ info: readAppInfo(appDir), entitlements: entOut, hasProfile: existsSync(join(appDir, 'Contents', 'embedded.provisionprofile')), leaks },
{ bundleId, version, build: buildNo, signed: !unsigned, pathFree });
{ bundleId, version, build: buildNo, signed: !unsigned, pathFree, allowEntitlements: allowEnt });
for (const w of checks.warnings) warn(TAG, w);
if (checks.errors.length) throw new Error(`${basenameOf(appDir)} was built, but the Mac App Store would refuse it:\n${checks.errors.map(x => ` ${x}`).join('\n')}`);
if (appCheck) {

View file

@ -1,5 +1,5 @@
// Archive and export an App Store IPA with xcodebuild, no Xcode UI.
// storebound apple build --platform macos --tauri <src-tauri> [--path-free] [--unsigned] [--target T] [--profile F] [--version x.y.z] [--build-number N] [--out D]
// storebound apple build --platform macos --tauri <src-tauri> [--path-free] [--unsigned] [--target T] [--profile F] [--entitlements <plist>] [--allow-entitlements] [--version x.y.z] [--build-number N] [--out D]
// (Mac App Store build of a Tauri app: cli/apple/build-mas.ts)
// storebound apple build [--version 1.2.0] [--build-number N] [--scheme S] [--workspace W | --project P]
// [--configuration Release] [--out build/ios] [--no-prebuild] [--allow-stale-web] [--dry] [--json]

View file

@ -69,12 +69,14 @@
"apple build": [
"allow-app-group-gap",
"allow-device-gap",
"allow-entitlements",
"allow-ssh",
"allow-stale-web",
"allow-version-mismatch",
"build-number",
"configuration",
"derived-data",
"entitlements",
"env-file",
"evidence",
"keychain",
@ -1181,6 +1183,7 @@
"allow-app-group-gap",
"allow-availability-changes",
"allow-device-gap",
"allow-entitlements",
"allow-existing",
"allow-origin",
"allow-price-changes",
@ -1243,6 +1246,7 @@
"download",
"enable",
"end",
"entitlements",
"env-file",
"every",
"evidence",
@ -1506,7 +1510,7 @@
"storebound apple appinfo [--dry] [--json] [--allow-price-changes] [--allow-availability-changes]"
],
"apple build": [
"storebound apple build --platform macos --tauri <src-tauri> [--path-free] [--unsigned] [--target T] [--profile F] [--version x.y.z] [--build-number N] [--out D]",
"storebound apple build --platform macos --tauri <src-tauri> [--path-free] [--unsigned] [--target T] [--profile F] [--entitlements <plist>] [--allow-entitlements] [--version x.y.z] [--build-number N] [--out D]",
"storebound apple build [--version 1.2.0] [--build-number N] [--scheme S] [--workspace W | --project P]"
],
"apple bundle": [

View file

@ -714,8 +714,10 @@ export interface ShipConfig {
* --platform macos`; `reviewNotes`: used instead of apple.info.reviewNotes by `apple review-details --platform macos`.
* `gameCenter: true`: the Mac app has Game Center (silences the Game Center check of `validate --platform macos`).
* `noIap: true`: the Mac app sells nothing (validate then flags in-app purchase lines in the Mac texts).
* `entitlements`: the Mac App Store entitlements plist of `apple build --platform macos --tauri` (e.g. "src-tauri/Entitlements.appstore.plist"),
* used instead of the Tauri config's bundle.macOS.entitlements (a project with a Developer ID channel keeps that one there); `--entitlements <plist>` wins over it.
*/
macos?: { listing?: string; reviewNotes?: string; gameCenter?: boolean; noIap?: boolean };
macos?: { listing?: string; reviewNotes?: string; gameCenter?: boolean; noIap?: boolean; entitlements?: string };
/** "Header and Search Results" creative assets (`storebound apple creative-assets`, iOS / iPadOS 27+). Files under
* <renders>/<lang>/creative/: header.{png,jpg,mov,m4v,mp4} (3840x1646, 21:9), search.{...} (3:2, 1920x1280 to
* 3840x2560) or universal.png (5244x2950) for both. `header` / `search` false skip that slot; `universal` is a
@ -1047,9 +1049,9 @@ export function validateExtras(cfg: Partial<ShipConfig>): string[] {
if (cfg.play?.free !== undefined && typeof cfg.play.free !== 'boolean') e.push('play.free: true (free) | false (paid)');
const mo = cfg.apple?.macos;
if (mo !== undefined) {
if (typeof mo !== 'object' || mo === null || Array.isArray(mo)) e.push('apple.macos: { listing?: "store/appstore-macos.md", reviewNotes?: "...", gameCenter?: true, noIap?: true }');
if (typeof mo !== 'object' || mo === null || Array.isArray(mo)) e.push('apple.macos: { listing?: "store/appstore-macos.md", reviewNotes?: "...", gameCenter?: true, noIap?: true, entitlements?: "src-tauri/Entitlements.appstore.plist" }');
else {
for (const k of ['listing', 'reviewNotes'] as const) if (mo[k] !== undefined && (typeof mo[k] !== 'string' || !mo[k]!.trim())) e.push(`apple.macos.${k}: a non-empty string${k === 'listing' ? ' (path of the macOS App Store listing file)' : ''}`);
for (const k of ['listing', 'reviewNotes', 'entitlements'] as const) if (mo[k] !== undefined && (typeof mo[k] !== 'string' || !mo[k]!.trim())) e.push(`apple.macos.${k}: a non-empty string${k === 'listing' ? ' (path of the macOS App Store listing file)' : k === 'entitlements' ? ' (path of the Mac App Store entitlements plist)' : ''}`);
for (const k of ['gameCenter', 'noIap'] as const) if (mo[k] !== undefined && typeof mo[k] !== 'boolean') e.push(`apple.macos.${k}: true | false`);
}
}

View file

@ -18,20 +18,52 @@ export interface TauriProject {
category?: string;
minOs?: string;
entitlements?: string;
/** The overlay file merged over the base config (tauri.<name>.conf.json), absolute; passed to `tauri build --config` too. */
overlayPath?: string;
/** True when the overlay was found next to tauri.conf.json without being named (tauri.appstore.conf.json / tauri.mas.conf.json). */
overlayAuto?: boolean;
conf: Record<string, unknown>;
}
/** Overlay files that are picked up on their own for a Mac App Store build, in this order. */
export const MAS_OVERLAY_NAMES = ['tauri.appstore.conf.json', 'tauri.mas.conf.json'] as const;
/** JSON merge patch (RFC 7386, what Tauri's --config does): objects merge, null removes a key, anything else (arrays too) replaces. */
export function mergePatch(base: unknown, patch: unknown): unknown {
if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) return patch;
const out: Record<string, unknown> = base !== null && typeof base === 'object' && !Array.isArray(base) ? { ...(base as Record<string, unknown>) } : {};
for (const [k, v] of Object.entries(patch as Record<string, unknown>)) {
if (v === null) delete out[k];
else out[k] = mergePatch(out[k], v);
}
return out;
}
/** tauri.<name>.conf.json (an overlay), not tauri.conf.json itself. */
export const isOverlayFile = (p: string): boolean => /(^|[\\/])tauri\.[^\\/]+\.conf\.json$/.test(p);
const SKIP = new Set(['node_modules', 'target', '.git', 'dist', 'build', '.storebound', 'Pods']);
/** The Tauri project: `hint` (a folder with tauri.conf.json, or the file itself) or the only tauri.conf.json found. */
/** The Tauri project: `hint` (a folder with tauri.conf.json, or a tauri.conf.json / tauri.conf.json5 / tauri.<name>.conf.json
* file) or the only tauri.conf.json found. An overlay file (tauri.<name>.conf.json) is merged over the base
* tauri.conf.json of its folder; tauri.appstore.conf.json / tauri.mas.conf.json next to the base config is picked up on its own. */
export function findTauri(root: string, hint?: string): TauriProject {
const names = ['tauri.conf.json', 'tauri.conf.json5'];
let confPath: string | undefined;
let confPath: string | undefined, overlayPath: string | undefined, overlayAuto = false;
const autoOverlay = (): void => { const o = MAS_OVERLAY_NAMES.map(n => join(dirname(confPath!), n)).find(existsSync); if (o) { overlayPath = o; overlayAuto = true; } };
if (hint && hint !== 'true') {
const p = join(root, hint);
if (/tauri\.conf\.json5?$/.test(p) && existsSync(p)) confPath = p;
else confPath = names.map(n => join(p, n)).find(existsSync);
if (!confPath) throw new Error(`--tauri ${hint}: no tauri.conf.json in it`);
if (isOverlayFile(p)) {
if (!existsSync(p)) throw new Error(`--tauri ${hint}: no such file`);
overlayPath = p;
confPath = names.map(n => join(dirname(p), n)).find(existsSync);
if (!confPath) throw new Error(`--tauri ${hint}: no tauri.conf.json next to this overlay (the base config it is merged over)`);
} else {
if (/tauri\.conf\.json5?$/.test(p) && existsSync(p)) confPath = p;
else confPath = names.map(n => join(p, n)).find(existsSync);
if (!confPath) throw new Error(`--tauri ${hint}: no tauri.conf.json in it`);
autoOverlay();
}
} else {
const hits: string[] = [];
const walk = (dir: string, d: number): void => {
@ -43,16 +75,22 @@ export function findTauri(root: string, hint?: string): TauriProject {
walk(root, 4);
if (hits.length !== 1) throw new Error(hits.length ? `several Tauri projects (${hits.map(h => relative(root, h)).join(', ')}): pass --tauri <folder with tauri.conf.json>` : 'no tauri.conf.json found: pass --tauri <folder with tauri.conf.json>');
confPath = hits[0]!;
autoOverlay();
}
let conf: Record<string, any>;
try { conf = JSON.parse(readFileSync(confPath, 'utf8')); } catch (e) { throw new Error(`${confPath}: not valid JSON (${(e as Error).message}); tauri.conf.json5 / Tauri.toml are not read, use tauri.conf.json`); }
if (overlayPath) {
let patch: unknown;
try { patch = JSON.parse(readFileSync(overlayPath, 'utf8')); } catch (e) { throw new Error(`${overlayPath}: not valid JSON (${(e as Error).message})`); }
conf = mergePatch(conf, patch) as Record<string, any>;
}
const mac = conf.bundle?.macOS ?? {};
const productName = String(conf.productName ?? '');
if (!productName) throw new Error(`${confPath}: productName is missing`);
const dir = dirname(confPath);
return { dir, confPath, cwd: dirname(dir), productName, identifier: String(conf.identifier ?? ''), version: typeof conf.version === 'string' ? conf.version : undefined,
category: typeof conf.bundle?.category === 'string' ? conf.bundle.category : undefined, minOs: typeof mac.minimumSystemVersion === 'string' ? mac.minimumSystemVersion : undefined,
entitlements: typeof mac.entitlements === 'string' ? join(dir, mac.entitlements) : undefined, conf };
entitlements: typeof mac.entitlements === 'string' ? join(dir, mac.entitlements) : undefined, ...(overlayPath ? { overlayPath, overlayAuto } : {}), conf };
}
/** Tauri's category names (and Apple's public.app-category.* ids) -> the LSApplicationCategoryType Tauri writes; null when unknown. */
@ -79,6 +117,43 @@ export function masEntitlements(o: { teamId: string; bundleId: string; existing?
};
}
/** Where the entitlements of a Mac App Store build come from, first hit wins: `--entitlements <plist>`, the config's
* `apple.macos.entitlements`, the merged Tauri config's `bundle.macOS.entitlements`. Paths are already absolute. Pure. */
export function entitlementsSource(o: { flag?: string; config?: string; tauri?: string }): { file: string; source: string } | null {
if (o.flag) return { file: o.flag, source: '--entitlements' };
if (o.config) return { file: o.config, source: 'apple.macos.entitlements' };
if (o.tauri) return { file: o.tauri, source: 'tauri config bundle.macOS.entitlements' };
return null;
}
/** Entitlements that App Store Connect refuses in a Mac App Store build (a Developer ID / development leftover). */
export const MAS_REFUSED_KEYS: Record<string, string> = {
'com.apple.security.get-task-allow': 'a development entitlement, App Store Connect refuses it',
'com.apple.security.cs.disable-library-validation': 'a Developer ID hardened-runtime exception, App Store Connect refuses it',
};
/** Hardened-runtime exceptions and Developer ID habits that do not belong in a Mac App Store build. App Review may
* question them or the sandbox ignores them, but they are not an automatic upload refusal: a warning, not a stop. */
export const MAS_SUSPECT_KEYS: Record<string, string> = {
'com.apple.security.cs.allow-jit': 'hardened-runtime exception for Developer ID builds; App Review asks why a sandboxed app needs it',
'com.apple.security.cs.allow-unsigned-executable-memory': 'hardened-runtime exception for Developer ID builds; App Review asks why',
'com.apple.security.cs.disable-executable-page-protection': 'hardened-runtime exception for Developer ID builds',
'com.apple.security.cs.allow-dyld-environment-variables': 'hardened-runtime exception for Developer ID builds',
'com.apple.security.cs.debugger': 'hardened-runtime exception for Developer ID builds (a debugger cannot be sandboxed)',
'com.apple.security.automation.apple-events': 'needs NSAppleEventsUsageDescription in Info.plist and a reason in the review notes; usually a Developer ID leftover',
};
/** The entitlements of a Mac App Store build against the two lists above (a key set to false is fine). Pure. */
export function masEntitlementProblems(ent: Record<string, PlistValue> | null | undefined): { errors: string[]; warnings: string[] } {
const errors: string[] = [], warnings: string[] = [];
for (const [k, v] of Object.entries(ent ?? {})) {
if (v === false) continue;
if (MAS_REFUSED_KEYS[k]) errors.push(`${k} is set: ${MAS_REFUSED_KEYS[k]}`);
else if (MAS_SUSPECT_KEYS[k]) warnings.push(`${k} is set: ${MAS_SUSPECT_KEYS[k]}`);
}
return { errors, warnings };
}
export const entitlementsXml = (e: Record<string, PlistValue>): string => toXmlPlist(e);
/** Keys of an entitlements plist (XML or what `codesign -d --entitlements :-` prints). */
@ -170,7 +245,7 @@ export const leakNeedles = (home: string, user: string, root: string): string[]
export interface AppFacts { info: Record<string, PlistValue>; entitlements: Record<string, PlistValue> | null; hasProfile: boolean; leaks: Leak[] }
/** Problems (stop) and warnings of a built .app before it goes into a .pkg. Pure. */
export function appProblems(f: AppFacts, o: { bundleId: string; version: string; build: string; signed: boolean; pathFree: boolean }): { errors: string[]; warnings: string[] } {
export function appProblems(f: AppFacts, o: { bundleId: string; version: string; build: string; signed: boolean; pathFree: boolean; allowEntitlements?: boolean }): { errors: string[]; warnings: string[] } {
const errors: string[] = [], warnings: string[] = [];
const s = (k: string): string | undefined => (typeof f.info[k] === 'string' ? f.info[k] as string : undefined);
if (s('CFBundleIdentifier') !== o.bundleId) errors.push(`CFBundleIdentifier is ${s('CFBundleIdentifier') ?? 'missing'}, not ${o.bundleId}`);
@ -184,7 +259,9 @@ export function appProblems(f: AppFacts, o: { bundleId: string; version: string;
else {
for (const k of MAS_REQUIRED) if (f.entitlements[k] === undefined) errors.push(`entitlement ${k} is missing`);
if (f.entitlements['com.apple.security.app-sandbox'] !== true) errors.push('com.apple.security.app-sandbox is not true');
if (f.entitlements['com.apple.security.get-task-allow'] === true) errors.push('com.apple.security.get-task-allow is true: a development entitlement, App Store Connect refuses it');
const ep = masEntitlementProblems(f.entitlements);
(o.allowEntitlements ? warnings : errors).push(...ep.errors);
warnings.push(...ep.warnings);
}
if (!f.hasProfile) errors.push('Contents/embedded.provisionprofile is missing: a Mac App Store app needs its provisioning profile inside (Tauri bundle.macOS.files)');
}
@ -230,7 +307,7 @@ export interface MasPlanInput {
export function masSteps(i: MasPlanInput): { tauri: string[]; productbuild: string[]; codesignVerify: string[]; pkgCheck: string[] } {
const appPath = join(i.tauri.dir, 'target', i.target, 'release', 'bundle', 'macos', `${i.tauri.productName}.app`);
return {
tauri: [...i.tauriCmd, 'build', '--bundles', 'app', '--target', i.target, '--config', i.configFile],
tauri: [...i.tauriCmd, 'build', '--bundles', 'app', '--target', i.target, ...(i.tauri.overlayPath ? ['--config', i.tauri.overlayPath] : []), '--config', i.configFile],
codesignVerify: ['codesign', '--verify', '--deep', '--strict', '--verbose=2', appPath],
productbuild: ['productbuild', '--component', appPath, '/Applications', ...(i.unsigned || !i.installerIdentity ? [] : ['--sign', i.installerIdentity]), i.pkg],
pkgCheck: ['pkgutil', '--check-signature', i.pkg],