fix(desktop): unbreak release crash via correct ProGuard rules (#5236)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-04-23 16:33:16 -05:00
committed by GitHub
parent dea9d86c52
commit 2e6730d1e3
5 changed files with 155 additions and 230 deletions

View File

@@ -44,17 +44,16 @@ internal fun Project.configureAndroidCompose(commonExtension: CommonExtension) {
"androidx.compose.runtime",
"androidx.compose.ui",
)
// The BOM exclusion above strips versions from transitive material deps
// (e.g. maps-compose-widgets, datadog). Pin the material group to the
// AndroidX version that matches this CMP release.
// The BOM exclusion above strips the version from `androidx.compose.material:material`
// requested by maps-compose-widgets (google flavor). Pin only that artifact — the
// group also contains `material-ripple`, which CMP publishes at the bom-aligned
// version and must not be force-downgraded.
val materialVersion = libs.version("androidx-compose-material")
configurations.configureEach {
resolutionStrategy.eachDependency {
if (requested.group in cmpAlignedGroups) {
useVersion(androidxComposeVersion)
} else if (requested.group == "androidx.compose.material") {
} else if (requested.group == "androidx.compose.material" && requested.name == "material") {
useVersion(materialVersion)
}
}

View File

@@ -1,166 +1,135 @@
# ============================================================================
# Meshtastic Shared ProGuard / R8 rules
# ============================================================================
# Cross-platform keep and dontwarn rules applied to BOTH the Android app
# release build (R8) and the Desktop distribution (ProGuard). Host-specific
# rules live in the per-module proguard-rules.pro file.
# Cross-platform keep rules applied to BOTH the Android app release (R8) and
# the Desktop distribution (ProGuard 7.7 invoked by compose-jb).
#
# Rule of thumb: anything describing a library shared between Android and
# Desktop (Koin, kotlinx-serialization, Wire, Room KMP, Ktor, Coil 3, Kable,
# Kermit, Okio, DataStore, Paging, Lifecycle / Navigation 3, AboutLibraries,
# Markdown renderer, QRCode, Compose Multiplatform resources, core modules)
# belongs here. Anything platform-specific (AWT/Skiko/JNA, AIDL, Android
# framework, JDK-version quirks, flavor specifics) stays in the host file.
# IMPORTANT: compose-jb's standalone ProGuard task does NOT auto-include
# `META-INF/proguard/*.pro` consumer rules from dependency jars (only R8 on
# Android does — https://github.com/Guardsquare/proguard/issues/423).
# So this file inlines all the consumer rules we depend on for desktop. On
# Android these are duplicates of what R8 already auto-discovers, which is
# harmless. Per JetBrains compose-multiplatform docs: keep it as a single
# static .pro file and add rules as shrinking surfaces problems.
# ============================================================================
# ---- Attributes -------------------------------------------------------------
-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations,AnnotationDefault
# Preserve line numbers for meaningful stack traces, plus metadata needed for
# reflective serializer/DI/Room lookups.
-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations
# ---- Compose Multiplatform 1.11 optimizer defense (#5146) -------------------
# CMP 1.11 ships consumer rules with `-assumenosideeffects` on
# Composer.<clinit>() / ComposerImpl.<clinit>() and `-assumevalues` on
# ComposeRuntimeFlags / ComposeStackTraceMode. The primary defence is
# `-dontoptimize` (set per-host in app/desktop proguard-rules.pro), which
# disables rewriting of these directives. Broad package-wide keeps below have
# been removed per R8_Configuration_Analysis.md as they are redundant — rely
# instead on CMP's own consumer rules + @DoNotInline annotations. If animations
# freeze in a future CMP/KGP release, replace with class-level keeps on the
# specific failure points (Composer, ComposerImpl, ComposeRuntimeFlags,
# ComposeStackTraceMode) rather than package-wide wildcards.
# ---- Kotlin / Coroutines ----------------------------------------------------
# Kotlin stdlib and kotlinx-coroutines ship their own consumer ProGuard rules
# (kotlin-stdlib and kotlinx-coroutines-core consumer-rules.pro) which keep
# Metadata, Continuation, kotlin.reflect internals, and debug metadata. No
# explicit wildcards needed here.
# ---- Compose Multiplatform resources ----------------------------------------
-keep class org.meshtastic.core.resources.Res { *; }
-keepclassmembers class org.meshtastic.core.resources.Res$* { *; }
# ---- Koin DI (reflection-based injection) -----------------------------------
# Prevent R8 from merging exception classes (observed as io.ktor.http.URLDecodeException
# replacing Koin's InstanceCreationException in stack traces, making crashes
# undiagnosable). Broadened to all of koin core to cover the KSP-generated graph.
-keep class org.koin.** { *; }
-dontwarn org.koin.**
# Keep Koin-annotated modules/components so Koin Annotations (KSP) output
# survives tree-shaking.
# ---- Koin Annotations (KSP-generated DI graph) ------------------------------
-keep @org.koin.core.annotation.Module class * { *; }
-keep @org.koin.core.annotation.ComponentScan class * { *; }
-keep @org.koin.core.annotation.Single class * { *; }
-keep @org.koin.core.annotation.Factory class * { *; }
-keep @org.koin.core.annotation.KoinViewModel class * { *; }
# ---- kotlinx-serialization --------------------------------------------------
# ---- kotlinx.coroutines (inlined from coroutines.pro consumer rules) --------
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}
-keepclassmembers class kotlin.coroutines.SafeContinuation {
volatile <fields>;
}
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn sun.misc.SignalHandler
-dontwarn java.lang.instrument.Instrumentation
-dontwarn sun.misc.Signal
-dontwarn java.lang.ClassValue
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-keep class kotlinx.serialization.** { *; }
-dontwarn kotlinx.serialization.**
# Keep @Serializable classes and their generated $serializer companions
# ---- kotlinx.serialization (inlined from kotlinx-serialization-common.pro) --
-keepclassmembers @kotlinx.serialization.Serializable class ** {
static ** Companion;
}
-if @kotlinx.serialization.internal.NamedCompanion class *
-keepclassmembers class * {
static <1> *;
}
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
-keep class **.$serializer { *; }
-keepclassmembers class **.$serializer { *; }
-keepclasseswithmembers class ** {
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
-dontnote kotlinx.serialization.**
-dontwarn kotlinx.serialization.internal.ClassValueReferences
-keepclassmembers public class **$$serializer {
private ** descriptor;
}
# ---- Wire Protobuf ----------------------------------------------------------
# ---- kotlinx.datetime (inlined from datetime.pro consumer rules) ------------
-dontwarn kotlinx.serialization.KSerializer
-dontwarn kotlinx.serialization.Serializable
# Wire generates an ADAPTER static field on every Message subclass accessed
# reflectively during encoding/decoding. Keep those fields and the
# ProtoAdapter subclasses themselves; Wire's bundled consumer rules preserve
# the runtime itself.
# ---- Ktor (inlined from ktor.pro + ServiceLoader gap) -----------------------
-keepclassmembers class io.ktor.** {
volatile <fields>;
}
-keepclassmembernames class io.ktor.** {
volatile <fields>;
}
-keep class io.ktor.client.engine.** implements io.ktor.client.HttpClientEngineContainer
# ktor consumer rules preserve the ServiceLoader META-INF/services file but not
# the impl classes; ContentNegotiation discovers KotlinxSerializationJsonExtensionProvider
# reflectively and crashes with ServiceConfigurationError without these.
-keep class * implements io.ktor.serialization.kotlinx.KotlinxSerializationExtensionProvider { *; }
-keep class io.ktor.serialization.kotlinx.json.** { *; }
# ---- androidx.annotation.Keep (inlined from androidx-annotations.pro) -------
-keep,allowobfuscation @interface androidx.annotation.Keep
-keep @androidx.annotation.Keep class * {*;}
-keepclasseswithmembers class * { @androidx.annotation.Keep <methods>; }
-keepclasseswithmembers class * { @androidx.annotation.Keep <fields>; }
-keepclasseswithmembers class * { @androidx.annotation.Keep <init>(...); }
-keepclassmembers,allowobfuscation class * {
@androidx.annotation.DoNotInline <methods>;
}
# ---- androidx.datastore (inlined from datastore-preferences-core.pro) -------
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
# ---- Wire Protobuf (no consumer rules shipped) ------------------------------
-keepclassmembers class * extends com.squareup.wire.Message {
public static *** ADAPTER;
}
-keepclassmembers class * extends com.squareup.wire.ProtoAdapter { *; }
# Suppress warnings about missing Android Parcelable (Wire cross-platform stubs
# when compiling for non-Android JVM targets; harmless on Android).
-dontwarn android.os.Parcel**
-dontwarn android.os.Parcelable**
# ---- androidx.sqlite bundled driver (JNI native bridge) ---------------------
# androidx.sqlite-bundled's consumer rule keeps native methods only but the
# bundled JNI library calls back into JVM methods on the driver class
# (e.g. `nativeThreadSafeMode`). Keep the whole driver package.
-keep class androidx.sqlite.driver.bundled.** { *; }
-keepclassmembers class androidx.sqlite.driver.bundled.** { native <methods>; *; }
# ---- Room KMP (room3) -------------------------------------------------------
# Preserve generated database constructors (Room uses reflection to instantiate)
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
-keep class * implements androidx.room3.RoomDatabaseConstructor { *; }
# Keep the expect/actual MeshtasticDatabaseConstructor + database surface
-keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; }
-keep class org.meshtastic.core.database.MeshtasticDatabase { *; }
# Room's own consumer rules (from androidx.room3) keep DAOs, entities,
# generated _Impl classes, and TypeConverters referenced from the database.
# ---- SQLite bundled --------------------------------------------------------
# androidx.sqlite ships consumer rules.
# ---- Ktor (ServiceLoader + plugin discovery) --------------------------------
# Keep ServiceLoader metadata files (ktor discovers HttpClientEngineFactory
# implementations reflectively via ServiceLoader).
-keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; }
# ---- Coil 3 (image loading) -------------------------------------------------
# coil3 ships consumer rules.
# ---- Kable BLE --------------------------------------------------------------
# com.juul.kable ships consumer rules; if release builds fail with missing
# Kable classes, restore a narrow keep for the specific reflection-loaded type.
# ---- Compose Multiplatform resources ----------------------------------------
# Generated resource accessor classes (Res.string.*, Res.drawable.*, etc.).
# Without these the fdroid flavor has crashed at startup with a misleading
# URLDecodeException due to R8 exception-class merging.
-keep class org.jetbrains.compose.resources.** { *; }
-keep class org.meshtastic.core.resources.Res { *; }
-keepclassmembers class org.meshtastic.core.resources.Res$* { *; }
# ---- AboutLibraries ---------------------------------------------------------
# com.mikepenz.aboutlibraries ships consumer rules.
# ---- Multiplatform Markdown Renderer ----------------------------------------
# com.mikepenz.markdown ships consumer rules.
# ---- QR Code Kotlin ---------------------------------------------------------
-keep class io.github.g0dkar.qrcode.** { *; }
-dontwarn io.github.g0dkar.qrcode.**
-keep class qrcode.** { *; }
-dontwarn qrcode.**
# ---- Kermit logging ---------------------------------------------------------
# co.touchlab.kermit ships consumer rules.
# ---- Okio -------------------------------------------------------------------
# okio ships consumer rules.
# ---- DataStore --------------------------------------------------------------
# androidx.datastore ships consumer rules.
# ---- Paging -----------------------------------------------------------------
# androidx.paging ships consumer rules.
# ---- Lifecycle / Navigation 3 / ViewModel (JetBrains forks) -----------------
# androidx.lifecycle and androidx.navigation3 ship consumer rules.
# ---- Meshtastic shared model ------------------------------------------------
# core.model types are reached via static references from Koin-wired graphs,
# Room entities, and kotlinx-serialization @Serializable companions all of
# which have their own keep rules above.
# ---- Compose Runtime & Animation --------------------------------------------
# Defence-in-depth: prevent tree-shaking of Compose infrastructure classes that
# are referenced indirectly through compiler-generated state machines. Applies
# to BOTH R8 (Android app) and ProGuard (desktop distribution).
#
# Why shared: CMP 1.11 ships consumer rules with -assumenosideeffects on
# Composer.<clinit>() / ComposerImpl.<clinit>() and -assumevalues on
# ComposeRuntimeFlags / ComposeStackTraceMode. If the optimizer runs (R8 full
# mode on Android, ProGuard with optimize.set(true) on desktop) these call
# sites can be rewritten even when the target classes are kept, causing the
# recomposer / frame-clock / animation state machines to silently freeze on
# the first frame. -dontoptimize (set per-host) is the primary defence; these
# keep rules are a safety net against future toolchain changes. See #5146.
-keep class androidx.compose.runtime.** { *; }
-keep class androidx.compose.ui.** { *; }
-keep class androidx.compose.animation.core.** { *; }
-keep class androidx.compose.animation.** { *; }
-keep class androidx.compose.foundation.** { *; }
-keep class androidx.compose.material3.** { *; }

View File

@@ -99,6 +99,12 @@ val generateBuildConfig =
sourceSets.main { kotlin.srcDir(generateBuildConfig.map { buildConfigOutputDir }) }
// ── ProGuard configuration ───────────────────────────────────────────────────
// compose-jb's standalone ProGuard 7.7 task does NOT auto-include
// `META-INF/proguard/*.pro` consumer rules from dependency jars (only R8 on
// Android does). We therefore inline every keep rule we need into the two
// static .pro files referenced below.
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(21))

View File

@@ -1,113 +1,64 @@
# ============================================================================
# Meshtastic Desktop ProGuard rules for release minification
# ============================================================================
# Open-source project: we rely on tree-shaking (unused code removal) for size
# reduction. Obfuscation is disabled in build.gradle.kts (obfuscate.set(false)).
# Open-source: obfuscation is OFF (build.gradle.kts: obfuscate.set(false)).
# Tree-shaking still runs.
#
# Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room,
# Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3,
# AboutLibraries, Markdown, QRCode, CMP resources, core model) live in
# config/proguard/shared-rules.pro and are wired in by this module's
# build.gradle.kts. This file holds only desktop/JVM-specific rules.
# Two rule sources are merged into the ProGuard run:
# 1. JetBrains' bundled `default-compose-desktop-rules.pro` (auto-injected
# by the compose.desktop Gradle plugin).
# 2. Cross-platform project keeps in config/proguard/shared-rules.pro,
# which inlines every dependency consumer rule we need on desktop
# compose-jb's standalone ProGuard task does NOT auto-discover
# `META-INF/proguard/*.pro` consumer rules from dependency jars (only
# R8 on Android does — https://github.com/Guardsquare/proguard/issues/423).
#
# This file only holds desktop/JVM-specific rules that aren't covered above.
# ============================================================================
# ---- General ----------------------------------------------------------------
# Suppress notes about duplicate resource files (common in fat JARs)
-dontnote **
# Disable ProGuard optimization passes. Tree-shaking (unused code removal) still
# runs — only method-body rewrites and call-site transformations are suppressed.
#
# Why: CMP 1.11 ships consumer rules with -assumenosideeffects on
# Composer.<clinit>() and ComposerImpl.<clinit>(), plus -assumevalues on
# ComposeRuntimeFlags and ComposeStackTraceMode. These optimization directives
# let the optimizer rewrite *call sites* (class-init triggers, flag reads) even
# when the target classes are preserved by -keep rules. The result is that the
# Compose recomposer/frame-clock/animation state machines silently freeze on
# their first frame in release builds. -dontoptimize is the only directive that
# disables processing of -assumenosideeffects/-assumevalues. The desktop compose
# build sets optimize.set(true), so this applies here as well as to R8. See #5146.
-dontoptimize
# Do not parse/rewrite Kotlin metadata during shrinking/optimization.
# ProGuard's KotlinShrinker cannot handle the metadata produced by Compose
# Multiplatform 1.11.x + Kotlin 2.3.x, causing a NullPointerException.
# Since we disable obfuscation (class names remain stable), metadata references
# stay valid and do not need rewriting. The annotations themselves are preserved
# by -keepattributes *Annotation*.
#
# NOTE: -dontprocesskotlinmetadata is a ProGuard-only directive; R8 does not
# recognize it, which is why it lives in the desktop-only file.
# ---- ProGuard 7.7 + Kotlin 2.3 metadata workaround --------------------------
# ProGuard 7.7's KotlinShrinker NPEs on metadata produced by CMP 1.11 +
# Kotlin 2.3.x. Because we don't obfuscate, class names stay stable and
# metadata references remain valid without rewriting. Annotations themselves
# are preserved by `-keepattributes *Annotation*` in shared-rules.pro.
# (R8-only directive equivalent does not exist; this is ProGuard-only.)
-dontprocesskotlinmetadata
# ---- Disable optimizer (CMP 1.11 -assumenosideeffects defense) --------------
# See shared-rules.pro for full rationale. Even though build.gradle.kts sets
# `optimize.set(true)` so compose-jb wires the optimization step, this rule
# turns it into a no-op — keeping CMP's `-assumenosideeffects` directives from
# rewriting Composer call sites and freezing the runtime. See #5146.
-dontoptimize
# ---- Entry point ------------------------------------------------------------
# (org.meshtastic.desktop.MainKt is covered by the package-wide keep below.)
# ---- Meshtastic desktop host shell ------------------------------------------
# Keep all desktop module classes (thin host shell not worth tree-shaking)
# Keep the desktop host shell (thin module — not worth tree-shaking).
-keep class org.meshtastic.desktop.** { *; }
# ---- JVM runtime suppression ------------------------------------------------
-dontwarn java.lang.reflect.**
-dontwarn sun.misc.Unsafe
-dontwarn java.lang.invoke.**
# ---- jSerialComm (cross-platform serial library with Android stubs) ---------
# ---- jSerialComm Android stubs (cross-platform serial library) --------------
# jSerialComm bundles Android shims that reference android.* classes; harmless
# on JVM/desktop but ProGuard fails the build on unresolved program classes
# unless suppressed.
-dontwarn com.fazecast.jSerialComm.android.**
# ---- Kotlin stdlib atomics (Kotlin 2.3+ intrinsics, not on JDK 17) ----------
# Wire ships AndroidMessage in its common runtime; on desktop classpath there is
# no android.os.Parcelable. We never use AndroidMessage on desktop.
-dontwarn com.squareup.wire.AndroidMessage
-dontwarn com.squareup.wire.AndroidMessage$*
-dontwarn android.os.Parcelable
-dontwarn android.os.Parcelable$*
-dontwarn kotlin.concurrent.atomics.**
-dontwarn kotlin.uuid.UuidV7Generator
# ---- Library consumer rules ------------------------------------------------
# The compose-jb gradle plugin auto-injects `default-compose-desktop-rules.pro`
# (bundled inside org.jetbrains.compose:compose-gradle-plugin) into every
# desktop ProGuard run. That file already covers:
# - kotlin.**, kotlinx.coroutines.** (incl. SwingDispatcherFactory ServiceLoader)
# - org.jetbrains.skiko.**, org.jetbrains.skia.**
# - kotlinx.serialization.** (incl. @Serializable companion keeps)
# - kotlinx.datetime.**
# - androidx.compose.runtime SnapshotStateKt + Material3 SliderDefaults
# So we DO NOT re-declare those here. Source of truth:
# https://github.com/JetBrains/compose-multiplatform/blob/master/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro
#
# However, the standalone ProGuard 7.7.0 that compose-jb invokes does NOT
# auto-import library `META-INF/proguard/*.pro` consumer rules from arbitrary
# jars (only R8/Android does). So any consumer-rule pattern outside the bundled
# defaults above must be copied here manually (see Ktor SL block below).
# ---- androidx.sqlite bundled driver (JNI native bridge) ---------------------
# BundledSQLiteDriver loads `libsqliteJni` and the native code calls back into
# JVM-land via methods on `BundledSQLiteDriverKt` (e.g. `nativeThreadSafeMode`)
# and member methods on `BundledSQLiteDriver` itself. Because those JVM symbols
# are referenced only from native code, ProGuard removes them as unused; the
# native loader then crashes with `NoSuchMethodError: ... name or signature does
# not match`. Keep the whole driver package — it's small and entirely needed at
# runtime once the bundled SQLite driver is selected.
-keep class androidx.sqlite.driver.bundled.** { *; }
-keepclassmembers class androidx.sqlite.driver.bundled.** { native <methods>; *; }
# ---- Ktor serialization extension providers (ServiceLoader) -----------------
# io.ktor.serialization.kotlinx-json discovers KotlinxSerializationJsonExtensionProvider
# via META-INF/services/io.ktor.serialization.kotlinx.KotlinxSerializationExtensionProvider.
# Without this keep the desktop HttpClient init throws ServiceConfigurationError
# at first request; on Windows jpackage's launcher swallows the trace and
# surfaces it as "Failed to launch JVM".
-keep class * implements io.ktor.serialization.kotlinx.KotlinxSerializationExtensionProvider { *; }
# ---- Vico 3.2.0-next.1 ColorScale (CMP API drift) ---------------------------
# Vico's new ColorScale* classes (ColorScaleShader, ColorScaleAreaFill,
# ColorScaleLineFill) reference CMP UI graphics members that don't exist in
# compose-multiplatform 1.11.0-beta03 (LinearGradientShader-VjE6UOU$default
# on ShaderKt and Paint.setShader(org.jetbrains.skia.Shader)). We don't use
# the ColorScale APIs in this app, so suppress these warnings to let ProGuard
# proceed; otherwise it aborts with "unresolved program class members".
# Remove once Vico ships a release built against CMP 1.11 stable.
# Vico's ColorScale* classes call into skia-shader bridges that aren't on the
# desktop ProGuard classpath. Vico ships no consumer rules.
-dontwarn com.patrykandpatrick.vico.compose.cartesian.ColorScaleShader
-dontwarn com.patrykandpatrick.vico.compose.cartesian.layer.ColorScaleAreaFill
-dontwarn com.patrykandpatrick.vico.compose.cartesian.layer.ColorScaleLineFill
# ---- Kotlin 2.3+ stdlib intrinsics not present on JDK 17 --------------------
-dontwarn kotlin.concurrent.atomics.**
-dontwarn kotlin.uuid.UuidV7Generator

View File

@@ -44,9 +44,9 @@ compose-multiplatform-material3 = "1.11.0-alpha07"
# AndroidCompose.kt's resolutionStrategy force-aligns these groups to *this* version
# at resolution time, so it is the source of truth for the Android target.
androidx-compose-bom-aligned = "1.11.0"
# `androidx-compose-material` (M2) is independent of CMP and pinned separately
# because some third-party libs (maps-compose-widgets, datadog) drag in
# unversioned material transitives.
# `androidx-compose-material` (M2) is independent of CMP. Pinned because
# maps-compose-widgets requests `androidx.compose.material:material` without
# a version (relying on a BOM that we exclude). M2 is frozen at 1.7.8.
androidx-compose-material = "1.7.8"
jetbrains-adaptive = "1.3.0-alpha07"