Check every JNI symbol, not just the first one
🚴 Build and Test BikeControl / Workspace tests (push) Successful in 16m55s
Build & Release / Run tests (push) Successful in 6m35s
🚴 Build and Test BikeControl / Android compile check (push) Successful in 3m41s
Build & Release / Build Linux (deb + AppImage) (push) Successful in 15m18s
Build & Release / Build Arch package (push) Successful in 29m19s
Build & Release / Build Android APK (push) Failing after 29s
Build & Release / Create release (push) Skipped

The android-check job failed on a tree that is perfectly correct:
android.rs exports three `Java_..._MainActivity_*` symbols, the check
compared all three against a single expected name and called it a
mismatch.

Two bugs, both from assuming one native:

- SYM collected every export while EXPECTED was built from one method, so
  the comparison was three lines against one.
- The Kotlin side matched `external fun name()` on empty parens, which
  quietly skipped nativeSetActivity and nativeBluetoothStateChanged —
  parameters do not appear in the symbol name, so stop at the paren.

Now both sides are collected into sets and diffed, so a native declared
in Kotlin with no Rust half fails as loudly as the reverse. Verified
against the real files: passes as-is, and fails with a readable diff for
a Kotlin-only native and for a renamed Rust export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 12:12:42 +02:00
co-authored by Claude Opus 5
parent 5a89433746
commit da56985499
+27 -12
View File
@@ -138,7 +138,7 @@ jobs:
# Cheap invariants that a compiler cannot see. Both failures are silent:
# the app builds, installs, launches, and then finds no trainer.
- name: Check the Android source layout and the JNI symbol
- name: Check the Android source layout and the JNI symbols
run: |
set -e
# gen/ is generated and must stay untracked, or a `tauri android init`
@@ -152,20 +152,35 @@ jobs:
KT=src-tauri/android/src/main/java/paris/tourolle/bikecontrol/MainActivity.kt
[ -f "$KT" ] || { echo "❌ $KT is missing"; exit 1; }
# The JNI symbol in android.rs is matched by the *runtime*, by name.
# Rename the Kotlin package or the method and nothing fails to
# compile — btleplug simply never gets initialised.
SYM=$(sed -n 's/.*pub extern "system" fn \(Java_[A-Za-z0-9_]*\).*/\1/p' src-tauri/src/android.rs)
# JNI symbols are matched by the *runtime*, by name. Rename the
# Kotlin package, add a native without its Rust half, or drop a Rust
# export, and nothing fails to compile — the method simply is not
# there when Java calls it. So compare the two sets, both ways.
#
# Sets, not a single pair: there are three natives now, and matching
# `external fun name()` on empty parens missed the two that take
# arguments. Parameters do not appear in the symbol name, so stop at
# the opening paren.
TMP="${RUNNER_TEMP:-/tmp}"
PKG=$(sed -n 's/^package \(.*\)$/\1/p' "$KT" | tr -d '\r')
METHOD=$(sed -n 's/.*external fun \([A-Za-z0-9_]*\)().*/\1/p' "$KT")
EXPECTED="Java_$(echo "$PKG" | tr '.' '_')_MainActivity_$METHOD"
if [ "$SYM" != "$EXPECTED" ]; then
echo "❌ JNI symbol mismatch:"
echo " android.rs exports: $SYM"
echo " MainActivity needs: $EXPECTED"
PREFIX="Java_$(echo "$PKG" | tr '.' '_')_MainActivity_"
sed -n 's/.*pub extern "system" fn \(Java_[A-Za-z0-9_]*\).*/\1/p' \
src-tauri/src/android.rs | sort -u > "$TMP/jni-rust"
sed -n "s/.*external fun \([A-Za-z0-9_]*\)(.*/$PREFIX\1/p" \
"$KT" | sort -u > "$TMP/jni-kotlin"
[ -s "$TMP/jni-rust" ] || { echo "❌ No JNI exports found in android.rs"; exit 1; }
if ! diff -u "$TMP/jni-kotlin" "$TMP/jni-rust" > "$TMP/jni-diff"; then
echo "❌ JNI symbols differ between MainActivity.kt and android.rs:"
echo " - declared in MainActivity.kt, not exported by android.rs"
echo " + exported by android.rs, not declared in MainActivity.kt"
tail -n +4 "$TMP/jni-diff"
exit 1
fi
echo "✅ JNI symbol $SYM matches $PKG.MainActivity.$METHOD"
echo "✅ $(wc -l < "$TMP/jni-rust") JNI symbols match $PKG.MainActivity:"
sed 's/^/ /' "$TMP/jni-rust"
- name: Cargo check (aarch64-linux-android)
run: |