Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("dev.kikugie.stonecutter")
alias(libs.plugins.fabric.loom)
alias(libs.plugins.fabric.loom.remap)
id("maven-publish")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.mod.publish.plugin)
Expand All @@ -19,11 +19,6 @@ base {
}

repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
maven { url = uri("https://maven.shedaniel.me") }
maven { url = uri("https://maven.isxander.dev/releases") }
maven { url = uri("https://maven.terraformersmc.com/releases") }
Expand Down Expand Up @@ -161,9 +156,10 @@ tasks {
processResources {
inputs.property("version", project.version)
inputs.property("minecraft_version", mcVersion)
inputs.property("java_version", 21)

filesMatching("fabric.mod.json") {
expand(mapOf("version" to inputs.properties["version"], "minecraft_version" to inputs.properties["minecraft_version"]))
expand(mapOf("version" to inputs.properties["version"], "minecraft_version" to inputs.properties["minecraft_version"], "java_version" to inputs.properties["java_version"]))
}
}
jar {
Expand Down Expand Up @@ -207,9 +203,6 @@ kotlin {
}

java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()

sourceCompatibility = JavaVersion.VERSION_21
Expand Down Expand Up @@ -248,26 +241,18 @@ publishing {
}
}

// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}

// Auto-start Xvfb for headless client test execution (Wayland / headless environments)
// Shared state for Xvfb process between run task and cleanup task
val xvfbState = objects.property<Process>()
val xvfbShutdownHook = objects.property<Thread>()

fun needsXvfb(): Boolean {
val display = System.getenv("DISPLAY")
if (display.isNullOrBlank()) return true
// For remote displays (e.g., SSH X11 forwarding like localhost:10.0), trust the env
if (display.contains(":") && !display.startsWith(":")) return false
// For local displays, check if the X11 socket file exists (simple heuristic; does not probe connection)
val displayNum = display.removePrefix(":").takeWhile { it.isDigit() }
val socket = File("/tmp/.X11-unix/X$displayNum")
return !socket.exists()
Expand All @@ -286,7 +271,6 @@ fun findXvfb(): String? {
}

fun startXvfb(xvfb: String): Pair<Process, String> {
// Try multiple display numbers to handle concurrent usage
for (displayNum in 99..199) {
val display = ":$displayNum"
if (File("/tmp/.X11-unix/X$displayNum").exists()) continue
Expand All @@ -295,7 +279,6 @@ fun startXvfb(xvfb: String): Pair<Process, String> {
.redirectErrorStream(true)
.start()

// Poll for X11 socket to appear (readiness check)
val socketFile = File("/tmp/.X11-unix/X$displayNum")
val deadline = System.currentTimeMillis() + 5_000
while (System.currentTimeMillis() < deadline) {
Expand All @@ -304,7 +287,6 @@ fun startXvfb(xvfb: String): Pair<Process, String> {
Thread.sleep(100)
}

// This display didn't work, clean up and try next
if (process.isAlive) process.destroyForcibly()
}
error("Failed to start Xvfb: no available display number in :99..:199")
Expand All @@ -321,7 +303,6 @@ val cleanupXvfbTask = tasks.register("cleanupXvfb") {
if (process.isAlive) process.destroyForcibly()
}
}
// Remove shutdown hook after process cleanup (keeps hook as safety net until stop completes)
xvfbShutdownHook.orNull?.let { hook ->
runCatching { Runtime.getRuntime().removeShutdownHook(hook) }
}
Expand All @@ -342,7 +323,6 @@ tasks.named<JavaExec>("runClientGameTest") {

val (process, display) = startXvfb(xvfb)
xvfbState.set(process)
// Last-resort cleanup for JVM crash (daemon shutdown)
val shutdownHook = Thread { if (process.isAlive) process.destroyForcibly() }
Runtime.getRuntime().addShutdownHook(shutdownHook)
xvfbShutdownHook.set(shutdownHook)
Expand Down
285 changes: 285 additions & 0 deletions build.unobfuscated.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import java.util.concurrent.TimeUnit
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("dev.kikugie.stonecutter")
alias(libs.plugins.fabric.loom)
id("maven-publish")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.mod.publish.plugin)
}

val mcVersion = stonecutter.current.version

version = providers.environmentVariable("MOD_VERSION").orElse("dev").get() + "+mc$mcVersion"
group = project.property("maven_group") as String

base {
archivesName.set(project.property("archives_base_name") as String)
}

repositories {
maven { url = uri("https://maven.isxander.dev/releases") }
maven { url = uri("https://maven.terraformersmc.com/releases") }

exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = uri("https://api.modrinth.com/maven")
}
}
filter {
includeGroup("maven.modrinth")
}
}
}

loom {
splitEnvironmentSourceSets()

mods {
register("connectedtank") {
sourceSet(sourceSets.main.get())
sourceSet(sourceSets.getByName("client"))
}
}
}

fabricApi {
configureDataGeneration {
client = true
}
@Suppress("UnstableApiUsage")
configureTests {
createSourceSet = true
modId = "connectedtank-test"
enableGameTests = true
enableClientGameTests = false
eula = true
}
}

sourceSets.named("gametest") {
kotlin.srcDir("src/gametest/kotlin")
}

val clientGametestSourceSet = sourceSets.create("clientGametest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
compileClasspath += sourceSets.getByName("client").output
runtimeClasspath += sourceSets.getByName("client").output
kotlin.srcDir("src/clientGametest/kotlin")
}

configurations.named(clientGametestSourceSet.compileClasspathConfigurationName) {
extendsFrom(configurations[sourceSets.main.get().compileClasspathConfigurationName])
extendsFrom(configurations[sourceSets.getByName("client").compileClasspathConfigurationName])
}
configurations.named(clientGametestSourceSet.runtimeClasspathConfigurationName) {
extendsFrom(configurations[sourceSets.main.get().runtimeClasspathConfigurationName])
extendsFrom(configurations[sourceSets.getByName("client").runtimeClasspathConfigurationName])
}

loom {
mods {
register("connectedtank-client-test") {
sourceSet(clientGametestSourceSet)
}
}

runs {
register("clientGameTest") {
inherit(runs.getByName("client"))
source(clientGametestSourceSet)
property("fabric.client.gametest")
property(
"fabric.client.gametest.testModResourcesPath",
file("src/clientGametest/resources").absolutePath,
)
runDir("build/run/clientGameTest")
}
}
}

dependencies {
val fabricApiVersion = "0.148.0+26.1.2"
val yaclVersion = "3.9.3+26.1-fabric"
val modmenuVersion = "18.0.0-alpha.8"
val jeiVersion = "29.5.0.28"
val jadeVersion = "26.1.0+fabric"

minecraft("com.mojang:minecraft:$mcVersion")
implementation(libs.fabric.loader)

implementation("net.fabricmc.fabric-api:fabric-api:$fabricApiVersion")
implementation(libs.fabric.language.kotlin)

compileOnly("dev.isxander:yet-another-config-lib:$yaclVersion")
runtimeOnly("dev.isxander:yet-another-config-lib:$yaclVersion")
compileOnly("com.terraformersmc:modmenu:$modmenuVersion")
runtimeOnly("com.terraformersmc:modmenu:$modmenuVersion")

runtimeOnly("maven.modrinth:jei:$jeiVersion")
compileOnly("maven.modrinth:jade:$jadeVersion")
runtimeOnly("maven.modrinth:jade:$jadeVersion")
}

tasks {
processResources {
inputs.property("version", project.version)
inputs.property("minecraft_version", mcVersion)
inputs.property("java_version", 25)

filesMatching("fabric.mod.json") {
expand(mapOf("version" to inputs.properties["version"], "minecraft_version" to inputs.properties["minecraft_version"], "java_version" to inputs.properties["java_version"]))
}
}
jar {
inputs.property("archivesName", base.archivesName)

from("LICENSE") {
rename { "${it}_${inputs.properties["archivesName"]}" }
}
}
withType<JavaCompile>().configureEach {
options.release.set(25)
}
}

kotlin {
compilerOptions {
jvmTarget = JvmTarget.fromTarget("25")
}
jvmToolchain(25)
}

java {
withSourcesJar()

sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}

publishMods {
file.set(tasks.jar.flatMap { it.archiveFile })
additionalFiles.from(tasks.named("sourcesJar").map { (it as Jar).archiveFile })
changelog.set(providers.environmentVariable("CHANGELOG").orElse(""))
type.set(STABLE)
modLoaders.add("fabric")

modrinth {
projectId.set(providers.environmentVariable("MODRINTH_ID"))
accessToken.set(providers.environmentVariable("MODRINTH_TOKEN"))
minecraftVersions.add(mcVersion)
requires("fabric-api")
requires("fabric-language-kotlin")
}
curseforge {
projectId.set(providers.environmentVariable("CURSEFORGE_ID"))
accessToken.set(providers.environmentVariable("CURSEFORGE_TOKEN"))
minecraftVersions.add(mcVersion)
requires("fabric-api")
requires("fabric-language-kotlin")
}
}

// configure the maven publication
publishing {
publications {
create<MavenPublication>("mavenJava") {
artifactId = project.property("archives_base_name") as String
from(components["java"])
}
}

repositories {
}
}

// Auto-start Xvfb for headless client test execution (Wayland / headless environments)
val xvfbState = objects.property<Process>()
val xvfbShutdownHook = objects.property<Thread>()

fun needsXvfb(): Boolean {
val display = System.getenv("DISPLAY")
if (display.isNullOrBlank()) return true
if (display.contains(":") && !display.startsWith(":")) return false
val displayNum = display.removePrefix(":").takeWhile { it.isDigit() }
val socket = File("/tmp/.X11-unix/X$displayNum")
return !socket.exists()
}

fun findXvfb(): String? {
val candidates = listOf("Xvfb", "/usr/bin/Xvfb")
return candidates.firstOrNull { name ->
runCatching {
ProcessBuilder("which", name)
.redirectErrorStream(true)
.start()
.waitFor() == 0
}.getOrDefault(false)
}
}

fun startXvfb(xvfb: String): Pair<Process, String> {
for (displayNum in 99..199) {
val display = ":$displayNum"
if (File("/tmp/.X11-unix/X$displayNum").exists()) continue

val process = ProcessBuilder(xvfb, display, "-screen", "0", "1280x1024x24", "-nolisten", "tcp")
.redirectErrorStream(true)
.start()

val socketFile = File("/tmp/.X11-unix/X$displayNum")
val deadline = System.currentTimeMillis() + 5_000
while (System.currentTimeMillis() < deadline) {
if (!process.isAlive) break
if (socketFile.exists()) return process to display
Thread.sleep(100)
}

if (process.isAlive) process.destroyForcibly()
}
error("Failed to start Xvfb: no available display number in :99..:199")
}

val cleanupXvfbTask = tasks.register("cleanupXvfb") {
notCompatibleWithConfigurationCache("Manages Xvfb process lifecycle at execution time")
doLast {
xvfbState.orNull?.let { process ->
if (process.isAlive) {
logger.lifecycle("Stopping Xvfb (pid: ${process.pid()})")
process.destroy()
process.waitFor(5, TimeUnit.SECONDS)
if (process.isAlive) process.destroyForcibly()
}
}
xvfbShutdownHook.orNull?.let { hook ->
runCatching { Runtime.getRuntime().removeShutdownHook(hook) }
}
}
}

tasks.named<JavaExec>("runClientGameTest") {
notCompatibleWithConfigurationCache("Manages Xvfb process lifecycle at execution time")
finalizedBy(cleanupXvfbTask)

doFirst {
if (!needsXvfb()) return@doFirst

val xvfb = findXvfb() ?: error(
"No usable DISPLAY found and Xvfb is not installed. " +
"Install Xvfb or run with a display server (e.g., xvfb-run ./gradlew runClientGameTest)",
)

val (process, display) = startXvfb(xvfb)
xvfbState.set(process)
val shutdownHook = Thread { if (process.isAlive) process.destroyForcibly() }
Runtime.getRuntime().addShutdownHook(shutdownHook)
xvfbShutdownHook.set(shutdownHook)

logger.lifecycle("Started Xvfb on display $display (pid: ${process.pid()})")
environment("DISPLAY", display)
}
}
Loading
Loading