import org.gradle.internal.os.OperatingSystem plugins { kotlin("jvm") id("org.jetbrains.compose") id("org.jetbrains.kotlin.plugin.compose") } val composeVersion = "1.11.1" val os = OperatingSystem.current() val arch = System.getProperty("os.arch") ?: "amd64" // The repo-root VERSION file is the single source of truth (bump it to cut // a release); CI can still override with -PappVersion. jpackage requires a // plain semver (no leading "v"). val appVersion = (project.findProperty("appVersion") as? String) ?: project .file("../../VERSION") .takeIf { it.exists() } ?.readText() ?.trim() ?: "0.1.0" val desktopTarget = when { os.isMacOsX -> if (arch == "aarch64") "macos-arm64" else "macos-x64" os.isWindows -> "windows-x64" else -> if (arch == "aarch64") "linux-arm64" else "linux-x64" } dependencies { implementation(project(":shared")) implementation("org.jetbrains.compose.desktop:desktop-jvm-$desktopTarget:$composeVersion") // M6: window-state persistence (window.json). implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") } compose.desktop { application { mainClass = "iris.desktop.MainKt" } } // M9: KCEF (JCEF/Chromium) for HTML artifact previews needs AWT reflection // access on modern JDKs. Applied to the `run` task (JavaExec) and, below, to // the jpackage `--java-options`. afterEvaluate { tasks.withType { jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED") jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED") } } // ── M6: native packaging (docs/11 §11.4) ───────────────────────────────── // `./gradlew :desktopApp:jpackage` builds a self-contained app image under // build/jpackage (JRE bundled). `./gradlew :desktopApp:jpackage // -PjpackageType=deb` builds a .deb instead (needs dpkg-deb). val jpackageInputDir = layout.buildDirectory.dir("jpackage-input") val fatJar = tasks.register("fatJar") { archiveClassifier.set("all") duplicatesStrategy = DuplicatesStrategy.EXCLUDE manifest { attributes("Main-Class" to "iris.desktop.MainKt") } from(sourceSets.main.get().output) from({ configurations.runtimeClasspath .get() .filter { it.name.endsWith(".jar") } .map { zipTree(it) } }) { exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") } } // Stage the fat jar into the jpackage input dir. A dedicated Sync task keeps // the Exec task free of task references (configuration-cache compatible) and // prunes stale jars from the input dir automatically. tasks.register("stageJpackageInput") { dependsOn(fatJar) from(fatJar) into(jpackageInputDir) } tasks.register("jpackage") { dependsOn("stageJpackageInput") val jpackageBin = file("${System.getProperty("java.home")}/bin/jpackage") val type = (project.findProperty("jpackageType") as? String) ?: "app-image" val inputDir = jpackageInputDir.get().asFile val destDir = layout.buildDirectory .dir("jpackage") .get() .asFile inputs.dir(inputDir) outputs.dir(destDir) commandLine( if (jpackageBin.exists()) jpackageBin.absolutePath else "jpackage", "--name", "iris", "--app-version", appVersion, "--vendor", "Iris", "--type", type, "--input", inputDir.absolutePath, "--main-jar", "desktopApp-all.jar", "--main-class", "iris.desktop.MainKt", "--icon", file("src/main/resources/icon.png").absolutePath, "--dest", destDir.absolutePath, "--java-options", "-Xmx1g", // M9: KCEF (JCEF) AWT reflection access (see the JavaExec block above). "--java-options", "--add-opens=java.desktop/sun.awt=ALL-UNNAMED", "--java-options", "--add-opens=java.desktop/java.awt.peer=ALL-UNNAMED", ) }