Four gaps that made "ship it" and "test it on hardware" impossible. RELEASE SIGNING. assembleRelease was silently producing app-release-unsigned.apk because there was no signingConfigs block at all — the failure mode where you only notice at install time. Credentials now resolve from keystore.properties, then local.properties, then WEBTERM_RELEASE_* env; nothing is created or committed, and keystore.properties.example is the template. The degradation is ENFORCED, not documented. The signingConfig is only created when credentials actually resolve — an empty config is precisely what makes AGP emit an unsigned artifact quietly — and a guard on packageRelease throws with an actionable message, distinguishing "not configured" from "credentials found but the keystore file is missing: <path>". It hooks the PACKAGE task on purpose: R8 has already run and reported by then, so a developer without a keystore still gets the full shrink result rather than an early abort. Verified: minifyReleaseWithR8 completes, packageRelease fails loudly, and no unsigned APK is written. VERSIONING. Was still the scaffold 1 / "0.1.0". Now 1 / "0.1.0-alpha01", where the suffix is a factual claim about device verification rather than decoration: alpha means device QA is essentially unstarted, beta means the A34/A35 checklist blocks pass on hardware, and plain 0.1.0 means the whole checklist is ticked. versionCode stays 1 because no artifact has ever left the build machine. MINIFICATION. release had isMinifyEnabled = false and an empty rules file — so nothing had ever exercised R8 on this app, and the first release build would have been the experiment. Now minify + resource shrinking, with a deliberately short rules file: the actual artifacts were checked, and kotlinx.serialization, Tink, Hilt, FCM, OkHttp, Compose and CameraX all ship their own consumer rules, so none are re-declared. The termux packages are kept whole because the JitPack artifacts ship no rules and :terminal-view binds to non-API internals — it writes the public TerminalView.mEmulator field and calls TerminalRenderer.getFontWidth()/getFontLineSpacing(). Getting that wrong yields a build that crashes only in production. lintVitalRelease also passes now, the two pre-existing lint ERRORS having been fixed earlier this session. ANDROID TEST ENABLEMENT. :app had no androidTest source set and no instrumentation runner, which is the mechanical reason A34/A35 have no code at all. The runner (Hilt-aware) and dependencies are wired, and a :macrobenchmark module exists. The tests themselves are a separate change. Verified: ./gradlew test :app:assembleDebug :app:assembleDebugAndroidTest :app:assembleBenchmark :macrobenchmark:assembleBenchmark :app:lintVitalRelease koverVerify -> BUILD SUCCESSFUL; 901 JVM tests, 0 failures on a forced re-execution with test-results deleted and --no-build-cache.
118 lines
7.7 KiB
Prolog
118 lines
7.7 KiB
Prolog
# ─────────────────────────────────────────────────────────────────────────────
|
|
# WebTerm :app — R8 keep rules (release + benchmark; both isMinifyEnabled = true).
|
|
#
|
|
# DISCIPLINE: every rule below is justified, and rules that turned out to be
|
|
# UNNECESSARY are recorded as such rather than added "just in case". A superfluous
|
|
# -keep silently costs size and hides real reachability problems; a MISSING one
|
|
# crashes only in production. Both were checked against the merged configuration
|
|
# R8 actually used, which is dumped to:
|
|
# app/build/outputs/mapping/<variant>/configuration.txt
|
|
# Read that file before adding anything here — most of our stack already ships its
|
|
# own consumer rules, and duplicating them is how this file rots.
|
|
#
|
|
# ALREADY COVERED BY EMBEDDED CONSUMER RULES — do NOT re-declare here:
|
|
# · kotlinx.serialization — kotlinx-serialization-core-jvm ships
|
|
# META-INF/com.android.tools/r8/kotlinx-serialization-{common,r8}.pro, which keeps
|
|
# @Serializable classes' `Companion` fields, `serializer(...)`, object `INSTANCE`,
|
|
# the `$$serializer.descriptor` field, and RuntimeVisibleAnnotations (the R8
|
|
# full-mode fix). Verified present in 1.9.0. Our @Serializable models live in
|
|
# :wire-protocol / :api-client; the rules are global, so they are covered.
|
|
# · Hilt / Dagger — hilt-android + dagger AARs ship proguard.txt; all injection is
|
|
# generated code with compile-time references, no runtime reflection to preserve.
|
|
# · Tink — tink-android ships META-INF/proguard/protobuf.pro, keeping <fields> on
|
|
# subclasses of the shaded protobuf GeneratedMessageLite (the reflective lite
|
|
# runtime). Tink's key managers are registered explicitly by AeadConfig.register(),
|
|
# not discovered reflectively, so nothing else needs keeping.
|
|
# · FCM — firebase-messaging ships consumer rules, AND `.push.FcmService`,
|
|
# `.push.DenyBroadcastReceiver`, `.push.AllowTrampolineActivity`, `.MainActivity`
|
|
# and `.WebTermApp` are all declared in AndroidManifest.xml, for which AGP
|
|
# auto-generates keep rules (build/intermediates/aapt_proguard_file/.../aapt_rules.txt).
|
|
# Manifest-declared components therefore need no rule here — ever.
|
|
# · OkHttp / Okio, Compose, CameraX, ML Kit barcode, DataStore, navigation-compose,
|
|
# biometric — all ship their own consumer rules.
|
|
# · JNI — the default proguard-android-optimize.txt already contains
|
|
# `-keepclasseswithmembernames class * { native <methods>; }`, which covers the
|
|
# Termux emulator's native entry points even though we never fork a subprocess.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# ── Termux terminal emulator + view (the ONE thing that genuinely needs a rule) ──
|
|
#
|
|
# Why this is not "just in case":
|
|
# 1. The JitPack artifacts (com.github.termux.termux-app:terminal-{emulator,view},
|
|
# v0.118.0) ship NO consumer rules at all — nobody upstream has reasoned about R8
|
|
# for these modules, because Termux itself ships unminified.
|
|
# 2. :terminal-view deliberately binds to NON-API internals of the pinned version
|
|
# (plan §6.1 — TerminalView is `public final`, so we fork rather than subclass):
|
|
# · RemoteTerminalHostView writes and reads the public field
|
|
# `com.termux.view.TerminalView.mEmulator` directly (installEmulator /
|
|
# the TerminalViewClient callbacks / computeVerticalScrollRange).
|
|
# · TerminalResizeDriver calls `TerminalRenderer.getFontWidth()` and
|
|
# `getFontLineSpacing()` — public accessors verified with `javap -p`, but not
|
|
# part of any documented API surface.
|
|
# · TerminalScrollGesture reads `mEmulator` and drives KeyHandler.
|
|
# Field/method RENAMING alone would survive that (R8 rewrites both sides), but R8
|
|
# full mode also does class merging, member inlining and access relaxation across
|
|
# a library whose invariants we are already stretching. A wrong outcome here is a
|
|
# crash in the app's single core surface — the terminal — visible ONLY in release.
|
|
# 3. The cost is bounded and small: two Apache-2.0 modules, ~120 KB of classes.
|
|
#
|
|
# So: keep them whole. Revisit only with a device-verified minified build in hand.
|
|
-keep class com.termux.terminal.** { *; }
|
|
-keep class com.termux.view.** { *; }
|
|
|
|
|
|
# ── ML Kit / Firebase component discovery (reflective no-arg constructors) ───────
|
|
#
|
|
# OBSERVED, not speculative. Installing the minified `benchmark` APK on an API-35
|
|
# emulator and cold-starting it produced, with no rule here:
|
|
#
|
|
# W ComponentDiscovery: Invalid component registrar.
|
|
# Could not instantiate com.google.mlkit.common.internal.CommonComponentRegistrar
|
|
# ... at com.google.mlkit.common.internal.MlKitInitProvider.onCreate
|
|
# Caused by: java.lang.NoSuchMethodException:
|
|
# com.google.mlkit.common.internal.CommonComponentRegistrar.<init> []
|
|
# (and the same for com.google.mlkit.vision.common.internal.VisionCommonRegistrar)
|
|
#
|
|
# Firebase's ComponentDiscovery instantiates registrars named in <meta-data> by
|
|
# `getDeclaredConstructor()`. The class names survive, but R8 removed the no-arg
|
|
# constructors because nothing in the app calls them. The failure is a WARNING, not a
|
|
# crash: the app starts fine and the ML Kit component graph is simply never registered,
|
|
# so **QR pairing (A19 barcode scanning) silently stops working in minified builds
|
|
# only**. Exactly the release-only breakage this file has to prevent.
|
|
#
|
|
# `<init>();` (no access modifier) matches any visibility, which is what
|
|
# getDeclaredConstructor needs. Scoped to registrars, so it keeps ~a dozen tiny classes.
|
|
-keep class * implements com.google.firebase.components.ComponentRegistrar {
|
|
<init>();
|
|
}
|
|
|
|
|
|
# ── javax.naming does not exist on Android ──────────────────────────────────────
|
|
#
|
|
# R8 refuses to run without these two, so they are load-bearing for ANY minified build.
|
|
# They are NOT a benign suppression — read this before deleting them:
|
|
#
|
|
# :client-tls CertificateSummary.kt uses `javax.naming.ldap.LdapName` to pull the CN
|
|
# out of an RFC2253 DN. That package is JDK-only; it is absent from android.jar and
|
|
# from the ART bootclasspath. So on a real device `CertificateSummaryReader.commonName`
|
|
# throws NoClassDefFoundError, which is an Error and therefore slips straight through
|
|
# that function's `catch (_: Exception)` guard.
|
|
#
|
|
# The module is pure-JVM and its unit tests run on a real JDK where LdapName DOES
|
|
# exist, which is why the green JVM suite never caught it. Tracked as a handoff to
|
|
# :client-tls's owner (hand-parse the DN, or catch Throwable). This -dontwarn only
|
|
# stops R8 from failing the build over an unresolvable reference; it does not and
|
|
# cannot fix the device behaviour.
|
|
-dontwarn javax.naming.ldap.LdapName
|
|
-dontwarn javax.naming.ldap.Rdn
|
|
|
|
|
|
# ── Crash triage ────────────────────────────────────────────────────────────────
|
|
# Keep line numbers so release stack traces are usable, and rename the source-file
|
|
# attribute to a single constant so no original filenames leak in the APK. Retrace
|
|
# release traces with app/build/outputs/mapping/release/mapping.txt (ARCHIVE IT with
|
|
# every distributed artifact — without it a crash report is unreadable).
|
|
-keepattributes SourceFile,LineNumberTable
|
|
-renamesourcefileattribute SourceFile
|