docs+scripts: add iOS build and simulator run commands

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
This commit is contained in:
2026-06-19 02:19:38 +02:00
parent e26e7db5b1
commit bf37fe8f0f
4 changed files with 378 additions and 13 deletions

View File

@@ -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

101
scripts/build-ios-client.sh Executable file
View File

@@ -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"

129
scripts/run-ios-simulator.sh Executable file
View File

@@ -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 <UUID>
# 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