diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 9f98f3ee9..36fa717a0 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -130,11 +130,18 @@ compose.desktop { } } - // Compose Multiplatform 1.11.0 tightened the default ProGuard rules so - // unresolved references in transitive deps (Jackson kotlin-module value - // classes, okhttp's optional TLS adapters, JNA cleaner internals) now - // fail `proguardReleaseJars`. Project-level rules below add the missing - // `-dontwarn` directives. + // Compose Multiplatform 1.11.0 wired ProGuard 7.7.0 into the release + // build. The Android (mobile) module already solved the same class of + // problems with `-dontobfuscate` plus global `-keepnames` / `-keep enum` + // rules (see `amethyst/proguard-rules.pro`). We mirror that strategy in + // `compose-rules.pro` so the desktop release survives JNI callbacks + // (secp256k1-kmp, sqlite-bundled, jkeychain, VLCj) and reflection-heavy + // libraries (Jackson, JNA) without renaming. + // + // Shrink and optimize stay ON. One ProGuard optimize sub-pass is + // disabled in `compose-rules.pro` to avoid a generated okio bridge + // whose declared return type the JVM verifier rejects (R8 doesn't hit + // this — it generates bridges differently from ProGuard). buildTypes.release.proguard { configurationFiles.from(project.file("compose-rules.pro")) } diff --git a/desktopApp/compose-rules.pro b/desktopApp/compose-rules.pro index 8ec043b22..e0b66a63d 100644 --- a/desktopApp/compose-rules.pro +++ b/desktopApp/compose-rules.pro @@ -2,76 +2,154 @@ # task (see `compose.desktop.application.buildTypes.release.proguard` wiring # in build.gradle.kts). # -# Compose Multiplatform 1.11.0 ships a stricter default ruleset than 1.10.x -# and now treats unresolved references as fatal. The warnings below come from -# third-party libraries that have always carried optional code paths; they -# were silently tolerated on 1.10.3 and broke desktop packaging on 1.11.0. +# Strategy mirrors the Android (mobile) module's `amethyst/proguard-rules.pro`: # -# This file MUST keep rules for libraries that rely on reflection at runtime, -# in addition to the dontwarn rules for compile-time noise. Otherwise the -# release build will package classes whose reflectively-required methods -# (`values()`/`valueOf()` on enums, JNA `Structure` field accessors, VLCj -# discovery providers loaded via Class.forName) have been stripped or -# renamed, and the app crashes at startup. See the v1.09.1 desktop-DMG -# regression for an example: Jackson's `MapperConfig.collectFeatureDefaults` -# called `SerializationFeature.values()` reflectively, got -# `NoSuchMethodException` (because ProGuard had removed it as "unused"), -# and the JVM failed to launch. +# - shrink: ON (size win, removes whole unused classes, no renames) +# - optimize: ON (speed win; one bridge-generating sub-pass disabled below +# because ProGuard's specialized return-type bridges trip +# the JVM verifier on okio — R8 doesn't hit this) +# - obfuscate: OFF (via `-dontobfuscate`; every native library we ship +# resolves Java callbacks by *class name* through JNI +# `FindClass` / `GetMethodID`. Renaming breaks them.) +# +# The Android module solved the exact same JNI-rename problem with: +# +# -dontobfuscate +# -keepnames class ** { *; } +# -keep enum ** { *; } +# +# Plus per-library `-keep` rules for JNA, libsodium, libscrypt, and the +# first-party (`com.vitorpamplona.**`) JSON-mapped types. We replicate that +# verbatim here, then add desktop-only rules for Jackson, VLCj, OkHttp, +# and SLF4J that the mobile build does not need (those code paths are +# Android-stripped on mobile but compiled on desktop). # ============================================================================ -# Jackson — required to keep app launchable +# Mirror amethyst/proguard-rules.pro +# ============================================================================ +# Preserve the line number information for debugging stack traces, and keep +# every class name + every enum intact so JNI / reflection / ServiceLoader +# lookups continue to resolve after the optimize pass. +-dontobfuscate +-keepattributes LocalVariableTable +-keepattributes LocalVariableTypeTable +-keepattributes *Annotation* +-keepattributes SourceFile +-keepattributes LineNumberTable +-keepattributes Signature +-keepattributes Exceptions +-keepattributes InnerClasses +-keepattributes EnclosingMethod +-keepattributes MethodParameters +-keepparameternames + +-keepdirectories libs + +# Keep all names — belt-and-suspenders against any rename ProGuard might +# still attempt with obfuscate off (e.g. on synthetic inner classes). +-keepnames class ** { *; } + +# Keep all enums whole — Jackson, Kotlin serialization, and many libraries +# enumerate features via `Class.getEnumConstants()` -> `values()`. Stripping +# `values()` / `valueOf(String)` returns null and crashes initialisation. +-keep enum ** { *; } + +# preserve access to native classes (mirrors mobile) +-keep class fr.acinq.secp256k1.** { *; } + +# JNA For Libsodium (mirrors mobile) +-keep class com.goterl.lazysodium.** { *; } + +# libscrypt (mirrors mobile) +-keep class com.lambdaworks.codec.** { *; } +-keep class com.lambdaworks.crypto.** { *; } +-keep class com.lambdaworks.jni.** { *; } + +-keep class info.guardianproject.** { *; } + +# JSON-mapped first-party types (mirrors mobile) +-keep class com.vitorpamplona.quartz.** { *; } +-keep class com.vitorpamplona.amethyst.** { *; } +-keep class com.vitorpamplona.ammolite.** { *; } + +# ============================================================================ +# Desktop-only JNI keep rules — libraries the mobile module doesn't ship +# ============================================================================ +# `-keepnames class ** { *; }` above prevents renames, but does NOT prevent +# the shrink pass from removing members whose only callers are native (JNI +# `FindClass` + `GetStaticMethodID` from a bundled .dylib/.so/.dll). Each of +# these libraries was the source of a v1.09.1 release crash: +# +# - androidx.sqlite (sqlite-bundled): nativeThreadSafeMode() stripped, +# so `BundledSQLiteDriver.threadingMode` threw NoSuchMethodError on the +# first relay-store query (LocalRelayStore.refreshStats). +# - pt.davidafsilva.apple (jkeychain): macOS Keychain JNI for nsec storage. +# +# secp256k1-kmp is already covered by the `-keep class fr.acinq.secp256k1.**` +# rule mirrored from mobile above. +-keep class androidx.sqlite.** { *; } +-keepclassmembers class androidx.sqlite.** { + native ; + static ; +} +-keep class pt.davidafsilva.apple.** { *; } +-keepclassmembers class pt.davidafsilva.apple.** { + native ; +} + +# ============================================================================ +# Optimize sub-pass — disable the one that produces invalid okio bytecode +# ============================================================================ +# ProGuard's `method/specialization/returntype` pass specialised +# `Okio__OkioKt.buffer(Source): BufferedSource` to a synthetic bridge +# `buffer$` whose declared return type was the more concrete +# `RealBufferedSource` while the bytecode body returned the +# `BufferedSource` super-interface. The JVM verifier rejected the bridge: +# +# VerifyError: Bad return type +# Exception Details: +# Location: okio/Okio__OkioKt.buffer$5ae116e(...) +# Reason: Type 'okio/BufferedSource' is not assignable to +# 'okio/RealBufferedSource'. +# +# Disabling just this sub-pass keeps merging, inlining, peephole and +# dead-code optimizations on. Everything else in `optimize` stays. +-optimizations !method/specialization/returntype + +# ============================================================================ +# Desktop-only: Jackson — mobile module does not ship Jackson on Android # ============================================================================ # Jackson uses reflection extensively at construction time (ObjectMapper, -# JsonMapper, SerializationConfig, DeserializationConfig). It enumerates -# feature enums via `Class.getEnumConstants()` -> `values()`, scans for -# `@JsonProperty`/`@JsonCreator`/etc. annotations on user types, and looks -# up sub-classes by name through `Class.forName`. None of that survives an -# optimize+shrink pass without explicit keep rules. +# JsonMapper, SerializationConfig, DeserializationConfig). The +# `-keep enum **` rule above already covers `values()`/`valueOf()`; this +# block also keeps the annotation/databind/core/kotlin-module classes +# intact and silences value-class MethodHandle warnings. -keep class com.fasterxml.jackson.databind.** { *; } -keep class com.fasterxml.jackson.annotation.** { *; } -keep class com.fasterxml.jackson.core.** { *; } -keep class com.fasterxml.jackson.module.kotlin.** { *; } --keepclassmembers enum com.fasterxml.jackson.** { - public static **[] values(); - public static ** valueOf(java.lang.String); -} --keepattributes Signature,*Annotation*,EnclosingMethod,InnerClasses -# Jackson kotlin-module 2.21.x value-class converters call -# `MethodHandle.invokeExact(...)` with polymorphic signatures that ProGuard -# cannot resolve against the abstract MethodHandle declaration. Safe to -# suppress: only used reflectively when (de)serializing Kotlin `value class` -# types. -dontwarn com.fasterxml.jackson.module.kotlin.** # ============================================================================ -# JNA + JNA platform — required by VLCj, kmp-tor, and any native binding +# Desktop-only: full JNA — mobile narrows JNA because Android has no AWT # ============================================================================ -# JNA loads native libraries by reflecting on Structure subclasses (field -# order discovered via `getDeclaredFields`), reads JNA callback interfaces by -# name, and uses `Native.register` against class metadata. Without -keep, -# `Native.` and `Structure.getFields` both fail at runtime. +# On desktop we run a real JVM with AWT, so VLCj / kmp-tor / etc. can use +# the full JNA surface. Keep it wholesale rather than the narrowed mobile +# allowlist. -keep class com.sun.jna.** { *; } -keep class * implements com.sun.jna.** { *; } -keepclassmembers class * extends com.sun.jna.Structure { ; } -# Suppress the inner-class accessor warnings from the new strict ruleset. -# `com.sun.jna.internal.Cleaner$CleanerThread` references package-private -# synthetic accessors (`access$100` …) that ProGuard's class member analysis -# cannot follow through nested inner classes. The accessors exist at runtime. -dontwarn com.sun.jna.internal.Cleaner -dontwarn com.sun.jna.internal.Cleaner$* # ============================================================================ -# VLCj — discovery providers loaded reflectively via java.util.ServiceLoader +# Desktop-only: VLCj — discovery providers loaded via ServiceLoader # ============================================================================ # VLCj enumerates implementations of # `uk.co.caprica.vlcj.factory.discovery.provider.DiscoveryDirectoryProvider` -# via the ServiceLoader mechanism, which reads class names from -# META-INF/services/* and resolves them with `Class.forName(...).newInstance()`. -# ProGuard's default settings strip both the service files and the impl -# classes (since nothing references them by name in user code). Keep -# everything VLCj ships; VLCj is reflection-heavy throughout. +# via META-INF/services and resolves them with `Class.forName(...).newInstance()`. -keep class uk.co.caprica.vlcj.** { *; } -keepclassmembers class * implements uk.co.caprica.vlcj.factory.discovery.provider.DiscoveryDirectoryProvider { public (...); @@ -81,13 +159,13 @@ } # ============================================================================ -# OkHttp — optional TLS adapters discovered via Class.forName +# Desktop-only: OkHttp / Conscrypt / BouncyCastle / OpenJSSE / Graal # ============================================================================ # okhttp ships TLS adapters for GraalVM native-image, Conscrypt, BouncyCastle, -# OpenJSSE, and Jetty boot. None of these are on the desktop classpath; okhttp -# falls back to the JDK default via `Class.forName(...)`-style probes that -# catch NoClassDefFoundError. Suppress the warnings without keeping the -# unused classes. +# OpenJSSE, and Jetty boot. None are on the desktop classpath; okhttp falls +# back to the JDK default via `Class.forName(...)`-style probes that catch +# NoClassDefFoundError. Suppress the warnings without keeping the unused +# classes. -dontwarn okhttp3.internal.graal.** -dontwarn okhttp3.internal.platform.BouncyCastlePlatform -dontwarn okhttp3.internal.platform.BouncyCastlePlatform$* @@ -102,7 +180,7 @@ -dontwarn com.oracle.svm.** # ============================================================================ -# SLF4J / logging — used reflectively by transitive deps +# Desktop-only: SLF4J — used reflectively by transitive deps # ============================================================================ # jackson-databind and a few other deps `Class.forName("org.slf4j.LoggerFactory")` # to detect logging. We ship slf4j-nop; keep it intact so detection succeeds.