1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-31 21:52:59 +00:00

Merge branch 'mc-1.16.x' into mc-1.18.x

I dare say there's going to be some bugs with this. I was as careful as
I could be, but yikes it was nasty.
This commit is contained in:
Jonathan Coates
2022-10-25 22:36:21 +01:00
465 changed files with 9259 additions and 7920 deletions

View File

@@ -0,0 +1,105 @@
import cc.tweaked.gradle.LicenseHeader
import com.diffplug.gradle.spotless.FormatExtension
import com.diffplug.spotless.LineEnding
import java.nio.charset.StandardCharsets
plugins {
`java-library`
jacoco
checkstyle
id("com.diffplug.spotless")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
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
fun FormatExtension.defaults() {
endWithNewline()
trimTrailingWhitespace()
indentWithSpaces(4)
}
val licenser = LicenseHeader.create(
api = file("config/license/api.txt"),
main = file("config/license/main.txt"),
)
java {
defaults()
addStep(licenser)
removeUnusedImports()
}
val ktlintConfig = mapOf(
"disabled_rules" to "no-wildcard-imports",
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
)
kotlinGradle {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
}
kotlin {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
}
}

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, "<no git hash>") {
ProcessHelpers.captureOut("git", "-C", project.projectDir.absolutePath, "rev-parse", "HEAD")
}
/** Get the current git branch. */
val gitBranch: Provider<String> = gitProvider(project, "<no git branch>") {
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, listOf()) {
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, default: T, supplier: () -> T): Provider<T> {
return project.provider {
try {
supplier()
} catch (e: IOException) {
project.logger.error("Cannot read Git repository: ${e.message}")
default
}
}
}
}
}

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

@@ -0,0 +1,73 @@
package cc.tweaked.gradle
import com.diffplug.spotless.FormatterFunc
import com.diffplug.spotless.FormatterStep
import com.diffplug.spotless.generic.LicenseHeaderStep
import java.io.File
import java.io.Serializable
import java.nio.charset.StandardCharsets
/**
* Similar to [LicenseHeaderStep], but supports multiple licenses.
*/
object LicenseHeader {
/**
* The current year to use in templates. Intentionally not dynamic to avoid failing the build.
*/
private const val YEAR = 2022
private val COMMENT = Regex("""^/\*(.*?)\*/\n?""", RegexOption.DOT_MATCHES_ALL)
fun create(api: File, main: File): FormatterStep = FormatterStep.createLazy(
"license",
{ Licenses(getTemplateText(api), getTemplateText(main)) },
{ state -> FormatterFunc.NeedsFile { contents, file -> formatFile(state, contents, file) } },
)
private fun getTemplateText(file: File): String =
file.readText().trim().replace("\${year}", "$YEAR")
private fun formatFile(licenses: Licenses, contents: String, file: File): String {
val license = getLicense(contents)
val expectedLicense = getExpectedLicense(licenses, file.parentFile)
return when {
license == null -> setLicense(expectedLicense, contents)
license.second != expectedLicense -> setLicense(expectedLicense, contents, license.first)
else -> contents
}
}
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
}
return licenses.main
}
private fun getLicense(contents: String): Pair<Int, String>? {
val match = COMMENT.find(contents) ?: return null
val license = match.groups[1]!!.value
.trim().lineSequence()
.map { it.trimStart(' ', '*') }
.joinToString("\n")
return Pair(match.range.last + 1, license)
}
private fun setLicense(license: String, contents: String, start: Int = 0): String {
val out = StringBuilder()
out.append("/*\n")
for (line in license.lineSequence()) out.append(" * ").append(line).append("\n")
out.append(" */\n")
out.append(contents, start, contents.length)
return out.toString()
}
private data class Licenses(val api: String, val main: String) : Serializable {
companion object {
private const val serialVersionUID: Long = 7741106448372435662L
}
}
}

View File

@@ -0,0 +1,20 @@
package cc.tweaked.gradle
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.tasks.JavaExec
fun DependencyHandler.annotationProcessorEverywhere(dep: Any) {
add("compileOnly", dep)
add("annotationProcessor", dep)
add("testCompileOnly", dep)
add("testAnnotationProcessor", dep)
}
fun JavaExec.copyToFull(spec: JavaExec) {
copyTo(spec)
spec.classpath = classpath
spec.mainClass.set(mainClass)
spec.javaLauncher.set(javaLauncher)
spec.args = args
}

View File

@@ -0,0 +1,60 @@
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import java.io.File
class NodePlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("node", NodeExtension::class.java)
project.tasks.register(NpmInstall.TASK_NAME, NpmInstall::class.java) {
projectRoot.convention(extension.projectRoot)
}
}
}
abstract class NodeExtension(project: Project) {
/** The directory containing `package-lock.json` and `node_modules/`. */
abstract val projectRoot: DirectoryProperty
init {
projectRoot.convention(project.layout.projectDirectory)
}
}
/** Installs node modules as dependencies. */
abstract class NpmInstall : DefaultTask() {
@get:Internal
abstract val projectRoot: DirectoryProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
val packageLock: Provider<File> = projectRoot.file("package-lock.json").map { it.asFile }
@get:OutputDirectory
val nodeModules: Provider<Directory> = projectRoot.dir("node_modules")
@TaskAction
fun install() {
project.exec {
commandLine("npm", "ci")
workingDir = projectRoot.get().asFile
}
}
companion object {
internal const val TASK_NAME: String = "npmInstall"
}
}
abstract class NpxExecToDir : ExecToDir() {
init {
dependsOn(NpmInstall.TASK_NAME)
executable = "npx"
}
}

View File

@@ -0,0 +1,35 @@
package cc.tweaked.gradle
import org.codehaus.groovy.runtime.ProcessGroovyMethods
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.stream.Collectors
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() }.collect(Collectors.toList())
}
ProcessGroovyMethods.closeStreams(process)
if (process.waitFor() != 0) throw IOException("Command exited with a non-0 status")
return out
}
}