Alexey Tsvetkov
4 years ago
committed by
Alexey Tsvetkov
19 changed files with 846 additions and 7 deletions
@ -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() |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue