diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runpaper/task/RunServer.kt b/plugin/src/main/kotlin/xyz/jpenilla/runpaper/task/RunServer.kt index 425ed0a..cb29e76 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runpaper/task/RunServer.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runpaper/task/RunServer.kt @@ -26,6 +26,7 @@ import xyz.jpenilla.runtask.pluginsapi.PluginDownloadService import xyz.jpenilla.runtask.service.DownloadsAPIService import xyz.jpenilla.runtask.task.RunWithPlugins import xyz.jpenilla.runtask.util.FileCopyingPluginHandler +import xyz.jpenilla.runtask.util.spec import java.io.File import java.nio.file.Path @@ -63,6 +64,15 @@ public abstract class RunServer : RunWithPlugins() { displayName.convention("Paper") } + override fun resolveBuild(): List { + val result = super.resolveBuild() + if (result.size != 1) { + // Default main class to CB main when the applied Paperclip classpath is resolved to multiple files + spec().mainClass.set(mainClass.orElse("org.bukkit.craftbukkit.Main")) + } + return result + } + override fun preExec(workingDir: Path) { super.preExec(workingDir) diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIService.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIService.kt index 2e2adb8..f638fc8 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIService.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIService.kt @@ -19,7 +19,10 @@ package xyz.jpenilla.runtask.service import org.gradle.api.Action import org.gradle.api.Project import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.kotlin.dsl.registerIfAbsent +import org.gradle.process.ExecOperations import xyz.jpenilla.runtask.paperapi.DownloadsAPI import xyz.jpenilla.runtask.paperapi.Projects import xyz.jpenilla.runtask.util.Constants @@ -43,9 +46,12 @@ public interface DownloadsAPIService { */ public fun resolveBuild( project: Project, + providers: ProviderFactory, + javaLauncher: JavaLauncher, + execOperations: ExecOperations, version: String, build: Build - ): Path + ): List public companion object { /** diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt index d5ce5b6..be904ad 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt @@ -26,18 +26,24 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters +import org.gradle.jvm.toolchain.JavaLauncher +import org.gradle.process.ExecOperations import xyz.jpenilla.runtask.paperapi.DownloadsAPI import xyz.jpenilla.runtask.util.Constants import xyz.jpenilla.runtask.util.Downloader import xyz.jpenilla.runtask.util.InvalidDurationException +import xyz.jpenilla.runtask.util.maybeApplyPaperclip import xyz.jpenilla.runtask.util.parseDuration import xyz.jpenilla.runtask.util.path import xyz.jpenilla.runtask.util.prettyPrint import xyz.jpenilla.runtask.util.sha256 +import xyz.jpenilla.runtask.util.walkMatching import java.io.IOException import java.net.URL +import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption import java.time.Duration @@ -96,11 +102,15 @@ internal abstract class DownloadsAPIServiceImpl : BuildService { versions = loadOrCreateVersions() val versionData = versions.versions.computeIfAbsent(version) { Version(it) } - val buildNumber = resolveBuildNumber(project, versionData, build) + val buildNumber = resolveBuildNumber(providers, versionData, build) + val jarsDir = jarsFor(version) val possible = versionData.knownJars[buildNumber] if (possible != null && !parameters.refreshDependencies.get()) { // We already have this jar! LOGGER.lifecycle("Located {} {} build {} in local cache.", displayName, version, buildNumber) + if (possible.classpath != null && possible.classpath.isNotEmpty()) { + return possible.classpath.flatMap { jarsDir.walkMatching(it) } + } + + requireNotNull(possible.fileName) + requireNotNull(possible.sha256) + // Verify hash is still correct - val localJar = jarsFor(version).resolve(possible.fileName) + val localJar = jarsDir.resolve(possible.fileName) val localBuildHash = localJar.sha256() if (localBuildHash == possible.sha256) { if (build is DownloadsAPIService.Build.Specific) { @@ -141,7 +162,36 @@ internal abstract class DownloadsAPIServiceImpl : BuildService + stream.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } + versionData.knownJars[buildNumber] = possible.copy(classpath = emptyList()) + writeVersions() + return listOf(localJar) + } + } else if (possible.classpath.isEmpty()) { + return listOf(localJar) + } } versionData.knownJars.remove(buildNumber) writeVersions() @@ -181,27 +231,40 @@ internal abstract class DownloadsAPIServiceImpl : BuildService Actual: {}", actual) } - private fun updateCheckFrequency(project: Project): Duration { - var prop = project.findProperty(Constants.Properties.UPDATE_CHECK_FREQUENCY) + private fun updateCheckFrequency(providers: ProviderFactory): Duration { + var prop = providers.gradleProperty(Constants.Properties.UPDATE_CHECK_FREQUENCY).orNull if (prop == null) { - prop = project.findProperty(Constants.Properties.UPDATE_CHECK_FREQUENCY_LEGACY) + prop = providers.gradleProperty(Constants.Properties.UPDATE_CHECK_FREQUENCY_LEGACY).orNull if (prop != null) { LOGGER.warn( "Use of legacy '{}' property detected. Please replace with '{}'.", @@ -263,7 +326,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService?, val keep: Boolean = false ) diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/AbstractRun.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/AbstractRun.kt index 4d3a6b6..039ed0c 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/AbstractRun.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/AbstractRun.kt @@ -22,11 +22,13 @@ import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFile import org.gradle.api.provider.Property import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.Optional +import org.gradle.process.ExecOperations import xyz.jpenilla.runtask.service.DownloadsAPIService import xyz.jpenilla.runtask.util.path import java.io.File @@ -88,6 +90,12 @@ public abstract class AbstractRun : JavaExec() { @get:Inject protected abstract val layout: ProjectLayout + @get:Inject + protected abstract val execOperations: ExecOperations + + @get:Inject + protected abstract val providers: ProviderFactory + init { init0() } @@ -113,6 +121,15 @@ public abstract class AbstractRun : JavaExec() { super.exec() } + protected open fun resolveBuild(): List = downloadsApiService.get().resolveBuild( + project, + providers, + javaLauncher.get(), + execOperations, + version.get(), + build.get() + ) + private fun preExec() { standardInput = System.`in` workingDir(runDirectory) @@ -123,11 +140,7 @@ public abstract class AbstractRun : JavaExec() { if (!version.isPresent) { error("'runClasspath' is empty and no version was specified for the '$name' task. Don't know what version to download.") } - downloadsApiService.get().resolveBuild( - project, - version.get(), - build.get() - ) + resolveBuild() } classpath(selectedClasspath) diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/extensions.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/extensions.kt index 2769aa0..26c0d11 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/extensions.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/extensions.kt @@ -23,6 +23,7 @@ import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.provider.Provider +import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.toolchain.JavaLauncher @@ -30,6 +31,7 @@ import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.register +import org.gradle.process.JavaExecSpec import kotlin.reflect.KClass internal fun Project.findJavaLauncher(): Provider? { @@ -59,3 +61,8 @@ internal fun PolymorphicDomainObjectContainer.configure( } internal fun ExtensiblePolymorphicDomainObjectContainer.registerFactory(type: KClass, config: (String) -> U): Unit = registerFactory(type.java, config) + +internal fun JavaExec.spec(): JavaExecSpec { + val spec: JavaExecSpec = JavaExec::class.java.getDeclaredField("javaExecSpec").also { it.isAccessible = true }.get(this) as JavaExecSpec + return spec +} diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/files.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/files.kt index f82ab9b..132a3a3 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/files.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/files.kt @@ -20,7 +20,11 @@ import org.gradle.api.Project import org.gradle.api.file.FileSystemLocation import org.gradle.api.file.FileSystemLocationProperty import org.gradle.api.provider.Provider +import java.nio.file.Files import java.nio.file.Path +import kotlin.io.path.relativeTo +import kotlin.streams.asSequence +import kotlin.use internal val FileSystemLocationProperty<*>.path: Path get() = get().path @@ -36,3 +40,11 @@ internal val FileSystemLocation.path: Path internal val Project.sharedCaches: Path get() = gradle.gradleUserHomeDir.toPath().resolve(Constants.GRADLE_CACHES_DIRECTORY_NAME) + +internal fun Path.walkMatching(glob: String): List = walkMatching { + it.fileSystem.getPathMatcher("glob:$glob").matches(it) +} + +internal fun Path.walkMatching(predicate: (Path) -> Boolean): List = Files.walk(this).use { stream -> + stream.asSequence().filter { p -> predicate(p.relativeTo(this)) }.toList() +} diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/paperclip.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/paperclip.kt new file mode 100644 index 0000000..e809aac --- /dev/null +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/paperclip.kt @@ -0,0 +1,111 @@ +package xyz.jpenilla.runtask.util + +import org.gradle.jvm.toolchain.JavaLauncher +import org.gradle.process.ExecOperations +import org.gradle.process.ExecResult +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.absolute +import kotlin.io.path.absolutePathString +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.isRegularFile +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.relativeTo +import kotlin.streams.asSequence + +internal fun maybeApplyPaperclip( + javaLauncher: JavaLauncher, + exec: ExecOperations, + file: Path, + workingDir: Path, + resultRelativeTo: Path, +): List? { + val type = isPaperclip(file) + if (type == PaperclipType.NONE) { + return null + } + + if (workingDir.exists()) { + Files.walk(workingDir).use { stream -> + stream.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } + } + workingDir.createDirectories() + applyPaperclip(javaLauncher, exec, file, workingDir) + + val classpath = mutableListOf() + val classpathPaths = mutableListOf() + + if (type == PaperclipType.MODERN) { + val patchedJar = Files.walk(workingDir.resolve("versions")).use { stream -> + stream.asSequence().filter { it.isRegularFile() && it.extension == "jar" }.single() + } + classpath += patchedJar.relativeTo(resultRelativeTo).toString() + classpathPaths.add(patchedJar.normalize().absolute()) + + classpath.add(workingDir.resolve("libraries").relativeTo(resultRelativeTo).toString() + "/**/*.jar") + val libs = workingDir.walkMatching("libraries/**/*.jar") + classpathPaths.addAll(libs) + } else if (type == PaperclipType.LEGACY) { + val patchedJar = workingDir.resolve("cache") + .listDirectoryEntries() + .single { it.name.startsWith("patched") && it.name.endsWith(".jar") } + classpath += patchedJar.relativeTo(resultRelativeTo).toString() + classpathPaths.add(patchedJar.normalize().absolute()) + } + + // Clean up leftover files in the working dir (i.e. vanilla jar) + Files.walk(workingDir).use { stream -> + stream.forEach { + if (it.isRegularFile() && it.normalize().absolute() !in classpathPaths) { + Files.delete(it) + } + } + } + + return classpath +} + +private enum class PaperclipType { + NONE, + LEGACY, + MODERN +} + +private fun isPaperclip(file: Path): PaperclipType { + if (!file.isRegularFile()) { + return PaperclipType.NONE + } + try { + JarFile(file.toFile()).use { + val main = it.manifest.mainAttributes.getValue("Main-Class") + if (main == "com.destroystokyo.paperclip.Main") { + return PaperclipType.LEGACY + } else if (main == "io.papermc.paperclip.Paperclip") { + return PaperclipType.LEGACY + } else if (main == "io.papermc.paperclip.Main") { + return PaperclipType.MODERN + } + } + } catch (_: IOException) { + return PaperclipType.NONE + } + return PaperclipType.NONE +} + +private fun applyPaperclip( + javaLauncher: JavaLauncher, + exec: ExecOperations, + paperclip: Path, + workingDir: Path, +): ExecResult = exec.javaexec { + executable = javaLauncher.executablePath.path.absolutePathString() + classpath(paperclip) + jvmArgs("-Dpaperclip.patchonly=true") + workingDir(workingDir) +}.assertNormalExitValue() diff --git a/tester/build.gradle.kts b/tester/build.gradle.kts index 70e66e6..8dad4e8 100644 --- a/tester/build.gradle.kts +++ b/tester/build.gradle.kts @@ -1,33 +1,54 @@ import xyz.jpenilla.runpaper.task.RunServer plugins { + java id("xyz.jpenilla.run-paper") id("xyz.jpenilla.run-velocity") id("xyz.jpenilla.run-waterfall") } +java.toolchain { + languageVersion = JavaLanguageVersion.of(21) +} + runPaper.folia.registerTask() val paperPlugins = runPaper.downloadPluginsSpec { - modrinth("carbon", "6dmNHzy8") - github("jpenilla", "MiniMOTD", "v2.1.0", "minimotd-bukkit-2.1.0.jar") - hangar("squaremap", "1.2.3") - url("https://download.luckperms.net/1530/bukkit/loader/LuckPerms-Bukkit-5.4.117.jar") + modrinth("carbon", "WPejrRaD") + github("jpenilla", "MiniMOTD", "v2.1.5", "minimotd-bukkit-2.1.5.jar") + hangar("squaremap", "1.3.4") + url("https://download.luckperms.net/1569/bukkit/loader/LuckPerms-Bukkit-5.4.152.jar") } +val toolchains = javaToolchains tasks { + register("run1_8") { + version = "1.8.8" + runDirectory = layout.projectDirectory.dir("run1_8") + javaLauncher = toolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) } + } + register("run1_12") { + version = "1.12.2" + runDirectory = layout.projectDirectory.dir("run1_12") + javaLauncher = toolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) } + } withType { - minecraftVersion("1.20.4") - runDirectory.set(layout.projectDirectory.dir("runServer")) + version.convention("1.21.4") + runDirectory.convention(layout.projectDirectory.dir("runServer")) + } + runServer { + downloadPlugins.from(paperPlugins) + } + runPaper.folia.task { downloadPlugins.from(paperPlugins) } runVelocity { - version("3.3.0-SNAPSHOT") - runDirectory.set(layout.projectDirectory.dir("runVelocity")) + version = "3.4.0-SNAPSHOT" + runDirectory = layout.projectDirectory.dir("runVelocity") downloadPlugins { - modrinth("minimotd", "z8DFFJMR") - hangar("Carbon", "3.0.0-beta.26") - url("https://download.luckperms.net/1530/velocity/LuckPerms-Velocity-5.4.117.jar") + modrinth("minimotd", "nFRYRCht") + hangar("Carbon", "3.0.0-beta.27") + url("https://download.luckperms.net/1569/velocity/LuckPerms-Velocity-5.4.152.jar") } } }