Desktop .deb package

This commit is contained in:
2026-10-27 12:52:55 +03:00
parent 371c63c0c3
commit 84fb48f39d
14 changed files with 1042 additions and 25 deletions

View File

@@ -1,13 +1,37 @@
// SleepyOwl for the Linux desktop (LINUX_PLAN.md): windows, desktop screens, later the mpv player and system integration.
// Run: ./gradlew :desktop:run, or tools/linux/desktop.sh without Gradle around the process.
// Package: ./gradlew :desktop:packageDeb → desktop/build/deb/sleepyowl_<version>_amd64.deb (LINUX_PLAN.md L10).
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermissions
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.compose.multiplatform)
}
/** The desktop app's version: the About screen, the log, the .deb. */
val appVersion = "0.1.0"
kotlin { jvmToolchain(21) }
// BuildInfo.VERSION from appVersion: one place for the number.
val buildInfo = tasks.register("buildInfo") {
val version = appVersion
val out = layout.buildDirectory.dir("generated/buildinfo")
inputs.property("version", version)
outputs.dir(out)
doLast {
out.get().file("BuildInfo.kt").asFile.apply { parentFile.mkdirs() }.writeText(
"package ru.stenfredd.sleepyowl.desktop\n\n/** Generated by desktop/build.gradle.kts. */\nobject BuildInfo {\n const val VERSION = \"$version\"\n}\n",
)
}
}
sourceSets.main { kotlin.srcDir(buildInfo) }
dependencies {
implementation(project(":ui:shared"))
implementation(project(":playback:engine-mpv"))
@@ -32,9 +56,26 @@ compose.desktop {
application {
mainClass = "ru.stenfredd.sleepyowl.desktop.MainKt"
jvmArgs += awtOpens
nativeDistributions {
// jpackage of JDK 21 with a jlink'd JRE inside: createDistributable → build/compose/binaries/main/app/sleepyowl.
// The .deb around it is ours (packageDeb below): jpackage's own one can't depend on libmpv2.
javaHome = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(21) }.get()
.metadata.installationPath.asFile.absolutePath
packageName = "sleepyowl"
packageVersion = appVersion
description = "Jellyfin client"
vendor = "StenFredd"
// suggestRuntimeModules gives java.instrument, jdk.security.auth (dbus-java), jdk.unsupported; jdeps can't see
// TLS (jdk.crypto.ec: the public address over https) or locale data (jdk.localedata: track languages in Russian).
modules("java.instrument", "jdk.security.auth", "jdk.unsupported", "jdk.crypto.ec", "jdk.localedata")
linux { iconFile = file("src/main/resources/icons/sleepyowl-256.png") }
}
}
}
// Licence texts of "О приложении": the TV's (app/src/main/assets/licenses) next to the desktop's own in resources/licenses.
tasks.processResources { from(rootProject.file("app/src/main/assets/licenses")) { into("licenses") } }
tasks.withType<Test>().configureEach { jvmArgs(awtOpens) }
// Classpath into a file: tools/linux/desktop.sh starts the app without Gradle.
@@ -45,3 +86,75 @@ tasks.register("desktopClasspath") {
outputs.file(out)
doLast { out.get().asFile.writeText(cp.joinToString(":")) }
}
/**
* The .deb: jpackage's app image in /opt/sleepyowl, `sleepyowl` in /usr/bin, the menu entry and the icons. The Debian
* version gets the build time (0.1.0-202610271530), so `sudo apt install ./sleepyowl_*.deb` always takes a new build as
* an update. Menus and icon caches refresh by dpkg's triggers (desktop-file-utils, hicolor-icon-theme): no scripts.
*/
abstract class PackageDeb : DefaultTask() {
@get:InputDirectory abstract val appImage: DirectoryProperty
@get:InputFile abstract val desktopFile: RegularFileProperty
@get:InputDirectory abstract val icons: DirectoryProperty
@get:Input abstract val version: Property<String>
@get:Input abstract val maintainer: Property<String>
@get:Input abstract val depends: ListProperty<String>
@get:OutputDirectory abstract val outDir: DirectoryProperty
@get:Inject abstract val exec: ExecOperations
@get:Inject abstract val fs: FileSystemOperations
@TaskAction fun build() {
val debVersion = "${version.get()}-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"))
val root = temporaryDir.resolve("root")
fs.delete { delete(root) }
fs.copy { from(appImage); into(root.resolve("opt/sleepyowl")) }
root.resolve("usr/bin").mkdirs()
Files.createSymbolicLink(root.resolve("usr/bin/sleepyowl").toPath(), Path.of("../../opt/sleepyowl/bin/sleepyowl"))
fs.copy { from(desktopFile); into(root.resolve("usr/share/applications")) }
icons.get().asFile.listFiles()!!.forEach { png ->
val size = Regex("sleepyowl-(\\d+)\\.png").matchEntire(png.name)?.groupValues?.get(1) ?: return@forEach
fs.copy { from(png); into(root.resolve("usr/share/icons/hicolor/${size}x$size/apps")); rename { "sleepyowl.png" } }
}
// Debian's modes whatever the umask: 755 for directories and programs, 644 for the rest.
root.walkTopDown().filter { !Files.isSymbolicLink(it.toPath()) }.forEach {
Files.setPosixFilePermissions(it.toPath(), PosixFilePermissions.fromString(if (it.isDirectory || it.canExecute()) "rwxr-xr-x" else "rw-r--r--"))
}
val kb = root.walkTopDown().filter { it.isFile }.sumOf { it.length() } / 1024
root.resolve("DEBIAN").mkdirs()
root.resolve("DEBIAN/control").writeText(
"""
Package: sleepyowl
Version: $debVersion
Architecture: amd64
Maintainer: ${maintainer.get()}
Installed-Size: $kb
Depends: ${depends.get().joinToString(", ")}
Section: video
Priority: optional
Description: Jellyfin client for movies and series
SleepyOwl plays movies and series from a Jellyfin server with mpv, in a window, full screen or in a
small always-on-top window.
""".trimIndent() + "\n",
)
val out = outDir.get().asFile.apply { listFiles()?.forEach { it.delete() } }.resolve("sleepyowl_${debVersion}_amd64.deb")
exec.exec { commandLine("dpkg-deb", "--root-owner-group", "-Zxz", "--build", root, out) }
logger.lifecycle("Built $out")
}
}
tasks.register<PackageDeb>("packageDeb") {
group = "distribution"
dependsOn("createDistributable")
appImage = layout.buildDirectory.dir("compose/binaries/main/app/sleepyowl")
desktopFile = layout.projectDirectory.file("packaging/sleepyowl.desktop")
icons = layout.projectDirectory.dir("src/main/resources/icons")
version = appVersion
maintainer = "StenFredd <d.v.bakaev@yandex.ru>"
// libmpv2, libEGL and libOpenGL are loaded through JNA, so nothing finds them on its own; the rest is what dpkg-shlibdeps gave
// for the JRE's, Skiko's and JNA's libraries in L10 (Mint 22 / Ubuntu 24.04 names).
depends = listOf(
"libmpv2 (>= 0.37)", "libegl1", "libopengl0", "libgl1", "libc6 (>= 2.34)", "libstdc++6", "libfontconfig1 (>= 2.12.6)",
"libx11-6", "libxext6", "libxi6", "libxrender1", "libxtst6", "libasound2t64 | libasound2",
)
outDir = layout.buildDirectory.dir("deb")
}