Browse Source

Implement packaging uber jar for current OS (#145)

pull/160/head
Alexey Tsvetkov 4 years ago committed by GitHub
parent
commit
d4e423e98f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 54
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  2. 37
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt
  3. 3
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt
  4. 9
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt
  5. 1
      gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle
  6. 1
      gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle
  7. 5
      tutorials/Native_distributions_and_local_execution/README.md

54
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 package org.jetbrains.compose.desktop.application.internal
import org.gradle.api.* 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.plugins.JavaPluginConvention
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider 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.Application
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
@ -51,6 +55,7 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
for (app in apps) { for (app in apps) {
configureRunTask(app) configureRunTask(app)
configurePackagingTasks(app) configurePackagingTasks(app)
configurePackageUberJarForCurrentOS(app)
} }
} }
@ -76,14 +81,11 @@ internal fun AbstractJPackageTask.configurePackagingTask(app: Application) {
} }
app.nativeDistributions.let { executables -> app.nativeDistributions.let { executables ->
packageName.set(provider { executables.packageName ?: project.name }) packageName.set(app._packageNameProvider(project))
packageDescription.set(provider { executables.description }) packageDescription.set(provider { executables.description })
packageCopyright.set(provider { executables.copyright }) packageCopyright.set(provider { executables.copyright })
packageVendor.set(provider { executables.vendor }) packageVendor.set(provider { executables.vendor })
packageVersion.set(provider { packageVersion.set(app._packageVersionInternal(project))
executables.version
?: project.version.toString().takeIf { it != "unspecified" }
})
} }
destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.id}") }) 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<Jar>(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 = private fun Application.javaHomeOrDefault(): String =
javaHome ?: System.getProperty("java.home") javaHome ?: System.getProperty("java.home")
@ -186,6 +221,15 @@ private inline fun <reified T : Task> TaskContainer.composeTask(
} }
} }
internal fun Application._packageNameProvider(project: Project): Provider<String> =
project.provider { nativeDistributions.packageName ?: project.name }
internal fun Application._packageVersionInternal(project: Project): Provider<String?> =
project.provider {
nativeDistributions.version
?: project.version.toString().takeIf { it != "unspecified" }
}
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
private fun taskName(action: String, app: Application, suffix: String? = null): String = private fun taskName(action: String, app: Application, suffix: String? = null): String =
listOf( listOf(

37
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.gradle.testkit.runner.TaskOutcome
import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.currentTarget
import org.jetbrains.compose.test.* import org.jetbrains.compose.test.*
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.jar.JarFile
class DesktopApplicationTest : GradlePluginTestBase() { class DesktopApplicationTest : GradlePluginTestBase() {
@Test @Test
@ -30,15 +33,15 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test @Test
fun packageJvm() = with(testProject(TestProjects.jvm)) { fun packageJvm() = with(testProject(TestProjects.jvm)) {
testPackage() testPackageNativeExecutables()
} }
@Test @Test
fun packageMpp() = with(testProject(TestProjects.mpp)) { fun packageMpp() = with(testProject(TestProjects.mpp)) {
testPackage() testPackageNativeExecutables()
} }
private fun TestProject.testPackage() { private fun TestProject.testPackageNativeExecutables() {
val result = gradle(":package").build() val result = gradle(":package").build()
val ext = when (currentOS) { val ext = when (currentOS) {
OS.Linux -> "deb" 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${ext.capitalize()}")?.outcome)
assertEquals(TaskOutcome.SUCCESS, result.task(":package")?.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")
}
}
}
}
} }

3
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProperties.kt

@ -1,7 +1,8 @@
package org.jetbrains.compose.test package org.jetbrains.compose.test
object TestProperties { object TestProperties {
val kotlinVersion: String = "1.4.0" // __KOTLIN_COMPOSE_VERSION__
val kotlinVersion: String = "1.4.20"
val composeVersion: String val composeVersion: String
get() = System.getProperty("compose.plugin.version")!! get() = System.getProperty("compose.plugin.version")!!

9
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/assertUtils.kt

@ -0,0 +1,9 @@
package org.jetbrains.compose.test
internal fun <T> Collection<T>.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(", ")}]")
}
}

1
gradle-plugins/compose/src/test/test-projects/application/jvm/build.gradle

@ -23,6 +23,7 @@ compose.desktop {
application { application {
mainClass = "MainKt" mainClass = "MainKt"
nativeDistributions { nativeDistributions {
version = "1.0"
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
} }
} }

1
gradle-plugins/compose/src/test/test-projects/application/mpp/build.gradle

@ -33,6 +33,7 @@ compose.desktop {
application { application {
mainClass = "MainKt" mainClass = "MainKt"
nativeDistributions { nativeDistributions {
version = "1.0"
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
} }
} }

5
tutorials/Native_distributions_and_local_execution/README.md

@ -28,7 +28,7 @@ plugins {
} }
dependencies { dependencies {
implementation(compose.desktop.all) implementation(compose.desktop.currentOS)
} }
compose.desktop { 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. 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, * `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. 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, * `run` is used to run an app locally. You need to define a `mainClass` — an fq-name of a class,
containing the `main` function. containing the `main` function.

Loading…
Cancel
Save