#!/usr/bin/env bash # Build DarkRoom into an installable APK, without Gradle. # # ./docker/android/package.sh # build + package, debug-signed # ./docker/android/package.sh --install # ...and adb install to a device # # Gradle would add a second build system, a second dependency tree, and a # second place for the toolchain versions to drift out of step with the # Dockerfile. The four tools it would have driven — aapt2, d8, zipalign, # apksigner — are in build-tools already and are enough on their own, because # the app has no Java of its own: android-activity's glue calls android_main # directly, and the only classes in the APK are the ones Slint's build script # compiles for its own helper. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(cd "${HERE}/../.." && pwd)" ABI="arm64-v8a" RUST_TARGET="aarch64-linux-android" PKG_DIR="${REPO}/apps/darkroom-android/android" # The container writes here (see build.sh); the APK is assembled in the same # place so both halves of the build agree on one output directory. CACHE="${XDG_CACHE_HOME:-${HOME}/.cache}/darkroom-android" OUT="${CACHE}/apk" APK="${OUT}/darkroom.apk" INSTALL=0 [[ "${1:-}" == "--install" ]] && INSTALL=1 # --------------------------------------------------------------------------- # 1. Cross-compile the shared library. # --------------------------------------------------------------------------- echo "==> building libdarkroom.so (${ABI})" "${HERE}/build.sh" cargo ndk -t "${ABI}" -o /work/target-android/jniLibs \ build --release -p darkroom-android SO="${CACHE}/target/jniLibs/${ABI}/libdarkroom.so" [[ -f "${SO}" ]] || { echo "error: ${SO} not built" >&2; exit 1; } # --------------------------------------------------------------------------- # 2. Assemble the APK inside the container, where the SDK lives. # # Everything below runs in one container invocation: aapt2 link produces a base # APK with the manifest, then the .so and Slint's dex are added as stored # entries, then zipalign and apksigner finish it. The .so is stored rather than # deflated so Android can mmap it directly (extractNativeLibs=false territory); # for a 37 MB library that also keeps install times sane. # --------------------------------------------------------------------------- echo "==> packaging APK" "${HERE}/build.sh" bash -euo pipefail -c ' SDK=/opt/android-sdk BT="${SDK}/build-tools/36.0.0" ABI="'"${ABI}"'" RT="'"${RUST_TARGET}"'" OUT=/work/target-android/apk rm -rf "${OUT}" && mkdir -p "${OUT}/staging/lib/${ABI}" # Slint compiles a Java helper (SlintAndroidJavaHelper) in its build script # and dexes it. The build-dir hash changes whenever its inputs change, so # find it rather than hard-coding a path; the newest wins if stale # directories from earlier builds are still around. DEX="$(find "/work/target-android/${RT}/release/build" \ -path "*i-slint-backend-android-activity*/out/classes.dex" \ -printf "%T@ %p\n" 2>/dev/null | sort -rn | head -1 | cut -d" " -f2-)" if [[ -z "${DEX}" ]]; then echo "error: Slint classes.dex not found — did the backend build?" >&2 exit 1 fi echo " dex: ${DEX}" # A debug keystore, created once and kept in the cache. Debug-signed only: # this exists to get the app onto a test device, not to release it. KS=/work/target-android/debug.keystore if [[ ! -f "${KS}" ]]; then echo " generating debug keystore" keytool -genkeypair -keystore "${KS}" -alias androiddebugkey \ -storepass android -keypass android \ -keyalg RSA -keysize 2048 -validity 10950 \ -dname "CN=Android Debug,O=Android,C=US" >/dev/null 2>&1 fi # aapt2 link needs the compile SDK to resolve android: attributes, and # --min-sdk-version is what ends up in the manifest the device reads. "${BT}/aapt2" link \ -I "${SDK}/platforms/android-36/android.jar" \ --manifest /work/apps/darkroom-android/android/AndroidManifest.xml \ --min-sdk-version 28 \ --target-sdk-version 36 \ -o "${OUT}/base.apk" \ --auto-add-overlay cp "/work/target-android/jniLibs/${ABI}/libdarkroom.so" \ "${OUT}/staging/lib/${ABI}/libdarkroom.so" cp "${DEX}" "${OUT}/staging/classes.dex" # -0 "" stores without compression; see the note above about mmap. cd "${OUT}/staging" cp "${OUT}/base.apk" "${OUT}/unaligned.apk" zip -q -0 -X "${OUT}/unaligned.apk" "lib/${ABI}/libdarkroom.so" zip -q -X "${OUT}/unaligned.apk" classes.dex # zipalign before signing: apksigner preserves alignment, the reverse order # invalidates the signature. "${BT}/zipalign" -p -f 4 "${OUT}/unaligned.apk" "${OUT}/darkroom.apk" "${BT}/apksigner" sign \ --ks "${KS}" --ks-pass pass:android --key-pass pass:android \ --min-sdk-version 28 \ "${OUT}/darkroom.apk" "${BT}/apksigner" verify --print-certs "${OUT}/darkroom.apk" | head -2 ' echo "==> ${APK}" ls -la "${APK}" # --------------------------------------------------------------------------- # 3. Install from the host. # # The container has adb but no device: build.sh mounts no USB and shares no # network, so the host's already-authorised adb server is the shorter path. # --------------------------------------------------------------------------- if [[ "${INSTALL}" == "1" ]]; then command -v adb >/dev/null || { echo "error: adb not on PATH" >&2; exit 1; } echo "==> installing" adb install -r "${APK}" echo "==> launching" adb shell am start -n paris.tourolle.darkroom/android.app.NativeActivity fi