ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the ThreadCount Staff app bundle.
|
||||
#
|
||||
# bash scripts/build-staff-aab.sh → android-staff/app/build/outputs/bundle/release/*.aab
|
||||
# bash scripts/build-staff-aab.sh debug → a debug APK, for putting on a phone quickly
|
||||
#
|
||||
# Two things here are not boilerplate.
|
||||
#
|
||||
# **The barcode scanner is stripped after every sync.** Capacitor has no per-app plugin list: it
|
||||
# scans package.json and wires every installed plugin into whichever native project it is syncing.
|
||||
# The counter app needs @capacitor-mlkit/barcode-scanning; this one does not, and taking it means
|
||||
# taking MLKit, CameraX, three native libraries and a CAMERA permission into an app that every
|
||||
# clinical staff member in a hospital installs. Nothing a wearer does involves scanning, so the
|
||||
# two generated gradle files and the generated plugin list get the plugin cut out of them again on
|
||||
# the way past. All three are regenerated by `cap sync`, which is exactly why this lives in the
|
||||
# build rather than in a one-off edit.
|
||||
#
|
||||
# **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-staff/local.properties
|
||||
|
||||
echo "==> syncing the staff shell into android-staff"
|
||||
TC_APP=staff npx cap sync android
|
||||
|
||||
PLUGINS_JSON=android-staff/app/src/main/assets/capacitor.plugins.json
|
||||
echo "==> removing the plugins Capacitor just put back"
|
||||
# Both files carry a "DO NOT EDIT" banner because cap regenerates them; we edit the regenerated
|
||||
# copy, every time, on purpose. Each plugin contributes two lines to settings.gradle (its include
|
||||
# and its projectDir) and one to capacitor.build.gradle, and all three name the plugin, so a
|
||||
# single match per file is enough.
|
||||
for plugin in capacitor-mlkit-barcode-scanning capacitor-haptics; do
|
||||
sed -i "/$plugin/d" android-staff/capacitor.settings.gradle
|
||||
sed -i "/$plugin/d" android-staff/app/capacitor.build.gradle
|
||||
done
|
||||
# The third generated file is the list Capacitor reads at startup, and it names the stripped
|
||||
# plugins by class. Leaving them in it is not harmless: PluginManager.loadPluginClasses() throws
|
||||
# on the first class that is no longer in the bundle, BridgeActivity catches it and carries on
|
||||
# with *no* plugins registered at all — so a plugin this app genuinely needs would silently never
|
||||
# load, and it would look like a bug in the page.
|
||||
#
|
||||
# It used to be written out empty, which was correct only while every installed plugin was one
|
||||
# this app strips. @capacitor/browser broke that: it is the thing that hands a tapped Privacy or
|
||||
# Terms link to the phone's browser instead of loading it into a shell with no way back, and an
|
||||
# empty list would have shipped it dead while package.json, the gradle files and the sync output
|
||||
# all said it was there. So the array is filtered by package name rather than blanked, and a
|
||||
# plugin added later survives unless it is named in the strip loop above.
|
||||
python3 - "$PLUGINS_JSON" <<'PY'
|
||||
import json, sys
|
||||
p = sys.argv[1]
|
||||
keep = [e for e in json.load(open(p)) if not any(k in e.get("pkg", "") for k in ("barcode-scanning", "haptics"))]
|
||||
json.dump(keep, open(p, "w"), indent=2)
|
||||
open(p, "a").write("\n")
|
||||
print(" kept in the staff bundle: " + (", ".join(e["pkg"] for e in keep) or "(none)"))
|
||||
PY
|
||||
# The scanner brings MLKit, CameraX, three native libraries and a CAMERA permission; haptics
|
||||
# brings VIBRATE. A wearer scans nothing and this app buzzes at nobody, so both go. The check is
|
||||
# a hard failure rather than a warning: shipping an app to every nurse in a hospital that asks
|
||||
# for the camera would be worth stopping a release over.
|
||||
if grep -qE "mlkit|haptics" android-staff/capacitor.settings.gradle android-staff/app/capacitor.build.gradle \
|
||||
"$PLUGINS_JSON"; then
|
||||
echo "FATAL: a stripped plugin is still wired in — refusing to build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "debug" ]; then
|
||||
echo "==> assembling a debug APK"
|
||||
( cd android-staff && ./gradlew --no-daemon assembleDebug )
|
||||
find android-staff/app/build/outputs/apk -name "*.apk" -print
|
||||
exit 0
|
||||
fi
|
||||
|
||||
KEYS="$HOME/threadcount-keys/staff-keystore.properties"
|
||||
[ -f "$KEYS" ] || echo "NOTE: $KEYS is missing — the bundle will be unsigned."
|
||||
|
||||
echo "==> building the release bundle"
|
||||
( cd android-staff && ./gradlew --no-daemon bundleRelease )
|
||||
|
||||
AAB=$(find android-staff/app/build/outputs/bundle/release -name "*.aab" | head -1)
|
||||
echo "==> $AAB"
|
||||
|
||||
# Prove the two things Play checks and the one thing this app promises.
|
||||
echo "==> checks"
|
||||
if unzip -l "$AAB" | grep -q "\.so$"; then
|
||||
echo " ! native libraries present — the scanner strip did not work"
|
||||
exit 1
|
||||
else
|
||||
echo " ✓ no native libraries (so no 16 KB alignment problem and no debug symbols to ship)"
|
||||
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
|
||||
|
||||
BT="$HOME/.cache/ca-android/bundletool.jar"
|
||||
if [ -f "$BT" ]; then
|
||||
# Only <uses-permission> counts. A permission name also appears as the `android:permission`
|
||||
# guard on a component — androidx.profileinstaller puts DUMP on its receiver so that only the
|
||||
# shell can broadcast to it — and matching those made this read as if the app asked for things
|
||||
# it does not.
|
||||
PERMS=$("$JAVA_HOME/bin/java" -jar "$BT" 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' ' ')"
|
||||
if echo "$PERMS" | grep -qE 'CAMERA|VIBRATE|RECORD_AUDIO|ACCESS_FINE_LOCATION'; then
|
||||
echo " ! this app asks for more than it needs"
|
||||
exit 1
|
||||
fi
|
||||
# Anything beyond INTERNET and the signature-level permission Capacitor defines for its own
|
||||
# dynamic receivers is a plugin that got in without anyone deciding it should.
|
||||
if echo "$PERMS" | grep -vqE 'android.permission.INTERNET|DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'; then
|
||||
echo " ! an unexpected permission is present"
|
||||
exit 1
|
||||
fi
|
||||
echo " ✓ internet only"
|
||||
fi
|
||||
Reference in New Issue
Block a user