161 lines
8.0 KiB
Kotlin
161 lines
8.0 KiB
Kotlin
// SleepyOwl for the Linux desktop (docs/desktop.md): windows, desktop screens, 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.
|
||
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 (gradle.properties, or `-Psleepyowl.versionName=…` from CI): the About screen, the log, the .deb. */
|
||
val appVersion = providers.gradleProperty("sleepyowl.versionName").get()
|
||
|
||
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"))
|
||
implementation(compose.desktop.currentOs)
|
||
implementation(libs.koin.core)
|
||
implementation(libs.koin.compose.viewmodel)
|
||
implementation(libs.coil.compose)
|
||
implementation(libs.coil.network.okhttp)
|
||
implementation(libs.kotlinx.coroutines.swing)
|
||
implementation(libs.jna)
|
||
implementation(libs.dbus.java.core)
|
||
implementation(libs.dbus.java.unixsocket)
|
||
|
||
testImplementation(libs.junit)
|
||
testImplementation(libs.koin.test)
|
||
}
|
||
|
||
// XToolkit: the windows' WM_CLASS (sun.awt.X11.XToolkit.awtAppClassName), set before the first window.
|
||
val awtOpens = listOf("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED", "--add-opens=java.desktop/sun.awt=ALL-UNNAMED")
|
||
|
||
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.
|
||
tasks.register("desktopClasspath") {
|
||
val cp = sourceSets.main.get().runtimeClasspath
|
||
val out = layout.buildDirectory.file("desktop.classpath")
|
||
inputs.files(cp)
|
||
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")
|
||
}
|