When no simulator is booted, grep -oE finds no UUID and exits 1. With set -euo pipefail, that non-zero exit propagates through the command substitution and kills the script at the UDID= assignment before it can fall through to the boot-one branch. Fix: add || true to both find_sim and sim_name pipelines so they always return 0 regardless of whether a match was found. Also replace sleep 2 with xcrun simctl bootstatus <UDID> -b, which blocks until the simulator is fully booted — more reliable than a fixed delay and faster when the simulator starts quickly. Update docs/building.md §9 to show the bootstatus command.
19 KiB
Building & Manual Testing
This doc explains what each CMake preset in CMakePresets.json is
for, which one to actually use day-to-day, and the commands to stand up a real server +
vccli clients against each other for manual testing. For the one-paragraph quick-start see
CLAUDE.md; for ctest targets see AGENTS.md. This doc is
the missing middle: how the presets relate to each other, which platform each targets,
and how to drive the binaries by hand.
Quick navigation:
| What you want to build | Section | Key command |
|---|---|---|
Server + vccli + tests (all platforms) |
§3 | cmake --preset dev && cmake --build --preset dev && ctest --preset dev |
| Production server (stripped, no tests) | §5 | cmake --preset server-release && cmake --build --preset server-release |
| Windows client (C# / WinForms) | §7 | dotnet build clients/windows/VoiceCat.slnx |
| macOS client (AppKit) | §8 | scripts/build-macos-client.sh |
| iOS client (SwiftUI / simulator) | §9 | scripts/build-ios-client.sh |
| Launch iOS app on simulator | §9 | scripts/run-ios-simulator.sh |
| Swift core + tests | §8 | cd clients/apple && swift test |
1. What each preset is for
| Preset | Binary dir | Deps | Build type | Server | Tools | Tests | Strip | Platform | What it's for |
|---|---|---|---|---|---|---|---|---|---|
vcpkg-common |
— | vcpkg | — | — | — | — | — | all | Hidden base. Sets the vcpkg toolchain wrapper (cmake/voicecat-toolchain.cmake) which auto-resolves the triplet from the host platform. Not used directly. |
skeleton |
build/skeleton |
none | Debug | ON | ON | ON | no | all | The M0 skeleton. Compiles with just a C++20 compiler — no VCPKG_ROOT needed. Subsystems are stubs (VC_ERR_NOT_IMPLEMENTED). Good for "does the repo even build" sanity checks. Runs 2 tests (smoke + frame_codec). |
dev |
build/dev |
vcpkg | Debug | ON | ON | ON | no | all | The one you actually want. Day-to-day development: real protocol, crypto, voice, server — everything. Builds server + tools + tests (21 tests). Works on Windows, Linux, and macOS (triplet auto-resolved). |
release |
build/release |
vcpkg | Release | ON | ON | ON | no | all | Optimized build with the full test suite. Use to run tests against optimized code, profile, or catch optimizer-sensitive bugs. Symbols kept (not stripped) so stack traces and profiling remain useful. |
server-release |
build/server-release |
vcpkg | Release | ON | ON | OFF | yes | all | Production-shaped build for deployment. Optimized + stripped binaries (-s), no tests. This is what you'd ship/run — see docs/deployment.md. |
windows-client |
build/windows-client |
vcpkg | Release | OFF | OFF | OFF | no | Windows | Produces a redistributable voicecat.dll for the C# WinForms client (M4). Static MinGW runtime — no libgcc_s_seh-1.dll etc. See clients/windows/README.md. |
apple-dev |
build/apple-dev |
vcpkg | Release | OFF | OFF | OFF | no | macOS | Static libvoicecat.a for the Swift Package / XCFramework (macOS slice). Validated on macOS 26.5 / Apple Silicon — builds green, produces valid arm64 .a + XCFramework. See clients/apple/README.md. |
apple-ios |
build/apple-ios |
vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS | Scaffolding — cross-compiled static libvoicecat.a for iOS device (arm64-ios). One XCFramework slice. Not yet CI-validated. |
apple-ios-sim |
build/apple-ios-sim |
vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS sim | Scaffolding — cross-compiled static libvoicecat.a for iOS simulator (arm64-ios-sim). One XCFramework slice. Not yet CI-validated. |
So in practice there are three presets that matter for day-to-day work:
dev— everything: real protocol, real voice, real manual testing. This is the loop you run constantly.release— same suite, optimized. Run it when you want to check optimized behavior or profile.skeleton— fast no-deps build to confirm the stub path still compiles (CI smoke check).
The rest are purpose-specific: server-release for deployment, windows-client for the DLL,
apple-* for Apple platform slices.
Platform matrix
The vcpkg presets (dev, release, server-release, windows-client, apple-*) auto-resolve
the vcpkg triplet via cmake/voicecat-toolchain.cmake:
| Host platform | Auto-resolved triplet | Notes |
|---|---|---|
| Windows (MinGW/MSYS2) | x64-mingw-static |
The project's Windows toolchain. MSVC users must set VCPKG_TARGET_TRIPLET=x64-windows explicitly. |
| Linux x64 | x64-linux |
Server's primary deployment target (Docker, systemd). |
| Linux arm64 | arm64-linux |
Raspberry Pi / ARM VPS. |
| macOS (Apple Silicon) | arm64-osx |
apple-dev uses this automatically. |
| macOS (Intel) | x64-osx |
apple-dev uses this automatically. |
Cross-compile presets (apple-ios, apple-ios-sim) override VCPKG_TARGET_TRIPLET explicitly;
VCPKG_HOST_TRIPLET stays the host's (e.g. arm64-osx when building iOS on Apple Silicon).
Preset history
The preset set was cleaned up on 2026-06-18 (see PROGRESS.md). The old names map as follows:
| Old name | New name | Notes |
|---|---|---|
dev |
skeleton |
Renamed to reflect its actual purpose (no-deps stub smoke check). |
m1-dev |
dev |
Renamed — the project is past M5, so milestone-named presets were misleading. This is now the default development preset. |
m2-dev |
(dropped) | Was cache-identical to m1-dev (same flags, same triplet, only the binary dir differed). Removed. |
server-release |
server-release |
Unchanged name; now stripped (-s) and auto-triplet. |
| (new) | release |
New: optimized build with tests on, symbols kept. |
windows-client |
windows-client |
Unchanged name; triplet now auto-resolved. |
| (new) | apple-dev, apple-ios, apple-ios-sim |
New: Apple platform scaffolding. |
If you see m1-dev or m2-dev in old scripts, commits, or PROGRESS.md history entries,
use dev instead. Historical PROGRESS.md entries are left intact as a true record of what
was run.
2. One-time setup for the real-deps presets
dev, release, server-release, windows-client, and apple-* all need VCPKG_ROOT
pointing at a bootstrapped vcpkg checkout:
# once:
git clone https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.sh # .bat on Windows
# every shell session (PowerShell):
$env:VCPKG_ROOT = "D:\path\to\vcpkg"
# or on Linux/macOS:
export VCPKG_ROOT=/path/to/vcpkg
vcpkg.json (manifest mode) pins every dependency (protobuf, mbedTLS, libsodium, asio,
sqlite3, spdlog, opus, miniaudio) — cmake --preset dev resolves and builds them
automatically on first configure. That first configure is slow (vcpkg building from source);
subsequent ones are cached.
3. Build + test (the loop you'll run constantly)
cmake --preset dev
cmake --build --preset dev
ctest --preset dev
Binaries land in build/dev/bin/ (.exe suffix on Windows):
build/dev/bin/voicecat-serverbuild/dev/bin/vcclibuild/dev/bin/voicecat-admin
To run the same suite against optimized code:
cmake --preset release
cmake --build --preset release
ctest --preset release
4. Manual testing: server + two clients
Start the server
./build/dev/bin/voicecat-server --name "Test Server" --data-dir ./voicecat-data
First run generates an Ed25519 identity + self-signed cert under --data-dir, creates the
SQLite store, and creates a default "Lobby" channel. Other server flags:
--port <n> control+media port (default 8384)
--data-dir <path> data directory (default ./voicecat-data)
--name <name> server name
--no-guests disable guest access (then provision accounts via voicecat-admin)
--print-config print effective config and exit
--version print version and exit
Drive it with vccli
vccli is the headless client used to exercise the protocol by hand. Full flag list:
vccli [--host H] [--port P] [--nick NAME] [--channel ID]
[--voice] [--mute] [--text MSG] [--list-devices]
[--input-device ID] [--input-mode vad|ptt] [--share-screen-audio]
--host H server host (default 127.0.0.1)
--port P server TCP port (default 8384)
--nick NAME guest nickname (default vccli-test)
--channel ID channel to join after auth (default 1, Lobby)
--voice start a MIC stream and stay connected until Ctrl+C
--mute start with the mic muted (only meaningful with --voice)
--text MSG send MSG to the channel, then exit
--list-devices print input/output devices (vc_list_devices) and exit
--input-device ID use device ID (from --list-devices) for the MIC stream
--input-mode vad|ptt send-side input gate mode (default vad)
--share-screen-audio also start a SCREEN_AUDIO stream (WASAPI loopback on Windows)
While --voice is running, stdin accepts ptt on, ptt off, mode vad, mode ptt to
toggle the input gate live.
Smoke test — two clients talking:
# terminal A
./build/dev/bin/vccli --nick Alice --text "hello from Alice"
# terminal B (separate window, after A confirms it sent)
./build/dev/bin/vccli --nick Bob --text "hello from Bob"
Real voice between two clients (needs working mic/speakers, two terminals):
# terminal A
./build/dev/bin/vccli --nick Alice --voice
# terminal B
./build/dev/bin/vccli --nick Bob --voice
Speak into the mic on one side; you should hear it on the other. Ctrl+C to disconnect.
Enumerate audio devices before picking one:
./build/dev/bin/vccli --list-devices
./build/dev/bin/vccli --nick Alice --voice --input-device <ID> --input-mode ptt
Provisioning a non-guest account (if the server was started with --no-guests):
./build/dev/bin/voicecat-admin --data-dir ./voicecat-data account add alice --password secret
./build/dev/bin/voicecat-admin --data-dir ./voicecat-data account list
5. server-release (production-shaped build)
Same dependency story, but Release build type, stripped binaries, and no tests — this is the
closest local analogue to what docs/deployment.md's "from source" path
produces:
cmake --preset server-release
cmake --build --preset server-release
./build/server-release/bin/voicecat-server
Use this to sanity-check release-mode behavior (e.g. perf, optimized codepaths) — not for
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
The apple-dev, apple-ios, and apple-ios-sim presets produce static libvoicecat.a
slices for the Swift Package / XCFramework. All three are validated on macOS 26.5 / Apple
Silicon. Build on macOS:
# macOS slice (arm64-osx on Apple Silicon, x64-osx on Intel)
cmake --preset apple-dev
cmake --build --preset apple-dev
# → build/apple-dev/lib/libvoicecat.a
# iOS device slice (arm64-ios)
cmake --preset apple-ios
cmake --build --preset apple-ios
# → build/apple-ios/lib/libvoicecat.a
# iOS simulator slice (arm64-ios-simulator)
cmake --preset apple-ios-sim
cmake --build --preset apple-ios-sim
# → build/apple-ios-sim/lib/libvoicecat.a
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:
# 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 for the XCFramework internals (fat static lib merge, module map staging).
7. Windows client (C# / WinForms)
The Windows client is a .NET 10 WinForms app that loads voicecat.dll (the MinGW-built
shared library from the windows-client preset) via P/Invoke. Full details in
clients/windows/README.md.
Prerequisites: .NET SDK 10, MinGW-w64 / MSYS2 UCRT64 (GCC 13+), vcpkg.
Build the DLL
cmake --preset windows-client
cmake --build --preset windows-client
# → build/windows-client/bin/voicecat.dll
Build the C# solution
cd clients/windows
dotnet build VoiceCat.slnx
Directory.Build.props copies voicecat.dll into the output directory automatically.
Run the app
# Terminal 1 — start the server (built with the dev preset)
./build/dev/bin/voicecat-server.exe --name "My Server"
# Terminal 2 — launch the client
dotnet run --project clients/windows/VoiceCat.App/VoiceCat.App.csproj
Run the C# interop tests
dotnet test clients/windows/VoiceCat.slnx
8. macOS client (AppKit)
The macOS client is an Xcode project (AppKit / Swift) that links libvoicecat via the
VoiceCatCore Swift Package, which consumes a binary XCFramework target. Full details in
clients/apple/README.md.
Prerequisites: Xcode, vcpkg (VCPKG_ROOT set), macOS 14+ (deployment target).
Build the XCFramework
The XCFramework is a local build artifact (gitignored, like the Windows DLL). It bundles
libvoicecat.a + all vcpkg static deps into a single fat .a per slice, plus staged
headers with a module map so Swift gets import VoiceCatC.
# macOS slice only (default, validated)
clients/apple/scripts/build-xcframework.sh
# → clients/apple/VoiceCatCore.xcframework/
# All 3 slices (macOS + iOS device + iOS sim — iOS still scaffolding)
clients/apple/scripts/build-xcframework.sh --all
The script runs cmake --preset apple-dev + cmake --build --preset apple-dev internally,
then merges vcpkg's static deps with libtool -static and stitches the XCFramework with
xcodebuild -create-xcframework.
Build the Swift core (SPM)
cd clients/apple
swift build # builds VoiceCatCore library
swift test # 6 smoke tests against a real voicecat-server
swift test requires the dev CMake preset to be built
(build/dev/bin/voicecat-server + voicecat-admin).
Build the macOS app (Xcode)
xcodebuild -project clients/apple/macOS/VoiceCatMac.xcodeproj \
-scheme VoiceCatMac -configuration Debug build
# → ~/Library/Developer/Xcode/DerivedData/VoiceCatMac-*/Build/Products/Debug/VoiceCatMac.app
Or open the project in Xcode and build from the UI:
open clients/apple/macOS/VoiceCatMac.xcodeproj
Run the app
# Terminal 1 — start the server (built with the dev preset)
./build/dev/bin/voicecat-server --name "My Server"
# 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/:
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.
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:
clients/apple/scripts/build-xcframework.sh --all
# → clients/apple/VoiceCatCore.xcframework/
# ├── macos-arm64/
# ├── ios-arm64/
# └── ios-arm64-simulator/
Build the iOS simulator app
# 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
# 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:
# Boot a simulator (if not already running):
xcrun simctl boot <UDID>
open -a Simulator
xcrun simctl bootstatus <UDID> -b # wait until boot is complete before installing
# Install the built .app:
xcrun simctl install <UDID> clients/apple/iOS/build/Debug-iphonesimulator/VoiceCatiOS.app
# Launch the app:
xcrun simctl launch <UDID> cat.voice.VoiceCatiOS
# Stream logs (Ctrl+C to stop — does not kill the app):
xcrun simctl spawn <UDID> log stream --predicate 'subsystem contains "VoiceCat"'
Run the full stack (server + iOS simulator)
# 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
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.