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:
@@ -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 ~5–10 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 (10–30 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: 60–90 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. |
|
||||
Executable
+191
@@ -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
|
||||
Reference in New Issue
Block a user