Files
amethyst/desktopApp/build.gradle.kts
T
mstrofnone fd7ce48964 ci(desktop): retry vlcDownload on transient network failures
Every desktop packaging job (Windows MSI, macOS DMG, Linux DEB) currently
breaks on the smallest blip when fetching VLC from get.videolan.org. The
`ir.mahozad.vlc-setup` plugin registers `vlcDownload` / `upxDownload`
tasks that extend `de.undercouch.gradle.tasks.download.Download`, but it
does not configure retries or a generous read timeout, so a single
`SocketTimeoutException` aborts the build.

Recent main runs all failed at the same step:

    > Task :desktopApp:vlcDownload FAILED
    > A failure occurred while executing ...DefaultWorkAction
       > java.net.SocketTimeoutException: Read timed out

Configure all `Download` tasks in this project with:

  - 5 attempts total (initial + 4 retries),
  - 30 s connect timeout,
  - 5 min read timeout (VLC archives are 40-90 MB and the mirror can be
    slow under load),
  - `tempAndMove` so a partial download from one attempt cannot poison
    the next.

This is a CI-only change \u2014 it does not alter what gets bundled, just
makes the fetch resilient.
2026-04-29 20:21:18 +10:00

232 lines
8.4 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)
}
// --- 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")
}