Files
offscale-mobile/scripts/build-android.sh
T
pastilhasandClaude Opus 5 859a716b5d fix(build): empty array expansion breaks under set -u on bash 3.2
scripts/build-android.sh died with 'MINIFY_ARGS[@]: unbound variable' before Gradle
ever ran. macOS ships bash 3.2, where expanding an EMPTY array under set -u is itself
an unbound-variable error — the array is declared and empty, which is the normal case,
since MINIFY defaults to off.

The monorepo's copy of this code has the same bare expansion and works, because that
script does not set -u. So the bug was latent there and became real the moment a
stricter copy ran it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 23:17:11 +01:00

76 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Clean-build the release APK and install it on the attached device.
#
# scripts/build-android.sh # build + install
# ABIS=arm64-v8a,x86_64 scripts/… # add an emulator ABI
# MINIFY=1 scripts/… # turn R8 on (see the note below — not the default)
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOGS="${TMPDIR:-/tmp}offscale-build"
mkdir -p "$LOGS"
log="$LOGS/android.log"
: >"$log"
cd "$ROOT" || exit 1
# The vendored Tailscale core is built by hand, per platform, and forgetting it fails much later as a
# missing symbol rather than a missing rebuild — Android says "Unresolved reference 'loginTo'" at
# Kotlin compile time. Do it here instead of relying on anyone remembering.
. "$ROOT/scripts/ensure-core.sh" android
echo "logs: $log"
# `bun run prebuild`, NOT `bunx expo prebuild`: the package.json script runs gen-adaptive-icon.sh
# first, and calling expo directly silently skips it — which ships an icon the Android launcher mask
# crops. Trailing args append to that script, and `expo prebuild` is its last command.
if ! bun run prebuild --platform android --clean --no-install >>"$log" 2>&1; then
echo "PREBUILD FAILED — $log"; exit 1
fi
# Build for phones only.
#
# Expo's generated gradle.properties asks for `armeabi-v7a,arm64-v8a,x86,x86_64`, and the native
# libraries are most of an APK — for an app this shape, roughly 65 MB of it is lib/x86 and lib/x86_64,
# Intel builds for emulators that no phone will ever execute, and another 21 MB is armeabi-v7a for
# 32-bit devices. The Tailscale core is a large .so and gets built once per ABI, so this matters more
# here than in an app without one.
#
# Overridden here rather than in gradle.properties because `expo prebuild --clean` on the line above
# regenerates that file every single build, so an edit there survives exactly nothing.
#
# ABIS=… to override — an emulator needs x86_64 and will refuse an APK without it.
ABIS="${ABIS:-arm64-v8a}"
# MINIFY=1 turns R8 on. OFF by default, and that default is the honest one.
#
# R8 deletes every class nothing reaches from an entry point. It decides "nothing reaches this" by
# reading the code — so anything reached by REFLECTION looks dead and gets deleted. React Native and
# Expo both do that, and so does the gomobile binding layer. The build still succeeds; the app crashes
# on the device. This cannot be verified by looking at a file size, and it must not become the default
# until a build with it has been opened on a real phone, connected to a tailnet, and used.
# Expanded below as ${MINIFY_ARGS[@]+...} rather than bare: macOS ships bash 3.2, where expanding an
# EMPTY array under `set -u` is itself an unbound-variable error. The monorepo's copy of this script
# survives the bare form only because it does not set -u — the bug was latent there and real here.
MINIFY_ARGS=()
if [[ "${MINIFY:-0}" == "1" ]]; then
MINIFY_ARGS=(-Pandroid.enableMinifyInReleaseBuilds=true)
fi
# assembleRelease, always. A debug build needs Metro attached and behaves differently from what ships,
# so testing it answers the wrong question.
if ! (cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" \
${MINIFY_ARGS[@]+"${MINIFY_ARGS[@]}"} --console=plain) >>"$log" 2>&1; then
echo "GRADLE FAILED — $log"; exit 1
fi
apk="$(find android/app/build/outputs/apk/release -name '*.apk' | head -1)"
[[ -z "$apk" ]] && { echo "NO APK"; exit 1; }
if ! adb install -r "$apk" >>"$log" 2>&1; then
# Almost always the cable, not the build. The APK is on disk either way: rerun just the install
# rather than the whole thing.
echo "INSTALL FAILED (device attached?) — apk at $apk"; exit 1
fi
echo "OK"