#!/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 tools it would have driven — javac, aapt2, d8, zipalign, # apksigner — are in the image already and are enough on their own, because # the app is Rust: android-activity's glue calls android_main directly, and # the only Java in the APK is Slint's helper plus the handful of classes # Android insists on constructing itself (see assemble-apk.sh's Java step). 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. # # The assembly itself is assemble-apk.sh, which runs in the image and is shared # with CI — see its header. Only the mount layout is decided here: the repo is # at /work and the cache's target directory at /work/target-android, so every # default in that script already points at the right place. # --------------------------------------------------------------------------- # # Release signing, when asked for. assemble-apk.sh selects it by the presence # of KEYSTORE_PASS (see its header), and the keystore has to be reachable from # inside the container, so a host path in KEYSTORE is copied under the mounted # target directory for the duration of the build and removed after. The # passwords travel as environment, never as arguments -- docs/android-signing.md # has the incantation. # --------------------------------------------------------------------------- echo "==> packaging APK" SIGNING_ENV=() CONTAINER_KEYSTORE="" if [[ -n "${KEYSTORE_PASS:-}" ]]; then [[ -f "${KEYSTORE:-}" ]] || { echo "error: KEYSTORE_PASS is set but KEYSTORE is not a file" >&2; exit 1; } install -m 600 "${KEYSTORE}" "${CACHE}/target/release.keystore" CONTAINER_KEYSTORE="${CACHE}/target/release.keystore" SIGNING_ENV=( KEYSTORE=/work/target-android/release.keystore KEYSTORE_PASS="${KEYSTORE_PASS}" KEY_PASS="${KEY_PASS:-${KEYSTORE_PASS}}" KEY_ALIAS="${KEY_ALIAS:?KEY_ALIAS is required when KEYSTORE_PASS is set}" ) fi "${HERE}/build.sh" env \ ABI="${ABI}" RUST_TARGET="${RUST_TARGET}" \ DARKROOM_DEBUGGABLE="${DARKROOM_DEBUGGABLE:-}" \ "${SIGNING_ENV[@]}" \ /work/docker/android/assemble-apk.sh if [[ -n "${CONTAINER_KEYSTORE}" ]]; then rm -f "${CONTAINER_KEYSTORE}" fi # --------------------------------------------------------------------------- # 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