Browse Source

Add ability to create/run distributable app

pull/224/head
Alexey Tsvetkov 4 years ago committed by Alexey Tsvetkov
parent
commit
3aff8f47e3
  1. 3
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/TargetFormat.kt
  2. 98
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  3. 10
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/osUtils.kt
  4. 117
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  5. 91
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt
  6. 53
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt
  7. 13
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt
  8. 5
      tutorials/Native_distributions_and_local_execution/README.md

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

@ -19,6 +19,9 @@ enum class TargetFormat(
internal fun isCompatibleWith(targetOS: OS): Boolean = targetOS in compatibleOSs internal fun isCompatibleWith(targetOS: OS): Boolean = targetOS in compatibleOSs
val outputDirName: String
get() = if (this == AppImage) "app" else id
val fileExt: String val fileExt: String
get() { get() {
check(this != AppImage) { "$this cannot have a file extension" } check(this != AppImage) { "$this cannot have a file extension" }

98
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt

@ -5,13 +5,12 @@ import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.FileCollection import org.gradle.api.file.FileCollection
import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.*
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar import org.gradle.jvm.tasks.Jar
import org.jetbrains.compose.desktop.application.dsl.Application import org.jetbrains.compose.desktop.application.dsl.Application
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
import org.jetbrains.compose.desktop.application.tasks.AbstractRunDistributableTask
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import java.io.File import java.io.File
@ -53,32 +52,47 @@ internal fun Project.configureFromMppPlugin(mainApplication: Application) {
internal fun Project.configurePackagingTasks(apps: Collection<Application>) { internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
for (app in apps) { for (app in apps) {
configureRunTask(app) val run = project.tasks.composeTask<JavaExec>(taskName("run", app)) {
configurePackagingTasks(app) configureRunTask(app)
configurePackageUberJarForCurrentOS(app) }
}
}
internal fun Project.configurePackagingTasks(app: Application): TaskProvider<DefaultTask> { val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat ->
val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> tasks.composeTask<AbstractJPackageTask>(
tasks.composeTask<AbstractJPackageTask>( taskName("package", app, targetFormat.name),
taskName("package", app, targetFormat.name), args = listOf(targetFormat)
args = listOf(targetFormat) ) {
configurePackagingTask(app)
}
}
val packageAll = tasks.composeTask<DefaultTask>(taskName("package", app)) {
dependsOn(packageFormats)
}
val packageUberJarForCurrentOS = project.tasks.composeTask<Jar>(taskName("package", app, "uberJarForCurrentOS")) {
configurePackageUberJarForCurrentOS(app)
}
val createDistributable = tasks.composeTask<AbstractJPackageTask>(
taskName("createDistributable", app),
args = listOf(TargetFormat.AppImage)
) { ) {
configurePackagingTask(app) configurePackagingTask(app)
} }
}
return tasks.composeTask<DefaultTask>(taskName("package", app)) { val runDistributable = project.tasks.composeTask<AbstractRunDistributableTask>(
dependsOn(packageFormats) taskName("runDistributable", app),
args = listOf(createDistributable)
)
} }
} }
internal fun AbstractJPackageTask.configurePackagingTask(app: Application) { internal fun AbstractJPackageTask.configurePackagingTask(
app: Application
) {
enabled = targetFormat.isCompatibleWithCurrentOS enabled = targetFormat.isCompatibleWithCurrentOS
if (targetFormat != TargetFormat.AppImage) { configurePlatformSettings(app)
configurePlatformSettings(app)
}
app.nativeDistributions.let { executables -> app.nativeDistributions.let { executables ->
packageName.set(app._packageNameProvider(project)) packageName.set(app._packageNameProvider(project))
@ -88,7 +102,7 @@ internal fun AbstractJPackageTask.configurePackagingTask(app: Application) {
packageVersion.set(app._packageVersionInternal(project)) packageVersion.set(app._packageVersionInternal(project))
} }
destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.id}") }) destinationDir.set(app.nativeDistributions.outputBaseDir.map { it.dir("${app.name}/${targetFormat.outputDirName}") })
javaHome.set(provider { app.javaHomeOrDefault() }) javaHome.set(provider { app.javaHomeOrDefault() })
launcherMainJar.set(app.mainJar.orNull) launcherMainJar.set(app.mainJar.orNull)
@ -147,33 +161,30 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) {
} }
} }
private fun Project.configureRunTask(app: Application) { private fun JavaExec.configureRunTask(app: Application) {
project.tasks.composeTask<JavaExec>(taskName("run", app)) { mainClass.set(provider { app.mainClass })
mainClass.set(provider { app.mainClass }) executable(javaExecutable(app.javaHomeOrDefault()))
executable(javaExecutable(app.javaHomeOrDefault())) jvmArgs = app.jvmArgs
jvmArgs = app.jvmArgs args = app.args
args = app.args
val cp = objects.fileCollection() val cp = project.objects.fileCollection()
// adding a null value will cause future invocations of `from` to throw an NPE // adding a null value will cause future invocations of `from` to throw an NPE
app.mainJar.orNull?.let { cp.from(it) } app.mainJar.orNull?.let { cp.from(it) }
cp.from(app._fromFiles) cp.from(app._fromFiles)
dependsOn(*app._dependenciesTaskNames.toTypedArray()) dependsOn(*app._dependenciesTaskNames.toTypedArray())
app._configurationSource?.let { configSource ->
dependsOn(configSource.jarTaskName)
cp.from(configSource.runtimeClasspath)
}
classpath = cp app._configurationSource?.let { configSource ->
dependsOn(configSource.jarTaskName)
cp.from(configSource.runtimeClasspath)
} }
classpath = cp
} }
private fun Project.configurePackageUberJarForCurrentOS(app: Application) = private fun Jar.configurePackageUberJarForCurrentOS(app: Application) {
project.tasks.composeTask<Jar>(taskName("package", app, "uberJarForCurrentOS")) {
fun flattenJars(files: FileCollection): FileCollection = fun flattenJars(files: FileCollection): FileCollection =
project.files({ project.files({
files.map { if (it.isZipOrJar()) zipTree(it) else it } files.map { if (it.isZipOrJar()) project.zipTree(it) else it }
}) })
// adding a null value will cause future invocations of `from` to throw an NPE // adding a null value will cause future invocations of `from` to throw an NPE
@ -205,11 +216,6 @@ private fun File.isZipOrJar() =
private fun Application.javaHomeOrDefault(): String = private fun Application.javaHomeOrDefault(): String =
javaHome ?: System.getProperty("java.home") 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( private inline fun <reified T : Task> TaskContainer.composeTask(
name: String, name: String,
args: List<Any> = emptyList(), args: List<Any> = emptyList(),

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

@ -1,5 +1,7 @@
package org.jetbrains.compose.desktop.application.internal package org.jetbrains.compose.desktop.application.internal
import java.io.File
internal enum class OS(val id: String) { internal enum class OS(val id: String) {
Linux("linux"), Linux("linux"),
Windows("windows"), Windows("windows"),
@ -37,4 +39,10 @@ internal val currentOS: OS by lazy {
os.startsWith("Linux", ignoreCase = true) -> OS.Linux os.startsWith("Linux", ignoreCase = true) -> OS.Linux
else -> error("Unknown OS name: $os") else -> error("Unknown OS name: $os")
} }
} }
internal fun executableName(nameWithoutExtension: String): String =
if (currentOS == OS.Windows) "$nameWithoutExtension.exe" else nameWithoutExtension
internal fun javaExecutable(javaHome: String): String =
File(javaHome).resolve("bin/${executableName("java")}").absolutePath

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

@ -1,56 +1,25 @@
package org.jetbrains.compose.desktop.application.tasks package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty 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.ListProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional import org.gradle.process.ExecResult
import org.gradle.process.ExecOperations
import org.gradle.process.ExecSpec import org.gradle.process.ExecSpec
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.*
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.io.File
import java.nio.file.Files
import javax.inject.Inject import javax.inject.Inject
abstract class AbstractJPackageTask @Inject constructor( abstract class AbstractJPackageTask @Inject constructor(
@get:Input @get:Input
val targetFormat: TargetFormat, val targetFormat: TargetFormat,
private val execOperations: ExecOperations, ) : AbstractJvmToolOperationTask("jpackage") {
private val fileOperations: FileOperations,
objects: ObjectFactory,
providers: ProviderFactory
) : DefaultTask() {
@get:InputFiles @get:InputFiles
val files: ConfigurableFileCollection = objects.fileCollection() 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:InputDirectory @get:InputDirectory
@get:Optional @get:Optional
/** @see internal/wixToolset.kt */ /** @see internal/wixToolset.kt */
@ -187,16 +156,10 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Input @get:Input
val modules: ListProperty<String> = objects.listProperty(String::class.java) val modules: ListProperty<String> = objects.listProperty(String::class.java)
@get:Input override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply {
@get:Optional cliArg("--input", tmpDir)
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)
}
cliArg("--type", targetFormat.id) cliArg("--type", targetFormat.id)
cliArg("--dest", destinationDir.asFile.get()) cliArg("--dest", destinationDir.asFile.get())
cliArg("--verbose", verbose) cliArg("--verbose", verbose)
@ -251,59 +214,28 @@ abstract class AbstractJPackageTask @Inject constructor(
modules.get().forEach { m -> modules.get().forEach { m ->
cliArg("--add-modules", m) cliArg("--add-modules", m)
} }
freeArgs.orNull?.forEach { add(it) }
} }
@TaskAction override fun prepareWorkingDir(tmpDir: File) {
fun run() { super.prepareWorkingDir(tmpDir)
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) launcherMainJar.asFile.orNull?.let { sourceFile ->
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) val targetFile = tmpDir.resolve(sourceFile.name)
sourceFile.copyTo(targetFile) sourceFile.copyTo(targetFile)
}
val myFiles = files val myFiles = files
fileOperations.copy { fileOperations.copy {
it.from(myFiles) it.from(myFiles)
it.into(tmpDir) 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 { exec ->
configureWixPathIfNeeded(exec)
exec.executable = jpackage.absolutePath
exec.setArgs(listOf("@${argsFile.absolutePath}"))
}.assertNormalExitValue()
val destinationDirFile = destinationDir.asFile.get()
val finalLocation = when (targetFormat) {
TargetFormat.AppImage -> destinationDirFile
else -> destinationDirFile.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) }
}
logger.lifecycle("The distribution is written to ${finalLocation.canonicalPath}")
} finally {
tmpDir.deleteRecursively()
} }
} }
override fun configureExec(exec: ExecSpec) {
super.configureExec(exec)
configureWixPathIfNeeded(exec)
}
private fun configureWixPathIfNeeded(exec: ExecSpec) { private fun configureWixPathIfNeeded(exec: ExecSpec) {
if (currentOS == OS.Windows) { if (currentOS == OS.Windows) {
val wixDir = wixToolsetDir.asFile.orNull ?: return val wixDir = wixToolsetDir.asFile.orNull ?: return
@ -312,4 +244,15 @@ abstract class AbstractJPackageTask @Inject constructor(
exec.environment("PATH", "$wixPath;$path") exec.environment("PATH", "$wixPath;$path")
} }
} }
override fun checkResult(result: ExecResult) {
super.checkResult(result)
val destinationDirFile = destinationDir.asFile.get()
val finalLocation = when (targetFormat) {
TargetFormat.AppImage -> destinationDirFile
else -> destinationDirFile.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) }
}
logger.lifecycle("The distribution is written to ${finalLocation.canonicalPath}")
}
} }

91
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt

@ -0,0 +1,91 @@
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
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.process.ExecOperations
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
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 java.io.File
import java.nio.file.Files
import javax.inject.Inject
abstract class AbstractJvmToolOperationTask(private val toolName: String) : DefaultTask() {
@get:Inject
protected abstract val objects: ObjectFactory
@get:Inject
protected abstract val providers: ProviderFactory
@get:Inject
protected abstract val execOperations: ExecOperations
@get:Inject
protected abstract val fileOperations: FileOperations
@get:Input
@get:Optional
val freeArgs: ListProperty<String> = objects.listProperty(String::class.java)
@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))
}
protected open fun makeArgs(tmpDir: File): MutableList<String> = arrayListOf<String>().apply {
freeArgs.orNull?.forEach { add(it) }
}
protected open fun prepareWorkingDir(tmpDir: File) {}
protected open fun configureExec(exec: ExecSpec) {}
protected open fun checkResult(result: ExecResult) {
result.assertNormalExitValue()
}
@TaskAction
fun run() {
val javaHomePath = javaHome.get()
val jtool = File(javaHomePath).resolve("bin/${executableName(toolName)}")
check(jtool.isFile) {
"Invalid JDK: $jtool is not a file! \n" +
"Ensure JAVA_HOME or buildSettings.javaHome is set to JDK 14 or newer"
}
fileOperations.delete(destinationDir)
val tmpDir = Files.createTempDirectory("compose-${toolName}").toFile().apply {
deleteOnExit()
}
try {
val args = makeArgs(tmpDir)
prepareWorkingDir(tmpDir)
val composeBuildDir = project.buildDir.resolve("compose").apply { mkdirs() }
val argsFile = composeBuildDir.resolve("${name}.args.txt")
argsFile.writeText(args.joinToString("\n"))
execOperations.exec { exec ->
configureExec(exec)
exec.executable = jtool.absolutePath
exec.setArgs(listOf("@${argsFile.absolutePath}"))
}.also { checkResult(it) }
} finally {
tmpDir.deleteRecursively()
}
}
}

53
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt

@ -0,0 +1,53 @@
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.process.ExecOperations
import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS
import org.jetbrains.compose.desktop.application.internal.executableName
import org.jetbrains.compose.desktop.application.internal.ioFile
import javax.inject.Inject
// Custom task is used instead of Exec, because Exec does not support
// lazy configuration yet. Lazy configuration is needed to
// calculate appImageDir after the evaluation of createApplicationImage
abstract class AbstractRunDistributableTask @Inject constructor(
createApplicationImage: TaskProvider<AbstractJPackageTask>,
private val execOperations: ExecOperations
) : DefaultTask() {
@get:InputDirectory
internal val appImageRootDir: Provider<Directory> = createApplicationImage.flatMap { it.destinationDir }
@get:Input
internal val packageName: Provider<String> = createApplicationImage.flatMap { it.packageName }
@TaskAction
fun run() {
val appDir = appImageRootDir.get().let { appImageRoot ->
val files = appImageRoot.asFile.listFiles()
if (files == null || files.isEmpty()) {
error("Could not find application image: $appImageRoot is empty!")
} else if (files.size > 1) {
error("Could not find application image: $appImageRoot contains multiple children [${files.joinToString(", ")}]")
} else files.single()
}
val appExecutableName = executableName(packageName.get())
val (workingDir, executable) = when (currentOS) {
OS.Linux -> appDir to "bin/$appExecutableName"
OS.Windows -> appDir to appExecutableName
OS.MacOS -> appDir.resolve("Contents") to "MacOS/$appExecutableName"
}
execOperations.exec { spec ->
spec.workingDir(workingDir)
spec.executable(executable)
}.assertNormalExitValue()
}
}

13
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/DesktopApplicationTest.kt

@ -19,11 +19,20 @@ class DesktopApplicationTest : GradlePluginTestBase() {
tasks.getByName("run").doFirst { tasks.getByName("run").doFirst {
throw new StopExecutionException("Skip run task") throw new StopExecutionException("Skip run task")
} }
tasks.getByName("runDistributable").doFirst {
throw new StopExecutionException("Skip runDistributable task")
}
} }
""".trimIndent() """.trimIndent()
} }
val result = gradle("run").build() gradle("run").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":run")?.outcome) assertEquals(TaskOutcome.SUCCESS, result.task(":run")?.outcome)
}
gradle("runDistributable").build().let { result ->
assertEquals(TaskOutcome.SUCCESS, result.task(":createDistributable")!!.outcome)
assertEquals(TaskOutcome.SUCCESS, result.task(":runDistributable")?.outcome)
}
} }
@Test @Test

5
tutorials/Native_distributions_and_local_execution/README.md

@ -54,6 +54,11 @@ The task is available starting from the M2 release.
The task expects `compose.desktop.currentOS` to be used as a `compile`/`implementation`/`runtime` dependency. The task expects `compose.desktop.currentOS` to be used as a `compile`/`implementation`/`runtime` dependency.
* `run` is used to run an app locally. You need to define a `mainClass` — an fq-name of a class, * `run` is used to run an app locally. You need to define a `mainClass` — an fq-name of a class,
containing the `main` function. containing the `main` function.
Note, that `run` starts a non-packaged JVM application with full runtime.
This is faster and easier to debug, than creating a compact binary image with minified runtime.
To run a final binary image, use `runDistributable` instead.
* `createDistributable` is used to create a prepackaged application image a final application image without creating an installer.
* `runDistributable` is used to run a prepackaged application image.
Note, that the tasks are created only if the `application` block/property is used in a script. Note, that the tasks are created only if the `application` block/property is used in a script.

Loading…
Cancel
Save