Skip to content

Commit

Permalink
Cache applied/extracted classpath from Paperclips instead of Papercli…
Browse files Browse the repository at this point in the history
…ps themselves

This means the Paperclip does not need to be setup for each project using it, saving on overall disk space
  • Loading branch information
jpenilla committed Jan 10, 2025
1 parent 79b0bc9 commit 7107ae0
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 40 deletions.
10 changes: 10 additions & 0 deletions plugin/src/main/kotlin/xyz/jpenilla/runpaper/task/RunServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -63,6 +64,15 @@ public abstract class RunServer : RunWithPlugins() {
displayName.convention("Paper")
}

override fun resolveBuild(): List<Path> {
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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,9 +46,12 @@ public interface DownloadsAPIService {
*/
public fun resolveBuild(
project: Project,
providers: ProviderFactory,
javaLauncher: JavaLauncher,
execOperations: ExecOperations,
version: String,
build: Build
): Path
): List<Path>

public companion object {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,11 +102,15 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
val removed = jars.remove(oldestBuild) ?: error("Build does not exist?")
version.knownJars.remove(oldestBuild)

val oldJar = jarsFor(versionName).resolve(removed.fileName)
try {
oldJar.deleteIfExists()
} catch (ex: IOException) {
LOGGER.warn("Failed to delete jar at {}", oldJar.absolutePathString(), ex)
val toDelete = removed.fileName?.let { listOf(it) }
?: requireNotNull(removed.classpath).toList()
for (path in toDelete) {
val delete = jarsFor(versionName).resolve(path)
try {
delete.deleteIfExists()
} catch (ex: IOException) {
LOGGER.warn("Failed to delete jar at {}", delete.absolutePathString(), ex)
}
}
writeVersions()
}
Expand All @@ -119,29 +129,69 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
@Synchronized
override fun resolveBuild(
project: Project,
providers: ProviderFactory,
javaLauncher: JavaLauncher,
execOperations: ExecOperations,
version: String,
build: DownloadsAPIService.Build
): Path {
): List<Path> {
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) {
versionData.knownJars[buildNumber] = possible.copy(keep = true)
writeVersions()
}
// Hash is good, return
return localJar
if (possible.classpath == null) {
val workDir = jarsDir.resolve(buildNumber.toString()).createDirectories()
val classpath = maybeApplyPaperclip(
javaLauncher,
execOperations,
localJar,
workDir,
jarsDir,
)

if (classpath != null) {
localJar.deleteIfExists()
versionData.knownJars[buildNumber] = possible.copy(
fileName = null,
sha256 = null,
classpath = classpath
)
writeVersions()
return classpath.flatMap { jarsDir.walkMatching(it) }
} else {
Files.walk(workDir).use { stream ->
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()
Expand Down Expand Up @@ -181,27 +231,40 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
}
LOGGER.lifecycle("Verified SHA256 hash of downloaded jar.")

val jarsDir = jarsFor(version)
jarsDir.createDirectories()
val fileName = "$buildNumber.jar"
val destination = jarsDir.resolve(fileName)

tempFile.moveTo(destination, StandardCopyOption.REPLACE_EXISTING)
val classpath = maybeApplyPaperclip(
javaLauncher,
execOperations,
tempFile,
jarsDir.resolve(buildNumber.toString()),
jarsDir,
)

if (classpath != null) {
tempFile.deleteIfExists()
} else {
tempFile.moveTo(destination, StandardCopyOption.REPLACE_EXISTING)
}

versionData.knownJars[buildNumber] = JarInfo(
buildNumber,
fileName,
download.sha256,
if (classpath == null) fileName else null,
if (classpath == null) download.sha256 else null,
classpath,
// If the build was specifically requested, (as opposed to resolved as latest) mark the jar for keeping
build is DownloadsAPIService.Build.Specific
)
writeVersions()

return destination
return classpath?.flatMap { jarsDir.walkMatching(it) }
?: listOf(destination)
}

private fun resolveBuildNumber(
project: Project,
providers: ProviderFactory,
version: Version,
build: DownloadsAPIService.Build
): Int {
Expand All @@ -215,7 +278,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
}

if (!parameters.refreshDependencies.get()) {
val checkFrequency = updateCheckFrequency(project)
val checkFrequency = updateCheckFrequency(providers)
val timeSinceLastCheck = System.currentTimeMillis() - version.lastUpdateCheck
if (timeSinceLastCheck <= checkFrequency.toMillis()) {
return resolveLatestLocalBuild(version)
Expand All @@ -238,7 +301,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
writeVersions()
}
} catch (ex: Exception) {
LOGGER.lifecycle("Failed to check for latest release, attempting to use latest local build.")
LOGGER.lifecycle("Failed to check for latest release, attempting to use latest local build.", ex)
resolveLatestLocalBuild(version)
}

Expand All @@ -247,10 +310,10 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
LOGGER.lifecycle(" > 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 '{}'.",
Expand All @@ -263,7 +326,7 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi
return Duration.ofHours(1) // default to 1 hour if unset
}
try {
return parseDuration(prop as String)
return parseDuration(prop)
} catch (ex: InvalidDurationException) {
throw InvalidUserDataException("Unable to parse value for property '${Constants.Properties.UPDATE_CHECK_FREQUENCY}'.\n${ex.message}", ex)
}
Expand All @@ -290,8 +353,9 @@ internal abstract class DownloadsAPIServiceImpl : BuildService<DownloadsAPIServi

private data class JarInfo(
val buildNumber: Int,
val fileName: String,
val sha256: String,
val fileName: String?,
val sha256: String?,
val classpath: List<String>?,
val keep: Boolean = false
)

Expand Down
23 changes: 18 additions & 5 deletions plugin/src/main/kotlin/xyz/jpenilla/runtask/task/AbstractRun.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand All @@ -113,6 +121,15 @@ public abstract class AbstractRun : JavaExec() {
super.exec()
}

protected open fun resolveBuild(): List<Path> = downloadsApiService.get().resolveBuild(
project,
providers,
javaLauncher.get(),
execOperations,
version.get(),
build.get()
)

private fun preExec() {
standardInput = System.`in`
workingDir(runDirectory)
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ 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
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<JavaLauncher>? {
Expand Down Expand Up @@ -59,3 +61,8 @@ internal fun <T : Any, U : T> PolymorphicDomainObjectContainer<T>.configure(
}

internal fun <T : Any, U : T> ExtensiblePolymorphicDomainObjectContainer<T>.registerFactory(type: KClass<U>, 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
}
12 changes: 12 additions & 0 deletions plugin/src/main/kotlin/xyz/jpenilla/runtask/util/files.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Path> = walkMatching {
it.fileSystem.getPathMatcher("glob:$glob").matches(it)
}

internal fun Path.walkMatching(predicate: (Path) -> Boolean): List<Path> = Files.walk(this).use { stream ->
stream.asSequence().filter { p -> predicate(p.relativeTo(this)) }.toList()
}
Loading

0 comments on commit 7107ae0

Please sign in to comment.