docs(release): add iOS build instructions + shell helper

The development host is Linux x86_64; the iOS toolchain (Xcode, xcrun,
codesign, iPhoneOS SDK) is macOS-only under Apple licence and cannot
be cross-compiled from Linux. This commit adds the instructions for
producing the iOS v0.2.0-beta.1 build on a macOS host plus a bash
helper that automates the build itself.

docs/release/ios-build.md (v0.1.0):
  - Toolchain pin table (macOS 14+, Xcode 26+ per DEC-021, iOS SDK
    26+, iOS deployment target 13.0 per DEC-003, Flutter 3.41.9,
    Rust 1.95 stable with aarch64-apple-ios / aarch64-apple-ios-sim /
    x86_64-apple-ios targets, CocoaPods 1.16+, FRB 2.12.0).
  - macOS host options: owned hardware vs rental (MacStadium,
    MacinCloud, Scaleway Apple silicon, AWS EC2 Mac) vs borrowed
    Mac. Realistic cost ranges per option.
  - Step-by-step Homebrew + Rust + Flutter + CocoaPods install.
  - Pre-built libopus.a per arch via a CMake invocation that
    targets the iOS SDK explicitly. Mirrors the Android build's
    LIBOPUS_LIB_DIR wrap-dir trick.
  - flutter create --platforms=ios to scaffold the ios/ folder
    (the product Flutter app was created with only linux + android).
  - Edits required to ios/Podfile and ios/Runner/Info.plist:
    iOS 13 deployment target (DEC-003), NSMicrophoneUsageDescription
    for the audio engine, UIBackgroundModes=audio for screen-locked
    playback.
  - Three cargo build --target invocations for device + both
    simulator slices.
  - lipo merge of the two simulator slices into one .a.
  - xcodebuild -create-xcframework to produce
    target/ChanoraBridge.xcframework with the right slices.
  - flutter build ios --release --no-codesign or
    flutter build ipa --release --export-method development for
    a signed .ipa.
  - Install paths: xcrun devicectl for wired install, altool for
    TestFlight upload.
  - Smoke-test instructions with the same hostname-resolution
    caveat that affects the Android Beta (hickory-resolver does
    not work on iOS; use the literal IP).
  - Packaging into chanora-v0.2.0-beta.1-ios.ipa.
  - Known-issue table covering: audiopus_sys cmake build failures
    on iOS, microphone permission prompt prerequisites,
    AVAudioSession category quirks for voice transmission, code-
    signing failure modes, TestFlight rejection causes.
  - Reproducibility note (build is not bit-reproducible).

tools/build-ios.sh:
  - Parameter switches: --version, --no-codesign,
    --regenerate-bindings, --export-method.
  - Verifies xcodebuild, xcrun, cargo, rustc, flutter, pod, lipo
    on PATH.
  - Adds the three rustup iOS targets if missing.
  - Verifies each pre-built libopus.a exists at the expected wrap
    dir before starting.
  - Optionally regenerates FRB bindings.
  - Three cargo build runs (device + Apple-silicon sim + Intel
    sim), each with LIBOPUS_LIB_DIR pointed at its arch's wrap dir
    and the audiopus_sys build-cache wiped per target.
  - lipo + xcodebuild -create-xcframework.
  - flutter pub get + pod install + flutter build {ios,ipa}.
  - Copies the .ipa to a versioned path under $HOME and prints
    SHA-256.

This is documentation + helper only; no actual iOS binaries are
produced by this commit. The Linux development host cannot run
Xcode. To produce the binaries, follow §3-§14 of
docs/release/ios-build.md on a macOS host, or run
tools/build-ios.sh there.

DEC-011.1 iOS audio status remains Deferred; the doc notes that
cpal's iOS backend has not been empirically verified and the
AVAudioSession category likely needs configuration for voice
transmission. Both are Beta+ items.
This commit is contained in:
EdisonJwa
2026-05-14 23:49:58 +08:00
parent c81ccfd9a9
commit 1324f478fe
2 changed files with 855 additions and 0 deletions
+664
View File
@@ -0,0 +1,664 @@
# Building Chanora v0.2.0-beta.1 on iOS via macOS host
**Document type:** Release / Platform Build Instructions
**Version:** 0.1.0
**Status:** Draft
**Language:** English
**Product:** Chanora
**Repo path:** `docs/release/ios-build.md`
---
## 1. Purpose
This document gives exact step-by-step instructions for building the
Chanora `v0.2.0-beta.1` Internal Beta on an Apple macOS host. It
exists because the development host is Linux x86_64; the entire
iOS toolchain (Xcode, `xcrun`, `codesign`, `xcodebuild`, the
`iPhoneOS` SDK) is macOS-only under Apple licence and cannot be
cross-compiled from Linux. Even projects like `xcbuild` or
`cctools-port` cannot produce a signed `.ipa` that an iOS device's
`installd` will accept.
After following this doc you will have:
- `Runner.app` — the iOS app bundle.
- `libchanora_bridge.a` and/or an XCFramework containing both
device (arm64) and simulator (arm64+x86_64) slices of the Rust
cdylib.
- A `Runner.ipa` archive suitable for ad-hoc / TestFlight / App
Store distribution depending on the provisioning profile you sign
with.
The doc assumes you have:
- A macOS host (physical Mac or rental — see §3 for options).
- An Apple Developer account ($99/year). A free Apple ID can build
for personal devices via Xcode automatic signing, but profiles
expire in 7 days; the paid account is strongly recommended.
- Permission to enrol a test iPhone or iPad in the developer team.
- The Chanora source code (this repository), reachable from the
Mac (git remote, scp, or AirDrop).
---
## 2. Toolchain versions pinned for this build
| Tool | Version | Why |
|---|---|---|
| macOS | 14 Sonoma or 15 Sequoia | Xcode 26 requires macOS 14+ at minimum |
| Xcode | **26 or newer** (DEC-021) | Required to upload to App Store Connect on or after 2026-04-28. Beta builds for internal devices also pin to 26+ for parity. |
| iOS SDK | **26 or newer** (DEC-021) | Bundled with Xcode 26+. |
| iOS deployment target | **iOS 13** (DEC-003) | Matches Flutter 3.41.9's baseline support. The Podfile and Runner.xcodeproj must set this. |
| Command Line Tools | Latest stable (`xcode-select --install` or via Xcode) | Provides `xcrun`, `xcodebuild`, `codesign`. |
| Flutter SDK | 3.41.9 stable | Same pin as Linux/Android/Windows builds. |
| Dart SDK | 3.11.5 | Bundled with Flutter. |
| Rust toolchain | stable 1.95 (or newer) | Workspace `rust-version = "1.95"`. |
| Rust targets | `aarch64-apple-ios`, `aarch64-apple-ios-sim`, `x86_64-apple-ios` | Device + Apple-silicon simulator + Intel-Mac simulator. |
| `cbindgen` (optional) | 0.27+ | Only if you wire a C header for the bridge; FRB does not require it. |
| `cargo-lipo` (legacy) | not required | `cargo-lipo` was deprecated for multi-arch iOS in favor of `cargo build --target <triple>` + `xcodebuild -create-xcframework`. |
| CocoaPods | 1.16+ | Required by Flutter iOS builds (Podfile in `apps/chanora_flutter/ios/`). |
| Opus library | bundled via `audiopus_sys` build script with iOS-specific CMake toolchain | Apple does not ship libopus; we cross-compile it per Rust target. |
| Apple Developer ID + certificate | one-time | Required to sign the .ipa. Free Apple ID OK for personal-device install (7-day profile). |
> **Why not Xcode 15 or 16?** Apple's App Store Connect upload gate
> (DEC-021) requires the iOS 26 SDK on or after 2026-04-28. Even for
> internal Beta where you don't upload, we pin Xcode 26+ so the same
> archive can be re-signed for TestFlight without re-building.
---
## 3. macOS host options
### 3.1 Owned Apple hardware (cheapest long-term)
| Hardware | Min spec | Notes |
|---|---|---|
| Mac mini (M2, 2023+) | 16 GiB RAM, 256 GiB SSD | Best price/perf; ~USD $799 new |
| MacBook Air / Pro (M2+) | 16 GiB RAM | Portable; ~USD $1099+ new |
| Used Intel Mac (2018+) | 16 GiB RAM | Will need macOS 14+; check Apple's compatibility list |
Apple's licence forbids running macOS on non-Apple hardware
("Hackintoshes") for any commercial purpose. Don't.
### 3.2 macOS rental (no hardware purchase)
| Provider | Cost | Notes |
|---|---|---|
| MacStadium | USD ~$79+/month for an M-series Mac mini | Dedicated host; supports Xcode + signing |
| MacinCloud | USD ~$30+/month "Pay-As-You-Go" or "Dedicated Server" plans | Shared options exist; choose Dedicated for full Xcode access |
| Scaleway Apple silicon (EU) | EUR ~0.13/hour for Mac mini M2 | Hourly billing; cheap for short builds |
| AWS EC2 Mac (`mac1.metal`, `mac2.metal`) | USD ~$1.08/hour with a 24h minimum allocation | Most expensive; use only if you need AWS integration |
> Apple's licence permits cloud hosts that comply with their terms.
> All of the above explicitly meet that requirement.
### 3.3 Borrowed Mac
Find a friend or coworker with a Mac that can run Xcode 26. Install
your dev certificate to their Keychain locally for the build session
only, then remove it. This is the cheapest option if you have access.
---
## 4. Install the toolchain (one-time per Mac)
All commands assume a normal user account; only the Xcode install
needs an Administrator password.
### 4.1 Xcode + Command Line Tools
Either:
```bash
# Easiest: install Xcode from the Mac App Store.
# (Or download the .xip from https://developer.apple.com/download/all/)
open "https://apps.apple.com/app/xcode/id497799835"
# Then accept the licence and install Command Line Tools.
sudo xcodebuild -license accept
xcode-select --install # idempotent; skip if already done
# Verify.
xcodebuild -version # expect Xcode 26.x
xcrun --version
xcrun simctl list runtimes | head
```
If you already have Xcode 16 or 17 installed, **add** Xcode 26
side-by-side (don't replace) and switch via `sudo xcode-select -s
/Applications/Xcode-26.app`.
### 4.2 Homebrew + Rust + Flutter + CocoaPods
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Rust (managed by rustup, not brew)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
source "$HOME/.cargo/env"
# Add the three iOS targets.
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
# Flutter — pinned to 3.41.9 stable to match the Linux / Android
# builds.
brew install --cask flutter
flutter --version | head -3
flutter precache --ios
# CocoaPods.
brew install cocoapods
pod --version # expect 1.16 or newer
```
> If your Mac is Apple-silicon and Homebrew installs to
> `/opt/homebrew`, make sure that's on `PATH` for new shells:
> `eval "$(/opt/homebrew/bin/brew shellenv)"`.
### 4.3 Opus
`audiopus_sys` ships an Opus source tree and tries to build it via
CMake. On iOS the cmake-crate's generated build sometimes confuses
the iOS toolchain. We use the same pre-built libopus strategy that
the Android build uses:
```bash
# Get the audiopus_sys-bundled libopus source after a first cargo
# fetch (it gets unpacked into the registry).
cd ~/chanora
cargo fetch
OPUS_SRC=$(find ~/.cargo/registry/src -path '*audiopus_sys-*/opus' -type d | head -1)
mkdir -p /tmp/opus-ios
cp -R "$OPUS_SRC"/* /tmp/opus-ios/
# Helper: build libopus.a for one iOS arch.
build_opus_ios() {
local ARCH=$1 # arm64 | x86_64 | arm64-simulator
local SDK=$2 # iphoneos | iphonesimulator
local OUT=/tmp/opus-ios-${ARCH}
rm -rf /tmp/opus-ios/build && mkdir /tmp/opus-ios/build
cd /tmp/opus-ios/build
cmake .. \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=$(xcrun --sdk "$SDK" --show-sdk-path) \
-DCMAKE_OSX_ARCHITECTURES="${ARCH%-simulator}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-DBUILD_SHARED_LIBS=OFF \
-DOPUS_BUILD_PROGRAMS=OFF \
-DOPUS_BUILD_TESTING=OFF \
-DCMAKE_INSTALL_PREFIX="$OUT" \
-G "Unix Makefiles"
make -j8 install
# Wrap so LIBOPUS_LIB_DIR appends /lib correctly.
mkdir -p "$OUT-wrap/lib"
cp "$OUT/lib/libopus.a" "$OUT-wrap/lib/"
cd /tmp/opus-ios
}
build_opus_ios arm64 iphoneos # device
build_opus_ios arm64-simulator iphonesimulator # Apple-silicon Mac sim
build_opus_ios x86_64 iphonesimulator # Intel Mac sim
```
You should end up with three wrap dirs each containing `lib/libopus.a`:
```
/tmp/opus-ios-arm64-wrap/lib/libopus.a
/tmp/opus-ios-arm64-simulator-wrap/lib/libopus.a
/tmp/opus-ios-x86_64-wrap/lib/libopus.a
```
### 4.4 Cargo target linker config
iOS targets need explicit linker config to find the SDK. Create
`~/.cargo/config.toml`:
```toml
[target.aarch64-apple-ios]
rustflags = ["-C", "link-arg=-fembed-bitcode"]
[target.aarch64-apple-ios-sim]
rustflags = ["-C", "link-arg=-fembed-bitcode"]
[target.x86_64-apple-ios]
rustflags = ["-C", "link-arg=-fembed-bitcode"]
```
Rust's built-in iOS targets already drive the right linker via
`xcrun`, so usually no further config is needed. If you hit a
"-platform_version not supported" error, ensure Xcode CLI tools are
fresh: `sudo xcode-select --reset`.
### 4.5 (Optional) `flutter_rust_bridge_codegen`
Only needed if you modify `crates/chanora_bridge/src/api.rs` on the
Mac. The bindings shipped in the repo are platform-agnostic Dart +
Rust source and do **not** need regenerating per platform.
```bash
cargo install flutter_rust_bridge_codegen --version "=2.12.0"
```
---
## 5. Get the source onto the Mac
Same two options as the Windows doc:
### Option A — git remote
```bash
# On Linux dev host: push to a private remote.
cd /home/milkice/chanora
git remote add origin git@github.com:<your-user>/chanora.git
git push --all
git push --tags
```
```bash
# On the Mac:
cd ~
git clone https://github.com/<your-user>/chanora.git
cd chanora
git checkout v0.2.0-beta.1
```
### Option B — zip / AirDrop
```bash
# On Linux:
git -C /home/milkice/chanora archive --format=zip \
--output=/tmp/chanora-v0.2.0-beta.1.zip v0.2.0-beta.1
# Transfer the zip to the Mac (AirDrop, scp, USB), then:
cd ~
unzip /Volumes/.../chanora-v0.2.0-beta.1.zip -d chanora
cd chanora
```
---
## 6. Add the iOS platform to the Flutter app
The product Flutter app was scaffolded with `--platforms=linux,android`,
so the `ios/` folder does not yet exist. Add it once on the Mac:
```bash
cd ~/chanora/apps/chanora_flutter
flutter create --platforms=ios .
```
This creates `apps/chanora_flutter/ios/` containing:
```
ios/
Runner/
AppDelegate.swift
Info.plist
Runner-Bridging-Header.h
Assets.xcassets/
Runner.xcodeproj/
Runner.xcworkspace/
Podfile
Flutter/
```
> **Important:** Do **not** commit this `ios/` folder back to the
> repo from the Mac unless you also do the corresponding edits the
> source-of-truth host uses (icons, bundle ID, signing config).
> For Beta it is fine to keep it Mac-only.
### 6.1 Set the iOS deployment target
Edit `apps/chanora_flutter/ios/Podfile`'s first line to:
```ruby
platform :ios, '13.0' # DEC-003
```
And in Xcode (`open Runner.xcworkspace`):
- Project → Runner → General → Minimum Deployments → iOS 13.0
- Project → Runner → Signing & Capabilities → set your Team
### 6.2 Add iOS-required entitlements + Info.plist keys
Chanora's audio engine needs the microphone. Edit
`apps/chanora_flutter/ios/Runner/Info.plist` to include:
```xml
<key>NSMicrophoneUsageDescription</key>
<string>Chanora uses the microphone for voice chat with TeamSpeak-compatible servers.</string>
<!-- Background-mode audio so playback continues when the screen
locks. Optional for Beta; required for a real release. -->
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<!-- Allow plain HTTP only for development. Remove before release.
The protocol layer itself uses UDP, not HTTP, but Flutter's hot
reload talks HTTP. -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
```
The bundle identifier should be set to a reverse-DNS string under
your developer team, e.g. `app.chanora.chanora_flutter`. Update it
in Xcode → Runner → General → Identity → Bundle Identifier.
---
## 7. Build the Rust libraries for iOS
We build three flavors and combine them into an XCFramework that
Xcode can link against from both device and simulator slices.
```bash
cd ~/chanora
export LIBOPUS_STATIC=1
export LIBOPUS_NO_PKG=1
build_bridge_for_ios() {
local TARGET=$1 # aarch64-apple-ios | aarch64-apple-ios-sim | x86_64-apple-ios
local OPUS_DIR=$2 # /tmp/opus-ios-*-wrap
export LIBOPUS_LIB_DIR="$OPUS_DIR"
# Wipe audiopus_sys build cache so LIBOPUS_LIB_DIR is re-read.
rm -rf target/$TARGET/release/build/audiopus_sys-*
cargo build --release --target "$TARGET" -p chanora_bridge
}
build_bridge_for_ios aarch64-apple-ios /tmp/opus-ios-arm64-wrap
build_bridge_for_ios aarch64-apple-ios-sim /tmp/opus-ios-arm64-simulator-wrap
build_bridge_for_ios x86_64-apple-ios /tmp/opus-ios-x86_64-wrap
```
Expected static-lib outputs (the `[lib]` section in
`crates/chanora_bridge/Cargo.toml` already declares `staticlib`):
```
target/aarch64-apple-ios/release/libchanora_bridge.a (~25 MiB, device)
target/aarch64-apple-ios-sim/release/libchanora_bridge.a (~25 MiB, M-series simulator)
target/x86_64-apple-ios/release/libchanora_bridge.a (~25 MiB, Intel simulator)
```
### 7.1 Build the simulator universal binary with `lipo`
The simulator side needs both arches in one `.a` for Xcode to pick
the host architecture automatically:
```bash
mkdir -p target/universal-ios-sim
lipo -create \
target/aarch64-apple-ios-sim/release/libchanora_bridge.a \
target/x86_64-apple-ios/release/libchanora_bridge.a \
-output target/universal-ios-sim/libchanora_bridge.a
file target/universal-ios-sim/libchanora_bridge.a
```
### 7.2 Build the XCFramework
```bash
rm -rf target/ChanoraBridge.xcframework
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libchanora_bridge.a \
-library target/universal-ios-sim/libchanora_bridge.a \
-output target/ChanoraBridge.xcframework
```
Result:
```
target/ChanoraBridge.xcframework/
Info.plist
ios-arm64/libchanora_bridge.a
ios-arm64_x86_64-simulator/libchanora_bridge.a
```
### 7.3 Link the XCFramework from the Flutter iOS app
Open `apps/chanora_flutter/ios/Runner.xcworkspace` in Xcode.
1. Drag `target/ChanoraBridge.xcframework` into the Runner project
(Embed: "Do Not Embed" — it's a static lib).
2. Project → Runner → Build Phases → Link Binary with Libraries →
ensure `ChanoraBridge.xcframework` is listed.
3. The FRB Dart loader expects to find the symbols in the main app
binary on iOS (static linkage). The generated
`apps/chanora_flutter/lib/src/rust/frb_generated.io.dart` should
look up via `DynamicLibrary.process()` on iOS — this is the
default for FRB v2 when the Rust crate type is `staticlib`.
If the dart-side loader expects a separate dylib, regenerate
bindings with the iOS target hint:
```bash
flutter_rust_bridge_codegen generate
```
(The repo's bindings already target this style; no change usually
needed.)
---
## 8. Regenerate the FRB bindings (optional)
Skip unless you've modified `crates/chanora_bridge/src/api.rs` on
the Mac. The bindings are platform-agnostic Dart + Rust source.
```bash
cd ~/chanora
flutter_rust_bridge_codegen generate
```
Takes ~510 minutes on first run (cargo expand).
---
## 9. Build the iOS app via Flutter
```bash
cd ~/chanora/apps/chanora_flutter
flutter pub get
cd ios && pod install && cd ..
flutter build ios --release --no-codesign
```
`flutter build ios --no-codesign` produces an unsigned `Runner.app`.
For signed `.ipa` for TestFlight or device install, use:
```bash
flutter build ipa --release --export-method development
# or: --export-method ad-hoc | enterprise | app-store
```
This requires:
- A configured Team and Bundle Identifier (set in Xcode in §6.1).
- A matching provisioning profile in your Keychain (Xcode's
Automatic Signing usually handles this if you've signed into
your Apple ID in Xcode → Settings → Accounts).
Expected output locations:
| Variant | Path |
|---|---|
| Unsigned app bundle | `apps/chanora_flutter/build/ios/iphoneos/Runner.app/` |
| Signed `.ipa` | `apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa` |
---
## 10. Install on a physical iPhone/iPad
### 10.1 Wired install (USB or USB-C)
```bash
# List paired devices.
xcrun devicectl list devices
# Install the ipa.
xcrun devicectl device install app --device <UDID> \
apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa
# Launch it.
xcrun devicectl device process launch \
--device <UDID> --terminate-existing \
app.chanora.chanora_flutter
```
### 10.2 TestFlight (wireless to many testers)
```bash
xcrun altool --upload-app \
--type ios \
--file apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa \
--username "<your-apple-id>" \
--password "<app-specific-password>"
```
Then in App Store Connect:
1. Wait for processing (1030 minutes).
2. Add the build to a TestFlight test group.
3. Invite testers by Apple ID email.
Testers install via the TestFlight app on their device.
---
## 11. Smoke test on the iPhone/iPad
Open the installed Chanora app:
1. **Banner**: "Beta build — voice in/out wired; not production
ready."
2. **Form**: pre-populated with `cn.teamspeak.app` + `ChanoraBeta`.
3. **Connect**.
> **Known limitation (same as Android):** the protocol layer's
> `hickory-resolver` cannot resolve hostnames on iOS by default. If
> Connect times out, enter the literal IP. For `cn.teamspeak.app`
> that is currently `175.178.125.23`. A proper fix wires the iOS
> system resolver into hickory; Beta+ work.
4. Once connected, the channel tree appears.
5. **Start audio** → grant microphone permission when prompted.
6. **Hold to talk** → frame counter increments under PTT.
If audio fails to start, check:
- Microphone permission granted in Settings → Chanora → Microphone.
- Device not in silent mode (output side only).
- iOS Audio Session category — by default Flutter uses
`AVAudioSessionCategoryPlayback`. Voice transmission needs
`PlayAndRecord` with `voiceChat` mode. **Beta+ work.**
---
## 12. Package the release artefact
`flutter build ipa` already produces a single `.ipa`. Stamp it with
the build identifier:
```bash
cd ~/chanora/apps/chanora_flutter
cp build/ios/ipa/chanora_flutter.ipa \
"$HOME/chanora-v0.2.0-beta.1-ios.ipa"
shasum -a 256 "$HOME/chanora-v0.2.0-beta.1-ios.ipa"
```
Typical size: 6090 MiB (includes Flutter engine + Dart AOT +
Rust static lib + bundled libopus + iOS resources).
---
## 13. Download to your local host
If on a rental Mac:
```bash
# From the Mac, copy back to Linux dev host.
scp "$HOME/chanora-v0.2.0-beta.1-ios.ipa" \
user@your-linux-host:/home/milkice/
```
Or pull from the rental's web file manager. Keep the `.ipa`
re-installable indefinitely if the provisioning profile is
ad-hoc/enterprise; for development profiles it expires in 7 days.
---
## 14. Final artefact paths (what you actually produce)
On the Mac, after a successful build:
| Artefact | Path |
|---|---|
| Rust device static lib | `~/chanora/target/aarch64-apple-ios/release/libchanora_bridge.a` |
| Rust Apple-silicon sim lib | `~/chanora/target/aarch64-apple-ios-sim/release/libchanora_bridge.a` |
| Rust Intel sim lib | `~/chanora/target/x86_64-apple-ios/release/libchanora_bridge.a` |
| Lipo'd simulator lib | `~/chanora/target/universal-ios-sim/libchanora_bridge.a` |
| XCFramework | `~/chanora/target/ChanoraBridge.xcframework/` |
| Unsigned app bundle | `~/chanora/apps/chanora_flutter/build/ios/iphoneos/Runner.app/` |
| Signed `.ipa` | `~/chanora/apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa` |
| Renamed release `.ipa` | `~/chanora-v0.2.0-beta.1-ios.ipa` |
---
## 15. After the build — deallocate (rental hosts)
| Provider | Deallocate command |
|---|---|
| MacStadium | Suspend / shut down via control panel |
| MacinCloud | "Stop" the session in account dashboard |
| Scaleway | `scw instance server stop <id>` |
| AWS EC2 Mac | Stop the host; release the dedicated host if at end of 24h minimum |
Always verify the billing dashboard shows the instance as stopped,
not just shut down at the OS level.
---
## 16. Known issues and caveats
| Issue | Workaround |
|---|---|
| `audiopus_sys` cmake-build fails with "no member named '__int128_t'" | Re-export `LIBOPUS_LIB_DIR` to a pre-built `.a` directory (§4.3); never let cargo's cmake-build attempt to compile Opus on iOS. |
| `hickory-resolver` cannot resolve hostnames on iOS | Use literal IP. Fix: wire `dns-via-system` resolver into `chanora_protocol`. Beta+ work. |
| Microphone permission prompt never appears | The `NSMicrophoneUsageDescription` is missing from Info.plist. See §6.2. |
| Audio plays but no transmission | iOS AVAudioSession category likely set to `Playback` not `PlayAndRecord`. Configure the session in Swift before Flutter loads, or via a small plugin. Beta+ work. |
| Code signing fails with "No signing certificate" | Open Xcode → Settings → Accounts → add your Apple ID, then close and reopen `Runner.xcworkspace` to refresh. |
| `flutter build ipa` complains about missing entitlements | Make sure the provisioning profile in Apple Developer console includes the App ID with microphone capability enabled. |
| TestFlight upload rejected: "Invalid bundle. Apple silicon support" | Build with `flutter build ipa --release` (not `--debug`); debug AOT is JIT-only and not allowed in TestFlight. |
| App crashes on launch with `dyld: Library not loaded: libchanora_bridge.dylib` | The bridge is built as `staticlib`, not `cdylib`, on iOS — FRB's loader uses `DynamicLibrary.process()`. If you accidentally built a `cdylib`, the loader path won't match. Stick to staticlib. |
| First `flutter build ios` takes > 30 minutes | Normal — first Pod install + CocoaPods cache. |
---
## 17. Reproducibility note
Like the Windows build, the iOS build is **not bit-reproducible**:
- `xcodebuild` embeds a creation timestamp in the `.ipa`.
- `audiopus_sys` picks the cmake-discovered Xcode toolchain version.
- Dart AOT compilation embeds a Dart kernel hash.
- Code-signing inherently makes every signed `.ipa` unique.
If true reproducibility is required for a future External Beta or
MVP Public release, that is Beta+ work; the build above is
sufficient for an Internal Beta milestone per DEC-001.
---
## 18. Change History
| Version | Date | Description |
|---|---|---|
| 0.1.0 | 2026-05-14 | Initial iOS build instructions for Chanora `v0.2.0-beta.1`. Targets a macOS 14+ host running Xcode 26+ (DEC-021), Rust 1.95 stable with `aarch64-apple-ios` + `aarch64-apple-ios-sim` + `x86_64-apple-ios` targets, Flutter 3.41.9 stable, `flutter_rust_bridge` 2.12.0. Produces `Runner.app` + `chanora_flutter.ipa` via `flutter build ipa --release`. iOS deployment target pinned to 13.0 (DEC-003). Surface caveat: DEC-011.1 iOS audio remains Deferred — `cpal`'s iOS backend has not been empirically verified, and AVAudioSession category configuration is required for voice transmission. |
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env bash
# Chanora iOS build helper.
#
# Run this on a macOS host that has already completed §4 of
# docs/release/ios-build.md:
# - Xcode 26+ with Command Line Tools
# - Rust stable with aarch64-apple-ios, aarch64-apple-ios-sim,
# x86_64-apple-ios targets
# - Flutter 3.41.9 stable on PATH
# - CocoaPods on PATH
# - Pre-built libopus.a for each iOS arch (the script in §4.3 of the
# docs leaves them in /tmp/opus-ios-<arch>-wrap/lib/libopus.a)
#
# Usage (from the repo root ~/chanora):
# ./tools/build-ios.sh
# ./tools/build-ios.sh --no-codesign # unsigned Runner.app only
# ./tools/build-ios.sh --regenerate-bindings # rerun FRB codegen first
# ./tools/build-ios.sh --version v0.2.0-beta.1
#
# Produces:
# target/aarch64-apple-ios/release/libchanora_bridge.a
# target/aarch64-apple-ios-sim/release/libchanora_bridge.a
# target/x86_64-apple-ios/release/libchanora_bridge.a
# target/universal-ios-sim/libchanora_bridge.a
# target/ChanoraBridge.xcframework/
# apps/chanora_flutter/build/ios/iphoneos/Runner.app/
# apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa (if signed)
# chanora-<version>-ios.ipa (renamed copy)
set -euo pipefail
VERSION="v0.2.0-beta.1"
SKIP_CODESIGN=0
REGEN=0
EXPORT_METHOD="development"
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2;;
--no-codesign) SKIP_CODESIGN=1; shift;;
--regenerate-bindings) REGEN=1; shift;;
--export-method) EXPORT_METHOD="$2"; shift 2;;
*) echo "unknown arg: $1" >&2; exit 2;;
esac
done
cd "$(dirname "$0")/.."
REPO_ROOT="$(pwd)"
bar() { printf '%s\n' '========================================================================'; }
bar
echo "Chanora iOS build"
echo "Repo root : $REPO_ROOT"
echo "Version : $VERSION"
echo "Sign : $([ "$SKIP_CODESIGN" = 1 ] && echo no || echo "yes (export-method=$EXPORT_METHOD)")"
bar
# ---------- 1. Verify toolchain ----------
echo "[1/7] Verify toolchain"
for cmd in xcodebuild xcrun cargo rustc flutter pod lipo; do
if ! command -v "$cmd" >/dev/null; then
echo " ERROR: '$cmd' not on PATH. See docs/release/ios-build.md §4." >&2
exit 1
fi
printf " %-10s : %s\n" "$cmd" "$($cmd --version 2>&1 | head -1)"
done
# Verify the three Rust iOS targets are installed.
for tgt in aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios; do
if ! rustup target list --installed | grep -q "^$tgt$"; then
echo " Adding rustup target $tgt..."
rustup target add "$tgt"
fi
done
# Verify the pre-built libopus.a for each arch is available.
for arch in arm64 arm64-simulator x86_64; do
if [[ ! -f "/tmp/opus-ios-${arch}-wrap/lib/libopus.a" ]]; then
echo " ERROR: /tmp/opus-ios-${arch}-wrap/lib/libopus.a not found." >&2
echo " See §4.3 of docs/release/ios-build.md to build libopus first." >&2
exit 1
fi
done
# ---------- 2. (Optional) regenerate FRB bindings ----------
if [[ $REGEN -eq 1 ]]; then
echo "[2/7] Regenerate flutter_rust_bridge bindings"
if ! command -v flutter_rust_bridge_codegen >/dev/null; then
echo " flutter_rust_bridge_codegen not on PATH; cargo install it first." >&2
exit 1
fi
flutter_rust_bridge_codegen generate
else
echo "[2/7] Skipping FRB codegen (use --regenerate-bindings if api.rs changed)"
fi
# ---------- 3. Add ios/ platform folder if missing ----------
FLUTTER_APP="$REPO_ROOT/apps/chanora_flutter"
if [[ ! -d "$FLUTTER_APP/ios" ]]; then
echo "[3/7] Add iOS platform to apps/chanora_flutter"
(cd "$FLUTTER_APP" && flutter create --platforms=ios .)
echo " NOTE: edit ios/Podfile to pin platform :ios, '13.0' (DEC-003)"
echo " NOTE: edit ios/Runner/Info.plist to add NSMicrophoneUsageDescription"
echo " NOTE: set Team + Bundle Identifier in Xcode Signing & Capabilities"
else
echo "[3/7] iOS platform already present in apps/chanora_flutter"
fi
# ---------- 4. Build Rust libs for the three iOS triples ----------
echo "[4/7] Build Rust libs for iOS"
export LIBOPUS_STATIC=1
export LIBOPUS_NO_PKG=1
build_one() {
local TARGET=$1
local OPUS_DIR=$2
echo " -> $TARGET"
export LIBOPUS_LIB_DIR="$OPUS_DIR"
rm -rf "target/$TARGET/release/build/audiopus_sys-"*
cargo build --release --target "$TARGET" -p chanora_bridge
}
build_one aarch64-apple-ios /tmp/opus-ios-arm64-wrap
build_one aarch64-apple-ios-sim /tmp/opus-ios-arm64-simulator-wrap
build_one x86_64-apple-ios /tmp/opus-ios-x86_64-wrap
# ---------- 5. lipo + xcodebuild -create-xcframework ----------
echo "[5/7] lipo simulator libs + create XCFramework"
mkdir -p target/universal-ios-sim
lipo -create \
target/aarch64-apple-ios-sim/release/libchanora_bridge.a \
target/x86_64-apple-ios/release/libchanora_bridge.a \
-output target/universal-ios-sim/libchanora_bridge.a
echo " Universal sim: $(file target/universal-ios-sim/libchanora_bridge.a)"
rm -rf target/ChanoraBridge.xcframework
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libchanora_bridge.a \
-library target/universal-ios-sim/libchanora_bridge.a \
-output target/ChanoraBridge.xcframework
echo " XCFramework at: $REPO_ROOT/target/ChanoraBridge.xcframework"
# ---------- 6. flutter build ios / ipa ----------
echo "[6/7] flutter pub get + pod install + flutter build"
cd "$FLUTTER_APP"
flutter pub get
(cd ios && pod install)
if [[ $SKIP_CODESIGN -eq 1 ]]; then
flutter build ios --release --no-codesign
APP_PATH="$FLUTTER_APP/build/ios/iphoneos/Runner.app"
echo " Unsigned .app: $APP_PATH"
else
flutter build ipa --release --export-method "$EXPORT_METHOD"
IPA_PATH="$FLUTTER_APP/build/ios/ipa/chanora_flutter.ipa"
if [[ ! -f "$IPA_PATH" ]]; then
echo " ERROR: expected $IPA_PATH, did not find one." >&2
ls -la "$FLUTTER_APP/build/ios/ipa/" 2>/dev/null || true
exit 1
fi
fi
# ---------- 7. Rename signed ipa, print final paths ----------
echo "[7/7] Package release artefact"
cd "$REPO_ROOT"
FINAL_IPA="$HOME/chanora-$VERSION-ios.ipa"
if [[ $SKIP_CODESIGN -ne 1 ]]; then
cp "$FLUTTER_APP/build/ios/ipa/chanora_flutter.ipa" "$FINAL_IPA"
shasum -a 256 "$FINAL_IPA"
fi
bar
echo "iOS build complete."
bar
echo "Artefacts:"
echo " Rust libs:"
echo " target/aarch64-apple-ios/release/libchanora_bridge.a"
echo " target/aarch64-apple-ios-sim/release/libchanora_bridge.a"
echo " target/x86_64-apple-ios/release/libchanora_bridge.a"
echo " target/universal-ios-sim/libchanora_bridge.a"
echo " XCFramework:"
echo " target/ChanoraBridge.xcframework/"
if [[ $SKIP_CODESIGN -eq 1 ]]; then
echo " Unsigned app bundle:"
echo " apps/chanora_flutter/build/ios/iphoneos/Runner.app/"
else
echo " Signed .ipa:"
echo " apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa"
echo " $FINAL_IPA"
fi