diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 564cbae..99422a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,8 +8,9 @@ kobweb-libs = "0.17.1" kotter = "1.1.2" kotlin = "1.8.10" kotlinx-coroutines = "1.6.0" -okhttp = "4.10.0" +okhttp = "4.12.0" shadow = "7.0.0" +proguard = "7.5.0" [libraries] clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } @@ -25,3 +26,4 @@ jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +proguard = { id = "com.guardsquare:proguard-gradle", version.ref = "proguard" } diff --git a/kobweb/build.gradle.kts b/kobweb/build.gradle.kts index 7cfcaa2..ddf2dc5 100644 --- a/kobweb/build.gradle.kts +++ b/kobweb/build.gradle.kts @@ -1,4 +1,7 @@ import org.jreleaser.model.Active +import java.io.FileNotFoundException +import java.nio.file.Paths +import java.util.jar.JarFile plugins { kotlin("jvm") @@ -71,6 +74,112 @@ tasks.jar { archiveFileName.set("kobweb-cli.jar") } +// Proguard for minimizing the bundle size +buildscript { + repositories { mavenCentral() } + dependencies { + classpath(libs.plugins.proguard.get().toString()) { + // On older versions of proguard, Android build tools will be included + exclude("com.android.tools.build") + } + } +} + +tasks.register("proguard") { + dependsOn(tasks.shadowJar) + + val originalJarFile = tasks.shadowJar.flatMap { it.archiveFile } + val originalJarFileNameWithoutExtension = originalJarFile.get().asFile.nameWithoutExtension + val originalJarFileDestination = tasks.shadowJar.flatMap { it.destinationDirectory } + + val minimizedJarFile = originalJarFileDestination.get().file("$originalJarFileNameWithoutExtension-min.jar") + + injars(originalJarFile) + outjars(minimizedJarFile) + + val javaHome = System.getProperty("java.home") + + // Starting from Java 9, runtime classes are packaged in modular JMOD files. + fun includeModuleFromJdk(jModFileNameWithoutExtension: String) { + val jModFilePath = Paths.get(javaHome, "jmods", "$jModFileNameWithoutExtension.jmod").toString() + val jModFile = File(jModFilePath) + if (!jModFile.exists()) { + throw FileNotFoundException("The '$jModFileNameWithoutExtension' at '$jModFilePath' doesn't exist.") + } + libraryjars( + mapOf("jarfilter" to "!**.jar", "filter" to "!module-info.class"), + jModFilePath, + ) + } + + val javaModules = + listOf( + "java.base", + // Needed to support Java Swing/Desktop (Required by Kotter Virtual Terminal) + "java.desktop", + // Java Data Transfer is required by Kotter Virtual Terminal + "java.datatransfer", + // Needed to support Java logging utils (required by Okio) + "java.logging", + // Java RMI is required by freemarker and some other dependencies + "java.rmi", + // Java XML is required by freemarker and some other dependencies + "java.xml", + // Java SQL is required by freemarker and some other dependencies + "java.sql", + ) + javaModules.forEach { includeModuleFromJdk(jModFileNameWithoutExtension = it) } + + // Includes the main source set's compile classpath for Proguard. + // Notice that Shadow JAR already includes Kotlin standard library and dependencies, yet this + // is essential for resolving Kotlin and other library warnings without using '-dontwarn kotlin.**' + injars(sourceSets.main.get().compileClasspath) + + printmapping(originalJarFileDestination.get().file("$originalJarFileNameWithoutExtension.map")) + + // Disabling obfuscation makes the JAR file size a bit bigger and makes the debugging process a bit less easy + dontobfuscate() + // Kotlinx serialization breaks when using optimizations + dontoptimize() + + configuration(file("proguard.pro")) + + // Use Proguard rules that provided by dependencies in JAR file + doFirst { + JarFile(originalJarFile.get().asFile).use { jarFile -> + val generatedRulesFiles = + jarFile.entries().asSequence() + .filter { it.name.startsWith("META-INF/proguard") && !it.isDirectory } + .map { entry -> + jarFile.getInputStream(entry).bufferedReader().use { reader -> + Pair(reader.readText(), entry) + } + } + .toList() + + val buildProguardDirectory = layout.buildDirectory.dir("proguard").get().asFile + if (!buildProguardDirectory.exists()) { + buildProguardDirectory.mkdir() + } + generatedRulesFiles.forEach { (rulesContent, rulesFileEntry) -> + val rulesFileNameWithExtension = rulesFileEntry.name.substringAfterLast("/") + val generatedProguardFile = File(buildProguardDirectory, "generated-$rulesFileNameWithExtension") + if (!generatedProguardFile.exists()) { + generatedProguardFile.createNewFile() + } + generatedProguardFile.bufferedWriter().use { bufferedWriter -> + bufferedWriter.appendLine("# Generated file from ($rulesFileEntry) - manual changes will be overwritten") + bufferedWriter.appendLine() + + bufferedWriter.appendLine(rulesContent) + } + + configuration(generatedProguardFile) + } + } + } +} + // These values are specified in ~/.gradle/gradle.properties; otherwise sorry, no jreleasing for you :P val (githubUsername, githubToken) = listOf("varabyte.github.username", "varabyte.github.token") .map { key -> findProperty(key) as? String } diff --git a/kobweb/proguard.pro b/kobweb/proguard.pro new file mode 100644 index 0000000..c8798fe --- /dev/null +++ b/kobweb/proguard.pro @@ -0,0 +1,40 @@ +# This file contain the rules that's either specific to the project or custom rules that's not from the dependencies +# and it doesn't contain the required configurations as it's already configured by Gradle task + +# Proguard Kotlin Example https://github.com/Guardsquare/proguard/blob/master/examples/application-kotlin/proguard.pro + +-keepattributes *Annotation* + +# Entry point to the app. +-keep class MainKt { *; } + +# Ignore all the warnings from Kotlinx Coroutines +-dontwarn kotlinx.coroutines.** + +# Leave Jansi deps in place, or else Windows won't work +-keep class org.fusesource.jansi.** { *; } +-keep class org.jline.jline-terminal-jansi.** { *; } + +# Exclude SLF4J from minimization +-keep class org.slf4j.** { *; } +-dontwarn org.slf4j.** + +# Suppress warnings coming from Gradle API +-dontwarn org.gradle.** + +# Suppress and fix Freemarker warnings +-dontwarn freemarker.ext.** +-dontwarn freemarker.template.utility.JythonRuntime +-keep class freemarker.template.utility.JythonRuntime + +# Suppress warnings about missing classes from 'org.python package' +# Freemarker relies on Jython, and some Jython classes may not be recognized. +-dontwarn org.python.** + +# Suppress warnings from Javax Servlet as Freemarker depends on it and is already part of the JAR +-dontwarn javax.servlet.** + +# Suppress warnings from Apache Logging as Freemarker depends on it +-dontwarn org.apache.log4j.** +-dontwarn org.apache.commons.logging.** +-dontwarn org.apache.log.** \ No newline at end of file