#!/usr/bin/env bash # Build the ThreadCount counter app bundle. # # bash scripts/build-counter-aab.sh → android/app/build/outputs/bundle/release/*.aab # bash scripts/build-counter-aab.sh debug → a debug APK, for putting on a phone quickly # # The sibling of scripts/build-staff-aab.sh, and deliberately not the same shape, because the two # apps are not the same shape. This one KEEPS the barcode scanner: native libraries, a CAMERA # permission and VIBRATE are expected here, and the staff script's refusal to ship any of them would # be wrong. What this one has instead is the alignment check below, which the staff app cannot fail # because it ships no native code at all. # # **The toolchain is user-space.** JDK 17 and the SDK live under ~/.cache/ca-android — no sudo, no # system Java, nothing installed outside the home directory. set -euo pipefail cd "$(dirname "$0")/.." MODE=${1:-release} export JAVA_HOME="${JAVA_HOME:-$HOME/.cache/ca-android/jdk}" export ANDROID_HOME="${ANDROID_HOME:-$HOME/.cache/ca-android/sdk}" export ANDROID_SDK_ROOT="$ANDROID_HOME" [ -x "$JAVA_HOME/bin/java" ] || { echo "FATAL: no JDK at $JAVA_HOME"; exit 1; } [ -d "$ANDROID_HOME/platforms" ] || { echo "FATAL: no Android SDK at $ANDROID_HOME"; exit 1; } echo "sdk.dir=$ANDROID_HOME" > android/local.properties echo "==> syncing the counter shell into android" # No TC_APP, so capacitor.config.ts resolves to the counter app and android/ is the target. npx cap sync android if [ "$MODE" = "debug" ]; then echo "==> assembling a debug APK" ( cd android && ./gradlew --no-daemon assembleDebug ) find android/app/build/outputs/apk -name "*.apk" -print exit 0 fi KEYS="$HOME/threadcount-keys/keystore.properties" [ -f "$KEYS" ] || echo "NOTE: $KEYS is missing — the bundle will be unsigned." echo "==> building the release bundle" ( cd android && ./gradlew --no-daemon bundleRelease ) AAB=$(find android/app/build/outputs/bundle/release -name "*.aab" | head -1) echo "==> $AAB" echo "==> checks" if unzip -l "$AAB" | grep -qiE "META-INF/.*\.(RSA|EC|DSA)"; then echo " ✓ signed" else echo " ! UNSIGNED — Play will refuse it" fi if unzip -p "$AAB" BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map >/dev/null 2>&1; then echo " ✓ mapping file bundled — Play can symbolicate a stack trace" else echo " ! no mapping file; Play will warn about deobfuscation" fi # 16 KB memory pages, the check that actually stops an upload. # # Devices from Android 15 can run 16 KB pages and Play rejects a bundle whose 64-bit native # libraries are only 4 KB-aligned. It is a 64-bit feature: armeabi-v7a and x86 cannot use 16 KB # pages, so a 32-bit library at 0x1000 is correct and must not be reported as a fault — a blunter # version of this check cried wolf over the 32-bit libbarhopper_v3.so and would have sent somebody # hunting a problem that was not there. # # The pins in android/variables.gradle are what make this pass; the check is here because a pin can # be lost in a merge and the failure is otherwise invisible until Play says no. echo " --- 16 KB alignment (64-bit only, which is what Play checks) ---" WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT BAD=0 for so in $(unzip -l "$AAB" | grep -oE "base/lib/[^ ]*\.so" | sort -u); do arch=$(basename "$(dirname "$so")") case "$arch" in arm64-v8a|x86_64) ;; *) continue ;; esac unzip -o -q -j "$AAB" "$so" -d "$WORK" al=$(readelf -lW "$WORK/$(basename "$so")" 2>/dev/null | awk '$1=="LOAD"{print $NF}' | sort -u | head -1) case "$al" in 0x4000|0x10000) printf " ok %-12s %-40s %s\n" "$arch" "$(basename "$so")" "$al" ;; *) printf " BAD %-12s %-40s %s\n" "$arch" "$(basename "$so")" "$al"; BAD=$((BAD + 1)) ;; esac done if [ "$BAD" -gt 0 ]; then echo " ! $BAD 64-bit library(ies) are not 16 KB aligned — Play would reject this upload." echo " Check the CameraX and MLKit pins in android/variables.gradle; the MLKit plugin reads" echo " all four androidxCamera* names and silently falls back to 1.1.0 if one is missing." exit 1 fi echo " ✓ every 64-bit native library is 16 KB aligned" PERMS=$("$JAVA_HOME/bin/java" -jar "$HOME/.cache/ca-android/bundletool.jar" dump manifest --bundle "$AAB" 2>/dev/null \ | grep -oE ']*android:name="[^"]+"' | grep -oE '"[^"]+"$' | tr -d '"' | sort -u) echo " permissions: $(echo "$PERMS" | tr '\n' ' ')" # CAMERA and VIBRATE belong to this app — it scans garments and buzzes on a good read. Anything # beyond this list is a plugin that got in without anyone deciding it should. if echo "$PERMS" | grep -qE 'RECORD_AUDIO|ACCESS_FINE_LOCATION|READ_CONTACTS|READ_EXTERNAL_STORAGE'; then echo " ! this app asks for more than it needs" exit 1 fi echo " ✓ nothing beyond what the counter actually uses"