feat(desktop): version source-of-truth + RPM + AppImage packaging

Phase 1+2 of multi-platform distribution plan:
- gradle/libs.versions.toml: add `app = "1.08.0"` as single source of truth
- Root build.gradle: allprojects { version = libs.versions.app.get() }
- amethyst/build.gradle: versionName from catalog (versionCode stays local)
- desktopApp/build.gradle.kts: drop hardcoded "1.0.0"; inherit project.version;
  add TargetFormat.Rpm; add linux DSL (menuGroup, appCategory, debMaintainer,
  rpmLicenseType, rpmPackageVersion with dashes stripped)
- desktopApp/build.gradle.kts: new createReleaseAppImage task wrapping
  createReleaseDistributable with linuxdeploy (TargetFormat.AppImage is
  broken in Compose 1.10.x — CMP-7101)
- packaging/appimage/: AppRun launcher (sets LD_LIBRARY_PATH for bundled VLC),
  amethyst.desktop XDG entry, 512x512 icon extracted from icon.icns
- scripts/asset-name.sh: single source for release asset naming contract
This commit is contained in:
nrobi144
2026-04-16 14:35:01 +03:00
parent 3356059d50
commit da1037423c
9 changed files with 1201 additions and 3 deletions
+80 -2
View File
@@ -1,4 +1,6 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import java.io.File
import java.nio.file.Files
plugins {
alias(libs.plugins.jetbrainsKotlinJvm)
@@ -7,6 +9,11 @@ plugins {
id("ir.mahozad.vlc-setup") version "0.1.0"
}
// RPM rejects dashes in version strings — strip prerelease suffix for Linux RPM only.
// Other formats accept full semver (DEB uses ~rc1, DMG/MSI accept bare versions).
val appVersion: String = project.version.toString()
val appVersionRpm: String = appVersion.substringBefore("-")
sourceSets {
main {
kotlin.srcDir("src/jvmMain/kotlin")
@@ -88,11 +95,11 @@ compose.desktop {
nativeDistributions {
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
modules("java.management") // Required by kmp-tor TorRuntime
packageName = "Amethyst"
packageVersion = "1.0.0"
packageVersion = appVersion
description = "Nostr client for desktop"
vendor = "Amethyst Contributors"
@@ -109,6 +116,12 @@ compose.desktop {
linux {
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
menuGroup = "Network"
appCategory = "Network"
debMaintainer = "vitor@vitorpamplona.com"
rpmLicenseType = "MIT"
// RPM version field rejects dashes; strip prerelease suffix for RPM builds.
rpmPackageVersion = appVersionRpm
}
}
}
@@ -126,3 +139,68 @@ vlcSetup {
tasks.named("spotlessKotlin") {
mustRunAfter("vlcSetup")
}
// --- AppImage packaging (Linux) ---
//
// Compose Multiplatform's TargetFormat.AppImage is known-broken in 1.10.x (CMP-7101).
// Instead: wrap `createReleaseDistributable` output with `linuxdeploy` (which
// auto-bundles libraries, handles rpath, and calls appimagetool internally).
//
// Build inputs live in packaging/appimage/:
// - AppRun shell launcher (sets LD_LIBRARY_PATH including bundled VLC)
// - amethyst.desktop XDG desktop entry
// - amethyst.png 512x512 icon
//
// linuxdeploy binary is fetched by CI (SHA-verified) into packaging/appimage/
// as linuxdeploy-x86_64.AppImage. BUILDING.md documents local-dev fetch.
val createReleaseAppImage by tasks.registering(Exec::class) {
group = "compose desktop"
description = "Bundle createReleaseDistributable output into a Linux AppImage via linuxdeploy."
dependsOn("createReleaseDistributable")
val distDir = layout.buildDirectory.dir("compose/binaries/main-release/app/Amethyst")
val appDir = layout.buildDirectory.dir("appimage/Amethyst.AppDir")
val outFile = layout.buildDirectory.file("appimage/Amethyst-$appVersion-x86_64.AppImage")
val toolRoot = layout.projectDirectory.dir("../packaging/appimage")
val linuxdeployTool = toolRoot.file("linuxdeploy-x86_64.AppImage")
inputs.dir(distDir)
inputs.dir(toolRoot)
outputs.file(outFile)
doFirst {
val dir = appDir.get().asFile
dir.deleteRecursively()
dir.mkdirs()
copy {
from(distDir) { into("usr") }
from(toolRoot.file("AppRun")) {
rename { "AppRun" }
filePermissions { unix("0755") }
}
from(toolRoot.file("amethyst.desktop"))
from(toolRoot.file("amethyst.png"))
into(dir)
}
// DirIcon is used by desktop integrations (file managers, AppImageLauncher)
val dirIcon = File(dir, ".DirIcon")
if (dirIcon.exists()) dirIcon.delete()
Files.createSymbolicLink(dirIcon.toPath(), File("amethyst.png").toPath())
if (!linuxdeployTool.asFile.canExecute()) {
linuxdeployTool.asFile.setExecutable(true)
}
}
commandLine(
linuxdeployTool.asFile.absolutePath,
"--appdir", appDir.get().asFile.absolutePath,
"--output", "appimage",
"--desktop-file", "${appDir.get().asFile}/amethyst.desktop",
"--icon-file", "${appDir.get().asFile}/amethyst.png",
)
environment("OUTPUT", outFile.get().asFile.absolutePath)
environment("ARCH", "x86_64")
// Suppress linuxdeploy's verbose library-scanner output; keep errors.
environment("LINUXDEPLOY_OUTPUT_VERSION", appVersion)
}