From c815098ae98d1c2ebc2239e981af1b568dbb229a Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 17 Jun 2025 14:05:24 +0300 Subject: [PATCH 01/20] Implemented support for App CDS. --- .../desktop/application/dsl/AppCdsSettings.kt | 159 ++++++++++++++++++ .../desktop/application/dsl/JvmApplication.kt | 2 + .../internal/JvmApplicationData.kt | 2 + .../internal/JvmApplicationInternal.kt | 7 + .../internal/configureJvmApplication.kt | 54 ++++-- .../AbstractCheckNativeDistributionRuntime.kt | 10 +- .../tasks/AbstractCreateAppCdsArchiveTask.kt | 95 +++++++++++ .../application/tasks/AbstractJLinkTask.kt | 10 +- .../tasks/AbstractRunDistributableTask.kt | 30 +--- .../compose/internal/utils/osUtils.kt | 47 ++++++ 10 files changed, 371 insertions(+), 45 deletions(-) create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt create mode 100644 gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt new file mode 100644 index 00000000000..acb458df267 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -0,0 +1,159 @@ +package org.jetbrains.compose.desktop.application.dsl + +import org.jetbrains.compose.internal.utils.packagedAppJarFilesDir +import java.io.File +import java.io.Serializable + +/** + * The configuration of AppCDS for the native distribution. + */ +abstract class AppCdsConfiguration { + /** + * The AppCDS mode to use. + */ + var mode: AppCdsMode = AppCdsMode.None + + /** + * Whether to ask the JVM to log AppCDS-related actions. + */ + @Suppress("MemberVisibilityCanBePrivate") + var logging: Boolean = false + + /** + * Returns the AppCDS-related arguments to pass the JVM when running the app. + */ + internal fun runtimeJvmArgs() = buildList { + addAll(mode.runtimeJvmArgs()) + if (logging) add("-Xlog:cds") + } +} + +/** + * The mode of use of AppCDS. + */ +abstract class AppCdsMode : Serializable { + + /** + * Whether to generate a classes.jsa archive for the JRE classes. + */ + internal abstract val generateJreClassesArchive: Boolean + + /** + * Returns whether this mode creates an archive of app classes at build time. + */ + internal open val generateAppClassesArchive: Boolean get() = false + + /** + * The arguments to pass to the JVM when running the app to create + * the archive for the app's class files. + * + * This will only be called if [generateAppClassesArchive] is `true`. + */ + internal open fun appClassesArchiveCreationJvmArgs(): List = + error("AppCdsMode '$this' does not create an archive") + + /** + * Returns the app's classes archive file, given the root directory of + * the packaged app. + */ + internal open fun appClassesArchiveFile(packagedAppRootDir: File): File = + error("AppCdsMode '$this' does not create an archive") + + /** + * The arguments to pass to the JVM when running the final app. + */ + internal abstract fun runtimeJvmArgs(): List + + /** + * Checks whether this mode is compatible with the given JDK major version. + * Throws an exception if not. + */ + internal open fun checkJdkCompatibility(jdkMajorVersion: Int) = Unit + + + companion object { + + /** + * The name of the AppCds archive file. + */ + private const val ARCHIVE_NAME = "app.jsa" + + /** + * AppCDS is not used. + */ + val None = object : AppCdsMode() { + override val generateJreClassesArchive: Boolean get() = false + override fun runtimeJvmArgs() = emptyList() + override fun toString() = "None" + } + + /** + * AppCDS is used via a dynamic shared archive created automatically + * when the app is run (using `-XX:+AutoCreateSharedArchive`). + * + * Pros: + * - Simplest - no additional step is needed to build the archive. + * - Creates a smaller distributable. + * + * Cons: + * - Requires JDK 19 or later. + * - The archive is not available at the first execution of the app, + * so it is slower. The archive is created when at shutdown time + * of the first execution, which also takes a little longer. + */ + @Suppress("unused") + val Auto = object : AppCdsMode() { + private val MIN_JDK_VERSION = 19 + override val generateJreClassesArchive: Boolean get() = true + override fun runtimeJvmArgs() = + listOf( + "-XX:SharedArchiveFile=\$APPDIR/$ARCHIVE_NAME", + "-XX:+AutoCreateSharedArchive" + ) + override fun checkJdkCompatibility(jdkMajorVersion: Int) { + if (jdkMajorVersion < MIN_JDK_VERSION) { + error( + "AppCdsMode '$this' is not supported on JDK earlier than" + + " $MIN_JDK_VERSION; current is $jdkMajorVersion" + ) + } + } + override fun toString() = "Auto" + } + + /** + * AppCDS is used via a dynamic shared archive created by executing + * the app before packaging (using `-XX:ArchiveClassesAtExit`). + * + * Pros: + * - Can be used with JDKs earlier than 19. + * - The first run of the distributed app is fast too. + * + * Cons: + * - Requires an additional step of running the app when building the + * distributable. + * - The distributable is larger because it includes the archive of + * the app's classes. + */ + @Suppress("unused") + val Prebuild = object : AppCdsMode() { + override val generateJreClassesArchive: Boolean get() = true + override val generateAppClassesArchive: Boolean get() = true + override fun appClassesArchiveCreationJvmArgs() = + listOf( + "-XX:ArchiveClassesAtExit=\$APPDIR/$ARCHIVE_NAME", + "-Dcompose.cds.create-archive=true" + ) + override fun appClassesArchiveFile(packagedAppRootDir: File): File { + val appDir = packagedAppJarFilesDir(packagedAppRootDir) + return appDir.resolve(ARCHIVE_NAME) + } + override fun runtimeJvmArgs() = + listOf( + "-XX:SharedArchiveFile=\$APPDIR/$ARCHIVE_NAME", + ) + + override fun toString() = "Prebuild" + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt index 7769f3567e6..c5c378f19c9 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt @@ -30,5 +30,7 @@ abstract class JvmApplication { abstract fun nativeDistributions(fn: Action) abstract val buildTypes: JvmApplicationBuildTypes abstract fun buildTypes(fn: Action) + abstract val appCds: AppCdsConfiguration + abstract fun appCds(fn: Action) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt index ebebf9fa9d4..b11d0b29eb7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt @@ -10,6 +10,7 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory +import org.jetbrains.compose.desktop.application.dsl.AppCdsConfiguration import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes import org.jetbrains.compose.internal.utils.new @@ -38,4 +39,5 @@ internal open class JvmApplicationData @Inject constructor( val jvmArgs: MutableList = ArrayList() val nativeDistributions: JvmApplicationDistributions = objects.new() val buildTypes: JvmApplicationBuildTypes = objects.new() + val appCds: AppCdsConfiguration = objects.new() } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt index 3736ca897c8..7056b1fd0ce 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt @@ -10,6 +10,7 @@ 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.compose.desktop.application.dsl.AppCdsConfiguration import org.jetbrains.compose.desktop.application.dsl.JvmApplication import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes @@ -70,4 +71,10 @@ internal open class JvmApplicationInternal @Inject constructor( final override fun buildTypes(fn: Action) { fn.execute(data.buildTypes) } + + final override val appCds: AppCdsConfiguration by data::appCds + final override fun appCds(fn: Action) { + fn.execute(data.appCds) + } + } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 35dec15a2fb..ae70e34e166 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -18,13 +18,6 @@ import org.jetbrains.compose.desktop.application.tasks.* import org.jetbrains.compose.desktop.tasks.AbstractJarsFlattenTask import org.jetbrains.compose.desktop.tasks.AbstractUnpackDefaultComposeApplicationResourcesTask import org.jetbrains.compose.internal.utils.* -import org.jetbrains.compose.internal.utils.OS -import org.jetbrains.compose.internal.utils.currentOS -import org.jetbrains.compose.internal.utils.currentTarget -import org.jetbrains.compose.internal.utils.dir -import org.jetbrains.compose.internal.utils.ioFile -import org.jetbrains.compose.internal.utils.ioFileOrNull -import org.jetbrains.compose.internal.utils.javaExecutable import org.jetbrains.compose.internal.utils.provider private val defaultJvmArgs = listOf("-D$CONFIGURE_SWING_GLOBALS=true") @@ -66,6 +59,7 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes taskNameObject = "runtime" ) { jdkHome.set(app.javaHomeProvider) + appCdsMode.set(app.appCds.mode) checkJdkVendor.set(ComposeProperties.checkJdkVendor(project.providers)) jdkVersionProbeJar.from( project.detachedComposeGradleDependency( @@ -111,14 +105,15 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) destinationDir.set(appTmpDir.dir("runtime")) + generateCdsArchive.set(app.appCds.mode.generateJreClassesArchive) } return CommonJvmDesktopTasks( - unpackDefaultResources, - checkRuntime, - suggestRuntimeModules, - prepareAppResources, - createRuntimeImage + unpackDefaultResources = unpackDefaultResources, + checkRuntime = checkRuntime, + suggestRuntimeModules = suggestRuntimeModules, + prepareAppResources = prepareAppResources, + createRuntimeImage = createRuntimeImage, ) } @@ -149,6 +144,18 @@ private fun JvmApplicationContext.configurePackagingTasks( ) } + val appCdsMode = app.appCds.mode + val createAppCdsArchive = if (appCdsMode.generateAppClassesArchive) { + tasks.register( + taskNameAction = "create", + taskNameObject = "appCdsArchive", + args = listOf(createDistributable) + ) { + dependsOn(createDistributable) + this.appCdsMode.set(appCdsMode) + } + } else null + val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> val packageFormat = tasks.register( taskNameAction = "package", @@ -168,14 +175,16 @@ private fun JvmApplicationContext.configurePackagingTasks( prepareAppResources = commonTasks.prepareAppResources, checkRuntime = commonTasks.checkRuntime, unpackDefaultResources = commonTasks.unpackDefaultResources, - runProguard = runProguard + runProguard = runProguard, + createAppCdsArchive = createAppCdsArchive ) } else { configurePackageTask( this, createAppImage = createDistributable, checkRuntime = commonTasks.checkRuntime, - unpackDefaultResources = commonTasks.unpackDefaultResources + unpackDefaultResources = commonTasks.unpackDefaultResources, + createAppCdsArchive = createAppCdsArchive ) } } @@ -233,7 +242,11 @@ private fun JvmApplicationContext.configurePackagingTasks( taskNameAction = "run", taskNameObject = "distributable", args = listOf(createDistributable) - ) + ) { + if (createAppCdsArchive != null) { + dependsOn(createAppCdsArchive) + } + } val run = tasks.register(taskNameAction = "run") { configureRunTask(this, commonTasks.prepareAppResources, runProguard) @@ -284,7 +297,8 @@ private fun JvmApplicationContext.configurePackageTask( prepareAppResources: TaskProvider? = null, checkRuntime: TaskProvider? = null, unpackDefaultResources: TaskProvider, - runProguard: Provider? = null + runProguard: Provider? = null, + createAppCdsArchive: TaskProvider? = null ) { packageTask.enabled = packageTask.targetFormat.isCompatibleWithCurrentOS @@ -338,8 +352,14 @@ private fun JvmApplicationContext.configurePackageTask( } } + if (createAppCdsArchive != null) { + packageTask.dependsOn(createAppCdsArchive) + } + packageTask.launcherMainClass.set(provider { app.mainClass }) - packageTask.launcherJvmArgs.set(provider { defaultJvmArgs + app.jvmArgs }) + packageTask.launcherJvmArgs.set( + provider { defaultJvmArgs + app.appCds.runtimeJvmArgs() + app.jvmArgs } + ) packageTask.launcherArgs.set(provider { app.args }) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt index e4e8010c894..7ad47354842 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt @@ -10,6 +10,7 @@ import org.gradle.api.file.RegularFile import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* +import org.jetbrains.compose.desktop.application.dsl.AppCdsMode import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner @@ -39,6 +40,9 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa @get:Input abstract val checkJdkVendor: Property + @get:Input + val appCdsMode: Property = objects.notNullProperty() + private val taskDir = project.layout.buildDirectory.dir("compose/tmp/$name") @get:OutputFile @@ -75,8 +79,8 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa val jdkHome = jdkHomeFile val javaExecutable = jdkHome.getJdkTool("java") val jlinkExecutable = jdkHome.getJdkTool("jlink") - val jpackageExecutabke = jdkHome.getJdkTool("jpackage") - ensureToolsExist(javaExecutable, jlinkExecutable, jpackageExecutabke) + val jpackageExecutable = jdkHome.getJdkTool("jpackage") + ensureToolsExist(javaExecutable, jlinkExecutable, jpackageExecutable) val jdkRuntimeProperties = getJDKRuntimeProperties(javaExecutable) @@ -109,6 +113,8 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa } } + appCdsMode.get().checkJdkCompatibility(jdkMajorVersion) + val modules = arrayListOf() runExternalTool( tool = javaExecutable, diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt new file mode 100644 index 00000000000..c972b666e97 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -0,0 +1,95 @@ +package org.jetbrains.compose.desktop.application.tasks + +import org.gradle.api.file.Directory +import org.gradle.api.file.FileTree +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.jetbrains.compose.desktop.application.dsl.AppCdsMode +import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask +import org.jetbrains.compose.internal.utils.* +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import javax.inject.Inject + +abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( + createDistributable: TaskProvider +) : AbstractComposeDesktopTask() { + @get:Internal + internal val appImageRootDir: Provider = createDistributable.flatMap { it.destinationDir } + + @get:Input + internal val packageName: Provider = createDistributable.flatMap { it.packageName } + + @get:Input + internal val appCdsMode: Property = objects.notNullProperty() + + @Suppress("unused") + @OutputFile + val appCdsArchiveFile: Provider = provider { + val packagedAppRootDir = packagedAppRootDir(appImageRootDir.get()) + appCdsMode.get().appClassesArchiveFile(packagedAppRootDir) + } + + // This is needed to correctly describe the dependencies to Gradle. + // Can't just use appImageRootDir because the AppCDS archive needs to be excluded + @Suppress("unused") + @get:InputFiles + internal val dependencyFiles: Provider = provider { + // If the app image root directory doesn't exist, return an empty file tree + appImageRootDir.get().let { + if (!it.asFile.isDirectory) { + return@provider it.asFileTree + } + } + + val appCdsArchiveFile = appCdsArchiveFile.get().relativeTo(appImageRootDir.get().asFile).path + appImageRootDir.get().asFileTree.matching { it.exclude(appCdsArchiveFile) } + } + + @TaskAction + fun run() { + // Before running the app, replace the 'java-options' corresponding to + // AppCdsMode.runtimeJvmArgs with AppCdsMode.appClassesArchiveCreationJvmArgs + // This must be done because, for example, -XX:SharedArchiveFile and + // -XX:ArchiveClassesAtExit can't be used at the same time + val packagedRootDir = packagedAppRootDir(appImageRootDir.get()) + val appDir = packagedAppJarFilesDir(packagedRootDir) + val cfgFile = appDir.resolve("${packageName.get()}.cfg") + val cfgFileTempCopy = File(cfgFile.parentFile, "${cfgFile.name}.tmp") + + // Save the original cfg file + Files.copy(cfgFile.toPath(), cfgFileTempCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) + try { + // Edit the cfg file + cfgFile.outputStream().bufferedWriter().use { output -> + // Copy lines, filtering the AppCdsMode's runtime options + val runtimeOptionCfgLines = appCdsMode.get().runtimeJvmArgs() + .mapTo(mutableSetOf()) { "java-options=$it" } + cfgFileTempCopy.useLines { lines -> + lines.forEach { line -> + if (line !in runtimeOptionCfgLines) { + output.appendLine(line) + } + } + } + + // Add the AppCdsMode's archive creation options + val archiveCreationOptions = appCdsMode.get().appClassesArchiveCreationJvmArgs().toSet() + for (arg in archiveCreationOptions) { + output.appendLine("java-options=$arg") + } + } + + // Run the app to create the AppCDS archive + execOperations.executePackagedApp( + appImageRootDir = appImageRootDir.get(), + packageName = packageName.get() + ) + } finally { + // Restore the original cfg file + Files.move(cfgFileTempCopy.toPath(), cfgFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } +} \ No newline at end of file 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 index b44237ef7b7..be42323461d 100644 --- 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 @@ -47,6 +47,9 @@ abstract class AbstractJLinkTask : AbstractJvmToolOperationTask("jlink") { @get:Optional internal val compressionLevel: Property = objects.nullableProperty() + @get:Input + internal val generateCdsArchive: Property = objects.notNullProperty(false) + override fun makeArgs(tmpDir: File): MutableList = super.makeArgs(tmpDir).apply { val modulesToInclude = if (includeAllModules.get()) { @@ -59,7 +62,12 @@ abstract class AbstractJLinkTask : AbstractJvmToolOperationTask("jlink") { cliArg("--strip-debug", stripDebug) cliArg("--no-header-files", noHeaderFiles) cliArg("--no-man-pages", noManPages) - cliArg("--strip-native-commands", stripNativeCommands) + if (generateCdsArchive.get()) { + cliArg("--generate-cds-archive", true) + } else { + // jlink (a native command) is needed to generate the CDS archive + cliArg("--strip-native-commands", stripNativeCommands) + } cliArg("--compress", compressionLevel.orNull?.id) cliArg("--output", destinationDir) 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 8d33b55c083..1964dcb84a1 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 @@ -11,11 +11,8 @@ 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.jetbrains.compose.internal.utils.OS -import org.jetbrains.compose.internal.utils.currentOS -import org.jetbrains.compose.internal.utils.executableName -import org.jetbrains.compose.internal.utils.ioFile import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask +import org.jetbrains.compose.internal.utils.executePackagedApp import javax.inject.Inject // Custom task is used instead of Exec, because Exec does not support @@ -32,26 +29,9 @@ abstract class AbstractRunDistributableTask @Inject constructor( @TaskAction fun run() { - val appDir = appImageRootDir.ioFile.let { appImageRoot -> - val files = appImageRoot.listFiles() - // Sometimes ".DS_Store" files are created on macOS, so ignore them. - ?.filterNot { it.name == ".DS_Store" } - 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(workingDir.resolve(executable).absolutePath) - }.assertNormalExitValue() + execOperations.executePackagedApp( + appImageRootDir = appImageRootDir.get(), + packageName = packageName.get() + ) } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt index 4537489e8f4..8855d858a41 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt @@ -5,7 +5,9 @@ package org.jetbrains.compose.internal.utils +import org.gradle.api.file.Directory import org.gradle.api.provider.Provider +import org.gradle.process.ExecOperations import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile import org.jetbrains.compose.desktop.application.tasks.MIN_JAVA_RUNTIME_VERSION import java.io.File @@ -52,6 +54,51 @@ internal val currentOS: OS by lazy { internal fun executableName(nameWithoutExtension: String): String = if (currentOS == OS.Windows) "$nameWithoutExtension.exe" else nameWithoutExtension +internal fun packagedAppRootDir(appImageRootDir: Directory): File { + return appImageRootDir.asFile.let { appImageRoot -> + val files = appImageRoot.listFiles() + // Sometimes ".DS_Store" files are created on macOS, so ignore them. + ?.filterNot { it.name == ".DS_Store" } + if (files.isNullOrEmpty()) { + 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() + } +} + +internal fun packagedAppExecutableName(packageName: String): String { + val appExecutableName = executableName(packageName) + return when (currentOS) { + OS.Linux -> "bin/$appExecutableName" + OS.Windows -> appExecutableName + OS.MacOS -> "Contents/MacOS/$appExecutableName" + } +} + +internal fun packagedAppJarFilesDir(packagedRootDir: File): File { + return packagedRootDir.resolve( + when (currentOS) { + OS.Linux -> "lib/app/" + OS.Windows -> "app/" + OS.MacOS -> "Contents/app/" + } + ) +} + +internal fun ExecOperations.executePackagedApp( + appImageRootDir: Directory, + packageName: String +) { + val workingDir = packagedAppRootDir(appImageRootDir = appImageRootDir) + val executableName = packagedAppExecutableName(packageName = packageName) + + exec { spec -> + spec.workingDir(workingDir) + spec.executable(workingDir.resolve(executableName).absolutePath) + }.assertNormalExitValue() +} + internal fun javaExecutable(javaHome: String): String = File(javaHome).resolve("bin/${executableName("java")}").absolutePath From 6aad5549ea1d731633c5ade3c63b6e2bae988de6 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Thu, 19 Jun 2025 16:07:49 +0300 Subject: [PATCH 02/20] Address PR feedback. --- .../desktop/application/dsl/AppCdsSettings.kt | 14 +++++++------- .../internal/configureJvmApplication.kt | 1 + .../tasks/AbstractCreateAppCdsArchiveTask.kt | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index acb458df267..a7200ffacbc 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -18,14 +18,14 @@ abstract class AppCdsConfiguration { */ @Suppress("MemberVisibilityCanBePrivate") var logging: Boolean = false +} - /** - * Returns the AppCDS-related arguments to pass the JVM when running the app. - */ - internal fun runtimeJvmArgs() = buildList { - addAll(mode.runtimeJvmArgs()) - if (logging) add("-Xlog:cds") - } +/** + * Returns the AppCDS-related arguments to pass the JVM when running the app. + */ +internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { + addAll(mode.runtimeJvmArgs()) + if (logging) add("-Xlog:cds") } /** diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index ae70e34e166..51735687611 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -13,6 +13,7 @@ import org.gradle.api.tasks.Sync import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.tasks.Jar import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.dsl.runtimeJvmArgs import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions import org.jetbrains.compose.desktop.application.tasks.* import org.jetbrains.compose.desktop.tasks.AbstractJarsFlattenTask diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index c972b666e97..21305304ecc 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -23,7 +23,7 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( internal val packageName: Provider = createDistributable.flatMap { it.packageName } @get:Input - internal val appCdsMode: Property = objects.notNullProperty() + internal abstract val appCdsMode: Property @Suppress("unused") @OutputFile From a8e376f18f43948e096227a30e2a993a46791c15 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Fri, 20 Jun 2025 22:18:43 +0300 Subject: [PATCH 03/20] Change AbstractCreateAppCdsArchiveTask.dependencyFiles to be a regular property. --- .../application/tasks/AbstractCreateAppCdsArchiveTask.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index 21305304ecc..344f301168f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -36,16 +36,16 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( // Can't just use appImageRootDir because the AppCDS archive needs to be excluded @Suppress("unused") @get:InputFiles - internal val dependencyFiles: Provider = provider { + internal val dependencyFiles: FileTree get() { // If the app image root directory doesn't exist, return an empty file tree appImageRootDir.get().let { if (!it.asFile.isDirectory) { - return@provider it.asFileTree + return it.asFileTree } } val appCdsArchiveFile = appCdsArchiveFile.get().relativeTo(appImageRootDir.get().asFile).path - appImageRootDir.get().asFileTree.matching { it.exclude(appCdsArchiveFile) } + return appImageRootDir.get().asFileTree.matching { it.exclude(appCdsArchiveFile) } } @TaskAction From ef4b437df07caf4721910ec37548309fb1b61309 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Sat, 21 Jun 2025 13:58:56 +0300 Subject: [PATCH 04/20] Added AppCDS tests. --- .../desktop/application/dsl/AppCdsSettings.kt | 28 ++--- .../tasks/AbstractCreateAppCdsArchiveTask.kt | 8 +- .../integration/DesktopApplicationTest.kt | 100 ++++++++++++++++++ .../compose/test/utils/assertUtils.kt | 6 ++ .../application/appCds/build.gradle.kts | 37 +++++++ .../application/appCds/settings.gradle | 31 ++++++ .../appCds/src/main/kotlin/main.kt | 5 + 7 files changed, 198 insertions(+), 17 deletions(-) create mode 100644 gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts create mode 100644 gradle-plugins/compose/src/test/test-projects/application/appCds/settings.gradle create mode 100644 gradle-plugins/compose/src/test/test-projects/application/appCds/src/main/kotlin/main.kt diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index a7200ffacbc..373676d6ef2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -31,7 +31,7 @@ internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { /** * The mode of use of AppCDS. */ -abstract class AppCdsMode : Serializable { +abstract class AppCdsMode(val name: String) : Serializable { /** * Whether to generate a classes.jsa archive for the JRE classes. @@ -70,21 +70,26 @@ abstract class AppCdsMode : Serializable { */ internal open fun checkJdkCompatibility(jdkMajorVersion: Int) = Unit + override fun toString() = name companion object { /** - * The name of the AppCds archive file. + * The name of the AppCDS archive file. */ private const val ARCHIVE_NAME = "app.jsa" + /** + * The AppCDS archive file. + */ + internal const val ARCHIVE_FILE_ARGUMENT = "\$APPDIR/$ARCHIVE_NAME" + /** * AppCDS is not used. */ - val None = object : AppCdsMode() { + val None = object : AppCdsMode("None") { override val generateJreClassesArchive: Boolean get() = false override fun runtimeJvmArgs() = emptyList() - override fun toString() = "None" } /** @@ -102,12 +107,12 @@ abstract class AppCdsMode : Serializable { * of the first execution, which also takes a little longer. */ @Suppress("unused") - val Auto = object : AppCdsMode() { + val Auto = object : AppCdsMode("Auto") { private val MIN_JDK_VERSION = 19 override val generateJreClassesArchive: Boolean get() = true override fun runtimeJvmArgs() = listOf( - "-XX:SharedArchiveFile=\$APPDIR/$ARCHIVE_NAME", + "-XX:SharedArchiveFile=$ARCHIVE_FILE_ARGUMENT", "-XX:+AutoCreateSharedArchive" ) override fun checkJdkCompatibility(jdkMajorVersion: Int) { @@ -118,7 +123,6 @@ abstract class AppCdsMode : Serializable { ) } } - override fun toString() = "Auto" } /** @@ -136,13 +140,13 @@ abstract class AppCdsMode : Serializable { * the app's classes. */ @Suppress("unused") - val Prebuild = object : AppCdsMode() { + val Prebuild = object : AppCdsMode("Prebuild") { override val generateJreClassesArchive: Boolean get() = true override val generateAppClassesArchive: Boolean get() = true override fun appClassesArchiveCreationJvmArgs() = listOf( - "-XX:ArchiveClassesAtExit=\$APPDIR/$ARCHIVE_NAME", - "-Dcompose.cds.create-archive=true" + "-XX:ArchiveClassesAtExit=$ARCHIVE_FILE_ARGUMENT", + "-Dcompose.appcds.create-archive=true" ) override fun appClassesArchiveFile(packagedAppRootDir: File): File { val appDir = packagedAppJarFilesDir(packagedAppRootDir) @@ -150,10 +154,8 @@ abstract class AppCdsMode : Serializable { } override fun runtimeJvmArgs() = listOf( - "-XX:SharedArchiveFile=\$APPDIR/$ARCHIVE_NAME", + "-XX:SharedArchiveFile=$ARCHIVE_FILE_ARGUMENT", ) - - override fun toString() = "Prebuild" } } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index 344f301168f..94445201b81 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -26,10 +26,10 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( internal abstract val appCdsMode: Property @Suppress("unused") - @OutputFile - val appCdsArchiveFile: Provider = provider { + @get:OutputFile + val appCdsArchiveFile: File get() { val packagedAppRootDir = packagedAppRootDir(appImageRootDir.get()) - appCdsMode.get().appClassesArchiveFile(packagedAppRootDir) + return appCdsMode.get().appClassesArchiveFile(packagedAppRootDir) } // This is needed to correctly describe the dependencies to Gradle. @@ -44,7 +44,7 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( } } - val appCdsArchiveFile = appCdsArchiveFile.get().relativeTo(appImageRootDir.get().asFile).path + val appCdsArchiveFile = appCdsArchiveFile.relativeTo(appImageRootDir.get().asFile).path return appImageRootDir.get().asFileTree.matching { it.exclude(appCdsArchiveFile) } } diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 497881597a1..872084a2e81 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -6,6 +6,8 @@ package org.jetbrains.compose.test.tests.integration import org.gradle.internal.impldep.org.testng.Assert +import org.gradle.internal.jvm.inspection.JvmVendor +import org.jetbrains.compose.desktop.application.dsl.AppCdsMode import org.jetbrains.compose.internal.utils.MacUtils import org.jetbrains.compose.internal.utils.OS import org.jetbrains.compose.internal.utils.currentArch @@ -33,6 +35,7 @@ import org.junit.jupiter.api.Test import java.io.File import java.util.* import java.util.jar.JarFile +import kotlin.test.assertTrue class DesktopApplicationTest : GradlePluginTestBase() { @Test @@ -571,6 +574,103 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } + @Suppress("SameParameterValue") + private fun appCdsProject( + appCdsMode: AppCdsMode, + javaVersion: Int, + javaVendor: JvmVendor.KnownJvmVendor = JvmVendor.KnownJvmVendor.AMAZON + ) : TestProject { + return testProject("application/appCds").apply { + modifyText("build.gradle.kts") { + it + .replace("%APP_CDS_MODE%", "AppCdsMode.$appCdsMode") + .replace("%JAVA_VERSION%", "$javaVersion") + .replace("%JVM_VENDOR%", javaVendor.name) + } + } + } + + @Test + fun testAppCdsAutoFailsOnJdk17() = with(appCdsProject(AppCdsMode.Auto, 17)) { + fun testRunTask(runTask: String) { + gradleFailure(runTask).checks { + check.logContains("AppCdsMode 'Auto' is not supported on JDK earlier than 19; current is 17") + } + } + + testRunTask(":runDistributable") + } + + private val loggedArchivePathRegex = + Regex("\\[cds] Opened archive " + + listOf( + ".*", + "build", + "compose", + "binaries", + ".*", + "app", + ".*", + AppCdsMode.ARCHIVE_FILE_ARGUMENT + .replace("\$APPDIR", "app") + .replace(".", "\\.") + ).joinToString(File.separator.replace("\\", "\\\\")) + ) + + @Test + fun testAppCdsAuto() = with(appCdsProject(AppCdsMode.Auto, 21)) { + fun testRunTask(taskName: String) { + gradle(taskName).checks { + check.taskSuccessful(taskName) + check.logContains("[cds] Dumping shared data to file") + } + gradle(taskName).checks { + check.taskSuccessful(taskName) + check.logContainsMatch(loggedArchivePathRegex) + check.logDoesntContain("[cds] Specified shared archive not found") + check.logDoesntContain("[cds] Dumping shared data to file") + check.logDoesntContain("[cds] Initialize dynamic archive failed") + check.logDoesntContain("[cds] An error has occurred while processing the shared archive file") + check.logDoesntContain("[cds] Failed to initialize dynamic archive") + } + gradle(":clean") + } + + testRunTask(":runDistributable") + testRunTask(":runReleaseDistributable") + } + + @Test + fun testAppCdsPrebuild() = with(appCdsProject(AppCdsMode.Prebuild, 17)) { + fun testPackageAndRun(release: Boolean) { + val releaseTag = if (release) "Release" else "" + val packageTaskName = ":package${releaseTag}DistributionForCurrentOS" + val createAppCdsTaskName = ":create${releaseTag}AppCdsArchive" + val runDistributableTaskName = ":run${releaseTag}Distributable" + gradle(packageTaskName).checks { + check.taskSuccessful(packageTaskName) + check.taskSuccessful(createAppCdsTaskName) + check.logContains("[cds] Dumping shared data to file") + check.logContains("Running app to create archive: true") + } + + gradle(runDistributableTaskName).checks { + check.taskSuccessful(runDistributableTaskName) + check.taskUpToDate(createAppCdsTaskName) + check.logContainsMatch(loggedArchivePathRegex) + check.logContains("Running app to create archive: false") + check.logDoesntContain("[cds] Specified shared archive not found") + check.logDoesntContain("[cds] Dumping shared data to file") + check.logDoesntContain("[cds] Initialize dynamic archive failed") + check.logDoesntContain("[cds] An error has occurred while processing the shared archive file") + check.logDoesntContain("[cds] Failed to initialize dynamic archive") + } + } + + testPackageAndRun(release = false) + testPackageAndRun(release = true) + } + private fun TestProject.enableJoinOutputJars() { val enableJoinOutputJars = """ compose.desktop { diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt index 16a5c1fecf9..1fea09f29d2 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt @@ -43,6 +43,12 @@ internal class BuildResultChecks(private val result: BuildResult) { } } + fun logContainsMatch(regex: Regex) { + if (!regex.containsMatchIn(result.output)) { + throw AssertionError("Test output does not match expected regular expression: $regex") + } + } + fun logDoesntContain(substring: String) { if (result.output.contains(substring)) { throw AssertionError("Test output contains the unexpected string: '$substring'") diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts new file mode 100644 index 00000000000..a0e1fb2dba9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts @@ -0,0 +1,37 @@ +import org.jetbrains.compose.* +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.dsl.AppCdsMode +import org.gradle.internal.jvm.inspection.JvmVendor +import org.gradle.jvm.toolchain.internal.DefaultJvmVendorSpec + +plugins { + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.plugin.compose") + id("org.jetbrains.compose") +} + +dependencies { + implementation(kotlin("stdlib")) + implementation(compose.desktop.currentOs) +} + +compose.desktop { + application { + javaHome = javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(%JAVA_VERSION%)) + vendor.set(DefaultJvmVendorSpec.of(JvmVendor.KnownJvmVendor.%JVM_VENDOR%)) + }.get().metadata.installationPath.asFile.absolutePath + + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageVersion = "1.0.0" + } + + jvmArgs += "-Xshare:on" // This forces failure if AppCDS doesn't work + appCds { + mode = %APP_CDS_MODE% + logging = true + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/settings.gradle b/gradle-plugins/compose/src/test/test-projects/application/appCds/settings.gradle new file mode 100644 index 00000000000..d65cfd9c22d --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/settings.gradle @@ -0,0 +1,31 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.kotlin.plugin.compose' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER' + id("org.gradle.toolchains.foojay-resolver-convention").version("1.0.0") + } + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + google() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + mavenLocal() + } +} +rootProject.name = "simple" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/src/main/kotlin/main.kt b/gradle-plugins/compose/src/test/test-projects/application/appCds/src/main/kotlin/main.kt new file mode 100644 index 00000000000..ab1d1c80a4f --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/src/main/kotlin/main.kt @@ -0,0 +1,5 @@ + +fun main() { + val creatingAppCdsArchive = System.getProperty("compose.appcds.create-archive") != null + println("Running app to create archive: $creatingAppCdsArchive") +} From c55d5c48e5bb34fb82b9cc9d754953fc3800fa29 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 1 Jul 2025 21:01:31 +0300 Subject: [PATCH 05/20] Moved `appCds` block into `nativeDistributions` --- .../compose/desktop/application/dsl/JvmApplication.kt | 2 -- .../application/dsl/JvmApplicationDistributions.kt | 6 ++++++ .../desktop/application/internal/JvmApplicationData.kt | 2 -- .../application/internal/JvmApplicationInternal.kt | 7 ------- .../application/internal/configureJvmApplication.kt | 8 ++++---- .../test-projects/application/appCds/build.gradle.kts | 9 +++++---- 6 files changed, 15 insertions(+), 19 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt index c5c378f19c9..7769f3567e6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplication.kt @@ -30,7 +30,5 @@ abstract class JvmApplication { abstract fun nativeDistributions(fn: Action) abstract val buildTypes: JvmApplicationBuildTypes abstract fun buildTypes(fn: Action) - abstract val appCds: AppCdsConfiguration - abstract fun appCds(fn: Action) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt index f59b838b6a0..cfa2212e0b0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt @@ -43,4 +43,10 @@ abstract class JvmApplicationDistributions : AbstractDistributions() { windows.fileAssociation(mimeType, extension, description, windowsIconFile) macOS.fileAssociation(mimeType, extension, description, macOSIconFile) } + + val appCds: AppCdsConfiguration = objects.newInstance(AppCdsConfiguration::class.java) + fun appCds(fn: Action) { + fn.execute(appCds) + } + } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt index b11d0b29eb7..ebebf9fa9d4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationData.kt @@ -10,7 +10,6 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory -import org.jetbrains.compose.desktop.application.dsl.AppCdsConfiguration import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes import org.jetbrains.compose.internal.utils.new @@ -39,5 +38,4 @@ internal open class JvmApplicationData @Inject constructor( val jvmArgs: MutableList = ArrayList() val nativeDistributions: JvmApplicationDistributions = objects.new() val buildTypes: JvmApplicationBuildTypes = objects.new() - val appCds: AppCdsConfiguration = objects.new() } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt index 7056b1fd0ce..3736ca897c8 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationInternal.kt @@ -10,7 +10,6 @@ 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.compose.desktop.application.dsl.AppCdsConfiguration import org.jetbrains.compose.desktop.application.dsl.JvmApplication import org.jetbrains.compose.desktop.application.dsl.JvmApplicationDistributions import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes @@ -71,10 +70,4 @@ internal open class JvmApplicationInternal @Inject constructor( final override fun buildTypes(fn: Action) { fn.execute(data.buildTypes) } - - final override val appCds: AppCdsConfiguration by data::appCds - final override fun appCds(fn: Action) { - fn.execute(data.appCds) - } - } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 51735687611..edc41a2ec52 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -60,7 +60,7 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes taskNameObject = "runtime" ) { jdkHome.set(app.javaHomeProvider) - appCdsMode.set(app.appCds.mode) + appCdsMode.set(app.nativeDistributions.appCds.mode) checkJdkVendor.set(ComposeProperties.checkJdkVendor(project.providers)) jdkVersionProbeJar.from( project.detachedComposeGradleDependency( @@ -106,7 +106,7 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) destinationDir.set(appTmpDir.dir("runtime")) - generateCdsArchive.set(app.appCds.mode.generateJreClassesArchive) + generateCdsArchive.set(app.nativeDistributions.appCds.mode.generateJreClassesArchive) } return CommonJvmDesktopTasks( @@ -145,7 +145,7 @@ private fun JvmApplicationContext.configurePackagingTasks( ) } - val appCdsMode = app.appCds.mode + val appCdsMode = app.nativeDistributions.appCds.mode val createAppCdsArchive = if (appCdsMode.generateAppClassesArchive) { tasks.register( taskNameAction = "create", @@ -359,7 +359,7 @@ private fun JvmApplicationContext.configurePackageTask( packageTask.launcherMainClass.set(provider { app.mainClass }) packageTask.launcherJvmArgs.set( - provider { defaultJvmArgs + app.appCds.runtimeJvmArgs() + app.jvmArgs } + provider { defaultJvmArgs + app.nativeDistributions.appCds.runtimeJvmArgs() + app.jvmArgs } ) packageTask.launcherArgs.set(provider { app.args }) } diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts index a0e1fb2dba9..ad5428a0a79 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts @@ -26,12 +26,13 @@ compose.desktop { nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageVersion = "1.0.0" + + appCds { + mode = %APP_CDS_MODE% + logging = true + } } jvmArgs += "-Xshare:on" // This forces failure if AppCDS doesn't work - appCds { - mode = %APP_CDS_MODE% - logging = true - } } } From 6154135bc2c4c78a4426e6ab3ee81582470e65af Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 1 Jul 2025 21:05:31 +0300 Subject: [PATCH 06/20] Removed `logging` param from `appCds` block. --- .../compose/desktop/application/dsl/AppCdsSettings.kt | 11 +---------- .../test-projects/application/appCds/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 373676d6ef2..0390a13eae0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -12,21 +12,12 @@ abstract class AppCdsConfiguration { * The AppCDS mode to use. */ var mode: AppCdsMode = AppCdsMode.None - - /** - * Whether to ask the JVM to log AppCDS-related actions. - */ - @Suppress("MemberVisibilityCanBePrivate") - var logging: Boolean = false } /** * Returns the AppCDS-related arguments to pass the JVM when running the app. */ -internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { - addAll(mode.runtimeJvmArgs()) - if (logging) add("-Xlog:cds") -} +internal fun AppCdsConfiguration.runtimeJvmArgs() = mode.runtimeJvmArgs() /** * The mode of use of AppCDS. diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts index ad5428a0a79..259ffe8c000 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts @@ -29,10 +29,10 @@ compose.desktop { appCds { mode = %APP_CDS_MODE% - logging = true } } jvmArgs += "-Xshare:on" // This forces failure if AppCDS doesn't work + jvmArgs += "-Xlog:cds" } } From 5ed97e1eede3edb8ffc06845396100b0de4831dd Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 11:35:57 +0300 Subject: [PATCH 07/20] `createDistributable` task now depends on `createAppCdsArchive`. --- .../desktop/application/internal/JvmTasks.kt | 12 +++++-- .../internal/configureJvmApplication.kt | 31 +++++++++++++------ .../integration/DesktopApplicationTest.kt | 19 +++++++++++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt index 429f9c40c38..c2aac248d01 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmTasks.kt @@ -29,23 +29,31 @@ internal class JvmTasks( taskNameAction: String, taskNameObject: String = "", args: List = emptyList(), + isHidden: Boolean = false, noinline configureFn: T.() -> Unit = {} ): TaskProvider { val buildTypeClassifier = buildType.classifier.uppercaseFirstChar() val objectClassifier = taskNameObject.uppercaseFirstChar() val taskName = "$taskNameAction$buildTypeClassifier$objectClassifier" - return register(taskName, klass = T::class.java, args = args, configureFn = configureFn) + return register( + name = taskName, + klass = T::class.java, + args = args, + isHidden = isHidden, + configureFn = configureFn + ) } fun register( name: String, klass: Class, args: List, + isHidden: Boolean = false, configureFn: T.() -> Unit ): TaskProvider = project.tasks.register(name, klass, *args.toTypedArray()).apply { configure { task -> - task.group = taskGroup + task.group = if (isHidden) null else taskGroup task.configureFn() } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index edc41a2ec52..b17a059f0c2 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -6,6 +6,7 @@ package org.jetbrains.compose.desktop.application.internal import org.gradle.api.DefaultTask +import org.gradle.api.Task import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.provider.Provider import org.gradle.api.tasks.JavaExec @@ -130,10 +131,11 @@ private fun JvmApplicationContext.configurePackagingTasks( } } else null - val createDistributable = tasks.register( + val createDistributableImpl = tasks.register( taskNameAction = "create", - taskNameObject = "distributable", - args = listOf(TargetFormat.AppImage) + taskNameObject = "distributableImpl", + args = listOf(TargetFormat.AppImage), + isHidden = true, ) { configurePackageTask( this, @@ -150,13 +152,24 @@ private fun JvmApplicationContext.configurePackagingTasks( tasks.register( taskNameAction = "create", taskNameObject = "appCdsArchive", - args = listOf(createDistributable) + args = listOf(createDistributableImpl), + isHidden = true, ) { - dependsOn(createDistributable) + dependsOn(createDistributableImpl) this.appCdsMode.set(appCdsMode) } } else null + val createDistributable = tasks.register( + taskNameAction = "create", + taskNameObject = "distributable", + ) { + dependsOn(createDistributableImpl) + if (createAppCdsArchive != null) { + dependsOn(createAppCdsArchive) + } + } + val packageFormats = app.nativeDistributions.targetFormats.map { targetFormat -> val packageFormat = tasks.register( taskNameAction = "package", @@ -182,7 +195,7 @@ private fun JvmApplicationContext.configurePackagingTasks( } else { configurePackageTask( this, - createAppImage = createDistributable, + createAppImage = createDistributableImpl, checkRuntime = commonTasks.checkRuntime, unpackDefaultResources = commonTasks.unpackDefaultResources, createAppCdsArchive = createAppCdsArchive @@ -242,11 +255,9 @@ private fun JvmApplicationContext.configurePackagingTasks( val runDistributable = tasks.register( taskNameAction = "run", taskNameObject = "distributable", - args = listOf(createDistributable) + args = listOf(createDistributableImpl) ) { - if (createAppCdsArchive != null) { - dependsOn(createAppCdsArchive) - } + dependsOn(createDistributable) } val run = tasks.register(taskNameAction = "run") { diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 872084a2e81..82700ead736 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -35,7 +35,6 @@ import org.junit.jupiter.api.Test import java.io.File import java.util.* import java.util.jar.JarFile -import kotlin.test.assertTrue class DesktopApplicationTest : GradlePluginTestBase() { @Test @@ -671,6 +670,24 @@ class DesktopApplicationTest : GradlePluginTestBase() { testPackageAndRun(release = true) } + @Test + fun testAppCdsCreateDistributable() = with(appCdsProject(AppCdsMode.Prebuild, 17)) { + fun testPackageAndRun(release: Boolean) { + val releaseTag = if (release) "Release" else "" + val createDistributableTaskName = ":create${releaseTag}Distributable" + val createAppCdsTaskName = ":create${releaseTag}AppCdsArchive" + gradle(createDistributableTaskName).checks { + check.taskSuccessful(createDistributableTaskName) + check.taskSuccessful(createAppCdsTaskName) + check.logContains("[cds] Dumping shared data to file") + check.logContains("Running app to create archive: true") + } + } + + testPackageAndRun(release = false) + testPackageAndRun(release = true) + } + private fun TestProject.enableJoinOutputJars() { val enableJoinOutputJars = """ compose.desktop { From 75995284366bb03a731efc9801c10ab39cd44a73 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 11:45:32 +0300 Subject: [PATCH 08/20] Documented more drawbacks of AppCdsMode.Auto, and the archive creation process of AppCdsMode.Prebuild --- .../desktop/application/dsl/AppCdsSettings.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 0390a13eae0..8ad84116e39 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -86,16 +86,21 @@ abstract class AppCdsMode(val name: String) : Serializable { /** * AppCDS is used via a dynamic shared archive created automatically * when the app is run (using `-XX:+AutoCreateSharedArchive`). + * Due to the drawbacks below, this mode is mostly recommended only + * to experiment and see how fast your app starts up with AppCDS. + * In production, we recomment [Prebuild] mode. * - * Pros: + * Advantages: * - Simplest - no additional step is needed to build the archive. * - Creates a smaller distributable. * - * Cons: + * Drawbacks: * - Requires JDK 19 or later. * - The archive is not available at the first execution of the app, - * so it is slower. The archive is created when at shutdown time - * of the first execution, which also takes a little longer. + * so it is slower (and possibly even slower than regular execution), + * The archive is created when at shutdown time of the first execution, + * which also takes a little longer. + * - Some OSes may block the creation of the archive file at runtime. */ @Suppress("unused") val Auto = object : AppCdsMode("Auto") { @@ -120,11 +125,18 @@ abstract class AppCdsMode(val name: String) : Serializable { * AppCDS is used via a dynamic shared archive created by executing * the app before packaging (using `-XX:ArchiveClassesAtExit`). * - * Pros: + * When using this mode, the app will be run during the creation of + * the distributable. In this run, a system property + * `compose.appcds.create-archive` will be set to the value `true`. + * The app should "exercise" itself and cause the loading of all + * the classes that should be written into the AppCDS archive. It + * should then shut down in order to let the build process continue. + * + * Advantages: * - Can be used with JDKs earlier than 19. * - The first run of the distributed app is fast too. * - * Cons: + * Drawbacks: * - Requires an additional step of running the app when building the * distributable. * - The distributable is larger because it includes the archive of From 7e85ae7fe596a3782812b8d3e496b0219652e12a Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 14:04:25 +0300 Subject: [PATCH 09/20] Add app.jsa to packaged files --- .../desktop/application/dsl/AppCdsSettings.kt | 9 ++++---- .../internal/configureJvmApplication.kt | 1 + .../tasks/AbstractCreateAppCdsArchiveTask.kt | 17 ++++++++------ .../compose/internal/utils/osUtils.kt | 23 ++++++++----------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 8ad84116e39..c5fcb12a916 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -1,7 +1,8 @@ package org.jetbrains.compose.desktop.application.dsl +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile import org.jetbrains.compose.internal.utils.packagedAppJarFilesDir -import java.io.File import java.io.Serializable /** @@ -47,7 +48,7 @@ abstract class AppCdsMode(val name: String) : Serializable { * Returns the app's classes archive file, given the root directory of * the packaged app. */ - internal open fun appClassesArchiveFile(packagedAppRootDir: File): File = + internal open fun appClassesArchiveFile(packagedAppRootDir: Directory): RegularFile = error("AppCdsMode '$this' does not create an archive") /** @@ -151,9 +152,9 @@ abstract class AppCdsMode(val name: String) : Serializable { "-XX:ArchiveClassesAtExit=$ARCHIVE_FILE_ARGUMENT", "-Dcompose.appcds.create-archive=true" ) - override fun appClassesArchiveFile(packagedAppRootDir: File): File { + override fun appClassesArchiveFile(packagedAppRootDir: Directory): RegularFile { val appDir = packagedAppJarFilesDir(packagedAppRootDir) - return appDir.resolve(ARCHIVE_NAME) + return appDir.file(ARCHIVE_NAME) } override fun runtimeJvmArgs() = listOf( diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index b17a059f0c2..1dde62bc3e4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -366,6 +366,7 @@ private fun JvmApplicationContext.configurePackageTask( if (createAppCdsArchive != null) { packageTask.dependsOn(createAppCdsArchive) + packageTask.files.from(project.file(createAppCdsArchive.flatMap { it.appCdsArchiveFile })) } packageTask.launcherMainClass.set(provider { app.mainClass }) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index 94445201b81..8cc8f8780dc 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -2,6 +2,7 @@ package org.jetbrains.compose.desktop.application.tasks import org.gradle.api.file.Directory import org.gradle.api.file.FileTree +import org.gradle.api.file.RegularFile import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* @@ -25,11 +26,13 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( @get:Input internal abstract val appCdsMode: Property - @Suppress("unused") @get:OutputFile - val appCdsArchiveFile: File get() { - val packagedAppRootDir = packagedAppRootDir(appImageRootDir.get()) - return appCdsMode.get().appClassesArchiveFile(packagedAppRootDir) + val appCdsArchiveFile: Provider = provider { + val appImageRootDir = appImageRootDir.get() + val packageName = packageName.get() + val appCdsMode = appCdsMode.get() + val packagedAppRootDir = packagedAppRootDir(appImageRootDir, packageName) + appCdsMode.appClassesArchiveFile(packagedAppRootDir) } // This is needed to correctly describe the dependencies to Gradle. @@ -44,7 +47,7 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( } } - val appCdsArchiveFile = appCdsArchiveFile.relativeTo(appImageRootDir.get().asFile).path + val appCdsArchiveFile = appCdsArchiveFile.ioFile.relativeTo(appImageRootDir.get().asFile).path return appImageRootDir.get().asFileTree.matching { it.exclude(appCdsArchiveFile) } } @@ -54,9 +57,9 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( // AppCdsMode.runtimeJvmArgs with AppCdsMode.appClassesArchiveCreationJvmArgs // This must be done because, for example, -XX:SharedArchiveFile and // -XX:ArchiveClassesAtExit can't be used at the same time - val packagedRootDir = packagedAppRootDir(appImageRootDir.get()) + val packagedRootDir = packagedAppRootDir(appImageRootDir.get(), packageName = packageName.get()) val appDir = packagedAppJarFilesDir(packagedRootDir) - val cfgFile = appDir.resolve("${packageName.get()}.cfg") + val cfgFile = appDir.asFile.resolve("${packageName.get()}.cfg") val cfgFileTempCopy = File(cfgFile.parentFile, "${cfgFile.name}.tmp") // Save the original cfg file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt index 8855d858a41..1c8490b4b7b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt @@ -54,17 +54,12 @@ internal val currentOS: OS by lazy { internal fun executableName(nameWithoutExtension: String): String = if (currentOS == OS.Windows) "$nameWithoutExtension.exe" else nameWithoutExtension -internal fun packagedAppRootDir(appImageRootDir: Directory): File { - return appImageRootDir.asFile.let { appImageRoot -> - val files = appImageRoot.listFiles() - // Sometimes ".DS_Store" files are created on macOS, so ignore them. - ?.filterNot { it.name == ".DS_Store" } - if (files.isNullOrEmpty()) { - 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() +internal fun packagedAppRootDir(appImageRootDir: Directory, packageName: String): Directory { + val dirName = when (currentOS) { + OS.MacOS -> "$packageName.app" + else -> packageName } + return appImageRootDir.dir(dirName) } internal fun packagedAppExecutableName(packageName: String): String { @@ -76,8 +71,8 @@ internal fun packagedAppExecutableName(packageName: String): String { } } -internal fun packagedAppJarFilesDir(packagedRootDir: File): File { - return packagedRootDir.resolve( +internal fun packagedAppJarFilesDir(packagedRootDir: Directory): Directory { + return packagedRootDir.dir( when (currentOS) { OS.Linux -> "lib/app/" OS.Windows -> "app/" @@ -90,12 +85,12 @@ internal fun ExecOperations.executePackagedApp( appImageRootDir: Directory, packageName: String ) { - val workingDir = packagedAppRootDir(appImageRootDir = appImageRootDir) + val workingDir = packagedAppRootDir(appImageRootDir = appImageRootDir, packageName = packageName) val executableName = packagedAppExecutableName(packageName = packageName) exec { spec -> spec.workingDir(workingDir) - spec.executable(workingDir.resolve(executableName).absolutePath) + spec.executable(workingDir.asFile.resolve(executableName).absolutePath) }.assertNormalExitValue() } From ac1c2e8e563e1e3a85815dd6b82893e7b86948b4 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 16:00:59 +0300 Subject: [PATCH 10/20] Set zip entry time when transforming jar, so that identical inputs results in identical outputs --- .../desktop/application/internal/files/fileUtils.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt index 64a314ad220..d11fe2773ce 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/fileUtils.kt @@ -48,7 +48,7 @@ internal fun File.contentHash(): String { private fun MessageDigest.digestContent(file: File) { file.inputStream().buffered().use { fis -> DigestInputStream(fis, this).use { ds -> - while (ds.read() != -1) {} + ds.readAllBytes() } } } @@ -75,6 +75,11 @@ internal fun copyZipEntry( val newEntry = ZipEntry(entry.name).apply { comment = entry.comment extra = entry.extra + // Need to preserve timestamp so that identical inputs result in identical outputs + // (the jar files are hashed) + entry.time.let { + time = if (it == -1L) 0L else it + } } to.withNewEntry(newEntry) { from.copyTo(to) From 34ef580228e3036b6554958888cc21bf840b9f82 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 16:20:19 +0300 Subject: [PATCH 11/20] Add more documentation to AppCdsMode.Auto --- .../compose/desktop/application/dsl/AppCdsSettings.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index c5fcb12a916..ab02001e878 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -101,7 +101,8 @@ abstract class AppCdsMode(val name: String) : Serializable { * so it is slower (and possibly even slower than regular execution), * The archive is created when at shutdown time of the first execution, * which also takes a little longer. - * - Some OSes may block the creation of the archive file at runtime. + * - Some OSes may block writing the archive file to the application's + * directory at runtime. */ @Suppress("unused") val Auto = object : AppCdsMode("Auto") { From 44330bd57b745a9daeb96582d142a6a5492e3fbc Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 19:51:17 +0300 Subject: [PATCH 12/20] Add `-Xlog:cds` JVM argument when building in non-release mode, and when running the app to create the archive in Prebuild mode. --- .../compose/desktop/application/dsl/AppCdsSettings.kt | 11 +++++++++-- .../application/dsl/JvmApplicationBuildTypes.kt | 5 +++++ .../application/internal/configureJvmApplication.kt | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index ab02001e878..c4239b9229b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -2,6 +2,7 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile +import org.jetbrains.compose.desktop.application.internal.JvmApplicationContext import org.jetbrains.compose.internal.utils.packagedAppJarFilesDir import java.io.Serializable @@ -18,7 +19,12 @@ abstract class AppCdsConfiguration { /** * Returns the AppCDS-related arguments to pass the JVM when running the app. */ -internal fun AppCdsConfiguration.runtimeJvmArgs() = mode.runtimeJvmArgs() +internal fun AppCdsConfiguration.runtimeJvmArgs(context: JvmApplicationContext) = buildList { + addAll(mode.runtimeJvmArgs()) + if (context.buildType.cdsLogging.get()) { + add("-Xlog:cds") + } +} /** * The mode of use of AppCDS. @@ -151,7 +157,8 @@ abstract class AppCdsMode(val name: String) : Serializable { override fun appClassesArchiveCreationJvmArgs() = listOf( "-XX:ArchiveClassesAtExit=$ARCHIVE_FILE_ARGUMENT", - "-Dcompose.appcds.create-archive=true" + "-Dcompose.appcds.create-archive=true", + "-Xlog:cds" ) override fun appClassesArchiveFile(packagedAppRootDir: Directory): RegularFile { val appDir = packagedAppJarFilesDir(packagedAppRootDir) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt index d542b3fae57..d89779af82b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt @@ -7,7 +7,9 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.jetbrains.compose.internal.utils.new +import org.jetbrains.compose.internal.utils.notNullProperty import javax.inject.Inject abstract class JvmApplicationBuildTypes @Inject constructor( @@ -25,6 +27,7 @@ abstract class JvmApplicationBuildTypes @Inject constructor( val release: JvmApplicationBuildType = objects.new("release").apply { proguard.isEnabled.set(true) + cdsLogging.set(false) } fun release(fn: Action) { fn.execute(release) @@ -43,4 +46,6 @@ abstract class JvmApplicationBuildType @Inject constructor( fun proguard(fn: Action) { fn.execute(proguard) } + + val cdsLogging: Property = objects.notNullProperty(true) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 1dde62bc3e4..3fa60f282d5 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -371,7 +371,7 @@ private fun JvmApplicationContext.configurePackageTask( packageTask.launcherMainClass.set(provider { app.mainClass }) packageTask.launcherJvmArgs.set( - provider { defaultJvmArgs + app.nativeDistributions.appCds.runtimeJvmArgs() + app.jvmArgs } + provider { defaultJvmArgs + app.nativeDistributions.appCds.runtimeJvmArgs(this) + app.jvmArgs } ) packageTask.launcherArgs.set(provider { app.args }) } From 216b85a9d41d36dcfc8fd623aedbcdbbca8f9205 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 2 Jul 2025 19:54:38 +0300 Subject: [PATCH 13/20] Add `exitAppOnCdsFailure` parameter to `AppCdsConfiguration` --- .../compose/desktop/application/dsl/AppCdsSettings.kt | 8 ++++++++ .../test-projects/application/appCds/build.gradle.kts | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index c4239b9229b..139b4478d7f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -14,6 +14,11 @@ abstract class AppCdsConfiguration { * The AppCDS mode to use. */ var mode: AppCdsMode = AppCdsMode.None + + /** + * Whether to fail running the app if unable to load the AppCDS archive. + */ + var exitAppOnCdsFailure: Boolean = false } /** @@ -21,6 +26,9 @@ abstract class AppCdsConfiguration { */ internal fun AppCdsConfiguration.runtimeJvmArgs(context: JvmApplicationContext) = buildList { addAll(mode.runtimeJvmArgs()) + if (exitAppOnCdsFailure) { + add("-Xshare:on") + } if (context.buildType.cdsLogging.get()) { add("-Xlog:cds") } diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts index 259ffe8c000..4ccc02c7d3e 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts @@ -29,10 +29,9 @@ compose.desktop { appCds { mode = %APP_CDS_MODE% + exitAppOnCdsFailure = true } } - - jvmArgs += "-Xshare:on" // This forces failure if AppCDS doesn't work jvmArgs += "-Xlog:cds" } } From 9b58449c089717a2c8cf784402a59c32b12d8ff9 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Fri, 4 Jul 2025 01:24:06 +0300 Subject: [PATCH 14/20] Added documentation on what AppCDS is. --- .../compose/desktop/application/dsl/AppCdsSettings.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 139b4478d7f..66857e2237d 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -8,6 +8,10 @@ import java.io.Serializable /** * The configuration of AppCDS for the native distribution. + * + * AppCDS is a JVM mechanism that allows to significantly speed up application + * startup by creating an archive of the classes it uses that can be loaded + * and used much faster than class files. */ abstract class AppCdsConfiguration { /** From 3f5edad0e49a6441626f8e2d487b49bb8c0a5df6 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Fri, 4 Jul 2025 18:29:42 +0300 Subject: [PATCH 15/20] Added sample code to detect and react to `compose.appcds.create-archive` --- .../desktop/application/dsl/AppCdsSettings.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 66857e2237d..e84ad067af4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -152,6 +152,19 @@ abstract class AppCdsMode(val name: String) : Serializable { * the classes that should be written into the AppCDS archive. It * should then shut down in order to let the build process continue. * + * For example: + * ``` + * application { + * ... + * if (System.getProperty("compose.appcds.create-archive") == "true") { + * LaunchedEffect(Unit) { + * delay(10.seconds) // Or a custom event indicating startup finished + * exitApplication() + * } + * } + * } + * ``` + * * Advantages: * - Can be used with JDKs earlier than 19. * - The first run of the distributed app is fast too. From 39abf0416b0eedfd7687fa8d97756966e6ff6d1a Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Fri, 4 Jul 2025 22:59:28 +0300 Subject: [PATCH 16/20] Move `appCds` block into buildType --- .../desktop/application/dsl/AppCdsSettings.kt | 10 +++-- .../dsl/JvmApplicationBuildTypes.kt | 13 ++++--- .../dsl/JvmApplicationDistributions.kt | 6 --- .../internal/JvmApplicationContext.kt | 4 ++ .../internal/configureJvmApplication.kt | 38 +++++++++---------- .../AbstractCheckNativeDistributionRuntime.kt | 15 ++++---- .../integration/DesktopApplicationTest.kt | 13 +++---- .../application/appCds/build.gradle.kts | 5 ++- 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index e84ad067af4..a57d3769bad 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -2,7 +2,6 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile -import org.jetbrains.compose.desktop.application.internal.JvmApplicationContext import org.jetbrains.compose.internal.utils.packagedAppJarFilesDir import java.io.Serializable @@ -19,6 +18,11 @@ abstract class AppCdsConfiguration { */ var mode: AppCdsMode = AppCdsMode.None + /** + * Whether to print AppCDS-related messages at application runtime. + */ + var logging: Boolean = false + /** * Whether to fail running the app if unable to load the AppCDS archive. */ @@ -28,12 +32,12 @@ abstract class AppCdsConfiguration { /** * Returns the AppCDS-related arguments to pass the JVM when running the app. */ -internal fun AppCdsConfiguration.runtimeJvmArgs(context: JvmApplicationContext) = buildList { +internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { addAll(mode.runtimeJvmArgs()) if (exitAppOnCdsFailure) { add("-Xshare:on") } - if (context.buildType.cdsLogging.get()) { + if (logging) { add("-Xlog:cds") } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt index d89779af82b..de86cd083ff 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationBuildTypes.kt @@ -7,14 +7,13 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property import org.jetbrains.compose.internal.utils.new -import org.jetbrains.compose.internal.utils.notNullProperty +import java.io.Serializable import javax.inject.Inject abstract class JvmApplicationBuildTypes @Inject constructor( objects: ObjectFactory -) { +): Serializable { /** * The default build type does not have a classifier * to preserve compatibility with tasks, existing before @@ -27,11 +26,12 @@ abstract class JvmApplicationBuildTypes @Inject constructor( val release: JvmApplicationBuildType = objects.new("release").apply { proguard.isEnabled.set(true) - cdsLogging.set(false) } fun release(fn: Action) { fn.execute(release) } + + internal val all: List = listOf(default, release) } abstract class JvmApplicationBuildType @Inject constructor( @@ -47,5 +47,8 @@ abstract class JvmApplicationBuildType @Inject constructor( fn.execute(proguard) } - val cdsLogging: Property = objects.notNullProperty(true) + val appCds: AppCdsConfiguration = objects.new() + fun appCds(fn: Action) { + fn.execute(appCds) + } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt index cfa2212e0b0..f59b838b6a0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt @@ -43,10 +43,4 @@ abstract class JvmApplicationDistributions : AbstractDistributions() { windows.fileAssociation(mimeType, extension, description, windowsIconFile) macOS.fileAssociation(mimeType, extension, description, macOSIconFile) } - - val appCds: AppCdsConfiguration = objects.newInstance(AppCdsConfiguration::class.java) - fun appCds(fn: Action) { - fn.execute(appCds) - } - } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt index dac795b4e4c..7245c6549ce 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/JvmApplicationContext.kt @@ -10,6 +10,7 @@ import org.gradle.api.Task import org.gradle.api.file.Directory import org.gradle.api.provider.Provider import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildType +import org.jetbrains.compose.desktop.application.dsl.JvmApplicationBuildTypes import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID import org.jetbrains.compose.internal.javaSourceSets @@ -34,6 +35,9 @@ internal data class JvmApplicationContext( "compose/tmp/$appDirName" ) + val buildTypes: JvmApplicationBuildTypes + get() = appInternal.buildTypes + fun T.useAppRuntimeFiles(fn: T.(JvmApplicationRuntimeFiles) -> Unit) { val runtimeFiles = app.jvmApplicationRuntimeFilesProvider?.jvmApplicationRuntimeFiles(project) ?: JvmApplicationRuntimeFiles( diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 3fa60f282d5..9286649abb1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -47,7 +47,6 @@ internal class CommonJvmDesktopTasks( val checkRuntime: TaskProvider, val suggestRuntimeModules: TaskProvider, val prepareAppResources: TaskProvider, - val createRuntimeImage: TaskProvider ) private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDesktopTasks { @@ -61,7 +60,7 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes taskNameObject = "runtime" ) { jdkHome.set(app.javaHomeProvider) - appCdsMode.set(app.nativeDistributions.appCds.mode) + appCdsModes.set(this@configureCommonJvmDesktopTasks.buildTypes.all.map { it.appCds.mode }) checkJdkVendor.set(ComposeProperties.checkJdkVendor(project.providers)) jdkVersionProbeJar.from( project.detachedComposeGradleDependency( @@ -97,31 +96,30 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes into(jvmTmpDirForTask()) } - val createRuntimeImage = tasks.register( - taskNameAction = "create", - taskNameObject = "runtimeImage" - ) { - dependsOn(checkRuntime) - javaHome.set(app.javaHomeProvider) - modules.set(provider { app.nativeDistributions.modules }) - includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) - javaRuntimePropertiesFile.set(checkRuntime.flatMap { it.javaRuntimePropertiesFile }) - destinationDir.set(appTmpDir.dir("runtime")) - generateCdsArchive.set(app.nativeDistributions.appCds.mode.generateJreClassesArchive) - } - return CommonJvmDesktopTasks( unpackDefaultResources = unpackDefaultResources, checkRuntime = checkRuntime, suggestRuntimeModules = suggestRuntimeModules, prepareAppResources = prepareAppResources, - createRuntimeImage = createRuntimeImage, ) } private fun JvmApplicationContext.configurePackagingTasks( commonTasks: CommonJvmDesktopTasks ) { + val createRuntimeImage = tasks.register( + taskNameAction = "create", + taskNameObject = "runtimeImage" + ) { + dependsOn(commonTasks.checkRuntime) + javaHome.set(app.javaHomeProvider) + modules.set(provider { app.nativeDistributions.modules }) + includeAllModules.set(provider { app.nativeDistributions.includeAllModules }) + javaRuntimePropertiesFile.set(commonTasks.checkRuntime.flatMap { it.javaRuntimePropertiesFile }) + destinationDir.set(appTmpDir.dir("runtime")) + generateCdsArchive.set(buildType.appCds.mode.generateJreClassesArchive) + } + val runProguard = if (buildType.proguard.isEnabled.orNull == true) { tasks.register( taskNameAction = "proguard", @@ -139,7 +137,7 @@ private fun JvmApplicationContext.configurePackagingTasks( ) { configurePackageTask( this, - createRuntimeImage = commonTasks.createRuntimeImage, + createRuntimeImage = createRuntimeImage, prepareAppResources = commonTasks.prepareAppResources, checkRuntime = commonTasks.checkRuntime, unpackDefaultResources = commonTasks.unpackDefaultResources, @@ -147,7 +145,7 @@ private fun JvmApplicationContext.configurePackagingTasks( ) } - val appCdsMode = app.nativeDistributions.appCds.mode + val appCdsMode = buildType.appCds.mode val createAppCdsArchive = if (appCdsMode.generateAppClassesArchive) { tasks.register( taskNameAction = "create", @@ -185,7 +183,7 @@ private fun JvmApplicationContext.configurePackagingTasks( if (currentOS != OS.MacOS) { configurePackageTask( this, - createRuntimeImage = commonTasks.createRuntimeImage, + createRuntimeImage = createRuntimeImage, prepareAppResources = commonTasks.prepareAppResources, checkRuntime = commonTasks.checkRuntime, unpackDefaultResources = commonTasks.unpackDefaultResources, @@ -371,7 +369,7 @@ private fun JvmApplicationContext.configurePackageTask( packageTask.launcherMainClass.set(provider { app.mainClass }) packageTask.launcherJvmArgs.set( - provider { defaultJvmArgs + app.nativeDistributions.appCds.runtimeJvmArgs(this) + app.jvmArgs } + provider { defaultJvmArgs + buildType.appCds.runtimeJvmArgs() + app.jvmArgs } ) packageTask.launcherArgs.set(provider { app.args }) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt index 7ad47354842..885d9f080f1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt @@ -7,20 +7,17 @@ package org.jetbrains.compose.desktop.application.tasks import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.jetbrains.compose.desktop.application.dsl.AppCdsMode import org.jetbrains.compose.desktop.application.internal.ComposeProperties -import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner import org.jetbrains.compose.desktop.application.internal.JdkVersionProbe +import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask -import org.jetbrains.compose.internal.utils.OS -import org.jetbrains.compose.internal.utils.currentOS -import org.jetbrains.compose.internal.utils.executableName -import org.jetbrains.compose.internal.utils.ioFile -import org.jetbrains.compose.internal.utils.notNullProperty +import org.jetbrains.compose.internal.utils.* import java.io.ByteArrayInputStream import java.io.File import java.util.* @@ -41,7 +38,7 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa abstract val checkJdkVendor: Property @get:Input - val appCdsMode: Property = objects.notNullProperty() + val appCdsModes: ListProperty = objects.listProperty(AppCdsMode::class.java) private val taskDir = project.layout.buildDirectory.dir("compose/tmp/$name") @@ -113,7 +110,9 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa } } - appCdsMode.get().checkJdkCompatibility(jdkMajorVersion) + for (appCdsMode in appCdsModes.get()) { + appCdsMode.checkJdkCompatibility(jdkMajorVersion) + } val modules = arrayListOf() runExternalTool( diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt index 82700ead736..38747f3674e 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/DesktopApplicationTest.kt @@ -590,14 +590,14 @@ class DesktopApplicationTest : GradlePluginTestBase() { } @Test - fun testAppCdsAutoFailsOnJdk17() = with(appCdsProject(AppCdsMode.Auto, 17)) { + fun testAppCdsAutoFailsOnJdk17() = with(appCdsProject(AppCdsMode.Auto, javaVersion = 17)) { fun testRunTask(runTask: String) { gradleFailure(runTask).checks { check.logContains("AppCdsMode 'Auto' is not supported on JDK earlier than 19; current is 17") } } - testRunTask(":runDistributable") + testRunTask(":runReleaseDistributable") } private val loggedArchivePathRegex = @@ -617,7 +617,7 @@ class DesktopApplicationTest : GradlePluginTestBase() { ) @Test - fun testAppCdsAuto() = with(appCdsProject(AppCdsMode.Auto, 21)) { + fun testAppCdsAuto() = with(appCdsProject(AppCdsMode.Auto, javaVersion = 21)) { fun testRunTask(taskName: String) { gradle(taskName).checks { check.taskSuccessful(taskName) @@ -635,12 +635,11 @@ class DesktopApplicationTest : GradlePluginTestBase() { gradle(":clean") } - testRunTask(":runDistributable") testRunTask(":runReleaseDistributable") } @Test - fun testAppCdsPrebuild() = with(appCdsProject(AppCdsMode.Prebuild, 17)) { + fun testAppCdsPrebuild() = with(appCdsProject(AppCdsMode.Prebuild, javaVersion = 17)) { fun testPackageAndRun(release: Boolean) { val releaseTag = if (release) "Release" else "" val packageTaskName = ":package${releaseTag}DistributionForCurrentOS" @@ -666,12 +665,11 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } - testPackageAndRun(release = false) testPackageAndRun(release = true) } @Test - fun testAppCdsCreateDistributable() = with(appCdsProject(AppCdsMode.Prebuild, 17)) { + fun testAppCdsCreateDistributable() = with(appCdsProject(AppCdsMode.Prebuild, javaVersion = 17)) { fun testPackageAndRun(release: Boolean) { val releaseTag = if (release) "Release" else "" val createDistributableTaskName = ":create${releaseTag}Distributable" @@ -684,7 +682,6 @@ class DesktopApplicationTest : GradlePluginTestBase() { } } - testPackageAndRun(release = false) testPackageAndRun(release = true) } diff --git a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts index 4ccc02c7d3e..fc9fbae135f 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts +++ b/gradle-plugins/compose/src/test/test-projects/application/appCds/build.gradle.kts @@ -26,12 +26,13 @@ compose.desktop { nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageVersion = "1.0.0" - + } + buildTypes.release { appCds { mode = %APP_CDS_MODE% + logging = true exitAppOnCdsFailure = true } } - jvmArgs += "-Xlog:cds" } } From e18e47d12f59f9e38391c7051320974e50b01489 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 8 Jul 2025 10:56:41 +0300 Subject: [PATCH 17/20] Run app with relative classpath --- .../tasks/AbstractCreateAppCdsArchiveTask.kt | 18 ++++++++++++++++-- .../application/tasks/AbstractJPackageTask.kt | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index 8cc8f8780dc..ea2c24ebadf 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -62,7 +62,21 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( val cfgFile = appDir.asFile.resolve("${packageName.get()}.cfg") val cfgFileTempCopy = File(cfgFile.parentFile, "${cfgFile.name}.tmp") - // Save the original cfg file + // Edit the cfg file, changing $APPDATA to a relative path + val cfgFileContentsWithRelativeAppData = buildString { + cfgFile.useLines { lines -> + lines.forEach { line -> + if (line.startsWith("app.classpath=\$APPDIR/")) { + appendLine(line.replace("\$APPDIR/", "")) + } else { + appendLine(line) + } + } + } + } + cfgFile.writeText(cfgFileContentsWithRelativeAppData) + + // Save the cfg file before making changes for the AppCDS archive-creating run Files.copy(cfgFile.toPath(), cfgFileTempCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) try { // Edit the cfg file @@ -91,7 +105,7 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( packageName = packageName.get() ) } finally { - // Restore the original cfg file + // Restore the cfg file Files.move(cfgFileTempCopy.toPath(), cfgFile.toPath(), StandardCopyOption.REPLACE_EXISTING) } } 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 d1935673dbd..b8409bac298 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 @@ -390,6 +390,7 @@ abstract class AbstractJPackageTask @Inject constructor( if (targetFormat == TargetFormat.AppImage || appImage.orNull == null) { // Args, that can only be used, when creating an app image or an installer w/o --app-image parameter cliArg("--input", libsDir) + javaOption("-Duser.dir=\$APPDIR") cliArg("--runtime-image", runtimeImage) cliArg("--resource-dir", jpackageResources) From a660deff460144bd2e76963965aab68d55a497e3 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 8 Jul 2025 21:06:27 +0300 Subject: [PATCH 18/20] Build all platforms from app image. --- .../desktop/application/dsl/AppCdsSettings.kt | 37 +++++++++++-------- .../internal/configureJvmApplication.kt | 32 ++++------------ .../AbstractCheckNativeDistributionRuntime.kt | 4 +- .../tasks/AbstractCreateAppCdsArchiveTask.kt | 14 ------- .../application/tasks/AbstractJPackageTask.kt | 11 +++--- 5 files changed, 35 insertions(+), 63 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index a57d3769bad..e3f704af8d4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -47,6 +47,11 @@ internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { */ abstract class AppCdsMode(val name: String) : Serializable { + /** + * The minimum JDK version for which this mode is supported. + */ + internal open val minJdkVersion: Int? = null + /** * Whether to generate a classes.jsa archive for the JRE classes. */ @@ -82,7 +87,15 @@ abstract class AppCdsMode(val name: String) : Serializable { * Checks whether this mode is compatible with the given JDK major version. * Throws an exception if not. */ - internal open fun checkJdkCompatibility(jdkMajorVersion: Int) = Unit + internal open fun checkJdkCompatibility(jdkMajorVersion: Int, jdkVendor: String) { + val minMajorJdkVersion = minJdkVersion ?: return + if (jdkMajorVersion < minMajorJdkVersion) { + error( + "AppCdsMode '$this' is not supported on JDK earlier than" + + " $minMajorJdkVersion; current is $jdkMajorVersion" + ) + } + } override fun toString() = name @@ -109,9 +122,6 @@ abstract class AppCdsMode(val name: String) : Serializable { /** * AppCDS is used via a dynamic shared archive created automatically * when the app is run (using `-XX:+AutoCreateSharedArchive`). - * Due to the drawbacks below, this mode is mostly recommended only - * to experiment and see how fast your app starts up with AppCDS. - * In production, we recomment [Prebuild] mode. * * Advantages: * - Simplest - no additional step is needed to build the archive. @@ -124,25 +134,20 @@ abstract class AppCdsMode(val name: String) : Serializable { * The archive is created when at shutdown time of the first execution, * which also takes a little longer. * - Some OSes may block writing the archive file to the application's - * directory at runtime. + * directory at runtime. Due to this, `Auto` mode is mostly recommended + * only to experiment and see how fast your app starts up with AppCDS. + * In production, we recommend [Prebuild] mode. + * */ @Suppress("unused") val Auto = object : AppCdsMode("Auto") { - private val MIN_JDK_VERSION = 19 + override val minJdkVersion = 19 override val generateJreClassesArchive: Boolean get() = true override fun runtimeJvmArgs() = listOf( "-XX:SharedArchiveFile=$ARCHIVE_FILE_ARGUMENT", "-XX:+AutoCreateSharedArchive" ) - override fun checkJdkCompatibility(jdkMajorVersion: Int) { - if (jdkMajorVersion < MIN_JDK_VERSION) { - error( - "AppCdsMode '$this' is not supported on JDK earlier than" + - " $MIN_JDK_VERSION; current is $jdkMajorVersion" - ) - } - } } /** @@ -170,10 +175,10 @@ abstract class AppCdsMode(val name: String) : Serializable { * ``` * * Advantages: - * - Can be used with JDKs earlier than 19. * - The first run of the distributed app is fast too. * * Drawbacks: + * - Requires JDK 22 or later. * - Requires an additional step of running the app when building the * distributable. * - The distributable is larger because it includes the archive of @@ -181,13 +186,13 @@ abstract class AppCdsMode(val name: String) : Serializable { */ @Suppress("unused") val Prebuild = object : AppCdsMode("Prebuild") { + override val minJdkVersion = 21 // required due to https://bugs.openjdk.org/browse/JDK-8279366 override val generateJreClassesArchive: Boolean get() = true override val generateAppClassesArchive: Boolean get() = true override fun appClassesArchiveCreationJvmArgs() = listOf( "-XX:ArchiveClassesAtExit=$ARCHIVE_FILE_ARGUMENT", "-Dcompose.appcds.create-archive=true", - "-Xlog:cds" ) override fun appClassesArchiveFile(packagedAppRootDir: Directory): RegularFile { val appDir = packagedAppJarFilesDir(packagedAppRootDir) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 9286649abb1..7b20fc6bf1b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -174,31 +174,13 @@ private fun JvmApplicationContext.configurePackagingTasks( taskNameObject = targetFormat.name, args = listOf(targetFormat) ) { - // On Mac we want to patch bundled Info.plist file, - // so we create an app image, change its Info.plist, - // then create an installer based on the app image. - // We could create an installer the same way on other platforms, but - // in some cases there are failures with JDK 15. - // See [AbstractJPackageTask.patchInfoPlistIfNeeded] - if (currentOS != OS.MacOS) { - configurePackageTask( - this, - createRuntimeImage = createRuntimeImage, - prepareAppResources = commonTasks.prepareAppResources, - checkRuntime = commonTasks.checkRuntime, - unpackDefaultResources = commonTasks.unpackDefaultResources, - runProguard = runProguard, - createAppCdsArchive = createAppCdsArchive - ) - } else { - configurePackageTask( - this, - createAppImage = createDistributableImpl, - checkRuntime = commonTasks.checkRuntime, - unpackDefaultResources = commonTasks.unpackDefaultResources, - createAppCdsArchive = createAppCdsArchive - ) - } + configurePackageTask( + this, + createAppImage = createDistributableImpl, + checkRuntime = commonTasks.checkRuntime, + unpackDefaultResources = commonTasks.unpackDefaultResources, + createAppCdsArchive = createAppCdsArchive + ) } if (targetFormat.isCompatibleWith(OS.MacOS)) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt index 885d9f080f1..196801a3f27 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt @@ -92,8 +92,8 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa ) } + val vendor = jdkRuntimeProperties.getProperty(JdkVersionProbe.JDK_VENDOR_KEY) if (checkJdkVendor.get()) { - val vendor = jdkRuntimeProperties.getProperty(JdkVersionProbe.JDK_VENDOR_KEY) if (vendor == null) { logger.warn("JDK vendor probe failed: $jdkHome") } else { @@ -111,7 +111,7 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa } for (appCdsMode in appCdsModes.get()) { - appCdsMode.checkJdkCompatibility(jdkMajorVersion) + appCdsMode.checkJdkCompatibility(jdkMajorVersion, vendor) } val modules = arrayListOf() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt index ea2c24ebadf..62323091177 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCreateAppCdsArchiveTask.kt @@ -62,20 +62,6 @@ abstract class AbstractCreateAppCdsArchiveTask @Inject constructor( val cfgFile = appDir.asFile.resolve("${packageName.get()}.cfg") val cfgFileTempCopy = File(cfgFile.parentFile, "${cfgFile.name}.tmp") - // Edit the cfg file, changing $APPDATA to a relative path - val cfgFileContentsWithRelativeAppData = buildString { - cfgFile.useLines { lines -> - lines.forEach { line -> - if (line.startsWith("app.classpath=\$APPDIR/")) { - appendLine(line.replace("\$APPDIR/", "")) - } else { - appendLine(line) - } - } - } - } - cfgFile.writeText(cfgFileContentsWithRelativeAppData) - // Save the cfg file before making changes for the AppCDS archive-creating run Files.copy(cfgFile.toPath(), cfgFileTempCopy.toPath(), StandardCopyOption.REPLACE_EXISTING) try { 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 b8409bac298..bea4f758d87 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 @@ -390,7 +390,6 @@ abstract class AbstractJPackageTask @Inject constructor( if (targetFormat == TargetFormat.AppImage || appImage.orNull == null) { // Args, that can only be used, when creating an app image or an installer w/o --app-image parameter cliArg("--input", libsDir) - javaOption("-Duser.dir=\$APPDIR") cliArg("--runtime-image", runtimeImage) cliArg("--resource-dir", jpackageResources) @@ -425,12 +424,12 @@ abstract class AbstractJPackageTask @Inject constructor( if (targetFormat != TargetFormat.AppImage) { // Args, that can only be used, when creating an installer - if (currentOS == OS.MacOS && jvmRuntimeInfo.majorVersion >= 18) { - // This is needed to prevent a directory does not exist error. - cliArg("--app-image", appImage.dir("${packageName.get()}.app")) - } else { - cliArg("--app-image", appImage) + val appImageDir = when { + jvmRuntimeInfo.majorVersion < 18 -> appImage + currentOS == OS.MacOS -> appImage.dir("${packageName.get()}.app") + else -> appImage.dir(packageName.get()) } + cliArg("--app-image", appImageDir) cliArg("--install-dir", installationPath) cliArg("--license-file", licenseFile) cliArg("--resource-dir", jpackageResources) From 79437f9745a88201cd892cba896827f15f1f7b69 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 9 Jul 2025 17:54:22 +0300 Subject: [PATCH 19/20] Don't set `-Xshare:on` if AppCDS mode is None. --- .../jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index e3f704af8d4..9e5073e5ff7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -34,7 +34,7 @@ abstract class AppCdsConfiguration { */ internal fun AppCdsConfiguration.runtimeJvmArgs() = buildList { addAll(mode.runtimeJvmArgs()) - if (exitAppOnCdsFailure) { + if (exitAppOnCdsFailure && (mode != AppCdsMode.None)) { add("-Xshare:on") } if (logging) { From 7176c6c3f9aece1adb13b2e86e4c2211cfdfd2d0 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 22 Jul 2025 20:36:39 +0300 Subject: [PATCH 20/20] Use `-XX:+NoClasspathInArchive` --- .../desktop/application/dsl/AppCdsSettings.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt index 9e5073e5ff7..4878d8f8be6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/AppCdsSettings.kt @@ -178,7 +178,7 @@ abstract class AppCdsMode(val name: String) : Serializable { * - The first run of the distributed app is fast too. * * Drawbacks: - * - Requires JDK 22 or later. + * - Requires JDK 21 or later. * - Requires an additional step of running the app when building the * distributable. * - The distributable is larger because it includes the archive of @@ -186,12 +186,22 @@ abstract class AppCdsMode(val name: String) : Serializable { */ @Suppress("unused") val Prebuild = object : AppCdsMode("Prebuild") { - override val minJdkVersion = 21 // required due to https://bugs.openjdk.org/browse/JDK-8279366 + override val minJdkVersion = 21 + override fun checkJdkCompatibility(jdkMajorVersion: Int, jdkVendor: String) { + super.checkJdkCompatibility(jdkMajorVersion, jdkVendor) +// if (jdkVendor != "JetBrains s.r.o.") { +// error( +// "Prebuild AppCDS mode is only supported on JetBrains JDK; " + +// "current vendor is '$jdkVendor'" +// ) +// } + } override val generateJreClassesArchive: Boolean get() = true override val generateAppClassesArchive: Boolean get() = true override fun appClassesArchiveCreationJvmArgs() = listOf( "-XX:ArchiveClassesAtExit=$ARCHIVE_FILE_ARGUMENT", + "-XX:+NoClasspathInArchive", "-Dcompose.appcds.create-archive=true", ) override fun appClassesArchiveFile(packagedAppRootDir: Directory): RegularFile { @@ -201,6 +211,7 @@ abstract class AppCdsMode(val name: String) : Serializable { override fun runtimeJvmArgs() = listOf( "-XX:SharedArchiveFile=$ARCHIVE_FILE_ARGUMENT", + "-XX:+NoClasspathInArchive", ) } }