Add iOS build, wireless install, and signing workflows
This commit is contained in:
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user