mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Add desktop application Gradle plugin
For simplified packaging and running an app
This commit is contained in:
committed by
Alexey Tsvetkov
parent
2451d79d06
commit
563dc37c9b
@@ -1,5 +1,8 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal().mavenContent {
|
||||
includeModule("org.jetbrains.compose", "compose-desktop-application-gradle-plugin")
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
@@ -7,6 +10,7 @@ buildscript {
|
||||
|
||||
dependencies {
|
||||
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(kotlin("gradle-plugin", version = "1.4.0"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import org.jetbrains.compose.compose
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22)
|
||||
id("org.jetbrains.compose")
|
||||
java
|
||||
application
|
||||
id("org.jetbrains.compose.desktop.application")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -21,6 +21,13 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClassName = "example.imageviewer.MainKt"
|
||||
}
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "example.imageviewer.MainKt"
|
||||
|
||||
nativeExecutables {
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||
packageName = "ImageViewer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ subprojects {
|
||||
configureIfExists<JavaPluginExtension> {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +54,9 @@ subprojects {
|
||||
fun Project.configureGradlePlugin(config: GradlePluginConfigExtension) {
|
||||
// maven publication for plugin
|
||||
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
|
||||
pom {
|
||||
name.set(config.displayName)
|
||||
|
||||
20
gradle-plugins/compose-desktop-application/build.gradle.kts
Normal file
20
gradle-plugins/compose-desktop-application/build.gradle.kts
Normal file
@@ -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"))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jetbrains.compose
|
||||
|
||||
import org.gradle.api.plugins.ExtensionAware
|
||||
|
||||
abstract class ComposeExtension : ExtensionAware
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jetbrains.compose.desktop
|
||||
|
||||
import org.gradle.api.plugins.ExtensionAware
|
||||
|
||||
abstract class DesktopExtension : ExtensionAware
|
||||
@@ -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("")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()}\"" }
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ pluginManagement {
|
||||
}
|
||||
}
|
||||
|
||||
include(":compose")
|
||||
include(":compose")
|
||||
include(":compose-desktop-application")
|
||||
Reference in New Issue
Block a user