1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-21 12:43:21 +00:00

Move some more build logic to buildSrc

Look, I don't enjoy having 600 LOC long build.gradle files, it's just
very easy to do! This at least moves some of the complexity elsewhere,
so the build script is a little more declarative.
This commit is contained in:
Jonathan Coates 2022-10-22 20:47:47 +01:00
parent af5d816798
commit 0cfdd7b5e9
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
11 changed files with 326 additions and 171 deletions

View File

@ -83,7 +83,7 @@ ### Testing
- In-game (`./src/testMod/java/dan200/computercraft/ingame/`): These tests are run on an actual Minecraft server, using
the same system Mojang do][mc-test]. The aim of these is to test in-game behaviour of blocks and peripherals.
These tests are run with `./gradlew testServer`.
These tests are run with `./gradlew runGametest`.
## CraftOS tests
CraftOS's tests are written using a test system called "mcfly", heavily inspired by [busted] (and thus RSpec). Groups of

View File

@ -1,6 +1,4 @@
plugins {
id "checkstyle"
id "jacoco"
id "maven-publish"
id "com.matthewprenger.cursegradle" version "1.4.0"
id "com.github.breadmoirai.github-release" version "2.2.12"
@ -12,9 +10,10 @@
id "com.github.johnrengelman.shadow" version "7.1.2"
id("cc-tweaked.illuaminate")
id("cc-tweaked.java-convention")
id("cc-tweaked")
}
import cc.tweaked.gradle.CheckChangelog
import cc.tweaked.gradle.ExtensionsKt
import cc.tweaked.gradle.IlluaminateExec
import cc.tweaked.gradle.IlluaminateExecToDir
@ -25,16 +24,7 @@
group = "org.squiddev"
archivesBaseName = "cc-tweaked-${mc_version}"
def javaVersion = JavaLanguageVersion.of(8)
java {
toolchain {
languageVersion = javaVersion
}
withSourcesJar()
withJavadocJar()
registerFeature("extraMods") { usingSourceSet(sourceSets.main) }
}
java.registerFeature("extraMods") { usingSourceSet(sourceSets.main) }
sourceSets {
main.resources {
@ -115,14 +105,6 @@ accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
shadowJar {}
}
repositories {
mavenCentral()
maven {
name "SquidDev"
url "https://squiddev.cc/maven"
}
}
configurations {
shade { transitive = false }
implementation.extendsFrom shade
@ -133,8 +115,6 @@ accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
}
dependencies {
checkstyle "com.puppycrawl.tools:checkstyle:8.25"
minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
@ -222,77 +202,21 @@ accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
assemble.dependsOn("shadowJar")
[
tasks.named("compileJava", JavaCompile.class),
tasks.named("compileTestJava", JavaCompile.class),
tasks.named("compileTestModJava", JavaCompile.class)
].forEach {
it.configure {
options.compilerArgs << "-Xlint" << "-Xlint:-processing"
}
}
processResources {
inputs.property "version", mod_version
inputs.property "mcversion", mc_version
def hash = 'none'
Set<String> contributors = []
try {
hash = ["git", "-C", projectDir, "rev-parse", "HEAD"].execute().text.trim()
inputs.property("gitHash", cct.gitHash)
def blacklist = ['GitHub', 'Daniel Ratcliffe', 'Weblate']
// Extract all authors, commiters and co-authors from the git log.
def authors = ["git", "-C", projectDir, "log", "--format=tformat:%an <%ae>%n%cn <%ce>%n%(trailers:key=Co-authored-by,valueonly)"]
.execute().text.readLines().unique()
// We now pass this through git's mailmap to de-duplicate some authors.
def remapAuthors = ["git", "check-mailmap", "--stdin"].execute()
remapAuthors.withWriter { stdin ->
if (stdin !instanceof BufferedWriter) stdin = new BufferedWriter(stdin)
authors.forEach {
if (it == "") return
if (!it.endsWith(">")) it += ">" // Some commits have broken Co-Authored-By lines!
stdin.writeLine(it)
}
stdin.close()
}
// And finally extract out the actual name.
def emailRegex = ~/^([^<]+) <.+>$/
remapAuthors.text.readLines().forEach {
def matcher = it =~ emailRegex
matcher.find()
def name = matcher.group(1)
if (!blacklist.contains(name)) contributors.add(name)
}
} catch (Exception e) {
e.printStackTrace()
}
inputs.property "commithash", hash
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from(sourceSets.main.resources.srcDirs) {
include 'META-INF/mods.toml'
include 'data/computercraft/lua/rom/help/credits.txt'
expand 'version': mod_version,
'mcversion': mc_version,
'gitcontributors': contributors.sort(false, String.CASE_INSENSITIVE_ORDER).join('\n')
filesMatching("data/computercraft/lua/rom/help/credits.txt") {
expand("gitContributors": cct.gitContributors.get().join("\n"))
}
from(sourceSets.main.resources.srcDirs) {
exclude 'META-INF/mods.toml'
exclude 'data/computercraft/lua/rom/help/credits.txt'
filesMatching("META-INF/mods.toml") {
expand("version": mod_version, "mcversion": mc_version)
}
}
sourcesJar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
// Web tasks
List<String> mkCommand(String command) {
@ -378,16 +302,6 @@ commandLine mkCommand('"node_modules/.bin/ts-node" -T --esm src/web/transform.ts
}
}
jacocoTestReport {
dependsOn('test')
reports {
xml.required = true
html.required = true
}
}
test.finalizedBy("jacocoTestReport")
def lintLua = tasks.register("lintLua", IlluaminateExec.class) {
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Lint Lua (and Lua docs) with illuaminate"
@ -417,88 +331,29 @@ commandLine mkCommand('"node_modules/.bin/ts-node" -T --esm src/web/transform.ts
into "test-files/server"
}
def testServerClassDumpDir = new File(buildDir, "jacocoClassDump/runTestServer")
def testServer = tasks.register("testServer", JavaExec.class) {
def runGametest = tasks.register("runGametest", JavaExec.class) {
group("In-game tests")
description("Runs tests on a temporary Minecraft instance.")
dependsOn(setupServer, "cleanTestServer")
finalizedBy("jacocoTestServerReport")
dependsOn(setupServer, "cleanRunGametest")
// Copy from runTestServer. We do it in this slightly odd way as runTestServer
// isn't created until the task is configured (which is no good for us).
ExtensionsKt.copyToFull(tasks.getByName("runTestServer"), it)
// Jacoco and modlauncher don't play well together as the classes loaded in-game don't
// match up with those written to disk. We get Jacoco to dump all classes to disk, and
// use that when generating the report.
jacoco.applyTo(it)
it.jacoco.setIncludes(["dan200.computercraft.*"])
it.jacoco.setClassDumpDir(testServerClassDumpDir)
outputs.dir(testServerClassDumpDir)
// Older versions of modlauncher don't include a protection domain (and thus no code
// source). Jacoco skips such classes by default, so we need to explicitly include them.
it.jacoco.setIncludeNoLocationClasses(true)
}
tasks.register("jacocoTestServerReport", JacocoReport.class) {
group("In-game tests")
description("Generate coverage reports for testServer")
dependsOn(testServer)
cct.jacoco(runGametest)
executionData(new File(buildDir, "jacoco/testServer.exec"))
sourceDirectories.from(sourceSets.main.allJava.srcDirs)
classDirectories.from(testServerClassDumpDir)
reports {
xml.enabled true
html.enabled true
}
}
check.dependsOn(testServer)
tasks.check { dependsOn(runGametest) }
// Upload tasks
def checkRelease = tasks.register("checkRelease") {
group "upload"
description "Verifies that everything is ready for a release"
inputs.property "version", mod_version
inputs.file("src/main/resources/data/computercraft/lua/rom/help/changelog.md")
inputs.file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")
doLast {
def ok = true
// Check we're targetting the current version
def whatsnew = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/whatsnew.md").readLines()
if (whatsnew[0] != "New features in CC: Tweaked $mod_version") {
ok = false
project.logger.error("Expected `whatsnew.md' to target $mod_version.")
}
// Check "read more" exists and trim it
def idx = whatsnew.findIndexOf { it == 'Type "help changelog" to see the full version history.' }
if (idx == -1) {
ok = false
project.logger.error("Must mention the changelog in whatsnew.md")
} else {
whatsnew = whatsnew.getAt(0..<idx)
}
// Check whatsnew and changelog match.
def versionChangelog = "# " + whatsnew.join("\n")
def changelog = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/changelog.md").getText()
if (!changelog.startsWith(versionChangelog)) {
ok = false
project.logger.error("whatsnew and changelog are not in sync")
}
if (!ok) throw new IllegalStateException("Could not check release")
}
def checkChangelog = tasks.register("checkChangelog", CheckChangelog.class) {
version.set(mod_version)
whatsNew.set(file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md"))
changelog.set(file("src/main/resources/data/computercraft/lua/rom/help/changelog.md"))
}
check.dependsOn(checkRelease)
tasks.check { dependsOn(checkChangelog) }
def isStable = true
@ -595,7 +450,7 @@ commandLine mkCommand('"node_modules/.bin/ts-node" -T --esm src/web/transform.ts
}
def uploadTasks = ["publish", "curseforge", "modrinth", "githubRelease"]
uploadTasks.forEach { tasks.named(it) { dependsOn(checkRelease) } }
uploadTasks.forEach { tasks.named(it) { dependsOn(checkChangelog) } }
tasks.register("uploadAll") {
group = "upload"

View File

@ -14,6 +14,11 @@ dependencies {
gradlePlugin {
plugins {
register("cc-tweaked") {
id = "cc-tweaked"
implementationClass = "cc.tweaked.gradle.CCTweakedPlugin"
}
register("cc-tweaked.illuaminate") {
id = "cc-tweaked.illuaminate"
implementationClass = "cc.tweaked.gradle.IlluaminatePlugin"

View File

@ -4,11 +4,68 @@ import com.diffplug.spotless.LineEnding
import java.nio.charset.StandardCharsets
plugins {
java
`java-library`
jacoco
checkstyle
id("com.diffplug.spotless")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
withSourcesJar()
withJavadocJar()
}
repositories {
mavenCentral()
maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
includeGroup("org.squiddev")
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("com.blamejared.crafttweaker")
includeGroup("commoble.morered")
includeGroup("maven.modrinth")
includeGroup("mezz.jei")
}
}
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
checkstyle(libs.findLibrary("checkstyle").get())
}
// Configure default JavaCompile tasks with our arguments.
sourceSets.all {
tasks.named(compileJavaTaskName, JavaCompile::class.java) {
// Processing just gives us "No processor claimed any of these annotations", so skip that!
options.compilerArgs.addAll(listOf("-Xlint", "-Xlint:-processing"))
}
}
tasks.withType(JavaCompile::class.java).configureEach {
options.encoding = "UTF-8"
}
tasks.test {
finalizedBy("jacocoTestReport")
useJUnitPlatform()
testLogging {
events("skipped", "failed")
}
}
tasks.withType(JacocoReport::class.java).configureEach {
reports.xml.required.set(true)
reports.html.required.set(true)
}
spotless {
encoding = StandardCharsets.UTF_8
lineEndings = LineEnding.UNIX

View File

@ -0,0 +1,124 @@
package cc.tweaked.gradle
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Provider
import org.gradle.api.reporting.ReportingExtension
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.configurationcache.extensions.capitalized
import org.gradle.kotlin.dsl.get
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.testing.jacoco.plugins.JacocoCoverageReport
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
import java.io.BufferedWriter
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.regex.Pattern
abstract class CCTweakedExtension(
private val project: Project,
private val fs: FileSystemOperations,
) {
/** Get the hash of the latest git commit. */
val gitHash: Provider<String> = gitProvider(project) {
ProcessHelpers.captureOut("git", "-C", project.projectDir.absolutePath, "rev-parse", "HEAD")
}
/** Get the current git branch. */
val gitBranch: Provider<String> = gitProvider(project) {
ProcessHelpers.captureOut("git", "-C", project.projectDir.absolutePath, "rev-parse", "--abbrev-ref", "HEAD")
}
/** Get a list of all contributors to the project. */
val gitContributors: Provider<List<String>> = gitProvider(project) {
val authors: Set<String> = HashSet(
ProcessHelpers.captureLines(
"git", "-C", project.projectDir.absolutePath, "log",
"--format=tformat:%an <%ae>%n%cn <%ce>%n%(trailers:key=Co-authored-by,valueonly)",
),
)
val process = ProcessHelpers.startProcess("git", "check-mailmap", "--stdin")
BufferedWriter(OutputStreamWriter(process.outputStream)).use { writer ->
for (authorName in authors) {
var author = authorName
if (author.isEmpty()) continue
if (!author.endsWith(">")) author += ">" // Some commits have broken Co-Authored-By lines!
writer.write(author)
writer.newLine()
}
}
val contributors: MutableSet<String> = HashSet()
for (authorLine in ProcessHelpers.captureLines(process)) {
val matcher = EMAIL.matcher(authorLine)
matcher.find()
val name = matcher.group(1)
if (!IGNORED_USERS.contains(name)) contributors.add(name)
}
contributors.sortedWith(String.CASE_INSENSITIVE_ORDER)
}
fun jacoco(task: NamedDomainObjectProvider<JavaExec>) {
val classDump = project.buildDir.resolve("jacocoClassDump/${task.name}")
val reportTaskName = "jacoco${task.name.capitalized()}Report"
val jacoco = project.extensions.getByType(JacocoPluginExtension::class.java)
task.configure {
finalizedBy(reportTaskName)
doFirst("Clean class dump directory") { fs.delete { delete(classDump) } }
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
classDumpDir = classDump
// Older versions of modlauncher don't include a protection domain (and thus no code
// source). Jacoco skips such classes by default, so we need to explicitly include them.
isIncludeNoLocationClasses = true
}
}
project.tasks.register(reportTaskName, JacocoReport::class.java) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Generates code coverage report for the ${task.name} task."
executionData(task.get())
classDirectories.from(classDump)
// Don't want to use sourceSets(...) here as we have a custom class directory.
val sourceSets = project.extensions.getByType(SourceSetContainer::class.java)
sourceDirectories.from(sourceSets["main"].allSource.sourceDirectories)
}
project.extensions.configure(ReportingExtension::class.java) {
reports.register("${task.name}CodeCoverageReport", JacocoCoverageReport::class.java) {
testType.set(TestSuiteType.INTEGRATION_TEST)
}
}
}
companion object {
private val EMAIL = Pattern.compile("^([^<]+) <.+>$")
private val IGNORED_USERS = setOf(
"GitHub", "Daniel Ratcliffe", "Weblate",
)
private fun <T> gitProvider(project: Project, supplier: () -> T): Provider<T> {
return project.provider {
try {
supplier()
} catch (e: IOException) {
project.logger.error("Cannot read Git Repository", e)
null
}
}
}
}
}

View File

@ -0,0 +1,13 @@
package cc.tweaked.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Configures projects to match a shared configuration.
*/
class CCTweakedPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("cct", CCTweakedExtension::class.java)
}
}

View File

@ -0,0 +1,65 @@
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.language.base.plugins.LifecycleBasePlugin
import java.nio.charset.StandardCharsets
/**
* Checks the `changelog.md` and `whatsnew.md` files are well-formed.
*/
@CacheableTask
abstract class CheckChangelog : DefaultTask() {
init {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Verifies the changelog and whatsnew file are consistent."
}
@get:Input
abstract val version: Property<String>
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val changelog: RegularFileProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val whatsNew: RegularFileProperty
@TaskAction
fun check() {
val version = version.get()
var ok = true
// Check we're targetting the current version
var whatsNew = whatsNew.get().asFile.readLines()
if (whatsNew[0] != "New features in CC: Tweaked $version") {
ok = false
logger.error("Expected `whatsnew.md' to target $version.")
}
// Check "read more" exists and trim it
val idx = whatsNew.indexOfFirst { it == "Type \"help changelog\" to see the full version history." }
if (idx == -1) {
ok = false
logger.error("Must mention the changelog in whatsnew.md")
} else {
whatsNew = whatsNew.slice(0 until idx)
}
// Check whatsnew and changelog match.
val expectedChangelog = sequenceOf("# ${whatsNew[0]}") + whatsNew.slice(1 until whatsNew.size).asSequence()
val changelog = changelog.get().asFile.readLines()
val mismatch = expectedChangelog.zip(changelog.asSequence()).filter { (a, b) -> a != b }.firstOrNull()
if (mismatch != null) {
ok = false
logger.error("whatsnew and changelog are not in sync")
}
if (!ok) throw GradleException("Could not check release")
}
}

View File

@ -25,7 +25,7 @@ fun create(api: File, main: File): FormatterStep = FormatterStep.createLazy(
)
private fun getTemplateText(file: File): String =
file.readText(StandardCharsets.UTF_8).trim().replace("\${year}", "$YEAR")
file.readText().trim().replace("\${year}", "$YEAR")
private fun formatFile(licenses: Licenses, contents: String, file: File): String {
val license = getLicense(contents)
@ -38,8 +38,8 @@ private fun formatFile(licenses: Licenses, contents: String, file: File): String
}
}
private fun getExpectedLicense(licenses: Licenses, file: File): String {
var file: File? = file
private fun getExpectedLicense(licenses: Licenses, root: File): String {
var file: File? = root
while (file != null) {
if (file.name == "api" && file.parentFile?.name == "computercraft") return licenses.api
file = file.parentFile

View File

@ -0,0 +1,34 @@
package cc.tweaked.gradle
import org.codehaus.groovy.runtime.ProcessGroovyMethods
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
internal object ProcessHelpers {
fun startProcess(vararg command: String): Process {
// Something randomly passes in "GIT_DIR=" as an environment variable which clobbers everything else. Don't
// inherit the environment array!
return Runtime.getRuntime().exec(command, arrayOfNulls(0))
}
fun captureOut(vararg command: String): String {
val process = startProcess(*command)
val result = ProcessGroovyMethods.getText(process)
if (process.waitFor() != 0) throw IOException("Command exited with a non-0 status")
return result
}
fun captureLines(vararg command: String): List<String> {
return captureLines(startProcess(*command))
}
fun captureLines(process: Process): List<String> {
val out = BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
reader.lines().filter { it.isNotEmpty() }.toList()
}
ProcessGroovyMethods.closeStreams(process)
if (process.waitFor() != 0) throw IOException("Command exited with a non-0 status")
return out
}
}

View File

@ -10,6 +10,7 @@ jqwik = "1.7.0"
junit = "5.9.1"
# Build tools
checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I can't remember what it is.
spotless = "6.8.0"
[libraries]
@ -26,7 +27,8 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
# Gradle plugins
# Build tools
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
[bundles]

View File

@ -12,4 +12,4 @@ Follow @DanTwoHundred on Twitter!
To help contribute to ComputerCraft, browse the source code at https://github.com/dan200/ComputerCraft.
GitHub Contributors:
${gitcontributors}
${gitContributors}