A fork of the JetBrains Compose Desktop Gradle plugin (org.jetbrains.compose) with additional features for building, packaging, and distributing Compose Desktop applications.
ComposeDeskKit extends the official plugin with native library optimization, AOT cache generation, advanced Linux packaging options, and more.
plugins {
id("io.github.kdroidfilter.composedeskkit") version "1.0.0"
}Note: The DSL extension is
composeDeskKitinstead ofcompose:composeDeskKit.desktop.application { mainClass = "com.example.MainKt" nativeDistributions { // ... } }
Below is the exhaustive list of features and changes introduced by ComposeDeskKit compared to org.jetbrains.compose.
Strips native libraries (.dll, .so, .dylib) for non-target platforms from dependency JARs, significantly reducing the final package size.
nativeDistributions {
cleanupNativeLibs = true
}How it works:
- Registers a Gradle artifact transform that processes JAR files at resolution time.
- Uses path-based detection (looking for OS/architecture indicators like
linux-x86-64,windows-x64,darwin-arm64in JAR entry paths). - Falls back to binary header detection (PE, ELF, Mach-O) for entries without path indicators.
- Only removes native files for non-matching OS/architecture combinations; Java classes are never touched.
Generates an ahead-of-time compilation cache using the JDK 25+ single-step AOT training, improving application startup time.
nativeDistributions {
enableAotCache = true
}How it works:
- A
generateAotCacheGradle task runs aftercreateDistributable. - It launches the packaged application with
-XX:AOTCacheOutput=<path>to produce anapp.aotcache file. - It then injects
-XX:AOTCache=$APPDIR/app.aotinto the launcher.cfgfile so the cache is used at runtime.
Requirements:
- JDK 25 or newer.
- The application must self-terminate during the training run.
If your app is built in this repository, add the runtime helper:
dependencies {
implementation(project(":aot-runtime"))
}Recommended app-side pattern using the helper API:
import io.github.kdroidfilter.composedeskkit.aot.runtime.AotRuntime
fun main() {
if (AotRuntime.isTraining()) {
Thread({ Thread.sleep(60_000); System.exit(0) }, "aot-timer")
.apply { isDaemon = true; start() }
}
if (AotRuntime.isRuntime()) {
// Optional runtime-only behavior
}
// normal app startup...
}Details:
- Default training duration: 60 seconds (configurable via the task's
trainDurationSecondsproperty). - Auto-provisions a
javalauncher in the bundled runtime if one is missing (Windows: copies essential DLLs). - On headless Linux, automatically starts Xvfb.
- AOT cache file is included in the final installer via
--app-image.
Adds a JVM splash screen from an image file in the application resources.
nativeDistributions {
splashImage = "splash.png" // relative to appResources
}This automatically injects -splash:$APPDIR/resources/splash.png into the JVM launcher arguments.
Installer filenames are automatically suffixed with the target architecture (_x64 or _arm64) for clarity:
| Before | After |
|---|---|
MyApp-1.0.0.dmg |
MyApp-1.0.0_arm64.dmg |
MyApp-1.0.0.deb |
MyApp-1.0.0_x64.deb |
MyApp-1.0.0.msi |
MyApp-1.0.0_x64.msi |
AppImage format is excluded from this renaming.
Override the StartupWMClass entry in the .desktop file (helps window managers associate windows with the correct desktop entry):
nativeDistributions {
linux {
startupWMClass = "com-example-MyApp"
}
}If left null, it is automatically derived from mainClass (dots replaced by hyphens).
Inject extra dependencies into .deb and .rpm packages:
nativeDistributions {
linux {
debDepends = listOf("libgtk-3-0", "libasound2")
rpmRequires = listOf("gtk3", "alsa-lib")
}
}Automatically rewrites Debian dependencies for the time64 transition on Ubuntu 24.04+:
nativeDistributions {
linux {
enableT64AlternativeDeps = true
}
}This rewrites known libraries to use fallback alternatives, for example:
libasound2becomeslibasound2t64 | libasound2libfreetype6becomeslibfreetype6t64 | libfreetype6libpng16-16becomeslibpng16-16t64 | libpng16-16
Control the compression algorithm and level used when building .deb packages:
nativeDistributions {
linux {
debCompression = DebCompression.ZSTD
debCompressionLevel = 19
}
}Available algorithms:
| Algorithm | Max level |
|---|---|
DebCompression.GZIP |
9 |
DebCompression.XZ |
9 |
DebCompression.ZSTD |
22 |
DebCompression.NONE |
0 |
If null, the dpkg-deb default is used.
Control the compression algorithm and level used when building .rpm packages:
nativeDistributions {
linux {
rpmCompression = RpmCompression.ZSTD
rpmCompressionLevel = 19
}
}Available algorithms:
| Algorithm | Max level | Default level |
|---|---|---|
RpmCompression.GZIP |
9 | 9 |
RpmCompression.XZ |
9 | 6 |
RpmCompression.ZSTD |
22 | 19 |
If null, the rpmbuild default is used.
The official plugin passes the parent directory to jpackage's --app-image argument. ComposeDeskKit fixes this by passing the actual platform-specific application directory:
- macOS:
<parent>/MyApp.app - Linux / Windows:
<parent>/MyApp
This ensures that files generated in-place (such as the AOT cache) are correctly included in the final installer.
Handles subdirectory paths when unpacking Skiko native dependencies, preserving correct file names in the output.
Adds native MSIX packaging support via TargetFormat.Msix.
By default, you do not need to configure windows { msix { ... } }.
MSIX packaging reuses global nativeDistributions settings and computes missing manifest values automatically.
nativeDistributions {
targetFormats(TargetFormat.Msix)
windows {
msix {
// Optional: PNG or SVG (default uses linux.iconFile, then built-in PNG)
// iconFile.set(project.file("packaging/msix/AppIcon.svg"))
// Optional signing
// signingPfxFile.set(project.file("packaging/msix/sign.pfx"))
// signingPassword = "secret"
// Optional manifest overrides
// identityName = "MyCompany.MyApp"
// publisher = "CN=MyCompany"
}
}
}Default behavior (without MSIX overrides):
packageVersion: uses the same version resolution as other formats (msixPackageVersion-> OS/global package version fallbacks).iconFile: fallback order iswindows.msix.iconFile->nativeDistributions.linux.iconFile-> built-in default PNG icon.displayName:nativeDistributions.packageName, then project name.description:nativeDistributions.description, then package/project name.publisherDisplayName:nativeDistributions.vendor, then project name.appExecutable:<packageName>.exe.identityName: auto-derived from vendor/package name (sanitized).publisher: auto-derived asCN=<...>from vendor/package name.processorArchitecture: auto-derived from host architecture (x64orarm64).- MSIX manifest version is normalized to 4 segments (
A.B.CbecomesA.B.C.0).
Implementation details:
- Uses the existing distributable (
app-image) then creates an MSIX withmakeappx.exe. - Generates
AppxManifest.xmlautomatically (overridable viamanifestTemplateFile). - Supports optional signing via
signtool.exe(signingPfxFile+signingPassword). - Environment fallback for CI signing is supported:
MSIX_SIGN_PFX_BASE64MSIX_SIGN_PFX_PASSWORD
Adds support for macOS layered icons (.icon directory) introduced in macOS 26. Layered icons enable the dynamic tilt/depth effects shown on the Dock and in Spotlight.
nativeDistributions {
macOS {
layeredIconDir.set(project.file("icons/MyApp.icon"))
}
}How it works:
- At packaging time,
xcrun actoolcompiles the.icondirectory into anAssets.carfile. - The
Assets.caris placed inside<AppName>.app/Contents/Resources/. - The
Info.plistis updated with aCFBundleIconNameentry referencing the compiled asset. - The traditional
.icnsicon (iconFile) is still used as a fallback for older macOS versions, so you should keep both.
Creating a .icon directory:
A .icon directory is a folder with the .icon extension that contains an icon.json manifest and image assets. The easiest way to create one is with Xcode 26+ or Apple Icon Composer:
- Open Xcode and create a new Asset Catalog (or use an existing one).
- Add a new App Icon asset.
- Configure the layers (front, back, etc.) with your images.
- Export the
.icondirectory from the asset catalog.
A minimal .icon directory structure looks like:
MyApp.icon/
icon.json
Assets/
MyImage.png
Requirements:
- Xcode Command Line Tools with
actoolversion 26.0 or higher (ships with Xcode 26+). - Only effective on macOS build hosts. On other platforms the property is ignored.
- If
actoolis missing or too old, a warning is logged and the build continues without layered icon support.
Full example with both icons:
nativeDistributions {
macOS {
// Traditional icon (required fallback for older macOS)
iconFile.set(project.file("icons/MyApp.icns"))
// Layered icon for macOS 26+ dynamic effects
layeredIconDir.set(project.file("icons/MyApp.icon"))
}
}Native Kotlin/Native application:
Layered icons also work with nativeApplication targets:
composeDeskKit.desktop.nativeApplication {
distributions {
macOS {
iconFile.set(project.file("icons/MyApp.icns"))
layeredIconDir.set(project.file("icons/MyApp.icon"))
}
}
}| Property | Type | Default | Description |
|---|---|---|---|
cleanupNativeLibs |
Boolean |
false |
Strip native libs for non-target platforms |
splashImage |
String? |
null |
Splash image filename (relative to appResources) |
enableAotCache |
Boolean |
false |
Enable JDK 25+ AOT cache generation |
| Property | Type | Default | Description |
|---|---|---|---|
startupWMClass |
String? |
null |
Override StartupWMClass in .desktop file |
debDepends |
List<String> |
[] |
Additional Debian dependencies |
rpmRequires |
List<String> |
[] |
Additional RPM requirements |
enableT64AlternativeDeps |
Boolean |
false |
Ubuntu 24.04+ time64 dep rewriting |
debCompression |
DebCompression? |
null |
.deb compression algorithm |
debCompressionLevel |
Int? |
null |
.deb compression level |
rpmCompression |
RpmCompression? |
null |
.rpm compression algorithm |
rpmCompressionLevel |
Int? |
null |
.rpm compression level |
| Property | Type | Default | Description |
|---|---|---|---|
layeredIconDir |
DirectoryProperty |
unset | Path to a .icon directory for macOS 26+ layered icons |
| Property | Type | Default | Description |
|---|---|---|---|
msixPackageVersion |
String? |
null |
Version override for TargetFormat.Msix |
| Property | Type | Default | Description |
|---|---|---|---|
iconFile |
RegularFileProperty |
linux.iconFile, then built-in PNG |
Icon source for MSIX logos (PNG or SVG) |
signingPfxFile |
RegularFileProperty |
unset | PFX used for optional MSIX signing |
signingPassword |
String? |
unset | Password for signingPfxFile |
manifestTemplateFile |
RegularFileProperty |
built-in template | Optional AppxManifest template override |
identityName |
String? |
derived | MSIX identity name |
publisher |
String? |
derived (CN=...) |
MSIX publisher |
publisherDisplayName |
String? |
vendor/project | Publisher display name |
displayName |
String? |
package/project name | App display name |
description |
String? |
package description/name | App description |
backgroundColor |
String |
"transparent" |
Tile background color |
appId |
String |
"App" |
App ID in manifest |
appExecutable |
String? |
<packageName>.exe |
Executable entry in manifest |
processorArchitecture |
String? |
host arch | Usually x64 or arm64 |
targetDeviceFamilyName |
String |
"Windows.Desktop" |
Target device family |
targetDeviceFamilyMinVersion |
String |
"10.0.17763.0" |
Minimum Windows version |
targetDeviceFamilyMaxVersionTested |
String |
"10.0.22621.2861" |
Maximum tested Windows version |
composeDeskKit.desktop.application {
mainClass = "com.example.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Msix, TargetFormat.Deb, TargetFormat.Rpm)
packageName = "MyApp"
packageVersion = "1.0.0"
// Strip unused native libraries (e.g. remove Linux .so from macOS build)
cleanupNativeLibs = true
// Splash screen
splashImage = "splash.png"
// JDK 25+ AOT cache for faster startup
enableAotCache = true
linux {
startupWMClass = "com-example-MyApp"
debDepends = listOf("libgtk-3-0", "libasound2")
rpmRequires = listOf("gtk3", "alsa-lib")
enableT64AlternativeDeps = true
debCompression = DebCompression.ZSTD
debCompressionLevel = 19
rpmCompression = RpmCompression.ZSTD
rpmCompressionLevel = 19
}
macOS {
iconFile.set(project.file("icons/MyApp.icns"))
layeredIconDir.set(project.file("icons/MyApp.icon"))
}
windows {
msix {
identityName = "MyCompany.MyApp"
publisher = "CN=MyCompany"
publisherDisplayName = "My Company"
// iconFile.set(project.file("packaging/msix/AppIcon.svg"))
}
}
}
}-
Add the plugin ID:
+ id("io.github.kdroidfilter.composedeskkit") version "1.0.0" -
Replace the DSL extension name:
- compose.desktop.application { + composeDeskKit.desktop.application {
-
All existing configuration from the official plugin is preserved. The new properties are purely additive.
This project uses a Gradle composite build. The plugin source is inside the plugin-build folder.
# Run all checks
./gradlew preMerge
# Format code
./gradlew reformatAll
# Run a task inside the plugin build
./gradlew -p plugin-build <task-name>This project is forked from the JetBrains Compose Desktop Gradle plugin. See LICENSE for details.