Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c815098
Implemented support for App CDS.
m-sasha Jun 17, 2025
6aad554
Address PR feedback.
m-sasha Jun 19, 2025
a8e376f
Change AbstractCreateAppCdsArchiveTask.dependencyFiles to be a regula…
m-sasha Jun 20, 2025
ef4b437
Added AppCDS tests.
m-sasha Jun 21, 2025
c55d5c4
Moved `appCds` block into `nativeDistributions`
m-sasha Jul 1, 2025
6154135
Removed `logging` param from `appCds` block.
m-sasha Jul 1, 2025
5ed97e1
`createDistributable` task now depends on `createAppCdsArchive`.
m-sasha Jul 2, 2025
7599528
Documented more drawbacks of AppCdsMode.Auto, and the archive creatio…
m-sasha Jul 2, 2025
7e85ae7
Add app.jsa to packaged files
m-sasha Jul 2, 2025
ac1c2e8
Set zip entry time when transforming jar, so that identical inputs re…
m-sasha Jul 2, 2025
34ef580
Add more documentation to AppCdsMode.Auto
m-sasha Jul 2, 2025
44330bd
Add `-Xlog:cds` JVM argument when building in non-release mode, and w…
m-sasha Jul 2, 2025
216b85a
Add `exitAppOnCdsFailure` parameter to `AppCdsConfiguration`
m-sasha Jul 2, 2025
9b58449
Added documentation on what AppCDS is.
m-sasha Jul 3, 2025
3f5edad
Added sample code to detect and react to `compose.appcds.create-archive`
m-sasha Jul 4, 2025
39abf04
Move `appCds` block into buildType
m-sasha Jul 4, 2025
e18e47d
Run app with relative classpath
m-sasha Jul 8, 2025
a660def
Build all platforms from app image.
m-sasha Jul 8, 2025
79437f9
Don't set `-Xshare:on` if AppCDS mode is None.
m-sasha Jul 9, 2025
7176c6c
Use `-XX:+NoClasspathInArchive`
m-sasha Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package org.jetbrains.compose.desktop.application.dsl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After merging the PR, please close the user PR #2080


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not add it to the DSL. Users are able to add the logging flag by their own if it is needed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure as well, but this is a very useful feature, and most developers aren't familiar with AppCDS or its flags. So I think it's worhwhile; it will reduce the amount of questions ("Why doesn't it work for me?", "How do I know whether it works?") asked of us.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add it to the javadoc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nobody reads the javadoc :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. We say about the case when user already need to debug something. How will they find the option?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a flag in the primary DSL of the API it's much more visible in the documentation/examples than a side-note.

Also, the IDE helps discoverability:

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading the API, without reading the implementation, I had the same questions why we need a separate logging property for a separate feature. By this logic, we should add a separate property for every feature (proguard, packaging resources, etc), which will be excessive.

It also confuses me as an user of the feature, adding more questions than answers: what is the difference with Gradle logging, when I need to set it, etc.

I would just add -Xlog:cds if (logger.isInfoEnabled). Info is the default. It doesn't look that cdc produces a lot of logs, so it shouldn't be an issue.

Copy link
Member Author

@m-sasha m-sasha Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the logging param.


/**
* 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<String> =
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<String>

/**
* 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<String>()
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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ abstract class JvmApplication {
abstract fun nativeDistributions(fn: Action<JvmApplicationDistributions>)
abstract val buildTypes: JvmApplicationBuildTypes
abstract fun buildTypes(fn: Action<JvmApplicationBuildTypes>)
abstract val appCds: AppCdsConfiguration
abstract fun appCds(fn: Action<AppCdsConfiguration>)
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,4 +39,5 @@ internal open class JvmApplicationData @Inject constructor(
val jvmArgs: MutableList<String> = ArrayList()
val nativeDistributions: JvmApplicationDistributions = objects.new()
val buildTypes: JvmApplicationBuildTypes = objects.new()
val appCds: AppCdsConfiguration = objects.new()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,4 +71,10 @@ internal open class JvmApplicationInternal @Inject constructor(
final override fun buildTypes(fn: Action<JvmApplicationBuildTypes>) {
fn.execute(data.buildTypes)
}

final override val appCds: AppCdsConfiguration by data::appCds
final override fun appCds(fn: Action<AppCdsConfiguration>) {
fn.execute(data.appCds)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)
}

Expand Down Expand Up @@ -149,6 +144,18 @@ private fun JvmApplicationContext.configurePackagingTasks(
)
}

val appCdsMode = app.appCds.mode
val createAppCdsArchive = if (appCdsMode.generateAppClassesArchive) {
tasks.register<AbstractCreateAppCdsArchiveTask>(
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<AbstractJPackageTask>(
taskNameAction = "package",
Expand All @@ -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
)
}
}
Expand Down Expand Up @@ -233,7 +242,11 @@ private fun JvmApplicationContext.configurePackagingTasks(
taskNameAction = "run",
taskNameObject = "distributable",
args = listOf(createDistributable)
)
) {
if (createAppCdsArchive != null) {
dependsOn(createAppCdsArchive)
}
}

val run = tasks.register<JavaExec>(taskNameAction = "run") {
configureRunTask(this, commonTasks.prepareAppResources, runProguard)
Expand Down Expand Up @@ -284,7 +297,8 @@ private fun JvmApplicationContext.configurePackageTask(
prepareAppResources: TaskProvider<Sync>? = null,
checkRuntime: TaskProvider<AbstractCheckNativeDistributionRuntime>? = null,
unpackDefaultResources: TaskProvider<AbstractUnpackDefaultComposeApplicationResourcesTask>,
runProguard: Provider<AbstractProguardTask>? = null
runProguard: Provider<AbstractProguardTask>? = null,
createAppCdsArchive: TaskProvider<AbstractCreateAppCdsArchiveTask>? = null
) {
packageTask.enabled = packageTask.targetFormat.isCompatibleWithCurrentOS

Expand Down Expand Up @@ -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 })
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +40,9 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa
@get:Input
abstract val checkJdkVendor: Property<Boolean>

@get:Input
val appCdsMode: Property<AppCdsMode> = objects.notNullProperty()

private val taskDir = project.layout.buildDirectory.dir("compose/tmp/$name")

@get:OutputFile
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -109,6 +113,8 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa
}
}

appCdsMode.get().checkJdkCompatibility(jdkMajorVersion)

val modules = arrayListOf<String>()
runExternalTool(
tool = javaExecutable,
Expand Down
Loading
Loading