Browse Source

Add desktop application Gradle plugin

For simplified packaging and running an app
pull/28/head
Alexey Tsvetkov 4 years ago committed by Alexey Tsvetkov
parent
commit
563dc37c9b
  1. 4
      examples/imageviewer/build.gradle.kts
  2. 17
      examples/imageviewer/desktop/build.gradle.kts
  3. 7
      gradle-plugins/build.gradle.kts
  4. 20
      gradle-plugins/compose-desktop-application/build.gradle.kts
  5. 10
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/ComposeBasePlugin.kt
  6. 5
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/ComposeExtension.kt
  7. 14
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/DesktopBasePlugin.kt
  8. 5
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt
  9. 225
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/ApplicationPlugin.kt
  10. 63
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/Application.kt
  11. 10
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ConfigurationSource.kt
  12. 48
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeExecutables.kt
  13. 52
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt
  14. 16
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt
  15. 27
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt
  16. 16
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt
  17. 15
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt
  18. 296
      gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  19. 3
      gradle-plugins/settings.gradle.kts

4
examples/imageviewer/build.gradle.kts

@ -1,5 +1,8 @@
buildscript { buildscript {
repositories { repositories {
mavenLocal().mavenContent {
includeModule("org.jetbrains.compose", "compose-desktop-application-gradle-plugin")
}
google() google()
jcenter() jcenter()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
@ -7,6 +10,7 @@ buildscript {
dependencies { dependencies {
classpath("org.jetbrains.compose:compose-gradle-plugin:0.1.0-dev109") classpath("org.jetbrains.compose:compose-gradle-plugin:0.1.0-dev109")
classpath("org.jetbrains.compose:compose-desktop-application-gradle-plugin:0.1.0-SNAPSHOT")
classpath("com.android.tools.build:gradle:4.0.1") classpath("com.android.tools.build:gradle:4.0.1")
classpath(kotlin("gradle-plugin", version = "1.4.0")) classpath(kotlin("gradle-plugin", version = "1.4.0"))
} }

17
examples/imageviewer/desktop/build.gradle.kts

@ -1,10 +1,10 @@
import org.jetbrains.compose.compose import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22) kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22)
id("org.jetbrains.compose") id("org.jetbrains.compose")
java id("org.jetbrains.compose.desktop.application")
application
} }
kotlin { kotlin {
@ -21,6 +21,13 @@ kotlin {
} }
} }
application { compose.desktop {
mainClassName = "example.imageviewer.MainKt" application {
} mainClass = "example.imageviewer.MainKt"
nativeExecutables {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "ImageViewer"
}
}
}

7
gradle-plugins/build.gradle.kts

@ -20,6 +20,9 @@ subprojects {
configureIfExists<JavaPluginExtension> { configureIfExists<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
} }
} }
@ -51,7 +54,9 @@ subprojects {
fun Project.configureGradlePlugin(config: GradlePluginConfigExtension) { fun Project.configureGradlePlugin(config: GradlePluginConfigExtension) {
// maven publication for plugin // maven publication for plugin
configureIfExists<PublishingExtension> { configureIfExists<PublishingExtension> {
publications.create<MavenPublication>("gradlePlugin") { // pluginMaven is a default publication created by java-gradle-plugin
// https://github.com/gradle/gradle/issues/10384
publications.create<MavenPublication>("pluginMaven") {
artifactId = config.artifactId artifactId = config.artifactId
pom { pom {
name.set(config.displayName) name.set(config.displayName)

20
gradle-plugins/compose-desktop-application/build.gradle.kts

@ -0,0 +1,20 @@
plugins {
kotlin("jvm")
id("com.gradle.plugin-publish")
id("java-gradle-plugin")
id("maven-publish")
}
gradlePluginConfig {
pluginId = "org.jetbrains.compose.desktop.application"
artifactId = "compose-desktop-application-gradle-plugin"
displayName = "Jetpack Compose Desktop Application Plugin"
description = "Plugin for creating native distributions and run configurations"
implementationClass = "org.jetbrains.compose.desktop.application.ApplicationPlugin"
}
dependencies {
compileOnly(gradleApi())
compileOnly(kotlin("gradle-plugin-api"))
compileOnly(kotlin("gradle-plugin"))
}

10
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/ComposeBasePlugin.kt

@ -0,0 +1,10 @@
package org.jetbrains.compose
import org.gradle.api.Plugin
import org.gradle.api.Project
open class ComposeBasePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("compose", ComposeExtension::class.java)
}
}

5
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/ComposeExtension.kt

@ -0,0 +1,5 @@
package org.jetbrains.compose
import org.gradle.api.plugins.ExtensionAware
abstract class ComposeExtension : ExtensionAware

14
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/DesktopBasePlugin.kt

@ -0,0 +1,14 @@
package org.jetbrains.compose.desktop
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.compose.ComposeBasePlugin
import org.jetbrains.compose.ComposeExtension
open class DesktopBasePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply(ComposeBasePlugin::class.java)
val composeExt = project.extensions.getByType(ComposeExtension::class.java)
composeExt.extensions.create("desktop", DesktopExtension::class.java)
}
}

5
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/DesktopExtension.kt

@ -0,0 +1,5 @@
package org.jetbrains.compose.desktop
import org.gradle.api.plugins.ExtensionAware
abstract class DesktopExtension : ExtensionAware

225
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/ApplicationPlugin.kt

@ -0,0 +1,225 @@
package org.jetbrains.compose.desktop.application
import org.gradle.api.*
import org.gradle.api.plugins.JavaPluginConvention
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.ComposeExtension
import org.jetbrains.compose.desktop.DesktopBasePlugin
import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.dsl.Application
import org.jetbrains.compose.desktop.application.dsl.ConfigurationSource
import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.provider
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import java.io.File
import java.util.*
private const val PLUGIN_ID = "org.jetbrains.compose.desktop.application"
// todo: fix windows
// todo: multiple launchers
// todo: file associations
// todo: icon
// todo: use workers
@Suppress("unused") // Gradle plugin entry point
open class ApplicationPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply(DesktopBasePlugin::class.java)
val composeExt = project.extensions.getByType(ComposeExtension::class.java)
val desktopExt = composeExt.extensions.getByType(DesktopExtension::class.java)
val mainApplication = project.objects.newInstance(Application::class.java, "main")
desktopExt.extensions.add("application", mainApplication)
project.plugins.withId("org.jetbrains.kotlin.jvm") {
val mainSourceSet = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.getByName("main")
mainApplication.from(mainSourceSet)
}
project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
project.configureFromMppPlugin(mainApplication)
}
project.afterEvaluate {
project.configurePackagingTasks(listOf(mainApplication))
}
}
}
internal fun Project.configureFromMppPlugin(mainApplication: Application) {
val kotlinExt = extensions.getByType(KotlinMultiplatformExtension::class.java)
var isJvmTargetConfigured = false
kotlinExt.targets.all { target ->
if (target.platformType == KotlinPlatformType.jvm) {
if (!isJvmTargetConfigured) {
mainApplication.from(target)
isJvmTargetConfigured = true
} else {
logger.error("w: Default configuration for '$PLUGIN_ID' is disabled: " +
"multiple Kotlin JVM targets definitions are detected. " +
"Specify, which target to use by using `compose.desktop.application.from(kotlinMppTarget)`")
mainApplication.disableDefaultConfiguration()
}
}
}
}
internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
for (app in apps) {
configureRunTask(app)
configurePackagingTasks(app)
}
}
internal fun Project.configurePackagingTasks(app: Application): TaskProvider<DefaultTask> {
val packageFormats = app.nativeExecutables.targetFormats.map { targetFormat ->
tasks.composeTask<AbstractJPackageTask>(
taskName("package", app, targetFormat.name),
args = listOf(targetFormat)
) {
configurePackagingTask(app)
}
}
return tasks.composeTask<DefaultTask>(taskName("package", app)) {
dependsOn(packageFormats)
}
}
internal fun AbstractJPackageTask.configurePackagingTask(app: Application) {
enabled = (currentOS == targetOS)
val targetPlatformSettings = when (targetOS) {
OS.Linux -> {
app.nativeExecutables.linux.also { linux ->
linuxShortcut.set(provider { linux.shortcut })
linuxAppCategory.set(provider { linux.appCategory })
linuxAppRelease.set(provider { linux.appRelease })
linuxDebMaintainer.set(provider { linux.debMaintainer })
linuxMenuGroup.set(provider { linux.menuGroup })
linuxPackageName.set(provider { linux.packageName })
linuxRpmLicenseType.set(provider { linux.rpmLicenseType })
}
}
OS.Windows -> {
app.nativeExecutables.windows.also { win ->
winConsole.set(provider { win.console })
winDirChooser.set(provider { win.dirChooser })
winPerUserInstall.set(provider { win.perUserInstall })
winShortcut.set(provider { win.shortcut })
winMenu.set(provider { win.menu })
winMenuGroup.set(provider { win.menuGroup })
winUpgradeUuid.set(provider { win.upgradeUuid })
}
}
OS.MacOS -> {
app.nativeExecutables.macOS.also { mac ->
macPackageName.set(provider { mac.packageName })
macPackageIdentifier.set(provider { mac.packageIdentifier })
macSign.set(provider { mac.signing.sign })
macSigningKeyUserName.set(provider { mac.signing.keyUserName })
macSigningKeychain.set(project.layout.file(provider { mac.signing.keychain }))
macBundleSigningPrefix.set(provider { mac.signing.bundlePrefix })
}
}
}
app.nativeExecutables.let { executables ->
packageName.set(provider { executables.packageName ?: project.name })
packageDescription.set(provider { executables.description })
packageCopyright.set(provider { executables.copyright })
packageVendor.set(provider { executables.vendor })
packageVersion.set(provider {
targetPlatformSettings.version
?: executables.version
?: project.version.toString().takeIf { it != "unspecified" }
})
}
destinationDir.set(app.nativeExecutables.outputBaseDir.map { it.dir("${app.name}/${targetFormat.id}") })
javaHome.set(provider { app.javaHomeOrDefault() })
launcherMainJar.set(app.mainJar.orNull)
app._fromFiles.forEach { files.from(it) }
dependsOn(*app._dependenciesTaskNames.toTypedArray())
when (val configSource = app._configurationSource) {
is ConfigurationSource.None -> {}
is ConfigurationSource.GradleSourceSet -> {
val sourceSet = configSource.sourceSet
dependsOn(sourceSet.jarTaskName)
launcherMainJar.set(app.mainJar.orElse(jarFromJarTaskByName(sourceSet.jarTaskName)))
files.from(sourceSet.runtimeClasspath)
}
is ConfigurationSource.KotlinMppTarget -> {
val target = configSource.target
dependsOn(target.artifactsTaskName)
launcherMainJar.set(app.mainJar.orElse(jarFromJarTaskByName(target.artifactsTaskName)))
files.from(project.configurations.named(target.runtimeElementsConfigurationName))
}
}
modules.set(provider { app.nativeExecutables.modules })
launcherMainClass.set(provider { app.mainClass })
launcherJvmArgs.set(provider { app.jvmArgs })
launcherArgs.set(provider { app.args })
}
private fun AbstractJPackageTask.jarFromJarTaskByName(jarTaskName: String) =
project.tasks.named(jarTaskName).map { (it as Jar).archiveFile.get() }
private fun Project.configureRunTask(app: Application) {
project.tasks.composeTask<JavaExec>(taskName("run", app)) {
mainClass.set(provider { app.mainClass })
executable = javaExecutable(app.javaHomeOrDefault())
jvmArgs = app.jvmArgs
args = app.args
val cp = objects.fileCollection()
cp.from(app.mainJar.orNull)
cp.from(app._fromFiles)
dependsOn(*app._dependenciesTaskNames.toTypedArray())
when (val configSource = app._configurationSource) {
is ConfigurationSource.None -> {}
is ConfigurationSource.GradleSourceSet -> {
val sourceSet = configSource.sourceSet
dependsOn(sourceSet.jarTaskName)
cp.from(sourceSet.runtimeClasspath)
}
is ConfigurationSource.KotlinMppTarget -> {
val target = configSource.target
dependsOn(target.artifactsTaskName)
cp.from(configurations.named(target.runtimeElementsConfigurationName))
}
}
classpath = cp
}
}
private fun Application.javaHomeOrDefault(): String =
javaHome ?: System.getProperty("java.home")
private fun javaExecutable(javaHome: String): String {
val executableName = if (currentOS == OS.Windows) "java.exe" else "java"
return File(javaHome).resolve("bin/$executableName").absolutePath
}
private inline fun <reified T : Task> TaskContainer.composeTask(
name: String,
args: List<Any> = emptyList(),
noinline configureFn: T.() -> Unit = {}
) = register(name, T::class.java, *args.toTypedArray()).apply {
configure {
it.group = "compose-desktop-application"
it.configureFn()
}
}
@OptIn(ExperimentalStdlibApi::class)
private fun taskName(action: String, app: Application, suffix: String? = null): String =
listOf(
action,
app.name.takeIf { it != "main" }?.capitalize(Locale.ROOT),
suffix?.capitalize(Locale.ROOT)
).filterNotNull().joinToString("")

63
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/Application.kt

@ -0,0 +1,63 @@
package org.jetbrains.compose.desktop.application.dsl
import org.gradle.api.Action
import org.gradle.api.Task
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.SourceSet
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
import java.util.*
import javax.inject.Inject
open class Application @Inject constructor(
@Suppress("unused")
val name: String,
objects: ObjectFactory
) {
internal var _configurationSource: ConfigurationSource = ConfigurationSource.None
internal val _fromFiles = objects.fileCollection()
internal val _dependenciesTaskNames = ArrayList<String>()
fun from(from: SourceSet) {
_configurationSource = ConfigurationSource.GradleSourceSet(from)
}
fun from(from: KotlinTarget) {
check(from is KotlinJvmTarget) { "Non JVM Kotlin MPP targets are not supported: ${from.javaClass.canonicalName} " +
"is not subtype of ${KotlinJvmTarget::class.java.canonicalName}" }
_configurationSource = ConfigurationSource.KotlinMppTarget(from)
}
fun disableDefaultConfiguration() {
_configurationSource = ConfigurationSource.None
}
fun fromFiles(vararg files: Any) {
_fromFiles.from(*files)
}
fun dependsOn(vararg tasks: String) {
_dependenciesTaskNames.addAll(tasks)
}
fun dependsOn(vararg tasks: Task) {
tasks.mapTo(_dependenciesTaskNames) { it.path }
}
var mainClass: String? = null
val mainJar: RegularFileProperty = objects.fileProperty()
var javaHome: String? = null
val args: MutableList<String> = ArrayList()
fun args(vararg args: String) {
this.args.addAll(args)
}
val jvmArgs: MutableList<String> = ArrayList()
fun jvmArgs(vararg jvmArgs: String) {
this.jvmArgs.addAll(jvmArgs)
}
val nativeExecutables: NativeExecutables = objects.newInstance(NativeExecutables::class.java)
fun nativeExecutables(fn: Action<NativeExecutables>) {
fn.execute(nativeExecutables)
}
}

10
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/ConfigurationSource.kt

@ -0,0 +1,10 @@
package org.jetbrains.compose.desktop.application.dsl
import org.gradle.api.tasks.SourceSet
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
internal sealed class ConfigurationSource {
object None : ConfigurationSource()
class GradleSourceSet(val sourceSet: SourceSet) : ConfigurationSource()
class KotlinMppTarget(val target: KotlinJvmTarget) : ConfigurationSource()
}

48
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/NativeExecutables.kt

@ -0,0 +1,48 @@
package org.jetbrains.compose.desktop.application.dsl
import org.gradle.api.Action
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.ProjectLayout
import org.gradle.api.model.ObjectFactory
import java.util.*
import javax.inject.Inject
open class NativeExecutables @Inject constructor(
objects: ObjectFactory,
layout: ProjectLayout
) {
var packageName: String? = null
var description: String? = null
var copyright: String? = null
var vendor: String? = null
var version: String? = null
val outputBaseDir: DirectoryProperty = objects.directoryProperty().apply {
set(layout.buildDirectory.dir("compose/binaries"))
}
var modules = arrayListOf("java.desktop")
fun modules(vararg modules: String) {
this.modules.addAll(modules.toList())
}
var targetFormats: Set<TargetFormat> = EnumSet.noneOf(TargetFormat::class.java)
fun targetFormats(vararg formats: TargetFormat) {
targetFormats = EnumSet.copyOf(formats.toList())
}
val linux: LinuxPlatformSettings = objects.newInstance(LinuxPlatformSettings::class.java)
fun linux(fn: Action<LinuxPlatformSettings>) {
fn.execute(linux)
}
val macOS: MacOSPlatformSettings = objects.newInstance(MacOSPlatformSettings::class.java)
fun macOS(fn: Action<MacOSPlatformSettings>) {
fn.execute(macOS)
}
val windows: WindowsPlatformSettings = objects.newInstance(WindowsPlatformSettings::class.java)
fun windows(fn: Action<WindowsPlatformSettings>) {
fn.execute(windows)
}
}

52
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt

@ -0,0 +1,52 @@
package org.jetbrains.compose.desktop.application.dsl
import org.gradle.api.Action
import java.io.File
abstract class PlatformSettings {
var version: String? = null
var installDir: String? = null
}
open class MacOSPlatformSettings : PlatformSettings() {
var packageIdentifier: String? = null
var packageName: String? = null
val signing: MacOSSigningSettings = MacOSSigningSettings()
private var isSignInitialized = false
fun signing(fn: Action<MacOSSigningSettings>) {
// enable sign if it the corresponding block is present in DSL
if (!isSignInitialized) {
isSignInitialized = true
signing.sign = true
}
fn.execute(signing)
}
}
open class MacOSSigningSettings {
var sign: Boolean = false
var keychain: File? = null
var bundlePrefix: String? = null
var keyUserName: String? = null
}
open class LinuxPlatformSettings : PlatformSettings() {
var shortcut: Boolean = false
var packageName: String? = null
var appRelease: String? = null
var appCategory: String? = null
var debMaintainer: String? = null
var menuGroup: String? = null
var rpmLicenseType: String? = null
}
open class WindowsPlatformSettings : PlatformSettings() {
var console: Boolean = false
var dirChooser: Boolean = false
var perUserInstall: Boolean = false
var shortcut: Boolean = false
var menu: Boolean = false
var menuGroup: String? = null
var upgradeUuid: String? = null
}

16
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt

@ -0,0 +1,16 @@
package org.jetbrains.compose.desktop.application.dsl
import org.jetbrains.compose.desktop.application.internal.OS
enum class TargetFormat(
internal val id: String,
internal val os: OS
) {
Deb("deb", OS.Linux),
Rpm("rpm", OS.Linux),
App("app-image", OS.MacOS),
Dmg("dmg", OS.MacOS),
Pkg("pkg", OS.MacOS),
Exe("exe", OS.Windows),
Msi("msi", OS.Windows)
}

27
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/cliArgUtils.kt

@ -0,0 +1,27 @@
package org.jetbrains.compose.desktop.application.internal
import org.gradle.api.provider.Provider
internal fun <T : Any?> MutableCollection<String>.cliArg(
name: String,
value: T?,
fn: (T) -> String = defaultToString()
) {
if (value is Boolean) {
if (value) add(name)
} else if (value != null) {
add(name)
add(fn(value))
}
}
internal fun <T : Any?> MutableCollection<String>.cliArg(
name: String,
value: Provider<T>,
fn: (T) -> String = defaultToString()
) {
cliArg(name, value.orNull, fn)
}
private fun <T : Any?> defaultToString(): (T) -> String =
{ "\"${it.toString()}\"" }

16
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt

@ -0,0 +1,16 @@
package org.jetbrains.compose.desktop.application.internal
import org.gradle.api.Task
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
@SuppressWarnings("UNCHECKED_CAST")
internal inline fun <reified T : Any> ObjectFactory.nullableProperty(): Property<T?> =
property(T::class.java) as Property<T?>
internal inline fun <reified T : Any> ObjectFactory.notNullProperty(): Property<T> =
property(T::class.java)
internal inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> =
project.provider(fn)

15
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt

@ -0,0 +1,15 @@
package org.jetbrains.compose.desktop.application.internal
internal enum class OS {
Linux, Windows, MacOS
}
internal val currentOS: OS by lazy {
val os = System.getProperty("os.name")
when {
os.equals("Mac OS X", ignoreCase = true) -> OS.MacOS
os.startsWith("Win", ignoreCase = true) -> OS.Windows
os.startsWith("Linux", ignoreCase = true) -> OS.Linux
else -> error("Unknown OS name: $os")
}
}

296
gradle-plugins/compose-desktop-application/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt

@ -0,0 +1,296 @@
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.internal.file.FileOperations
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.gradle.process.ExecOperations
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.cliArg
import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.notNullProperty
import org.jetbrains.compose.desktop.application.internal.nullableProperty
import java.io.File
import java.nio.file.Files
import javax.inject.Inject
abstract class AbstractJPackageTask @Inject constructor(
@get:Input
val targetFormat: TargetFormat,
private val execOperations: ExecOperations,
private val fileOperations: FileOperations,
objects: ObjectFactory,
providers: ProviderFactory
) : DefaultTask() {
@get:Input
internal val targetOS: OS
get() = targetFormat.os
@get:InputFiles
val files: ConfigurableFileCollection = objects.fileCollection()
@get:OutputDirectory
val destinationDir: DirectoryProperty = objects.directoryProperty()
@get:Internal
val javaHome: Property<String> = objects.notNullProperty<String>().apply {
set(providers.systemProperty("java.home"))
}
@get:Internal
val verbose: Property<Boolean> = objects.notNullProperty<Boolean>().apply {
val composeVerbose = providers
.gradleProperty("compose.desktop.verbose")
.map { "true".equals(it, ignoreCase = true) }
set(providers.provider { logger.isDebugEnabled }.orElse(composeVerbose))
}
@get:Input
@get:Optional
val installationPath: Property<String?> = objects.nullableProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val licenseFile: RegularFileProperty = objects.fileProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val iconFile: RegularFileProperty = objects.fileProperty()
@get:Input
val launcherMainClass: Property<String> = objects.notNullProperty()
@get:InputFile
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val launcherMainJar: RegularFileProperty = objects.fileProperty()
@get:Input
@get:Optional
val launcherArgs: ListProperty<String> = objects.listProperty(String::class.java)
@get:Input
@get:Optional
val launcherJvmArgs: ListProperty<String> = objects.listProperty(String::class.java)
@get:Input
val packageName: Property<String> = objects.notNullProperty()
@get:Input
@get:Optional
val packageDescription: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val packageCopyright: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val packageVendor: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val packageVersion: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxShortcut: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxPackageName: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxAppRelease: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxAppCategory: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxDebMaintainer: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxMenuGroup: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val linuxRpmLicenseType: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val macPackageIdentifier: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val macPackageName: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val macBundleSigningPrefix: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val macSign: Property<Boolean?> = objects.nullableProperty()
@get:InputFile
@get:Optional
val macSigningKeychain: RegularFileProperty = objects.fileProperty()
@get:Input
@get:Optional
val macSigningKeyUserName: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val winConsole: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val winDirChooser: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val winPerUserInstall: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val winShortcut: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val winMenu: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val winMenuGroup: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val winUpgradeUuid: Property<String?> = objects.nullableProperty()
@get:Input
val modules: ListProperty<String> = objects.listProperty(String::class.java)
@get:Input
@get:Optional
val freeArgs: ListProperty<String> = objects.listProperty(String::class.java)
private fun makeArgs(vararg inputDirs: File) = arrayListOf<String>().apply {
for (dir in inputDirs) {
cliArg("--input", dir.absolutePath)
}
cliArg("--type", targetFormat.id)
cliArg("--dest", destinationDir.asFile.get().absolutePath)
cliArg("--verbose", verbose)
cliArg("--install-dir", installationPath)
cliArg("--license-file", licenseFile.asFile.orNull?.absolutePath)
cliArg("--icon", iconFile.asFile.orNull?.absolutePath)
cliArg("--name", packageName)
cliArg("--description", packageDescription)
cliArg("--copyright", packageCopyright)
cliArg("--app-version", packageVersion)
cliArg("--vendor", packageVendor)
cliArg("--main-jar", launcherMainJar.asFile.get().name)
cliArg("--main-class", launcherMainClass)
launcherArgs.orNull?.forEach {
cliArg("--arguments", it)
}
launcherJvmArgs.orNull?.forEach {
cliArg("--java-options", it)
}
when (currentOS) {
OS.Linux -> {
cliArg("--linux-shortcut", linuxShortcut)
cliArg("--linux-package-name", linuxPackageName)
cliArg("--linux-app-release", linuxAppRelease)
cliArg("--linux-app-category", linuxAppCategory)
cliArg("--linux-deb-maintainer", linuxDebMaintainer)
cliArg("--linux-menu-group", linuxMenuGroup)
cliArg("--linux-rpm-license-type", linuxRpmLicenseType)
}
OS.MacOS -> {
cliArg("--mac-package-identifier", macPackageIdentifier)
cliArg("--mac-package-name", macPackageName)
cliArg("--mac-bundle-signing-prefix", macBundleSigningPrefix)
cliArg("--mac-sign", macSign)
cliArg("--mac-signing-keychain", macSigningKeychain.asFile.orNull)
cliArg("--mac-signing-key-user-name", macSigningKeyUserName)
}
OS.Windows -> {
cliArg("--win-console", winConsole)
cliArg("--win-dir-chooser", winDirChooser)
cliArg("--win-per-user-install", winPerUserInstall)
cliArg("--win-shortcut", winShortcut)
cliArg("--win-menu", winMenu)
cliArg("--win-menu-group", winMenuGroup)
cliArg("--win-upgrade-uuid", winUpgradeUuid)
}
}
modules.get().forEach { m ->
cliArg("--add-modules", m)
}
freeArgs.orNull?.forEach { add(it) }
}
@TaskAction
fun run() {
val javaHomePath = javaHome.get()
val executableName = if (currentOS == OS.Windows) "jpackage.exe" else "jpackage"
val jpackage = File(javaHomePath).resolve("bin/$executableName")
check(jpackage.isFile) {
"Invalid JDK: $jpackage is not a file! \n" +
"Ensure JAVA_HOME or buildSettings.javaHome for '${packageName.get()}' app package is set to JDK 14 or newer"
}
fileOperations.delete(destinationDir)
val tmpDir = Files.createTempDirectory("compose-package").toFile().apply {
deleteOnExit()
}
try {
val args = makeArgs(tmpDir)
val sourceFile = launcherMainJar.get().asFile
val targetFile = tmpDir.resolve(sourceFile.name)
sourceFile.copyTo(targetFile)
val myFiles = files
fileOperations.copy {
it.from(myFiles)
it.into(tmpDir)
}
val composeBuildDir = project.buildDir.resolve("compose").apply { mkdirs() }
val argsFile = composeBuildDir.resolve("${name}.args.txt")
argsFile.writeText(args.joinToString("\n"))
execOperations.exec {
it.executable = jpackage.absolutePath
it.setArgs(listOf("@${argsFile.absolutePath}"))
}.assertNormalExitValue()
} finally {
tmpDir.deleteRecursively()
}
}
}

3
gradle-plugins/settings.gradle.kts

@ -5,4 +5,5 @@ pluginManagement {
} }
} }
include(":compose") include(":compose")
include(":compose-desktop-application")
Loading…
Cancel
Save