android: the keep rules, and the release build that proves them
Every APK this port has produced is `assembleArm64Debug`, and the generated project turns R8 on for `release` and only for `release` — while tauri finds a plugin class by string, files every `@Command` under `commands[method.name]`, and lets Jackson fill every `@InvokeArg` field by field. Minification breaks exactly that, and it breaks it without a word: the app compiles, installs, launches, shows Home, and then a tap on Library folder answers "No command pickFolder found for plugin ..." in logcat and nothing on screen. So each plugin module now carries its own `consumer-rules.pro` naming its own reflected surface — the shape tauri-android and tauri-plugin-dialog already use in this build, and one that travels with the plugin into a regenerated `gen/android`, which a rule written into that gitignored tree would not. What belongs to no single plugin is in `src-tauri/android/proguard-soulstream.pro`, copied into the generated project by the build script because the app's own Gradle file is regenerated: the runtime annotation attributes, a `-dontwarn` pair for the two `java.beans` classes jackson-databind references and Android does not have (R8 turns a missing class into a build error, so without them there may be no APK at all), and a keep for the four `@Command` methods declared on `app.tauri.plugin.Plugin` itself. Tauri's own rule matches classes annotated `@TauriPlugin` and the members they declare; the base class carries no such annotation, so nothing names `registerListener` — which is what the lock-screen card's buttons and the download notice's Cancel and Open ride on. `build.sh` grows `--release`: the release profile, `--features tauri/devtools` so the walk can still attach at all, `assembleArm64Release`, and a zipalign and sign with the same debug key the phone already trusts, so the APK installs over the app rather than forcing an uninstall that would take Kyle's settings with it. With no argument the script is what it has always been, cargo line for cargo line, so deploy.sh and ship.sh keep the path they run every twenty minutes. And it has been built. Not on the phone and not as arm64 — as **x86_64**, on an emulator, because that is what this machine can run — but the flavour is the only difference: the same Gradle, the same R8, the same rules, the same zipalign and apksigner. `minifyX86_64ReleaseWithR8` succeeded and wrote **no** `missing_rules.txt` at all, which is the `-dontwarn` pair doing its job; `lintVitalX86_64Release` passed, so the `-x lintVital` advice is for a failure that has not happened. The signed APK went over the debug app with `adb install -r` and left its data alone, exactly as the same-key argument above says it must. Then the minified app was walked: Home, the record, playback, the queue, the sheets, the lyrics, the drags, the export — **131 of 131, no exceptions, no console errors, every step as expected**, and nothing in logcat about a missing command or class. That is the failure this round exists to prevent, looked for and not found. What it does not cover is the hands-only list in ANDROID.md — the pickers, the notification buttons, the share sheet — which no walk reaches, and the four `@Command` methods on `Plugin` itself remain a precaution rather than a diagnosed bug. The document says so, command by command.
This commit is contained in:
1 parent
9c3273e6af
commit
07dad65ca5
13 files changed
+452
-11
No files matched your search
@@ -4,12 +4,46 @@ set -euo pipefail
|
||||
cd /f/Coding/SoulStream
|
||||
source src-tauri/android/env.sh
|
||||
|
||||
# Which build. The default is exactly what this script has always done, and
|
||||
# has to stay that way: deploy.sh calls it with no argument every twenty
|
||||
# minutes, and ship.sh calls deploy.sh. `--release` (or SS_ANDROID_RELEASE=1)
|
||||
# is the other path, and as of this round nobody has run it — see ANDROID.md
|
||||
# → "The release build (nobody has made one yet)".
|
||||
REL=0
|
||||
case "${1:-}" in
|
||||
--release) REL=1 ;;
|
||||
"") ;;
|
||||
*) echo "usage: build.sh [--release]"; exit 2 ;;
|
||||
esac
|
||||
if [ "${SS_ANDROID_RELEASE:-0}" = "1" ]; then REL=1; fi
|
||||
if [ "$REL" = "1" ]; then
|
||||
PROFILE=release
|
||||
VARIANT=Arm64Release
|
||||
# --features is repeatable and cargo unions them, so custom-protocol on
|
||||
# the cargo line below is kept. devtools is not a nicety: wry compiles the
|
||||
# setWebContentsDebuggingEnabled call only under
|
||||
# cfg(any(debug_assertions, feature = "devtools")), so a plain release APK
|
||||
# opens no webview_devtools_remote socket and `smoke.mjs --phone` cannot
|
||||
# attach to the one build that most needs walking. R8 never sees it.
|
||||
CARGO_EXTRA="--release --features tauri/devtools"
|
||||
test -d src-tauri/gen/android/app || { echo "no generated Android project — run the CLI's android init first"; exit 1; }
|
||||
# The app-wide keep rules, into a project that cannot hold them itself:
|
||||
# gen/ is regenerated and gitignored. The release build type globs
|
||||
# **/*.pro out of this directory; the debug type reads none of them.
|
||||
cp src-tauri/android/proguard-soulstream.pro src-tauri/gen/android/app/proguard-soulstream.pro
|
||||
echo "== RELEASE: R8 on, rules from src-tauri/android/proguard-soulstream.pro + each plugin's consumer-rules.pro"
|
||||
else
|
||||
PROFILE=debug
|
||||
VARIANT=Arm64Debug
|
||||
CARGO_EXTRA=""
|
||||
fi
|
||||
|
||||
echo "== cargo $(date +%T)"
|
||||
cargo build --lib -p app --target aarch64-linux-android --features tauri/custom-protocol 2>&1 | grep -v "^\s*Compiling\|^\s*Fresh"
|
||||
cargo build --lib -p app --target aarch64-linux-android --features tauri/custom-protocol $CARGO_EXTRA 2>&1 | grep -v "^\s*Compiling\|^\s*Fresh"
|
||||
# the grep above hides cargo's exit code; a failed compile must not package
|
||||
# the previous library as if it were this one
|
||||
test "${PIPESTATUS[0]}" -eq 0 || { echo "cargo failed — not packaging"; exit 1; }
|
||||
SO=target/aarch64-linux-android/debug/libapp_lib.so
|
||||
SO=target/aarch64-linux-android/$PROFILE/libapp_lib.so
|
||||
test -f "$SO" || { echo "no .so built"; exit 1; }
|
||||
echo "== strip $(date +%T)"
|
||||
DEST=src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a
|
||||
@@ -19,8 +53,33 @@ cp "$SO" "$DEST/libapp_lib.so"
|
||||
ls -la "$DEST/libapp_lib.so"
|
||||
echo "== gradle $(date +%T)"
|
||||
cd src-tauri/gen/android
|
||||
./gradlew.bat assembleArm64Debug -x rustBuildArm64Debug -q 2>&1 | grep -v "^$" | tail -20 || true
|
||||
./gradlew.bat assemble$VARIANT -x rustBuild$VARIANT -q 2>&1 | grep -v "^$" | tail -20 || true
|
||||
cd /f/Coding/SoulStream
|
||||
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
|
||||
if [ "$REL" = "1" ]; then
|
||||
UNSIGNED=src-tauri/gen/android/app/build/outputs/apk/arm64/release/app-arm64-release-unsigned.apk
|
||||
if [ ! -f "$UNSIGNED" ]; then
|
||||
echo "no release APK — gradle's tail is above. The two failures we expect:"
|
||||
echo " R8 missing class — its own list is at src-tauri/gen/android/app/build/outputs/mapping/arm64Release/missing_rules.txt;"
|
||||
echo " add what it names to src-tauri/android/proguard-soulstream.pro and run again"
|
||||
echo " lintVital — re-run gradle by hand with -x lintVitalArm64Release"
|
||||
echo " (AGP 8 splits it; -x lintVitalAnalyzeArm64Release may be the one)"
|
||||
exit 1
|
||||
fi
|
||||
KS="$HOME/.android/debug.keystore"
|
||||
test -f "$KS" || { echo "no $KS — the release APK is unsigned and no phone will install it"; exit 1; }
|
||||
BT="$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1 || true)"
|
||||
test -n "$BT" || { echo "no build-tools under $SDK — cannot sign"; exit 1; }
|
||||
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/release/app-arm64-release.apk
|
||||
echo "== sign $(date +%T) ($(basename "$BT"))"
|
||||
"$BT/zipalign.exe" -p -f 4 "$UNSIGNED" "$APK"
|
||||
# The SAME key the debug builds are signed with, deliberately. A release
|
||||
# APK signed with any other key cannot be installed over the app already
|
||||
# on the phone, and the obvious next move — uninstall, then install —
|
||||
# takes the app's data with it. This is a test build, not a distributable
|
||||
# one; a real one needs a real key and this script is not where that goes.
|
||||
MSYS_NO_PATHCONV=1 "$BT/apksigner.bat" sign --ks "$(cygpath -w "$KS")" --ks-key-alias androiddebugkey --ks-pass pass:android --key-pass pass:android "$APK"
|
||||
else
|
||||
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
|
||||
fi
|
||||
ls -la "$APK"
|
||||
echo "== done $(date +%T)"
|
||||
@@ -0,0 +1,96 @@
|
||||
# The app-wide R8 rules for a SoulStream release APK — the ones that belong
|
||||
# to no single plugin.
|
||||
#
|
||||
# This file lives in the repo on purpose. The Gradle project that consumes it,
|
||||
# src-tauri/gen/android/app, is GENERATED and gitignored, and
|
||||
# `tauri android init` rewrites it, so a rule written into that project's own
|
||||
# proguard-rules.pro is a rule with a short life.
|
||||
# `bash src-tauri/android/build.sh --release` copies this file next to that
|
||||
# project's build.gradle.kts, whose release build type reads
|
||||
#
|
||||
# proguardFiles(*fileTree(".") { include("**/*.pro") }
|
||||
# .plus(getDefaultProguardFile("proguard-android-optimize.txt")) ...)
|
||||
#
|
||||
# — anything ending in .pro in that directory. Copying it is a no-op for the
|
||||
# debug build: the debug build type sets isMinifyEnabled = false and names no
|
||||
# proguard files at all, so the path the deploy pipeline runs every twenty
|
||||
# minutes never reads a line of this.
|
||||
#
|
||||
# NOR are the rules for the wry/JNI bridge here. wry's build script writes
|
||||
# proguard-wry.pro next to the Kotlin it generates, in
|
||||
# app/src/main/java/com/soulstream/app/generated/, and the release build type's
|
||||
# **/*.pro glob is recursive, so it is already picked up: Rust's native
|
||||
# methods, WryActivity, Ipc's @JavascriptInterface, RustWebView.evalScript.
|
||||
# proguard-tauri.pro, beside this file, keeps TauriActivity.getPluginManager().
|
||||
# Duplicating either here would be a second place to get them wrong.
|
||||
#
|
||||
# Per-plugin rules are NOT here. Each plugin carries its own
|
||||
# consumer-rules.pro inside crates/tauri-plugin-*/android, so they travel with
|
||||
# the plugin into a regenerated gen/android and into a plain
|
||||
# `npx tauri android build` too.
|
||||
#
|
||||
# NOTHING BELOW HAS BEEN PROVEN BY A BUILD. Every APK this project has ever
|
||||
# made is assembleArm64Debug with minification off. See ANDROID.md →
|
||||
# "The release build".
|
||||
|
||||
|
||||
# Tauri reads its own annotations off the class files at RUNTIME:
|
||||
# PluginHandle.indexMethods() asks each Method whether @Command,
|
||||
# @ActivityCallback and @PermissionCallback are present, and all three are
|
||||
# @Retention(AnnotationRetention.RUNTIME). proguard-android-optimize.txt is
|
||||
# expected to keep them with `-keepattributes *Annotation*`, but that file is
|
||||
# extracted from the Android Gradle plugin when a release build runs and no
|
||||
# release build has ever run here, so it has never existed in this tree to
|
||||
# read. Say it outright rather than inherit it from a file this project does
|
||||
# not own and cannot see. Signature, InnerClasses
|
||||
# and EnclosingMethod are for Jackson, which resolves an @InvokeArg field's
|
||||
# declared type before it fills it.
|
||||
-keepattributes RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,AnnotationDefault,Signature,InnerClasses,EnclosingMethod
|
||||
|
||||
|
||||
# THE GAP. Tauri's own consumer rule is
|
||||
#
|
||||
# -keep @app.tauri.annotation.TauriPlugin public class * {
|
||||
# @app.tauri.annotation.Command public <methods>; ... }
|
||||
#
|
||||
# and a class-member specification matches the members a matched class
|
||||
# DECLARES. These four @Command methods are declared on app.tauri.plugin.Plugin
|
||||
# — the abstract base every plugin extends — which carries no @TauriPlugin of
|
||||
# its own; tauri's other rule for that package keeps only @JniMethod and
|
||||
# native members. So no rule in the build names them, while
|
||||
# PluginHandle.indexMethods() walks the superclass chain with
|
||||
# getDeclaredMethods() and registers them by name regardless.
|
||||
#
|
||||
# registerListener is the one this app rides on. addPluginListener() in
|
||||
# src/mobile/media.svelte.js and src/mobile/xfernote.svelte.js invokes
|
||||
# plugin:media-session|registerListener and
|
||||
# plugin:transfer-notice|registerListener, and those subscriptions are how the
|
||||
# lock-screen card's prev/play/next/stop and the download notice's cancel and
|
||||
# open get back into the app. Lose them and everything still looks right and
|
||||
# nothing either of those buttons does works.
|
||||
#
|
||||
# Written as a precaution, not as a diagnosed bug: whether R8 reaches
|
||||
# inherited members through tauri's rule could not be settled from source, and
|
||||
# it cannot be settled on this machine at all. Six lines to stop asking.
|
||||
-keep class app.tauri.plugin.Plugin {
|
||||
public void registerListener(app.tauri.plugin.Invoke);
|
||||
public void removeListener(app.tauri.plugin.Invoke);
|
||||
public void checkPermissions(app.tauri.plugin.Invoke);
|
||||
public void requestPermissions(app.tauri.plugin.Invoke);
|
||||
}
|
||||
|
||||
|
||||
# jackson-databind 2.15.3 — tauri's own dependency, the thing that fills every
|
||||
# @InvokeArg — ships no META-INF/proguard rules of its own, and its
|
||||
# databind/ext/Java7SupportImpl references java.beans.ConstructorProperties and
|
||||
# java.beans.Transient, neither of which exists on Android. R8 in AGP 7+ turns
|
||||
# a missing class into a BUILD ERROR, so without these two lines the release
|
||||
# build may not get as far as producing an APK.
|
||||
#
|
||||
# If R8 names others, its own list is the answer: add what
|
||||
# app/build/outputs/mapping/arm64Release/missing_rules.txt says to this file
|
||||
# and run again. (jackson-core 2.15.3 scanned clean, and the
|
||||
# java.lang.reflect.RecordComponent in jdk14/JDK14Util is a reflective string
|
||||
# rather than a class reference, so it should not be reported.)
|
||||
-dontwarn java.beans.ConstructorProperties
|
||||
-dontwarn java.beans.Transient
|
||||
Reference in new issue
Block a user