Skip to content

Commit

Permalink
Option to pack jars as uber JAR, support Proguard for uber JAR (#4136)
Browse files Browse the repository at this point in the history
## Proposed changes
1. Added support to join JARs to the uber JAR with ProGuard, disabled by
default:
```
compose.desktop {
    application {
        buildTypes.release.proguard {
            joinOutputJars.set(true)
        }
    }
}
```
2. All 'release' tasks now really depend on ProGuard, as stated in
[tutorial](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials/Native_distributions_and_local_execution#minification--obfuscation).

## Testing
- A new auto test

- Manual:
1. Test on Windows/macOs/Linux
2. Test the new Gradle parameter `joinOutputJars`:
```
compose.desktop {
    application {
        buildTypes.release.proguard {
            joinOutputJars.set(true)
        }
    }
}
```
`false` (by default) should generate multiple jars (except for
`package*UberJarForCurrentOS`)
`true` should generate a single jar in a result distribution
3. Test debug tasks:
```
run
runDistributable
createDistributable
packageUberJarForCurrentOS
```
4. Test release tasks:
```
runRelease
runReleaseDistributable
createReleaseDistributable
packageReleaseUberJarForCurrentOS
```
The jars should be reduced in size (because Proguard is enabled in the
release mode)

This should be test by QA.

## Issues fixed
Fixes #4129

---------

Co-authored-by: Igor Demin <igordmn@users.noreply.github.com>
  • Loading branch information
badmannersteam and igordmn committed Apr 12, 2024
1 parent 6a481b3 commit 994f0c6
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ abstract class ProguardSettings @Inject constructor(
val isEnabled: Property<Boolean> = objects.notNullProperty(false)
val obfuscate: Property<Boolean> = objects.notNullProperty(false)
val optimize: Property<Boolean> = objects.notNullProperty(true)
val joinOutputJars: Property<Boolean> = objects.notNullProperty(false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package org.jetbrains.compose.desktop.application.internal

import org.gradle.api.DefaultTask
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.Sync
Expand All @@ -16,6 +15,7 @@ import org.gradle.jvm.tasks.Jar
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions
import org.jetbrains.compose.desktop.application.tasks.*
import org.jetbrains.compose.desktop.tasks.AbstractJarsFlattenTask
import org.jetbrains.compose.desktop.tasks.AbstractUnpackDefaultComposeApplicationResourcesTask
import org.jetbrains.compose.internal.utils.*
import org.jetbrains.compose.internal.utils.OS
Expand All @@ -26,7 +26,6 @@ import org.jetbrains.compose.internal.utils.ioFile
import org.jetbrains.compose.internal.utils.ioFileOrNull
import org.jetbrains.compose.internal.utils.javaExecutable
import org.jetbrains.compose.internal.utils.provider
import java.io.File

private val defaultJvmArgs = listOf("-D$CONFIGURE_SWING_GLOBALS=true")
internal const val composeDesktopTaskGroup = "compose desktop"
Expand Down Expand Up @@ -220,11 +219,18 @@ private fun JvmApplicationContext.configurePackagingTasks(
}
}

val flattenJars = tasks.register<AbstractJarsFlattenTask>(
taskNameAction = "flatten",
taskNameObject = "Jars"
) {
configureFlattenJars(this, runProguard)
}

val packageUberJarForCurrentOS = tasks.register<Jar>(
taskNameAction = "package",
taskNameObject = "uberJarForCurrentOS"
) {
configurePackageUberJarForCurrentOS(this)
configurePackageUberJarForCurrentOS(this, flattenJars)
}

val runDistributable = tasks.register<AbstractRunDistributableTask>(
Expand All @@ -234,7 +240,7 @@ private fun JvmApplicationContext.configurePackagingTasks(
)

val run = tasks.register<JavaExec>(taskNameAction = "run") {
configureRunTask(this, commonTasks.prepareAppResources)
configureRunTask(this, commonTasks.prepareAppResources, runProguard)
}
}

Expand All @@ -260,6 +266,8 @@ private fun JvmApplicationContext.configureProguardTask(
dontobfuscate.set(settings.obfuscate.map { !it })
dontoptimize.set(settings.optimize.map { !it })

joinOutputJars.set(settings.joinOutputJars)

dependsOn(unpackDefaultResources)
defaultComposeRulesFile.set(unpackDefaultResources.flatMap { it.resources.defaultComposeProguardRules })

Expand Down Expand Up @@ -326,6 +334,7 @@ private fun JvmApplicationContext.configurePackageTask(
packageTask.files.from(project.fileTree(runProguard.flatMap { it.destinationDir }))
packageTask.launcherMainJar.set(runProguard.flatMap { it.mainJarInDestinationDir })
packageTask.mangleJarFilesNames.set(false)
packageTask.packageFromUberJar.set(runProguard.flatMap { it.joinOutputJars })
} else {
packageTask.useAppRuntimeFiles { (runtimeJars, mainJar) ->
files.from(runtimeJars)
Expand Down Expand Up @@ -412,7 +421,8 @@ internal fun JvmApplicationContext.configurePlatformSettings(

private fun JvmApplicationContext.configureRunTask(
exec: JavaExec,
prepareAppResources: TaskProvider<Sync>
prepareAppResources: TaskProvider<Sync>,
runProguard: Provider<AbstractProguardTask>?
) {
exec.dependsOn(prepareAppResources)

Expand All @@ -431,34 +441,49 @@ private fun JvmApplicationContext.configureRunTask(
add("-D$APP_RESOURCES_DIR=${appResourcesDir.absolutePath}")
}
exec.args = app.args
exec.useAppRuntimeFiles { (runtimeJars, _) ->
classpath = runtimeJars

if (runProguard != null) {
exec.dependsOn(runProguard)
exec.classpath = project.fileTree(runProguard.flatMap { it.destinationDir })
} else {
exec.useAppRuntimeFiles { (runtimeJars, _) ->
classpath = runtimeJars
}
}
}

private fun JvmApplicationContext.configurePackageUberJarForCurrentOS(jar: Jar) {
fun flattenJars(files: FileCollection): FileCollection =
jar.project.files({
files.map { if (it.isZipOrJar()) jar.project.zipTree(it) else it }
})
private fun JvmApplicationContext.configureFlattenJars(
flattenJars: AbstractJarsFlattenTask,
runProguard: Provider<AbstractProguardTask>?
) {
if (runProguard != null) {
flattenJars.dependsOn(runProguard)
flattenJars.inputFiles.from(runProguard.flatMap { it.destinationDir })
} else {
flattenJars.useAppRuntimeFiles { (runtimeJars, _) ->
inputFiles.from(runtimeJars)
}
}

flattenJars.flattenedJar.set(appTmpDir.file("flattenJars/flattened.jar"))
}

jar.useAppRuntimeFiles { (runtimeJars, _) ->
from(flattenJars(runtimeJars))
}
private fun JvmApplicationContext.configurePackageUberJarForCurrentOS(
jar: Jar,
flattenJars: Provider<AbstractJarsFlattenTask>
) {
jar.dependsOn(flattenJars)
jar.from(project.zipTree(flattenJars.flatMap { it.flattenedJar }))

app.mainClass?.let { jar.manifest.attributes["Main-Class"] = it }
jar.duplicatesStrategy = DuplicatesStrategy.EXCLUDE
jar.archiveAppendix.set(currentTarget.id)
jar.archiveBaseName.set(packageNameProvider)
jar.archiveVersion.set(packageVersionFor(TargetFormat.AppImage))
jar.archiveClassifier.set(buildType.classifier)
jar.destinationDirectory.set(jar.project.layout.buildDirectory.dir("compose/jars"))

jar.doLast {
jar.logger.lifecycle("The jar is written to ${jar.archiveFile.ioFile.canonicalPath}")
}
}

private fun File.isZipOrJar() =
name.endsWith(".jar", ignoreCase = true)
|| name.endsWith(".zip", ignoreCase = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Input
val mangleJarFilesNames: Property<Boolean> = objects.notNullProperty(true)

/**
* Indicates that task will get the uber JAR as input.
*/
@get:Input
val packageFromUberJar: Property<Boolean> = objects.notNullProperty(false)

@get:InputDirectory
@get:Optional
/** @see internal/wixToolset.kt */
Expand Down Expand Up @@ -323,7 +329,7 @@ abstract class AbstractJPackageTask @Inject constructor(

javaOption("-D$APP_RESOURCES_DIR=${appDir(packagedResourcesDir.ioFile.name)}")

val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull()
val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull { it.isJarFile }
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}")
val mainJarPath = mappedJar.normalizedPath(base = libsDir.ioFile)
cliArg("--main-jar", mainJarPath)
Expand Down Expand Up @@ -468,11 +474,14 @@ abstract class AbstractJPackageTask @Inject constructor(
return targetFile
}

// skiko can be bundled to the main uber jar by proguard
fun File.isMainUberJar() = packageFromUberJar.get() && name == launcherMainJar.ioFile.name

val outdatedLibs = invalidateMappedLibs(inputChanges)
for (sourceFile in outdatedLibs) {
assert(sourceFile.exists()) { "Lib file does not exist: $sourceFile" }

libsMapping[sourceFile] = if (isSkikoForCurrentOS(sourceFile)) {
libsMapping[sourceFile] = if (isSkikoForCurrentOS(sourceFile) || sourceFile.isMainUberJar()) {
val unpackedFiles = unpackSkikoForCurrentOS(sourceFile, skikoDir.ioFile, fileOperations)
unpackedFiles.map { copyFileToLibsDir(it) }
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ abstract class AbstractProguardTask : AbstractComposeDesktopTask() {
@get:Input
val dontoptimize: Property<Boolean?> = objects.nullableProperty()

@get:Optional
@get:Input
val joinOutputJars: Property<Boolean?> = objects.nullableProperty()

// todo: DSL for excluding default rules
// also consider pulling coroutines rules from coroutines artifact
// https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
Expand Down Expand Up @@ -98,10 +102,14 @@ abstract class AbstractProguardTask : AbstractComposeDesktopTask() {
}

jarsConfigurationFile.ioFile.bufferedWriter().use { writer ->
val toSingleOutputJar = joinOutputJars.orNull == true
for ((input, output) in inputToOutputJars.entries) {
writer.writeLn("-injars '${input.normalizedPath()}'")
writer.writeLn("-outjars '${output.normalizedPath()}'")
if (!toSingleOutputJar)
writer.writeLn("-outjars '${output.normalizedPath()}'")
}
if (toSingleOutputJar)
writer.writeLn("-outjars '${mainJarInDestinationDir.ioFile.normalizedPath()}'")

for (jmod in jmods) {
writer.writeLn("-libraryjars '${jmod.normalizedPath()}'(!**.jar;!module-info.class)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.jetbrains.compose.desktop.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.file.ArchiveOperations
import org.gradle.api.file.Directory
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.model.ObjectFactory
Expand Down Expand Up @@ -33,6 +34,9 @@ abstract class AbstractComposeDesktopTask : DefaultTask() {
@get:Inject
protected abstract val fileOperations: FileSystemOperations

@get:Inject
protected abstract val archiveOperations: ArchiveOperations

@get:LocalState
protected val logsDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/logs/$name")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package org.jetbrains.compose.desktop.tasks

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.jetbrains.compose.desktop.application.internal.files.copyZipEntry
import org.jetbrains.compose.desktop.application.internal.files.isJarFile
import org.jetbrains.compose.internal.utils.delete
import org.jetbrains.compose.internal.utils.ioFile
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream


/**
* This task flattens all jars from the input directory into the single one,
* which is used later as a single source for uberjar.
*
* This task is necessary because the standard Jar/Zip task evaluates own `from()` args eagerly
* [in the configuration phase](https://discuss.gradle.org/t/why-is-the-closure-in-from-method-of-copy-task-evaluated-in-config-phase/23469/4)
* and snapshots an empty list of files in the Proguard destination directory,
* instead of a list of real jars after Proguard task execution.
*
* Also, we use output to the single jar instead of flattening to the directory in the filesystem because:
* - Windows filesystem is case-insensitive and not every jar can be unzipped without losing files
* - it's just faster
*/
abstract class AbstractJarsFlattenTask : AbstractComposeDesktopTask() {

@get:InputFiles
val inputFiles: ConfigurableFileCollection = objects.fileCollection()

@get:OutputFile
val flattenedJar: RegularFileProperty = objects.fileProperty()

@get:Internal
val seenEntryNames = hashSetOf<String>()

@TaskAction
fun execute() {
seenEntryNames.clear()
fileOperations.delete(flattenedJar)

ZipOutputStream(FileOutputStream(flattenedJar.ioFile).buffered()).use { outputStream ->
inputFiles.asFileTree.visit {
when {
!it.isDirectory && it.file.isJarFile -> outputStream.writeJarContent(it.file)
!it.isDirectory -> outputStream.writeFile(it.file)
}
}
}
}

private fun ZipOutputStream.writeJarContent(jarFile: File) =
ZipInputStream(FileInputStream(jarFile)).use { inputStream ->
var inputEntry: ZipEntry? = inputStream.nextEntry
while (inputEntry != null) {
writeEntryIfNotSeen(inputEntry, inputStream)
inputEntry = inputStream.nextEntry
}
}

private fun ZipOutputStream.writeFile(file: File) =
FileInputStream(file).use { inputStream ->
writeEntryIfNotSeen(ZipEntry(file.name), inputStream)
}

private fun ZipOutputStream.writeEntryIfNotSeen(entry: ZipEntry, inputStream: InputStream) {
if (entry.name !in seenEntryNames) {
copyZipEntry(entry, inputStream, this)
seenEntryNames += entry.name
}
}
}
Loading

0 comments on commit 994f0c6

Please sign in to comment.