mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-14 10:58:59 -04:00
fix(build): overhaul R8 rules and DRY up build-logic conventions (#5109)
This commit is contained in:
@@ -61,6 +61,10 @@ When reviewing code, meticulously verify the following categories. Flag any devi
|
||||
- [ ] **Libraries:** Verify usage of `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking.
|
||||
- [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues.
|
||||
|
||||
### 8. ProGuard / R8 Rules
|
||||
- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `app/proguard-rules.pro` (R8) and `desktop/proguard-rules.pro` (ProGuard). The two files must stay aligned.
|
||||
- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktop:runRelease` succeed.
|
||||
|
||||
## Review Output Guidelines
|
||||
1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern.
|
||||
2. **Reference the Docs:** Cite `AGENTS.md` and project architecture playbooks to justify change requests (e.g., "Per AGENTS.md, `java.io.*` cannot be used in `commonMain`; please migrate to Okio").
|
||||
|
||||
@@ -35,3 +35,7 @@ A step-by-step workflow for implementing a new feature in the Meshtastic-Android
|
||||
```bash
|
||||
./gradlew spotlessCheck detekt assembleDebug test allTests
|
||||
```
|
||||
- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro`, then verify release builds:
|
||||
```bash
|
||||
./gradlew assembleFdroidRelease :desktop:runRelease
|
||||
```
|
||||
|
||||
84
app/proguard-rules.pro
vendored
84
app/proguard-rules.pro
vendored
@@ -1,61 +1,61 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
# ============================================================================
|
||||
# Meshtastic Android — ProGuard / R8 rules for release minification
|
||||
# ============================================================================
|
||||
# Open-source project: obfuscation is disabled. We rely on tree-shaking and
|
||||
# code optimization for APK size reduction.
|
||||
# ============================================================================
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# ---- General ----------------------------------------------------------------
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
# Preserve line numbers for meaningful crash stack traces
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# Open-source — no need to obfuscate
|
||||
-dontobfuscate
|
||||
|
||||
# Room KMP: preserve generated database constructor (required for R8/ProGuard)
|
||||
-keep class * extends androidx.room.RoomDatabase { <init>(); }
|
||||
# ---- Networking (transitive references from Ktor) ---------------------------
|
||||
|
||||
# Needed for protobufs
|
||||
-keep class com.google.protobuf.** { *; }
|
||||
-keep class org.meshtastic.proto.** { *; }
|
||||
|
||||
# Networking
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
||||
# ?
|
||||
-dontwarn java.lang.reflect.**
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
# ---- Wire Protobuf ----------------------------------------------------------
|
||||
|
||||
# Our app is opensource no need to obsfucate
|
||||
-dontobfuscate
|
||||
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
|
||||
# Wire-generated proto message classes (accessed via ADAPTER companion reflection)
|
||||
-keep class org.meshtastic.proto.** { *; }
|
||||
|
||||
# Koin DI: prevent R8 from merging exception classes (observed as io.ktor.http.URLDecodeException
|
||||
# ---- Room KMP (room3) ------------------------------------------------------
|
||||
|
||||
# Preserve generated database constructors (Room uses reflection to instantiate)
|
||||
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
|
||||
|
||||
# ---- Koin DI ----------------------------------------------------------------
|
||||
|
||||
# Prevent R8 from merging exception classes (observed as io.ktor.http.URLDecodeException
|
||||
# replacing Koin's InstanceCreationException in stack traces, making crashes undiagnosable).
|
||||
-keep class org.koin.core.error.** { *; }
|
||||
|
||||
# R8 optimization for Kotlin null checks (AGP 9.0+)
|
||||
-processkotlinnullchecks remove
|
||||
# ---- Compose Multiplatform --------------------------------------------------
|
||||
|
||||
# Compose Multiplatform resources: keep the resource library internals and generated Res
|
||||
# accessor classes so R8 does not tree-shake the resource loading infrastructure.
|
||||
# Without these rules the fdroid flavor (which has fewer transitive Compose dependencies
|
||||
# than google) crashes at startup with a misleading URLDecodeException due to R8
|
||||
# exception-class merging (see Koin keep rule above).
|
||||
# Keep resource library internals and generated Res accessor classes so R8 does
|
||||
# not tree-shake the resource loading infrastructure. Without these rules the
|
||||
# fdroid flavor crashes at startup with a misleading URLDecodeException due to
|
||||
# R8 exception-class merging.
|
||||
-keep class org.jetbrains.compose.resources.** { *; }
|
||||
-keep class org.meshtastic.core.resources.** { *; }
|
||||
|
||||
# Nordic BLE
|
||||
-dontwarn no.nordicsemi.kotlin.ble.environment.android.mock.**
|
||||
-keep class no.nordicsemi.kotlin.ble.environment.android.mock.** { *; }
|
||||
-keep class no.nordicsemi.kotlin.ble.environment.android.compose.** { *; }
|
||||
# Compose Animation: prevent R8 from merging animation spec classes (easing
|
||||
# curves, transition specs, Animatable internals) which can cause animations to
|
||||
# silently snap in release builds.
|
||||
#
|
||||
# -keep prevents class merging (EnterTransition/ExitTransition into *Impl,
|
||||
# VectorizedSpringSpec/TweenSpec elimination, etc.).
|
||||
# allowshrinking lets R8 remove genuinely unreachable classes (e.g.
|
||||
# SharedTransition APIs, RepeatableSpec — unused by this app). Verified via
|
||||
# dex analysis: 278 classes survive in release vs 139 without this rule;
|
||||
# all actively used classes (AnimatedVisibility, Crossfade, SpringSpec,
|
||||
# TweenSpec, EnterTransition, ExitTransition, etc.) are preserved.
|
||||
# allowobfuscation is moot (-dontobfuscate is set above) but explicit for
|
||||
# clarity.
|
||||
# The ** wildcard is recursive and covers animation.core.* sub-packages.
|
||||
-keep,allowshrinking,allowobfuscation class androidx.compose.animation.** { *; }
|
||||
|
||||
@@ -40,14 +40,9 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||
configureKotlinAndroid(this)
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
|
||||
@@ -38,11 +38,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// When flavorless modules depend on flavored modules (like :core:data),
|
||||
|
||||
@@ -44,19 +44,19 @@ internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
|
||||
compileSdk = compileSdkVersion
|
||||
|
||||
defaultConfig.minSdk = minSdkVersion
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
if (this is ApplicationExtension) {
|
||||
defaultConfig.targetSdk = targetSdkVersion
|
||||
}
|
||||
|
||||
val javaVersion = if (project.name in listOf("api", "model", "proto")) {
|
||||
JavaVersion.VERSION_17
|
||||
} else {
|
||||
JavaVersion.VERSION_21
|
||||
}
|
||||
val javaVersion = if (project.name in PUBLISHED_MODULES) JavaVersion.VERSION_17 else JavaVersion.VERSION_21
|
||||
compileOptions.sourceCompatibility = javaVersion
|
||||
compileOptions.targetCompatibility = javaVersion
|
||||
|
||||
testOptions.animationsDisabled = true
|
||||
testOptions.unitTests.isReturnDefaultValues = true
|
||||
|
||||
// Exclude duplicate META-INF license files shipped by JUnit Platform JARs
|
||||
packaging.resources.excludes.addAll(
|
||||
listOf(
|
||||
@@ -190,11 +190,25 @@ internal fun Project.configureKotlinJvm() {
|
||||
configureKotlin<KotlinJvmProjectExtension>()
|
||||
}
|
||||
|
||||
/** Modules published for external consumers — use Java 17 for broader compatibility. */
|
||||
private val PUBLISHED_MODULES = setOf("api", "model", "proto")
|
||||
|
||||
/** Compiler args shared across all Kotlin targets (JVM, Android, iOS, etc.). */
|
||||
private val SHARED_COMPILER_ARGS = listOf(
|
||||
"-opt-in=kotlin.uuid.ExperimentalUuidApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-Xskip-prerelease-check",
|
||||
)
|
||||
|
||||
/** Configure base Kotlin options */
|
||||
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
val isPublishedModule = project.name in PUBLISHED_MODULES
|
||||
|
||||
extensions.configure<T> {
|
||||
val javaVersion = if (project.name in listOf("api", "model", "proto")) 17 else 21
|
||||
val isPublishedModule = project.name in listOf("api", "model", "proto")
|
||||
val javaVersion = if (isPublishedModule) 17 else 21
|
||||
// Using Java 17 for published modules for better compatibility with consumers (e.g. plugins, older environments),
|
||||
// and Java 21 for the rest of the app.
|
||||
jvmToolchain(javaVersion)
|
||||
@@ -208,14 +222,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
if (!isPublishedModule) {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
}
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=kotlin.uuid.ExperimentalUuidApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-Xskip-prerelease-check",
|
||||
)
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
if (isJvmTarget) {
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
}
|
||||
@@ -230,21 +237,13 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
val isPublishedModule = project.name in listOf("api", "model", "proto")
|
||||
jvmTarget.set(if (isPublishedModule) JvmTarget.JVM_17 else JvmTarget.JVM_21)
|
||||
allWarningsAsErrors.set(warningsAsErrors)
|
||||
if (!isPublishedModule) {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
}
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=kotlin.uuid.ExperimentalUuidApi",
|
||||
"-opt-in=kotlin.time.ExperimentalTime",
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xcontext-parameters",
|
||||
"-Xannotation-default-target=param-property",
|
||||
"-Xskip-prerelease-check",
|
||||
"-jvm-default=no-compatibility",
|
||||
)
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,18 @@ A Compose Desktop application target — the first full non-Android target for t
|
||||
|
||||
## 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.
|
||||
Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. Rules are aligned with the Android R8 rules in `app/proguard-rules.pro` — both targets share the same anti-class-merging philosophy.
|
||||
|
||||
**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).
|
||||
- `proguard-rules.pro` — Keep-rules for reflection/JNI-sensitive dependencies (Koin, kotlinx-serialization, Wire protobuf, Room KMP `androidx.room3`, Ktor, Kable BLE, Coil, SQLite JNI, Compose Multiplatform resources) and an anti-merge rule for Compose animation classes.
|
||||
|
||||
**Key rules:**
|
||||
- **Compose animation anti-merge** (`-keep,allowshrinking,allowobfuscation class androidx.compose.animation.** { *; }`) — Prevents ProGuard's optimizer from merging animation class hierarchies (e.g. `EnterTransition`/`ExitTransition` into `*Impl`), which causes animations to silently snap. Same rule as Android.
|
||||
- **Room KMP** — Uses `androidx.room3` package path (Room KMP 3.x).
|
||||
|
||||
**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`.
|
||||
- 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` **and** the corresponding rule in `app/proguard-rules.pro` to keep both targets aligned.
|
||||
- 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.
|
||||
|
||||
8
desktop/proguard-rules.pro
vendored
8
desktop/proguard-rules.pro
vendored
@@ -147,6 +147,14 @@
|
||||
-keep class org.jetbrains.compose.resources.** { *; }
|
||||
-keep class org.meshtastic.core.resources.** { *; }
|
||||
|
||||
# ---- Compose Animation (anti-merge) ----------------------------------------
|
||||
|
||||
# Prevent ProGuard from merging animation spec class hierarchies (same issue
|
||||
# as R8 on Android — EnterTransition/ExitTransition merged into *Impl,
|
||||
# VectorizedSpringSpec/TweenSpec eliminated). allowshrinking lets ProGuard
|
||||
# remove genuinely unreachable classes.
|
||||
-keep,allowshrinking,allowobfuscation class androidx.compose.animation.** { *; }
|
||||
|
||||
# ---- AboutLibraries ---------------------------------------------------------
|
||||
|
||||
-keep class com.mikepenz.aboutlibraries.** { *; }
|
||||
|
||||
@@ -129,27 +129,17 @@ kotlin {
|
||||
|
||||
### Example: Adding Android-specific test config
|
||||
|
||||
**Pattern:** Add to `AndroidLibraryConventionPlugin.kt`:
|
||||
**Pattern:** Test options (`animationsDisabled`, `testInstrumentationRunner`, `unitTests.isReturnDefaultValues`) are centralized in `configureKotlinAndroid()` via `CommonExtension`, so they apply to both app and library modules automatically. To add new test config, update `KotlinAndroid.kt::configureKotlinAndroid()`:
|
||||
|
||||
```kotlin
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
testOptions.apply {
|
||||
animationsDisabled = true
|
||||
// NEW: Android-specific test config
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative:** If it applies to both app and library, consider extracting a function:
|
||||
|
||||
```kotlin
|
||||
internal fun Project.configureAndroidTestOptions() {
|
||||
extensions.configure<CommonExtension> {
|
||||
testOptions.apply {
|
||||
internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
) {
|
||||
commonExtension.apply {
|
||||
testOptions {
|
||||
animationsDisabled = true
|
||||
// Shared test options
|
||||
unitTests.isReturnDefaultValues = true
|
||||
// NEW: Add shared test options here
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +167,8 @@ internal fun Project.configureAndroidTestOptions() {
|
||||
| `AndroidApplicationFlavorsConventionPlugin` ≈ `AndroidLibraryFlavorsConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
|
||||
| `configureKmpTestDependencies()` (7 modules) | **Consolidated** | Large duplication; single source of truth; all KMP modules benefit |
|
||||
| `jvmAndroidMain` hierarchy setup (4 modules) | **Consolidated** | Shared KMP hierarchy pattern; avoids manual `dependsOn(...)` edges and hierarchy warnings |
|
||||
| `PUBLISHED_MODULES` set (4 usages) | **Consolidated** | Was repeated as `listOf(...)` in 4 places; now a single `setOf(...)` constant in `KotlinAndroid.kt` |
|
||||
| `SHARED_COMPILER_ARGS` list (2 code paths) | **Consolidated** | Eliminates duplicated `-opt-in` flags between KMP target compilations and `KotlinCompile` task configuration |
|
||||
|
||||
## Testing Convention Changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user