#!/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" # Under the cache's target/ rather than beside it: the container writes to # /work/target-android/apk, and that is the directory bind-mounted here. OUT="${CACHE}/target/apk" APK="${OUT}/darkroom.apk" # The version, taken from the workspace rather than restated here. # # `AndroidManifest.xml` deliberately declares none: a manifest that states a # version is a second place for one to be wrong, and it was — the APK reported # `versionName=null` and `versionCode=0` on the device while the binary inside # it knew perfectly well what it was. # # `versionCode` must be a single increasing integer, which a semantic version # is not, so it is packed: MAJOR*10000 + MINOR*100 + PATCH. That keeps the # ordering Android needs (it refuses to install an APK whose code is lower than # the installed one) and stays readable — 0.4.0 is 400. It allows 99 minors and # 99 patches per major, which is a limit worth knowing about and a long way off. VERSION_NAME="$(sed -n 's/^version = "\(.*\)"$/\1/p' "${REPO}/Cargo.toml" | head -1)" if [[ -z "${VERSION_NAME}" ]]; then echo "error: no version in Cargo.toml" >&2 exit 1 fi VERSION_CODE="$(awk -F. '{ print $1 * 10000 + $2 * 100 + $3 }' <<< "${VERSION_NAME}")" echo "==> version ${VERSION_NAME} (code ${VERSION_CODE})" 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 # The launcher icon is the only resource the app has, but resources go # through aapt2 in two steps regardless: compile turns the source tree into # an intermediate archive of flat files, link folds that into the APK and # builds the resources.arsc table that @mipmap/ic_launcher in the manifest # resolves against. Skipping compile and handing link the directory does # not work — link only reads compiled input. "${BT}/aapt2" compile \ --dir /work/apps/darkroom-android/android/res \ -o "${OUT}/res.zip" # 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 \ -R "${OUT}/res.zip" \ --min-sdk-version 28 \ --target-sdk-version 36 \ --version-name "'"${VERSION_NAME}"'" \ --version-code "'"${VERSION_CODE}"'" \ -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