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

Option to pack jars as uber JAR, support Proguard for uber JAR #4136

Merged
merged 7 commits into from
Apr 12, 2024
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
Loading