Port :desktop packaging to Nucleus framework

Replaces Compose Desktop's bespoke nativeDistributions + the hand-rolled
packageMsix, snapcraft, flatpak-builder, and appimagetool tasks with
the Nucleus plugin's unified DSL. Nucleus is a fork of compose-desktop-
application that adds first-class AppX/Portable/Snap/Flatpak/AppImage
targets, so org.jetbrains.compose can no longer be applied alongside
it (task name collisions); compose.desktop.currentOs and
compose.components.uiToolingPreview are now resolved via explicit
Maven coordinates instead.

The hand-edited msix/AppxManifest.xml, snap/snapcraft.yaml, and
flatpak/*.yaml are now generated from the DSL; the source files
remain in-tree for now and will be deleted once CI validates the
nucleus output across all targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wavesonics 2026-05-13 17:25:11 -07:00 committed by Adam Brown
parent 2ba59f7910
commit 4af31163b2
No known key found for this signature in database
3 changed files with 122 additions and 195 deletions

View file

@ -69,7 +69,6 @@ plugins {
alias(libs.plugins.aboutlibraries.plugin.android) apply false
alias(libs.plugins.jetbrains.kover)
alias(libs.plugins.kotlinx.atomicfu)
alias(libs.plugins.flatpak.gradle.generator) apply false
}
dependencies {
@ -87,7 +86,6 @@ kover {
}
registerPublishTasks()
registerLinuxDistributionTasks(libs.versions.app.get())
val releasePreFlightChecks = tasks.register("releasePreFlightChecks") {
doLast {
@ -121,15 +119,8 @@ val releasePreFlightChecks = tasks.register("releasePreFlightChecks") {
}
}
// Ensure flatpak generator runs after pre-flight checks
project(":desktop").tasks.configureEach {
if (name == "flatpakGradleGenerator") {
mustRunAfter(releasePreFlightChecks)
}
}
tasks.register("prepareForRelease") {
dependsOn(releasePreFlightChecks, ":desktop:flatpakGradleGenerator")
dependsOn(releasePreFlightChecks)
doLast {
val releaseInfo =
configureRelease(libs.versions.app.get()) ?: error("Failed to configure new release")
@ -162,19 +153,6 @@ tasks.register("prepareForRelease") {
val globalChangelogFile = File("${project.rootDir}/CHANGELOG.md")
writeChangelogMarkdown(releaseInfo, globalChangelogFile)
// Update snapcraft.yaml with new version and JVM version
val snapcraftPath = "snap/snapcraft.yaml".replace("/", File.separator)
val snapcraftFile = project.rootDir.resolve(snapcraftPath)
val jvmVersion = libs.versions.jvm.get()
updateSnapcraftYaml(releaseInfo.semVar, jvmVersion, snapcraftFile)
// Update Flatpak manifest and metainfo with new version and JVM version
val flatpakManifestPath = "flatpak/studio.darkrock.hammer.yaml".replace("/", File.separator)
val flatpakManifestFile = project.rootDir.resolve(flatpakManifestPath)
val flatpakMetainfoPath = "flatpak/studio.darkrock.hammer.metainfo.xml".replace("/", File.separator)
val flatpakMetainfoFile = project.rootDir.resolve(flatpakMetainfoPath)
updateFlatpakFiles(releaseInfo.semVar, jvmVersion, flatpakManifestFile, flatpakMetainfoFile, releaseInfo.changeLog)
fun git(vararg args: String) {
val cmd = listOf("git") + args.toList()
println("> ${cmd.joinToString(" ")}")
@ -193,12 +171,6 @@ tasks.register("prepareForRelease") {
git("add", changeLogFile.absolutePath)
git("add", versionsFile.absolutePath)
git("add", globalChangelogFile.absolutePath)
git("add", snapcraftFile.absolutePath)
git("add", flatpakManifestFile.absolutePath)
git("add", flatpakMetainfoFile.absolutePath)
val flatpakSourcesPath = "flatpak/flatpak-sources.json".replace("/", File.separator)
val flatpakSourcesFile = project.rootDir.resolve(flatpakSourcesPath)
git("add", flatpakSourcesFile.absolutePath)
git("commit", "-m", "Prepared for release: v${releaseInfo.semVar}")
// Switch to release and reset to origin/release HEAD

View file

@ -1,5 +1,9 @@
import com.darkrockstudios.build.registerLinuxDistributionTasks
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import io.github.kdroidfilter.nucleus.desktop.application.dsl.AppImageCategory
import io.github.kdroidfilter.nucleus.desktop.application.dsl.SnapCompression
import io.github.kdroidfilter.nucleus.desktop.application.dsl.SnapConfinement
import io.github.kdroidfilter.nucleus.desktop.application.dsl.SnapGrade
import io.github.kdroidfilter.nucleus.desktop.application.dsl.SnapPlug
import io.github.kdroidfilter.nucleus.desktop.application.dsl.TargetFormat
val data_version: String by extra
@ -7,10 +11,11 @@ plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.jetbrains.compose)
// Nucleus brings its own forked compose-desktop application tasks;
// applying org.jetbrains.compose alongside it collides on task names.
alias(libs.plugins.nucleus)
alias(libs.plugins.jetbrains.kover)
alias(libs.plugins.aboutlibraries.plugin)
alias(libs.plugins.flatpak.gradle.generator)
}
group = "com.darkrockstudios.apps.hammer.desktop"
@ -40,8 +45,10 @@ kotlin {
implementation(project(":base"))
implementation(project(":common"))
implementation(project(":composeUi"))
implementation(compose.components.uiToolingPreview)
implementation(compose.desktop.currentOs)
// Explicit coords — org.jetbrains.compose plugin isn't applied here
// because nucleus brings its own forked desktop-application tasks.
implementation(libs.jetbrains.compose.ui.tooling.preview)
implementation(libs.jetbrains.compose.desktop)
implementation(libs.darklaf.core)
implementation(libs.kotlinx.cli)
}
@ -55,59 +62,117 @@ kotlin {
}
}
compose.desktop {
application {
mainClass = "com.darkrockstudios.apps.hammer.desktop.MainKt"
nativeDistributions {
targetFormats(
TargetFormat.Dmg,
TargetFormat.Pkg,
TargetFormat.AppImage,
TargetFormat.Msi,
TargetFormat.Exe,
TargetFormat.Deb,
TargetFormat.Rpm
)
modules = arrayListOf(":base", ":common", ":composeUi", ":desktop")
includeAllModules = true
packageName = "hammer"
packageVersion = libs.versions.app.get()
description = "A simple tool for building stories."
copyright = "© 2025 Adam W. Brown, All rights reserved."
licenseFile.set(project.file("../LICENSE"))
outputBaseDir.set(project.layout.buildDirectory.dir("installers"))
nucleus.application {
mainClass = "com.darkrockstudios.apps.hammer.desktop.MainKt"
jvmArgs += listOf("-Dcompose.application.configure.swing.globals=false")
windows {
menuGroup = "Hammer"
shortcut = true
console = false
buildTypes.release.proguard {
version.set("7.6.0")
isEnabled.set(true)
obfuscate.set(false)
optimize.set(false)
configurationFiles.from("proguard-rules.pro")
}
iconFile.set(project.file("icons/windows.ico"))
}
nativeDistributions {
targetFormats(
// macOS
TargetFormat.Dmg,
TargetFormat.Pkg,
// Windows
TargetFormat.Msi,
TargetFormat.Exe, // alias for Nsis
TargetFormat.Portable,
TargetFormat.AppX, // replaces bespoke packageMsix
// Linux
TargetFormat.Deb,
TargetFormat.Rpm,
TargetFormat.AppImage,
TargetFormat.Snap, // replaces bespoke snapcraft build
TargetFormat.Flatpak, // replaces bespoke flatpak-builder
)
includeAllModules = true
packageName = "hammer"
packageVersion = libs.versions.app.get()
description = "A simple tool for building stories."
copyright = "© 2025 Adam W. Brown, All rights reserved."
vendor = "Dark Rock Studios"
licenseFile.set(project.file("../LICENSE"))
linux {
rpmLicenseType = "MIT"
shortcut = true
windows {
menuGroup = "Hammer"
console = false
iconFile.set(project.file("icons/windows.ico"))
iconFile.set(project.file("icons/linux.png"))
}
macOS {
dockName = "Hammer"
appStore = false
iconFile.set(project.file("icons/macos.icns"))
appx {
// Mirrors the previous msix/AppxManifest.xml.
applicationId = "Hammer"
identityName = "DarkRockStudios.HammerEditor"
publisher = "CN=1CB419E3-12E3-4F8B-B2CC-2C4F16D8E686"
displayName = "Hammer Editor"
publisherDisplayName = "Dark Rock Studios"
languages = listOf(
"en-US", "de-DE", "fr-FR", "es-ES", "it-IT", "pt-BR", "uk-UA"
)
storeLogo.set(project.file("../msix/Assets/StoreLogo.png"))
square44x44Logo.set(project.file("../msix/Assets/Square44x44Logo.png"))
square150x150Logo.set(project.file("../msix/Assets/Square150x150Logo.png"))
wide310x150Logo.set(project.file("../msix/Assets/Wide310x150Logo.png"))
}
}
jvmArgs("-Dcompose.application.configure.swing.globals=false")
buildTypes.release.proguard {
version.set("7.6.0")
isEnabled.set(true)
obfuscate.set(false)
optimize.set(false)
//joinOutputJars.set(true)
configurationFiles.from("proguard-rules.pro")
linux {
shortcut = true
rpmLicenseType = "MIT"
iconFile.set(project.file("icons/linux.png"))
appImage {
category = AppImageCategory.Office
genericName = "Story Editor"
synopsis = "A simple tool for building stories."
desktopEntries = mapOf("Keywords" to "writing;editor;story;novel;")
}
snap {
confinement = SnapConfinement.Strict
grade = SnapGrade.Stable
summary = "A simple tool for building stories"
base = "core22"
compression = SnapCompression.Xz
plugs = listOf(
SnapPlug.Home,
SnapPlug.Network,
SnapPlug.NetworkBind,
SnapPlug.Desktop,
SnapPlug.DesktopLegacy,
SnapPlug.Wayland,
SnapPlug.X11,
SnapPlug.Opengl,
SnapPlug.RemovableMedia,
)
}
flatpak {
runtime = "org.freedesktop.Platform"
runtimeVersion = "24.08"
sdk = "org.freedesktop.Sdk"
finishArgs = listOf(
"--socket=x11",
"--socket=wayland",
"--share=ipc",
"--share=network",
"--device=dri",
"--filesystem=home",
"--filesystem=xdg-documents",
)
license.set(project.file("../LICENSE"))
}
}
macOS {
dockName = "Hammer"
bundleID = "studio.darkrock.hammer"
iconFile.set(project.file("icons/macos.icns"))
}
}
}
@ -118,117 +183,3 @@ aboutLibraries {
excludeFields.addAll("generated")
}
}
registerLinuxDistributionTasks(libs.versions.app.get())
// MSIX packaging task for Windows Store
tasks.register("packageMsix") {
group = "distribution"
description = "Creates an MSIX package for Microsoft Store submission"
// Only run on Windows
onlyIf {
org.gradle.internal.os.OperatingSystem.current().isWindows
}
// Depend on the distributable being created first
dependsOn("createDistributable")
doLast {
val appVersion = libs.versions.app.get()
val msixVersion = "$appVersion.0" // MSIX requires 4-part version
val buildDir = project.layout.buildDirectory.get().asFile
val distributableDir = buildDir.resolve("installers/main/app/hammer")
val msixDir = project.rootDir.resolve("msix")
val outputMsix = buildDir.resolve("installers/Hammer-${appVersion}.msix")
// Copy manifest
println("Copying AppxManifest.xml...")
val manifestSrc = msixDir.resolve("AppxManifest.xml")
val manifestDst = distributableDir.resolve("AppxManifest.xml")
manifestSrc.copyTo(manifestDst, overwrite = true)
// Update version in manifest (only in Identity element)
println("Updating version to $msixVersion...")
val manifestContent = manifestDst.readText()
val updatedManifest = manifestContent.replace(
Regex("""(<Identity[^>]*Version=")[\d\.]+""""),
"$1$msixVersion\""
)
manifestDst.writeText(updatedManifest)
// Copy assets
println("Copying Assets...")
val assetsSrc = msixDir.resolve("Assets")
val assetsDst = distributableDir.resolve("Assets")
assetsSrc.copyRecursively(assetsDst, overwrite = true)
// Find makeappx.exe
val makeappxPath = findMakeAppx()
if (makeappxPath == null) {
throw GradleException(
"makeappx.exe not found. Please install Windows SDK.\n" +
"Download from: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/"
)
}
println("Packaging MSIX with makeappx...")
val result = project.providers.exec {
commandLine(
makeappxPath,
"pack",
"/d", distributableDir.absolutePath,
"/p", outputMsix.absolutePath,
"/o"
)
isIgnoreExitValue = true
}.result.get()
if (result.exitValue == 0) {
println("✓ MSIX package created successfully!")
println(" Location: ${outputMsix.absolutePath}")
println(" Size: ${String.format("%.2f MB", outputMsix.length() / 1024.0 / 1024.0)}")
println("\nReady for Microsoft Store submission!")
} else {
throw GradleException("Failed to create MSIX package")
}
}
}
fun findMakeAppx(): String? {
// Common Windows SDK locations
val searchPaths = listOf(
"C:\\Program Files (x86)\\Windows Kits\\10\\bin",
"C:\\Program Files (x86)\\Windows Kits\\10\\App Certification Kit"
)
for (basePath in searchPaths) {
val baseDir = file(basePath)
if (baseDir.exists()) {
// Search in bin subdirectories (e.g., 10.0.22621.0/x64/)
baseDir.walk().forEach { file ->
if (file.name == "makeappx.exe") {
return file.absolutePath
}
}
}
}
return null
}
tasks.named("flatpakGradleGenerator") {
setProperty("outputFile", rootProject.file("flatpak/flatpak-sources.json"))
// Exclude test configurations to reduce the dependency list
setProperty("excludeConfigurations", listOf(
"testCompileClasspath",
"testRuntimeClasspath",
"jvmTestCompileClasspath",
"jvmTestRuntimeClasspath"
))
// Set architecture (x86_64 is the default, but can specify for clarity)
// setProperty("onlyArches", "x86_64")
}

View file

@ -93,6 +93,7 @@ kotlinx-io-okio = "0.9.0"
compose-texteditor = "2.0.0"
platform-spellcheckerkt = "1.1.0"
flatpak-gradle-generator = "1.7.0"
nucleus = "1.15.4"
colorpicker-compose = "1.1.4"
material-kolor = "4.1.1"
@ -116,6 +117,8 @@ cryptography-provider-jdk = { module = "dev.whyoleg.cryptography:cryptography-pr
jetbrains_compose_ui_util = { module = "org.jetbrains.compose.ui:ui-util", version.ref = "jetbrains_compose" }
jetbrains_compose_ui_text = { module = "org.jetbrains.compose.ui:ui-text", version.ref = "jetbrains_compose" }
jetbrains_compose_ui_backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrains_compose" }
jetbrains_compose_ui_tooling_preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrains_compose" }
jetbrains_compose_desktop = { module = "org.jetbrains.compose.desktop:desktop", version.ref = "jetbrains_compose" }
coil_compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil_network_ktor = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" }
@ -296,6 +299,7 @@ buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig
aboutlibraries_plugin = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
aboutlibraries_plugin_android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" }
flatpak_gradle_generator = { id = "io.github.jwharm.flatpak-gradle-generator", version.ref = "flatpak-gradle-generator" }
nucleus = { id = "io.github.kdroidfilter.nucleus", version.ref = "nucleus" }
[bundles]
koin_server = ["koin_core", "koin_core_coroutines", "koin_sl4j", "koin_ktor"]