Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

application-kotlin example doesn't support using Kotlin imports #403

Closed
EchoEllet opened this issue May 21, 2024 · 2 comments
Closed

application-kotlin example doesn't support using Kotlin imports #403

EchoEllet opened this issue May 21, 2024 · 2 comments

Comments

@EchoEllet
Copy link
Contributor

EchoEllet commented May 21, 2024

There might be a bug or something outdated in the application-kotlin example, I'm not sure if it's from my side but it is worth the trying

  1. Create a new Kotlin/JVM project (not an Android one)
  2. Make sure you have src/main/kotlin/Main.kt with the following:
import kotlin.random.Random
// import java.util.Random

fun main() = println("Hello World! ${Random.nextInt()}")

it's important to import Random from kotlin and not java.util because all java imports (from java) are already in the JRE, Kotlin imports are not and need to be included in the Jar file, the example doesn't use any Kotlin imports (from kotlin) or any other third party libraries which doesn't show the power or effect of minimizing the jar using Proguard
3. This is why I suggest using Shadow Jar Gradle Plugin since it's the easiest way to include all the dependencies and it's the most used and commonly known, by default Shadow includes all the dependencies without minimizing the jar, which create 1.7MB for a Kotlin Hello world program even if it doesn't use any imports as it will use Kotlin standard library by default
4. Configure proguard task to take the Jar from Shadow Plugin and then reduce it, create proguard.pro in the root project folder with the rules from (https://github.com/Guardsquare/proguard/blob/master/examples/application-kotlin/proguard.pro)
5. Now try to run proguard task using ./gradlew proguard, you will probably get

> Task :proguard FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':proguard'.
> java.io.IOException: Please correct the above warnings first.

with no warnings, using ./gradlew shadowJar --info will output too much info, see this link for the complete output to view it quickly

The complete build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.23"
    id("com.github.johnrengelman.shadow") version "8.1.1"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test"))
//    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
//    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}

kotlin {
    jvmToolchain(11)
}

application {
    mainClass = "MainKt"
}

// Disable the normal jar task as we will use the one from Shadow plugin
tasks.jar { enabled = false }

tasks.shadowJar {
    archiveFileName.set("application.jar")
    // minimize()
}

buildscript {
    repositories { mavenCentral() }
    dependencies {
        classpath("com.guardsquare:proguard-gradle:7.4.2") {
            exclude("com.android.tools.build") // Optional, not needed in newer versions of Proguard
        }
    }
}

tasks.register<proguard.gradle.ProGuardTask>("proguard") {
    configuration(file("proguard.pro"))
    injars(tasks.shadowJar.flatMap { it.archiveFile })
    outjars(
        layout.buildDirectory.file("libs/${tasks.shadowJar.get().archiveFile.get().asFile.nameWithoutExtension}-minified.jar"),
    )

    // Automatically handle the Java version of this build.
    if (System.getProperty("java.version").startsWith("1.")) {
        // Before Java 9, the runtime classes were packaged in a single jar file.
        libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
    } else {
        // As of Java 9, the runtime classes are packaged in modular jmod files.
//        libraryjars("${System.getProperty("java.home")}/jmods/java.base.jmod", jarfilter: "!**.jar", filter: "!module-info.class")
        libraryjars("${System.getProperty("java.home")}/jmods/.....")
    }

    // This will include the Kotlin library jars
    libraryjars(sourceSets.main.get().compileClasspath)

    verbose()
}

The proguard.pro file:

-verbose

-keepattributes *Annotation*

-keep class kotlin.Metadata { *; }

# Entry point to the app.
-keep class MainKt { *; }

The failure mentioned above can be solved by adding -ignorewarnings to the proguard.pro as a workaround which will solve the problem and still reduce the jar size with no issues (java -jar build/libs/application-minified.jar will output something like:
Hello World! -391866888)

But when including any library like kotlinx-coroutines-core or okhttp and start using them or use something like suspend function keyword (using -keep class kotlin.Metadata doesn't fix the task failure) you will get another error

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':proguard'.
> 
  
  It appears you are missing some classes resulting in an incomplete class hierarchy, 
  please refer to the troubleshooting page in the manual: 
  https://www.guardsquare.com/en/products/proguard/manual/troubleshooting#superclass

Adding the rules of kotlinx-coroutines-core library from (https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro) doesn't solve the issue

I tried using Java JDK 11, 17, and even 21, macOS Homebrew, and Corretto, the latest version of Proguard (7.4.2)

This issue might be in newer versions of Kotlin, Proguard or the example might be outdated, while this issue might be out of scope, I still think the example might need to be updated to support using the Kotlin imports, and maybe an example of using at least one popular Kotlin or java library in the example to showcase Proguard

even if the should should not use any third party libraries or even the ones from Kotlin, still using suspend or any Kotlin imports is not supported in the example and will cause build failure as mentioned above

Thank you.

@EchoEllet EchoEllet changed the title application-kotlin example might be outdated application-kotlin example doesn't support using Kotlin imports May 21, 2024
@mrjameshamilton
Copy link
Collaborator

Hi @ellet0 !

The kotlin application example is set up to be processed as a "thin" jar. If you run ./gradlew proguard --info, you'll see in the log that the Kotlin standard library jars are passed to ProGuard as library jars:

...
Reading program jar [/home/james/Projects/proguard/examples/application-kotlin/build/libs/application-kotlin-1.0-SNAPSHOT.jar] (filtered)
Reading library jmod [/home/james/.sdkman/candidates/java/17-open/jmods/java.base.jmod] (filtered)
Reading library jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.8.0/ed04f49e186a116753ad70d34f0ac2925d1d8020/kotlin-stdlib-jdk8-1.8.0.jar] (filtered)
Reading library jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.0/3c91271347f678c239607abb676d4032a7898427/kotlin-stdlib-jdk7-1.8.0.jar] (filtered)
Reading library jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.8.0/1796921c7a3e2e2665a83e6c8d33399336cd39bc/kotlin-stdlib-1.8.0.jar] (filtered)
Reading library jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.8.0/f7197e7cc76453ac59f8b0f8d5137cc600becd36/kotlin-stdlib-common-1.8.0.jar] (filtered)
...

This is done with the line here:

libraryjars sourceSets.main.compileClasspath

This means that the standard library classes will be missing from the processed jar indeed; but you can run the jar with the kotlin command instead of java, since the standard library classes will be added:

$ java -cp build/libs/application-kotlin-1.0-SNAPSHOT-minified.jar com.example.AppKt 
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
	at com.example.AppKt.main(Unknown Source)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
	... 1 more
$ kotlin -cp build/libs/application-kotlin-1.0-SNAPSHOT-minified.jar com.example.AppKt 
Hello World!
Program arguments: 

If you instead want to create a "fat" jar including the standard library, instead of using the shadowjar technique, you can change the libraryjars to injars on the previously mentioned line:

    // This will include the Kotlin library jars
    injars sourceSets.main.compileClasspath

You'll then notice that the standard library jars are read as program jars, rather than library jars:

Reading program jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.8.0/ed04f49e186a116753ad70d34f0ac2925d1d8020/kotlin-stdlib-jdk8-1.8.0.jar] (filtered)
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Reading program jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.0/3c91271347f678c239607abb676d4032a7898427/kotlin-stdlib-jdk7-1.8.0.jar] (filtered)
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Reading program jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.8.0/1796921c7a3e2e2665a83e6c8d33399336cd39bc/kotlin-stdlib-1.8.0.jar] (filtered)
Note: duplicate definition of resource file [META-INF/MANIFEST.MF]
Reading program jar [/home/james/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.8.0/f7197e7cc76453ac59f8b0f8d5137cc600becd36/kotlin-stdlib-common-1.8.0.jar] (filtered)

And then you'll get a "fat" jar in the end, which contains the Kotlin classes; so you can run the jar with java:

$ java -cp build/libs/application-kotlin-1.0-SNAPSHOT-minified.jar com.example.AppKt 
Hello World!
Program arguments: 

@EchoEllet
Copy link
Contributor Author

I would like to thank you for the detailed answer, while this solution might include the Kotlin standard library it still doesn't include the libraries, usually the jar will become too large after using libraries, is there a way to achieve that? I'm sure there are but still haven't figured that out, maybe at least an example with a popular library like Kotlinx Coroutines (it already has Proguard rules) but I always get errors as mentioned in the issue, and I still get warnings issue with no warnings and the only fix is to use -ignorewarnings because ./gradlew proguard -i include too many warnings and I didn't understand where exactly the issue is, consider enabling GitHub discussions as this would allow the developers to ask directly in GitHub without having to create an account in Proguard community.

I'm interested in knowing more about using Proguard with libraries in Kotlin/JVM (non-android project)

Thank you once again!

@EchoEllet EchoEllet closed this as not planned Won't fix, can't repro, duplicate, stale May 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants