diff --git a/desktop/README.md b/desktop/README.md index 5e177a548..ea17d0eb7 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -16,10 +16,27 @@ A Compose Desktop application target — the first full non-Android target for t # Run tests ./gradlew :desktop:test -# Package native distribution (DMG/MSI/DEB) +# Package native distribution (DMG/MSI/DEB) — debug (no ProGuard) ./gradlew :desktop:packageDistributionForCurrentOS + +# Package native distribution (DMG/MSI/DEB) — release (ProGuard minified) +./gradlew :desktop:packageReleaseDistributionForCurrentOS ``` +## ProGuard / Minification + +Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. + +**Configuration:** +- `build.gradle.kts` — `buildTypes.release.proguard` block enables ProGuard with `optimize.set(true)` and `obfuscate.set(false)`. +- `proguard-rules.pro` — Comprehensive keep-rules for all reflection/JNI-sensitive dependencies (Koin, kotlinx-serialization, Wire protobuf, Room KMP, Ktor, Kable BLE, Coil, SQLite JNI, Compose Multiplatform resources). + +**Troubleshooting ProGuard issues:** +- If the release build crashes at runtime with `ClassNotFoundException` or `NoSuchMethodError`, a library is loading classes via reflection that ProGuard stripped. Add a `-keep` rule in `proguard-rules.pro`. +- To debug which classes ProGuard removes, temporarily add `-printusage proguard-usage.txt` to the rules file and inspect the output in `desktop/proguard-usage.txt`. +- To see the full mapping of optimizations applied, add `-printseeds proguard-seeds.txt`. +- Run `./gradlew :desktop:runRelease` for a quick smoke-test of the minified app before packaging. + ## Architecture The module depends on the JVM variants of KMP modules: diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index fe4e86c0c..1ffb0f96a 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -45,10 +45,9 @@ compose.desktop { mainClass = "org.meshtastic.desktop.MainKt" buildTypes.release.proguard { - // Note: Enabling ProGuard will reduce final distribution size significantly, - // but will require thorough testing of serialization, reflection (Koin), and JNI (SQLite). - // Recommend enabling when ready: isEnabled.set(true) - isEnabled.set(false) + isEnabled.set(true) + obfuscate.set(false) // Open-source project — obfuscation adds no value + optimize.set(true) configurationFiles.from(project.file("proguard-rules.pro")) } diff --git a/desktop/proguard-rules.pro b/desktop/proguard-rules.pro index 10a50ac4e..a73c347d1 100644 --- a/desktop/proguard-rules.pro +++ b/desktop/proguard-rules.pro @@ -1,14 +1,215 @@ --dontwarn android.os.Parcel** --dontwarn android.os.Parcelable** --dontwarn com.squareup.wire.AndroidMessage** --dontwarn io.ktor.** +# ============================================================================ +# 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)). +# +# Key libraries requiring keep-rules (reflection, JNI, code generation): +# Koin (DI via reflection), kotlinx-serialization (generated serializers), +# Wire protobuf (ADAPTER reflection), Room KMP (generated DB + converters), +# Ktor (Java engine + ServiceLoader), Kable BLE, Coil, Compose Multiplatform +# resources, SQLite bundled (JNI), AboutLibraries. +# ============================================================================ -# Room KMP: preserve generated database constructor (required for R8/ProGuard) --keep class * extends androidx.room.RoomDatabase { (); } +# ---- General ---------------------------------------------------------------- -# Suppress ProGuard notes about duplicate resource files (common in Compose Desktop) +# Preserve line numbers for meaningful stack traces +-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions + +# Suppress notes about duplicate resource files (common in fat JARs) -dontnote ** -# Suppress specific reflection warnings that are safe to ignore +# 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*. +-dontprocesskotlinmetadata + +# ---- Entry point ------------------------------------------------------------ + +-keep class org.meshtastic.desktop.MainKt { *; } + +# ---- Kotlin / Coroutines --------------------------------------------------- + +# Keep Kotlin metadata for reflection-dependent libraries +-keep class kotlin.Metadata { *; } +-keep class kotlin.reflect.** { *; } + +# Coroutines internals +-dontwarn kotlinx.coroutines.** +-keep class kotlinx.coroutines.** { *; } +-keep class kotlin.coroutines.Continuation { *; } + +# ---- Koin DI (reflection-based injection) ----------------------------------- + +# Koin core — uses reflection to instantiate definitions +-keep class org.koin.** { *; } +-dontwarn org.koin.** + +# Keep all Koin-annotated @Module / @ComponentScan classes and their generated +# counterparts so Koin K2 plugin output survives tree-shaking. +-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 * { *; } + +# Generated Koin module extensions (K2 plugin output) +-keep class org.meshtastic.**.di.** { *; } + +# ---- kotlinx-serialization -------------------------------------------------- + +# The serialization plugin generates companion $serializer classes and +# serializer() factory methods that are invoked reflectively. +-keepattributes RuntimeVisibleAnnotations +-keep class kotlinx.serialization.** { *; } +-dontwarn kotlinx.serialization.** + +# Keep @Serializable classes and their generated serializers +-keepclassmembers @kotlinx.serialization.Serializable class ** { + # Companion object that holds the serializer() factory + static ** Companion; + kotlinx.serialization.KSerializer serializer(...); +} +-keepclassmembers class **.$serializer { *; } +-keep class **.$serializer { *; } +-keepclasseswithmembers class ** { + kotlinx.serialization.KSerializer serializer(...); +} + +# ---- Wire protobuf ---------------------------------------------------------- + +# Wire generates ADAPTER companion objects accessed via reflection +-keep class com.squareup.wire.** { *; } +-dontwarn com.squareup.wire.** + +# All generated proto message classes +-keep class org.meshtastic.proto.** { *; } +-keep class meshtastic.** { *; } + +# Suppress warnings about missing Android Parcelable (Wire cross-platform stubs) +-dontwarn android.os.Parcel** +-dontwarn android.os.Parcelable** + +# ---- Room KMP --------------------------------------------------------------- + +# Preserve generated database constructors (required for Room's reflective init) +-keep class * extends androidx.room3.RoomDatabase { (); } +-keep class * implements androidx.room3.RoomDatabaseConstructor { *; } + +# Keep the expect/actual MeshtasticDatabaseConstructor +-keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; } +-keep class org.meshtastic.core.database.MeshtasticDatabase { *; } + +# Room DAOs — Room generates implementations at compile time; keep interfaces +-keep class org.meshtastic.core.database.dao.** { *; } + +# Room Entities — accessed via reflection for column mapping +-keep class org.meshtastic.core.database.entity.** { *; } + +# Room TypeConverters — invoked reflectively +-keep class org.meshtastic.core.database.Converters { *; } + +# Room generated _Impl classes +-keep class **_Impl { *; } + +# ---- SQLite bundled (JNI) --------------------------------------------------- + +-keep class androidx.sqlite.** { *; } +-dontwarn androidx.sqlite.** + +# ---- Ktor (Java engine + ServiceLoader + content negotiation) --------------- + +# Ktor uses ServiceLoader and reflection for engine/plugin discovery +-keep class io.ktor.** { *; } +-dontwarn io.ktor.** + +# Keep ServiceLoader metadata files +-keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; } + +# Java HTTP client engine +-keep class io.ktor.client.engine.java.** { *; } + +# ---- Coil (image loading) --------------------------------------------------- + +-keep class coil3.** { *; } +-dontwarn coil3.** + +# ---- Kable BLE -------------------------------------------------------------- + +-keep class com.juul.kable.** { *; } +-dontwarn com.juul.kable.** + +# ---- Compose Multiplatform resources ---------------------------------------- + +# Generated resource accessor classes (Res.string.*, Res.drawable.*, etc.) +-keep class org.jetbrains.compose.resources.** { *; } +-keep class org.meshtastic.core.resources.** { *; } + +# ---- AboutLibraries --------------------------------------------------------- + +-keep class com.mikepenz.aboutlibraries.** { *; } +-dontwarn com.mikepenz.aboutlibraries.** + +# ---- Multiplatform Markdown Renderer ---------------------------------------- + +-keep class com.mikepenz.markdown.** { *; } +-dontwarn com.mikepenz.markdown.** + +# ---- QR Code Kotlin --------------------------------------------------------- + +-keep class io.github.g0dkar.qrcode.** { *; } +-dontwarn io.github.g0dkar.qrcode.** +-keep class qrcode.** { *; } +-dontwarn qrcode.** + +# ---- Kermit logging ---------------------------------------------------------- + +-keep class co.touchlab.kermit.** { *; } +-dontwarn co.touchlab.kermit.** + +# ---- Okio ------------------------------------------------------------------- + +-dontwarn okio.** +-keep class okio.** { *; } + +# ---- DataStore -------------------------------------------------------------- + +-keep class androidx.datastore.** { *; } +-dontwarn androidx.datastore.** + +# ---- Paging ----------------------------------------------------------------- + +-keep class androidx.paging.** { *; } +-dontwarn androidx.paging.** + +# ---- Lifecycle / Navigation / ViewModel (JetBrains forks) ------------------- + +-keep class androidx.lifecycle.** { *; } +-keep class androidx.navigation3.** { *; } +-dontwarn androidx.lifecycle.** +-dontwarn androidx.navigation3.** + +# ---- Meshtastic application code -------------------------------------------- + +# Keep all desktop module classes (thin host shell — not worth tree-shaking) +-keep class org.meshtastic.desktop.** { *; } + +# Core model classes (used in serialization, Room, and Koin injection) +-keep class org.meshtastic.core.model.** { *; } + +# ---- JVM runtime suppression ------------------------------------------------ + -dontwarn java.lang.reflect.** --dontwarn sun.misc.Unsafe \ No newline at end of file +-dontwarn sun.misc.Unsafe +-dontwarn java.lang.invoke.** + +# ---- jSerialComm (cross-platform serial library with Android stubs) --------- + +-dontwarn com.fazecast.jSerialComm.android.** + +# ---- Kotlin stdlib atomics (Kotlin 2.3+ intrinsics, not on JDK 17) ---------- + +-dontwarn kotlin.concurrent.atomics.** +-dontwarn kotlin.uuid.UuidV7Generator