Add iOS build, wireless install, and signing workflows
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# TZGyn-skill
|
||||
|
||||
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 /
|
||||
notarizing / releasing macOS apps.
|
||||
to the TZGyn Homebrew tap (casks for GUI apps, formulae for CLIs), signing /
|
||||
notarizing / releasing macOS apps, and building native SwiftUI iOS apps and
|
||||
installing them over the network.
|
||||
|
||||
See [SKILL.md](./SKILL.md).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ skill at `~/.agents/skills/TZGyn-skill` (a symlink to the checkout, so editing
|
||||
the repo updates the live skill).
|
||||
|
||||
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
|
||||
|
||||
@@ -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`.
|
||||
- **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.
|
||||
- **Every GUI release must be notarized and stapled.** An unnotarized build is
|
||||
not a release: never publish one, and never paper over Gatekeeper with
|
||||
`xattr -dr com.apple.quarantine` or `brew install --cask --no-quarantine`.
|
||||
- **Every macOS GUI release must be notarized and stapled.** An unnotarized
|
||||
build is not a release: never publish one, and never paper over Gatekeeper
|
||||
with `xattr -dr com.apple.quarantine` or `brew install --cask --no-quarantine`.
|
||||
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/`,
|
||||
`dist/`, `.swiftpm/`, `xcuserdata/`, `DerivedData/`, `.DS_Store`.
|
||||
(`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
|
||||
app or the scripts.
|
||||
|
||||
## 6. Mobile
|
||||
## 6. iOS apps
|
||||
|
||||
For mobile (iOS/Android), notarization does not apply; the equivalent is store
|
||||
signing (App Store Connect / Play Console) or EAS. Ask for the specific stack
|
||||
before assuming; this skill currently only codifies the macOS path.
|
||||
Native SwiftUI iOS apps are built with **XcodeGen** — no CocoaPods, no SwiftPM
|
||||
packages. `project.yml` is the source of truth; the generated
|
||||
`<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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user