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
+1 -1
View File
@@ -55,7 +55,7 @@ android {
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 442
versionName = generateVersionName("1.08.0")
versionName = generateVersionName(libs.versions.app.get())
buildConfigField "String", "RELEASE_NOTES_ID", "\"be99e8c8d4df0f54b44eb6c96976ccb38baeea0192436a1c6fc8bc5e930da6b0\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+5
View File
@@ -12,7 +12,12 @@ plugins {
alias(libs.plugins.serialization)
}
// Shared app version for all subprojects — read from gradle/libs.versions.toml.
// Android versionCode stays local in amethyst/build.gradle (must be monotonic int).
// Desktop packageVersion inherits via project.version in desktopApp/build.gradle.kts.
allprojects {
version = libs.versions.app.get()
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
+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)
}
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1,4 +1,8 @@
[versions]
# Amethyst app version — single source of truth consumed by both Android (amethyst/)
# and Desktop (desktopApp/). Android versionCode is bumped independently in
# amethyst/build.gradle because it must be a monotonic integer.
app = "1.08.0"
accompanistAdaptive = "0.37.3"
cachemapVersion = "0.2.4"
composeMultiplatform = "1.10.3"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# AppImage launcher for Amethyst Desktop.
# Sets LD_LIBRARY_PATH to find bundled VLC natives (vlcj dlopens libvlc.so at runtime).
set -e
HERE="$(dirname "$(readlink -f "${0}")")"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/lib/vlc:${HERE}/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}"
export PATH="${HERE}/usr/bin:${PATH}"
export APPDIR="${HERE}"
exec "${HERE}/usr/bin/Amethyst" "$@"
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=Amethyst
GenericName=Nostr Client
Comment=Nostr client for desktop
Exec=Amethyst %u
Icon=amethyst
Categories=Network;InstantMessaging;
Terminal=false
StartupNotify=true
StartupWMClass=Amethyst
Keywords=nostr;client;social;chat;
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Single source of truth for Amethyst Desktop release asset naming.
#
# Contract: amethyst-desktop-<version>-<family>-<arch>.<ext>
# <version> = tag stripped of leading 'v' (e.g. "1.08.0")
# <family> = macos | windows | linux
# <arch> = x64 | arm64
# <ext> = dmg | msi | zip | deb | rpm | AppImage | tar.gz
#
# Consumed by: .github/workflows/create-release.yml, .github/workflows/bump-*.yml,
# BUILDING.md, local release runbooks.
#
# Usage:
# source scripts/asset-name.sh
# collect_assets <family> <arch> <version> <dest_dir>
#
# Expected examples:
# amethyst-desktop-1.08.0-macos-x64.dmg
# amethyst-desktop-1.08.0-macos-arm64.dmg
# amethyst-desktop-1.08.0-windows-x64.msi
# amethyst-desktop-1.08.0-windows-x64.zip
# amethyst-desktop-1.08.0-linux-x64.deb
# amethyst-desktop-1.08.0-linux-x64.rpm
# amethyst-desktop-1.08.0-linux-x64.AppImage
# amethyst-desktop-1.08.0-linux-x64.tar.gz
set -euo pipefail
# Print the canonical asset filename for a given family/arch/version/extension.
# Usage: asset_name <family> <arch> <version> <ext>
asset_name() {
local family="$1" arch="$2" version="$3" ext="$4"
printf 'amethyst-desktop-%s-%s-%s.%s' "$version" "$family" "$arch" "$ext"
}
# Copy + rename build outputs into <dest_dir> using the canonical naming scheme.
# Usage: collect_assets <family> <arch> <version> <dest_dir>
# Expects build outputs under desktopApp/build/... (Compose binaries + custom tasks + portable archives).
collect_assets() {
local family="$1" arch="$2" version="$3" dest="$4"
mkdir -p "$dest"
shopt -s nullglob
# Compose Desktop jpackage outputs (main-release/<format>/*.ext)
local src ext base dst
for src in \
desktopApp/build/compose/binaries/main-release/dmg/*.dmg \
desktopApp/build/compose/binaries/main-release/msi/*.msi \
desktopApp/build/compose/binaries/main-release/deb/*.deb \
desktopApp/build/compose/binaries/main-release/rpm/*.rpm \
desktopApp/build/appimage/*.AppImage \
desktopApp/build/portable/*.tar.gz \
desktopApp/build/portable/*.zip \
; do
[ -f "$src" ] || continue
base="$(basename "$src")"
case "$base" in
*.tar.gz) ext="tar.gz" ;;
*) ext="${base##*.}" ;;
esac
dst="$dest/$(asset_name "$family" "$arch" "$version" "$ext")"
cp "$src" "$dst"
echo "Collected: $dst"
done
shopt -u nullglob
}