@@ -144,6 +144,131 @@ jobs:
144144 - name : Build APK (release, all ABIs)
145145 run : cargo tauri android build --apk
146146
147+ - name : Apply wry getId() ProGuard workaround (issue # 8)
148+ # `wry <= 0.55.0`'s `proguard-wry.pro` template is missing
149+ # `int getId();` for `WryActivity`. R8 strips the Kotlin auto-
150+ # generated getter, then tao 0.35's JNI bridge calls
151+ # `activity.getId()` at onActivityCreate, gets NoSuchMethodError,
152+ # panics, and the app crashes before its first frame.
153+ #
154+ # Upstream fix: tauri-apps/wry#1721 (merged 2026-05-04, awaiting
155+ # release). Until that flows in transitively, patch the rule
156+ # post-init and force R8 to re-run. Two complications:
157+ #
158+ # 1. `cargo tauri android init` (run by `cargo tauri android
159+ # build`) regenerates the file from wry's bundled template
160+ # every time, so the patch only sticks between init and the
161+ # next R8 invocation.
162+ # 2. Gradle's R8 task does not declare the proguard file as an
163+ # input, so editing it in place doesn't invalidate R8's
164+ # incremental cache. We have to wipe R8 outputs to force a
165+ # re-run.
166+ #
167+ # Sequence: patch → wipe R8 + dex outputs → re-run gradle
168+ # `assembleUniversalRelease`, excluding rust tasks since the
169+ # `.so`s are already on disk from the previous build step.
170+ # Once the next wry release lands, delete this whole step.
171+ shell : bash
172+ run : |
173+ set -euo pipefail
174+
175+ # `-print -quit` avoids the SIGPIPE-on-`find | head` risk
176+ # under pipefail. Guard the search root so a missing tree
177+ # surfaces a clear error rather than an opaque empty result.
178+ if [ ! -d src-tauri/gen/android ]; then
179+ echo "::error::src-tauri/gen/android missing — Tauri Android init did not run"
180+ exit 1
181+ fi
182+ PROGUARD=$(find src-tauri/gen/android -name "proguard-wry.pro" -path "*/generated/*" -print -quit)
183+ if [ -z "$PROGUARD" ]; then
184+ echo "::error::proguard-wry.pro not found under src-tauri/gen/android — Tauri's Android scaffolding may have changed"
185+ exit 1
186+ fi
187+ echo "Patching $PROGUARD"
188+
189+ if grep -q "int getId();" "$PROGUARD"; then
190+ echo "::notice::proguard-wry.pro already has 'int getId();' — wry release likely shipped the upstream fix; this workflow step can be removed"
191+ else
192+ python3 - "$PROGUARD" <<'PY'
193+ import pathlib, re, sys
194+ p = pathlib.Path(sys.argv[1])
195+ s = p.read_text()
196+ # Match the WryActivity keep block for any package and insert
197+ # `int getId();` between `getAppClass(...)` and `getVersion()`,
198+ # mirroring the position in tauri-apps/wry#1721.
199+ new, n = re.subn(
200+ r'(-keep class \S+\.WryActivity \{[^}]*?\n)(\s+java\.lang\.String getVersion\(\);)',
201+ r'\1 int getId();\n\2',
202+ s,
203+ count=1,
204+ flags=re.DOTALL,
205+ )
206+ if n != 1:
207+ sys.stderr.write("could not locate WryActivity keep block in proguard-wry.pro — wry template shape may have changed\n")
208+ sys.exit(1)
209+ p.write_text(new)
210+ PY
211+ grep -q "int getId();" "$PROGUARD" || { echo "::error::patch did not apply"; exit 1; }
212+ echo "Patch applied."
213+ fi
214+
215+ # Wipe R8 outputs so the next gradle run actually re-runs R8
216+ # (Gradle's incremental check misses our edit).
217+ rm -rf src-tauri/gen/android/app/build/outputs/apk
218+ rm -rf src-tauri/gen/android/app/build/outputs/mapping
219+ rm -rf src-tauri/gen/android/app/build/intermediates/dex
220+ rm -rf src-tauri/gen/android/app/build/intermediates/r8_dex_archive_for_full_bundle
221+
222+ # Re-run gradle assemble, skipping the rust-build tasks (the
223+ # `.so` files are already in jniLibs from the prior build).
224+ # Skipping is required because those tasks invoke `cargo tauri`
225+ # standalone, which fails outside of the Tauri parent process
226+ # (it tries to connect to a WebSocket the parent owns).
227+ (
228+ cd src-tauri/gen/android
229+ ./gradlew :app:assembleUniversalRelease \
230+ -x :app:rustBuildArm64Release \
231+ -x :app:rustBuildArmRelease \
232+ -x :app:rustBuildX86_64Release \
233+ -x :app:rustBuildX86Release \
234+ --no-daemon --console=plain
235+ )
236+
237+ # Sanity-check: confirm getId() landed inside the WryActivity
238+ # class in the dex. This is the workaround's load-bearing
239+ # invariant — anything that prevents the check itself from
240+ # running is treated as a hard failure, not a silent skip.
241+ DEXDUMP=$(find "$ANDROID_HOME/build-tools" -name dexdump -type f -print -quit 2>/dev/null || true)
242+ if [ -z "$DEXDUMP" ] || [ ! -x "$DEXDUMP" ]; then
243+ echo "::error::dexdump not found in $ANDROID_HOME/build-tools — cannot verify the workaround"
244+ exit 1
245+ fi
246+ shopt -s nullglob
247+ apks=(src-tauri/gen/android/app/build/outputs/apk/universal/release/*-unsigned.apk)
248+ if [ "${#apks[@]}" -eq 0 ]; then
249+ echo "::error::no universal-release APK produced after re-running gradle"
250+ exit 1
251+ fi
252+ UNIVERSAL="${apks[0]}"
253+ # Scope the grep to the WryActivity class block so we don't get
254+ # a false-positive from any other class that defines getId.
255+ # dexdump emits class descriptor lines with leading spaces
256+ # (" Class descriptor : ..."), so anchor the block-end
257+ # match with [[:space:]]*. Without that, in_block stays
258+ # true past WryActivity and a later RustWebView.getId would
259+ # satisfy the check even if WryActivity.getId was stripped.
260+ if LC_ALL=C "$DEXDUMP" "$UNIVERSAL" 2>/dev/null | LC_ALL=C awk '
261+ /Class descriptor.*WryActivity;/ { in_block=1; next }
262+ in_block && /^[[:space:]]*Class descriptor/ { in_block=0 }
263+ in_block && /name *: .getId./ { found=1 }
264+ END { exit !found }
265+ '; then
266+ echo "✓ WryActivity.getId() present in dex"
267+ else
268+ echo "::error::workaround failed — WryActivity.getId() still missing from dex"
269+ exit 1
270+ fi
271+
147272 - name : Sign APKs with Android debug keystore
148273 # `cargo tauri android build --apk` produces unsigned release
149274 # APKs, which Android refuses to install
0 commit comments