Files
chanora/tools/build-ios.sh
T
EdisonJwa 1324f478fe 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.
2026-05-14 23:49:58 +08:00

192 lines
6.8 KiB
Bash
Executable File

#!/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