From bf37fe8f0f91eeafc36f68d8e939fb89e3d84e20 Mon Sep 17 00:00:00 2001 From: Talon Date: Fri, 19 Jun 2026 02:19:38 +0200 Subject: [PATCH] docs+scripts: add iOS build and simulator run commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/build-ios-client.sh: builds VoiceCatiOS.app for iOS simulator - calls build-xcframework.sh --all (all 3 slices required for iOS sim slice) - xcodebuild -target VoiceCatiOS with SYMROOT=OBJROOT to co-locate SPM and app build products (fixes "unable to resolve module dependency" error) - stages result to dist/ios-client/ scripts/run-ios-simulator.sh: installs and launches on an iPhone simulator - finds a booted simulator or boots the first available iPhone sim - opens Simulator.app, installs via simctl install, launches via simctl launch - --build flag to build first; --log to stream app logs; --device / --udid to pin a specific simulator scripts/build-all.sh: add --ios-client opt-in flag (macOS only) docs/building.md: - add iOS entry to quick-nav table - update §6 (apple platform): remove "scaffolding" caveat now that iOS slices are validated; trim to essentials + pointer to build-xcframework.sh - add §9 (iOS client SwiftUI): XCFramework, xcodebuild command with rationale for -target/-SYMROOT flags, run script usage, full simctl commands, Xcode UI --- docs/building.md | 156 ++++++++++++++++++++++++++++++++--- scripts/build-all.sh | 5 ++ scripts/build-ios-client.sh | 101 +++++++++++++++++++++++ scripts/run-ios-simulator.sh | 129 +++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 13 deletions(-) create mode 100755 scripts/build-ios-client.sh create mode 100755 scripts/run-ios-simulator.sh diff --git a/docs/building.md b/docs/building.md index 155679b..928342c 100644 --- a/docs/building.md +++ b/docs/building.md @@ -14,7 +14,9 @@ and *how to drive the binaries by hand*. | Server + `vccli` + tests (all platforms) | [§3](#3-build--test-the-loop-youll-run-constantly) | `cmake --preset dev && cmake --build --preset dev && ctest --preset dev` | | Production server (stripped, no tests) | [§5](#5-server-release-production-shaped-build) | `cmake --preset server-release && cmake --build --preset server-release` | | Windows client (C# / WinForms) | [§7](#7-windows-client-c--winforms) | `dotnet build clients/windows/VoiceCat.slnx` | -| macOS client (AppKit) | [§8](#8-macos-client-appkit) | `xcodebuild -project clients/apple/macOS/VoiceCatMac.xcodeproj -scheme VoiceCatMac build` | +| macOS client (AppKit) | [§8](#8-macos-client-appkit) | `scripts/build-macos-client.sh` | +| iOS client (SwiftUI / simulator) | [§9](#9-ios-client-swiftui) | `scripts/build-ios-client.sh` | +| Launch iOS app on simulator | [§9](#9-ios-client-swiftui) | `scripts/run-ios-simulator.sh` | | Swift core + tests | [§8](#8-macos-client-appkit) | `cd clients/apple && swift test` | ## 1. What each preset is for @@ -214,13 +216,11 @@ Use this to sanity-check release-mode behavior (e.g. perf, optimized codepaths) day-to-day development, since it has no test target wired up. The `-s` linker flag strips symbol tables from the binaries, producing smaller executables suitable for distribution. -## 6. Apple platform builds (scaffolding) +## 6. Apple platform builds The `apple-dev`, `apple-ios`, and `apple-ios-sim` presets produce static `libvoicecat.a` -slices for the Swift Package / XCFramework. The `apple-dev` preset (macOS slice) is -**validated** — it builds green on macOS 26.5 / Apple Silicon and produces a valid arm64 -`.a` + XCFramework. The iOS cross-compile presets (`apple-ios`, `apple-ios-sim`) are still -**scaffolding** — not yet CI-validated. Build on macOS: +slices for the Swift Package / XCFramework. All three are validated on macOS 26.5 / Apple +Silicon. Build on macOS: ```bash # macOS slice (arm64-osx on Apple Silicon, x64-osx on Intel) @@ -228,22 +228,31 @@ cmake --preset apple-dev cmake --build --preset apple-dev # → build/apple-dev/lib/libvoicecat.a -# iOS device slice +# iOS device slice (arm64-ios) cmake --preset apple-ios cmake --build --preset apple-ios # → build/apple-ios/lib/libvoicecat.a -# iOS simulator slice +# iOS simulator slice (arm64-ios-simulator) cmake --preset apple-ios-sim cmake --build --preset apple-ios-sim # → build/apple-ios-sim/lib/libvoicecat.a ``` -The three `.a` files are then stitched into an XCFramework via `xcodebuild --create-xcframework` (see [clients/apple/README.md](../clients/apple/README.md) for the -planned workflow). Actual XCFramework stitching, `AVAudioSession` integration, and iOS -UI work are tracked as follow-up tasks — the presets exist so the build entry point is ready -when that work starts. +You rarely need to run these CMake commands directly. The +`clients/apple/scripts/build-xcframework.sh` script drives them internally and stitches +the result into a self-contained `VoiceCatCore.xcframework`: + +```bash +# macOS slice only (default — used by macOS AppKit client) +clients/apple/scripts/build-xcframework.sh + +# All 3 slices (macOS + iOS device + iOS sim — required for iOS client) +clients/apple/scripts/build-xcframework.sh --all +``` + +See [clients/apple/README.md](../clients/apple/README.md) for the XCFramework internals +(fat static lib merge, module map staging). ## 7. Windows client (C# / WinForms) @@ -347,3 +356,124 @@ open clients/apple/macOS/VoiceCatMac.xcodeproj # Terminal 2 — launch the client open ~/Library/Developer/Xcode/DerivedData/VoiceCatMac-*/Build/Products/Debug/VoiceCatMac.app ``` + +Or use the per-artifact script which builds and stages to `dist/macos-client/`: + +```bash +scripts/build-macos-client.sh +``` + +## 9. iOS client (SwiftUI) + +The iOS client is an Xcode project (`clients/apple/iOS/VoiceCatiOS.xcodeproj`) that +links `libvoicecat` via the same `VoiceCatCore` Swift Package as the macOS client. +Full details in [`clients/apple/README.md`](../clients/apple/README.md). + +**Prerequisites:** Xcode, vcpkg (`VCPKG_ROOT` set), iOS Simulator runtime installed +(Xcode > Settings > Platforms > iOS). iOS deployment target: 17.0. + +### Build the XCFramework (all slices) + +The iOS build requires the `ios-arm64-simulator` slice in the XCFramework — not just the +macOS slice. Use the `--all` flag to produce all three slices: + +```bash +clients/apple/scripts/build-xcframework.sh --all +# → clients/apple/VoiceCatCore.xcframework/ +# ├── macos-arm64/ +# ├── ios-arm64/ +# └── ios-arm64-simulator/ +``` + +### Build the iOS simulator app + +```bash +# Via the convenience script (recommended): +scripts/build-ios-client.sh +# → clients/apple/iOS/build/Debug-iphonesimulator/VoiceCatiOS.app +# → dist/ios-client/VoiceCatiOS.app + +# Or as a single command (what the script does under the hood): +SIM_SDK="iphonesimulator$(xcrun --sdk iphonesimulator --show-sdk-version)" +BUILD_DIR="$(pwd)/clients/apple/iOS/build" +xcodebuild \ + -project clients/apple/iOS/VoiceCatiOS.xcodeproj \ + -target VoiceCatiOS \ + -sdk "$SIM_SDK" \ + -configuration Debug \ + CODE_SIGNING_ALLOWED=NO \ + ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=YES \ + SYMROOT="$BUILD_DIR" \ + OBJROOT="$BUILD_DIR" \ + build +``` + +**Why `-target` instead of `-scheme -destination`?** Using `-scheme VoiceCatiOS +-destination 'platform=iOS Simulator,OS=latest'` requires a simulator runtime whose iOS +version exactly matches the SDK version (`iphonesimulatorX.Y`). If you have an older +runtime installed (common when the SDK ships ahead of runtime availability in Xcode), the +build fails with "Unable to find a destination matching the provided destination specifier." +Using `-target` bypasses destination matching and builds against the SDK directly. + +**Why `SYMROOT=OBJROOT=clients/apple/iOS/build`?** When building with `-target` (not +`-scheme`), the local Swift Package (`VoiceCatCore`) resolves its build products relative +to `OBJROOT`. By default, SPM resolves into `clients/apple/build/`, while the app target +looks in `clients/apple/iOS/build/`. Pointing both to the same directory fixes the +"unable to resolve module dependency: 'VoiceCatCore'" error. + +### Run on the iOS Simulator + +```bash +# Via the script (finds or boots an iPhone simulator, installs, launches): +scripts/run-ios-simulator.sh + +# Build and run in one step: +scripts/run-ios-simulator.sh --build + +# Stream app logs after launch: +scripts/run-ios-simulator.sh --log + +# Target a specific device by name or UDID: +scripts/run-ios-simulator.sh --device "iPhone 16 Pro" +scripts/run-ios-simulator.sh --udid XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +``` + +Under the hood the script uses `xcrun simctl` commands: + +```bash +# Boot a simulator (if not already running): +xcrun simctl boot +open -a Simulator + +# Install the built .app: +xcrun simctl install clients/apple/iOS/build/Debug-iphonesimulator/VoiceCatiOS.app + +# Launch the app: +xcrun simctl launch cat.voice.VoiceCatiOS + +# Stream logs (Ctrl+C to stop — does not kill the app): +xcrun simctl spawn log stream --predicate 'subsystem contains "VoiceCat"' +``` + +### Run the full stack (server + iOS simulator) + +```bash +# Terminal 1 — start the server +./build/dev/bin/voicecat-server --name "My Server" + +# Terminal 2 — build + launch iOS client on simulator +scripts/run-ios-simulator.sh --build +``` + +On first connect the app will show a TOFU identity sheet — accept it, then join a channel. + +### Open in Xcode + +```bash +open clients/apple/iOS/VoiceCatiOS.xcodeproj +``` + +Xcode can build and run on the simulator directly. The XCFramework must already exist +(`clients/apple/VoiceCatCore.xcframework/`) — run `build-xcframework.sh --all` once +before opening Xcode. diff --git a/scripts/build-all.sh b/scripts/build-all.sh index 2fe865e..0941421 100755 --- a/scripts/build-all.sh +++ b/scripts/build-all.sh @@ -4,6 +4,7 @@ # dist/. Convenience wrapper over the per-artifact scripts in scripts/. # # On macOS: server + libs (macOS slice + xcframework) + macos-client (.app) +# optionally: iOS client for simulator (pass --ios-client) # On Windows: server + libs (windows DLL) + windows-client (.exe folder) # On Linux: server only (no native client presets target Linux) # @@ -14,6 +15,7 @@ # scripts/build-all.sh --skip-client # skip the GUI client(s) # scripts/build-all.sh --skip-libs # skip the library artifacts # scripts/build-all.sh --skip-server # skip the server +# scripts/build-all.sh --ios-client # also build iOS simulator client (macOS only) # scripts/build-all.sh -h|--help set -euo pipefail @@ -25,6 +27,7 @@ PASS_ARGS=() SKIP_CLIENT=false SKIP_LIBS=false SKIP_SERVER=false +BUILD_IOS_CLIENT=false while [[ $# -gt 0 ]]; do case "$1" in --dist) PASS_ARGS+=( --dist "$2" ); vc_set_dist "$2"; shift 2 ;; @@ -32,6 +35,7 @@ while [[ $# -gt 0 ]]; do --skip-client) SKIP_CLIENT=true; shift ;; --skip-libs) SKIP_LIBS=true; shift ;; --skip-server) SKIP_SERVER=true; shift ;; + --ios-client) BUILD_IOS_CLIENT=true; shift ;; -h|--help) vc_print_help "$0"; exit 0 ;; *) vc_die "unknown arg: $1 (try --help)" ;; esac @@ -61,6 +65,7 @@ fi if ! $SKIP_CLIENT; then if [[ "$HOST" == "macos" ]]; then run build-macos-client.sh + $BUILD_IOS_CLIENT && run build-ios-client.sh elif [[ "$HOST" == "windows" ]]; then run build-windows-client.sh fi diff --git a/scripts/build-ios-client.sh b/scripts/build-ios-client.sh new file mode 100755 index 0000000..ecf24ef --- /dev/null +++ b/scripts/build-ios-client.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# build-ios-client.sh — build the iOS SwiftUI client (VoiceCatiOS.app) for the +# iOS simulator and stage it into dist/ios-client/. +# +# Two steps: +# 1. Build VoiceCatCore.xcframework with all 3 slices (macOS + iOS device + +# iOS simulator) via clients/apple/scripts/build-xcframework.sh --all. +# The XCFramework must include the ios-arm64-simulator slice so the Swift +# Package binary target resolves when xcodebuild compiles the iOS app. +# 2. xcodebuild the VoiceCatiOS target against the iOS Simulator SDK, with +# SYMROOT and OBJROOT pointed at the same directory so the Swift Package +# and the app target find each other's build products. +# +# Usage: +# scripts/build-ios-client.sh # Debug sim build into ./dist +# scripts/build-ios-client.sh --dist /out +# scripts/build-ios-client.sh --no-configure # skip cmake configure (xcframework step) +# scripts/build-ios-client.sh --config Debug # Xcode config (default Debug) +# scripts/build-ios-client.sh -h|--help +# +# Output: +# clients/apple/iOS/build/Debug-iphonesimulator/VoiceCatiOS.app +# dist/ios-client/VoiceCatiOS.app +# +# To install and launch on a simulator after building, use: +# scripts/run-ios-simulator.sh +# +# macOS only. Requires VCPKG_ROOT and Xcode (with iOS Simulator runtime installed). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VC_SCRIPT_NAME="build-ios-client" +source "$SCRIPT_DIR/common.sh" + +DO_CONFIGURE=true +XCODE_CONFIG="Debug" +while [[ $# -gt 0 ]]; do + case "$1" in + --dist) vc_set_dist "$2"; shift 2 ;; + --no-configure) DO_CONFIGURE=false; shift ;; + --config) XCODE_CONFIG="$2"; shift 2 ;; + -h|--help) vc_print_help "$0"; exit 0 ;; + *) vc_die "unknown arg: $1 (try --help)" ;; + esac +done + +vc_require_macos +vc_log "dist dir: $VC_DIST_DIR config: $XCODE_CONFIG" + +# ── Step 1: XCFramework (all slices) ────────────────────────────────────────── +# The iOS simulator build requires the ios-arm64-simulator slice, so we always +# build all three slices. --no-configure skips cmake configure but still runs +# cmake --build (which is fast with a warm build cache). +vc_step "build VoiceCatCore.xcframework (all slices: macOS + iOS device + iOS sim)" +XCFW_ARGS=( --all ) +$DO_CONFIGURE || XCFW_ARGS+=( --no-configure ) +"$VC_REPO_ROOT/clients/apple/scripts/build-xcframework.sh" "${XCFW_ARGS[@]}" +XCFW="$VC_REPO_ROOT/clients/apple/VoiceCatCore.xcframework" +[[ -d "$XCFW" ]] || vc_die "xcframework not found at $XCFW" +vc_ok "xcframework ready -> $XCFW" + +# ── Step 2: xcodebuild for iOS Simulator ────────────────────────────────────── +# We use -target (not -scheme + -destination) to avoid the requirement of a +# matching simulator runtime in xcrun simctl. When using -target, xcodebuild +# skips destination resolution and builds directly against the requested SDK. +# +# SYMROOT and OBJROOT are both pinned to the same directory so the Swift Package +# (VoiceCatCore) and the app target (VoiceCatiOS) resolve each other's products. +# Without this, the package builds to clients/apple/build/ while the app target +# looks in clients/apple/iOS/build/ — the module import fails with "unable to +# resolve module dependency: 'VoiceCatCore'". +# +# CODE_SIGNING_ALLOWED=NO avoids provisioning-profile errors for local sim builds. +XCODEPROJ="$VC_REPO_ROOT/clients/apple/iOS/VoiceCatiOS.xcodeproj" +BUILD_DIR="$VC_REPO_ROOT/clients/apple/iOS/build" +SIM_SDK_VER="$(xcrun --sdk iphonesimulator --show-sdk-version 2>/dev/null)" +SIM_SDK="iphonesimulator${SIM_SDK_VER}" +vc_step "xcodebuild VoiceCatiOS ($XCODE_CONFIG / $SIM_SDK)" +xcodebuild \ + -project "$XCODEPROJ" \ + -target VoiceCatiOS \ + -sdk "$SIM_SDK" \ + -configuration "$XCODE_CONFIG" \ + CODE_SIGNING_ALLOWED=NO \ + ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=YES \ + SYMROOT="$BUILD_DIR" \ + OBJROOT="$BUILD_DIR" \ + build + +APP="$BUILD_DIR/${XCODE_CONFIG}-iphonesimulator/VoiceCatiOS.app" +[[ -d "$APP" ]] || vc_die "VoiceCatiOS.app not found at $APP" +vc_ok "built -> $APP" + +# ── Stage ───────────────────────────────────────────────────────────────────── +vc_step "stage -> $VC_DIST_DIR/ios-client" +STAGE="$(vc_dist_subdir ios-client)" +cp -R "$APP" "$STAGE/" +vc_ok "VoiceCatiOS.app -> $STAGE/ ($(du -sh "$APP" | cut -f1))" +vc_ok "done -> $STAGE" diff --git a/scripts/run-ios-simulator.sh b/scripts/run-ios-simulator.sh new file mode 100755 index 0000000..bdfea2e --- /dev/null +++ b/scripts/run-ios-simulator.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# run-ios-simulator.sh — install and launch VoiceCatiOS on an iPhone simulator. +# +# Picks a simulator in this order: +# 1. Any already-booted iPhone simulator. +# 2. The first available iPhone simulator (boots it, opens Simulator.app). +# Pass --device or --udid to pin a specific device. +# +# Usage: +# scripts/run-ios-simulator.sh # install + launch (Debug build) +# scripts/run-ios-simulator.sh --config Release +# scripts/run-ios-simulator.sh --device "iPhone 16 Pro" +# scripts/run-ios-simulator.sh --udid +# scripts/run-ios-simulator.sh --build # run build-ios-client.sh first +# scripts/run-ios-simulator.sh --log # stream app logs after launch +# scripts/run-ios-simulator.sh -h|--help +# +# The app is installed via `xcrun simctl install` and launched via +# `xcrun simctl launch`. With --log, log lines stream to stdout until Ctrl+C +# (this does not kill the app). +# +# Bundle ID: cat.voice.VoiceCatiOS +# macOS only. Requires Xcode. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VC_SCRIPT_NAME="run-ios-simulator" +source "$SCRIPT_DIR/common.sh" + +XCODE_CONFIG="Debug" +DEVICE_FILTER="" +DEVICE_UDID="" +DO_BUILD=false +DO_LOG=false +while [[ $# -gt 0 ]]; do + case "$1" in + --config) XCODE_CONFIG="$2"; shift 2 ;; + --device) DEVICE_FILTER="$2"; shift 2 ;; + --udid) DEVICE_UDID="$2"; shift 2 ;; + --build) DO_BUILD=true; shift ;; + --log) DO_LOG=true; shift ;; + -h|--help) vc_print_help "$0"; exit 0 ;; + *) vc_die "unknown arg: $1 (try --help)" ;; + esac +done + +vc_require_macos + +# ── Optionally build first ───────────────────────────────────────────────────── +if $DO_BUILD; then + vc_step "building VoiceCatiOS" + "$SCRIPT_DIR/build-ios-client.sh" --config "$XCODE_CONFIG" +fi + +# ── Locate the .app ─────────────────────────────────────────────────────────── +APP="$VC_REPO_ROOT/clients/apple/iOS/build/${XCODE_CONFIG}-iphonesimulator/VoiceCatiOS.app" +if [[ ! -d "$APP" ]]; then + vc_err "VoiceCatiOS.app not found at:" + vc_err " $APP" + vc_die "build it first with: scripts/build-ios-client.sh (or pass --build)" +fi +vc_log "app: $APP" + +# ── Find a simulator ────────────────────────────────────────────────────────── +# Resolve: explicit UDID > explicit name filter > booted iPhone > any iPhone. +find_sim() { + # Each line from simctl looks like: + # " iPhone 16 (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) (Shutdown)" + local filter="${1:-}" state="${2:-}" + local pattern="iPhone" + [[ -n "$filter" ]] && pattern="$filter" + xcrun simctl list devices available 2>/dev/null \ + | grep -E "[[:space:]]$pattern" \ + | { [[ -n "$state" ]] && grep "$state" || cat; } \ + | head -1 \ + | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' \ + | head -1 +} + +sim_name() { + xcrun simctl list devices available 2>/dev/null \ + | grep "$1" | head -1 \ + | sed 's/[[:space:]]*('"$1"').*//' | sed 's/^[[:space:]]*//' +} + +if [[ -n "$DEVICE_UDID" ]]; then + UDID="$DEVICE_UDID" +elif [[ -n "$DEVICE_FILTER" ]]; then + UDID="$(find_sim "$DEVICE_FILTER")" + [[ -n "$UDID" ]] || vc_die "no available simulator matching '$DEVICE_FILTER'" +else + # Prefer a booted simulator; boot one if none is active. + UDID="$(find_sim "" "Booted")" + if [[ -z "$UDID" ]]; then + UDID="$(find_sim "")" + [[ -n "$UDID" ]] || vc_die "no iPhone simulator found. + Install an iOS runtime: Xcode > Settings > Platforms > iOS." + NAME="$(sim_name "$UDID")" + vc_step "booting simulator: $NAME ($UDID)" + xcrun simctl boot "$UDID" + open -a Simulator + # Give Simulator.app a moment to display the device. + sleep 2 + fi +fi + +NAME="$(sim_name "$UDID")" +vc_log "simulator: $NAME ($UDID)" + +# If the simulator exists but Simulator.app isn't open, open it. +open -a Simulator &>/dev/null || true + +# ── Install ─────────────────────────────────────────────────────────────────── +vc_step "installing VoiceCatiOS" +xcrun simctl install "$UDID" "$APP" +vc_ok "installed -> $UDID" + +# ── Launch ──────────────────────────────────────────────────────────────────── +BUNDLE_ID="cat.voice.VoiceCatiOS" +vc_step "launching $BUNDLE_ID" +xcrun simctl launch "$UDID" "$BUNDLE_ID" +vc_ok "launched — VoiceCat is running on $NAME" + +# ── Optional log streaming ──────────────────────────────────────────────────── +if $DO_LOG; then + vc_log "streaming logs (Ctrl+C to stop — does not kill the app)" + xcrun simctl spawn "$UDID" log stream --predicate "subsystem contains \"VoiceCat\"" +fi