Add iOS build, wireless install, and signing workflows

This commit is contained in:
2026-10-26 21:31:14 +08:00
parent 198bc560a8
commit 13fbe97126
2 changed files with 218 additions and 11 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
# TZGyn-skill # TZGyn-skill
My personal Crush skill: conventions for starting a project on Gitea, publishing My personal Crush skill: conventions for starting a project on Gitea, publishing
to the TZGyn Homebrew tap (casks for GUI apps, formulae for CLIs), and signing / to the TZGyn Homebrew tap (casks for GUI apps, formulae for CLIs), signing /
notarizing / releasing macOS apps. notarizing / releasing macOS apps, and building native SwiftUI iOS apps and
installing them over the network.
See [SKILL.md](./SKILL.md). See [SKILL.md](./SKILL.md).
+215 -9
View File
@@ -11,7 +11,7 @@ skill at `~/.agents/skills/TZGyn-skill` (a symlink to the checkout, so editing
the repo updates the live skill). the repo updates the live skill).
The reference implementations to copy from are `~/Git/kapture` and The reference implementations to copy from are `~/Git/kapture` and
`~/Git/konference` (macOS menu-bar apps). `~/Git/konference` (macOS menu-bar apps); for iOS, `~/Git/koins/apps/ios`.
## Ground rules ## Ground rules
@@ -23,11 +23,12 @@ The reference implementations to copy from are `~/Git/kapture` and
`gitea.tzgyn.dev`. SSH: `git@gitea.tzgyn.dev:TZGyn/<repo>.git`. `gitea.tzgyn.dev`. SSH: `git@gitea.tzgyn.dev:TZGyn/<repo>.git`.
- **Always print the upgrade script** (the `brew` commands below) in the final - **Always print the upgrade script** (the `brew` commands below) in the final
answer after any release or version bump. The user wants it every time. answer after any release or version bump. The user wants it every time.
- **Every GUI release must be notarized and stapled.** An unnotarized build is - **Every macOS GUI release must be notarized and stapled.** An unnotarized
not a release: never publish one, and never paper over Gatekeeper with build is not a release: never publish one, and never paper over Gatekeeper
`xattr -dr com.apple.quarantine` or `brew install --cask --no-quarantine`. with `xattr -dr com.apple.quarantine` or `brew install --cask --no-quarantine`.
If notarization fails, fix the cause (see If notarization fails, fix the cause (see
[Troubleshooting](#notarization-troubleshooting)) before publishing. [Troubleshooting](#notarization-troubleshooting)) before publishing. (iOS
apps are not notarized — Apple has no such step; see section 6.)
- Never commit build artifacts. `.gitignore` covers `.build/`, `*.app/`, - Never commit build artifacts. `.gitignore` covers `.build/`, `*.app/`,
`dist/`, `.swiftpm/`, `xcuserdata/`, `DerivedData/`, `.DS_Store`. `dist/`, `.swiftpm/`, `xcuserdata/`, `DerivedData/`, `.DS_Store`.
(`go build ./...` drops a binary next to the source — ignore it explicitly.) (`go build ./...` drops a binary next to the source — ignore it explicitly.)
@@ -242,11 +243,216 @@ xcrun notarytool history --key ... --key-id ... --issuer ... # 403 here = acco
problem is the account, not the bundle, so there is nothing to change in the problem is the account, not the bundle, so there is nothing to change in the
app or the scripts. app or the scripts.
## 6. Mobile ## 6. iOS apps
For mobile (iOS/Android), notarization does not apply; the equivalent is store Native SwiftUI iOS apps are built with **XcodeGen** — no CocoaPods, no SwiftPM
signing (App Store Connect / Play Console) or EAS. Ask for the specific stack packages. `project.yml` is the source of truth; the generated
before assuming; this skill currently only codifies the macOS path. `<Name>.xcodeproj/` is gitignored and regenerated after any file is added or
removed. Working reference: `~/Git/koins/apps/ios`.
One-time tool install: `brew install xcodegen`.
### Layout and project file
```
apps/ios/
project.yml XcodeGen project (committed)
<Name>/ Swift sources, assets, Info.plist
<Name>/Bridge/ optional ObjC/ObjC++ bridge + bridging header
<Name>/Libs/ optional prebuilt static archives (not bundled)
.gitignore <Name>.xcodeproj/, build/, DerivedData/, xcuserdata/
```
Minimal `project.yml`:
```yaml
name: <Name>
options:
bundleIdPrefix: dev.tzgyn
deploymentTarget:
iOS: "16.0"
xcodeVersion: "16.0"
settings:
base:
SWIFT_VERSION: "5.0"
ENABLE_USER_SCRIPT_SANDBOXING: NO
targets:
<Name>:
type: application
platform: iOS
sources:
- path: <Name>
excludes:
- "Libs" # static archives are linked, never bundled
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: dev.tzgyn.<name>
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: RX6767S6BW
CODE_SIGNING_ALLOWED[sdk=iphonesimulator*]: NO
SWIFT_OBJC_BRIDGING_HEADER: <Name>/Bridge/<Name>-Bridging-Header.h
LIBRARY_SEARCH_PATHS:
- "$(SRCROOT)/<Name>/Libs"
OTHER_LDFLAGS[sdk=iphoneos*]:
- "$(inherited)"
- "-l<name>_device"
OTHER_LDFLAGS[sdk=iphonesimulator*]:
- "$(inherited)"
- "-l<name>_merged"
info:
path: <Name>/Info.plist
properties:
UILaunchScreen: {}
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
UIUserInterfaceStyle: Dark
NSCameraUsageDescription: Scan a QR code.
NSFaceIDUsageDescription: Unlock the app and approve payments.
```
- Bundle id convention: `dev.tzgyn.<name>` (Koins predates it and uses
`com.koins.app`; keep an existing id rather than migrating).
- Privacy usage strings are mandatory. A missing `NSCameraUsageDescription`,
`NSPhotoLibraryUsageDescription`, or `NSFaceIDUsageDescription` crashes the
app the first time that API is used.
- Prebuilt static archives need separate device and simulator builds; select
them with `OTHER_LDFLAGS[sdk=…]` as above. If ObjC categories fail to load,
link with `-Wl,-force_load,$(SRCROOT)/<Name>/Libs/lib<name>.a` instead.
- The simulator target uses `CODE_SIGNING_ALLOWED[sdk=iphonesimulator*] = NO`;
device builds use automatic signing with the TZGyn team.
After adding/removing files or changing `project.yml`:
```bash
cd apps/ios && xcodegen generate
```
### Build
```bash
cd apps/ios
xcodegen generate
# Simulator (Apple Silicon)
xcodebuild -project <Name>.xcodeproj -scheme <Name> -configuration Debug \
-sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO -derivedDataPath /tmp/<name>-dd build
# Device
xcodebuild -project <Name>.xcodeproj -scheme <Name> -configuration Debug \
-sdk iphoneos -destination 'generic/platform=iOS' \
-allowProvisioningUpdates -derivedDataPath /tmp/<name>-dd-device build
```
`-allowProvisioningUpdates` creates/refreshes the development profile
automatically; no manual profile management is needed with the paid team.
Derived data under `/tmp` is lost on reboot — rebuild rather than chasing a
missing `.app`.
### Install on a physical iPhone (over the network, no cable)
This is the normal way to update a TZGyn iPhone app: build the `iphoneos` slice,
then install over Wi-Fi with `devicectl`. `simctl` only works with simulators.
One-time setup:
1. Connect the iPhone once with a cable, open Xcode → **Window → Devices and
Simulators**, select the phone, and tick **Connect via network**.
2. On the phone: **Settings → Privacy & Security → Developer Mode → On**
(requires a reboot), then trust the Mac when prompted.
Wireless install and launch:
```bash
# 1. Find the paired phone (UDID, ECID, name and DNS name are all accepted).
xcrun devicectl list devices
# want: <phone name> <phone>.coredevice.local <UDID> available (paired)
# 2. Install the device build. This replaces the app in place and keeps its data.
xcrun devicectl device install app --device <UDID|name> --timeout 180 \
/tmp/<name>-dd-device/Build/Products/Debug-iphoneos/<Name>.app
# 3. Launch it (the phone must be unlocked).
xcrun devicectl device process launch --device <UDID|name> <bundle-id>
```
- Use the **device** build path (`-dd-device/.../Debug-iphoneos`), not the
simulator build; installing the wrong slice fails.
- `--timeout 180` is deliberate: the app bundle transfer over Wi-Fi can take a
while, and the default timeout is easy to hit.
- Installing over the network **does not delete app data**. Wallet files, seeds
and preferences survive updates (seeds live in the Keychain).
- If the device shows `unavailable`, or install fails with
`Network.NWError error 60 - Operation timed out`, the phone is asleep,
locked, or off Wi-Fi. Wake/unlock it, keep it on the same network, retry. A
timed-out install leaves the previous build installed.
- Launch errors with `BSErrorCodeDescription = Locked` mean exactly that: the
phone must be unlocked for the launch.
- `devicectl` is for physical devices only; use `simctl` for simulators.
### Simulator workflow
```bash
xcrun simctl list devices available
xcrun simctl boot <UDID>
open -a Simulator
xcrun simctl install <UDID> /tmp/<name>-dd/Build/Products/Debug-iphonesimulator/<Name>.app
xcrun simctl launch <UDID> <bundle-id>
xcrun simctl io <UDID> screenshot /tmp/screen.png
xcrun simctl get_app_container <UDID> <bundle-id> data
```
- Files the app writes (self-test reports, exported data) can be read from the
data container printed by `get_app_container`.
- Simulator preferences are cached by `cfprefsd`; `xcrun simctl spawn <UDID>
defaults delete …` often does not stick. To reset a preference: terminate the
app, `xcrun simctl spawn <UDID> launchctl stop com.apple.cfprefsd.xpc.daemon`,
then edit or delete the key in the plist directly.
### Local self-tests
Gate verification code behind launch arguments, write the report into the app's
Documents folder, and read it back from the container (console output is not
always reachable):
```bash
xcrun simctl launch <UDID> <bundle-id> -<name>SelfTest
DATA=$(xcrun simctl get_app_container <UDID> <bundle-id> data)
cat "$DATA/Documents/<name>-selftest.txt"
```
For unified-log checks:
```bash
xcrun simctl spawn <UDID> log show --last 1m \
--predicate 'subsystem == "<bundle-id>"' --style compact
```
Keep network tests explicit (`-<name>SelfTestNetwork`) so the default launch is
offline and fast.
### Signing and distribution
- **Team: `RX6767S6BW` for every TZGyn app, macOS and iOS.** iOS device builds
use automatic signing (`CODE_SIGN_STYLE: Automatic`, `DEVELOPMENT_TEAM:
RX6767S6BW`) and `-allowProvisioningUpdates`; no manual profiles.
- **iOS apps are not notarized.** Apple has no notarization step for iOS —
device installs are gated by code signing + Developer Mode. Do not add a
`notarytool`/`stapler` step to an iOS build, and do not `spctl` an
`iphoneos` app.
- **macOS GUI releases are always notarized and stapled** (section 5), with the
shared App Store Connect key:
`~/.config/kapture/AuthKey_K5KXMMSB28.p8`, key ID `K5KXMMSB28`, issuer
`3e25e5dc-8b75-453f-a71c-c7f3feaac628`, team `RX6767S6BW`. Never publish a
macOS release until `spctl -a -vv` reports `accepted` /
`source=Notarized Developer ID`.
- **App Store Connect** (only if TestFlight/App Store is ever used) reuses the
same key; koins does not use it yet.
- Android is not codified yet; ask which stack (native, Compose, RN, EAS)
before assuming.
## 7. Extending this skill (self-improvement) ## 7. Extending this skill (self-improvement)