#!/bin/sh # Builds one test app's APK. Run from the test project's own directory, # which is where Dev Updater runs a component's build from. # # ../lib/build-apk.sh --package com.example.dutest.hello --label "Hello" \ # [--variant debug] # # Deliberately not Gradle, which is the obvious way to build an Android app # and the wrong one here for two reasons. A Gradle daemon is around a # gigabyte resident, and several test projects each holding one is how this # machine ran itself out of memory on 2026-08-30 -- the emulators alone are # already close to the limit. And a Gradle build takes tens of seconds to do # what this does in about two, which matters because the thing under test is # Dev Updater, not the app: a fixture you wait for stops getting used. # # What it costs is that this is a small build system rather than a # declaration. It is kept to one file, and the whole of it is: link a # manifest, compile one class, dex it, add it, align, sign. Nothing here is # a general Android build and it should not grow into one -- a test project # needing more than a screen with its own name on it wants a real project. # # The signing key lives outside the repository # ($XDG_DATA_HOME/dev-updater/test-projects/debug.keystore) because the # repository is a mount shared with the host, and because a stable key is # what lets a rebuilt APK install *over* the copy already on the emulator # rather than being refused for a signature mismatch. Deleting it is how you # produce that refusal on purpose. set -eu PACKAGE= LABEL= VARIANT=debug while [ $# -gt 0 ]; do case "$1" in --package) PACKAGE=$2; shift 2 ;; --label) LABEL=$2; shift 2 ;; --variant) VARIANT=$2; shift 2 ;; *) echo "usage: $0 --package ID --label TEXT [--variant NAME]" >&2; exit 2 ;; esac done [ -n "$PACKAGE" ] || { echo "$0: --package is required" >&2; exit 2; } [ -n "$LABEL" ] || { echo "$0: --label is required" >&2; exit 2; } LIB=$(cd "$(dirname "$0")" && pwd) PROJECT=$(pwd) NAME=$(basename "$PROJECT") # Same cascade as app/build-apk.sh: the host and this VM do not keep the SDK # in the same place, and the ambient ANDROID_HOME points at one with no # build-tools under it. if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then SDK="$ANDROID_HOME" elif [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "${ANDROID_SDK_ROOT}/build-tools" ]; then SDK="$ANDROID_SDK_ROOT" elif [ -d "$HOME/Android/Sdk/build-tools" ]; then SDK="$HOME/Android/Sdk" else echo "No Android SDK with build-tools found. Set ANDROID_HOME to one." >&2 exit 1 fi # Newest of whatever is installed, rather than a pinned version this script # would have to be edited to follow. TOOLS="$SDK/build-tools/$(ls "$SDK/build-tools" | sort -V | tail -n1)" PLATFORM="$SDK/platforms/$(ls "$SDK/platforms" | sort -V | tail -n1)" JAR="$PLATFORM/android.jar" [ -f "$JAR" ] || { echo "No android.jar under $PLATFORM -- install a platform." >&2; exit 1; } MIN_SDK=24 TARGET_SDK=$(basename "$PLATFORM" | sed 's/^android-//; s/\..*//') KEYSTORE="${XDG_DATA_HOME:-$HOME/.local/share}/dev-updater/test-projects/debug.keystore" # Six steps, counted out for the progress bar. Emitted directly rather than # through $DEV_UPDATER_PROGRESS: that wrapper exists to count Gradle tasks # for a build that cannot report its own, and this one knows exactly what it # is doing. The format is the same either way -- see server/src/build_state.rs. STEPS=6 step() { echo "@@progress $1/$STEPS" echo "==> $2" } BUILD="$PROJECT/app/build" GEN="$BUILD/gen" OUT="$BUILD/outputs/apk/$VARIANT" # `app/` is not decoration. Dev Updater matches APKs at up to two directories # below a project root, so a test project building straight into # test-projects//build/ would also be found from the repository root -- # i.e. offered as a build of Dev Updater itself, and being newest, served as # the default. One more level puts it out of that reach while keeping it one # level below the test project, where its own patterns find it. rm -rf "$GEN" mkdir -p "$GEN/classes" "$OUT" step 1 "Writing the manifest for $PACKAGE" sed -e "s|__PACKAGE__|$PACKAGE|" -e "s|__LABEL__|$LABEL|" \ "$LIB/AndroidManifest.xml" >"$GEN/AndroidManifest.xml" step 2 "Linking resources (aapt2)" "$TOOLS/aapt2" link \ -I "$JAR" \ --manifest "$GEN/AndroidManifest.xml" \ --min-sdk-version "$MIN_SDK" \ --target-sdk-version "$TARGET_SDK" \ -o "$GEN/linked.apk" step 3 "Compiling StubActivity" javac -nowarn -Xlint:-options --release 17 \ -classpath "$JAR" -d "$GEN/classes" "$LIB/StubActivity.java" step 4 "Dexing" find "$GEN/classes" -name '*.class' -print0 | xargs -0 \ "$TOOLS/d8" --release --lib "$JAR" --min-api "$MIN_SDK" --output "$GEN" step 5 "Packaging and aligning" (cd "$GEN" && jar --update --file linked.apk classes.dex) "$TOOLS/zipalign" -p -f 4 "$GEN/linked.apk" "$GEN/aligned.apk" step 6 "Signing" if [ ! -f "$KEYSTORE" ]; then echo " (generating a signing key at $KEYSTORE)" mkdir -p "$(dirname "$KEYSTORE")" chmod 700 "$(dirname "$KEYSTORE")" keytool -genkeypair -keystore "$KEYSTORE" \ -storepass android -keypass android -alias test \ -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=dev-updater test projects" >/dev/null fi "$TOOLS/apksigner" sign \ --ks "$KEYSTORE" --ks-pass pass:android --key-pass pass:android \ --ks-key-alias test --min-sdk-version "$MIN_SDK" \ --out "$OUT/$NAME-$VARIANT.apk" "$GEN/aligned.apk" echo "@@progress $STEPS/$STEPS" echo "==> Built $OUT/$NAME-$VARIANT.apk"