JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.32k stars 1.18k forks source link

Default Proguard rules cause release task failure when using Material 3 instead of 2 in Compose Desktop #4883

Closed EchoEllet closed 2 months ago

EchoEllet commented 6 months ago

Describe the bug Starting from 1.2, Compose Desktop Plugin supports Proguard without any additional rules to get it working, but if you use Material 3 by usingimplementation(compose.material3) then it will cause failure with all the tasks that use Proguard likecreateReleaseDistributable

Affected platforms

Versions

To Reproduce Steps to reproduce the behavior:

  1. Create a new compose desktop project, either by using https://kmp.jetbrains.com/ or the Compose Desktop template, make sure to use Kotlin 2.0.0 and Compose 1.6.10
  2. Make sure to use a JDK version that's supported by JPackage (17 or above) and if you're on macOS, use Amazon Corretto or a similar distribution. Try to package the application using ./gradlew createReleaseDistributable and see if there is an error, I didn't get any in my case
  3. Go to build.gradle.kts of the desktop source set in thecomposeApp Gradle module (if you used the kmp.jetbrains.com) and replace implementation(compose.material) with implementation(compose.material3), remove any usages to Material module (Material 2) and use anything from Material 3 module
    
    // composeApp/desktopMain/kotlin/App.kt

import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable

@Composable fun App() { Button(onClick = {}) { Text(text = "Hi") } }

5. Try to run any task that uses Proguard like `createReleaseDistributable` and see if there is any error

**Expected behavior**
The bundle size remains quite large, even with Java installed on the machine, which is why I use Proguard. It would be nice to have Proguard support with Material 3.

**Screenshots**
![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/e7ab212d-37ff-484a-8e31-1d8cc07b7c54)

> Notice that if you have Java 21 (or any version higher than Java 17) installed and configured to be used in the system path, you should make sure to use Java 17 in both Project settings and Project Gradle settings and run the tasks from IDE and not in the command line as it will cause the `java` command from your system which will use Java 21
> See #4125 and #3818 for more details

**Additional context**

```console

> Task :composeApp:proguardReleaseJars
ProGuard, version 7.2.2
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]

Note: androidx.lifecycle.viewmodel.internal.JvmViewModelProviders accesses a declared constructor '<init>()' dynamically
Warning: there were 884 unresolved references to classes or interfaces.
         You may need to add missing library jars or update their versions.
         If your code works fine without the missing classes, you can suppress
         the warnings with '-dontwarn' options.
         (https://www.guardsquare.com/proguard/manual/troubleshooting#unresolvedclass)
Unexpected error
java.io.IOException: Please correct the above warnings first.
        at proguard.Initializer.execute(Initializer.java:544) ~[proguard-base-7.2.2.jar:7.2.2]
        at proguard.pass.PassRunner.run(PassRunner.java:24) ~[proguard-base-7.2.2.jar:7.2.2]
        at proguard.ProGuard.initialize(ProGuard.java:328) ~[proguard-base-7.2.2.jar:7.2.2]
        at proguard.ProGuard.execute(ProGuard.java:130) ~[proguard-base-7.2.2.jar:7.2.2]
        at proguard.ProGuard.main(ProGuard.java:623) [proguard-base-7.2.2.jar:7.2.2]

> Task :composeApp:proguardReleaseJars FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':composeApp:proguardReleaseJars'.
> External tool execution failed:
  * Command: [/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home/bin/java, -cp, "/Users/ellet/.gradle/caches/modules-2/files-2.1/com.guardsquare/proguard-gradle/7.2.2/3ee8f512db2bee70d3eca9be10986d96404dc2f0/proguard-gradle-7.2.2.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/com.github.zafarkhaja/java-semver/0.9.0/59a83ca73c72a5e25b3f0b1bb305230a11000329/java-semver-0.9.0.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/com.guardsquare/proguard-base/7.2.2/5c67dd9184bfb7d01b082b45bf3a6599b43d5c25/proguard-base-7.2.2.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.6.10/e80fe6ac3c3573a80305f5ec43f86b829e8ab53d/kotlin-stdlib-jdk8-1.6.10.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/f645ed69d595b24d4cf8b3fbb64cc505bede8829/gson-2.8.5.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/com.guardsquare/proguard-core/9.0.1/b1c26f3a6fc947a1bdfa67fd8c96230df0471156/proguard-core-9.0.1.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-core/2.17.1/779f60f3844dadc3ef597976fcb1e5127b1f343d/log4j-core-2.17.1.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.17.1/d771af8e336e372fb5399c99edabe0919aeaf5b2/log4j-api-2.17.1.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.json/json/20211205/47032dcf2f69880f07dab3dc60b4b0ad97318308/json-20211205.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.6.10/e1c380673654a089c4f0c9f83d0ddfdc1efdb498/kotlin-stdlib-jdk7-1.6.10.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-metadata-jvm/0.4.1/834f5bdb32c504ee72fa50750ad16ab1679c19e6/kotlinx-metadata-jvm-0.4.1.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.6.10/b8af3fe6f1ca88526914929add63cf5e7c5049af/kotlin-stdlib-1.6.10.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.6.10/c118700e3a33c8a0d9adc920e9dec0831171925/kotlin-stdlib-common-1.6.10.jar:/Users/ellet/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar", proguard.ProGuard, -include, "/Users/ellet/Downloads/KotlinProject/composeApp/build/compose/tmp/proguardReleaseJars/root-config.pro"]
  * Working dir: []
  * Exit code: 1
  * Standard output log: /Users/ellet/Downloads/KotlinProject/composeApp/build/compose/logs/proguardReleaseJars/java-2024-05-28-05-06-55-out.txt
  * Error log: /Users/ellet/Downloads/KotlinProject/composeApp/build/compose/logs/proguardReleaseJars/java-2024-05-28-05-06-55-err.txt

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.7/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 6s
12 actionable tasks: 7 executed, 5 up-to-date

The Proguard version that's used in the Compose desktop Gradle plugin is 7.2.2 and the one that supports Kotlin 2.0.0 and versions of Java 21 or higher is 7.5.0 More details: https://github.com/Guardsquare/proguard/issues/376 and https://github.com/Guardsquare/proguard/issues/387

I was able to run the example application using 7.2.2 with no issues

image

With Material 2 (the module name is called Material)

The file KotlinProject/composeApp/build/compose/logs/proguardReleaseJars/java-2024-05-28-05-06-55-err.txt is empty, the KotlinProject/composeApp/build/compose/logs/proguardReleaseJars/java-2024-05-28-05-06-55-out.txt was mostly warnings (about 940 lines), the end of the file:

Note: androidx.lifecycle.viewmodel.internal.JvmViewModelProviders accesses a declared constructor '<init>()' dynamically
Warning: there were 884 unresolved references to classes or interfaces.
         You may need to add missing library jars or update their versions.
         If your code works fine without the missing classes, you can suppress
         the warnings with '-dontwarn' options.
         (https://www.guardsquare.com/proguard/manual/troubleshooting#unresolvedclass)
Unexpected error
java.io.IOException: Please correct the above warnings first.
    at proguard.Initializer.execute(Initializer.java:544) ~[proguard-base-7.2.2.jar:7.2.2]
    at proguard.pass.PassRunner.run(PassRunner.java:24) ~[proguard-base-7.2.2.jar:7.2.2]
    at proguard.ProGuard.initialize(ProGuard.java:328) ~[proguard-base-7.2.2.jar:7.2.2]
    at proguard.ProGuard.execute(ProGuard.java:130) ~[proguard-base-7.2.2.jar:7.2.2]
    at proguard.ProGuard.main(ProGuard.java:623) [proguard-base-7.2.2.jar:7.2.2]

https://github.com/JetBrains/compose-multiplatform/blob/7131b5b9a665e8b1d72aabc9476947650145c11e/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt#L15

https://github.com/JetBrains/compose-multiplatform/blob/7131b5b9a665e8b1d72aabc9476947650145c11e/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractUnpackDefaultComposeApplicationResourcesTask.kt#L19

This file might need to be updated: https://github.com/JetBrains/compose-multiplatform/blob/master/gradle-plugins/compose/src/main/resources/default-compose-desktop-rules.pro

Disabling Proguard optimization will give you the exact same issue:


buildTypes.release.proguard {
            optimize.set(false)
        }

Related Issues

EchoEllet commented 5 months ago

This comment provide a better workaround.

Old Workaround

I managed to get it working with a workaround that's far from perfect and shouldn't be used in real production app 1. First of all, try to reproduce the bug mentioned above 2. Exclude Material module (Material 2) as mentioned in this https://github.com/JetBrains/compose-multiplatform/issues/497#issuecomment-1472218306 in [#497](https://github.com/JetBrains/compose-multiplatform/issues/497) from all compose libraries: ```kotlin implementation(compose.foundation) { exclude("org.jetbrains.compose.material") } implementation(compose.material3) { exclude("org.jetbrains.compose.material") } implementation(compose.ui) { exclude("org.jetbrains.compose.material") } // ... ``` Or: ``` implementation(compose.desktop.currentOs) { exclude("org.jetbrains.compose.material") } ``` 3. Create a new Proguard rules file and use it in the compose desktop module/source set ```kotlin compose.desktop { application { // Your configurations buildTypes.release.proguard { configurationFiles.from(files("compose-desktop.pro")) } } } ``` The name of the file is up to you, in my case, I created this project using (kmp.jetbrains.com), so I have to include this file in `composeApp` module (not inside `desktopMain` source set), the content will be something like ``` # TODO: Workaround to solve https://github.com/JetBrains/compose-multiplatform/issues/4883 -dontwarn androidx.compose.material.** -keep class androidx.compose.material3.** { *; } -ignorewarnings ``` This will keep all classes from Material 2 and ignore all the warnings from Material 2 and other warnings 4. Try to launch the release version of the app to use Proguard, a task like `runReleaseDistributable`, make sure to run it using your IDE as using `./gradlew runReleaseDistributable` might use Java version 21 or above if installed and configured in the system path, the app will compile but you will get a runtime error if you use anything from Material 3 that depends on Material 2 which might be another issue, the code snippet: ```kotlin import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable @Composable fun App() { Button(onClick = {}) { Text(text = "Hi") } } ``` 5. You should get the following error when launching the release version of the app: ![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/ed136f63-d9b2-432a-bf44-562ec8af627e) This can be easily fixed by not excluding Material 2 from Material 3: ```kotlin implementation(compose.material3) ``` or exclude Material 2 completely if used from Material 3 and only include the needed modules: ```kotlin implementation(compose.material3) { // Explicitly exclude Material 2 completely from Material 3 in case Material 3 or any of its modules depends on the Material 2 module or any of its modules // At the moment, it seems to be only depending on material-ripple and material-icons-core modules exclude("org.jetbrains.compose.material") } // TODO: A workaround to include the needed modules after excluding the Material 2 module from Material 3 (see https://github.com/JetBrains/compose-multiplatform/issues/4883#issuecomment-2142445581) for more details implementation("org.jetbrains.compose.material:material-ripple:${libs.versions.compose.get()}") implementation("org.jetbrains.compose.material:material-icons-core:${libs.versions.compose.get()}") ``` The app works now ![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/9ab1aa95-c2eb-4a0b-8871-73d3aeccfc6b) Noticed that the default Kotlin icon included in Compose desktop is not included and you will get this instead ![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/32006b2d-c9f9-4252-8232-dbdc670bc202) I didn't investigate further about this Another unrelated issue is that Material 3 depends on some Material 2 modules like the ripple effect and icons core https://github.com/JetBrains/compose-multiplatform-core/blob/1694668f0d423babe94805bba603e087bd5ca943/compose/material3/material3/build.gradle#L104 This issue in https://github.com/JetBrains/compose-multiplatform-core which is not in this repository **Additional details**: The binary bundle size that's created with `packageReleaseDmg` (requires Java installed) after the workaround to use Material 3 with Proguard in the release version of the app ![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/a08207dc-2547-4c28-8340-e07082acc0c1) Before the workaround (Use Material 2) ![image](https://github.com/JetBrains/compose-multiplatform/assets/73608287/ee5c1c4d-12db-4210-9eb6-a3e05edd3698) The bundle size is around 6MB larger when using Material 3, I only used a `Button` and `Text` from Material 2 and Material 3, the size might not get much bigger when using more components from Material 3, this is another thing to look at but I'm not sure as I haven't looked into the source code in: https://github.com/JetBrains/compose-multiplatform-core

See the comment bellow for a better workaround

EchoEllet commented 5 months ago

A better workaround, to use Material 3 (excluding Material 2), Kotlin 2.0.0, Proguard 7.5.0, and Compose 1.6.10:

  1. Exclude Material 2 and include kotlinx-serialization-core:
implementation(compose.material3)
implementation(compose.desktop.currentOs) {
   exclude("org.jetbrains.compose.material")
}
// Explicitly include this is required to fix Proguard warnings coming from Kotlinx.DateTime
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0")
  1. Update Compose Desktop Proguard settings to use Proguard 7.5.0 and include rules from a file:
compose.desktop {
    application {
        buildTypes.release.proguard {
            version.set("7.5.0")
            configurationFiles.from("proguard.pro")
        }
    }
}
  1. Create the file proguard.pro if doesn't exist, use the following rules:
-keep class androidx.compose.runtime.** { *; }
-keep class androidx.collection.** { *; }
-keep class androidx.lifecycle.** { *; }
-keep class androidx.compose.ui.text.platform.ReflectionUtil { *; }

# We're excluding Material 2 from the project as we're using Material 3
-dontwarn androidx.compose.material.**

# Kotlinx coroutines rules seems to be outdated with the latest version of Kotlin and Proguard
-keep class kotlinx.coroutines.** { *; }

This doesn't have the icon issue as the previous workaround does.

The Uber JAR on macOS Apple silicon:

image

The native application binary created with packageReleaseDmg

image

Note that there are Proguard notes that need to be solved.

syt0r commented 5 months ago

I'm facing this issue too, is it going to be addressed in the next release? I think it should be mentioned in release notes too

EchoEllet commented 5 months ago

I'm facing this issue too, is it going to be addressed in the next release? I think it should be mentioned in release notes too

A workaround solution for now is to disable Proguard in the release or fix the issue with a workaround as mentioned above.

The workaround solution doesn't increase the size much. The difference is due to using Material 3

hmy65 commented 3 months ago

I'm using material 3(1.7.0-alpha02), ktor, androidx.navigation, androidx.datastore, coroutine, coil on compose desktop, My proguard is the default 7.2.2. I got no problems with release build. @EchoEllet Full proguard.pro below.

-obfuscationdictionary "dic.txt" -renamesourcefileattribute "dic.txt" -classobfuscationdictionary "dic.txt" -packageobfuscationdictionary "dic.txt"

-allowaccessmodification # for some reason, app crashed

-mergeinterfacesaggressively -overloadaggressively -repackageclasses

-keep class androidx.datastore.preferences. { *; } -keep class io.ktor.* { ; } -keep class coil3. { *; } -keep class ui.navigation.* { ; }

for desktop TextField

-keepclasseswithmembernames class androidx.compose.foundation.text.* { ; }

Kotlin & java

-assumenosideeffects class kotlin.jvm.internal.Intrinsics { public static void check(...); public static void throw(...); } -assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt { private static getDebugMetadataAnnotation(...); } -assumenosideeffects class java.util.Objects { public static requireNonNull(...); }

####################################################################################################

slf4j

-assumenosideeffects interface org.slf4j.Logger { public void trace(...); public void debug(...); public void info(...); public void warn(...); public void error(...);

public boolean isTraceEnabled(...);
public boolean isDebugEnabled(...);
public boolean isWarnEnabled(...);

}

-assumenosideeffects class org.slf4j.LoggerFactory { public static ** getLogger(...); }

-dontwarn org.slf4j.**

####################################################################################################

kotlinx.coroutines

https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro

ServiceLoader support

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

Most of volatile fields are updated with AFU and should not be mangled

-keepclassmembers class kotlinx.coroutines.** { volatile ; }

Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater

-keepclassmembers class kotlin.coroutines.SafeContinuation { volatile ; }

These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when

kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.

-dontwarn java.lang.instrument.ClassFileTransformer -dontwarn sun.misc.SignalHandler -dontwarn java.lang.instrument.Instrumentation -dontwarn sun.misc.Signal

Only used in kotlinx.coroutines.internal.ExceptionsConstructor.

The case when it is not available is hidden in a try-catch, as well as a check for Android.

-dontwarn java.lang.ClassValue

An annotation used for build tooling, won't be directly accessed.

-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

https://github.com/Kotlin/kotlinx.coroutines/issues/4025

-keep class kotlinx.coroutines.internal.MainDispatcherFactory { ; } -keep class kotlinx.coroutines.swing.SwingDispatcherFactory { ; }

####################################################################################################

kotlinx.serialization

https://github.com/Kotlin/kotlinx.serialization/blob/master/rules/common.pro

Keep Companion object fields of serializable classes.

This avoids serializer lookup through getDeclaredClasses as done for named companion objects.

-if @kotlinx.serialization.Serializable class ** -keepclassmembers class <1> { static <1>$Companion Companion; }

Keep serializer() on companion objects (both default and named) of serializable classes.

-if @kotlinx.serialization.Serializable class { static $ ; } -keepclassmembers class <2>$<3> { kotlinx.serialization.KSerializer serializer(...); }

Keep INSTANCE.serializer() of serializable objects.

-if @kotlinx.serialization.Serializable class { public static INSTANCE; } -keepclassmembers class <1> { public static <1> INSTANCE; kotlinx.serialization.KSerializer serializer(...); }

@Serializable and @Polymorphic are used at runtime for polymorphic serialization.

-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes

See also https://github.com/Kotlin/kotlinx.serialization/issues/1900

-dontnote kotlinx.serialization.**

Serialization core uses java.lang.ClassValue for caching inside these specified classes.

If there is no java.lang.ClassValue (for example, in Android), then R8/ProGuard will print a warning.

However, since in this case they will not be used, we can disable these warnings

-dontwarn kotlinx.serialization.internal.ClassValueReferences

okushnikov commented 3 months ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

kodeplateform commented 2 months ago

Thanks to the workaround , Ive been able to compile but i still have this crash error on,1.7.0-alpha03 from MaterialTheme comosable on MaterialTheme.shapes

: Exception in thread "main" java.lang.ClassFormatError: Invalid index 2 in LocalVariableTable in class file androidx/compose/material3/ShapesKt at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(Unknown Source) at java.base/java.security.SecureClassLoader.defineClass(Unknown Source) at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown Source) at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Unknown Source) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown Source) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source) at java.base/java.lang.ClassLoader.loadClass(Unknown Source) at androidx.compose.material3.MaterialTheme.getShapes(MaterialTheme.kt:100)