diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 876c922..c8d8e8a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -5,3 +5,10 @@ plugins { repositories { mavenCentral() } + +dependencies { + implementation(libs.proguard) { + // On older versions of proguard, Android build tools will be included + exclude("com.android.tools.build") + } +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 57b0e8c..5b4cf1f 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -2,3 +2,12 @@ // Project accessors enabled, but root project name not explicitly set for 'buildSrc'. Checking out the project // in different folders will impact the generated code and implicitly the buildscript classpath, breaking caching. rootProject.name = "buildSrc" + +// To use Gradle Version catalogs +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/BuildMinimizedJarTask.kt b/buildSrc/src/main/kotlin/BuildMinimizedJarTask.kt new file mode 100644 index 0000000..75ceed5 --- /dev/null +++ b/buildSrc/src/main/kotlin/BuildMinimizedJarTask.kt @@ -0,0 +1,193 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.property +import proguard.gradle.ProGuardTask +import java.io.File +import java.io.OutputStream +import java.io.PrintStream +import java.nio.file.NoSuchFileException +import java.nio.file.Paths +import java.util.jar.JarFile +import kotlin.io.path.exists +import kotlin.math.abs as kotlinMathAbs + +open class BuildMinimizedJarTask : DefaultTask() { + @get:InputFile + val inputJarFile: RegularFileProperty = project.objects.fileProperty() + + @get:OutputFile + val outputJarFile: RegularFileProperty = project.objects.fileProperty() + + @get:InputFile + val proguardConfigFile: RegularFileProperty = project.objects.fileProperty() + + @get:Input + val obfuscate: Property = project.objects.property() + + @get:InputFiles + val compileClasspath: ConfigurableFileCollection = project.objects.fileCollection() + + @TaskAction + fun execute() { + val buildProguardDirectory = + project.layout.buildDirectory + .dir("proguard") + .get() + .asFile + + if (!buildProguardDirectory.exists()) { + buildProguardDirectory.mkdir() + } + + val generatedProguardConfigurationFiles = mutableListOf() + JarFile(inputJarFile.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() + + 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) + } + + generatedProguardConfigurationFiles.add(generatedProguardFile) + } + } + + val proguardTask = project.tasks.create("proguard") + proguardTask.apply { + injars(inputJarFile) + outjars(outputJarFile) + + val javaHome = System.getProperty("java.home") + if (System.getProperty("java.version").startsWith("1.")) { + // Before Java 9, runtime classes are packaged in a single JAR file. + libraryjars(Paths.get(javaHome, "lib", "rt.jar").toString()) + } else { + // Starting from Java 9, runtime classes are packaged in modular JMOD files. + fun includeJavaModuleFromJdk(jModFileNameWithoutExtension: String) { + val jModFilePath = Paths.get(javaHome, "jmods", "$jModFileNameWithoutExtension.jmod") + if (!jModFilePath.exists()) { + throw NoSuchFileException("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 + "java.desktop", + // Needed to support Java system preferences + "java.prefs", + // Needed to support Java logging utils (needed by Okio) + "java.logging", + ) + javaModules.forEach { includeJavaModuleFromJdk(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(compileClasspath) + + printmapping( + outputJarFile + .get() + .asFile.parentFile + .resolve("${outputJarFile.get().asFile.nameWithoutExtension}.map"), + ) + + if (!obfuscate.get()) { + // Disabling obfuscation makes the JAR file size a bit larger, and the debugging process a bit less easy + dontobfuscate() + } + + // Kotlinx serialization breaks when using Proguard optimizations + dontoptimize() + printconfiguration(buildProguardDirectory.resolve("proguard-configuration.pro")) + + configuration(proguardConfigFile) + + generatedProguardConfigurationFiles.forEach { configuration(it) } + } + + // Execute the Proguard task + + // A workaround for executing ProGuard without getting the notes by disabling the logging + // when the `-i` or `--info` is not set + + if (project.gradle.startParameter.logLevel != LogLevel.INFO) { + suppressOutputAndExecute { + proguardTask.actions.forEach { it.execute(proguardTask) } + } + } else { + proguardTask.actions.forEach { it.execute(proguardTask) } + } + + logResultMessage() + } + + private fun logResultMessage() { + val original = inputJarFile.get().asFile + val minimized = outputJarFile.get().asFile + val minimizedFileSizeInMegabytes = String.format("%.2f", minimized.length().toDouble() / (1024L * 1024L)) + + val percentageDifference = + ((minimized.length() - original.length()).toDouble() / original.length()) * 100 + val formattedPercentageDifference = String.format("%.2f%%", kotlinMathAbs(percentageDifference)) + + logger.lifecycle( + "📦 The size of the Proguard minimized JAR file (${minimized.name}) is $minimizedFileSizeInMegabytes MB." + + " The size has been reduced \uD83D\uDCC9 by $formattedPercentageDifference. Location: ${minimized.path}", + ) + } + + private fun suppressOutputAndExecute(action: () -> Unit) { + val oldStandardOut = System.out + val oldStandardErr = System.err + val noOpOutputStream = + object : OutputStream() { + override fun write(b: Int) { + // Do nothing + } + } + + try { + System.setOut(PrintStream(noOpOutputStream)) + System.setErr(PrintStream(noOpOutputStream)) + + action() + } finally { + System.setOut(oldStandardOut) + System.setErr(oldStandardErr) + } + } +} diff --git a/sync-script/proguard.pro b/common/proguard.pro similarity index 100% rename from sync-script/proguard.pro rename to common/proguard.pro diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53d881a..6493beb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,9 +31,11 @@ ktoml-core = { module = "com.akuleshov7:ktoml-core", version.ref = "ktoml" } # For tests kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +# For buildSrc +proguard = { module = "com.guardsquare:proguard-gradle", version.ref = "proguard" } + [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } shadow-jar = { id = "com.github.johnrengelman.shadow", version.ref = "shadow-jar" } -proguard = { id = "com.guardsquare:proguard-gradle", version.ref = "proguard" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } \ No newline at end of file diff --git a/sync-script/build.gradle.kts b/sync-script/build.gradle.kts index 670c1d1..c8f7db9 100644 --- a/sync-script/build.gradle.kts +++ b/sync-script/build.gradle.kts @@ -1,9 +1,3 @@ -import java.nio.file.NoSuchFileException -import java.nio.file.Paths -import java.util.jar.JarFile -import kotlin.io.path.exists -import kotlin.math.abs as kotlinMathAbs - plugins { application alias(libs.plugins.kotlin.jvm) @@ -83,141 +77,19 @@ tasks.shadowJar { // Proguard for minimizing the JAR file -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") - } - } -} - -fun getMinimizedJarFile( - fatJarFileNameWithoutExtension: String, - fatJarFileDestinationDirectory: DirectoryProperty, -): Provider = fatJarFileDestinationDirectory.file("$fatJarFileNameWithoutExtension.min.jar") - val minimizedJar = - tasks.register("minimizedJar") { + tasks.register("minimizedJar") { dependsOn(tasks.shadowJar) val fatJarFile = tasks.shadowJar.flatMap { it.archiveFile } val fatJarFileDestinationDirectory = tasks.shadowJar.get().destinationDirectory - val minimizedJarFile = - getMinimizedJarFile( - fatJarFileNameWithoutExtension = fatJarFile.get().asFile.nameWithoutExtension, - fatJarFileDestinationDirectory = fatJarFileDestinationDirectory, - ) - - injars(fatJarFile) - outjars(minimizedJarFile) - - val javaHome = System.getProperty("java.home") - if (System.getProperty("java.version").startsWith("1.")) { - // Before Java 9, runtime classes are packaged in a single JAR file. - libraryjars(Paths.get(javaHome, "lib", "rt.jar").toString()) - } else { - - // Starting from Java 9, runtime classes are packaged in modular JMOD files. - fun includeJavaModuleFromJdk(jModFileNameWithoutExtension: String) { - val jModFilePath = Paths.get(javaHome, "jmods", "$jModFileNameWithoutExtension.jmod") - if (!jModFilePath.exists()) { - throw NoSuchFileException("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 - "java.desktop", - // Needed to support Java system preferences - "java.prefs", - // Needed to support Java logging utils (needed by Okio) - "java.logging", - ) - javaModules.forEach { includeJavaModuleFromJdk(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( - fatJarFileDestinationDirectory.get().file("${minimizedJarFile.get().asFile.nameWithoutExtension}.map"), - ) - - // Disabling obfuscation makes the JAR file size a bit larger, and the debugging process a bit less easy -// dontobfuscate() - // Kotlinx serialization breaks when using Proguard optimizations - dontoptimize() - - configuration(file("proguard.pro")) - - doFirst { - JarFile(fatJarFile.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() + inputJarFile = fatJarFile + outputJarFile = fatJarFileDestinationDirectory.file("${fatJarFile.get().asFile.nameWithoutExtension}.min.jar") - 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) - } - } - } - - doLast { - val original = fatJarFile.get().asFile - val minimized = minimizedJarFile.get().asFile - val minimizedFileSizeInMegabytes = String.format("%.2f", minimized.length().toDouble() / (1024L * 1024L)) - - val percentageDifference = - ((minimized.length() - original.length()).toDouble() / original.length()) * 100 - val formattedPercentageDifference = String.format("%.2f%%", kotlinMathAbs(percentageDifference)) - - logger.lifecycle( - "📦 The size of the Proguard minimized JAR file (${minimized.name}) is $minimizedFileSizeInMegabytes MB." + - " The size has been reduced \uD83D\uDCC9 by $formattedPercentageDifference. Location: ${minimized.path}", - ) - } + proguardConfigFile = project(projects.common.identityPath.path).file("proguard.pro") + obfuscate = true + compileClasspath = sourceSets.main.get().compileClasspath } minimizedJar.configure { @@ -246,13 +118,13 @@ val createTestDirectory = private fun registerExecuteJavaJarTask( taskName: String, buildJarFileTaskProvider: TaskProvider, - jarFile: RegularFile, + getJarFile: () -> RegularFile, additionalArgs: List = emptyList(), overrideHeadless: Boolean? = null, ) { tasks.register(taskName) { dependsOn(createTestDirectory, buildJarFileTaskProvider) - classpath = files(jarFile) + classpath = files(getJarFile()) workingDir = devWorkingDirectory args = additionalArgs group = tasks.run.get().group @@ -263,31 +135,38 @@ private fun registerExecuteJavaJarTask( } fun registerRunTasks() { - val fatJarFile = + val getFatJarFile = { tasks.shadowJar .get() .archiveFile .get() + } registerExecuteJavaJarTask( - "runJar", - tasks.shadowJar, - fatJarFile, + taskName = "runJar", + buildJarFileTaskProvider = tasks.shadowJar, + getJarFile = getFatJarFile, ) registerExecuteJavaJarTask( - "runJarCli", - tasks.shadowJar, - fatJarFile, - listOf("nogui"), + taskName = "runJarCli", + buildJarFileTaskProvider = tasks.shadowJar, + getJarFile = getFatJarFile, + additionalArgs = listOf("nogui"), ) - val minimizedJarFile = - getMinimizedJarFile( - fatJarFileNameWithoutExtension = - fatJarFile.asFile.nameWithoutExtension, - fatJarFileDestinationDirectory = tasks.shadowJar.get().destinationDirectory, - ).get() - registerExecuteJavaJarTask("runMinimizedJar", minimizedJar, minimizedJarFile) - registerExecuteJavaJarTask("runMinimizedJarCli", minimizedJar, minimizedJarFile, listOf("nogui")) + val getMinimizedJarFile = { + minimizedJar.get().outputJarFile.get() + } + registerExecuteJavaJarTask( + taskName = "runMinimizedJar", + buildJarFileTaskProvider = minimizedJar, + getJarFile = getMinimizedJarFile, + ) + registerExecuteJavaJarTask( + taskName = "runMinimizedJarCli", + buildJarFileTaskProvider = minimizedJar, + getJarFile = getMinimizedJarFile, + additionalArgs = listOf("nogui"), + ) // A task that will help simulate as if we were running the // application in a system that doesn't support mouse and keyboard. @@ -295,7 +174,7 @@ fun registerRunTasks() { registerExecuteJavaJarTask( "runHeadlessJar", tasks.shadowJar, - fatJarFile, + getFatJarFile, overrideHeadless = true, ) }