# 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: ```bash CORE_CRATE=netdesk-frb LIB_NAME=netdesk_frb packaging/build-ios.sh --team ``` --- ## 2. Prerequisites ### 2.1 Apple Developer Program — **$99/year** A paid **Apple Developer Program** membership (, 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 ```bash # 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: ```bash 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 `.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`](../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: ```ruby 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) ```bash # 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: ```bash 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): ```bash # 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 ```bash # TestFlight / App Store (App Store Connect API key): xcrun altool --upload-app -f ui/build/ios/ipa/netdesk.ipa -t ios \ --apiKey --apiIssuer ``` 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 -rf`s 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) |