#!/usr/bin/env bash # Assert that R8 left the names JNI resolves at runtime alone. # # iperf_jni.c looks up the callback methods by name with GetMethodID. R8 renames # by default, and a renamed callback does not fail the build — it produces an # APK that aborts on the first callback of every test: # # NoSuchMethodError: no non-static method "Lp0;.onOutput(Ljava/lang/String;)V" # # That shipped once. Building the release APK is not enough to catch it, because # the build succeeds; this reads R8's own mapping file and checks the names # survived. Run after `flutter build apk --release`. set -euo pipefail MAPPING="${1:-build/app/outputs/mapping/release/mapping.txt}" APK="${2:-build/app/outputs/flutter-apk/app-release.apk}" if [[ ! -f "$MAPPING" ]]; then echo "FAIL: no R8 mapping at $MAPPING" echo " Run 'flutter build apk --release' first. If the release build" echo " no longer minifies, this check is meaningless — say so" echo " explicitly rather than deleting it." exit 1 fi failed=0 # A class kept by name maps to itself: "a.b.C -> a.b.C:". require_class_kept() { local fqcn="$1" if grep -qF -- "$fqcn -> $fqcn:" "$MAPPING"; then echo " ok class $fqcn" else echo " FAIL class $fqcn was renamed or removed" grep -F -- "$fqcn ->" "$MAPPING" | head -3 | sed 's/^/ got: /' || true failed=1 fi } # A method kept by name ends with "-> ". require_method_kept() { local signature="$1" name="$2" if grep -qE -- "$signature.*-> ${name}\$" "$MAPPING"; then echo " ok method $name" else echo " FAIL method $name was renamed" grep -E -- "$signature" "$MAPPING" | head -3 | sed 's/^/ got: /' || true failed=1 fi } echo "Checking JNI-visible names in $MAPPING" require_class_kept 'com.novoyuuparosk.iperf3gui.IperfRunner' require_class_kept 'com.novoyuuparosk.iperf3gui.IperfRunner$IperfCallback' require_class_kept 'com.novoyuuparosk.iperf3gui.IperfRunner$MainThreadCallback' # Resolved by iperf_jni.c on the concrete callback object's class. require_method_kept 'void onOutput\(java\.lang\.String\)' 'onOutput' require_method_kept 'void onError\(java\.lang\.String\)' 'onError' require_method_kept 'void onComplete\(\)' 'onComplete' # Native methods are NOT checked against mapping.txt: R8 only emits entries for # members it has line-number info or a rename for, and a native method has no # body, so it is absent whether or not it was renamed. Absence would prove # nothing. Check the DEX string pool instead — a surviving name is interned # there, a renamed one is not. check_dex_names() { local apk="$1" shift if [[ ! -f "$apk" ]]; then echo " skip DEX name check: no APK at $apk" return fi local tmp tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' RETURN if ! unzip -o -q "$apk" 'classes*.dex' -d "$tmp" 2>/dev/null; then echo " FAIL could not extract classes*.dex from $apk" failed=1 return fi # Dump the string pool once to a file, then grep the file. Never # `... | strings | grep -q`: grep -q exits at the first match, SIGPIPEs # strings, and `set -o pipefail` reports the whole pipeline as failed — so # a name found early "fails" and one found late passes. That flakiness cost # an afternoon; leave the intermediate file in place. cat "$tmp"/classes*.dex | strings > "$tmp/dex-strings.txt" local name for name in "$@"; do if grep -qxF -- "$name" "$tmp/dex-strings.txt"; then echo " ok native $name" else echo " FAIL native $name is absent from the DEX string pool" failed=1 fi done } check_dex_names "${2:-build/app/outputs/flutter-apk/app-release.apk}" \ runIperfLive forceStopIperfTest setTempDir isTestRunning if [[ $failed -ne 0 ]]; then echo echo "R8 renamed something JNI resolves by name. The release APK will abort" echo "on the first callback. Fix android/app/proguard-rules.pro." exit 1 fi echo "All JNI-visible names survived minification."