import io.papermc.paperweight.util.* import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import java.io.IOException import java.net.URI import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.SimpleFileVisitor import kotlin.io.path.* import java.nio.file.Path import kotlin.random.Random plugins { id("io.papermc.paperweight.core") version "2.0.0-beta.4" apply false } subprojects { apply(plugin = "java-library") apply(plugin = "maven-publish") extensions.configure { toolchain { languageVersion = JavaLanguageVersion.of(21) } } dependencies { "testRuntimeOnly"("org.junit.platform:junit-platform-launcher") } tasks.withType().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true } } val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/" subprojects { tasks.withType { options.encoding = Charsets.UTF_8.name() options.release = 21 options.isFork = true } tasks.withType { options.encoding = Charsets.UTF_8.name() } tasks.withType { filteringCharset = Charsets.UTF_8.name() } tasks.withType { testLogging { showStackTraces = true exceptionFormat = TestExceptionFormat.FULL events(TestLogEvent.STANDARD_OUT) } } repositories { mavenCentral() maven(paperMavenPublicUrl) } extensions.configure { repositories { maven("https://repo.papermc.io/repository/maven-snapshots/") { name = "paperSnapshots" credentials(PasswordCredentials::class) } } } } tasks.register("printMinecraftVersion") { val mcVersion = providers.gradleProperty("mcVersion") doLast { println(mcVersion.get().trim()) } } tasks.register("printPaperVersion") { val paperVersion = provider { project.version } doLast { println(paperVersion.get()) } } tasks.register("gibWork") { val issue = providers.gradleProperty("updateTaskListIssue").get() val patchesFolder = layout.projectDirectory.dir("paper-server/patches/").convertToPath() val storage = layout.cache.resolve("last-updating-folder").also { it.parent.createDirectories() } doLast { val html = URI(issue).toURL().readText() val beginMarker = "```[tasklist]" val start = html.indexOf(beginMarker) val end = html.indexOf("```", start + beginMarker.length) val taskList = html.substring(start + beginMarker.length, end) // Extract all incomplete tasks and select a random one val incompleteTasks = taskList.split("\\n").filter { it.startsWith("- [ ]") }.map { it.replace("- [ ] ", "") } if (incompleteTasks.isEmpty()) { error("No incomplete tasks found in the task list.") } val next = incompleteTasks[Random.nextInt(incompleteTasks.size)] println("checking out $next...") val dir = patchesFolder.resolve("unapplied").resolve(next) if (!dir.exists()) { error("Unapplied patch folder $next does not exist, did someone else already check it out and forgot to mark it?") } dir.listDirectoryEntries("*.patch").forEach { patch -> patch.copyTo(patchesFolder.resolve("sources").resolve(next).resolve(patch.fileName).also { it.createDirectories() }, overwrite = true) patch.deleteIfExists() } if (dir.listDirectoryEntries().isEmpty()) { dir.deleteIfExists() } storage.writeText(next) println("please tick the box in the issue: $issue") println("if you don't finish it, uncheck the task again after you commited") } } tasks.register("showWork") { val patchDir = layout.projectDirectory.dir("paper-server/patches/unapplied/").convertToPath() doLast { Files.walkFileTree(patchDir, object : SimpleFileVisitor() { override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult { dir?.takeIf { it.listDirectoryEntries("*.patch").isNotEmpty() }?.let { println("- [ ] ${patchDir.relativize(it).pathString.replace("\\", "/")}") } return FileVisitResult.CONTINUE } }) } } tasks.register("checkWork") { notCompatibleWithConfigurationCache("This task is interactive") fun expandUserHome(path: String): Path { return Path.of(path.replaceFirst("^~".toRegex(), System.getProperty("user.home"))) } val input = layout.cache.resolve("last-updating-folder").readText().trim() val patchFolder = layout.projectDirectory.file("paper-server/patches/sources").convertToPath().resolve(input) val sourceFolder = layout.projectDirectory.file("paper-server/src/vanilla/java/").convertToPath().resolve(input) val targetFolder = expandUserHome( providers.gradleProperty("cleanPaperRepo").orNull ?: error("cleanPaperRepo is required, define it in gradle.properties") ).resolve(input) fun copy(back: Boolean = false) { patchFolder.listDirectoryEntries().forEach { val relative = patchFolder.relativize(it).toString().replace(".patch", "") val source = sourceFolder.resolve(relative) val target = targetFolder.resolve(relative) if (target.isDirectory()) { return@forEach } if (back) { target.copyTo(source, overwrite = true) } else { source.copyTo(target, overwrite = true) } } } doLast { copy() val files = patchFolder.listDirectoryEntries().map { it.fileName.toString().replace(".patch", "") } println("Copied $files from $sourceFolder to $targetFolder") println("Make the files compile, then press enter to copy them back!") System.`in`.read() copy(back = true) println("copied back!") } } // see gradle.properties /* if (providers.gradleProperty("updatingMinecraft").getOrElse("false").toBoolean()) { tasks.collectAtsFromPatches { val dir = layout.projectDirectory.dir("patches/unapplied/server") if (dir.path.isDirectory()) { extraPatchDir = dir } } tasks.withType().configureEach { filterPatches = false } tasks.register("continueServerUpdate", RebasePatches::class) { description = "Moves the next X patches from unapplied to applied, and applies them. X being the number of patches that apply cleanly, plus the terminal failure if any." projectDir = project.projectDir appliedPatches = file("patches/server") unappliedPatches = file("patches/unapplied/server") applyTaskName = "applyServerPatches" patchedDir = "Paper-Server" } } @UntrackedTask(because = "Does not make sense to track state") abstract class RebasePatches : BaseTask() { @get:Internal abstract val projectDir: DirectoryProperty @get:InputFiles abstract val appliedPatches: DirectoryProperty @get:InputFiles abstract val unappliedPatches: DirectoryProperty @get:Input abstract val applyTaskName: Property @get:Input abstract val patchedDir: Property private fun unapplied(): List = unappliedPatches.path.listDirectoryEntries("*.patch").sortedBy { it.name } private fun appliedLoc(patch: Path): Path = appliedPatches.path.resolve(unappliedPatches.path.relativize(patch)) companion object { val regex = Pattern.compile("Patch failed at ([0-9]{4}) (.*)") val continuationRegex = Pattern.compile("^\\s{1}.+\$") const val subjectPrefix = "Subject: [PATCH] " } @TaskAction fun run() { val patchedDirPath = projectDir.path.resolve(patchedDir.get()) if (patchedDirPath.isDirectory()) { val status = Git(patchedDirPath)("status").getText() if (status.contains("You are in the middle of an am session.")) { throw PaperweightException("Cannot continue update when $patchedDirPath is in the middle of an am session.") } } val unapplied = unapplied() for (patch in unapplied) { patch.copyTo(appliedLoc(patch)) } val out = ByteArrayOutputStream() val proc = ProcessBuilder() .directory(projectDir.path) .command("./gradlew", applyTaskName.get()) .redirectErrorStream(true) .start() val f = redirect(proc.inputStream, out) val exit = proc.waitFor() f.get() if (exit != 0) { val outStr = String(out.toByteArray()) val matcher = regex.matcher(outStr) if (!matcher.find()) error("Could not determine failure point") val failedSubjectFragment = matcher.group(2) val failed = unapplied.single { p -> p.useLines { lines -> val collect = mutableListOf() for (line in lines) { if (line.startsWith(subjectPrefix)) { collect += line } else if (collect.size == 1) { if (continuationRegex.matcher(line).matches()) { collect += line } else { break } } } val subjectLine = collect.joinToString("").substringAfter(subjectPrefix) subjectLine.startsWith(failedSubjectFragment) } } // delete successful & failure point from unapplied patches dir for (path in unapplied) { path.deleteIfExists() if (path == failed) { break } } // delete failed from patches dir var started = false for (path in unapplied) { if (path == failed) { started = true continue } if (started) { appliedLoc(path).deleteIfExists() } } // Delete the build file before resetting the AM session in case it has compilation errors patchedDirPath.resolve("build.gradle.kts").deleteIfExists() // Apply again to reset the am session (so it ends on the failed patch, to allow us to rebuild after fixing it) val apply2 = ProcessBuilder() .directory(projectDir.path) .command("./gradlew", applyTaskName.get()) .redirectErrorStream(true) .start() val f1 = redirect(apply2.inputStream, System.out) apply2.waitFor() f1.get() logger.lifecycle(outStr) logger.lifecycle("Patch failed at $failed; See Git output above.") } else { unapplied.forEach { it.deleteIfExists() } logger.lifecycle("All patches applied!") } val git = Git(projectDir.path) git("add", appliedPatches.path.toString() + "/*").runSilently() git("add", unappliedPatches.path.toString() + "/*").runSilently() } } */