diff --git a/Android/ship-companion.sh b/Android/ship-companion.sh index 3c7f62d7..cd32646c 100755 --- a/Android/ship-companion.sh +++ b/Android/ship-companion.sh @@ -9,6 +9,10 @@ # # ./Android/ship-companion.sh # +# The actual build/sign/verify/stage is done by scripts/publish-companion-apk.sh +# (single source of truth, shared with the pre-push hook). It does a CLEAN build, +# forces v1+v2+v3 signing, and ABORTS if any signature scheme is missing — so a +# broken or v2-only APK can never be shipped. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -17,25 +21,15 @@ cd "$ROOT" export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17}" export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" -APK="Android/app/build/outputs/apk/debug/app-debug.apk" DEST="neode-ui/public/packages/archipelago-companion.apk" -OLD_ZIP="neode-ui/public/packages/archipelago-companion.apk.zip" -echo "==> Building debug APK" -( cd Android && ./gradlew :app:assembleDebug --console=plain -q ) -[ -f "$APK" ] || { echo "ERROR: APK not found at $APK" >&2; exit 1; } +echo "==> Building + signing + verifying companion APK" +bash scripts/publish-companion-apk.sh -echo "==> Publishing -> $DEST" -mkdir -p "$(dirname "$DEST")" -cp "$APK" "$DEST" -# Drop the legacy zipped artifact so the served download is the raw APK only. -if [ -f "$OLD_ZIP" ]; then - git rm -q --ignore-unmatch "$OLD_ZIP" 2>/dev/null || rm -f "$OLD_ZIP" -fi +[ -f "$DEST" ] || { echo "ERROR: served APK not found at $DEST" >&2; exit 1; } -git add "$DEST" -if git diff --cached --quiet; then - echo "==> Nothing to commit (working tree + APK unchanged)" +if git diff --cached --quiet -- "$DEST"; then + echo "==> Nothing to commit (APK unchanged)" else git commit -q -m "chore(android): update companion apk download" echo "==> Committed" diff --git a/neode-ui/public/packages/archipelago-companion.apk b/neode-ui/public/packages/archipelago-companion.apk index c364b143..eee04860 100644 Binary files a/neode-ui/public/packages/archipelago-companion.apk and b/neode-ui/public/packages/archipelago-companion.apk differ diff --git a/scripts/publish-companion-apk.sh b/scripts/publish-companion-apk.sh index 85c4dcab..11e504e9 100755 --- a/scripts/publish-companion-apk.sh +++ b/scripts/publish-companion-apk.sh @@ -4,6 +4,16 @@ # can install it straight from the link — no unzip step). # # Run manually, or automatically via the pre-push hook (.githooks/pre-push). +# +# Hardened (2026-06-26) so a broken APK can never ship again: +# 1. Aborts on stray resource dirs whose names contain spaces (these break a +# clean build with "Invalid resource directory name"). Empty ones — junk +# left by some icon-export tools — are auto-removed; non-empty ones error. +# 2. Always a CLEAN build (incremental builds masked the bad resource dirs). +# 3. Forces v1 + v2 + v3 signing with zipalign + apksigner. AGP's +# `enableV1Signing = true` flag is silently ignored for minSdk>=24, which +# shipped a v2-only APK that some OEM installers reject ("App not installed"). +# 4. VERIFIES all three schemes and ABORTS if any is missing — no silent ship. set -euo pipefail ROOT="$(git rev-parse --show-toplevel)" @@ -17,16 +27,63 @@ if [ ! -x "$JAVA/bin/java" ] || [ ! -d "$SDK" ]; then echo " (set JAVA_HOME and ANDROID_HOME to build the companion APK)" >&2 exit 0 fi +export JAVA_HOME="$JAVA" +export PATH="$JAVA/bin:$PATH" -echo "publish-companion-apk: building debug APK…" >&2 -( cd Android && JAVA_HOME="$JAVA" ANDROID_HOME="$SDK" ./gradlew -q :app:assembleDebug ) - +RES="Android/app/src/main/res" APK="Android/app/build/outputs/apk/debug/app-debug.apk" +SIGNED="Android/app/build/outputs/apk/debug/app-debug-signed.apk" DEST="neode-ui/public/packages/archipelago-companion.apk" OLD_ZIP="neode-ui/public/packages/archipelago-companion.apk.zip" -mkdir -p "$(dirname "$DEST")" +KS="Android/app/debug.keystore" -cp "$APK" "$DEST" +# 1. Guard against resource dirs with spaces (Android forbids them; a clean +# build aborts on them). Empty ones are removed; non-empty ones are fatal. +while IFS= read -r d; do + [ -n "$d" ] || continue + if [ -n "$(ls -A "$d" 2>/dev/null)" ]; then + echo "publish-companion-apk: ERROR — resource dir with a space is not empty:" >&2 + echo " $d" >&2 + echo " Rename it (Android resource dir names cannot contain spaces)." >&2 + exit 1 + fi + rmdir "$d" && echo "publish-companion-apk: removed stray empty resource dir: $d" >&2 +done < <(find "$RES" -type d -name '* *' 2>/dev/null) + +# 2. Clean build. +echo "publish-companion-apk: clean build of debug APK…" >&2 +( cd Android && ./gradlew -q --console=plain :app:clean :app:assembleDebug ) +[ -f "$APK" ] || { echo "publish-companion-apk: ERROR — APK not produced at $APK" >&2; exit 1; } + +# 3. Force v1 + v2 + v3 signing (AGP's enableV1Signing flag is ignored here). +BT="$(ls -d "$SDK"/build-tools/*/ | sort -V | tail -1)" +ZIPALIGN="${BT}zipalign"; APKSIGNER="${BT}apksigner" +[ -x "$ZIPALIGN" ] && [ -x "$APKSIGNER" ] || { + echo "publish-companion-apk: ERROR — zipalign/apksigner not found under $BT" >&2; exit 1; } +[ -f "$KS" ] || { echo "publish-companion-apk: ERROR — keystore missing at $KS" >&2; exit 1; } + +echo "publish-companion-apk: zipalign + sign (v1+v2+v3)…" >&2 +"$ZIPALIGN" -p -f 4 "$APK" "$SIGNED" +"$APKSIGNER" sign \ + --ks "$KS" --ks-pass pass:android \ + --ks-key-alias androiddebugkey --key-pass pass:android \ + --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \ + "$SIGNED" + +# 4. Verify all three schemes (min-sdk 21 forces the v1 path to be exercised). +VERIFY="$("$APKSIGNER" verify -v --min-sdk-version 21 "$SIGNED" 2>&1)" +for scheme in "v1 scheme" "v2 scheme" "v3 scheme"; do + if ! printf '%s\n' "$VERIFY" | grep -iq "$scheme.*: true"; then + echo "publish-companion-apk: ERROR — $scheme NOT present after signing. Aborting." >&2 + printf '%s\n' "$VERIFY" | grep -iE "scheme" >&2 + exit 1 + fi +done +echo "publish-companion-apk: verified v1 + v2 + v3 signatures." >&2 + +# 5. Publish. +mkdir -p "$(dirname "$DEST")" +cp "$SIGNED" "$DEST" # Drop the legacy zipped artifact so the served download is the raw APK only. if [ -f "$OLD_ZIP" ]; then