test: guard the JNI-visible names against R8 renaming
A renamed JNI callback does not fail the build — it produces a release APK that aborts on the first callback of every test. That shipped once, because every on-device check used the debug build, which does not minify. This reads R8's own mapping file for the classes and callback methods, and the DEX string pool for the native entry points. Native methods are deliberately not checked against mapping.txt: R8 only emits entries for members with a body or a rename, so a native method is absent either way and absence would prove nothing. Verified by deleting the callback keep rules and rebuilding: the check reports MainThreadCallback renamed along with onOutput, onError and onComplete, which is exactly the failure that shipped — with no device involved. Also avoids `... | strings | grep -q` and dumps to a file first. Under `set -o pipefail`, grep -q exits at the first match and SIGPIPEs strings, so the pipeline reports failure for a name found early and success for one found late. It reported two of four names missing while all four were present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+112
@@ -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 "-> <name>".
|
||||||
|
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."
|
||||||
Reference in New Issue
Block a user