# 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 ` + `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:/chanora.git git push --all git push --tags ``` ```bash # On the Mac: cd ~ git clone https://github.com//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 NSMicrophoneUsageDescription Chanora uses the microphone for voice chat with TeamSpeak-compatible servers. UIBackgroundModes audio NSAppTransportSecurity NSAllowsArbitraryLoads ``` 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 \ apps/chanora_flutter/build/ios/ipa/chanora_flutter.ipa # Launch it. xcrun devicectl device process launch \ --device --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 "" \ --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 ` | | 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. |