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 a4e8a65..29d0d52 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/pluginsapi/PluginDownloadService.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadService.kt index 83ecf62..acf268c 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadService.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadService.kt @@ -23,6 +23,7 @@ import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters +import org.gradle.internal.logging.progress.ProgressLoggerFactory import org.gradle.kotlin.dsl.registerIfAbsent import xyz.jpenilla.runtask.paperapi.Projects import xyz.jpenilla.runtask.util.Constants @@ -53,7 +54,7 @@ public interface PluginDownloadService : BuildService): Provider { diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt index 1f1ecfc..9305ca4 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt @@ -21,8 +21,8 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue -import org.gradle.api.Project import org.gradle.api.logging.Logging +import org.gradle.internal.logging.progress.ProgressLoggerFactory import xyz.jpenilla.runtask.util.Constants import xyz.jpenilla.runtask.util.Downloader import xyz.jpenilla.runtask.util.HashingAlgorithm @@ -85,14 +85,14 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { } @Synchronized - override fun resolvePlugin(project: Project, download: PluginApiDownload): Path { + override fun resolvePlugin(progressLoggerFactory: ProgressLoggerFactory, download: PluginApiDownload): Path { manifest = loadOrCreateManifest() return when (download) { - is HangarApiDownload -> resolveHangarPlugin(project, download) - is ModrinthApiDownload -> resolveModrinthPlugin(project, download) - is GitHubApiDownload -> resolveGitHubPlugin(project, download) - is UrlDownload -> resolveUrl(project, download) + is HangarApiDownload -> resolveHangarPlugin(progressLoggerFactory, download) + is ModrinthApiDownload -> resolveModrinthPlugin(progressLoggerFactory, download) + is GitHubApiDownload -> resolveGitHubPlugin(progressLoggerFactory, download) + is UrlDownload -> resolveUrl(progressLoggerFactory, download) } } @@ -101,18 +101,18 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { private val offlineMode: Boolean get() = parameters.offlineMode.get() - private fun resolveUrl(project: Project, download: UrlDownload): Path { + private fun resolveUrl(progressLoggerFactory: ProgressLoggerFactory, download: UrlDownload): Path { val cacheDir = parameters.cacheDirectory.get().asFile.toPath() val targetDir = cacheDir.resolve(Constants.URL_PLUGIN_DIR) val urlHash = download.urlHash() val version = manifest.urlProvider[urlHash] ?: PluginVersion(fileName = "$urlHash.jar", displayName = download.url.get()) val targetFile = targetDir.resolve(version.fileName) val setter: (PluginVersion) -> Unit = { manifest.urlProvider[urlHash] = it } - val ctx = DownloadCtx(project, "url", download.url.get(), targetDir, targetFile, version, setter) + val ctx = DownloadCtx(progressLoggerFactory, "url", download.url.get(), targetDir, targetFile, version, setter) return download(ctx) } - private fun resolveHangarPlugin(project: Project, download: HangarApiDownload): Path { + private fun resolveHangarPlugin(progressLoggerFactory: ProgressLoggerFactory, download: HangarApiDownload): Path { val platformType = parameters.platformType.get() val cacheDir = parameters.cacheDirectory.get().asFile.toPath() @@ -134,11 +134,11 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val downloadUrl = "$apiUrl/api/v1/projects/$apiPlugin/versions/$apiVersion/$platformType/download" val setter: (PluginVersion) -> Unit = { platform[apiVersion] = it } - val ctx = DownloadCtx(project, apiUrl, downloadUrl, targetDir, targetFile, version, setter) + val ctx = DownloadCtx(progressLoggerFactory, apiUrl, downloadUrl, targetDir, targetFile, version, setter) return download(ctx) } - private fun resolveModrinthPlugin(project: Project, download: ModrinthApiDownload): Path { + private fun resolveModrinthPlugin(progressLoggerFactory: ProgressLoggerFactory, download: ModrinthApiDownload): Path { val cacheDir = parameters.cacheDirectory.get().asFile.toPath() val apiVersion = download.version.get() @@ -156,7 +156,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val versionRequestUrl = "$apiUrl/v2/project/$apiPlugin/version/$apiVersion" val versionJsonPath = download( - DownloadCtx(project, apiUrl, versionRequestUrl, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it }, requireValidJar = false) + DownloadCtx(progressLoggerFactory, apiUrl, versionRequestUrl, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it }, requireValidJar = false) ) val versionInfo = mapper.readValue(versionJsonPath.toFile()) val primaryFile = versionInfo.files.find { it.primary } ?: error("Could not find primary file for $download in $versionInfo") @@ -169,11 +169,11 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val targetFile = targetDir.resolve(version.fileName) return download( - DownloadCtx(project, apiUrl, primaryFile.url, targetDir, targetFile, version, setter = { plugin[apiVersion] = it }) + DownloadCtx(progressLoggerFactory, apiUrl, primaryFile.url, targetDir, targetFile, version, setter = { plugin[apiVersion] = it }) ) } - private fun resolveGitHubPlugin(project: Project, download: GitHubApiDownload): Path { + private fun resolveGitHubPlugin(progressLoggerFactory: ProgressLoggerFactory, download: GitHubApiDownload): Path { val cacheDir = parameters.cacheDirectory.get().asFile.toPath() val owner = download.owner.get() @@ -192,7 +192,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val downloadUrl = "https://github.com/$owner/$repo/releases/download/$tag/$asset" val setter: (PluginVersion) -> Unit = { tagProvider[asset] = it } - val ctx = DownloadCtx(project, "github.com", downloadUrl, targetDir, targetFile, version, setter) + val ctx = DownloadCtx(progressLoggerFactory, "github.com", downloadUrl, targetDir, targetFile, version, setter) return download(ctx) } @@ -262,7 +262,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val opName = "${ctx.baseUrl}:${ctx.version.fileName}" val start = Instant.now() LOGGER.lifecycle("Downloading {}...", displayName) - when (val res = Downloader(url, ctx.targetFile, displayName, opName).download(ctx.project, connection)) { + when (val res = Downloader(url, ctx.targetFile, displayName, opName).download(ctx.progressLoggerFactory, connection)) { is Downloader.Result.Success -> LOGGER.lifecycle("Done downloading {}, took {}.", displayName, Duration.between(start, Instant.now()).prettyPrint()) is Downloader.Result.Failure -> throw IllegalStateException("Failed to download $displayName.", res.throwable) } @@ -311,7 +311,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { } private data class DownloadCtx( - val project: Project, + val progressLoggerFactory: ProgressLoggerFactory, val baseUrl: String, val downloadUrl: String, val targetDir: Path, 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 9f2d33c..2531879 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,11 @@ 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.internal.logging.progress.ProgressLoggerFactory +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,10 +47,13 @@ public interface DownloadsAPIService { * @param build build to resolve */ public fun resolveBuild( - project: Project, + providers: ProviderFactory, + javaLauncher: JavaLauncher, + execOperations: ExecOperations, + progressLoggerFactory: ProgressLoggerFactory, 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..ae6d07c 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/service/DownloadsAPIServiceImpl.kt @@ -21,23 +21,30 @@ import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue import org.gradle.api.InvalidUserDataException -import org.gradle.api.Project 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.internal.logging.progress.ProgressLoggerFactory +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.deleteEmptyParents +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 +103,16 @@ 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 +164,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() @@ -166,7 +218,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService LOGGER.lifecycle("Done downloading {}, took {}.", displayName, Duration.ofMillis(System.currentTimeMillis() - start).prettyPrint()) is Downloader.Result.Failure -> throw IllegalStateException("Failed to download $displayName.", downloadResult.throwable) } @@ -181,27 +233,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 +328,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService?, val keep: Boolean = false ) @@ -302,6 +368,6 @@ internal abstract class DownloadsAPIServiceImpl : BuildService = HashMap(), + val knownJars: MutableMap = HashMap(), ) } 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..4adf8c7 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,14 @@ 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.internal.logging.progress.ProgressLoggerFactory +import org.gradle.process.ExecOperations import xyz.jpenilla.runtask.service.DownloadsAPIService import xyz.jpenilla.runtask.util.path import java.io.File @@ -88,6 +91,15 @@ 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 + + @get:Inject + protected abstract val progressLoggerFactory: ProgressLoggerFactory + init { init0() } @@ -113,6 +125,15 @@ public abstract class AbstractRun : JavaExec() { super.exec() } + protected open fun resolveBuild(): List = downloadsApiService.get().resolveBuild( + providers, + javaLauncher.get(), + execOperations, + progressLoggerFactory, + version.get(), + build.get() + ) + private fun preExec() { standardInput = System.`in` workingDir(runDirectory) @@ -123,11 +144,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/task/RunWithPlugins.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/RunWithPlugins.kt index 7ee263c..f83c609 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/RunWithPlugins.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/task/RunWithPlugins.kt @@ -87,7 +87,7 @@ public abstract class RunWithPlugins : AbstractRun() { val service = pluginDownloadService.get() for (download in downloadPlugins.downloads) { - ourPluginJars.from(service.resolvePlugin(project, download)) + ourPluginJars.from(service.resolvePlugin(progressLoggerFactory, download)) } } diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Downloader.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Downloader.kt index 0853135..edb85dd 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Downloader.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Downloader.kt @@ -16,9 +16,9 @@ */ package xyz.jpenilla.runtask.util -import org.gradle.api.Project import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging +import org.gradle.internal.logging.progress.ProgressLoggerFactory import java.io.IOException import java.net.URL import java.net.URLConnection @@ -53,18 +53,18 @@ internal class Downloader( @Volatile private var expectedSize = 0L - fun download(project: Project): Result { + fun download(progressLoggerFactory: ProgressLoggerFactory): Result { if (started) { error("Cannot start download a second time.") } started = true val connection = remote.openConnection() - return download(project, connection) + return download(progressLoggerFactory, connection) } - fun download(project: Project, connection: URLConnection): Result { - val listener = createDownloadListener(project) + fun download(progressLoggerFactory: ProgressLoggerFactory, connection: URLConnection): Result { + val listener = createDownloadListener(progressLoggerFactory) val downloadFuture = CompletableFuture.runAsync { val expected = connection.contentLengthLong @@ -108,11 +108,8 @@ internal class Downloader( return Result.Failure(failure) } - private fun createDownloadListener(project: Project): ProgressListener { - // ProgressLogger is internal Gradle API and can technically be changed, - // (although it hasn't since 3.x) so we access it using reflection, and - // fallback to using LOGGER if it fails - val progressLogger = ProgressLoggerUtil.createProgressLogger(project, operationName) + private fun createDownloadListener(progressLoggerFactory: ProgressLoggerFactory): ProgressListener { + val progressLogger = progressLoggerFactory.newOperation(operationName) return if (progressLogger != null) { LoggingDownloadListener( progressLogger, diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/ProgressLoggerUtil.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/ProgressLoggerUtil.kt deleted file mode 100644 index dacc0bc..0000000 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/ProgressLoggerUtil.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Run Task Gradle Plugins - * Copyright (c) 2024 Jason Penilla - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package xyz.jpenilla.runtask.util - -import org.gradle.api.Project -import java.lang.reflect.Method - -internal object ProgressLoggerUtil { - fun createProgressLogger(project: Project, operationName: String): ProgressLoggerWrapper? = try { - val serviceFactory = project.javaClass.getMethod("getServices").invoke(project) - val get = serviceFactory.javaClass.getMethod("get", Class::class.java) - val progressLoggerFactoryClass = Class.forName("org.gradle.internal.logging.progress.ProgressLoggerFactory") - val factory = get(serviceFactory, progressLoggerFactoryClass) - val newOperation = progressLoggerFactoryClass.getMethod("newOperation", String::class.java) - val progressLoggerClass = Class.forName("org.gradle.internal.logging.progress.ProgressLogger") - val start = progressLoggerClass.getMethod("start", String::class.java, String::class.java) - val progress = progressLoggerClass.getMethod("progress", String::class.java) - val completed = progressLoggerClass.getMethod("completed") - ProgressLoggerWrapper(newOperation(factory, operationName), start, progress, completed) - } catch (ex: ReflectiveOperationException) { - null - } - - class ProgressLoggerWrapper( - private val logger: Any, - private val start: Method, - private val progress: Method, - private val completed: Method - ) { - fun start(description: String, status: String) { - start(logger, description, status) - } - - fun progress(status: String) { - progress(logger, status) - } - - fun completed() { - completed(logger) - } - } -} 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 45f49d8..61469df 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 java.util.Locale import kotlin.reflect.KClass @@ -63,3 +65,8 @@ internal fun ExtensiblePolymorphicDomainObjectContainer.regi internal fun String.capitalized(locale: Locale = Locale.ROOT): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } + +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..1f29f5b 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,18 @@ 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() +} + +internal fun Path.deleteEmptyParents() { + if (Files.isDirectory(parent) && Files.list(parent).use { s -> s.toList().isEmpty() }) { + Files.deleteIfExists(parent) + parent.deleteEmptyParents() + } +} 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..4635606 --- /dev/null +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/paperclip.kt @@ -0,0 +1,112 @@ +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) + it.deleteEmptyParents() + } + } + } + + 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..67f69ed 100644 --- a/tester/build.gradle.kts +++ b/tester/build.gradle.kts @@ -1,33 +1,55 @@ 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", "DQoDwRaq") + 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(11) } + ignoreUnsupportedJvm() + } + register("run1_12") { + version = "1.12.2" + runDirectory = layout.projectDirectory.dir("run1_12") + ignoreUnsupportedJvm() + } 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.28") + url("https://download.luckperms.net/1569/velocity/LuckPerms-Velocity-5.4.152.jar") } } }