13 KiB
NetDesk — iOS build (.ipa)
How to build the signed NetDesk iOS app — the Flutter UI on top of the compiled
Rust netdesk-core engine — and ship it to TestFlight / the App Store.
This only builds on a Mac. iOS toolchains (Xcode, the iOS SDKs,
xcodebuild, code signing) exist on macOS only. Nothing here can run on the Windows dev box — Windows is used to author these scripts, the Mac runs them. Everything below assumes macOS + Xcode.
1. What gets built
core crate (netdesk-core, lib netdesk_core)
│ cargo build --release --target aarch64-apple-ios → libnetdesk_core.a (device)
│ cargo build --release --target aarch64-apple-ios-sim → libnetdesk_core.a (simulator)
▼
NetDeskCore.xcframework (xcodebuild -create-xcframework, both slices)
│ vendored via ui/ios/netdesk_core.podspec + pod 'netdesk_core' in the Podfile
▼
Flutter iOS Runner ── flutter build ipa ──► ui/build/ios/ipa/netdesk.ipa
The Rust static library is the FFI surface the Flutter UI binds to. Today that
is the workspace core crate (crate-type = ["lib","staticlib","cdylib"],
lib name netdesk_core → libnetdesk_core.a). When the Phase-3
flutter_rust_bridge wrapper crate lands under a different package name (e.g.
netdesk-frb), build it with the same script by overriding two variables:
CORE_CRATE=netdesk-frb LIB_NAME=netdesk_frb packaging/build-ios.sh --team <TEAM_ID>
2. Prerequisites
2.1 Apple Developer Program — $99/year
A paid Apple Developer Program membership (https://developer.apple.com/programs/, US$99/yr) is required to:
- create a distribution signing certificate,
- register an App ID (bundle identifier) and a provisioning profile,
- upload to App Store Connect / TestFlight.
The free personal team can only sideload to your own device for 7 days — not enough to ship. Enroll first; enrollment can take a day or two to clear.
2.2 Signing certificate + provisioning profile
You need, inside your developer account:
| Thing | Where | Notes |
|---|---|---|
| Team ID | developer.apple.com → Membership | 10 chars, e.g. ABCDE12345. You pass this to the script. |
| App ID / bundle id | Certificates, IDs & Profiles → Identifiers | Must match the app's PRODUCT_BUNDLE_IDENTIFIER (default space.netdesk.netdesk, set via --org). |
| Distribution certificate | Certificates, IDs & Profiles → Certificates → Apple Distribution | One per team; Xcode can create it for you. |
| Provisioning profile | Certificates, IDs & Profiles → Profiles → App Store (or Ad Hoc) | Ties the App ID + certificate + devices. |
Easiest path — automatic signing. Open the project once in Xcode, sign in
with your Apple ID, tick Automatically manage signing, pick your Team. Xcode
creates the certificate + profile for you and the command-line build reuses
them. build-ios.sh writes DEVELOPMENT_TEAM + CODE_SIGN_STYLE = Automatic
into ui/ios/Flutter/{Release,Debug}.xcconfig, so after that one-time Xcode
sign-in the whole build is non-interactive.
Manual signing (CI, locked-down teams): download the distribution
certificate (.p12) and the provisioning profile (.mobileprovision), install
both into the login keychain / ~/Library/MobileDevice/Provisioning Profiles/,
then set signingStyle to manual and add a provisioningProfiles dict in
ui/ios/ExportOptions.plist.
2.3 Toolchains on the Mac
# Xcode + command line tools
xcode-select --install # or install full Xcode from the App Store
sudo xcode-select -s /Applications/Xcode.app
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Flutter SDK (>= 3.27, per ui/pubspec.yaml) — add its bin/ to PATH
# https://docs.flutter.dev/get-started/install/macos
flutter doctor
# CocoaPods (Flutter will also install pods during the build)
sudo gem install cocoapods
PATH note. On Windows the project builds with
export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:/c/dev/flutter/bin:$PATH". On the Mac that line does not apply — just make surecargo,rustup,flutterandxcrunare on PATH (source $HOME/.cargo/env, Flutter'sbin/in your shell profile).
3. Generate the Flutter iOS project (one time)
ui/ currently has lib/ + pubspec.yaml but no ios/ folder — it was
never generated (it's platform scaffolding, not source). Create it on the Mac:
cd ui
flutter create --org space.netdesk --project-name netdesk --platforms=ios .
This adds ui/ios/ (the Runner Xcode project, Runner.xcworkspace,
Info.plist, launch assets, the Flutter xcconfig layer) without touching your
lib/ Dart code. The bundle identifier becomes <org>.netdesk, i.e.
space.netdesk.netdesk — change --org to use your own (e.g.
--org com.akronlogic → com.akronlogic.netdesk), and register that exact id
as the App ID in your developer account.
On the Windows box the equivalent invocation (for reference only — it does not produce a buildable iOS app on Windows) is:
C:\dev\flutter\bin\flutter.bat create --platforms=ios .
build-ios.sh runs this step for you if ui/ios/ does not exist, so you
can skip straight to §5. Run it by hand only if you want to pick the --org
yourself before the first build.
4. The VideoToolbox decode path (why the frameworks matter)
NetDesk's iOS H.264 decode is hardware via VideoToolbox, implemented in
core/src/decode_vt.rs and gated
#[cfg(any(target_os = "macos", target_os = "ios"))]. It feeds Annex-B →
AVCC-repacked access units into a VTDecompressionSession, builds the
CMVideoFormatDescription from the first keyframe's SPS/PPS, and asks for
32-bit BGRA output (VideoToolbox does the YUV→RGB on the GPU); openh264 is the
software fallback if the session can't start.
That module links three Apple system frameworks directly with
#[link(kind = "framework")]:
| Framework | Used for |
|---|---|
| VideoToolbox | VTDecompressionSession* (the decoder itself) |
| CoreMedia | CMVideoFormatDescription / CMSampleBuffer / CMBlockBuffer |
| CoreVideo | CVPixelBuffer* (the decoded BGRA surface) |
| CoreFoundation | CFDictionary/CFString/CFRelease (via core-foundation-sys, for the pixel-buffer attributes) |
A static Rust library records those #[link] directives in its metadata but
does not pull the frameworks into the final app link. So the podspec
(ui/ios/netdesk_core.podspec) names them explicitly:
s.frameworks = 'VideoToolbox', 'CoreMedia', 'CoreVideo', 'CoreFoundation'
If you ever see link errors like Undefined symbol: _VTDecompressionSessionCreate
or _CMVideoFormatDescriptionCreateFromH264ParameterSets, a framework is
missing from that line. At runtime, the log line
decode: VideoToolbox H.264 (hardware) confirms the hardware path was selected.
5. Build the .ipa — the exact commands
One command (recommended)
# from the repo root, on the Mac:
packaging/build-ios.sh --team ABCDE12345
Replace ABCDE12345 with your 10-character Apple Team ID. That script does
every step in §6 and prints the path to the finished .ipa. Useful variants:
packaging/build-ios.sh --team ABCDE12345 --method app-store # default: App Store / TestFlight
packaging/build-ios.sh --team ABCDE12345 --method ad-hoc # ad-hoc distribution
packaging/build-ios.sh --team ABCDE12345 --method development # dev devices
packaging/build-ios.sh --team ABCDE12345 --org com.akronlogic # bundle id com.akronlogic.netdesk (first run only)
packaging/build-ios.sh --skip-ipa # build NetDeskCore.xcframework only, no signing needed
Output:
ui/build/ios/ipa/netdesk.ipa— the signed apppackaging/dist/ios/NetDeskCore.xcframework— the Rust core (also staged intoui/ios/)
The same steps by hand
If you'd rather run it step by step (this is exactly what the script does):
# 0. Rust iOS targets
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
# 1. Build the Rust core static lib for device + simulator
export IPHONEOS_DEPLOYMENT_TARGET=13.0
cargo build --release -p netdesk-core --target aarch64-apple-ios
cargo build --release -p netdesk-core --target aarch64-apple-ios-sim
# 2. Assemble the xcframework (delete any stale one first)
rm -rf packaging/dist/ios/NetDeskCore.xcframework
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libnetdesk_core.a \
-library target/aarch64-apple-ios-sim/release/libnetdesk_core.a \
-output packaging/dist/ios/NetDeskCore.xcframework
# 3. Generate ui/ios if needed, then wire the xcframework in
cd ui
flutter create --org space.netdesk --project-name netdesk --platforms=ios . # only if ui/ios is missing
flutter pub get
cp -R ../packaging/dist/ios/NetDeskCore.xcframework ios/
cp ../packaging/ios/netdesk_core.podspec ios/netdesk_core.podspec
# ... add `pod 'netdesk_core', :path => '.'` to ios/Podfile (Runner target),
# ... add DEVELOPMENT_TEAM + CODE_SIGN_STYLE=Automatic to ios/Flutter/*.xcconfig,
# ... write ios/ExportOptions.plist with your teamID + method.
cd ios && pod install && cd ..
# 4. Build + sign the ipa
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
# → ui/build/ios/ipa/netdesk.ipa
Ship it
# TestFlight / App Store (App Store Connect API key):
xcrun altool --upload-app -f ui/build/ios/ipa/netdesk.ipa -t ios \
--apiKey <KEY_ID> --apiIssuer <ISSUER_ID>
Or open ui/build/ios/archive/Runner.xcarchive in Xcode → Window →
Organizer → Distribute App and follow the GUI.
6. What build-ios.sh automates (summary)
- Preflight — macOS, Xcode,
rustup/cargo,flutter, and a Team ID (unless--skip-ipa). rustup target addaarch64-apple-ios aarch64-apple-ios-sim.cargo build --releasethe core staticlib for both targets (IPHONEOS_DEPLOYMENT_TARGET=13.0).xcodebuild -create-xcframework→NetDeskCore.xcframework.flutter createui/iosif missing, copy the xcframework + podspec in, inject the pod into the Podfile, set the signing team in the Flutter xcconfigs, writeExportOptions.plist,pod install.flutter build ipawith your team and export method.
7. Gotchas
- Apple-silicon vs Intel Mac. The simulator slice defaults to
aarch64-apple-ios-sim(Apple silicon). On an Intel Mac build the Intel simulator slice instead:SIM_TARGET=x86_64-apple-ios packaging/build-ios.sh …(andrustup target add x86_64-apple-ios). The device slice is alwaysaarch64-apple-ios. - First build compiles openh264 from source (the software-decode fallback).
That needs the Xcode clang/C++ toolchain (present with Xcode) — the first
cargo buildis slow; later ones are cached. - Deployment target. Keep
IPHONEOS_DEPLOYMENT_TARGET, the podspecplatform :ios, and the Runner's min iOS in sync (all13.0here). Mismatches cause CocoaPods warnings or link errors. - Bundle id must match the App ID. The provisioning profile is tied to a
specific bundle id; if
--orgdoesn't match a registered App ID, signing fails. Register the id in your developer account first. - Phase-3 FFI. This build links the Rust core but the Flutter UI does not
call it yet (the Dart bindings are Phase 3: uncomment
flutter_rust_bridgeffiinui/pubspec.yaml, addcore/src/ffi.rs, generate the header withcbindgen, add-headersto thexcodebuild -create-xcframeworkcall, and runflutter_rust_bridge_codegen generate). Until then the archive may be dead-stripped out of the app — harmless; the signed.ipastill builds. See the commented-force_loadblock innetdesk_core.podspecto force a link test before Phase 3.
xcodebuild -create-xcframeworkrefuses to overwrite an existing output — the scriptrm -rfs the old xcframework first; do the same by hand.
8. Files
| Path | Role |
|---|---|
packaging/build-ios.sh |
the end-to-end Mac build script |
packaging/ios/netdesk_core.podspec |
CocoaPods spec vendoring the xcframework + system frameworks (copied to ui/ios/) |
packaging/ios/Podfile |
Flutter iOS Podfile template with the netdesk_core pod (used if ui/ios/Podfile is absent) |
packaging/dist/ios/NetDeskCore.xcframework |
build output — the compiled Rust core |
core/src/decode_vt.rs |
the VideoToolbox hardware-decode implementation |
docs/cross-platform-decode.md |
the per-platform decode design (VideoToolbox section) |