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