From 7e243b5cbd37e9fa6a80da6b31478606ffbb24fd Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Wed, 23 Dec 2020 18:34:33 +0300 Subject: [PATCH] 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). --- .../dsl/RuntimeCompressionLevel.kt | 9 ++++ .../internal/configureApplication.kt | 23 +++++++-- .../desktop/application/internal/dslUtils.kt | 3 ++ .../application/internal/gradleUtils.kt | 11 +++++ .../application/tasks/AbstractJLinkTask.kt | 48 +++++++++++++++++++ .../application/tasks/AbstractJPackageTask.kt | 37 +++++++------- .../tasks/AbstractJvmToolOperationTask.kt | 21 ++++++-- .../tasks/AbstractRunDistributableTask.kt | 5 +- 8 files changed, 124 insertions(+), 33 deletions(-) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/RuntimeCompressionLevel.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/RuntimeCompressionLevel.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/RuntimeCompressionLevel.kt new file mode 100644 index 0000000000..d710e914a9 --- /dev/null +++ b/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) +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index 920b59db06..fb694bdeee 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/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.jetbrains.compose.desktop.application.dsl.Application 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.AbstractRunDistributableTask import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -56,12 +57,20 @@ internal fun Project.configurePackagingTasks(apps: Collection) { configureRunTask(app) } + val createRuntimeImage = tasks.composeTask( + 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 -> tasks.composeTask( taskName("package", app, targetFormat.name), args = listOf(targetFormat) ) { - configurePackagingTask(app) + configurePackagingTask(app, createRuntimeImage) } } @@ -77,7 +86,7 @@ internal fun Project.configurePackagingTasks(apps: Collection) { taskName("createDistributable", app), args = listOf(TargetFormat.AppImage) ) { - configurePackagingTask(app) + configurePackagingTask(app, createRuntimeImage) } val runDistributable = project.tasks.composeTask( @@ -88,10 +97,15 @@ internal fun Project.configurePackagingTasks(apps: Collection) { } internal fun AbstractJPackageTask.configurePackagingTask( - app: Application + app: Application, + createRuntimeImage: TaskProvider ) { enabled = targetFormat.isCompatibleWithCurrentOS + val runtimeImageDir = createRuntimeImage.flatMap { it.destinationDir } + dependsOn(createRuntimeImage) + runtimeImage.set(runtimeImageDir) + configurePlatformSettings(app) app.nativeDistributions.let { executables -> @@ -115,7 +129,6 @@ internal fun AbstractJPackageTask.configurePackagingTask( launcherMainJar.set(app.mainJar.orElse(configSource.jarTask(project).flatMap { it.archiveFile })) } - modules.set(provider { app.nativeDistributions.modules }) launcherMainClass.set(provider { app.mainClass }) launcherJvmArgs.set(provider { app.jvmArgs }) launcherArgs.set(provider { app.args }) @@ -205,7 +218,7 @@ private fun Jar.configurePackageUberJarForCurrentOS(app: Application) { destinationDirectory.set(project.layout.buildDirectory.dir("compose/jars")) doLast { - logger.lifecycle("The jar is written to ${archiveFile.get().asFile.canonicalPath}") + logger.lifecycle("The jar is written to ${archiveFile.ioFile.canonicalPath}") } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt index b28787d57d..28d4b9cbd1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/dslUtils.kt @@ -12,5 +12,8 @@ internal inline fun ObjectFactory.nullableProperty(): Property internal inline fun ObjectFactory.notNullProperty(): Property = property(T::class.java) +internal inline fun ObjectFactory.notNullProperty(defaultValue: T): Property = + property(T::class.java).value(defaultValue) + internal inline fun Task.provider(noinline fn: () -> T): Provider = project.provider(fn) \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/gradleUtils.kt new file mode 100644 index 0000000000..2c15e193a5 --- /dev/null +++ b/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 Provider.ioFile: File + get() = get().asFile + +internal val Provider.ioFileOrNull: File? + get() = orNull?.asFile diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJLinkTask.kt new file mode 100644 index 0000000000..e4050fbcfe --- /dev/null +++ b/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 = objects.listProperty(String::class.java) + + @get:Input + internal val stripDebug: Property = objects.notNullProperty(true) + + @get:Input + internal val noHeaderFiles: Property = objects.notNullProperty(true) + + @get:Input + internal val noManPages: Property = objects.notNullProperty(true) + + @get:Input + internal val stripNativeCommands: Property = objects.notNullProperty(true) + + @get:Input + @get:Optional + internal val compressionLevel: Property = objects.nullableProperty() + + override fun makeArgs(tmpDir: File): MutableList = 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) + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index 2dd0fff996..e342427701 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/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 val targetFormat: TargetFormat, ) : AbstractJvmToolOperationTask("jpackage") { - @get:OutputDirectory - val destinationDir: DirectoryProperty = objects.directoryProperty() - @get:InputFiles val files: ConfigurableFileCollection = objects.fileCollection() @@ -158,19 +155,20 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Optional val winUpgradeUuid: Property = objects.nullableProperty() - @get:Input - val modules: ListProperty = objects.listProperty(String::class.java) + @get:InputDirectory + @get:Optional + val runtimeImage: DirectoryProperty = objects.directoryProperty() override fun makeArgs(tmpDir: File): MutableList = super.makeArgs(tmpDir).apply { cliArg("--input", tmpDir) cliArg("--type", targetFormat.id) - cliArg("--dest", destinationDir.asFile.get()) + cliArg("--dest", destinationDir.ioFile) cliArg("--verbose", verbose) cliArg("--install-dir", installationPath) - cliArg("--license-file", licenseFile.asFile.orNull) - cliArg("--icon", iconFile.asFile.orNull) + cliArg("--license-file", licenseFile.ioFileOrNull) + cliArg("--icon", iconFile.ioFileOrNull) cliArg("--name", packageName) cliArg("--description", packageDescription) @@ -178,7 +176,7 @@ abstract class AbstractJPackageTask @Inject constructor( cliArg("--app-version", packageVersion) cliArg("--vendor", packageVendor) - cliArg("--main-jar", launcherMainJar.asFile.get().name) + cliArg("--main-jar", launcherMainJar.ioFile.name) cliArg("--main-class", launcherMainClass) launcherArgs.orNull?.forEach { cliArg("--arguments", it) @@ -202,7 +200,7 @@ abstract class AbstractJPackageTask @Inject constructor( 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-keychain", macSigningKeychain.ioFileOrNull) cliArg("--mac-signing-key-user-name", macSigningKeyUserName) } OS.Windows -> { @@ -216,13 +214,11 @@ abstract class AbstractJPackageTask @Inject constructor( } } - modules.get().forEach { m -> - cliArg("--add-modules", m) - } + cliArg("--runtime-image", runtimeImage) } override fun prepareWorkingDir(inputChanges: InputChanges) { - fileOperations.delete(destinationDir) + val workingDir = workingDir.ioFile if (inputChanges.isIncremental) { logger.debug("Updating working dir incrementally: $workingDir") @@ -243,7 +239,7 @@ abstract class AbstractJPackageTask @Inject constructor( } else { logger.debug("Updating working dir non-incrementally: $workingDir") fileOperations.delete(workingDir) - workingDir.mkdirs() + fileOperations.mkdir(workingDir) fileOperations.copy { it.from(files) it.from(launcherMainJar) @@ -259,7 +255,7 @@ abstract class AbstractJPackageTask @Inject constructor( private fun configureWixPathIfNeeded(exec: ExecSpec) { if (currentOS == OS.Windows) { - val wixDir = wixToolsetDir.asFile.orNull ?: return + val wixDir = wixToolsetDir.ioFileOrNull ?: return val wixPath = wixDir.absolutePath val path = System.getenv("PATH") ?: "" exec.environment("PATH", "$wixPath;$path") @@ -269,10 +265,11 @@ abstract class AbstractJPackageTask @Inject constructor( 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) } + val finalLocation = destinationDir.ioFile.let { destinationDir -> + when (targetFormat) { + TargetFormat.AppImage -> destinationDir + else -> destinationDir.walk().first { it.isFile && it.name.endsWith(targetFormat.fileExt) } + } } logger.lifecycle("The distribution is written to ${finalLocation.canonicalPath}") } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt index 565ada51b8..e37a338159 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJvmToolOperationTask.kt +++ b/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 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.model.ObjectFactory import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider 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.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.OS 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 @get:LocalState - protected val workingDir: File = project.buildDir.resolve("compose/tmp/$name") + protected val workingDir: Provider = project.layout.buildDirectory.dir("compose/tmp/$name") + + @get:OutputDirectory + val destinationDir: DirectoryProperty = objects.directoryProperty() @get:Input @get:Optional @@ -47,7 +54,7 @@ abstract class AbstractJvmToolOperationTask(private val toolName: String) : Defa protected open fun prepareWorkingDir(inputChanges: InputChanges) { fileOperations.delete(workingDir) - workingDir.mkdirs() + fileOperations.mkdir(workingDir) } protected open fun makeArgs(tmpDir: File): MutableList = arrayListOf().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" } + fileOperations.delete(destinationDir) prepareWorkingDir(inputChanges) - val args = makeArgs(workingDir) - val argsFile = workingDir.parentFile.resolve("${name}.args.txt") - argsFile.writeText(args.joinToString("\n")) + val argsFile = workingDir.ioFile.let { dir -> + val args = makeArgs(dir) + dir.resolveSibling("${name}.args.txt").apply { + writeText(args.joinToString("\n")) + } + } try { execOperations.exec { exec -> diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt index 76d992c6a8..7938641c91 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractRunDistributableTask.kt @@ -29,9 +29,8 @@ abstract class AbstractRunDistributableTask @Inject constructor( @TaskAction fun run() { - val appDir = appImageRootDir.get().let { appImageRoot -> - val files = appImageRoot.asFile.listFiles() - + val appDir = appImageRootDir.ioFile.let { appImageRoot -> + val files = appImageRoot.listFiles() if (files == null || files.isEmpty()) { error("Could not find application image: $appImageRoot is empty!") } else if (files.size > 1) {