Browse Source

Extract runtime image creation into separate task

This change speeds up incremental build of
an application image.
For example, for one (relatively small) tested application
the overall time spent on creating binary images in incremental case
(after one file change) went from ~5 seconds down to 1.4 seconds
(non-rigorous benchmark using a relatively fast 8-core 2.3Ghz i9 MBP).
pull/224/head
Alexey Tsvetkov 4 years ago committed by Alexey Tsvetkov
parent
commit
7e243b5cbd
  1. 9
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/RuntimeCompressionLevel.kt
  2. 23
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  3. 3
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt
  4. 11
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt
  5. 48
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt
  6. 37
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  7. 21
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt
  8. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt

9
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/RuntimeCompressionLevel.kt

@ -0,0 +1,9 @@
package org.jetbrains.compose.desktop.application.dsl
internal enum class RuntimeCompressionLevel(internal val id: Int) {
// For ID values see the docs on "--compress" https://docs.oracle.com/javase/9/tools/jlink.htm
NO_COMPRESSION(0),
CONSTANT_STRING_SHARING(1),
ZIP(2)
}

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

@ -9,6 +9,7 @@ import org.gradle.api.tasks.*
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.AbstractJLinkTask
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.compose.desktop.application.tasks.AbstractRunDistributableTask
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
@ -56,12 +57,20 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
configureRunTask(app) configureRunTask(app)
} }
val createRuntimeImage = tasks.composeTask<AbstractJLinkTask>(
taskName("createRuntimeImage", app)
) {
javaHome.set(provider { app.javaHomeOrDefault() })
modules.set(provider { app.nativeDistributions.modules })
destinationDir.set(project.layout.buildDirectory.dir("compose/tmp/${app.name}/runtime"))
}
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) configurePackagingTask(app, createRuntimeImage)
} }
} }
@ -77,7 +86,7 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
taskName("createDistributable", app), taskName("createDistributable", app),
args = listOf(TargetFormat.AppImage) args = listOf(TargetFormat.AppImage)
) { ) {
configurePackagingTask(app) configurePackagingTask(app, createRuntimeImage)
} }
val runDistributable = project.tasks.composeTask<AbstractRunDistributableTask>( val runDistributable = project.tasks.composeTask<AbstractRunDistributableTask>(
@ -88,10 +97,15 @@ internal fun Project.configurePackagingTasks(apps: Collection<Application>) {
} }
internal fun AbstractJPackageTask.configurePackagingTask( internal fun AbstractJPackageTask.configurePackagingTask(
app: Application app: Application,
createRuntimeImage: TaskProvider<AbstractJLinkTask>
) { ) {
enabled = targetFormat.isCompatibleWithCurrentOS enabled = targetFormat.isCompatibleWithCurrentOS
val runtimeImageDir = createRuntimeImage.flatMap { it.destinationDir }
dependsOn(createRuntimeImage)
runtimeImage.set(runtimeImageDir)
configurePlatformSettings(app) configurePlatformSettings(app)
app.nativeDistributions.let { executables -> app.nativeDistributions.let { executables ->
@ -115,7 +129,6 @@ internal fun AbstractJPackageTask.configurePackagingTask(
launcherMainJar.set(app.mainJar.orElse(configSource.jarTask(project).flatMap { it.archiveFile })) launcherMainJar.set(app.mainJar.orElse(configSource.jarTask(project).flatMap { it.archiveFile }))
} }
modules.set(provider { app.nativeDistributions.modules })
launcherMainClass.set(provider { app.mainClass }) launcherMainClass.set(provider { app.mainClass })
launcherJvmArgs.set(provider { app.jvmArgs }) launcherJvmArgs.set(provider { app.jvmArgs })
launcherArgs.set(provider { app.args }) launcherArgs.set(provider { app.args })
@ -205,7 +218,7 @@ private fun Jar.configurePackageUberJarForCurrentOS(app: Application) {
destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars")) destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars"))
doLast { doLast {
logger.lifecycle("The jar is written to ${archiveFile.get().asFile.canonicalPath}") logger.lifecycle("The jar is written to ${archiveFile.ioFile.canonicalPath}")
} }
} }

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

@ -12,5 +12,8 @@ internal inline fun <reified T : Any> ObjectFactory.nullableProperty(): Property
internal inline fun <reified T : Any> ObjectFactory.notNullProperty(): Property<T> = internal inline fun <reified T : Any> ObjectFactory.notNullProperty(): Property<T> =
property(T::class.java) property(T::class.java)
internal inline fun <reified T : Any> ObjectFactory.notNullProperty(defaultValue: T): Property<T> =
property(T::class.java).value(defaultValue)
internal inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> = internal inline fun <reified T> Task.provider(noinline fn: () -> T): Provider<T> =
project.provider(fn) project.provider(fn)

11
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt

@ -0,0 +1,11 @@
package org.jetbrains.compose.desktop.application.internal
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider
import java.io.File
internal val <T : FileSystemLocation> Provider<T>.ioFile: File
get() = get().asFile
internal val <T : FileSystemLocation> Provider<T>.ioFileOrNull: File?
get() = orNull?.asFile

48
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt

@ -0,0 +1,48 @@
package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.dsl.RuntimeCompressionLevel
import org.jetbrains.compose.desktop.application.internal.cliArg
import org.jetbrains.compose.desktop.application.internal.notNullProperty
import org.jetbrains.compose.desktop.application.internal.nullableProperty
import java.io.File
// todo: public DSL
// todo: deduplicate if multiple runtimes are created
abstract class AbstractJLinkTask : AbstractJvmToolOperationTask("jlink") {
@get:Input
val modules: ListProperty<String> = objects.listProperty(String::class.java)
@get:Input
internal val stripDebug: Property<Boolean> = objects.notNullProperty(true)
@get:Input
internal val noHeaderFiles: Property<Boolean> = objects.notNullProperty(true)
@get:Input
internal val noManPages: Property<Boolean> = objects.notNullProperty(true)
@get:Input
internal val stripNativeCommands: Property<Boolean> = objects.notNullProperty(true)
@get:Input
@get:Optional
internal val compressionLevel: Property<RuntimeCompressionLevel?> = objects.nullableProperty()
override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply {
modules.get().forEach { m ->
cliArg("--add-modules", m)
}
cliArg("--strip-debug", stripDebug)
cliArg("--no-header-files", noHeaderFiles)
cliArg("--no-man-pages", noManPages)
cliArg("--strip-native-commands", stripNativeCommands)
cliArg("--compress", compressionLevel.orNull?.id )
cliArg("--output", destinationDir)
}
}

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

@ -19,9 +19,6 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Input @get:Input
val targetFormat: TargetFormat, val targetFormat: TargetFormat,
) : AbstractJvmToolOperationTask("jpackage") { ) : AbstractJvmToolOperationTask("jpackage") {
@get:OutputDirectory
val destinationDir: DirectoryProperty = objects.directoryProperty()
@get:InputFiles @get:InputFiles
val files: ConfigurableFileCollection = objects.fileCollection() val files: ConfigurableFileCollection = objects.fileCollection()
@ -158,19 +155,20 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Optional @get:Optional
val winUpgradeUuid: Property<String?> = objects.nullableProperty() val winUpgradeUuid: Property<String?> = objects.nullableProperty()
@get:Input @get:InputDirectory
val modules: ListProperty<String> = objects.listProperty(String::class.java) @get:Optional
val runtimeImage: DirectoryProperty = objects.directoryProperty()
override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply { override fun makeArgs(tmpDir: File): MutableList<String> = super.makeArgs(tmpDir).apply {
cliArg("--input", tmpDir) cliArg("--input", tmpDir)
cliArg("--type", targetFormat.id) cliArg("--type", targetFormat.id)
cliArg("--dest", destinationDir.asFile.get()) cliArg("--dest", destinationDir.ioFile)
cliArg("--verbose", verbose) cliArg("--verbose", verbose)
cliArg("--install-dir", installationPath) cliArg("--install-dir", installationPath)
cliArg("--license-file", licenseFile.asFile.orNull) cliArg("--license-file", licenseFile.ioFileOrNull)
cliArg("--icon", iconFile.asFile.orNull) cliArg("--icon", iconFile.ioFileOrNull)
cliArg("--name", packageName) cliArg("--name", packageName)
cliArg("--description", packageDescription) cliArg("--description", packageDescription)
@ -178,7 +176,7 @@ abstract class AbstractJPackageTask @Inject constructor(
cliArg("--app-version", packageVersion) cliArg("--app-version", packageVersion)
cliArg("--vendor", packageVendor) cliArg("--vendor", packageVendor)
cliArg("--main-jar", launcherMainJar.asFile.get().name) cliArg("--main-jar", launcherMainJar.ioFile.name)
cliArg("--main-class", launcherMainClass) cliArg("--main-class", launcherMainClass)
launcherArgs.orNull?.forEach { launcherArgs.orNull?.forEach {
cliArg("--arguments", it) cliArg("--arguments", it)
@ -202,7 +200,7 @@ abstract class AbstractJPackageTask @Inject constructor(
cliArg("--mac-package-name", macPackageName) cliArg("--mac-package-name", macPackageName)
cliArg("--mac-bundle-signing-prefix", macBundleSigningPrefix) cliArg("--mac-bundle-signing-prefix", macBundleSigningPrefix)
cliArg("--mac-sign", macSign) cliArg("--mac-sign", macSign)
cliArg("--mac-signing-keychain", macSigningKeychain.asFile.orNull) cliArg("--mac-signing-keychain", macSigningKeychain.ioFileOrNull)
cliArg("--mac-signing-key-user-name", macSigningKeyUserName) cliArg("--mac-signing-key-user-name", macSigningKeyUserName)
} }
OS.Windows -> { OS.Windows -> {
@ -216,13 +214,11 @@ abstract class AbstractJPackageTask @Inject constructor(
} }
} }
modules.get().forEach { m -> cliArg("--runtime-image", runtimeImage)
cliArg("--add-modules", m)
}
} }
override fun prepareWorkingDir(inputChanges: InputChanges) { override fun prepareWorkingDir(inputChanges: InputChanges) {
fileOperations.delete(destinationDir) val workingDir = workingDir.ioFile
if (inputChanges.isIncremental) { if (inputChanges.isIncremental) {
logger.debug("Updating working dir incrementally: $workingDir") logger.debug("Updating working dir incrementally: $workingDir")
@ -243,7 +239,7 @@ abstract class AbstractJPackageTask @Inject constructor(
} else { } else {
logger.debug("Updating working dir non-incrementally: $workingDir") logger.debug("Updating working dir non-incrementally: $workingDir")
fileOperations.delete(workingDir) fileOperations.delete(workingDir)
workingDir.mkdirs() fileOperations.mkdir(workingDir)
fileOperations.copy { fileOperations.copy {
it.from(files) it.from(files)
it.from(launcherMainJar) it.from(launcherMainJar)
@ -259,7 +255,7 @@ abstract class AbstractJPackageTask @Inject constructor(
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.ioFileOrNull ?: return
val wixPath = wixDir.absolutePath val wixPath = wixDir.absolutePath
val path = System.getenv("PATH") ?: "" val path = System.getenv("PATH") ?: ""
exec.environment("PATH", "$wixPath;$path") exec.environment("PATH", "$wixPath;$path")
@ -269,10 +265,11 @@ abstract class AbstractJPackageTask @Inject constructor(
override fun checkResult(result: ExecResult) { override fun checkResult(result: ExecResult) {
super.checkResult(result) super.checkResult(result)
val destinationDirFile = destinationDir.asFile.get() val finalLocation = destinationDir.ioFile.let { destinationDir ->
val finalLocation = when (targetFormat) { when (targetFormat) {
TargetFormat.AppImage -> destinationDirFile TargetFormat.AppImage -> destinationDir
else -> destinationDirFile.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) } else -> destinationDir.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) }
}
} }
logger.lifecycle("The distribution is written to ${finalLocation.canonicalPath}") logger.lifecycle("The distribution is written to ${finalLocation.canonicalPath}")
} }

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

@ -1,16 +1,20 @@
package org.jetbrains.compose.desktop.application.tasks package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.internal.file.FileOperations import org.gradle.api.internal.file.FileOperations
import org.gradle.api.model.ObjectFactory 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.Provider
import org.gradle.api.provider.ProviderFactory import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.* import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec import org.gradle.process.ExecSpec
import org.gradle.work.InputChanges import org.gradle.work.InputChanges
import org.jetbrains.compose.desktop.application.internal.*
import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.desktop.application.internal.OS import org.jetbrains.compose.desktop.application.internal.OS
import org.jetbrains.compose.desktop.application.internal.currentOS import org.jetbrains.compose.desktop.application.internal.currentOS
@ -29,7 +33,10 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa
protected abstract val fileOperations: FileOperations protected abstract val fileOperations: FileOperations
@get:LocalState @get:LocalState
protected val workingDir: File = project.buildDir.resolve("compose/tmp/$name") protected val workingDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/$name")
@get:OutputDirectory
val destinationDir: DirectoryProperty = objects.directoryProperty()
@get:Input @get:Input
@get:Optional @get:Optional
@ -47,7 +54,7 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa
protected open fun prepareWorkingDir(inputChanges: InputChanges) { protected open fun prepareWorkingDir(inputChanges: InputChanges) {
fileOperations.delete(workingDir) fileOperations.delete(workingDir)
workingDir.mkdirs() fileOperations.mkdir(workingDir)
} }
protected open fun makeArgs(tmpDir: File): MutableList<String> = arrayListOf<String>().apply { protected open fun makeArgs(tmpDir: File): MutableList<String> = arrayListOf<String>().apply {
@ -69,10 +76,14 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa
"Ensure JAVA_HOME or buildSettings.javaHome is set to JDK 14 or newer" "Ensure JAVA_HOME or buildSettings.javaHome is set to JDK 14 or newer"
} }
fileOperations.delete(destinationDir)
prepareWorkingDir(inputChanges) prepareWorkingDir(inputChanges)
val args = makeArgs(workingDir) val argsFile = workingDir.ioFile.let { dir ->
val argsFile = workingDir.parentFile.resolve("${name}.args.txt") val args = makeArgs(dir)
argsFile.writeText(args.joinToString("\n")) dir.resolveSibling("${name}.args.txt").apply {
writeText(args.joinToString("\n"))
}
}
try { try {
execOperations.exec { exec -> execOperations.exec { exec ->

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

@ -29,9 +29,8 @@ abstract class AbstractRunDistributableTask @Inject constructor(
@TaskAction @TaskAction
fun run() { fun run() {
val appDir = appImageRootDir.get().let { appImageRoot -> val appDir = appImageRootDir.ioFile.let { appImageRoot ->
val files = appImageRoot.asFile.listFiles() val files = appImageRoot.listFiles()
if (files == null || files.isEmpty()) { if (files == null || files.isEmpty()) {
error("Could not find application image: $appImageRoot is empty!") error("Could not find application image: $appImageRoot is empty!")
} else if (files.size > 1) { } else if (files.size > 1) {

Loading…
Cancel
Save