NetDesk 1.5.0 — source release
This commit is contained in:
commit
40318aafdd
227 files changed
+80798
No files matched your search
@@ -0,0 +1,180 @@
|
||||
# Building NetDesk for Android
|
||||
|
||||
NetDesk's Android app is a Flutter UI (`ui/`) bound, through
|
||||
[flutter_rust_bridge](https://cjycode.com/flutter_rust_bridge/) (FRB v2), to a
|
||||
compiled Rust library. The Rust FFI layer is the **`netdesk-frb`** crate
|
||||
(`frb/`), a `cdylib` that statically links **`netdesk-core`** (signaling, E2E
|
||||
crypto, transport, H.264 decode). On Android the library ships inside the
|
||||
APK/AAB as `lib/<abi>/libnetdesk_frb.so` and the Dart side `dlopen`s it (the FRB
|
||||
loader looks for the stem `netdesk_frb`).
|
||||
|
||||
```
|
||||
netdesk-frb (+ netdesk-core) --cargo-ndk--> ui/android/app/src/main/jniLibs/<abi>/libnetdesk_frb.so
|
||||
Flutter (ui/) --gradle-----> release .apk / split .apk / .aab (signed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The one command
|
||||
|
||||
From the repo root (`netdesk-native/`), in a shell whose `PATH` contains the
|
||||
MinGW tools, cargo, and Flutter:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1
|
||||
```
|
||||
|
||||
That single command:
|
||||
|
||||
1. verifies / auto-provisions the toolchain (cargo-ndk, the rustup Android
|
||||
targets),
|
||||
2. detects the Android NDK at `C:\dev\android-ndk`,
|
||||
3. compiles `netdesk-frb` for **arm64-v8a, armeabi-v7a, x86_64** with cargo-ndk
|
||||
and drops `libnetdesk_frb.so` straight into
|
||||
`ui/android/app/src/main/jniLibs/<abi>/`,
|
||||
4. builds the release **fat APK**, the **per-ABI split APKs**, and the Play
|
||||
**App Bundle (.aab)**.
|
||||
|
||||
Useful switches:
|
||||
|
||||
| Switch | Effect |
|
||||
| --- | --- |
|
||||
| `-Target apk\|split\|aab\|rust\|all` | Build only that deliverable (default `all`; `rust` = just the `.so`). |
|
||||
| `-SkipRust` | Reuse the `.so` already in `jniLibs`, build Flutter only. |
|
||||
| `-Abis arm64-v8a` | Restrict the ABIs (default all three). |
|
||||
| `-GenerateKeystore` | Create the release keystore + `key.properties` first (see below). |
|
||||
| `-MinSdk 21` | Android API the `.so` links against (must be `<=` Flutter `minSdk`). |
|
||||
| `-NdkHome <path>` | Override the NDK location (default `C:\dev\android-ndk`). |
|
||||
|
||||
---
|
||||
|
||||
## Toolchain
|
||||
|
||||
| Component | Version / location used here |
|
||||
| --- | --- |
|
||||
| Rust | stable (cargo 1.99) on `PATH` |
|
||||
| rustup targets | `aarch64-linux-android`, `armv7-linux-androideabi`, `x86_64-linux-android` |
|
||||
| cargo-ndk | 4.1.2 (`cargo install cargo-ndk`) |
|
||||
| Android NDK | r26d (`26.3.11579264`) at `C:\dev\android-ndk` (`ANDROID_NDK_HOME`) |
|
||||
| Android SDK | `%LOCALAPPDATA%\Android\Sdk` |
|
||||
| Flutter | `C:\dev\flutter\bin\flutter.bat` |
|
||||
| JDK (keytool + Gradle) | Android Studio JBR (auto-detected) |
|
||||
| nasm | on `PATH` (needed by `openh264` for the x86_64 ABI) |
|
||||
|
||||
One-time setup (the build script also does the first three automatically):
|
||||
|
||||
```powershell
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
||||
cargo install cargo-ndk
|
||||
# NDK: unzip the Android NDK r26+ to C:\dev\android-ndk (set ANDROID_NDK_HOME)
|
||||
```
|
||||
|
||||
Recommended `PATH` (Git Bash form):
|
||||
|
||||
```bash
|
||||
export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:/c/dev/flutter/bin:$PATH"
|
||||
```
|
||||
|
||||
### What cargo-ndk runs
|
||||
|
||||
The Rust step is exactly:
|
||||
|
||||
```
|
||||
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \
|
||||
-o ui/android/app/src/main/jniLibs --platform 21 \
|
||||
build --release --package netdesk-frb --lib
|
||||
```
|
||||
|
||||
cargo-ndk sets the per-ABI clang/linker from the NDK, and the `-o` flag copies
|
||||
each produced `libnetdesk_frb.so` into `jniLibs/<abi>/`. The `.so` are build
|
||||
artifacts (git-ignored) and are regenerated on every run.
|
||||
|
||||
---
|
||||
|
||||
## Release signing
|
||||
|
||||
Signing credentials live in `ui/android/key.properties`, which is **git-ignored**
|
||||
(so are `*.keystore` / `*.jks`). A committable template is
|
||||
`ui/android/key.properties.example`.
|
||||
|
||||
`app/build.gradle.kts` loads `key.properties` when present and signs the release
|
||||
build with it; when it is absent the release build falls back to **debug**
|
||||
signing so `flutter run --release` still works during development.
|
||||
|
||||
Create the upload keystore once (the password is read from the environment and
|
||||
is **never** written into a committed file):
|
||||
|
||||
```powershell
|
||||
$env:NETDESK_KEYSTORE_PASSWORD = '<your-strong-password>'
|
||||
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -GenerateKeystore
|
||||
```
|
||||
|
||||
This writes:
|
||||
|
||||
* `ui/android/app/netdesk-release.keystore` - RSA 4096, alias `netdesk`, 10000-day validity,
|
||||
* `ui/android/key.properties` - with the alias, store file, and the password you
|
||||
supplied (local only; keep it out of version control).
|
||||
|
||||
> Back up the keystore and remember the password. If you lose either you can no
|
||||
> longer push updates to an app already published under that key (unless it uses
|
||||
> Play App Signing with a separate upload key you can reset).
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
After a full build:
|
||||
|
||||
| Deliverable | Path (under `ui/`) |
|
||||
| --- | --- |
|
||||
| Fat APK (all ABIs) | `build/app/outputs/flutter-apk/app-release.apk` |
|
||||
| Split APKs (per ABI) | `build/app/outputs/flutter-apk/app-arm64-v8a-release.apk`, `-armeabi-v7a-`, `-x86_64-` |
|
||||
| Play App Bundle | `build/app/outputs/bundle/release/app-release.aab` |
|
||||
|
||||
The fat APK is convenient for side-loading / direct download; the split APKs are
|
||||
smaller per device; the `.aab` is what Google Play requires.
|
||||
|
||||
---
|
||||
|
||||
## Publishing to Google Play (AAB)
|
||||
|
||||
1. Produce the signed bundle:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File packaging\build-android.ps1 -Target aab
|
||||
```
|
||||
|
||||
(or run the default `all` target). The bundle is
|
||||
`ui/build/app/outputs/bundle/release/app-release.aab`.
|
||||
|
||||
2. In the [Play Console](https://play.google.com/console), create the app, then
|
||||
upload the `.aab` under a release track (internal testing first).
|
||||
|
||||
3. Enrol in **Play App Signing** (recommended). Google then holds the *app
|
||||
signing key*; the keystore above becomes your *upload key*. If the upload key
|
||||
is ever compromised you can ask Google to reset it - which is why App Signing
|
||||
is preferred over shipping the raw signing key.
|
||||
|
||||
4. `versionCode` / `versionName` come from `ui/pubspec.yaml`
|
||||
(`version: x.y.z+build`). Bump the `+build` number for every Play upload - the
|
||||
Console rejects a duplicate `versionCode`.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **"Android NDK not found"** - the script could not find a complete NDK at
|
||||
`C:\dev\android-ndk` (it checks for `source.properties` +
|
||||
`toolchains\llvm\prebuilt`). Finish extracting the NDK, set `ANDROID_NDK_HOME`,
|
||||
or pass `-NdkHome <path>`. Use `-SkipRust` to build only the Flutter layer
|
||||
against an already-built `.so`.
|
||||
* **x86_64 ABI fails in `openh264`** - that ABI needs `nasm` on `PATH`. arm64-v8a
|
||||
and armeabi-v7a do not. Drop x86_64 with `-Abis arm64-v8a,armeabi-v7a` if you
|
||||
only ship to real devices.
|
||||
* **Gradle wants to download an NDK** - `app/build.gradle.kts` points AGP at
|
||||
`ANDROID_NDK_HOME` when it is set, so the prebuilt (already stripped) `.so` are
|
||||
packaged without a second NDK download. Make sure the env var is set (the build
|
||||
script sets it).
|
||||
* **`UnsatisfiedLinkError: libnetdesk_frb.so`** at runtime - the `.so` for the
|
||||
device's ABI is missing from `jniLibs`. Re-run the Rust step (`-Target rust`)
|
||||
for that ABI, or confirm the FRB loader stem is still `netdesk_frb`.
|
||||
@@ -0,0 +1,288 @@
|
||||
# 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 <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
|
||||
|
||||
```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 `<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`](../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 <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 -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) |
|
||||
@@ -0,0 +1,189 @@
|
||||
# NetDesk — Linux build & packaging
|
||||
|
||||
Build the native `netdesk-viewer` for Linux and bundle the two distributions
|
||||
**NetDesk by Akronlogic** ships on Linux:
|
||||
|
||||
| Edition | Artifact | Install? |
|
||||
|--------------|-----------------------------------|----------------------------------------|
|
||||
| **AppImage** | `NetDesk-<ver>-x86_64.AppImage` | No — `chmod +x` and run anywhere |
|
||||
| **.deb** | `netdesk_<ver>_amd64.deb` | Yes — `apt install ./…` (launcher + icon) |
|
||||
|
||||
Both are produced by [`packaging/build-linux.sh`](../packaging/build-linux.sh).
|
||||
Everything it writes lands in **`packaging/dist/linux/`** (git-ignored). This is
|
||||
the Linux counterpart of the Windows [`packaging/README.md`](../packaging/README.md).
|
||||
|
||||
> This target must be **built on Linux** (x86_64). The viewer links the system
|
||||
> FFmpeg/VAAPI libraries for the Linux hardware-decode path
|
||||
> (`core/src/decode_vaapi.rs`), which are not present when cross-building from
|
||||
> Windows or macOS.
|
||||
|
||||
---
|
||||
|
||||
## 1. Build dependencies
|
||||
|
||||
The Linux decode backend pulls in FFmpeg through the `ffmpeg-next` crate, whose
|
||||
build script (`ffmpeg-sys-next`) locates `libav*` with **pkg-config** and
|
||||
generates bindings with **libclang**. The GUI (eframe/egui on winit + glow)
|
||||
needs OpenGL at runtime. Install the toolchain + dev libraries first.
|
||||
|
||||
### Debian / Ubuntu
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential clang pkg-config nasm \
|
||||
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
|
||||
libavfilter-dev libavdevice-dev libswresample-dev \
|
||||
libva-dev \
|
||||
libgl1-mesa-dev
|
||||
```
|
||||
|
||||
- `build-essential` → the C/C++ linker (`cc`/`g++`) Rust and `openh264-sys2` need.
|
||||
- `clang` → libclang for `ffmpeg-sys-next`'s bindgen.
|
||||
- `pkg-config` → locates the FFmpeg and VAAPI libraries.
|
||||
- `nasm` → assembler for the bundled `openh264` software-decode fallback.
|
||||
- `libav*-dev` → FFmpeg ≥ 4 development libraries (the full set matches the
|
||||
default features of `ffmpeg-next`).
|
||||
- `libva-dev` → VAAPI headers for the hardware-decode path.
|
||||
- `libgl1-mesa-dev` → OpenGL for the glow renderer.
|
||||
|
||||
### Fedora (FFmpeg dev libs come from RPM Fusion)
|
||||
|
||||
```bash
|
||||
sudo dnf install -y gcc gcc-c++ clang pkgconf-pkg-config nasm \
|
||||
ffmpeg-devel libva-devel mesa-libGL-devel
|
||||
```
|
||||
|
||||
### Optional — native file-picker
|
||||
|
||||
The viewer's **File transfer → Upload…** button uses `rfd`, which is compiled
|
||||
with **no Linux backend** in `viewer/Cargo.toml` (`default-features = false`).
|
||||
The build and the app run fine without it, but the picker is a no-op on Linux
|
||||
until `rfd` is given a backend feature:
|
||||
|
||||
- **GTK 3** backend → add `features = ["gtk3"]` to `rfd`, then install
|
||||
`libgtk-3-dev` (build) / `libgtk-3-0` (runtime).
|
||||
- **XDG portal** backend (no GTK) → add `features = ["xdg-portal", "tokio"]`;
|
||||
needs a running `xdg-desktop-portal` at runtime, nothing extra at build.
|
||||
|
||||
This is the only reason `libgtk` is ever involved on Linux — it is **not**
|
||||
required to build or run NetDesk as shipped.
|
||||
|
||||
---
|
||||
|
||||
## 2. The one command
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.cargo/bin:$PATH" # ensure cargo/rustup are reachable
|
||||
packaging/build-linux.sh
|
||||
```
|
||||
|
||||
That runs `cargo build --release -p netdesk-viewer`
|
||||
(target `x86_64-unknown-linux-gnu`) and then produces **both** the `.deb` and
|
||||
the AppImage in `packaging/dist/linux/`:
|
||||
|
||||
```
|
||||
packaging/dist/linux/
|
||||
├── netdesk_0.1.0_amd64.deb ← dpkg package (launcher + hicolor icons)
|
||||
├── NetDesk-0.1.0-x86_64.AppImage ← portable single file
|
||||
├── com.akronlogic.netdesk.desktop (generated launcher, shared by both)
|
||||
├── AppDir/ (AppImage staging)
|
||||
└── deb/ (.deb staging tree)
|
||||
```
|
||||
|
||||
> The underlying build command on its own:
|
||||
> ```bash
|
||||
> cargo build --release -p netdesk-viewer --target x86_64-unknown-linux-gnu
|
||||
> ```
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Effect |
|
||||
|-----------------------|----------------------------------------------------|
|
||||
| `--skip-build` | reuse an existing `target/.../release/netdesk-viewer` |
|
||||
| `--no-appimage` | build the `.deb` only |
|
||||
| `--no-deb` | build the AppImage only |
|
||||
| `--target <triple>` | override the Rust target (default `x86_64-unknown-linux-gnu`) |
|
||||
| `LINUXDEPLOY=/path` | env: use a local `linuxdeploy` instead of downloading it |
|
||||
|
||||
The AppImage step downloads **linuxdeploy** once (cached in
|
||||
`packaging/dist/.tools/`) and runs it with `APPIMAGE_EXTRACT_AND_RUN=1`, so it
|
||||
works in containers/CI without FUSE. linuxdeploy `ldd`-scans the binary and
|
||||
bundles its shared libraries (`libavcodec`, `libavutil`, `libswscale`, …) into
|
||||
the AppImage; `libGL`, `libX11`/`xcb`, Wayland and `libva` are intentionally
|
||||
**not** bundled — they are `dlopen()`ed and must come from the host graphics
|
||||
stack. The `.deb` instead declares those as package `Depends:`.
|
||||
|
||||
---
|
||||
|
||||
## 3. VAAPI hardware-decode note
|
||||
|
||||
NetDesk hardware-decodes the host's H.264 stream on Linux through
|
||||
**VAAPI via FFmpeg/libavcodec** (`core/src/decode_vaapi.rs`): it opens the
|
||||
libavcodec H.264 decoder with a VAAPI `hw_device_ctx` on the default DRI render
|
||||
node (`/dev/dri/renderD128`), decodes on the GPU, pulls the surface to system
|
||||
memory (NV12), crops to the true display size, and swscales to the packed RGB8
|
||||
`Frame` the viewer paints — see [`cross-platform-decode.md`](cross-platform-decode.md)
|
||||
for the full backend matrix.
|
||||
|
||||
**Hardware decode is optional at runtime.** `best_decoder()` tries VAAPI first
|
||||
and **falls back to the openh264 software decoder** if no usable VAAPI device is
|
||||
present, so headless / GPU-less boxes still work. VAAPI is a build dependency
|
||||
(the libraries must link) but not a runtime requirement.
|
||||
|
||||
To get the hardware path at runtime on the target machine:
|
||||
|
||||
1. **Install a VAAPI driver** (the `.deb` lists these as `Recommends`):
|
||||
- Intel (Broadwell+): `intel-media-va-driver` (iHD)
|
||||
- Intel (older): `i965-va-driver`
|
||||
- AMD / others (Mesa): `mesa-va-drivers` (radeonsi)
|
||||
2. **Verify it:** `vainfo` should list an `H264` decode profile (e.g.
|
||||
`VAProfileH264High : VAEntrypointVLD`). Install `vainfo` from `libva-utils`.
|
||||
3. **Render-node access:** the user must be able to read
|
||||
`/dev/dri/renderD128` — typically by membership of the `render` (or `video`)
|
||||
group.
|
||||
4. **Driver override (if needed):** `export LIBVA_DRIVER_NAME=iHD` (or `i965`,
|
||||
`radeonsi`) when autodetection picks the wrong driver.
|
||||
|
||||
Confirm which path is live from the viewer's log:
|
||||
|
||||
```
|
||||
decode: VAAPI H.264 (FFmpeg hardware) ← hardware path selected
|
||||
VAAPI init failed (...); using openh264 ... ← fell back to software
|
||||
```
|
||||
|
||||
If it fell back, check `vainfo`, the `/dev/dri/renderD128` permissions, and
|
||||
`LIBVA_DRIVER_NAME`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
- **`libavcodec` not found via pkg-config / bindgen errors** — the FFmpeg dev
|
||||
libraries or `clang` are missing. Re-run the install command in §1. For FFmpeg
|
||||
in a non-standard prefix, set `PKG_CONFIG_PATH`.
|
||||
- **`openh264` build fails for want of an assembler** — install `nasm`.
|
||||
- **AppImage not produced** (linuxdeploy download/FUSE error) — the script warns
|
||||
and still writes the `.deb`. Supply a local tool with `LINUXDEPLOY=/path`, or
|
||||
run with `--no-appimage`. `APPIMAGE_EXTRACT_AND_RUN=1` is already exported for
|
||||
FUSE-less hosts.
|
||||
- **`.deb` won't install — unmet FFmpeg dependency** — soname package names
|
||||
differ per release; the control file lists `libavcodec60 | 59 | 58 | ffmpeg`
|
||||
(and likewise for `libavutil`/`libswscale`). On an unusual release, installing
|
||||
the distro `ffmpeg` package satisfies the catch-all.
|
||||
- **Wrong window/taskbar icon on GNOME/Wayland** — the launcher file name and
|
||||
its `Icon=` key both use the app-id `com.akronlogic.netdesk`, matching the
|
||||
viewer's `with_app_id(...)`; for the `.deb`, `gtk-update-icon-cache` runs in
|
||||
`postinst`. Log out/in if the shell cached the old icon.
|
||||
|
||||
---
|
||||
|
||||
## 5. Test
|
||||
|
||||
```bash
|
||||
vainfo # confirm a VAAPI driver + H264 profile (optional)
|
||||
cargo test -p netdesk-core # core decode/transport tests
|
||||
./packaging/dist/linux/NetDesk-*.AppImage # smoke-test the viewer
|
||||
```
|
||||
|
||||
Smoke-test end to end against a live host; the log line
|
||||
`decode: VAAPI H.264 (FFmpeg hardware)` confirms the hardware path was selected.
|
||||
@@ -0,0 +1,293 @@
|
||||
# Building the NetDesk desktop app for macOS
|
||||
|
||||
This builds the **same** egui viewer/host-panel binary the Windows editions
|
||||
ship (crate `netdesk-viewer`) as a signed, notarized **universal** macOS app
|
||||
(`NetDesk.app`) and a distributable `.dmg`.
|
||||
|
||||
- One binary, two roles: the viewer window **and** the host panel, identical to
|
||||
the Windows build — only the packaging here is Mac-specific.
|
||||
- **Decode path:** on macOS the host H.264 (Annex-B) stream is decoded on the
|
||||
Apple video engine through **VideoToolbox** — `core/src/decode_vt.rs`
|
||||
(`VTDecompressionSession`, 32-bit BGRA output repacked to RGB8), gated
|
||||
`#[cfg(any(target_os = "macos", target_os = "ios"))]`. It falls back to the
|
||||
portable openh264 software decoder if the hardware path can't initialize. See
|
||||
[`cross-platform-decode.md`](./cross-platform-decode.md). No VideoToolbox-
|
||||
specific flags are needed at build time: the CoreMedia / CoreVideo /
|
||||
VideoToolbox framework `#[link]`s resolve against the active SDK.
|
||||
|
||||
> **Where this runs:** on the Mac (the M4), **not** the Windows dev box. Cross-
|
||||
> compiling a macOS `.app` from Windows is not supported — codesign, notarytool,
|
||||
> iconutil, lipo and hdiutil are all macOS-only tools.
|
||||
|
||||
The script is [`packaging/build-macos.sh`](../packaging/build-macos.sh). All
|
||||
outputs land in `packaging/dist/`.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — the Mac build command
|
||||
|
||||
Once the one-time Apple Developer setup below is done (Developer ID cert in the
|
||||
keychain + a stored `notarytool` profile named `netdesk`), the whole signed +
|
||||
notarized build is:
|
||||
|
||||
```bash
|
||||
# on the Mac, from the repo root:
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
bash packaging/build-macos.sh --notary-profile netdesk
|
||||
```
|
||||
|
||||
That produces, in `packaging/dist/`:
|
||||
|
||||
| Artifact | What it is |
|
||||
|----------------------------------------|---------------------------------------------|
|
||||
| `NetDesk.app` | signed + notarized + **stapled** universal app |
|
||||
| `NetDesk-macOS-universal.dmg` | signed + notarized + stapled disk image (ship this) |
|
||||
|
||||
Useful flags: `--skip-build` (reuse the per-target binaries), `--no-sign`
|
||||
(local unsigned test build), `--no-notarize` (sign but skip the Apple round-
|
||||
trip), `--identity "…"`, `--icon <png>`, `--version <x.y.z>`, `--entitlements
|
||||
<plist>`. Run `bash packaging/build-macos.sh --help` for the full list.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites (on the Mac)
|
||||
|
||||
1. **Xcode command-line tools** (gives `clang`, the SDKs, `lipo`, `iconutil`,
|
||||
`sips`, `hdiutil`, `codesign`, `xcrun`):
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
A full Xcode install from the App Store also works.
|
||||
|
||||
2. **Rust** via rustup. The script adds the two Apple-silicon/Intel std targets
|
||||
for you, but you can pre-add them:
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
```
|
||||
|
||||
3. An **Apple Developer Program** membership (needed only for signing +
|
||||
notarization — a `--no-sign` build needs none). Set that up once, below.
|
||||
|
||||
---
|
||||
|
||||
## One-time Apple Developer setup
|
||||
|
||||
You need a **Developer ID Application** certificate (this is the identity that
|
||||
lets an app run on *any* Mac after notarization — distinct from the "Apple
|
||||
Development" / "Apple Distribution" / Mac App Store certs) and a way for
|
||||
`notarytool` to authenticate.
|
||||
|
||||
### 1. Enrol in the Apple Developer Program
|
||||
|
||||
<https://developer.apple.com/programs/> — \$99/yr. You must be the **Account
|
||||
Holder** (or have the Admin role) to create a Developer ID certificate.
|
||||
|
||||
### 2. Create the "Developer ID Application" certificate
|
||||
|
||||
Easiest, via Xcode:
|
||||
|
||||
1. Xcode → **Settings… → Accounts**, sign in with the Apple ID on the team.
|
||||
2. Select the team → **Manage Certificates…**
|
||||
3. Click **+** → **Developer ID Application**.
|
||||
4. Xcode creates the private key + cert and installs both into your **login
|
||||
keychain**. (Back up the private key — it cannot be re-downloaded.)
|
||||
|
||||
Or manually at <https://developer.apple.com/account/resources/certificates> →
|
||||
**+** → *Developer ID → Developer ID Application*, upload a CSR made with
|
||||
**Keychain Access → Certificate Assistant → Request a Certificate from a
|
||||
Certificate Authority**, then double-click the downloaded `.cer` to install it.
|
||||
|
||||
Verify it is present and usable for signing:
|
||||
|
||||
```bash
|
||||
security find-identity -v -p codesigning
|
||||
# look for a line like:
|
||||
# 1) ABCD… "Developer ID Application: Akronlogic LLC (TEAMID1234)"
|
||||
```
|
||||
|
||||
The script auto-detects this identity; override with `--identity "Developer ID
|
||||
Application: Akronlogic LLC (TEAMID1234)"` if you have more than one.
|
||||
|
||||
### 3. Find your Team ID
|
||||
|
||||
<https://developer.apple.com/account> → **Membership details** → *Team ID* (a
|
||||
10-character string, also shown in the parentheses of the identity name above).
|
||||
|
||||
### 4. Create credentials for notarytool
|
||||
|
||||
Two supported options — pick one:
|
||||
|
||||
**A. App-specific password (simplest).** At <https://appleid.apple.com> →
|
||||
*Sign-In and Security → App-Specific Passwords* → **+**, label it e.g.
|
||||
`netdesk-notarytool`. Then store it once in the keychain as a named profile:
|
||||
|
||||
```bash
|
||||
xcrun notarytool store-credentials netdesk \
|
||||
--apple-id "you@akronlogic.com" \
|
||||
--team-id "TEAMID1234" \
|
||||
--password "abcd-efgh-ijkl-mnop" # the app-specific password
|
||||
```
|
||||
|
||||
`netdesk` is the profile name you pass to `--notary-profile netdesk`.
|
||||
|
||||
**B. App Store Connect API key.** Create a key at *App Store Connect → Users and
|
||||
Access → Integrations → App Store Connect API* (role: *Developer*), download the
|
||||
`AuthKey_XXXX.p8`, and store it:
|
||||
|
||||
```bash
|
||||
xcrun notarytool store-credentials netdesk \
|
||||
--key "/path/AuthKey_XXXXXXXX.p8" \
|
||||
--key-id "XXXXXXXXXX" \
|
||||
--issuer "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
```
|
||||
|
||||
> Instead of a stored profile you can pass credentials inline each run:
|
||||
> `--apple-id you@akronlogic.com --team-id TEAMID1234` **plus** the app-specific
|
||||
> password in the environment: `export NETDESK_NOTARY_PASSWORD='abcd-efgh-…'`.
|
||||
|
||||
---
|
||||
|
||||
## What the script does (step by step)
|
||||
|
||||
`packaging/build-macos.sh`:
|
||||
|
||||
1. **rustup target add** `aarch64-apple-darwin` `x86_64-apple-darwin`.
|
||||
2. **cargo build --release -p netdesk-viewer** once per target — producing
|
||||
`target/aarch64-apple-darwin/release/netdesk-viewer` and the `x86_64` one.
|
||||
(Cross-building the Intel slice on an Apple-silicon Mac uses the same SDK and
|
||||
needs no extra tooling.)
|
||||
3. **lipo -create** the two into one universal Mach-O at
|
||||
`NetDesk.app/Contents/MacOS/NetDesk` (`lipo -info` prints `arm64 x86_64`).
|
||||
4. **iconutil** builds `NetDesk.icns` from the largest PNG master it finds
|
||||
(`assets/icon-1024.png` → `icon-512.png` → `icon.png`, or `--icon <png>`):
|
||||
`sips` resizes the master into a complete `.iconset` (16→1024 px, @1x/@2x),
|
||||
then `iconutil -c icns` packs it into `Contents/Resources/NetDesk.icns`.
|
||||
5. **Assembles `NetDesk.app`** — writes `Contents/Info.plist` with bundle id
|
||||
**`com.akronlogic.netdesk`**, `CFBundleVersion` / `CFBundleShortVersionString`
|
||||
= the workspace version (read from `Cargo.toml`, currently **0.1.0**),
|
||||
`CFBundleExecutable`/`CFBundleIconFile` = `NetDesk`, `LSMinimumSystemVersion`
|
||||
11.0, `NSHighResolutionCapable`, plus a `PkgInfo` (`APPL????`).
|
||||
6. **codesign** the executable and the bundle with the Developer ID Application
|
||||
identity, **`--options runtime`** (hardened runtime, required to notarize) and
|
||||
**`--timestamp`** (secure timestamp), then `codesign --verify --deep --strict`.
|
||||
7. **Notarize + staple the `.app`**: `ditto -c -k --keepParent` it into a zip,
|
||||
`xcrun notarytool submit … --wait`, then `xcrun stapler staple NetDesk.app`
|
||||
so the ticket travels with the app offline. (Skip with `--no-staple-app`.)
|
||||
8. **Build the `.dmg`** (`hdiutil create`, UDZO, with an `/Applications`
|
||||
symlink for drag-install), **sign** it, **notarize** it
|
||||
(`notarytool submit … --wait`), and **staple** it.
|
||||
|
||||
If no Developer ID identity is found (or `--no-sign`), it still produces an
|
||||
**unsigned** `.app` + `.dmg` and tells you so — handy for a local smoke test.
|
||||
If signing works but no notarization credentials are present, it signs and
|
||||
prints exactly how to add them, like the Windows installer script does when
|
||||
NSIS is missing.
|
||||
|
||||
---
|
||||
|
||||
## Exact commands (equivalent to what the script runs)
|
||||
|
||||
If you want to drive it by hand, this is the full sequence (`VERSION=0.1.0`,
|
||||
`ID="Developer ID Application: Akronlogic LLC (TEAMID1234)"`):
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# 1-2. targets + release builds
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
cargo build --release -p netdesk-viewer --target aarch64-apple-darwin
|
||||
cargo build --release -p netdesk-viewer --target x86_64-apple-darwin
|
||||
|
||||
# 3. universal binary
|
||||
mkdir -p packaging/dist/NetDesk.app/Contents/MacOS packaging/dist/NetDesk.app/Contents/Resources
|
||||
lipo -create -output packaging/dist/NetDesk.app/Contents/MacOS/NetDesk \
|
||||
target/aarch64-apple-darwin/release/netdesk-viewer \
|
||||
target/x86_64-apple-darwin/release/netdesk-viewer
|
||||
|
||||
# 4. icon (ICONSET fully populated 16..1024 as in the script)
|
||||
iconutil -c icns NetDesk.iconset -o packaging/dist/NetDesk.app/Contents/Resources/NetDesk.icns
|
||||
|
||||
# 5. Info.plist (the script writes com.akronlogic.netdesk + version 0.1.0)
|
||||
|
||||
# 6. sign (hardened runtime + timestamp)
|
||||
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app/Contents/MacOS/NetDesk
|
||||
codesign --force --timestamp --options runtime --sign "$ID" packaging/dist/NetDesk.app
|
||||
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app
|
||||
|
||||
# 7. notarize + staple the app
|
||||
ditto -c -k --keepParent packaging/dist/NetDesk.app packaging/dist/NetDesk.zip
|
||||
xcrun notarytool submit packaging/dist/NetDesk.zip --keychain-profile netdesk --wait
|
||||
xcrun stapler staple packaging/dist/NetDesk.app
|
||||
|
||||
# 8. dmg -> sign -> notarize -> staple
|
||||
hdiutil create -volname NetDesk -srcfolder <stage-with-app+Applications-symlink> \
|
||||
-fs HFS+ -format UDZO -ov packaging/dist/NetDesk-macOS-universal.dmg
|
||||
codesign --force --timestamp --sign "$ID" packaging/dist/NetDesk-macOS-universal.dmg
|
||||
xcrun notarytool submit packaging/dist/NetDesk-macOS-universal.dmg --keychain-profile netdesk --wait
|
||||
xcrun stapler staple packaging/dist/NetDesk-macOS-universal.dmg
|
||||
```
|
||||
|
||||
The script wraps all of this (with the full `.iconset` generation, the plist,
|
||||
auto-detection, and graceful fallbacks) — prefer it over the manual steps.
|
||||
|
||||
---
|
||||
|
||||
## Verify a finished build
|
||||
|
||||
```bash
|
||||
# signature valid + hardened runtime + Developer-ID authority
|
||||
codesign --verify --deep --strict --verbose=2 packaging/dist/NetDesk.app
|
||||
codesign -dv --verbose=4 packaging/dist/NetDesk.app 2>&1 | grep -E 'Authority|Timestamp|flags|Identifier'
|
||||
|
||||
# Gatekeeper will accept it (Developer ID + notarized)
|
||||
spctl -a -vvv --type exec packaging/dist/NetDesk.app
|
||||
|
||||
# notarization ticket is stapled (works offline)
|
||||
xcrun stapler validate packaging/dist/NetDesk.app
|
||||
xcrun stapler validate packaging/dist/NetDesk-macOS-universal.dmg
|
||||
```
|
||||
|
||||
`spctl` should print `source=Notarized Developer ID` and `accepted`.
|
||||
|
||||
---
|
||||
|
||||
## Runtime permissions (host mode)
|
||||
|
||||
Signing/notarization are **not** the macOS capture permissions. When the binary
|
||||
runs as the **host** and first tries to capture the screen or inject input,
|
||||
macOS prompts the user (TCC) to grant, under **System Settings → Privacy &
|
||||
Security**:
|
||||
|
||||
- **Screen Recording** — to capture the remote screen.
|
||||
- **Accessibility** — to inject mouse/keyboard input (CGEvent).
|
||||
|
||||
These are user-granted at runtime and are **not** code-signing entitlements, so
|
||||
the default build needs no entitlements file. (If a future host feature drives
|
||||
*other* apps via Apple Events, add an entitlements plist with the matching keys
|
||||
and pass it via `--entitlements`.)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`no identity found` / unsigned output** — the Developer ID Application cert
|
||||
(and its private key) must be in the login keychain of the user running the
|
||||
build; confirm with `security find-identity -v -p codesigning`.
|
||||
- **Notarization `Invalid` / rejected** — fetch the log:
|
||||
`xcrun notarytool log <submission-id> --keychain-profile netdesk`. Almost
|
||||
always a missing `--options runtime` (hardened runtime) or a missing secure
|
||||
`--timestamp`; the script sets both.
|
||||
- **`The binary is not signed with a valid Developer ID certificate`** — an
|
||||
*Apple Development* cert was used instead of *Developer ID Application*; only
|
||||
the latter notarizes for distribution outside the App Store.
|
||||
- **openh264 fails to cross-compile the x86_64 slice** — it builds C++ via the
|
||||
`cc` crate; the Xcode CLT clang handles `-arch x86_64` on an arm Mac, so a
|
||||
full `xcode-select --install` (or Xcode) is enough. If it still fails, build
|
||||
only the native slice with `--target aarch64-apple-darwin` and drop the lipo
|
||||
step (the app is then arm64-only).
|
||||
- **Icon looks soft** — add a 1024×1024 `assets/icon-1024.png` master (the repo
|
||||
currently ships 256 and 512); the script prefers the largest it finds.
|
||||
@@ -0,0 +1,223 @@
|
||||
# Cross-platform hardware H.264 decode
|
||||
|
||||
NetDesk's core decodes the host's H.264 (Annex-B) stream into packed-RGB8
|
||||
[`Frame`]s for the UI to paint. Every platform shares one trait and one output
|
||||
type, so the viewer never knows which backend is running:
|
||||
|
||||
```rust
|
||||
pub struct Frame { pub w: usize, pub h: usize, pub rgb: Vec<u8> } // w*h*3 bytes
|
||||
pub trait VideoDecoder { fn decode(&mut self, au: &[u8]) -> Result<Option<Frame>>; }
|
||||
pub fn best_decoder() -> Box<dyn VideoDecoder + Send>;
|
||||
```
|
||||
|
||||
`best_decoder()` selects the fastest decoder for the current OS at runtime and
|
||||
**always** falls back to the portable openh264 software decoder if the native
|
||||
hardware path cannot initialize (no GPU, missing runtime, headless box, etc.).
|
||||
|
||||
| Target | Backend | Module | API | Crate(s) |
|
||||
|---------------|----------------|---------------------------|-----------------------------|---------------------------------|
|
||||
| Windows | Media Foundation (DXVA2) | `core/src/decode_mf.rs` | `IMFTransform` (MSH264 MFT) | `windows` |
|
||||
| macOS / iOS | VideoToolbox | `core/src/decode_vt.rs` | `VTDecompressionSession` | `core-foundation(-sys)` + frameworks |
|
||||
| Android | MediaCodec | `core/src/decode_mediacodec.rs` | `AMediaCodec` (NDK) | `ndk-sys` / `libmediandk` |
|
||||
| Linux | VAAPI | `core/src/decode_vaapi.rs` | FFmpeg/libavcodec + VAAPI | `ffmpeg-next` |
|
||||
| All (fallback)| openh264 | `core/src/decode.rs` | software | `openh264` |
|
||||
|
||||
Each native module is `#[cfg]`-gated to its own OS, so building for one target
|
||||
never compiles (or even links) another target's module or its dependencies. The
|
||||
per-platform crates are declared under `[target.'cfg(...)'.dependencies]` in
|
||||
`core/Cargo.toml`; on a non-matching host they are only added to `Cargo.lock`
|
||||
(resolved), never built, and their build scripts never run.
|
||||
|
||||
All backends emit the **true display dimensions** (e.g. 1920×1080), cropping out
|
||||
any codec padding / stride / alignment, so frames are never stretched.
|
||||
|
||||
---
|
||||
|
||||
## Windows — Media Foundation (reference, builds here)
|
||||
|
||||
Already shipping. Build with the portable MinGW toolchain:
|
||||
|
||||
```bash
|
||||
export PATH="/c/mingw-netdesk/mingw64/bin:$HOME/.cargo/bin:$PATH"
|
||||
cargo build -p netdesk-core # or: --target x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
### Dimension fix (2026-10)
|
||||
|
||||
The MS H.264 decoder MFT reports the **coded** size in `MF_MT_FRAME_SIZE`, which
|
||||
is padded up to the codec's macroblock alignment (1080p → a 1088+ coded height;
|
||||
other resolutions pad larger — this is what produced the wrong `1920x18xx` in the
|
||||
stats). The real picture rectangle is carried separately in a video-aperture
|
||||
blob. `decode_mf.rs` now:
|
||||
|
||||
1. Reads `MF_MT_FRAME_SIZE` into `coded_w`/`coded_h` — used **only** for the NV12
|
||||
plane layout (stride × plane height, and the UV plane offset).
|
||||
2. Reads the display rectangle from, in priority order,
|
||||
`MF_MT_MINIMUM_DISPLAY_APERTURE` → `MF_MT_GEOMETRIC_APERTURE` →
|
||||
`MF_MT_PAN_SCAN_APERTURE` (the last only when `MF_MT_PAN_SCAN_ENABLED`), as an
|
||||
`MFVideoArea` blob giving offset `(x, y)` and visible `width`×`height`. The
|
||||
aperture is clamped inside the coded frame so a malformed blob can't over-read.
|
||||
3. `nv12_to_rgb(..)` copies only the visible `width`×`height` region at
|
||||
`(crop_x, crop_y)` out of the padded NV12 (system-memory and DXGI/D3D11
|
||||
staging paths both), so `Frame.w`/`Frame.h` are the true `1920×1080`.
|
||||
|
||||
When no aperture is present the whole coded frame is treated as the picture
|
||||
(unchanged behaviour).
|
||||
|
||||
---
|
||||
|
||||
## macOS / iOS — VideoToolbox
|
||||
|
||||
`decode_vt.rs`, gated `#[cfg(any(target_os = "macos", target_os = "ios"))]`.
|
||||
|
||||
Feeds AUs into a `VTDecompressionSession` on the platform video engine and
|
||||
requests 32-bit BGRA output (VideoToolbox does YUV→RGB on the GPU), which is
|
||||
repacked to RGB8. The session is built lazily from the first keyframe's SPS/PPS
|
||||
(`CMVideoFormatDescriptionCreateFromH264ParameterSets`); Annex-B is repacked to
|
||||
AVCC for the `CMSampleBuffer`. `CVPixelBufferGetWidth/Height` already return the
|
||||
cropped display size, so no manual crop is needed.
|
||||
|
||||
Links the system **CoreMedia**, **CoreVideo** and **VideoToolbox** frameworks
|
||||
directly; `core-foundation` is used for the pixel-buffer attribute dictionary.
|
||||
|
||||
### Build (requires macOS + Xcode)
|
||||
|
||||
```bash
|
||||
xcode-select --install # or a full Xcode install
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin # macOS
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim # iOS
|
||||
|
||||
cargo build -p netdesk-core --target aarch64-apple-darwin # Apple-silicon mac
|
||||
cargo build -p netdesk-core --target aarch64-apple-ios # device
|
||||
cargo build -p netdesk-core --target aarch64-apple-ios-sim # simulator
|
||||
```
|
||||
|
||||
The framework `#[link]`s resolve against the SDK the active toolchain selects
|
||||
(`xcrun --sdk macosx|iphoneos|iphonesimulator --show-sdk-path`). No extra flags.
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cargo test -p netdesk-core --target aarch64-apple-darwin
|
||||
```
|
||||
|
||||
Smoke-test end to end by running the viewer against a live host; the log line
|
||||
`decode: VideoToolbox H.264 (hardware)` confirms the path was selected.
|
||||
|
||||
---
|
||||
|
||||
## Android — MediaCodec
|
||||
|
||||
`decode_mediacodec.rs`, gated `#[cfg(target_os = "android")]`.
|
||||
|
||||
Drives a byte-buffer-mode `AMediaCodec` AVC decoder (the SoC hardware decoder via
|
||||
OMX/Codec2). The codec is configured lazily from the first keyframe's SPS/PPS as
|
||||
`csd-0`/`csd-1`; each `decode` queues the Annex-B AU as one input buffer and
|
||||
dequeues one output buffer. Output is YUV 4:2:0 — NV12 semi-planar or I420
|
||||
planar depending on the device — converted to RGB8 honouring the format's
|
||||
`stride`, `slice-height`, `color-format`, and `crop-left/top/right/bottom` keys,
|
||||
so the emitted frame is the true display size.
|
||||
|
||||
Talks to the stable NDK Media C ABI in **`libmediandk`** directly (the same ABI
|
||||
the `ndk-sys` crate binds), so it is version-independent.
|
||||
|
||||
### Build (requires the Android NDK)
|
||||
|
||||
Install the NDK (via Android Studio's SDK Manager or `sdkmanager "ndk;27.0.x"`),
|
||||
then use [`cargo-ndk`]:
|
||||
|
||||
```bash
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi \
|
||||
x86_64-linux-android i686-linux-android
|
||||
cargo install cargo-ndk
|
||||
export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/27.0.12077973
|
||||
|
||||
# API 24+; links libmediandk / liblog automatically via the NDK sysroot.
|
||||
cargo ndk -t arm64-v8a -t armeabi-v7a -p 24 build -p netdesk-core --release
|
||||
```
|
||||
|
||||
Or without cargo-ndk, point Cargo at the NDK clang linker for each target, e.g.:
|
||||
|
||||
```bash
|
||||
export CC_aarch64_linux_android=$ANDROID_NDK_HOME/.../aarch64-linux-android24-clang
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$CC_aarch64_linux_android
|
||||
cargo build -p netdesk-core --target aarch64-linux-android --release
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
Run on a device/emulator (the `mediandk` APIs are not present on the host):
|
||||
|
||||
```bash
|
||||
cargo ndk -t arm64-v8a build -p netdesk-core --tests
|
||||
# push the test binary with `adb push` and run it on-device, or exercise it
|
||||
# through the app's instrumented tests.
|
||||
```
|
||||
|
||||
The log line `decode: MediaCodec H.264 (hardware)` confirms selection.
|
||||
|
||||
---
|
||||
|
||||
## Linux — VAAPI (via FFmpeg)
|
||||
|
||||
`decode_vaapi.rs`, gated `#[cfg(target_os = "linux")]`.
|
||||
|
||||
Opens the libavcodec H.264 decoder with a VAAPI `hw_device_ctx` (default DRI
|
||||
render node). libavcodec does all bitstream parsing; decode runs on the GPU, the
|
||||
surface is pulled to system memory with `av_hwframe_transfer_data` (NV12),
|
||||
cropped to the display rectangle with `av_frame_apply_cropping`, and converted to
|
||||
RGB8 with swscale. If no VAAPI device is usable, `new()` errors and the core
|
||||
falls back to openh264, so GPU-less / headless boxes still work.
|
||||
|
||||
Pulls libav* in through the **`ffmpeg-next`** crate and needs the system FFmpeg
|
||||
(≥ 4) development libraries and a VAAPI driver at build/run time.
|
||||
|
||||
### Build (requires system FFmpeg + VAAPI dev packages)
|
||||
|
||||
Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt install -y clang pkg-config \
|
||||
libavcodec-dev libavutil-dev libswscale-dev libavformat-dev \
|
||||
libva-dev vainfo intel-media-va-driver # or mesa-va-drivers for AMD
|
||||
|
||||
cargo build -p netdesk-core # host target
|
||||
```
|
||||
|
||||
Fedora:
|
||||
|
||||
```bash
|
||||
sudo dnf install clang pkgconf-pkg-config \
|
||||
ffmpeg-devel libva-devel libva-utils mesa-va-drivers
|
||||
cargo build -p netdesk-core
|
||||
```
|
||||
|
||||
`ffmpeg-sys-next` locates the libraries with `pkg-config`; set
|
||||
`PKG_CONFIG_PATH` if FFmpeg is in a non-standard prefix. `bindgen` needs
|
||||
`clang`/`libclang` (the `clang` package above).
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
vainfo # confirm a VAAPI driver + H264 profile
|
||||
cargo test -p netdesk-core
|
||||
```
|
||||
|
||||
At runtime the log line `decode: VAAPI H.264 (FFmpeg hardware)` confirms the
|
||||
hardware path; `VAAPI init failed (...); using openh264 software decode` means it
|
||||
fell back (check `vainfo`, `/dev/dri/renderD128` permissions, and the
|
||||
`LIBVA_DRIVER_NAME` driver).
|
||||
|
||||
---
|
||||
|
||||
## Adding another backend
|
||||
|
||||
1. Add `core/src/decode_<name>.rs` implementing `VideoDecoder` + `unsafe impl
|
||||
Send`, with a `new() -> Result<Self>` that errors when the platform can't
|
||||
support it.
|
||||
2. Declare the module in `decode.rs` behind the right `#[cfg(...)]`.
|
||||
3. Add a `#[cfg(...)]` branch in `best_decoder()` before the openh264 fallback.
|
||||
4. Add the deps under a matching `[target.'cfg(...)'.dependencies]` in
|
||||
`core/Cargo.toml`.
|
||||
5. Emit the **true display size** (crop any codec padding) and BT.601 limited-
|
||||
range RGB to match the other backends.
|
||||
Reference in new issue
Block a user