Skip to content

Commit 9768ba5

Browse files
authored
fix(ios): raise the deployment target to iOS 15.0 (#1731)
* fix(ios): raise the deployment target to iOS 15.0 Nothing set an iOS deployment target, so the Tauri CLI's own default of 14.0 applied silently and App Store Connect answered the 2.5.0 build 7 upload with ITMS-90068. Apple refuses anything below 15.0 from Spring 2027. CI now asserts the configured value against that floor, against the generated project.yml, and against the exported IPA, since the warning only arrives after a successful upload. * Address review feedback - Replace `sort -V` in the deployment-target check with a POSIX awk comparator. The step runs on macOS, whose sort is BSD, and a `-V` that errored would fail the step with the "below Apple's floor" message no matter what the config said. - Point the local verification at `project.yml` and the built app instead of the generated source Info.plist. Xcode writes `MinimumOSVersion` from the build setting, so it never appears in the source plist. - Give the regeneration commands an explicit `cd apps/geolibre-desktop` and spell out the config path, since the new section precedes the one that establishes that working directory. - Correct the `Info.ios.plist` merge timing: `ios init` applies `tauri.ios.conf.json`, `ios build`/`dev` merges the plist. - Use the American spelling "afterward". * Address Claude review feedback Scope the project.yml parse to the `deploymentTarget:` block, resetting at the first line that dedents out of it. The previous version latched onto the first `iOS:` line anywhere after the block opened, so a future template that carried such a key under `targets:` would have been read as the deployment target. * Address CodeRabbit review feedback The local verification recipe read MinimumOSVersion out of an xcarchive that nothing in the recipe produced. Add the `tauri ios build --no-sign` that creates it. * Address Claude review feedback Give the missing-key case its own actionable message. `jq -e` aborted the step with a bare "Error: null (null)" if the config key were ever removed or renamed, unlike every other failure path here.
1 parent 863a4aa commit 9768ba5

3 files changed

Lines changed: 144 additions & 7 deletions

File tree

.github/workflows/ios.yml

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,78 @@ jobs:
143143
# upload rather than at build time.
144144
run: ./scripts/sync-ios-app-icon.sh
145145

146+
- name: Verify the iOS deployment target
147+
# Apple's floor, not ours: from Spring 2027 App Store Connect refuses any
148+
# upload whose MinimumOSVersion is below 15.0, and today it accepts the
149+
# build and answers with a warning email instead (ITMS-90068, which is how
150+
# 2.5.0 build 7 was found to still carry 14.0). A warning that arrives
151+
# after the upload is exactly the kind of failure this job asserts away.
152+
#
153+
# The value is NOT written by anything in this repo: it comes from
154+
# `bundle.iOS.minimumSystemVersion` in tauri.ios.conf.json, which the Tauri
155+
# CLI templates into the generated project.yml as XcodeGen's
156+
# `deploymentTarget.iOS` (-> IPHONEOS_DEPLOYMENT_TARGET -> the .app's
157+
# MinimumOSVersion). Omit the key and the CLI's own default (14.0) applies
158+
# silently. So the config is read as the source of truth, checked against
159+
# Apple's floor, and then compared with what `ios init` actually
160+
# generated: a CLI that stopped honoring the key would otherwise look
161+
# identical here and only surface as another warning email.
162+
run: |
163+
set -euo pipefail
164+
conf="apps/geolibre-desktop/src-tauri/tauri.ios.conf.json"
165+
project="apps/geolibre-desktop/src-tauri/gen/apple/project.yml"
166+
167+
# `// empty` rather than `jq -e` so a removed or renamed key lands on
168+
# the message below instead of jq's bare "Error: null (null)". Invalid
169+
# JSON still fails the step through `set -e`, as it should.
170+
want="$(jq -r '.bundle.iOS.minimumSystemVersion // empty' "$conf")"
171+
if [ -z "$want" ]; then
172+
echo "::error::$conf does not set bundle.iOS.minimumSystemVersion, so the Tauri CLI's 14.0 default would apply (ITMS-90068)"
173+
exit 1
174+
fi
175+
176+
# Component-wise dotted-version compare, exiting 0 when the configured
177+
# target is at or above the floor. Deliberately not `sort -V`: this
178+
# runs on macOS, whose sort is BSD rather than GNU, and a `-V` that
179+
# errored out would fail the step with the message below no matter what
180+
# the config actually says. awk behaves the same on both.
181+
floor="15.0"
182+
if ! awk -v want="$want" -v floor="$floor" 'BEGIN {
183+
n = split(want, a, "."); m = split(floor, b, ".")
184+
for (i = 1; i <= (n > m ? n : m); i++) {
185+
if (a[i] + 0 != b[i] + 0) exit (a[i] + 0 < b[i] + 0)
186+
}
187+
exit 0
188+
}'; then
189+
echo "::error::$conf sets bundle.iOS.minimumSystemVersion to '$want', below Apple's $floor floor (ITMS-90068)"
190+
exit 1
191+
fi
192+
193+
if [ ! -f "$project" ]; then
194+
echo "::error::No generated project.yml at $project"
195+
exit 1
196+
fi
197+
# Scoped to the `deploymentTarget:` block, resetting at the first line
198+
# that dedents back out of it, so an `iOS:` key elsewhere in the file
199+
# can never be read as this one. Parsed as text rather than YAML on
200+
# purpose: a YAML loader turns `15.0` into a float, and `15.10` would
201+
# come back out as `15.1`. tr strips quoting if the template emits any.
202+
got="$(awk '
203+
/^[[:space:]]*deploymentTarget:/ { depth = match($0, /[^[:space:]]/); inblock = 1; next }
204+
!inblock || /^[[:space:]]*$/ { next }
205+
match($0, /[^[:space:]]/) <= depth { inblock = 0; next }
206+
$1 == "iOS:" { print $2; exit }
207+
' "$project" | tr -cd '0-9.')"
208+
if [ "$got" != "$want" ]; then
209+
echo "::error::Generated project.yml has deploymentTarget.iOS '$got', expected the configured '$want'. Did the Tauri CLI stop honoring bundle.iOS.minimumSystemVersion?"
210+
exit 1
211+
fi
212+
213+
echo "::notice::iOS deployment target $want"
214+
# Handed to the IPA verify step so it asserts against the configured
215+
# value rather than a second hardcoded copy of it.
216+
echo "IOS_MIN_OS=$want" >> "$GITHUB_ENV"
217+
146218
- name: Check for Apple signing secrets
147219
id: signing
148220
# `secrets` can't be used in a step-level `if:`, so presence is turned
@@ -481,7 +553,16 @@ jobs:
481553
exit 1
482554
fi
483555
484-
echo "IPA $ipa: bundle id $EXPECTED_BUNDLE_ID, version $short ($build), signed by $authority."
556+
# The deployment target was checked against the generated project.yml
557+
# before the archive; this asserts it survived into the shipped bytes,
558+
# which is the copy App Store Connect reads for ITMS-90068.
559+
minos="$(/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "$plist")"
560+
if [ "$minos" != "$IOS_MIN_OS" ]; then
561+
echo "::error::$ipa has MinimumOSVersion '$minos', expected the configured '$IOS_MIN_OS'"
562+
exit 1
563+
fi
564+
565+
echo "IPA $ipa: bundle id $EXPECTED_BUNDLE_ID, version $short ($build), min iOS $minos, signed by $authority."
485566
echo "IPA_PATH=$ipa" >> "$GITHUB_ENV"
486567
487568
- name: Clean up the signing keychain

apps/geolibre-desktop/src-tauri/tauri.ios.conf.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"productName": "GeoLibre",
44
"identifier": "org.geolibre.app",
55
"bundle": {
6-
"resources": []
6+
"resources": [],
7+
"iOS": {
8+
"minimumSystemVersion": "15.0"
9+
}
710
}
811
}

docs/ios.md

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,58 @@ It covers all three location consumers: Field Collection, GPS Tracking, and the
6464
Controls → GeoLocate map control. After a build, confirm the key survived the
6565
merge (see *Build* below).
6666

67+
## Minimum iOS version
68+
69+
GeoLibre targets **iOS 15.0**, set once in
70+
`apps/geolibre-desktop/src-tauri/tauri.ios.conf.json`:
71+
72+
```json
73+
"bundle": { "iOS": { "minimumSystemVersion": "15.0" } }
74+
```
75+
76+
Tauri templates that value into the generated `gen/apple/project.yml` as
77+
XcodeGen's `deploymentTarget.iOS`, which Xcode turns into
78+
`IPHONEOS_DEPLOYMENT_TARGET` and finally the app's `MinimumOSVersion`. There is
79+
nothing to hand-edit in the generated project, and nothing in `Info.ios.plist`:
80+
Xcode writes the plist key from the build setting.
81+
82+
Leave the key out and the **Tauri CLI's own default applies**, which is `14.0`.
83+
That is below Apple's floor: starting Spring 2027, App Store Connect refuses any
84+
upload with a `MinimumOSVersion` under 15.0. Until then it accepts the build and
85+
sends a warning email afterward (`ITMS-90068`), which is how version 2.5.0
86+
build 7 was found to have shipped at 14.0.
87+
88+
Because `gen/apple` is generated, a change here only takes effect once the
89+
project is regenerated. CI regenerates on every run, so it picks the value up
90+
automatically; locally, regenerate it yourself (paths relative to
91+
`apps/geolibre-desktop`, as everywhere else in this document):
92+
93+
```bash
94+
cd apps/geolibre-desktop
95+
rm -rf src-tauri/gen/apple
96+
npx tauri ios init
97+
98+
# What init generated:
99+
grep -A2 'deploymentTarget:' src-tauri/gen/apple/project.yml
100+
101+
# What a build actually shipped. `MinimumOSVersion` exists only in the *built*
102+
# app, since Xcode writes it from the build setting; it is not in the generated
103+
# source Info.plist. So this needs an archive, which `init` alone does not
104+
# produce.
105+
npx tauri ios build --no-sign
106+
/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' \
107+
src-tauri/gen/apple/build/geolibre-desktop_iOS.xcarchive/Products/Applications/GeoLibre.app/Info.plist
108+
```
109+
110+
The CI job asserts this in two places (see *Continuous integration*): the
111+
configured value against Apple's floor and against the generated `project.yml`
112+
right after `ios init`, and then the exported `.ipa`'s `MinimumOSVersion`, which
113+
is the copy App Store Connect actually reads.
114+
115+
Raising the floor further is a product decision, not a technical one. iOS 15 is
116+
the last release for the iPhone 6s/7 and iPad Air 2 generation, so 16.0 would
117+
drop those devices in exchange for a newer WebKit baseline.
118+
67119
## Toolchain setup (one time)
68120

69121
You need a **Mac** with **Xcode** (from the App Store; open it once to accept the
@@ -143,9 +195,10 @@ npx tauri ios build --no-sign # unsigned .ipa + .xcarchive (no Apple ac
143195
> *not* appear in `security find-identity -v -p codesigning`. An empty identity
144196
> list is not evidence the signing failed — check the `.ipa` itself.
145197
146-
- `gen/apple` is generated (git-ignored) and regenerated on demand. `init` also
147-
merges `tauri.ios.conf.json` (bundle id, drops the Python backend) and
148-
`Info.ios.plist` (the location string).
198+
- `gen/apple` is generated (git-ignored) and regenerated on demand. `init`
199+
applies `tauri.ios.conf.json` (bundle id, deployment target, drops the Python
200+
backend); `Info.ios.plist` (the location string) is merged later, by
201+
`tauri ios build`/`dev`, as noted below.
149202
- The app is named **GeoLibre** on iOS (the desktop build is "GeoLibre Desktop")
150203
and uses the bundle id **`org.geolibre.app`**, both set via
151204
`src-tauri/tauri.ios.conf.json` — the same override pattern and reasoning as
@@ -204,8 +257,8 @@ not free to choose — see the Xcode floor below.
204257

205258
- **With Apple signing secrets set**, it imports the identity into a throwaway
206259
keychain, archives **unsigned**, exports a signed `.ipa` under manual signing,
207-
verifies the bundle id and the signature, and uploads it as the
208-
`geolibre-ios-ipa` artifact:
260+
verifies the bundle id, the signature, the build number shape and the
261+
`MinimumOSVersion`, and uploads it as the `geolibre-ios-ipa` artifact:
209262
- `APPLE_IOS_CERTIFICATE_BASE64``base64 -i dist.p12`
210263
- `APPLE_IOS_CERTIFICATE_PASSWORD`
211264
- `APPLE_IOS_PROVISIONING_PROFILE_BASE64``base64 -i profile.mobileprovision`

0 commit comments

Comments
 (0)