Cosmo Realtime SDK
Guides

Packaging a macOS app

Embedding LiveKit's binary frameworks, the rpath, signing order, entitlements, and the microphone purpose string — what `swift build` alone does not do.

swift build links the SDK's binary dependencies but does not embed them. A hand-assembled .app therefore builds clean and dies at launch:

dyld[13110]: Library not loaded: @rpath/LiveKitWebRTC.framework/LiveKitWebRTC

Xcode's Embed Frameworks phase does this for you. Nothing else does — not swift build, not swift run, not a shell script that copies the binary into a bundle. This page covers what an app bundle needs beyond the compile.

None of this applies to a plain command-line binary you run with swift run. It applies the moment you ship a .app.

The short path: an Xcode app target

If you can use an Xcode app target, do. Add the package dependency and add the CosmoRealtime product to your target's Frameworks, Libraries, and Embedded Content — that is all. Xcode embeds and signs the transitive binary frameworks for you, sets the rpath, and gets the signing order right. (You do not set the package product itself to Embed & Sign; it is a source product, and its binary dependencies are what get embedded. Our own Mac app links it exactly this way, with no Embed Frameworks phase.) Skip to Entitlements.

Everything below is for hand-assembled bundles: CI packaging, a SwiftPM-only build, or a script that produces the .app.

1. Copy the frameworks in

The binary xcframeworks resolve under .build/artifacts/. Copy the macOS slice of each into the bundle:

APP="YourApp.app"
mkdir -p "$APP/Contents/Frameworks"

BIN_DIR="$(swift build --show-bin-path)"
find "$BIN_DIR" -maxdepth 1 -name '*.framework' -exec cp -R {} "$APP/Contents/Frameworks/" \;
find "$BIN_DIR" -maxdepth 1 -name '*.dylib'     -exec cp    {} "$APP/Contents/Frameworks/" \;

Flat .dylibs matter as much as the frameworks — the binary links them through @rpath too, and a missing one fails identically at launch.

2. Add the rpath

@rpath/LiveKitWebRTC.framework/LiveKitWebRTC only resolves if the executable carries a run-path that reaches Contents/Frameworks:

install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/YourApp"

In an Xcode target this is LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks.

3. Sign nested code first, then the bundle

Signing order is not cosmetic. codesign seals the bundle's contents into Contents/_CodeSignature/CodeResources; signing the outer app before its nested frameworks seals stale signatures, and verification fails.

for fw in "$APP"/Contents/Frameworks/*; do
  codesign --force --options runtime --timestamp --sign "$IDENTITY" "$fw"
done
codesign --force --options runtime --timestamp \
  --entitlements YourApp.entitlements --sign "$IDENTITY" "$APP"

--timestamp on every nested item and on the outer app is required for notarization — a signature without a secure timestamp is rejected.

Re-signing the frameworks with your own identity is also what makes them load. Under the hardened runtime, library validation admits code signed by Apple or by the same Team ID as the main executable. Copied-in frameworks arrive carrying their upstream signature, so a bundle that ships them unmodified fails at launch with:

code signature ... not valid for use in process:
mapping process and mapped file (non-platform) have different Team IDs

com.apple.security.cs.disable-library-validation also silences that error, and you will find it recommended. Don't reach for it here. It is meant for apps that deliberately load third-party plug-ins they cannot sign, and it weakens every load in the process. Re-signing the embedded frameworks fixes the cause.

4. Entitlements

EntitlementWhen
com.apple.security.device.audio-inputMicrophone capture under the hardened runtime.
com.apple.security.network.clientOnly under App Sandbox — the SDK's REST call and WebRTC connections need outbound network. Non-sandboxed apps do not declare it.

Add only what your configuration requires. A hardened-runtime, non-sandboxed voice app needs just audio-input — that is exactly what our own shipped Mac app declares.

If you additionally enable App Sandbox, check the current sandbox entitlement names against Apple's documentation rather than assuming the hardened-runtime ones carry over; the microphone key differs between the two.

5. NSMicrophoneUsageDescription

An app bundle that captures audio must declare the purpose string in Info.plist. Without it macOS does not prompt — it terminates the app the moment capture is attempted, so this shows up as a crash on first Talk, not as silence:

<key>NSMicrophoneUsageDescription</key>
<string>YourApp uses the microphone so you can talk to the assistant.</string>

This is also why microphone behavior differs between swift run and a real app. A bare SwiftPM executable has no bundle and therefore no purpose string of its own — it runs under the host terminal's microphone grant. That is fine for a prototype and misleading as a test of your shipped app: grant the terminal microphone access once and the CLI works, while the same code in an unconfigured .app does not.

6. Verify before you ship

codesign --verify --deep --strict --verbose=2 "$APP"
open "$APP"    # must not die in dyld

In CI, build the bundle and assert it launches — a --smoke-test flag that initializes the SDK and exits 0 is enough. A packaging regression is invisible to swift build and to unit tests; only launching catches it.

Next steps

On this page