#!/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"