Files
llamamod/build.gradle
T

294 lines
13 KiB
Groovy

plugins {
id 'application'
id 'com.diffplug.spotless' version '7.0.2'
}
group = 'com.eliasmueller.llamamod'
// Release builds pass -PreleaseVersion=<tag without v> (see .gitea/workflows/release.yml).
version = findProperty('releaseVersion') ?: '0.2.1'
repositories {
mavenCentral()
}
def lwjglVersion = '3.4.2'
def lwjglNatives = ['natives-linux', 'natives-windows']
dependencies {
implementation 'com.formdev:flatlaf:3.5.4'
implementation 'com.formdev:flatlaf-fonts-inter:4.1'
// level editor viewport: LWJGL (OpenGL) inside Swing via lwjgl3-awt; natives for both packages
implementation platform("org.lwjgl:lwjgl-bom:${lwjglVersion}")
implementation('org.lwjglx:lwjgl3-awt:0.2.5') { transitive = false }
implementation 'org.joml:joml:1.10.8'
['lwjgl', 'lwjgl-opengl', 'lwjgl-jawt', 'lwjgl-egl'].each { m ->
implementation "org.lwjgl:${m}"
if (m in ['lwjgl', 'lwjgl-opengl']) {
lwjglNatives.each { n -> runtimeOnly "org.lwjgl:${m}::${n}" }
}
}
testImplementation platform('org.junit:junit-bom:5.11.4')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
def targetJavaVersion = 25
java {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.release.set(targetJavaVersion)
}
application {
mainClass = 'com.eliasmueller.llamamod.Main'
}
// build-info.properties: version and a build id (hash of the compiled classes and resources, without
// the edition switch, so an unchanged rebuild keeps the id and the mod cache stays valid)
def buildInfoDir = layout.buildDirectory.dir('generated/buildinfo')
tasks.register('buildInfo') {
def classes = sourceSets.main.output.classesDirs
def resources = fileTree('src/main/resources') { exclude 'com/eliasmueller/llamamod/edition.properties' }
def ver = project.version.toString()
def outDir = buildInfoDir
inputs.files(classes).withPathSensitivity(PathSensitivity.RELATIVE)
inputs.files(resources).withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property('version', ver)
outputs.dir(outDir)
doLast {
def md = java.security.MessageDigest.getInstance('SHA-256')
def add = { File root ->
if (!root.exists()) {
return
}
def files = []
root.eachFileRecurse(groovy.io.FileType.FILES) { files << it }
files.sort { root.toPath().relativize(it.toPath()).toString().replace('\\', '/') }.each { f ->
if (f.name == 'edition.properties') {
return
}
md.update(root.toPath().relativize(f.toPath()).toString().replace('\\', '/').getBytes('UTF-8'))
md.update(f.bytes)
}
}
classes.files.each(add)
add(file('src/main/resources'))
def id = md.digest().encodeHex().toString().substring(0, 10)
def f = outDir.get().file('com/eliasmueller/llamamod/build-info.properties').asFile
f.parentFile.mkdirs()
f.text = "version=${ver}\nbuild=${id}\n"
}
}
// user manual (docs/manual, Markdown + images) bundled for the editor's Help → Manual window
processResources {
from('docs/manual') {
into 'com/eliasmueller/llamamod/editor/tools/help/manual'
}
from(tasks.named('buildInfo'))
}
test {
useJUnitPlatform()
// golden tests: -Dllamamod.cd=<cd dir> (or env LLAMAMOD_CD) -Dllamamod.ref=<python reference dir>;
// tests that need the CD are skipped when the folder does not exist
systemProperty 'llamamod.cd', System.getProperty('llamamod.cd', System.getenv('LLAMAMOD_CD') ?: "${System.getProperty('user.home')}/.wine-lama/iso-dk/Setup/Eng/Cd")
systemProperty 'llamamod.ref', System.getProperty('llamamod.ref', '')
// level builder references: python3 tools/levelbuild.py levels/testwelt.py <lbref>/testwelt.wad etc.
systemProperty 'llamamod.lbref', System.getProperty('llamamod.lbref', '')
// English groove.exe (2000-10-17) for the version tests; never committed, skipped when absent
systemProperty 'llamamod.exeEn', System.getProperty('llamamod.exeEn', "${projectDir}/re/groove_en.exe")
maxHeapSize = '2g'
}
// fat jars with all dependencies: full edition (fatJar) and launcher edition without the editor UI
// (fatJarLauncher, edition.properties editor=false; the editor classes stay, mods need them)
def editions = ['full', 'launcher']
def launcherEditionDir = layout.buildDirectory.dir('generated/edition-launcher')
tasks.register('launcherEdition') {
def outDir = launcherEditionDir
outputs.dir(outDir)
doLast {
def f = outDir.get().file('com/eliasmueller/llamamod/edition.properties').asFile
f.parentFile.mkdirs()
f.text = 'editor=false\n'
}
}
editions.each { ed ->
tasks.register(ed == 'full' ? 'fatJar' : 'fatJarLauncher', Jar) {
archiveClassifier = ed == 'full' ? 'all' : 'launcher'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes 'Main-Class': application.mainClass, 'Implementation-Version': project.version, 'Multi-Release': 'true'
}
if (ed == 'full') {
from sourceSets.main.output
} else {
from(tasks.named('launcherEdition'))
from(sourceSets.main.output) { exclude 'com/eliasmueller/llamamod/edition.properties' }
}
dependsOn configurations.runtimeClasspath
from { configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) } }
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'module-info.class', 'META-INF/versions/*/module-info.class'
}
}
spotless {
java {
target 'src/**/*.java'
palantirJavaFormat()
removeUnusedImports()
}
}
// ---------------------------------------------------------------- packaging
// Self-contained Linux folder (+ .tar.gz) and Windows zip (LlamaMod.exe + runtime), both built on
// Linux. jlink cross-links using the Temurin jmods of the target platform; the jlink binary must
// be the exact same JDK version (JDK 25 ships no jmods, so they are downloaded separately).
// Downloads are cached in ~/opt (override: -Pllamamod.opt=<dir>).
def runtimeVersion = '25.0.5+8'
def optDir = file(findProperty('llamamod.opt') ?: "${System.getProperty('user.home')}/opt")
def distDir = layout.buildDirectory.dir('dist').get().asFile
// jdk.unsupported: sun.misc.Unsafe for LWJGL
def runtimeModules = 'java.base,java.desktop,java.prefs,java.logging,jdk.charsets,jdk.zipfs,jdk.unsupported'
def extraModules = [linux: '', windows: ',jdk.crypto.mscapi']
def adoptium = { String os, String imageType ->
"https://api.adoptium.net/v3/binary/version/jdk-${runtimeVersion.replace('+', '%2B')}/${os}/x64/${imageType}/hotspot/normal/eclipse"
}
def sh = { List cmd ->
def p = new ProcessBuilder(cmd.collect { it.toString() }).inheritIO().start()
if (p.waitFor() != 0) {
throw new GradleException("Command failed: ${cmd.join(' ')}")
}
}
def download = { String url, File target ->
if (!target.exists()) {
target.parentFile.mkdirs()
logger.lifecycle("Downloading ${url}")
def tmp = new File(target.path + '.part')
new URI(url).toURL().withInputStream { i -> tmp.withOutputStream { it << i } }
tmp.renameTo(target)
}
target
}
tasks.register('fetchJdk') {
description = 'Downloads the Linux JDK (for jlink) and the jmods of both target platforms.'
doLast {
def jdk = new File(optDir, "jdk-${runtimeVersion}")
if (!new File(jdk, 'bin/jlink').exists()) {
def tgz = download(adoptium('linux', 'jdk'), new File(optDir, "dl/jdk-${runtimeVersion}-linux.tar.gz"))
sh(['tar', 'xzf', tgz, '-C', optDir])
}
['linux': 'tar.gz', 'windows': 'zip'].each { os, ext ->
def dir = new File(optDir, "jmods-${runtimeVersion}-${os}")
if (!dir.exists()) {
def arc = download(adoptium(os, 'jmods'), new File(optDir, "dl/jmods-${runtimeVersion}-${os}.${ext}"))
def tmp = new File(optDir, "jmods-${runtimeVersion}-${os}.tmp")
project.delete(tmp)
tmp.mkdirs()
if (ext == 'zip') {
project.copy { from project.zipTree(arc); into tmp }
} else {
sh(['tar', 'xzf', arc, '-C', tmp])
}
// archives contain one top-level folder holding the *.jmod files
def jmodDir = fileTree(tmp).matching { include '**/java.base.jmod' }.singleFile.parentFile
jmodDir.renameTo(dir)
project.delete(tmp)
}
}
}
}
def jlinkTask = { String name, String os ->
tasks.register(name) {
dependsOn 'fetchJdk'
def out = new File(layout.buildDirectory.get().asFile, "runtime-${os}")
outputs.dir(out)
doLast {
project.delete(out)
sh([new File(optDir, "jdk-${runtimeVersion}/bin/jlink").path,
'--module-path', new File(optDir, "jmods-${runtimeVersion}-${os}").path,
'--add-modules', runtimeModules + extraModules[os],
'--strip-debug', '--no-header-files', '--no-man-pages',
'--compress', 'zip-9',
'--output', out.path])
}
}
}
jlinkTask('jlinkLinux', 'linux')
jlinkTask('jlinkWindows', 'windows')
def jarTask = { String ed -> ed == 'full' ? 'fatJar' : 'fatJarLauncher' }
def suffix = { String ed -> ed == 'full' ? '' : 'Launcher' }
def packageName = { String ed, String os -> "LlamaMod-${project.version}-${ed}-${os}-x64" }
editions.each { ed ->
def stage = new File(distDir, "${ed}/LlamaMod-linux-x64")
tasks.register("linuxImage${suffix(ed)}", Sync) {
dependsOn jarTask(ed), 'jlinkLinux'
from(layout.buildDirectory.dir('runtime-linux')) { into 'runtime' }
from(tasks.named(jarTask(ed))) { into 'lib'; rename { 'llamamod.jar' } }
from('packaging/linux') { filePermissions { unix('rwxr-xr-x') }; include 'LlamaMod', '*.sh' }
from('packaging/linux') { exclude 'LlamaMod', '*.sh' }
from('src/main/resources/com/eliasmueller/llamamod/ui/icon-256.png') { rename { 'llamamod.png' } }
into stage
// jlink marks legal/ files read-only, so Sync cannot overwrite them
doFirst { project.delete(stage) }
}
tasks.register("packageLinux${suffix(ed)}") {
description = "Self-contained Linux build (${ed} edition): build/dist/${packageName(ed, 'linux')}.tar.gz."
dependsOn "linuxImage${suffix(ed)}"
doLast {
// system tar keeps the executable bits of the runtime (Gradle's Tar normalizes them)
sh(['tar', 'czf', new File(distDir, "${packageName(ed, 'linux')}.tar.gz"), '-C', stage.parentFile, stage.name])
}
}
def exe = new File(layout.buildDirectory.get().asFile, "exe/${ed}/LlamaMod.exe")
tasks.register("exeWindows${suffix(ed)}") {
description = 'Wraps the fat jar into LlamaMod.exe with launch4j (Linux binary, downloaded to ~/opt).'
dependsOn jarTask(ed)
inputs.files(tasks.named(jarTask(ed)))
inputs.dir('packaging/windows')
outputs.file(exe)
doLast {
def l4j = new File(optDir, 'launch4j')
if (!new File(l4j, 'launch4j.jar').exists()) {
def tgz = download('https://sourceforge.net/projects/launch4j/files/launch4j-3/3.50/launch4j-3.50-linux-x64.tgz/download',
new File(optDir, 'dl/launch4j-3.50-linux-x64.tgz'))
sh(['tar', 'xzf', tgz, '-C', optDir])
}
exe.parentFile.mkdirs()
def cfg = new File(exe.parentFile, 'launch4j.xml')
cfg.text = file('packaging/windows/launch4j.xml.in').text
.replace('@JAR@', tasks.named(jarTask(ed)).get().archiveFile.get().asFile.path)
.replace('@OUT@', exe.path)
.replace('@ICON@', file('packaging/icon.ico').path)
.replace('@VERSION@', project.version.toString())
sh([new File(optDir, "jdk-${runtimeVersion}/bin/java").path, '-jar', new File(l4j, 'launch4j.jar').path, cfg.path])
}
}
tasks.register("packageWindows${suffix(ed)}", Zip) {
description = "Windows build (made on Linux, ${ed} edition): build/dist/${packageName(ed, 'windows')}.zip."
dependsOn "exeWindows${suffix(ed)}", 'jlinkWindows'
from(layout.buildDirectory.dir('runtime-windows')) { into 'LlamaMod/runtime' }
from(exe) { into 'LlamaMod' }
from('packaging/windows') { exclude '*.in'; into 'LlamaMod' }
destinationDirectory = distDir
archiveFileName = "${packageName(ed, 'windows')}.zip"
}
}
tasks.register('packageAll') {
description = 'Linux and Windows packages of both editions (full, launcher) in build/dist.'
dependsOn editions.collectMany { ["packageLinux${suffix(it)}", "packageWindows${suffix(it)}"] }
}