diff --git a/tool/check_jni_keep_rules.sh b/tool/check_jni_keep_rules.sh new file mode 100755 index 0000000..9ac7bd7 --- /dev/null +++ b/tool/check_jni_keep_rules.sh @@ -0,0 +1,112 @@ +#!/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."