Files
netdesk/native/docs/build-android.md
T

7.0 KiB

Building NetDesk for Android

NetDesk's Android app is a Flutter UI (ui/) bound, through 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 dlopens 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 -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):

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):

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):

$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 -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, 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.