ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:54:35 +10:00
commit 344b1701dd
505 changed files with 56231 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
#!/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 '<uses-permission[^>]*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"