From e4ac7ef933d04f71e3e55a14fb02ee9b8a997708 Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Wed, 17 Jan 2024 17:35:45 +0100 Subject: [PATCH 1/7] Added support to join JARs to the uber JAR with ProGuard, all 'release' tasks now depend on Proguard. --- .../application/dsl/ProguardSettings.kt | 1 + .../internal/configureJvmApplication.kt | 36 ++++++++++++++----- .../application/tasks/AbstractJPackageTask.kt | 13 +++++-- .../application/tasks/AbstractProguardTask.kt | 10 +++++- .../README.md | 31 +++++++++++----- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt index 3a6ef8d84ff..a3459d40d5c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ProguardSettings.kt @@ -23,4 +23,5 @@ abstract class ProguardSettings @Inject constructor( val isEnabled: Property = objects.notNullProperty(false) val obfuscate: Property = objects.notNullProperty(false) val optimize: Property = objects.notNullProperty(true) + val joinOutputJars: Property = objects.notNullProperty(false) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index e93462ea7e0..756f8811aaa 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -224,7 +224,7 @@ private fun JvmApplicationContext.configurePackagingTasks( taskNameAction = "package", taskNameObject = "uberJarForCurrentOS" ) { - configurePackageUberJarForCurrentOS(this) + configurePackageUberJarForCurrentOS(this, runProguard) } val runDistributable = tasks.register( @@ -234,7 +234,7 @@ private fun JvmApplicationContext.configurePackagingTasks( ) val run = tasks.register(taskNameAction = "run") { - configureRunTask(this, commonTasks.prepareAppResources) + configureRunTask(this, commonTasks.prepareAppResources, runProguard) } } @@ -260,6 +260,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 }) @@ -326,6 +328,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) @@ -412,7 +415,8 @@ internal fun JvmApplicationContext.configurePlatformSettings( private fun JvmApplicationContext.configureRunTask( exec: JavaExec, - prepareAppResources: TaskProvider + prepareAppResources: TaskProvider, + runProguard: Provider? ) { exec.dependsOn(prepareAppResources) @@ -431,20 +435,33 @@ 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) { +private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( + jar: Jar, + runProguard: Provider? +) { fun flattenJars(files: FileCollection): FileCollection = jar.project.files({ files.map { if (it.isZipOrJar()) jar.project.zipTree(it) else it } }) - - jar.useAppRuntimeFiles { (runtimeJars, _) -> - from(flattenJars(runtimeJars)) + if (runProguard != null) { + jar.dependsOn(runProguard) + jar.from(flattenJars(project.fileTree(runProguard.flatMap { it.destinationDir }))) + } else { + jar.useAppRuntimeFiles { (runtimeJars, _) -> + from(flattenJars(runtimeJars)) + } } app.mainClass?.let { jar.manifest.attributes["Main-Class"] = it } @@ -452,6 +469,7 @@ private fun JvmApplicationContext.configurePackageUberJarForCurrentOS(jar: Jar) 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 { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index 8dbb0dae75a..ad56727c059 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -65,6 +65,12 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Input val mangleJarFilesNames: Property = objects.notNullProperty(true) + /** + * Indicates that task will get the uber JAR as input. + */ + @get:Input + val packageFromUberJar: Property = objects.notNullProperty(false) + @get:InputDirectory @get:Optional /** @see internal/wixToolset.kt */ @@ -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) @@ -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 { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt index 201d5353b53..fa04502a912 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractProguardTask.kt @@ -42,6 +42,10 @@ abstract class AbstractProguardTask : AbstractComposeDesktopTask() { @get:Input val dontoptimize: Property = objects.nullableProperty() + @get:Optional + @get:Input + val joinOutputJars: Property = 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 @@ -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)") diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index c822be32861..ffc91c4904d 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -596,15 +596,16 @@ that is developed by [Guardsquare](https://www.guardsquare.com/). The Gradle plugin provides a *release* task for each corresponding *default* packaging task: -Default task (w/o ProGuard)| Release task (w. ProGuard) |Description ----------------------------|----------------------------------|----------- -`createDistributable` | `createReleaseDistributable` |Creates an application image with bundled JDK & resources -`runDistributable` | `runReleaseDistributable` |Runs an application image with bundled JDK & resources -`run` | `runRelease` |Runs a non-packaged application `jar` using Gradle JDK -`package` | `packageRelease` |Packages an application image into a `` file -`packageForCurrentOS` | `packageReleaseForCurrentOS` |Packages an application image into a format compatible with current OS -`notarize` | `notarizeRelease` |Uploads a `` application image for notarization (macOS only) -`checkNotarizationStatus` | `checkReleaseNotarizationStatus` |Checks if notarization succeeded (macOS only) + Default task (w/o ProGuard) | Release task (w. ProGuard) | Description +-----------------------------------|------------------------------------------|-------------------------------------------------------------------------- + `createDistributable` | `createReleaseDistributable` | Creates an application image with bundled JDK & resources + `runDistributable` | `runReleaseDistributable` | Runs an application image with bundled JDK & resources + `run` | `runRelease` | Runs a non-packaged application `jar` using Gradle JDK + `package` | `packageRelease` | Packages an application image into a `` file + `packageDistributionForCurrentOS` | `packageReleaseDistributionForCurrentOS` | Packages an application image into a format compatible with current OS + `packageUberJarForCurrentOS` | `packageReleaseUberJarForCurrentOS` | Packages an application image into an uber (fat) JAR + `notarize` | `notarizeRelease` | Uploads a `` application image for notarization (macOS only) + `checkNotarizationStatus` | `checkReleaseNotarizationStatus` | Checks if notarization succeeded (macOS only) The default configuration adds a few ProGuard rules: * an application image is minified, i.e. non-used classes are removed; @@ -650,3 +651,15 @@ compose.desktop { } } ``` + +Joining to the uber JAR is disabled by default - ProGuard produces the corresponding JAR for every input JAR. +To enable it, set the following property via Gradle DSL: +``` +compose.desktop { + application { + buildTypes.release.proguard { + joinOutputJars.set(true) + } + } +} +``` From c54a6ca062091ec9f5ae50ba08596c22c5672c77 Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Thu, 1 Feb 2024 21:30:39 +0100 Subject: [PATCH 2/7] Tests for JARs joining and 'release' tasks. --- .../integration/DesktopApplicationTest.kt | 138 +++++++++++++++--- 1 file changed, 114 insertions(+), 24 deletions(-) diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 9dd9e572e86..2473ab89d1d 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -32,20 +32,33 @@ class DesktopApplicationTest : GradlePluginTestBase() { tasks.getByName("run").doFirst { throw new StopExecutionException("Skip run task") } + tasks.getByName("runRelease").doFirst { + throw new StopExecutionException("Skip runRelease task") + } tasks.getByName("runDistributable").doFirst { throw new StopExecutionException("Skip runDistributable task") } + tasks.getByName("runReleaseDistributable").doFirst { + throw new StopExecutionException("Skip runReleaseDistributable task") + } } """.trimIndent() } gradle("run").checks { check.taskSuccessful(":run") } + gradle("runRelease").checks { + check.taskSuccessful(":runRelease") + } gradle("runDistributable").checks { check.taskSuccessful(":createDistributable") check.taskSuccessful(":runDistributable") } + gradle("runReleaseDistributable").checks { + check.taskSuccessful(":createReleaseDistributable") + check.taskSuccessful(":runReleaseDistributable") + } } @Test @@ -55,11 +68,20 @@ class DesktopApplicationTest : GradlePluginTestBase() { check.taskSuccessful(":run") check.logContains(logLine) } + gradle("runRelease").checks { + check.taskSuccessful(":runRelease") + check.logContains(logLine) + } gradle("runDistributable").checks { check.taskSuccessful(":createDistributable") check.taskSuccessful(":runDistributable") check.logContains(logLine) } + gradle("runReleaseDistributable").checks { + check.taskSuccessful(":createReleaseDistributable") + check.taskSuccessful(":runReleaseDistributable") + check.logContains(logLine) + } } /** @@ -165,6 +187,33 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } + @Test + fun joinOutputJarsJvm() = with(testProject(TestProjects.jvm)) { + joinOutputJars() + } + + @Test + fun joinOutputJarsMpp() = with(testProject(TestProjects.mpp)) { + joinOutputJars() + } + + private fun TestProject.joinOutputJars() { + enableJoinOutputJars() + gradle(":createReleaseDistributable").checks { + check.taskSuccessful(":createReleaseDistributable") + + val distributionPathPattern = "The distribution is written to (.*)".toRegex() + val m = distributionPathPattern.find(check.log) + val distributionDir = m?.groupValues?.get(1)?.let(::File) + if (distributionDir == null || !distributionDir.exists()) { + error("Invalid distribution path: $distributionDir") + } + val appDir = distributionDir.resolve("TestPackage/app") + val jarsCount = appDir.listFiles()?.count { it.name.endsWith(".jar", ignoreCase = true) } ?: 0 + assert(jarsCount == 1) + } + } + @Test fun gradleBuildCache() = with(testProject(TestProjects.jvm)) { modifyGradleProperties { @@ -265,19 +314,39 @@ class DesktopApplicationTest : GradlePluginTestBase() { @Test fun packageUberJarForCurrentOSJvm() = with(testProject(TestProjects.jvm)) { - testPackageUberJarForCurrentOS() + testPackageUberJarForCurrentOS(false) } @Test fun packageUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { - testPackageUberJarForCurrentOS() + testPackageUberJarForCurrentOS(false) + } + + @Test + fun packageReleaseUberJarForCurrentOSJvm() = with(testProject(TestProjects.jvm)) { + testPackageUberJarForCurrentOS(true) + } + + @Test + fun packageReleaseUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { + testPackageUberJarForCurrentOS(true) } - private fun TestProject.testPackageUberJarForCurrentOS() { - gradle(":packageUberJarForCurrentOS").checks { - check.taskSuccessful(":packageUberJarForCurrentOS") + private fun TestProject.testPackageUberJarForCurrentOS(release: Boolean) { + val task = when { + release -> ":packageReleaseUberJarForCurrentOS" + else -> ":packageUberJarForCurrentOS" + } + + val jarFileName = when { + release -> "build/compose/jars/TestPackage-${currentTarget.id}-1.0.0-release.jar" + else -> "build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar" + } + + gradle(task).checks { + check.taskSuccessful(task) - val resultJarFile = file("build/compose/jars/TestPackage-${currentTarget.id}-1.0.0.jar") + val resultJarFile = file(jarFileName) resultJarFile.checkExists() JarFile(resultJarFile).use { jar -> @@ -470,25 +539,33 @@ class DesktopApplicationTest : GradlePluginTestBase() { } @Test - fun testUnpackSkiko() { - with(testProject(TestProjects.unpackSkiko)) { - gradle(":runDistributable").checks { - check.taskSuccessful(":runDistributable") + fun testUnpackSkiko() = with(testProject(TestProjects.unpackSkiko)) { + testUnpackSkiko(":runDistributable") + } - val libraryPathPattern = "Read skiko library path: '(.*)'".toRegex() - val m = libraryPathPattern.find(check.log) - val skikoDir = m?.groupValues?.get(1)?.let(::File) - if (skikoDir == null || !skikoDir.exists()) { - error("Invalid skiko path: $skikoDir") - } - val filesToFind = when (currentOS) { - OS.Linux -> listOf("libskiko-linux-${currentArch.id}.so") - OS.Windows -> listOf("skiko-windows-${currentArch.id}.dll", "icudtl.dat") - OS.MacOS -> listOf("libskiko-macos-${currentArch.id}.dylib") - } - for (fileName in filesToFind) { - skikoDir.resolve(fileName).checkExists() - } + @Test + fun testUnpackSkikoFromUberJar() = with(testProject(TestProjects.unpackSkiko)) { + enableJoinOutputJars() + testUnpackSkiko(":runReleaseDistributable") + } + + private fun TestProject.testUnpackSkiko(runDistributableTask: String) { + gradle(runDistributableTask).checks { + check.taskSuccessful(runDistributableTask) + + val libraryPathPattern = "Read skiko library path: '(.*)'".toRegex() + val m = libraryPathPattern.find(check.log) + val skikoDir = m?.groupValues?.get(1)?.let(::File) + if (skikoDir == null || !skikoDir.exists()) { + error("Invalid skiko path: $skikoDir") + } + val filesToFind = when (currentOS) { + OS.Linux -> listOf("libskiko-linux-${currentArch.id}.so") + OS.Windows -> listOf("skiko-windows-${currentArch.id}.dll", "icudtl.dat") + OS.MacOS -> listOf("libskiko-macos-${currentArch.id}.dylib") + } + for (fileName in filesToFind) { + skikoDir.resolve(fileName).checkExists() } } } @@ -518,4 +595,17 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } } + + private fun TestProject.enableJoinOutputJars() { + val enableJoinOutputJars = """ + compose.desktop { + application { + buildTypes.release.proguard { + joinOutputJars.set(true) + } + } + } + """.trimIndent() + file("build.gradle").modify { "$it\n$enableJoinOutputJars" } + } } From fd26dd93456c246018b1eb4e8ed01bdf17851220 Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Fri, 16 Feb 2024 00:33:10 +0100 Subject: [PATCH 3/7] Jars flattening as a separate task to fix packageUberJarForCurrentOS. --- .../internal/configureJvmApplication.kt | 45 +++++++++++-------- .../tasks/AbstractComposeDesktopTask.kt | 4 ++ .../desktop/tasks/AbstractJarsFlattenTask.kt | 45 +++++++++++++++++++ 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 756f8811aaa..466e6f61b97 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -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 @@ -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 @@ -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" @@ -220,11 +219,18 @@ private fun JvmApplicationContext.configurePackagingTasks( } } + val flattenJars = tasks.register( + taskNameAction = "flatten", + taskNameObject = "Jars" + ) { + configureFlattenJars(this, runProguard) + } + val packageUberJarForCurrentOS = tasks.register( taskNameAction = "package", taskNameObject = "uberJarForCurrentOS" ) { - configurePackageUberJarForCurrentOS(this, runProguard) + configurePackageUberJarForCurrentOS(this, flattenJars) } val runDistributable = tasks.register( @@ -446,24 +452,29 @@ private fun JvmApplicationContext.configureRunTask( } } -private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( - jar: Jar, +private fun JvmApplicationContext.configureFlattenJars( + flattenJars: AbstractJarsFlattenTask, runProguard: Provider? ) { - fun flattenJars(files: FileCollection): FileCollection = - jar.project.files({ - files.map { if (it.isZipOrJar()) jar.project.zipTree(it) else it } - }) - if (runProguard != null) { - jar.dependsOn(runProguard) - jar.from(flattenJars(project.fileTree(runProguard.flatMap { it.destinationDir }))) + flattenJars.dependsOn(runProguard) + flattenJars.inputFiles.from(project.fileTree(runProguard.flatMap { it.destinationDir })) } else { - jar.useAppRuntimeFiles { (runtimeJars, _) -> - from(flattenJars(runtimeJars)) + flattenJars.useAppRuntimeFiles { (runtimeJars, _) -> + inputFiles.from(runtimeJars) } } + flattenJars.destinationDir.set(appTmpDir.dir("flattenJars")) +} + +private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( + jar: Jar, + flattenJars: Provider +) { + jar.dependsOn(flattenJars) + jar.from(flattenJars.flatMap { it.destinationDir }) + app.mainClass?.let { jar.manifest.attributes["Main-Class"] = it } jar.duplicatesStrategy = DuplicatesStrategy.EXCLUDE jar.archiveAppendix.set(currentTarget.id) @@ -475,8 +486,4 @@ private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( 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) \ No newline at end of file +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt index 63110a417d1..f5c8e600a66 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractComposeDesktopTask.kt @@ -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 @@ -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 = project.layout.buildDirectory.dir("compose/logs/$name") diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt new file mode 100644 index 00000000000..cf1d436f54b --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt @@ -0,0 +1,45 @@ +/* + * 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.DirectoryProperty +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.jetbrains.compose.internal.utils.clearDirs +import java.io.File + +abstract class AbstractJarsFlattenTask : AbstractComposeDesktopTask() { + + @get:InputFiles + val inputFiles: ConfigurableFileCollection = objects.fileCollection() + + @get:OutputDirectory + val destinationDir: DirectoryProperty = objects.directoryProperty() + + @TaskAction + fun execute() { + fileOperations.clearDirs(destinationDir) + + fileOperations.copy { + it.duplicatesStrategy = DuplicatesStrategy.EXCLUDE + it.from(flattenJars(inputFiles)) + it.into(destinationDir) + } + } + + private fun flattenJars(files: FileCollection) = files.map { + when { + it.isZipOrJar() -> this.archiveOperations.zipTree(it) + else -> it + } + } + + private fun File.isZipOrJar() = name.endsWith(".jar", ignoreCase = true) || name.endsWith(".zip", ignoreCase = true) +} \ No newline at end of file From 3da362ad57fc7ffa1f5937c128f2659c4e59a239 Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Tue, 12 Mar 2024 14:32:42 +0100 Subject: [PATCH 4/7] Fix for join output jars tests on Linux and MacOS. --- .../test/tests/integration/DesktopApplicationTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 2473ab89d1d..0653807888a 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -208,7 +208,12 @@ class DesktopApplicationTest : GradlePluginTestBase() { if (distributionDir == null || !distributionDir.exists()) { error("Invalid distribution path: $distributionDir") } - val appDir = distributionDir.resolve("TestPackage/app") + val appDirSubPath = when (currentOS) { + OS.Linux -> "TestPackage/lib/app" + OS.Windows -> "TestPackage/app" + OS.MacOS -> "TestPackage.app/Contents/app" + } + val appDir = distributionDir.resolve(appDirSubPath) val jarsCount = appDir.listFiles()?.count { it.name.endsWith(".jar", ignoreCase = true) } ?: 0 assert(jarsCount == 1) } From 7fe647fd21a5f459453047c15ffac030aba6236e Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Tue, 12 Mar 2024 20:32:28 +0100 Subject: [PATCH 5/7] Jars flattening task now flattens to the single jar instead of directory. --- .../internal/configureJvmApplication.kt | 6 +- .../desktop/tasks/AbstractJarsFlattenTask.kt | 77 ++++++++++++++----- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 466e6f61b97..d53ad3dd621 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -458,14 +458,14 @@ private fun JvmApplicationContext.configureFlattenJars( ) { if (runProguard != null) { flattenJars.dependsOn(runProguard) - flattenJars.inputFiles.from(project.fileTree(runProguard.flatMap { it.destinationDir })) + flattenJars.inputFiles.from(runProguard.flatMap { it.destinationDir }) } else { flattenJars.useAppRuntimeFiles { (runtimeJars, _) -> inputFiles.from(runtimeJars) } } - flattenJars.destinationDir.set(appTmpDir.dir("flattenJars")) + flattenJars.flattenedJar.set(appTmpDir.file("flattenJars/flattened.jar")) } private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( @@ -473,7 +473,7 @@ private fun JvmApplicationContext.configurePackageUberJarForCurrentOS( flattenJars: Provider ) { jar.dependsOn(flattenJars) - jar.from(flattenJars.flatMap { it.destinationDir }) + jar.from(project.zipTree(flattenJars.flatMap { it.flattenedJar })) app.mainClass?.let { jar.manifest.attributes["Main-Class"] = it } jar.duplicatesStrategy = DuplicatesStrategy.EXCLUDE diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt index cf1d436f54b..b0cd78af478 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt @@ -6,40 +6,81 @@ package org.jetbrains.compose.desktop.tasks import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.DuplicatesStrategy -import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import org.jetbrains.compose.internal.utils.clearDirs +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-sensitive 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:OutputDirectory - val destinationDir: DirectoryProperty = objects.directoryProperty() + @get:OutputFile + val flattenedJar: RegularFileProperty = objects.fileProperty() + + @get:Internal + val entries = hashSetOf() @TaskAction fun execute() { - fileOperations.clearDirs(destinationDir) + entries.clear() + fileOperations.delete(flattenedJar) - fileOperations.copy { - it.duplicatesStrategy = DuplicatesStrategy.EXCLUDE - it.from(flattenJars(inputFiles)) - it.into(destinationDir) + 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 flattenJars(files: FileCollection) = files.map { - when { - it.isZipOrJar() -> this.archiveOperations.zipTree(it) - else -> it + 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 File.isZipOrJar() = name.endsWith(".jar", ignoreCase = true) || name.endsWith(".zip", ignoreCase = true) + 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 entries) { + copyZipEntry(entry, inputStream, this) + entries += entry.name + } + } } \ No newline at end of file From ab9a71267099ea418eb9bfc5fb6a6e3098b0997b Mon Sep 17 00:00:00 2001 From: badmannersteam Date: Fri, 15 Mar 2024 14:08:01 +0100 Subject: [PATCH 6/7] Small fixes. --- .../compose/desktop/tasks/AbstractJarsFlattenTask.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt index b0cd78af478..161a9b106e4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/tasks/AbstractJarsFlattenTask.kt @@ -34,7 +34,7 @@ import java.util.zip.ZipOutputStream * 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-sensitive and not every jar can be unzipped without losing files + * - Windows filesystem is case-insensitive and not every jar can be unzipped without losing files * - it's just faster */ abstract class AbstractJarsFlattenTask : AbstractComposeDesktopTask() { @@ -46,11 +46,11 @@ abstract class AbstractJarsFlattenTask : AbstractComposeDesktopTask() { val flattenedJar: RegularFileProperty = objects.fileProperty() @get:Internal - val entries = hashSetOf() + val seenEntryNames = hashSetOf() @TaskAction fun execute() { - entries.clear() + seenEntryNames.clear() fileOperations.delete(flattenedJar) ZipOutputStream(FileOutputStream(flattenedJar.ioFile).buffered()).use { outputStream -> @@ -78,9 +78,9 @@ abstract class AbstractJarsFlattenTask : AbstractComposeDesktopTask() { } private fun ZipOutputStream.writeEntryIfNotSeen(entry: ZipEntry, inputStream: InputStream) { - if (entry.name !in entries) { + if (entry.name !in seenEntryNames) { copyZipEntry(entry, inputStream, this) - entries += entry.name + seenEntryNames += entry.name } } } \ No newline at end of file From 957cdb4b58fffac459477fad57ba602cc469ac47 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 12 Apr 2024 04:04:16 +0200 Subject: [PATCH 7/7] Revert README.md --- .../README.md | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index ffc91c4904d..c822be32861 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -596,16 +596,15 @@ that is developed by [Guardsquare](https://www.guardsquare.com/). The Gradle plugin provides a *release* task for each corresponding *default* packaging task: - Default task (w/o ProGuard) | Release task (w. ProGuard) | Description ------------------------------------|------------------------------------------|-------------------------------------------------------------------------- - `createDistributable` | `createReleaseDistributable` | Creates an application image with bundled JDK & resources - `runDistributable` | `runReleaseDistributable` | Runs an application image with bundled JDK & resources - `run` | `runRelease` | Runs a non-packaged application `jar` using Gradle JDK - `package` | `packageRelease` | Packages an application image into a `` file - `packageDistributionForCurrentOS` | `packageReleaseDistributionForCurrentOS` | Packages an application image into a format compatible with current OS - `packageUberJarForCurrentOS` | `packageReleaseUberJarForCurrentOS` | Packages an application image into an uber (fat) JAR - `notarize` | `notarizeRelease` | Uploads a `` application image for notarization (macOS only) - `checkNotarizationStatus` | `checkReleaseNotarizationStatus` | Checks if notarization succeeded (macOS only) +Default task (w/o ProGuard)| Release task (w. ProGuard) |Description +---------------------------|----------------------------------|----------- +`createDistributable` | `createReleaseDistributable` |Creates an application image with bundled JDK & resources +`runDistributable` | `runReleaseDistributable` |Runs an application image with bundled JDK & resources +`run` | `runRelease` |Runs a non-packaged application `jar` using Gradle JDK +`package` | `packageRelease` |Packages an application image into a `` file +`packageForCurrentOS` | `packageReleaseForCurrentOS` |Packages an application image into a format compatible with current OS +`notarize` | `notarizeRelease` |Uploads a `` application image for notarization (macOS only) +`checkNotarizationStatus` | `checkReleaseNotarizationStatus` |Checks if notarization succeeded (macOS only) The default configuration adds a few ProGuard rules: * an application image is minified, i.e. non-used classes are removed; @@ -651,15 +650,3 @@ compose.desktop { } } ``` - -Joining to the uber JAR is disabled by default - ProGuard produces the corresponding JAR for every input JAR. -To enable it, set the following property via Gradle DSL: -``` -compose.desktop { - application { - buildTypes.release.proguard { - joinOutputJars.set(true) - } - } -} -```