Files
SoulStream/src-tauri/android/verify-icon.sh
T
KeelrLeek c36ec439ce android: the icon gate answered fail / pass / fail to the same APK
Round 44's verify-icon.sh had never run inside deploy.sh — only by hand — and
it does two things there that it cannot do standalone.

deploy.sh exports MSYS_NO_PATHCONV=1 for its adb calls, and that switch is
global, so the APK path reached aapt2 as /f/Coding/... Windows binaries cannot
open an MSYS path, and aapt2's answer is "failed opening zip: I/O error",
which reads exactly like a corrupt APK rather than like a path. The four
aapt2 calls now get cygpath -w explicitly; unzip and cmp keep the MSYS form,
because they want that one.

The second is worse, because it is intermittent. Every membership test was
`echo "$VAR" | grep -q PATTERN`, under the `set -o pipefail` at the top.
grep -q exits the moment it matches; echo is still pushing 70 KB of unzip
listing into a pipe that just closed, takes SIGPIPE, exits 141 — and pipefail
promotes the writer's status to the pipeline, so a check that FOUND what it
was looking for reports failure. Whether it fires depends on where the match
falls in the stream, so three runs against one unchanged APK gave fail, pass,
fail, each naming a different missing file. Six of those, now herestrings:
no writer process, nothing to kill. Eight runs green afterwards.

Worth saying plainly: the APK was correct the entire time. The gate was
wrong, and a gate that is green at random is worse than no gate, because the
failure it invents points at the build.
2026-09-15 07:43:54 -05:00

249 lines
15 KiB
Bash

#!/usr/bin/env bash
# Is the falcon actually in the APK? The way this loses is silent: gen/android
# is regenerated and gitignored, so a project rebuilt without the overlay copy
# still compiles, still installs, and wears tauri's logo again with nothing in
# the log. So every APK this repo makes is opened and read before it is
# installed. Called by build.sh and dev.sh; run it by hand on any APK.
#
# bash src-tauri/android/verify-icon.sh <apk> [--crunched]
#
# Since the background became a gradient there is a second way to lose, and it
# is quieter still: the adaptive icon's <background> is a reference, and a
# reference resolves to whatever the APK happens to hold. A stale APK, or an
# overlay copy that did not run, and the name points at something else while
# every PNG in the file is still the falcon's. So the check does not ask
# whether drawable/ic_launcher_ground.xml is in the APK — it asks what
# <background> RESOLVES to, through the resource table, in both build types,
# and then reads concept 04's six gradient stops back out of the thing it
# found. That half needs aapt2; without it the ground says NOT CHECKED and the
# rest of the script still runs.
#
# Debug and release are read DIFFERENTLY, because AGP treats their resources
# differently and both differences were measured on 2026-09-14:
#
# debug packaged with --no-crunch (AGP's default for the debuggable build
# type), so every entry is byte for byte the file on disk — checked
# against the real 107 MB APK. Names are names: res/mipmap-hdpi-v4/…
# release --crunched. Two things change. aapt2 re-encodes every PNG, so the
# bytes are no longer the file; and AGP's resource optimization
# (OptimizeResourcesTask → SHORTEN_RESOURCE_PATHS) renames the lot
# to res/Ag.png, so the names are gone as well — an APK optimized
# here had zero entries matching "res/mipmap". Nothing but the
# resource table can say what is what, so that is what is read:
# `aapt2 dump resources`, then the pixel dimensions of whatever it
# points at. 72x72 at hdpi still tells the kit's legacy icon from
# tauri's 49x49.
#
# If anyone ever sets isCrunchPngs = true on the debug build type, the debug
# path fails on byte-equality with the file named; --crunched is the answer.
set -uo pipefail
# Every membership test below reads its haystack from a HERESTRING, never from
# `echo "$VAR" | grep -q`. Measured 2026-09-15: that pipeline plus the
# `pipefail` above is a race. `grep -q` exits the instant it matches, `echo` is
# still pushing 70 KB of `unzip -l` down a closed pipe, takes SIGPIPE and exits
# 141 — and pipefail promotes the dead writer's status to the whole pipeline,
# so a check that FOUND what it wanted reports failure. Whether it fires
# depends on where the match falls in the stream, so three runs against one
# unchanged APK gave fail / pass / fail. A herestring has no writer process and
# nothing to kill.
APK="${1:-}"
test -n "$APK" || { echo "usage: verify-icon.sh <apk> [--crunched]"; exit 2; }
# resolved before the cd, so a relative path typed by hand still means what it
# said; the repo root is where $RES is
[ -f "$APK" ] && APK="$(cd "$(dirname "$APK")" && pwd)/$(basename "$APK")"
# the repo this script is IN, not a drive letter. build.sh and dev.sh have
# already cd'd to /f/Coding/SoulStream and call this as
# src-tauri/android/verify-icon.sh, so ../.. lands on the same place they are
# — but a checkout on another letter, or an APK checked by hand from
# somewhere else, then compares against ITS OWN overlay rather than F:'s.
cd "$(dirname "$0")/../.." || { echo "cannot find the repo above $0"; exit 2; }
CRUNCH=0; [ "${2:-}" = "--crunched" ] && CRUNCH=1
RES=src-tauri/android/res
DPI="mdpi hdpi xhdpi xxhdpi xxxhdpi"
NAMES="ic_launcher ic_launcher_round ic_launcher_foreground ic_launcher_monochrome"
bad() { echo "ICON CHECK FAILED: $*"; exit 1; }
test -f "$APK" || bad "no APK at $APK"
test -d "$RES" || bad "$RES is missing — there is nothing to check the APK against"
# 1. the overlay itself, before the APK is opened. The monochrome layer is the
# one Android 13 tints for themed icons and the one tauri's generator has
# never made, so a later edit that drops it should not pass quietly.
for x in ic_launcher ic_launcher_round; do
test -f "$RES/mipmap-anydpi-v26/$x.xml" || bad "$RES/mipmap-anydpi-v26/$x.xml is missing from the overlay"
grep -q "<monochrome" "$RES/mipmap-anydpi-v26/$x.xml" \
|| bad "$RES/mipmap-anydpi-v26/$x.xml declares no <monochrome> layer — themed icons need one"
done
# The ground is the other half of the icon and three quarters of what a
# masked tile shows, so a later edit that flattens it back to one colour
# should not pass quietly either.
GROUND="$RES/drawable/ic_launcher_ground.xml"
test -f "$GROUND" || bad "$GROUND is missing from the overlay — the adaptive background has nowhere to come from"
grep -q 'android:type="linear"' "$GROUND" || bad "$GROUND declares no linear gradient — concept 04's surface"
grep -q 'android:type="radial"' "$GROUND" || bad "$GROUND declares no radial gradient — concept 04's glow"
# quote-anchored, so the comment above them cannot satisfy the check
for c in FF0A2330 FF07141F FF040A10 2E31C0CB 0D1F8FC9 0007141F; do
grep -qi "\"#$c\"" "$GROUND" || bad "$GROUND has lost concept 04's stop #$c — it is no longer the desktop's ground"
done
for x in ic_launcher ic_launcher_round; do
grep -q '@drawable/ic_launcher_ground' "$RES/mipmap-anydpi-v26/$x.xml" \
|| bad "$RES/mipmap-anydpi-v26/$x.xml does not point its background at @drawable/ic_launcher_ground"
done
for d in $DPI; do for n in $NAMES; do
test -f "$RES/mipmap-$d/$n.png" || bad "$RES/mipmap-$d/$n.png is missing from the overlay"
done; done
# the PNG header's width and height, eight bytes at offset 16 — the one thing
# about an image that survives being re-encoded. Printed as 72x72, so a failure
# reads as a size rather than as hex.
dims() {
local h
h="$(xxd -s 16 -l 8 -p)"
[ "${#h}" = 16 ] || { echo "unreadable"; return; }
printf '%dx%d\n' "$((16#${h:0:8}))" "$((16#${h:8:8}))"
}
LIST="$(unzip -l "$APK")" || bad "cannot read $APK"
# aapt2 is beside the zipalign and apksigner build.sh has just used. Found
# here rather than in the release branch below, because both build types want
# it now: only the resource table can say what <background> resolves to.
SDKU=""
[ -n "${ANDROID_HOME:-}" ] && SDKU="$(cygpath -u "$ANDROID_HOME" 2>/dev/null || echo "")"
[ -z "$SDKU" ] && [ -n "${LOCALAPPDATA:-}" ] && SDKU="$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || echo "")/Android/Sdk"
A2="$(ls -1 "$SDKU"/build-tools/*/aapt2.exe 2>/dev/null | sort -V | tail -1 || true)"
# aapt2 is a WINDOWS binary and $APK is an MSYS path. Normally MSYS rewrites
# one into the other on the way across, but deploy.sh exports
# MSYS_NO_PATHCONV=1 for the adb calls, and that switch is global — so inside
# a deploy the path arrives as /f/Coding/... and aapt2 answers "failed opening
# zip: I/O error", which reads exactly like a corrupt APK. Hand aapt2 the
# Windows form explicitly and keep $APK for unzip and cmp, which want the
# MSYS one. (Checked standalone this never fires; it only bites under deploy.)
APKW="$(cygpath -w "$APK" 2>/dev/null || echo "$APK")"
# 2. the ground, in BOTH build types. Not "is the file in the APK" — the
# adaptive icon's <background> is a REFERENCE, so what matters is what it
# resolves to, and that is the one question a stale APK answers wrongly
# while every other check still passes. (build.sh pipes Gradle through
# `| tail -20 || true` and then runs `ls -la "$APK"`, so a Gradle run that
# failed leaves the PREVIOUS build's APK exactly where the next step looks
# for it.) Everything below comes out of the resource table and never out
# of a path: a release APK has had res/mipmap-anydpi-v26/ic_launcher.xml
# renamed to res/BW.xml, and the table is the only thing that still knows.
TBL=""
[ -n "$A2" ] && { TBL="$("$A2" dump resources "$APKW" 2>/dev/null)" || bad "aapt2 could not read the resource table of $APK"; }
# name [config] -> the file that resource points at. The config is not
# optional detail: mipmap/ic_launcher has SIX entries — the anydpi adaptive
# XML and five legacy PNGs — so a name-only lookup hands back
# res/mipmap-mdpi-v4/ic_launcher.png and the whole check collapses. An empty
# config means the entry with no qualifier at all, which is what a plain
# drawable has. "anydpi" matches the "anydpi-v26" too, as look() does.
path_of() { echo "$TBL" | awk -v n="$1" -v c="${2:-}" '
$1 == "resource" && $3 == n { f = 1; next }
/^[ \t]*resource 0x/ { f = 0 }
f { cfg = $1; gsub(/[()]/, "", cfg)
if (c == "" || cfg == c || cfg ~ "^" c "-v[0-9]+$")
for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { print $i; exit } }'; }
name_of() { echo "$TBL" | awk -v id="$1" '$1 == "resource" && $2 == id { print $3; exit }'; }
if [ -z "$TBL" ]; then
echo "== ground: NOT CHECKED — no aapt2 under ${SDKU:-(no SDK path)}/build-tools."
echo " What <background> points at is a resource id; only the table reads it."
elif grep -q "0_resource_name_obfuscated" <<< "$TBL"; then
echo "== ground: NOT CHECKED — this APK's resource names are collapsed."
else
ADAPT="$(path_of mipmap/ic_launcher anydpi)"
test -n "$ADAPT" || bad "mipmap/ic_launcher has no anydpi entry in this APK — the adaptive icon is not in it, so $RES never reached src-tauri/gen/android/app/src/main/res"
BGID="$("$A2" dump xmltree "$APKW" --file "$ADAPT" 2>/dev/null \
| awk '/E: background/ { f = 1 } f && /android:drawable\(/ { sub(/.*=@?/, ""); print; exit }')"
test -n "$BGID" || bad "the adaptive icon in this APK ($ADAPT) declares no <background>"
BGNAME="$(name_of "$BGID")"
[ "$BGNAME" = "drawable/ic_launcher_ground" ] \
|| bad "the APK's adaptive background is ${BGNAME:-$BGID}, not drawable/ic_launcher_ground — a stale APK, or the overlay did not reach src-tauri/gen/android/app/src/main/res"
# and that what it resolves to is still concept 04 rather than a flat
# fill. aapt:attr lifts each inline <gradient> out into a resource of its
# own, so the two fills are references as well; follow the ids rather than
# the $name__N filenames aapt2 happens to give them this version.
GPATH="$(path_of drawable/ic_launcher_ground)"
test -n "$GPATH" || bad "drawable/ic_launcher_ground resolves to no file in this APK"
GTREE="$("$A2" dump xmltree "$APKW" --file "$GPATH" 2>/dev/null)" || bad "aapt2 cannot read $GPATH out of this APK"
IDS="$(echo "$GTREE" | grep -oE 'fillColor\(0x[0-9a-f]+\)=@0x[0-9a-f]+' | grep -oE '0x[0-9a-f]+$')"
NFILL="$(echo "$IDS" | grep -c . || true)"
[ "$NFILL" = 2 ] || bad "the ground in this APK has $NFILL gradient fills, not 2 — concept 04 is a surface and the glow over it"
STOPS=""
for id in $IDS; do
g="$(path_of "$(name_of "$id")")"
test -n "$g" || bad "the ground's gradient $id has no file in the resource table"
STOPS="$STOPS $("$A2" dump xmltree "$APKW" --file "$g" | grep -oE 'color\(0x[0-9a-f]+\)=#[0-9a-f]{8}' | grep -oE '[0-9a-f]{8}$' | tr '\n' ' ')"
done
for c in ff0a2330 ff07141f ff040a10 2e31c0cb 0d1f8fc9 0007141f; do
grep -qw "$c" <<< "$STOPS" || bad "the ground in this APK has lost concept 04's stop #$c"
done
echo "== ground: <background> = $BGNAME, concept 04's six stops, read back out of the APK"
fi
# What none of this can see is a pixel. Whether the gradient BANDS on the
# phone's OLED, how the glow reads under the Pixel's mask and how the falcon
# sits on a lit ground rather than a flat one are a launcher screenshot, not a
# resource table.
if [ "$CRUNCH" = 0 ]; then
# 3. debug: the adaptive XMLs by name, then all twenty PNGs byte for byte.
# Nothing but the overlay makes a mipmap-anydpi-v26 file, so its absence
# is exactly the failure this script exists for.
for x in ic_launcher ic_launcher_round; do
grep -q " res/mipmap-anydpi-v26/$x\.xml$" <<< "$LIST" \
|| bad "res/mipmap-anydpi-v26/$x.xml is not in the APK — $RES never reached src-tauri/gen/android/app/src/main/res"
done
# and the ground by name as well. In release the paths are shortened
# away and only the table above can find it; here it is still a file.
grep -q " res/drawable/ic_launcher_ground\.xml$" <<< "$LIST" \
|| bad "res/drawable/ic_launcher_ground.xml is not in the APK — the adaptive background never reached src-tauri/gen/android/app/src/main/res"
for d in $DPI; do for n in $NAMES; do
got="res/mipmap-$d-v4/$n.png"
grep -q " $got$" <<< "$LIST" || bad "$got is not in the APK"
unzip -p "$APK" "$got" | cmp -s - "$RES/mipmap-$d/$n.png" \
|| bad "$got in the APK is not $RES/mipmap-$d/$n.png — a stale res in gen/, the overlay lost a file, or this build crunched its PNGs (then: --crunched)"
done; done
echo "== icon: adaptive + monochrome, five densities, byte for byte from $RES"
exit 0
fi
# 4. release. Ask the resource table what the names point at, since the paths
# have very likely been shortened away.
# $A2 and $SDKU come from section 2, which both build types now need.
if [ -z "$A2" ]; then
echo "== icon: NOT CHECKED — no aapt2 under ${SDKU:-(no SDK path)}/build-tools."
echo " A release APK's resource paths are shortened (res/Ag.png), so names"
echo " alone cannot find the icon. Unverified here, not missing."
exit 0
fi
# one line per file entry: "<name> <config> <path in the APK>"
MAP="$(echo "$TBL" | awk '
/^[ \t]*resource 0x[0-9a-f]+ mipmap\// { split($3, a, "/"); cur = a[2]; next }
/^[ \t]*resource 0x/ { cur = ""; next }
cur != "" { for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { cfg = $1; gsub(/[()]/, "", cfg); print cur, cfg, $i } }
')" || bad "aapt2 could not read the resource table of $APK"
# the other half of the same optimization pass: with resource shrinking on,
# aapt2 also collapses the NAMES, and then even the table cannot say which
# entry is the launcher icon
if grep -q "0_resource_name_obfuscated" <<< "$MAP"; then
echo "== icon: NOT CHECKED — this APK's resource names are collapsed"
echo " (aapt2 optimize --collapse-resource-names, which comes with resource"
echo " shrinking). Nothing left to match on. Unverified here, not missing."
exit 0
fi
# the config is printed as "mdpi" or as "mdpi-v4" depending on the link's
# min-sdk — both seen on this machine from the same resource tree, so both are
# taken, and "anydpi" matches the "anydpi-v26" the adaptive icon lives in
look() { echo "$MAP" | awk -v n="$1" -v c="$2" '$1 == n && ($2 == c || $2 ~ "^" c "-v[0-9]+$") { print $3; exit }'; }
for x in ic_launcher ic_launcher_round; do
got="$(look "$x" anydpi)"
test -n "$got" || bad "mipmap/$x has no anydpi entry in this APK — the adaptive icon is not in it, so $RES never reached src-tauri/gen/android/app/src/main/res"
done
for d in $DPI; do for n in $NAMES; do
got="$(look "$n" "$d")"
test -n "$got" || bad "mipmap/$n has no $d entry in this APK"
a="$(unzip -p "$APK" "$got" | dims)"
b="$(dims < "$RES/mipmap-$d/$n.png")"
[ "$a" = "$b" ] || bad "mipmap/$n at $d ($got) is $a, not the overlay's $b"
done; done
echo "== icon: adaptive + monochrome, five densities, by resource table ($(basename "$(dirname "$A2")")); release re-encodes the bytes, so dimensions only"