|
1
|
#!/usr/bin/env bash |
|
2
|
# build-appstore.sh |
|
3
|
# |
|
4
|
# Full macOS App Store pipeline for Siti AI: |
|
5
|
# 1. Build — cargo tauri build (universal, App Store config, datetime CFBundleVersion) |
|
6
|
# 2. Sign — xcrun productbuild (installer package signing) |
|
7
|
# 3. Upload — xcrun altool (App Store Connect submission) |
|
8
|
# |
|
9
|
# Every configurable value has a hardcoded default that can be overridden by |
|
10
|
# setting the corresponding environment variable before invoking the script. |
|
11
|
# |
|
12
|
# Usage: |
|
13
|
# ./scripts/build-appstore.sh # full pipeline |
|
14
|
# ./scripts/build-appstore.sh build # build only |
|
15
|
# ./scripts/build-appstore.sh sign # sign only (assumes .app already built) |
|
16
|
# ./scripts/build-appstore.sh upload # upload only (assumes .pkg already signed) |
|
17
|
# |
|
18
|
# make asmacos # full pipeline via Makefile |
|
19
|
# make asmacos-build |
|
20
|
# make asmacos-sign |
|
21
|
# make asmacos-upload |
|
22
|
|
|
23
|
set -euo pipefail |
|
24
|
|
|
25
|
# ── Paths ───────────────────────────────────────────────────────────────────── |
|
26
|
|
|
27
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|
28
|
APPSTORE_CONFIG="${REPO_ROOT}/src-tauri/tauri.prod-macos-appstore.conf.json" |
|
29
|
|
|
30
|
# Auto-load the untracked .env.appstore (signing identities + ASC creds) so |
|
31
|
# `make asmacos` works without the caller exporting them first. See |
|
32
|
# .env.appstore.example for the expected keys. |
|
33
|
if [[ -f "${REPO_ROOT}/.env.appstore" ]]; then |
|
34
|
set -a |
|
35
|
# shellcheck disable=SC1091 |
|
36
|
. "${REPO_ROOT}/.env.appstore" |
|
37
|
set +a |
|
38
|
fi |
|
39
|
|
|
40
|
# Xcode toolchain. App Store Connect rejects binaries built with a beta Xcode or |
|
41
|
# beta SDK, so pin a release Xcode here regardless of the global `xcode-select`. |
|
42
|
# Default: the active xcode-select if it is a release, otherwise the newest |
|
43
|
# non-beta Xcode under /Applications. |
|
44
|
# Override: DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer ./scripts/build-appstore.sh |
|
45
|
pick_release_xcode() { |
|
46
|
local active; active="$(xcode-select -p 2>/dev/null)" |
|
47
|
if [[ -n "$active" && "$active" != *[Bb]eta* ]]; then |
|
48
|
printf '%s\n' "$active"; return |
|
49
|
fi |
|
50
|
local app |
|
51
|
for app in /Applications/Xcode*.app; do |
|
52
|
case "$app" in *[Bb]eta*) continue ;; esac |
|
53
|
[[ -d "$app/Contents/Developer" ]] && { printf '%s\n' "$app/Contents/Developer"; return; } |
|
54
|
done |
|
55
|
} |
|
56
|
DEVELOPER_DIR="${DEVELOPER_DIR:-$(pick_release_xcode)}" |
|
57
|
export DEVELOPER_DIR |
|
58
|
|
|
59
|
# ── Overridable config ───────────────────────────────────────────────────────── |
|
60
|
# |
|
61
|
# Installer signing identity: SHA-1 fingerprint of the "3rd Party Mac Developer |
|
62
|
# Installer" certificate in your keychain. This is NOT the same as the app |
|
63
|
# signing identity declared in tauri.prod-macos-appstore.conf.json |
|
64
|
# (bundle.macOS.signingIdentity = D865610A9D550A397A90E07EB476118EDC48F829), |
|
65
|
# which Tauri uses to codesign the .app bundle itself. productbuild requires a |
|
66
|
# separate "3rd Party Mac Developer Installer: …" cert. Not secret — safe to |
|
67
|
# commit once you know the fingerprint. |
|
68
|
# |
|
69
|
# Override: SIGNING_IDENTITY=<fingerprint> ./scripts/build-appstore.sh |
|
70
|
SIGNING_IDENTITY="${SIGNING_IDENTITY:-}" |
|
71
|
|
|
72
|
# App Store Connect API credentials. |
|
73
|
# ASC_API_KEY — Key ID shown in App Store Connect → Users & Access → Keys. |
|
74
|
# ASC_ISSUER_ID — Issuer UUID on the same page. |
|
75
|
# The private key (.p8) must be present at: |
|
76
|
# ~/.appstoreconnect/private_keys/AuthKey_<ASC_API_KEY>.p8 |
|
77
|
ASC_API_KEY="${ASC_API_KEY:-L84N624YQH}" |
|
78
|
ASC_ISSUER_ID="${ASC_ISSUER_ID:-b4e8d369-8b7d-4538-8435-643b73237575}" |
|
79
|
|
|
80
|
# App name — must match the productName in tauri.prod-macos-appstore.conf.json |
|
81
|
# and therefore the .app bundle produced by tauri build. |
|
82
|
APP_NAME="${APP_NAME:-Siti AI}" |
|
83
|
|
|
84
|
# Derived paths — quoted to handle the space in "Siti AI". |
|
85
|
APP_PATH="${REPO_ROOT}/src-tauri/target/universal-apple-darwin/release/bundle/macos/${APP_NAME}.app" |
|
86
|
PKG_PATH="${REPO_ROOT}/${APP_NAME}.pkg" |
|
87
|
|
|
88
|
# ── Helpers ─────────────────────────────────────────────────────────────────── |
|
89
|
|
|
90
|
step() { echo ""; echo "── $* ──────────────────────────────────────────────"; } |
|
91
|
ok() { echo "✅ $*"; } |
|
92
|
fail() { printf "❌ %b\n" "$*" >&2; exit 1; } |
|
93
|
|
|
94
|
# Refuse to build with a beta toolchain — App Store Connect rejects beta-built |
|
95
|
# binaries (the cause of an "Invalid Binary" after upload). DEVELOPER_DIR is |
|
96
|
# pinned to a release Xcode above; `cargo tauri build` resolves the macOS |
|
97
|
# linker/SDK via xcrun, which honors DEVELOPER_DIR. |
|
98
|
require_release_xcode() { |
|
99
|
[[ -n "${DEVELOPER_DIR}" && -d "${DEVELOPER_DIR}" ]] \ |
|
100
|
|| fail "No release Xcode found.\n Install a release Xcode, or set DEVELOPER_DIR to its Contents/Developer dir." |
|
101
|
case "${DEVELOPER_DIR}" in |
|
102
|
*[Bb]eta*) |
|
103
|
fail "DEVELOPER_DIR points at a beta Xcode:\n ${DEVELOPER_DIR}\n App Store Connect rejects beta-built binaries. Point DEVELOPER_DIR at a release Xcode." ;; |
|
104
|
esac |
|
105
|
echo "🛠 Xcode → $(xcodebuild -version 2>/dev/null | head -1) [${DEVELOPER_DIR}]" |
|
106
|
} |
|
107
|
|
|
108
|
# A genuinely full disk during a large release build writes truncated/corrupt |
|
109
|
# proc-macro dylibs, which fail with the same "can't find crate" errors as the |
|
110
|
# beta-macOS strip bug — easy to conflate. Guard against it up front. |
|
111
|
preflight_disk() { |
|
112
|
local free_gb; free_gb=$(df -g / | awk 'NR==2 {print $4}') |
|
113
|
[[ "${free_gb:-0}" -ge 20 ]] || fail "Only ${free_gb} GB free on / — a universal release build needs ~20 GB+. Free space and retry." |
|
114
|
} |
|
115
|
|
|
116
|
# ── Steps ───────────────────────────────────────────────────────────────────── |
|
117
|
|
|
118
|
do_build() { |
|
119
|
step "1/3 Build" |
|
120
|
|
|
121
|
require_release_xcode |
|
122
|
preflight_disk |
|
123
|
[[ -f "$APPSTORE_CONFIG" ]] || fail "Config not found: $APPSTORE_CONFIG" |
|
124
|
|
|
125
|
# CFBundleVersion: YYYYMMDD.HHMM (UTC) |
|
126
|
# Two period-separated integers → valid for Apple |
|
127
|
# Monotonically increasing → satisfies App Store requirement |
|
128
|
BUNDLE_VERSION="$(date -u +"%Y%m%d.%H%M")" |
|
129
|
BUNDLE_VERSION_JSON="{\"bundle\":{\"macOS\":{\"bundleVersion\":\"${BUNDLE_VERSION}\"}}}" |
|
130
|
|
|
131
|
echo "🏷 bundleVersion → ${BUNDLE_VERSION}" |
|
132
|
echo "📦 app config → ${APPSTORE_CONFIG}" |
|
133
|
echo "" |
|
134
|
|
|
135
|
# Tauri merges multiple --config flags in order via json_patch::merge. |
|
136
|
# The inline JSON overrides only bundleVersion; all other keys come from |
|
137
|
# tauri.prod-macos-appstore.conf.json. The config file is never mutated. |
|
138
|
cargo tauri build \ |
|
139
|
--bundles app \ |
|
140
|
--target universal-apple-darwin \ |
|
141
|
--config "${APPSTORE_CONFIG}" \ |
|
142
|
--config "${BUNDLE_VERSION_JSON}" |
|
143
|
|
|
144
|
[[ -d "$APP_PATH" ]] || fail "Build succeeded but .app not found at: $APP_PATH" |
|
145
|
ok "Build complete → ${APP_PATH}" |
|
146
|
} |
|
147
|
|
|
148
|
do_sign() { |
|
149
|
step "2/3 Sign" |
|
150
|
|
|
151
|
[[ -d "$APP_PATH" ]] || fail ".app not found: ${APP_PATH}\n Run 'build' step first." |
|
152
|
|
|
153
|
if [[ -z "${SIGNING_IDENTITY}" ]]; then |
|
154
|
fail "SIGNING_IDENTITY is not set.\n Export the SHA-1 fingerprint of your '3rd Party Mac Developer Installer' certificate:\n export SIGNING_IDENTITY=<fingerprint>" |
|
155
|
fi |
|
156
|
|
|
157
|
echo "🔏 identity → ${SIGNING_IDENTITY}" |
|
158
|
echo "📂 component → ${APP_PATH}" |
|
159
|
echo "📦 output → ${PKG_PATH}" |
|
160
|
echo "" |
|
161
|
|
|
162
|
xcrun productbuild \ |
|
163
|
--sign "${SIGNING_IDENTITY}" \ |
|
164
|
--component "${APP_PATH}" /Applications \ |
|
165
|
"${PKG_PATH}" |
|
166
|
|
|
167
|
[[ -f "$PKG_PATH" ]] || fail "productbuild succeeded but .pkg not found at: $PKG_PATH" |
|
168
|
ok "Signed → ${PKG_PATH}" |
|
169
|
} |
|
170
|
|
|
171
|
do_upload() { |
|
172
|
step "3/3 Upload" |
|
173
|
|
|
174
|
[[ -f "$PKG_PATH" ]] || fail ".pkg not found: ${PKG_PATH}\n Run 'sign' step first." |
|
175
|
|
|
176
|
echo "📤 file → ${PKG_PATH}" |
|
177
|
echo "🔑 apiKey → ${ASC_API_KEY}" |
|
178
|
echo "🏢 issuer → ${ASC_ISSUER_ID}" |
|
179
|
echo "" |
|
180
|
|
|
181
|
xcrun altool \ |
|
182
|
--upload-app \ |
|
183
|
--type macos \ |
|
184
|
--file "${PKG_PATH}" \ |
|
185
|
--apiKey "${ASC_API_KEY}" \ |
|
186
|
--apiIssuer "${ASC_ISSUER_ID}" |
|
187
|
|
|
188
|
ok "Uploaded to App Store Connect" |
|
189
|
} |
|
190
|
|
|
191
|
# ── Entry point ─────────────────────────────────────────────────────────────── |
|
192
|
|
|
193
|
STEP="${1:-all}" |
|
194
|
|
|
195
|
case "$STEP" in |
|
196
|
build) do_build ;; |
|
197
|
sign) do_sign ;; |
|
198
|
upload) do_upload ;; |
|
199
|
all) |
|
200
|
do_build |
|
201
|
do_sign |
|
202
|
do_upload |
|
203
|
echo "" |
|
204
|
echo "🎉 Pipeline complete: built, signed, and uploaded ${APP_NAME}." |
|
205
|
;; |
|
206
|
*) |
|
207
|
echo "Usage: $0 [build|sign|upload|all]" >&2 |
|
208
|
exit 1 |
|
209
|
;; |
|
210
|
esac |