diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e67bb..2559431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased +- `apple build --platform macos`: runs `apple.macos.prebuild` (new) before `tauri build`, no longer `build.prebuild` / `apple.prebuild`, which belong to the iOS project (a Capacitor sync or OTA gate ran for a Tauri Mac build); the dry run names the skipped one. + ## 0.8.66 ยท 2026-11-03 - `apple.macos: { listing, reviewNotes, gameCenter, noIap }`: an app with iOS and macOS versions gets its own Mac listing file and review notes (`apple listing --platform macos`, `apple review-details --platform macos`, new there); `validate` warns about iOS-only claims in a macOS version's texts (Game Center, tap / swipe, iPhone / iPad, in-app purchases with `noIap`). diff --git a/cli/apple/build-mas.ts b/cli/apple/build-mas.ts index 85de5d6..6c16e59 100644 --- a/cli/apple/build-mas.ts +++ b/cli/apple/build-mas.ts @@ -162,9 +162,12 @@ export async function run(ctx: Ctx, platform: string = process.platform): Promis const overlay = tauriOverlay({ appIdentity, entitlementsFile: entFile, profile, version, build: buildNo, unsigned, minOs: tauri.minOs, category }); const input = { tauri, tauriCmd, target, out, configFile, entitlementsFile: entFile, profile, appIdentity, installerIdentity: installerIdentity ?? '', version, build: buildNo, unsigned, pathFree, pkg }; const steps = masSteps(input); - // the same hooks as apple build: build.prebuild / apple.prebuild first (a project computes its build-time env there), - // apple.macAppCheck on the built .app before productbuild (e.g. a project's own check that a value was stamped in) - const prebuild = prebuildFor(config, 'apple') ?? null; + // apple.macos.prebuild first (a project computes its build-time env there), apple.macAppCheck on the built .app before + // productbuild (e.g. a project's own check that a value was stamped in). build.prebuild / apple.prebuild belong to the + // iOS / Android project (a Capacitor sync, an OTA gate) and are not run for a Tauri Mac build + const prebuild = config.apple?.macos?.prebuild?.trim() || null; + const mobilePrebuild = prebuildFor(config, 'apple'); + if (!prebuild && mobilePrebuild && !json) log(TAG, `not run for the Mac build: ${mobilePrebuild} (build.prebuild / apple.prebuild are for the iOS project; set apple.macos.prebuild for the Mac)`); 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; // entitlements: --entitlements, apple.macos.entitlements, the merged Tauri config (the first one that is set) diff --git a/cli/lib/config.ts b/cli/lib/config.ts index 01da2ed..cd3acd1 100644 --- a/cli/lib/config.ts +++ b/cli/lib/config.ts @@ -716,8 +716,10 @@ export interface ShipConfig { * `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 ` wins over it. + * `prebuild`: shell command `apple build --platform macos` runs first in the project root (build.prebuild / + * apple.prebuild are the iOS project's and are not run for the Mac build); `--no-prebuild` skips it. */ - macos?: { listing?: string; reviewNotes?: string; gameCenter?: boolean; noIap?: boolean; entitlements?: string }; + macos?: { listing?: string; reviewNotes?: string; gameCenter?: boolean; noIap?: boolean; entitlements?: string; prebuild?: string }; /** "Header and Search Results" creative assets (`storebound apple creative-assets`, iOS / iPadOS 27+). Files under * //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 @@ -1049,9 +1051,9 @@ export function validateExtras(cfg: Partial): 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, entitlements?: "src-tauri/Entitlements.appstore.plist" }'); + 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", prebuild?: "" }'); else { - 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 ['listing', 'reviewNotes', 'entitlements', 'prebuild'] 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`); } } diff --git a/tests/mas-entitlements.test.ts b/tests/mas-entitlements.test.ts index 4e62c5d..3de48d6 100644 --- a/tests/mas-entitlements.test.ts +++ b/tests/mas-entitlements.test.ts @@ -175,6 +175,15 @@ describe('apple build --platform macos --dry', () => { expect(viaFlag.plan.entitlements).toContain('com.apple.security.files.user-selected.read-write'); expect(viaFlag.plan.entitlements).not.toContain('com.apple.security.files.downloads.read-only'); }); + test('only apple.macos.prebuild runs before the Mac build; the iOS project\'s build.prebuild is named, not run', async () => { + const mobile: ShipConfig = { ...CONFIG, build: { prebuild: 'npx cap sync' } }; + expect((await dry(mobile, files(), [])).plan.prebuild).toBeNull(); + const text = await capture(() => buildMas(project(mobile, files()).ctx(['--tauri', 'src-tauri', '--unsigned', '--build-number', '7', '--dry']), 'linux')); + expect(text.out).toContain('not run for the Mac build: npx cap sync'); + const mac: ShipConfig = { ...mobile, apple: { ...CONFIG.apple!, macos: { prebuild: 'bun run desktop:env' } } }; + expect((await dry(mac, files(), [])).plan.prebuild).toBe('bun run desktop:env'); + expect((await dry(mac, files(), ['--no-prebuild'])).plan.prebuildSkip).toBe('--no-prebuild'); + }); test('a named plist that does not exist stops the run', async () => { const p = project(CONFIG, files()); await expect(buildMas(p.ctx(['--tauri', 'src-tauri', '--unsigned', '--build-number', '7', '--dry', '--entitlements', 'nope.plist']), 'linux')).rejects.toThrow('does not exist');