Files

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 sure cargo, rustup, flutter and xcrun are on PATH (source $HOME/.cargo/env, Flutter's bin/ 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

# 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 app
  • packaging/dist/ios/NetDeskCore.xcframework — the Rust core (also staged into ui/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)

  1. Preflight — macOS, Xcode, rustup/cargo, flutter, and a Team ID (unless --skip-ipa).
  2. rustup target add aarch64-apple-ios aarch64-apple-ios-sim.
  3. cargo build --release the core staticlib for both targets (IPHONEOS_DEPLOYMENT_TARGET=13.0).
  4. xcodebuild -create-xcframework → NetDeskCore.xcframework.
  5. flutter create ui/ios if missing, copy the xcframework + podspec in, inject the pod into the Podfile, set the signing team in the Flutter xcconfigs, write ExportOptions.plist, pod install.
  6. flutter build ipa with 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 … (and rustup target add x86_64-apple-ios). The device slice is always aarch64-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 build is slow; later ones are cached.
  • Deployment target. Keep IPHONEOS_DEPLOYMENT_TARGET, the podspec platform :ios, and the Runner's min iOS in sync (all 13.0 here). 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 --org doesn'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_bridge
    • ffi in ui/pubspec.yaml, add core/src/ffi.rs, generate the header with cbindgen, add -headers to the xcodebuild -create-xcframework call, and run flutter_rust_bridge_codegen generate). Until then the archive may be dead-stripped out of the app — harmless; the signed .ipa still builds. See the commented -force_load block in netdesk_core.podspec to force a link test before Phase 3.
  • xcodebuild -create-xcframework refuses to overwrite an existing output — the script rm -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)