f922cfd5bc
Disabling only vlcDownload/upxDownload/vlcSetup left the chained *Extract tasks in the graph with @InputFile properties pointing at archives that were never downloaded — Gradle 9 then fails the build during input validation: property 'upxArchiveFile' specifies file '/root/.gradle/vlcSetup/upx-4.2.4.tar.xz' which doesn't exist. Disable the full vlc-setup plugin task set (vlcDownload, vlcExtract, vlcFilterPlugins, vlcCompressPlugins, upxDownload, upxExtract, vlcSetup) so packaging skips VLC bundling cleanly. Verified locally: :desktopApp:packageDeb now proceeds past those tasks straight to jpackage with -PskipVlcSetup=true.
259 lines
9.5 KiB
Kotlin
259 lines
9.5 KiB
Kotlin
import de.undercouch.gradle.tasks.download.Download
|
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|
import java.nio.file.Files
|
|
|
|
plugins {
|
|
alias(libs.plugins.jetbrainsKotlinJvm)
|
|
alias(libs.plugins.composeMultiplatform)
|
|
alias(libs.plugins.jetbrainsComposeCompiler)
|
|
id("ir.mahozad.vlc-setup") version "0.1.0"
|
|
}
|
|
|
|
// RPM rejects dashes in version strings — replace with tilde (~) which RPM uses
|
|
// for prerelease ordering: 1.08.0~rc1 < 1.08.0 per RPM version comparison rules.
|
|
val appVersion: String = project.version.toString()
|
|
|
|
sourceSets {
|
|
main {
|
|
kotlin.srcDir("src/jvmMain/kotlin")
|
|
resources.srcDir("src/jvmMain/resources")
|
|
}
|
|
test {
|
|
kotlin.srcDir("src/jvmTest/kotlin")
|
|
resources.srcDir("src/jvmTest/resources")
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(21)
|
|
}
|
|
|
|
dependencies {
|
|
implementation(compose.desktop.currentOs)
|
|
implementation(libs.jetbrains.compose.material3)
|
|
implementation(libs.jetbrains.compose.components.resources)
|
|
|
|
// Quartz Nostr library (will use JVM target)
|
|
implementation(project(":quartz"))
|
|
|
|
// Commons library
|
|
implementation(project(":commons"))
|
|
|
|
// Lifecycle ViewModel (needed to access ViewModel supertype from commons)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
|
|
// Coroutines
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
implementation(libs.kotlinx.coroutines.swing)
|
|
|
|
// Networking
|
|
implementation(libs.okhttp)
|
|
|
|
// JSON
|
|
implementation(libs.jackson.module.kotlin)
|
|
|
|
// Image loading (Coil3 — explicit because commons uses implementation, not api)
|
|
implementation(libs.coil.compose)
|
|
implementation(libs.coil.okhttp)
|
|
implementation(libs.coil.svg)
|
|
|
|
// Video playback
|
|
implementation(libs.vlcj)
|
|
|
|
// EXIF stripping (lossless)
|
|
implementation(libs.commons.imaging)
|
|
|
|
// Collections
|
|
implementation(libs.kotlinx.collections.immutable)
|
|
implementation(libs.androidx.collection)
|
|
|
|
// Tor daemon (desktop embedded via kmp-tor)
|
|
implementation(libs.kmp.tor.runtime)
|
|
implementation(libs.kmp.tor.resource.exec.tor)
|
|
|
|
// SLF4J no-op — silence "No SLF4J providers" warnings from transitive deps
|
|
implementation(libs.slf4j.nop)
|
|
|
|
// QR code generation (ZXing core)
|
|
implementation(libs.zxing)
|
|
|
|
// Testing
|
|
testImplementation(libs.kotlin.test)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.mockk)
|
|
testImplementation(libs.okhttp)
|
|
}
|
|
|
|
compose.desktop {
|
|
application {
|
|
mainClass = "com.vitorpamplona.amethyst.desktop.MainKt"
|
|
jvmArgs += "--add-opens=java.base/java.nio=ALL-UNNAMED"
|
|
|
|
jvmArgs += "-Xmx2g"
|
|
|
|
// Forward platform-preview overrides from the gradle invocation to the
|
|
// launched app's JVM so `./gradlew :desktopApp:run -Damethyst.platform=GNOME`
|
|
// works in addition to the env-var form (`AMETHYST_PLATFORM=GNOME`).
|
|
listOf("amethyst.platform", "amethyst.appearance", "amethyst.accent").forEach { key ->
|
|
System.getProperty(key)?.let { jvmArgs += "-D$key=$it" }
|
|
}
|
|
|
|
nativeDistributions {
|
|
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
|
modules("java.management") // Required by kmp-tor TorRuntime
|
|
|
|
packageName = "Amethyst"
|
|
packageVersion = appVersion
|
|
description = "Nostr client for desktop"
|
|
vendor = "Amethyst Contributors"
|
|
|
|
macOS {
|
|
bundleID = "com.vitorpamplona.amethyst.desktop"
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
|
|
}
|
|
|
|
windows {
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
|
|
menuGroup = "Amethyst"
|
|
upgradeUuid = "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
|
|
}
|
|
|
|
linux {
|
|
iconFile.set(project.file("src/jvmMain/resources/icon.png"))
|
|
menuGroup = "Network"
|
|
appCategory = "Network"
|
|
debMaintainer = "vitor@vitorpamplona.com"
|
|
rpmLicenseType = "MIT"
|
|
// RPM version: replace dashes with tilde (1.08.0~rc1 < 1.08.0 per RPM ordering).
|
|
rpmPackageVersion = appVersion.replace("-", "~")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vlcSetup {
|
|
vlcVersion.set("3.0.21")
|
|
shouldCompressVlcFiles.set(true)
|
|
shouldIncludeAllVlcFiles.set(true)
|
|
pathToCopyVlcLinuxFilesTo.set(file("src/jvmMain/appResources/linux/vlc"))
|
|
pathToCopyVlcMacosFilesTo.set(file("src/jvmMain/appResources/macos/vlc"))
|
|
pathToCopyVlcWindowsFilesTo.set(file("src/jvmMain/appResources/windows/vlc"))
|
|
}
|
|
|
|
tasks.named("spotlessKotlin") {
|
|
mustRunAfter("vlcSetup")
|
|
}
|
|
|
|
// `ir.mahozad.vlc-setup` registers `vlcDownload` / `upxDownload` tasks that
|
|
// extend `de.undercouch.gradle.tasks.download.Download`. Defaults are 0 retries
|
|
// and a short read timeout, so a transient blip on get.videolan.org fails the
|
|
// whole desktop build on CI (Windows MSI, macOS DMG, Linux DEB). Configure all
|
|
// Download tasks in this project to retry with generous timeouts so flaky
|
|
// network conditions do not break packaging jobs.
|
|
tasks.withType<Download>().configureEach {
|
|
// 5 attempts total (initial + 4 retries) before failing the task.
|
|
retries(4)
|
|
// 30s to establish a TCP / TLS connection.
|
|
connectTimeout(30_000)
|
|
// 5 minutes per attempt for the body — VLC archives are 40-90 MB and
|
|
// get.videolan.org can be slow under load.
|
|
readTimeout(5 * 60_000)
|
|
// Stage to a temp file and rename only on full success, so a partial
|
|
// download from one attempt cannot poison the next.
|
|
tempAndMove(true)
|
|
}
|
|
|
|
// Opt-out for VLC bundling. CI (build.yml) and ephemeral build sandboxes
|
|
// (Claude Code web) routinely lose connectivity to get.videolan.org and the
|
|
// retry budget above is not enough to mask it. Setting `-PskipVlcSetup=true`
|
|
// (or env `AMETHYST_SKIP_VLC=true`) disables the vlc/upx download + extract
|
|
// tasks so packaging proceeds without bundling VLC. The release workflow
|
|
// (create-release.yml) does NOT set this, so production artifacts still ship
|
|
// with VLC bundled.
|
|
val skipVlcSetup =
|
|
(project.findProperty("skipVlcSetup") as? String)?.toBoolean() == true ||
|
|
System.getenv("AMETHYST_SKIP_VLC") == "true"
|
|
|
|
if (skipVlcSetup) {
|
|
logger.lifecycle("desktopApp: skipVlcSetup=true — disabling vlc-setup plugin tasks. Built artifacts will NOT bundle VLC.")
|
|
val vlcPluginTasks = setOf(
|
|
"vlcDownload",
|
|
"vlcExtract",
|
|
"vlcFilterPlugins",
|
|
"vlcCompressPlugins",
|
|
"upxDownload",
|
|
"upxExtract",
|
|
"vlcSetup",
|
|
)
|
|
tasks.matching { it.name in vlcPluginTasks }.configureEach {
|
|
enabled = false
|
|
}
|
|
}
|
|
|
|
// --- 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")
|
|
// Bypass FUSE requirement on CI runners (ubuntu-latest lacks libfuse.so.2).
|
|
// AppImage standard env var: extracts + runs without mounting.
|
|
environment("APPIMAGE_EXTRACT_AND_RUN", "1")
|
|
}
|