Merge pull request #2922 from mstrofnone/fix/desktop-proguard-jni-and-bytecode
fix(desktop): mirror Android ProGuard strategy for release builds
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
|
||||
+129
-51
@@ -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 <methods>;
|
||||
static <methods>;
|
||||
}
|
||||
-keep class pt.davidafsilva.apple.** { *; }
|
||||
-keepclassmembers class pt.davidafsilva.apple.** {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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$<hash>` 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.<clinit>` 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 {
|
||||
<fields>;
|
||||
}
|
||||
# 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 <init>(...);
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user