fix(ios): enable community builds — dynamic DEVELOPMENT_TEAM, APP_GROUP_IDENTIFIER, and stripped dev entitlements - #7641
Conversation
Greptile SummaryThis PR makes the Omi iOS dev flavor buildable by any community developer with a paid Apple account by dynamically resolving the signing team ID, app group identifier, and bundle identifier at build time — and stripping capabilities from dev entitlements that only BasedHardware can provision.
Confidence Score: 3/5The production entitlement gap in Runner.entitlements and BatteryWidget.entitlements is a concrete present defect on the production build path; a one-line default in Base.xcconfig would close it. The production entitlement gap means any CI build that does not run setup.sh ios will embed an empty app group string, either breaking code-signing or silently making the battery widget unusable. The setup.sh fallback and validation issues are real but affect the developer setup experience rather than the shipped binary. Runner.entitlements and BatteryWidget.entitlements both reference an undefined variable for prod builds; setup.sh multi-account fallback and unvalidated interactive team ID prompt also deserve a closer look. Important Files Changed
Sequence DiagramsequenceDiagram
participant Dev as Developer
participant setup as setup.sh
participant xcconfig as Custom.xcconfig
participant xcode as Xcode Build
participant plist as Info.plist
participant swift as Swift Runtime
Dev->>setup: bash setup.sh ios
setup->>setup: generate_device_suffix() hostname
setup->>setup: detect_apple_team_id()
Note over setup: 1. APPLE_DEVELOPMENT_TEAM env var 2. Scan provisioning profiles 3. Keychain cert fallback 4. Interactive prompt
setup->>xcconfig: APP_BUNDLE_IDENTIFIER
setup->>xcconfig: APP_GROUP_IDENTIFIER
setup->>xcconfig: DEVELOPMENT_TEAM
Dev->>xcode: flutter run --flavor dev
xcode->>xcconfig: reads devDebug.xcconfig and Custom.xcconfig
xcode->>xcode: expands DEVELOPMENT_TEAM in project.pbxproj
xcode->>xcode: expands APP_GROUP_IDENTIFIER in entitlements
xcode->>plist: writes AppGroupIdentifier value
xcode->>swift: App launches
swift->>plist: Bundle.main.object forInfoDictionaryKey AppGroupIdentifier
plist-->>swift: resolved group identifier
swift->>swift: UserDefaults suiteName groupId
Reviews (1): Last reviewed commit: "fix(ios): strip unprovisionable capabili..." | Re-trigger Greptile |
| <key>CFBundleVersion</key> | ||
| <string>1.0</string> | ||
| <key>MinimumOSVersion</key> | ||
| <string>13.0</string> | ||
| </dict> |
There was a problem hiding this comment.
PR description contradicts the actual diff
The PR description states AppFrameworkInfo.plist: "add missing MinimumOSVersion key", but the diff removes the MinimumOSVersion / 13.0 entry. Flutter's toolchain regenerates this file on every build from its own SDK templates, so the runtime impact is minimal — but the description is inverted, which makes the intent hard to review.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| # 3. Fallback: grab first team ID that has a valid signing cert in the keychain | ||
| if [ -z "$team_id" ] && [ -d "$profiles_dir" ]; then | ||
| while IFS= read -r -d '' profile; do | ||
| local plist candidate | ||
| plist=$(security cms -D -i "$profile" 2>/dev/null) || continue | ||
| candidate=$(echo "$plist" | xmllint --xpath \ | ||
| "string(//key[text()='TeamIdentifier']/following-sibling::array[1]/string[1])" \ | ||
| - 2>/dev/null) | ||
| if [ -n "$candidate" ]; then | ||
| if security find-identity -v -p codesigning 2>/dev/null | grep -q "$candidate"; then | ||
| team_id="$candidate" | ||
| break | ||
| fi | ||
| fi | ||
| done < <(find "$profiles_dir" -name '*.mobileprovision' -print0 2>/dev/null) | ||
| fi |
There was a problem hiding this comment.
Step 3 fallback silently picks the wrong team on multi-account machines
When no provisioning profile matches the machine's bundle pattern (step 2), the code falls back to the first team ID that has a codesigning cert in the keychain — regardless of which account the developer intends to use. Developers with two or more Apple accounts (e.g., personal + employer) will have multiple valid certs, and the script will silently select one based on directory enumeration order. There is no disambiguation prompt before the wrong team ID is written to Custom.xcconfig.
| # 4. Last resort: prompt the user | ||
| if [ -z "$team_id" ]; then | ||
| echo "⚠️ Could not auto-detect your Apple Development Team ID." >&2 | ||
| echo " Find it at: https://developer.apple.com/account -> Membership" >&2 | ||
| echo " or run: APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX bash setup.sh ios" >&2 | ||
| read -rp " Enter your Team ID (10 characters): " team_id | ||
| team_id=$(echo "${team_id}" | tr '[:lower:]' '[:upper:]' | tr -d ' ') | ||
| fi |
There was a problem hiding this comment.
Interactive team ID fallback has no format validation
The prompt says "10 characters" but team_id is accepted as entered (after uppercasing and stripping spaces). An Apple Team ID must be exactly 10 uppercase alphanumeric characters. Entering fewer characters, a non-alphanumeric value, or an empty string would write a malformed value to Custom.xcconfig, producing confusing Xcode errors instead of a clear failure at setup time.
| <key>AppGroupIdentifier</key> | ||
| <string>$(APP_GROUP_IDENTIFIER)</string> | ||
| </dict> | ||
| </plist> No newline at end of file |
There was a problem hiding this comment.
The file is missing a trailing newline after the closing
</plist> tag. This can cause noisy diffs and some plist tooling warnings.
| <key>AppGroupIdentifier</key> | |
| <string>$(APP_GROUP_IDENTIFIER)</string> | |
| </dict> | |
| </plist> | |
| <key>AppGroupIdentifier</key> | |
| <string>$(APP_GROUP_IDENTIFIER)</string> | |
| </dict> | |
| </plist> |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
d89c131 to
b246990
Compare
|
Thanks for this @formed2forge — really solid work on the core problem here. Enabling community iOS builds by replacing hardcoded team IDs and entitlements with xcconfig-driven variables is the right architecture, and the A few things to address before this is ready: 1. Scope — this PR mixes two concerns. Commits 1-5 implement the community-build fix (dynamic 2. Custom.xcconfig deletion needs maintainer confirmation. This file was previously tracked with 3. The entitlement stripping is the right call. Push notifications, associated domains, HotspotConfiguration, and wifi-info are genuinely unprovisionable for community developers. Keeping them in prod-flavor entitlements while stripping from dev is exactly the right boundary. 4. detect_apple_team_id() feedback. The Greptile bot already flagged the multi-account disambiguation and input validation — good to see commit 5. AppFrameworkInfo.plist. The PR description mentions adding a Looking forward to v2 once these are addressed. The community-build direction is valuable and this is a strong foundation. |
|
Thanks for this work, @formed2forge. Building on @Git-on-my-level's June 26 review, I did a focused pass on the signing/config mechanics. Verified: GOOGLE_REVERSE_CLIENT_ID is still covered. Runtime app-group resolution is well done. Status: the head commit (4ea1709) hasn't changed since June 10, so the scope-split request is still pending. Splitting the personal-configs overlay ( Minor: Leaving for human maintainer review — the scope-split decision and by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
…the app delegate Second half of the UIScene migration for BasedHardware#11568. Under the UIScene lifecycle the app delegate owns no window at launch, so every binaryMessenger derived from window?.rootViewController is nil in didFinishLaunching — and registering plugins against the app delegate as the registry is itself fatal. Measured on iPhone 17 Pro / iOS 27.0 with a file-backed probe: the log line before GeneratedPluginRegistrant.register(with: self) wrote, the line after it never did, and the process died. That is why this is not merely a nil-messenger problem. Moves into didInitializeImplicitFlutterEngine(_:), sourcing the registry from engineBridge.pluginRegistry and the messenger from engineBridge.applicationRegistrar.messenger(): - GeneratedPluginRegistrant registration - OmiPhoneCallsPlugin (now via registry.registrar(forPlugin:)) - Watch/WatchRecorder, BLE, Ray-Ban Meta and phone-mic Pigeon APIs - notifyOnKill, apple_reminders, apple_health, speech, environment, audioSession, battery_widget channels and WifiNetworkPlugin No window?.rootViewController reference remains in the file, and all nine force unwraps of it are gone — each was an independent crash. Left alone deliberately: SwiftFlutterForegroundTaskPlugin's registrant callback and the top-level registerPlugins(registry:), which both receive a registry from their caller and are already scene-independent. Incidental fix: a launch carrying a deep link previously hit an early `return true` in didFinishLaunching and skipped ALL channel setup. Registration no longer shares a code path with link handling. Still outstanding for BasedHardware#11568, not in this commit: applicationWillEnterForeground never fires under scenes (measured, 0 of 4 foregrounds, while the notification fires 4 of 4), so OmiBleManager.reconnectStalePeripherals() silently stops running. That is a behavioural change and deserves its own commit. Verification: `flutter build ios --flavor dev --debug` compiles with zero Swift diagnostics. It cannot be signed on this branch — upstream/main hardcodes team 9536L8KLMP and unsuffixed bundle ids, which is what BasedHardware#7641 fixes — so runtime verification is done with those signing changes applied on top. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
|
Pushed a reconciliation commit: this PR's branch had diverged from ongoing local work on the same fix (certificate-fingerprint-aware team detection instead of a name-substring match, non-interactive-TTY handling so a CI/automation run fails fast instead of hanging on Also added Verified end-to-end this session (gold-standard local-dev setup investigation, see #11730/#11652/#11782): |
…the app delegate Second half of the UIScene migration for BasedHardware#11568. Under the UIScene lifecycle the app delegate owns no window at launch, so every binaryMessenger derived from window?.rootViewController is nil in didFinishLaunching — and registering plugins against the app delegate as the registry is itself fatal. Measured on iPhone 17 Pro / iOS 27.0 with a file-backed probe: the log line before GeneratedPluginRegistrant.register(with: self) wrote, the line after it never did, and the process died. That is why this is not merely a nil-messenger problem. Moves into didInitializeImplicitFlutterEngine(_:), sourcing the registry from engineBridge.pluginRegistry and the messenger from engineBridge.applicationRegistrar.messenger(): - GeneratedPluginRegistrant registration - OmiPhoneCallsPlugin (now via registry.registrar(forPlugin:)) - Watch/WatchRecorder, BLE, Ray-Ban Meta and phone-mic Pigeon APIs - notifyOnKill, apple_reminders, apple_health, speech, environment, audioSession, battery_widget channels and WifiNetworkPlugin No window?.rootViewController reference remains in the file, and all nine force unwraps of it are gone — each was an independent crash. Left alone deliberately: SwiftFlutterForegroundTaskPlugin's registrant callback and the top-level registerPlugins(registry:), which both receive a registry from their caller and are already scene-independent. Incidental fix: a launch carrying a deep link previously hit an early `return true` in didFinishLaunching and skipped ALL channel setup. Registration no longer shares a code path with link handling. Still outstanding for BasedHardware#11568, not in this commit: applicationWillEnterForeground never fires under scenes (measured, 0 of 4 foregrounds, while the notification fires 4 of 4), so OmiBleManager.reconnectStalePeripherals() silently stops running. That is a behavioural change and deserves its own commit. Verification: `flutter build ios --flavor dev --debug` compiles with zero Swift diagnostics. It cannot be signed on this branch — upstream/main hardcodes team 9536L8KLMP and unsuffixed bundle ids, which is what BasedHardware#7641 fixes — so runtime verification is done with those signing changes applied on top. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
|
@Git-on-my-level — split done. Pushed a commit removing the personal-configs overlay ( This PR is now scoped to just the community-build signing fix — dynamic On your other two points from the June 26 review:
|
Addresses review feedback on BasedHardware#11793: - check_ios_prerequisites() silently passed when xcodebuild is on PATH but unusable (license not accepted, components missing) — xcodebuild -version then prints nothing matching, the version guard short-circuits, and a broken Xcode install sailed through the exact gate meant to catch it. Named explicitly now, with a remedy. - select_ios_device()'s comment cited detect_apple_team_id as precedent; that function doesn't exist in setup.sh on main (it's from the separate, unmerged BasedHardware#7641). Removed the stale reference. - The CocoaPods outdated-version remedy always said `sudo gem install cocoapods`, wrong for a Homebrew-installed CocoaPods (gem-installing over a brew-managed one doesn't actually update what's on PATH). Now names both. New test confirms the broken-Xcode gap was real: fails (rc=0, silent pass) against the pre-fix check_ios_prerequisites(), passes against the fix. Failure-Class: none
…eam (#11817) A contributor with no Apple developer account couldn't run the app on the iOS simulator, even though the simulator is normally the account-free on-ramp — the watch companion target requires a team even for a simulator destination: Error (Xcode): No Account for Team "9536L8KLMP". Add a new account... Error (Xcode): No profiles for '...development.watchapp' were found Root-caused with `-showBuildSettings` and the real build log's tool invocations, not guessed. Two hypotheses were tested and ruled out first: - A missing watchOS simulator runtime looked promising (this machine had none installed) but was disproven with a clean rebuild after installing the runtime and pairing a watch simulator to the test device — the watch target's resolved SDKROOT was still the *device* WatchOS SDK, confirmed both via `-showBuildSettings` and the `ExecuteExternalTool ... -isysroot .../WatchOS.platform/...` lines in the actual build log. - An `Base.xcconfig`-level `CODE_SIGNING_ALLOWED[sdk=*simulator*] = NO` override (an earlier attempt) never matched anything, for the same reason: the watch target's own resolved SDK isn't a simulator SDK even when the overall scheme destination is a simulator, so an SDK-qualified condition can't distinguish this build from a genuine device build. Since Runner and the widget already build for the simulator without a signing identity, and the watch companion target is `SKIP_INSTALL = YES` (it is only ever embedded in Runner.app, never independently installed), disabling signing specifically for its dev-flavor configurations is safe: it does not touch Runner's or the widget's signing at all, and does not affect prod/beta, where the watch app may still need real signing for distribution. Verified live and reproducibly: a fully clean `xcodebuild` (cleared DerivedData) using the exact invocation `flutter run` produces succeeded, and the real `flutter run --flavor dev -d <simulator>` reached `Launching lib/main.dart on iPhone 17 Pro in debug mode...` with the same team (9536L8KLMP) this machine has no account for. No automated test: this is an Xcode project-settings fix with no Linux-CI-reachable seam (`Dart Analyze & Tests` runs on Ubuntu, where Xcode does not exist), consistent with how #7641's signing changes were verified — manually, on macOS, with a real build. Failure-Class: none Fixes #11776.
…1793) * fix(app): pin an iOS build destination and validate prerequisites Two related gaps in setup.sh, both hit before any app code matters: 1. run_build_ios() never passed -d to `flutter run`, so Flutter picked a destination itself. On a machine with no iOS simulator runtime installed and only a wirelessly-paired phone visible, it silently built for macOS desktop instead — after a full pod install --repo-update and build_runner pass — and failed with an unrelated "No macOS desktop project configured" error (#11775). 2. setup.sh prints a prerequisite list (Xcode v16.4, CocoaPods v1.16.2, Flutter v3.44.5) but validated almost none of it — one `command -v` check in the whole script. A missing or outdated tool surfaced as a confusing downstream failure instead of a named error, unlike the dev harness's own `Cannot start; missing prerequisites:` pattern, which names each gap with a remedy. select_ios_device() enumerates iOS-platform destinations from `flutter devices --machine`, returns the one candidate directly, prompts when there are several (failing fast without a TTY, same reasoning as detect_apple_team_id's prompt), and fails with a named error instead of a silent fallback when there are none. check_ios_prerequisites() validates Flutter/Xcode/CocoaPods/jq against the versions the script already documents, listing every gap at once with its remedy. Verified live, not just in the new shell tests: ran the real `bash setup.sh ios` three ways — non-interactively with two real devices connected (correctly failed fast rather than hang), through a real pty feeding the interactive prompt (correctly built and reached `Launching lib/main.dart on iPhone 17 Pro`, the device actually chosen), and with a stubbed toolchain reporting only macOS as a destination (correctly failed with the named "no iOS device or simulator found" error instead of the original silent-fallback bug). Failure-Class: none Fixes #11775. * fix(app): close review gaps in the iOS prerequisite check Addresses review feedback on #11793: - check_ios_prerequisites() silently passed when xcodebuild is on PATH but unusable (license not accepted, components missing) — xcodebuild -version then prints nothing matching, the version guard short-circuits, and a broken Xcode install sailed through the exact gate meant to catch it. Named explicitly now, with a remedy. - select_ios_device()'s comment cited detect_apple_team_id as precedent; that function doesn't exist in setup.sh on main (it's from the separate, unmerged #7641). Removed the stale reference. - The CocoaPods outdated-version remedy always said `sudo gem install cocoapods`, wrong for a Homebrew-installed CocoaPods (gem-installing over a brew-managed one doesn't actually update what's on PATH). Now names both. New test confirms the broken-Xcode gap was real: fails (rc=0, silent pass) against the pre-fix check_ios_prerequisites(), passes against the fix. Failure-Class: none
…11789) Introduces a .personal_configs/ convention at the repo root for contributors to store machine-local Firebase credentials and dev env config without committing them. Run app/setup-personal.sh after setup.sh to copy them into place. Split out of #7641 at maintainer request (community-build signing fix and this overlay are unrelated concerns and easier to review apart) — carries the same content as that PR's commits 1295dc3/b246990fa9, rebased onto current main. Failure-Class: none
|
Scope split done (personal-configs moved to #11789), verified on real hardware. Ready for maintainer sign-off. |
|
All CI checks pass, scope split done (personal configs moved to a separate PR), and the change has been hardware-verified. Pinging for maintainer sign-off. |
…the app delegate Second half of the UIScene migration for BasedHardware#11568. Under the UIScene lifecycle the app delegate owns no window at launch, so every binaryMessenger derived from window?.rootViewController is nil in didFinishLaunching — and registering plugins against the app delegate as the registry is itself fatal. Measured on iPhone 17 Pro / iOS 27.0 with a file-backed probe: the log line before GeneratedPluginRegistrant.register(with: self) wrote, the line after it never did, and the process died. That is why this is not merely a nil-messenger problem. Moves into didInitializeImplicitFlutterEngine(_:), sourcing the registry from engineBridge.pluginRegistry and the messenger from engineBridge.applicationRegistrar.messenger(): - GeneratedPluginRegistrant registration - OmiPhoneCallsPlugin (now via registry.registrar(forPlugin:)) - Watch/WatchRecorder, BLE, Ray-Ban Meta and phone-mic Pigeon APIs - notifyOnKill, apple_reminders, apple_health, speech, environment, audioSession, battery_widget channels and WifiNetworkPlugin No window?.rootViewController reference remains in the file, and all nine force unwraps of it are gone — each was an independent crash. Left alone deliberately: SwiftFlutterForegroundTaskPlugin's registrant callback and the top-level registerPlugins(registry:), which both receive a registry from their caller and are already scene-independent. Incidental fix: a launch carrying a deep link previously hit an early `return true` in didFinishLaunching and skipped ALL channel setup. Registration no longer shares a code path with link handling. Still outstanding for BasedHardware#11568, not in this commit: applicationWillEnterForeground never fires under scenes (measured, 0 of 4 foregrounds, while the notification fires 4 of 4), so OmiBleManager.reconnectStalePeripherals() silently stops running. That is a behavioural change and deserves its own commit. Verification: `flutter build ios --flavor dev --debug` compiles with zero Swift diagnostics. It cannot be signed on this branch — upstream/main hardcodes team 9536L8KLMP and unsuffixed bundle ids, which is what BasedHardware#7641 fixes — so runtime verification is done with those signing changes applied on top. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
a47ca67 to
1a037ae
Compare
|
Follow-up review on the new head (1a037ae), covering the outstanding items from the June 26 maintainer review and the earlier automated pass, plus what's failing CI. Prior asks — resolved. The scope split is done: the personal-configs overlay is gone from the diff (now #11789) and what remains is a cohesive iOS community-build change. The Verified on this head:
The failing Once that lands, this looks ready to come out of draft. Leaving the final sign-off to @Git-on-my-level — this changes repo-wide iOS signing defaults (team resolution, production entitlement resolution, untracking by AI on behalf of David |
…the app delegate Second half of the UIScene migration for BasedHardware#11568. Under the UIScene lifecycle the app delegate owns no window at launch, so every binaryMessenger derived from window?.rootViewController is nil in didFinishLaunching — and registering plugins against the app delegate as the registry is itself fatal. Measured on iPhone 17 Pro / iOS 27.0 with a file-backed probe: the log line before GeneratedPluginRegistrant.register(with: self) wrote, the line after it never did, and the process died. That is why this is not merely a nil-messenger problem. Moves into didInitializeImplicitFlutterEngine(_:), sourcing the registry from engineBridge.pluginRegistry and the messenger from engineBridge.applicationRegistrar.messenger(): - GeneratedPluginRegistrant registration - OmiPhoneCallsPlugin (now via registry.registrar(forPlugin:)) - Watch/WatchRecorder, BLE, Ray-Ban Meta and phone-mic Pigeon APIs - notifyOnKill, apple_reminders, apple_health, speech, environment, audioSession, battery_widget channels and WifiNetworkPlugin No window?.rootViewController reference remains in the file, and all nine force unwraps of it are gone — each was an independent crash. Left alone deliberately: SwiftFlutterForegroundTaskPlugin's registrant callback and the top-level registerPlugins(registry:), which both receive a registry from their caller and are already scene-independent. Incidental fix: a launch carrying a deep link previously hit an early `return true` in didFinishLaunching and skipped ALL channel setup. Registration no longer shares a code path with link handling. Still outstanding for BasedHardware#11568, not in this commit: applicationWillEnterForeground never fires under scenes (measured, 0 of 4 foregrounds, while the notification fires 4 of 4), so OmiBleManager.reconnectStalePeripherals() silently stops running. That is a behavioural change and deserves its own commit. Verification: `flutter build ios --flavor dev --debug` compiles with zero Swift diagnostics. It cannot be signed on this branch — upstream/main hardcodes team 9536L8KLMP and unsuffixed bundle ids, which is what BasedHardware#7641 fixes — so runtime verification is done with those signing changes applied on top. Failure-Class: none Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
…up, dev entitlements
Community developers cannot build the iOS app: signing credentials are
hardcoded to BasedHardware's Apple team, and the dev-flavor entitlements
request capabilities a personal team cannot provision.
Three blockers, three fixes:
1. DEVELOPMENT_TEAM. setup.sh gains detect_apple_team_id(), which resolves the
team from APPLE_DEVELOPMENT_TEAM, then a provisioning profile matching this
machine's bundle ID, then any profile with a valid signing cert in the
keychain, then an interactive prompt. The resolved value is written to
Custom.xcconfig, and the nine dev-flavor build configurations in
project.pbxproj read $(DEVELOPMENT_TEAM) instead of a literal team.
Prod/beta/raybanDat configurations are deliberately left on the literal team
— community builders cannot sign those anyway.
2. App group. The widget shares state with the app through an app group whose
name must track the (per-machine suffixed) bundle ID. Base.xcconfig carries
the unsuffixed default; setup.sh appends a suffixed APP_GROUP_IDENTIFIER for
dev builds only, so prod/beta keep today's literal group. The Runner and
BatteryWidget entitlements, both Info.plists, SharedDefaults.swift, and
AppDelegate.swift all read it indirectly, each with a fallback to the
original literal so a build without setup.sh still works.
3. Dev entitlements. RunnerDebug/Profile/Release-dev drop aps-environment,
associated-domains, HotspotConfiguration, and wifi-info — the capabilities a
free or personal Apple team cannot provision.
project.pbxproj is edited surgically: exactly nine DEVELOPMENT_TEAM lines
change and nothing else. Patching or 3-way merging this file does not work —
Xcode regenerates object IDs, so a merge silently adopts one whole side and
reverts unrelated upstream additions. An earlier attempt at this change did
exactly that, dropping ~670 lines of upstream Swift sources.
Verification (run locally on macOS 27, Aug 11 2026):
- detect_apple_team_id: APPLE_DEVELOPMENT_TEAM override returns the given team;
with no override and no discoverable profiles it fails fast rather than
hanging (see the following commit's regression test).
- End-to-end generate_ios_custom_config with a stub GoogleService-Info.plist:
dev -> APP_BUNDLE_IDENTIFIER=...ios12-mycomputer
APP_GROUP_IDENTIFIER=group....ios12-mycomputer
DEVELOPMENT_TEAM=98SC8JJDRG
beta -> APP_BUNDLE_IDENTIFIER=...ios12.beta, no APP_GROUP_IDENTIFIER line,
so it inherits the unsuffixed group from Base.xcconfig
- plutil -lint passes on project.pbxproj and all six touched plists/entitlements.
- bash -n passes on setup.sh.
- pbxproj audit: 9 dev-flavor configs now dynamic, 24 non-dev still literal,
9 + 24 == 33 == the pre-change count, so no config was added or lost.
Not exercised: a full `flutter build ios`. This machine has no provisioning
profiles and no Apple team configured, so signing cannot be attempted here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
detect_apple_team_id() ends in an interactive prompt. In any non-interactive context — CI, nested automation, a script whose stdin is an open-but-idle pipe — `read` never sees EOF, so setup.sh blocked forever at the prompt instead of failing with a usable message. Reproduced on this machine, which has zero provisioning profiles and therefore always reaches that branch: the call sat past a 120s deadline with no output. Both `read` sites now require a TTY. Without one, the function prints what to set (APPLE_DEVELOPMENT_TEAM) and returns non-zero, so setup.sh fails fast under its own `set -e`. Adds app/test/shell/ as the home for hermetic shell tests of setup.sh helpers, discovered by app/test.sh (which mobile-app-checks.yml already runs, so these execute in PR CI). The test drives the real function through two seams — $HOME, which is where the profile scan looks, and stdin — rather than asserting on source text, so it is behavioral coverage and not a static tripwire. It carries its own deadline instead of depending on GNU timeout(1) being installed. Verification: - With the guard removed, the test reports "hung waiting for input with no TTY" and exits 1. With the guard, both cases pass. Re-ran after restoring to confirm setup.sh was left byte-identical. - bash -n passes on the test, setup.sh, and test.sh. - The discovery loop in test.sh finds and runs the test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`detect_apple_team_id`'s fallback decided which teams to offer by collecting every
team named anywhere in `security find-identity -v -p codesigning` output. That is
wrong in both directions.
It accepted teams that cannot build: the list includes "Developer ID Application"
certificates, which sign Mac distribution builds and cannot sign an iOS
development build. And it matched as an unanchored substring, so a team ID
composed only of hex characters could match inside the 40-char SHA-1 that begins
every identity line.
More importantly, a certificate's common name is not authoritative about its
team. Apple keeps the original personal-team identifier in the common name when a
developer joins a paid team, so a certificate reading
"Apple Development: NAME (PERSONALTEAM)" can be issued under a different team
entirely — the real one is the OU, readable only by decoding the certificate.
Observed on the machine this was developed on: an identity whose common name says
(LW4P2T66Q4) but whose OU is 98SC8JJDRG, and which signs successfully for
98SC8JJDRG. A name-based check rejects exactly the team such a developer can use.
The fallback now asks the question Xcode asks when it picks a profile: does the
profile embed a certificate whose private key is on this machine? Fingerprints of
held iOS development identities are compared against the SHA-1 of each embedded
certificate, extracted with `plutil -extract DeveloperCertificates.<n> raw` and
`openssl x509`. The profile's own TeamIdentifier is authoritative for the team, so
that is what gets offered, and no inference is made from certificate names. The
identity list is still filtered to "Apple Development" and the legacy
"iPhone Developer" spelling, so a machine holding only a Mac Developer ID
certificate offers nothing.
Verified against the real profiles on this machine: the embedded certificates of
the installed profile have fingerprints 18309B14…, DBF7EBAE… and 8E1514B1…, of
which DBF7EBAE… and 8E1514B1… are held, so team 98SC8JJDRG is correctly offered.
Detection returns 98SC8JJDRG both through step 2's bundle match and — with the
bundle pattern forced not to match, so step 3 is reached — through this fallback.
Tests (app/test/shell/detect_apple_team_id_test.sh) generate real certificates
with openssl so a profile's embedded certificate and the stubbed identity list
agree on a genuine fingerprint:
- the profile's team is offered when we hold its embedded certificate, even though
that certificate's common name names a different team
- a team whose embedded certificate we do not hold is not offered
- a machine holding only a Developer ID Application certificate offers nothing
Verification (macOS 27.0, 2026-08-13): `bash -n` clean; all 5 cases in the file
pass. Against the previous name-matching implementation, case 1 fails
("expected PAIDTEAM01 …, got ''") — the regression this fixes. Cases 2 and 3 pass
either way; they guard this implementation against being too permissive rather
than covering the old defect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
Removing the hardcoded GOOGLE_REVERSE_CLIENT_ID from devDebug.xcconfig left
community builds with no value at all. Runner/Info.plist emits it as a
CFBundleURLSchemes entry, so the build shipped an empty URL scheme and the Google
Sign-In redirect had nowhere to land.
The chain failed in two places at once:
setup/prebuilt/GoogleService-Info-Local.plist no REVERSED_CLIENT_ID key
generate_ios_custom_config.sh extracted empty, wrote
"GOOGLE_REVERSE_CLIENT_ID="
Base.xcconfig no default to fall back to
devDebug.xcconfig includes Custom.xcconfig LAST,
so the empty value won
Runner/Info.plist:88 <string>$(GOOGLE_REVERSE_CLIENT_ID)</string>
Both halves are fixed, because either alone is insufficient: Base.xcconfig now
carries the default the June 26 review asked for, and the generator no longer
writes the key when the plist has no value — an empty assignment in
Custom.xcconfig overrides the default rather than deferring to it, since that
file is included afterwards. A plist that does carry REVERSED_CLIENT_ID still
overrides, so nobody silently builds against the checked-in fallback.
How this was missed twice: the PR review on Jun 26 asked to "verify
GOOGLE_REVERSE_CLIENT_ID has an equivalent fallback in Base.xcconfig", and both a
maintainer review comment (Aug 12) and my own earlier answer concluded it was
covered because the generator still writes the key. It does — it just writes
nothing useful for the community Firebase config. Running setup.sh's real config
path surfaced it immediately: the generated Custom.xcconfig read
"GOOGLE_REVERSE_CLIENT_ID=" with nothing after the equals sign. Asserting the
mechanism is not the same as asserting the value.
Tests (app/test/shell/google_reverse_client_id_test.sh) resolve the key the way
Xcode does — later includes win — and assert the resolved value, not the
mechanism:
- the community config resolves to a non-empty value
- the generator writes no empty assignment for a plist without the key
- a plist carrying the key still overrides the default
- Base.xcconfig carries a default at all
Verification (macOS 27.0, 2026-08-13): bash -n clean on the generator; all 4
cases pass. Reverting either half fails 2 cases — removing the Base default
fails "resolved to EMPTY" and "no default to fall back to"; restoring the
unconditional write fails "resolved to EMPTY" and "wrote an empty assignment".
Also confirmed by hand that a plist carrying REVERSED_CLIENT_ID produces that
value in Custom.xcconfig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
detect_apple_team_id's real certificate-matching path shells out to `plutil -extract ... raw`, which is macOS-only. CI runs shell tests on an Ubuntu runner, so every candidate certificate silently failed to decode there and the function always reported "no match" — the two sub-tests expecting rejection passed for the wrong reason (a broken matcher rejects everything), while the one sub-test expecting a real match failed, which is what surfaced this in CI. Skip the whole candidate-filtering section when plutil is unavailable instead of asserting on a matcher that can't run. Verified both paths: unchanged pass with plutil on PATH, clean skip (exit 0) with a PATH that omits it. Failure-Class: none
1a037ae to
1240a53
Compare
Problem
Community developers with a paid Apple Developer account cannot build the dev flavor of the Omi iOS app out of the box. There are three blockers:
project.pbxprojhas the BasedHardware team ID (9536L8KLMP) in all 9 dev-flavor build configurations. Xcode rejects signing for any other team.devDebug.xcconfig—APP_BUNDLE_IDENTIFIER=com.friend-app-with-wearable.ios12.developmentis set here, butCustom.xcconfig(written bysetup.sh) sets a machine-specific bundle at higher priority. The stale duplicate indevDebug.xcconfigcauses confusion and overrides the dynamic value in some tool flows.RunnerDebug/Profile/Release-dev.entitlementsrequest push notifications (aps-environment), associated domains (h.omi.me,try.omi.me), HotspotConfiguration, and Wi-Fi info. Community developers cannot provision these against their own team without modifying files, causing code-signing to fail with "entitlements do not match."Solution
Commit 1 — Dynamic DEVELOPMENT_TEAM and APP_GROUP_IDENTIFIER
setup.sh: addsdetect_apple_team_id()which auto-discovers the developer's team ID from their local provisioning profiles (withAPPLE_DEVELOPMENT_TEAMenv var override and interactive fallback). Writes bothDEVELOPMENT_TEAMandAPP_GROUP_IDENTIFIER(machine-specific, hostname-derived) toCustom.xcconfig.project.pbxproj: all 9 dev-flavorDEVELOPMENT_TEAMentries changed from literal9536L8KLMPto$(DEVELOPMENT_TEAM), resolved at build time fromCustom.xcconfig.devDebug.xcconfig: removesAPP_BUNDLE_IDENTIFIERandGOOGLE_REVERSE_CLIENT_IDduplicates — both are already set correctly viaCustom.xcconfigat higher xcconfig priority.BatteryWidget.entitlements,Runner.entitlements: use$(APP_GROUP_IDENTIFIER)instead of the hardcodedgroup.com.friend-app-with-wearable.ios12.BatteryWidget-Info.plist,Runner/Info.plist: expose$(APP_GROUP_IDENTIFIER)viaAppGroupIdentifierkey so Swift code reads it at runtime.SharedDefaults.swift,AppDelegate.swift: read app group ID fromInfo.plistwith fallback to the base identifier, instead of hardcoded string.Commit 2 — Strip unprovisionable capabilities from dev-flavor entitlements
Removes from
RunnerDebug/Profile/Release-dev.entitlements:aps-environment(push notifications — requires explicit portal provisioning)com.apple.developer.associated-domains(h.omi.me / try.omi.me — requires domain ownership)com.apple.developer.networking.HotspotConfigurationcom.apple.developer.networking.wifi-infoReplaces hardcoded
group.com.friend-app-with-wearable.ios12with$(APP_GROUP_IDENTIFIER).These capabilities remain in the prod-flavor and release entitlements where BasedHardware provisions them.
Testing
Tested on macOS with a personal Apple Developer account using
bash setup.sh ios. The app builds and runs on a physical device with automatic signing, no manual Xcode project edits required.Notes
Custom.xcconfigis gitignored — it is machine-generated bysetup.shand must never be committed.APP_GROUP_IDENTIFIERis derived from the machine hostname, giving each developer a unique app group that they can provision under their own team.setup.shwill detect their team ID from their existing provisioning profiles and write it toCustom.xcconfigas before.Update
Rebased onto current
mainand reconciled with newer work on the underlying localfix/ios-community-build-rebuildbranch (certificate-fingerprint-aware team detection, non-interactive TTY handling,GOOGLE_REVERSE_CLIENT_IDfallback fix) — verified end-to-end this session on both an iOS simulator and a physical device via the gold-standard local-dev setup investigation (see #11730/#11652/#11782).Failure-Class: none