From d4e423e98f1ca70776f8b9f7ab10abead8970238 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Mon, 30 Nov 2020 13:28:27 +0300 Subject: [PATCH] Implement packaging uber jar for current OS (#145) --- .../internal/configureApplication.kt | 54 +++++++++++++++++-- .../compose/DesktopApplicationTest.kt | 37 +++++++++++-- .../jetbrains/compose/test/TestProperties.kt | 3 +- .../org/jetbrains/compose/test/assertUtils.kt | 9 ++++ .../application/jvm/build.gradle | 1 + .../application/mpp/build.gradle | 1 + .../README.md | 5 +- 7 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index c672b1f411..cc950d5847 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt @@ -1,10 +1,14 @@ package org.jetbrains.compose.desktop.application.internal import org.gradle.api.* +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileCollection import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.provider.Provider import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider +import org.gradle.jvm.tasks.Jar import org.jetbrains.compose.desktop.application.dsl.Application import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask @@ -51,6 +55,7 @@ internal fun Project.configurePackagingTasks(apps: Collection) { for (app in apps) { configureRunTask(app) configurePackagingTasks(app) + configurePackageUberJarForCurrentOS(app) } } @@ -76,14 +81,11 @@ internal fun AbstractJPackageTask.configurePackagingTask(app: Application) { } app.nativeDistributions.let { executables -> - packageName.set(provider { executables.packageName ?: project.name }) + packageName.set(app._packageNameProvider(project)) packageDescription.set(provider { executables.description }) packageCopyright.set(provider { executables.copyright }) packageVendor.set(provider { executables.vendor }) - packageVersion.set(provider { - executables.version - ?: project.version.toString().takeIf { it != "unspecified" } - }) + packageVersion.set(app._packageVersionInternal(project)) } destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.id}") }) @@ -167,6 +169,39 @@ private fun Project.configureRunTask(app: Application) { } } +private fun Project.configurePackageUberJarForCurrentOS(app: Application) = + project.tasks.composeTask(taskName("package", app, "uberJarForCurrentOS")) { + fun flattenJars(files: FileCollection): FileCollection = + project.files({ + files.map { if (it.isZipOrJar()) zipTree(it) else it } + }) + + // adding a null value will cause future invocations of `from` to throw an NPE + app.mainJar.orNull?.let { from(it) } + from(flattenJars(app._fromFiles)) + dependsOn(*app._dependenciesTaskNames.toTypedArray()) + + app._configurationSource?.let { configSource -> + dependsOn(configSource.jarTaskName) + from(flattenJars(configSource.runtimeClasspath)) + } + + app.mainClass?.let { manifest.attributes["Main-Class"] = it } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + archiveAppendix.set(currentTarget.id) + archiveBaseName.set(app._packageNameProvider(project)) + archiveVersion.set(app._packageVersionInternal(project)) + destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars")) + + doLast { + logger.lifecycle("The jar is written to ${archiveFile.get().asFile.canonicalPath}") + } + } + +private fun File.isZipOrJar() = + name.endsWith(".jar", ignoreCase = true) + || name.endsWith(".zip", ignoreCase = true) + private fun Application.javaHomeOrDefault(): String = javaHome ?: System.getProperty("java.home") @@ -186,6 +221,15 @@ private inline fun TaskContainer.composeTask( } } +internal fun Application._packageNameProvider(project: Project): Provider = + project.provider { nativeDistributions.packageName ?: project.name } + +internal fun Application._packageVersionInternal(project: Project): Provider = + project.provider { + nativeDistributions.version + ?: project.version.toString().takeIf { it != "unspecified" } + } + @OptIn(ExperimentalStdlibApi::class) private fun taskName(action: String, app: Application, suffix: String? = null): String = listOf( diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt index c66f90ff79..4ab0594092 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt @@ -3,9 +3,12 @@ package org.jetbrains.compose import org.gradle.testkit.runner.TaskOutcome import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.currentOS +import org.jetbrains.compose.desktop.application.internal.currentTarget import org.jetbrains.compose.test.* +import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import java.util.jar.JarFile class DesktopApplicationTest : GradlePluginTestBase() { @Test @@ -30,15 +33,15 @@ class DesktopApplicationTest : GradlePluginTestBase() { @Test fun packageJvm() = with(testProject(TestProjects.jvm)) { - testPackage() + testPackageNativeExecutables() } @Test fun packageMpp() = with(testProject(TestProjects.mpp)) { - testPackage() + testPackageNativeExecutables() } - private fun TestProject.testPackage() { + private fun TestProject.testPackageNativeExecutables() { val result = gradle(":package").build() val ext = when (currentOS) { OS.Linux -> "deb" @@ -50,4 +53,32 @@ class DesktopApplicationTest : GradlePluginTestBase() { assertEquals(TaskOutcome.SUCCESS, result.task(":package${ext.capitalize()}")?.outcome) assertEquals(TaskOutcome.SUCCESS, result.task(":package")?.outcome) } + + @Test + fun packageUberJarForCurrentOSJvm() = with(testProject(TestProjects.jvm)) { + testPackageNativeExecutables() + } + + @Test + fun packageUberJarForCurrentOSMpp() = with(testProject(TestProjects.mpp)) { + testPackageNativeExecutables() + } + + private fun TestProject.testPackageUberJarForCurrentOS() { + gradle(":packageUberJarForCurrentOS").build().let { result -> + assertEquals(TaskOutcome.SUCCESS, result.task(":packageUberJarForCurrentOS")?.outcome) + + val resultJarFile = file("build/compose/jars/simple-${currentTarget.id}-1.0.jar") + resultJarFile.checkExists() + + JarFile(resultJarFile).use { jar -> + val mainClass = jar.manifest.mainAttributes.getValue("Main-Class") + assertEquals("MainKt", mainClass, "Unexpected main class") + + jar.entries().toList().mapTo(HashSet()) { it.name }.apply { + checkContains("MainKt.class", "org/jetbrains/skiko/SkiaWindow.class") + } + } + } + } } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt index 261be42023..43ea5c4740 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt @@ -1,7 +1,8 @@ package org.jetbrains.compose.test object TestProperties { - val kotlinVersion: String = "1.4.0" + // __KOTLIN_COMPOSE_VERSION__ + val kotlinVersion: String = "1.4.20" val composeVersion: String get() = System.getProperty("compose.plugin.version")!! diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt new file mode 100644 index 0000000000..930955e392 --- /dev/null +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt @@ -0,0 +1,9 @@ +package org.jetbrains.compose.test + +internal fun Collection.checkContains(vararg elements: T) { + val expectedElements = elements.toMutableSet() + forEach { expectedElements.remove(it) } + if (expectedElements.isNotEmpty()) { + error("Expected elements are missing from the collection: [${expectedElements.joinToString(", ")}]") + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle index f2c1a05ef2..5169378f90 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle @@ -23,6 +23,7 @@ compose.desktop { application { mainClass = "MainKt" nativeDistributions { + version = "1.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } } diff --git a/gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle index 3086e46281..7c5ee02c76 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle @@ -33,6 +33,7 @@ compose.desktop { application { mainClass = "MainKt" nativeDistributions { + version = "1.0" targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) } } diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index 12e7d92bc6..b726d299bb 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -28,7 +28,7 @@ plugins { } dependencies { - implementation(compose.desktop.all) + implementation(compose.desktop.currentOS) } compose.desktop { @@ -49,6 +49,9 @@ so the formats can only be built using the specific OS (e.g. to build `.dmg` you Tasks that are not compatible with the current OS are skipped by default. * `package` is a [lifecycle](https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:lifecycle_tasks) task, aggregating all package tasks for an application. +* `packageUberJarForCurrentOS` is used to create a single jar file, containing all dependencies for current OS. +The task is available starting from the M2 release. +The task expects `compose.desktop.currentOS` to be used as a `compile`/`implementation`/`runtime` dependency. * `run` is used to run an app locally. You need to define a `mainClass` — an fq-name of a class, containing the `main` function.