diff --git a/docs/release/ios-build.md b/docs/release/ios-build.md new file mode 100644 index 0000000..8972c57 --- /dev/null +++ b/docs/release/ios-build.md @@ -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 ` + `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. | diff --git a/tools/build-ios.sh b/tools/build-ios.sh new file mode 100755 index 0000000..cfa1b73 --- /dev/null +++ b/tools/build-ios.sh @@ -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--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--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