feat(userdev): Add experimental (for now) option for shared userdev caches (#187)
This commit is contained in:
parent
aa146cdd5f
commit
b56b118555
5 changed files with 211 additions and 27 deletions
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
|
@ -20,3 +20,11 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GRADLE_PUBLISH_KEY: "${{ secrets.GRADLE_PLUGIN_PORTAL_KEY }}"
|
GRADLE_PUBLISH_KEY: "${{ secrets.GRADLE_PLUGIN_PORTAL_KEY }}"
|
||||||
GRADLE_PUBLISH_SECRET: "${{ secrets.GRADLE_PLUGIN_PORTAL_SECRET }}"
|
GRADLE_PUBLISH_SECRET: "${{ secrets.GRADLE_PLUGIN_PORTAL_SECRET }}"
|
||||||
|
- name: Parse tag
|
||||||
|
id: vars
|
||||||
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||||
|
- name: Create release and changelog
|
||||||
|
uses: MC-Machinations/auto-release-changelog@v1.1.3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
title: paperweight ${{ steps.vars.outputs.tag }}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* paperweight is a Gradle plugin for the PaperMC project.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Kyle Wood (DenWav)
|
||||||
|
* Contributors
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation;
|
||||||
|
* version 2.1 only, no later versions.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with this library; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||||
|
* USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.papermc.paperweight.util
|
||||||
|
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of accepted abbreviation [Char]s to [ChronoUnit].
|
||||||
|
*/
|
||||||
|
private val units = mapOf(
|
||||||
|
'd' to ChronoUnit.DAYS,
|
||||||
|
'h' to ChronoUnit.HOURS,
|
||||||
|
'm' to ChronoUnit.MINUTES,
|
||||||
|
's' to ChronoUnit.SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a [Duration] from [input].
|
||||||
|
*
|
||||||
|
* Accepted format is a number followed by a unit abbreviation.
|
||||||
|
* See [units] for possible units.
|
||||||
|
* Example input strings: `["1d", "12h", "1m", "30s"]`
|
||||||
|
*
|
||||||
|
* @param input formatted input string
|
||||||
|
* @throws InvalidDurationException when [input] is improperly formatted
|
||||||
|
*/
|
||||||
|
@Throws(InvalidDurationException::class)
|
||||||
|
fun parseDuration(input: String): Duration {
|
||||||
|
if (input.isBlank()) {
|
||||||
|
throw InvalidDurationException.noInput(input)
|
||||||
|
}
|
||||||
|
if (input.length < 2) {
|
||||||
|
throw InvalidDurationException.invalidInput(input)
|
||||||
|
}
|
||||||
|
val unitAbbreviation = input.last()
|
||||||
|
|
||||||
|
val unit = units[unitAbbreviation] ?: throw InvalidDurationException.invalidInput(input)
|
||||||
|
|
||||||
|
val length = try {
|
||||||
|
input.substring(0, input.length - 1).toLong()
|
||||||
|
} catch (ex: NumberFormatException) {
|
||||||
|
throw InvalidDurationException.invalidInput(input, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Duration.of(length, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InvalidDurationException private constructor(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable? = null
|
||||||
|
) : IllegalArgumentException(message, cause) {
|
||||||
|
companion object {
|
||||||
|
private val infoMessage = """
|
||||||
|
Accepted format is a number followed by a unit abbreviation.
|
||||||
|
Possible units: $units
|
||||||
|
Example input strings: ["1d", "12h", "1m", "30s"]
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
fun noInput(input: String): InvalidDurationException =
|
||||||
|
InvalidDurationException("Cannot parse a Duration from a blank input string '$input'.\n$infoMessage")
|
||||||
|
|
||||||
|
fun invalidInput(input: String, cause: Throwable? = null) =
|
||||||
|
InvalidDurationException("Cannot parse a Duration from input '$input'.\n$infoMessage", cause)
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,12 +28,17 @@ import io.papermc.paperweight.tasks.*
|
||||||
import io.papermc.paperweight.userdev.attribute.Obfuscation
|
import io.papermc.paperweight.userdev.attribute.Obfuscation
|
||||||
import io.papermc.paperweight.userdev.internal.setup.SetupHandler
|
import io.papermc.paperweight.userdev.internal.setup.SetupHandler
|
||||||
import io.papermc.paperweight.userdev.internal.setup.UserdevSetup
|
import io.papermc.paperweight.userdev.internal.setup.UserdevSetup
|
||||||
import io.papermc.paperweight.userdev.internal.setup.util.genSources
|
import io.papermc.paperweight.userdev.internal.setup.util.*
|
||||||
import io.papermc.paperweight.util.*
|
import io.papermc.paperweight.util.*
|
||||||
import io.papermc.paperweight.util.constants.*
|
import io.papermc.paperweight.util.constants.*
|
||||||
|
import java.nio.file.Path
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.ModuleDependency
|
||||||
|
import org.gradle.api.artifacts.ProjectDependency
|
||||||
|
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
|
||||||
|
import org.gradle.api.artifacts.result.ResolvedDependencyResult
|
||||||
import org.gradle.api.attributes.Bundling
|
import org.gradle.api.attributes.Bundling
|
||||||
import org.gradle.api.attributes.Category
|
import org.gradle.api.attributes.Category
|
||||||
import org.gradle.api.attributes.LibraryElements
|
import org.gradle.api.attributes.LibraryElements
|
||||||
|
@ -54,32 +59,27 @@ abstract class PaperweightUser : Plugin<Project> {
|
||||||
abstract val javaToolchainService: JavaToolchainService
|
abstract val javaToolchainService: JavaToolchainService
|
||||||
|
|
||||||
override fun apply(target: Project) {
|
override fun apply(target: Project) {
|
||||||
|
val sharedCacheRootRoot = target.gradle.gradleUserHomeDir.toPath().resolve("caches/paperweight-userdev")
|
||||||
|
val sharedCacheRoot = if (target.sharedCaches) sharedCacheRootRoot.resolve(paperweightHash) else null
|
||||||
|
|
||||||
target.gradle.sharedServices.registerIfAbsent("download", DownloadService::class) {}
|
target.gradle.sharedServices.registerIfAbsent("download", DownloadService::class) {}
|
||||||
|
|
||||||
|
val cleanAll = target.tasks.register<Delete>("cleanAllPaperweightUserdevCaches") {
|
||||||
|
group = "paperweight"
|
||||||
|
description = "Delete the project & all shared paperweight-userdev setup cache."
|
||||||
|
delete(target.layout.cache)
|
||||||
|
delete(sharedCacheRootRoot)
|
||||||
|
}
|
||||||
val cleanCache by target.tasks.registering<Delete> {
|
val cleanCache by target.tasks.registering<Delete> {
|
||||||
group = "paperweight"
|
group = "paperweight"
|
||||||
description = "Delete the project setup cache and task outputs."
|
description = "Delete the project paperweight-userdev setup cache."
|
||||||
delete(target.layout.cache)
|
delete(target.layout.cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
target.configurations.register(DEV_BUNDLE_CONFIG)
|
target.configurations.register(DEV_BUNDLE_CONFIG)
|
||||||
|
|
||||||
// these must not be initialized until afterEvaluate, as they resolve the dev bundle
|
// must not be initialized until afterEvaluate, as it resolves the dev bundle
|
||||||
val userdevSetup by lazy {
|
val userdevSetup by lazy { createSetup(target, sharedCacheRoot) }
|
||||||
val devBundleZip = target.configurations.named(DEV_BUNDLE_CONFIG).map { it.singleFile }.convertToPath()
|
|
||||||
val serviceName = "paperweight-userdev:setupService:${devBundleZip.sha256asHex()}"
|
|
||||||
|
|
||||||
target.gradle.sharedServices
|
|
||||||
.registerIfAbsent(serviceName, UserdevSetup::class) {
|
|
||||||
parameters {
|
|
||||||
cache.set(target.layout.cache)
|
|
||||||
bundleZip.set(devBundleZip)
|
|
||||||
downloadService.set(target.download)
|
|
||||||
genSources.set(target.genSources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
val userdev = target.extensions.create(
|
val userdev = target.extensions.create(
|
||||||
PAPERWEIGHT_EXTENSION,
|
PAPERWEIGHT_EXTENSION,
|
||||||
|
@ -133,7 +133,7 @@ abstract class PaperweightUser : Plugin<Project> {
|
||||||
val cleaningCache = gradle.startParameter.taskRequests
|
val cleaningCache = gradle.startParameter.taskRequests
|
||||||
.any { req ->
|
.any { req ->
|
||||||
req.args.any { arg ->
|
req.args.any { arg ->
|
||||||
NameMatcher().find(arg, tasks.names) == cleanCache.name
|
NameMatcher().find(arg, tasks.names) in setOf(cleanCache.name, cleanAll.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cleaningCache) {
|
if (cleaningCache) {
|
||||||
|
@ -141,10 +141,10 @@ abstract class PaperweightUser : Plugin<Project> {
|
||||||
}
|
}
|
||||||
|
|
||||||
userdev.reobfArtifactConfiguration.get()
|
userdev.reobfArtifactConfiguration.get()
|
||||||
.configure(target, reobfJar)
|
.configure(this, reobfJar)
|
||||||
|
|
||||||
if (userdev.injectPaperRepository.get()) {
|
if (userdev.injectPaperRepository.get()) {
|
||||||
target.repositories.maven(PAPER_MAVEN_REPO_URL) {
|
repositories.maven(PAPER_MAVEN_REPO_URL) {
|
||||||
content { onlyForConfigurations(DEV_BUNDLE_CONFIG) }
|
content { onlyForConfigurations(DEV_BUNDLE_CONFIG) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,8 @@ abstract class PaperweightUser : Plugin<Project> {
|
||||||
checkForDevBundle()
|
checkForDevBundle()
|
||||||
|
|
||||||
configureRepositories(userdevSetup)
|
configureRepositories(userdevSetup)
|
||||||
|
|
||||||
|
cleanSharedCaches(this, sharedCacheRootRoot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,4 +256,40 @@ abstract class PaperweightUser : Plugin<Project> {
|
||||||
|
|
||||||
private fun createContext(project: Project): SetupHandler.Context =
|
private fun createContext(project: Project): SetupHandler.Context =
|
||||||
SetupHandler.Context(project, workerExecutor, javaToolchainService)
|
SetupHandler.Context(project, workerExecutor, javaToolchainService)
|
||||||
|
|
||||||
|
private fun createSetup(target: Project, sharedCacheRoot: Path?): UserdevSetup {
|
||||||
|
val bundleConfig = target.configurations.named(DEV_BUNDLE_CONFIG)
|
||||||
|
val devBundleZip = bundleConfig.map { it.singleFile }.convertToPath()
|
||||||
|
val bundleHash = devBundleZip.sha256asHex()
|
||||||
|
val cacheDir = if (sharedCacheRoot == null) {
|
||||||
|
target.layout.cache
|
||||||
|
} else {
|
||||||
|
when (bundleConfig.get().dependencies.single()) {
|
||||||
|
is ProjectDependency -> {
|
||||||
|
throw PaperweightException("Shared caches does not support the dev bundle being a ProjectDependency.")
|
||||||
|
}
|
||||||
|
|
||||||
|
is ModuleDependency -> {
|
||||||
|
val resolved =
|
||||||
|
bundleConfig.get().incoming.resolutionResult.rootComponent.get().dependencies.single() as ResolvedDependencyResult
|
||||||
|
val resolvedId = resolved.selected.id as ModuleComponentIdentifier
|
||||||
|
sharedCacheRoot.resolve("module/${resolvedId.group}/${resolvedId.module}/${resolvedId.version}")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> sharedCacheRoot.resolve("non-module/$bundleHash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val serviceName = "paperweight-userdev:setupService:$bundleHash"
|
||||||
|
return target.gradle.sharedServices
|
||||||
|
.registerIfAbsent(serviceName, UserdevSetup::class) {
|
||||||
|
parameters {
|
||||||
|
cache.set(cacheDir)
|
||||||
|
bundleZip.set(devBundleZip)
|
||||||
|
downloadService.set(target.download)
|
||||||
|
genSources.set(target.genSources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
package io.papermc.paperweight.userdev.internal.setup
|
package io.papermc.paperweight.userdev.internal.setup
|
||||||
|
|
||||||
import io.papermc.paperweight.DownloadService
|
import io.papermc.paperweight.DownloadService
|
||||||
import io.papermc.paperweight.userdev.internal.setup.util.lockSetup
|
import io.papermc.paperweight.userdev.internal.setup.util.*
|
||||||
import io.papermc.paperweight.util.*
|
import io.papermc.paperweight.util.*
|
||||||
import io.papermc.paperweight.util.constants.*
|
import io.papermc.paperweight.util.constants.*
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.*
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.artifacts.DependencySet
|
import org.gradle.api.artifacts.DependencySet
|
||||||
import org.gradle.api.artifacts.repositories.IvyArtifactRepository
|
import org.gradle.api.artifacts.repositories.IvyArtifactRepository
|
||||||
|
@ -52,10 +53,12 @@ abstract class UserdevSetup : BuildService<UserdevSetup.Parameters>, SetupHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
private val extractDevBundle: ExtractedBundle<Any> = lockSetup(parameters.cache.path) {
|
private val extractDevBundle: ExtractedBundle<Any> = lockSetup(parameters.cache.path) {
|
||||||
extractDevBundle(
|
val extract = extractDevBundle(
|
||||||
parameters.cache.path.resolve(paperSetupOutput("extractDevBundle", "dir")),
|
parameters.cache.path.resolve(paperSetupOutput("extractDevBundle", "dir")),
|
||||||
parameters.bundleZip.path
|
parameters.bundleZip.path
|
||||||
)
|
)
|
||||||
|
lastUsedFile(parameters.cache.path).writeText(System.currentTimeMillis().toString())
|
||||||
|
extract
|
||||||
}
|
}
|
||||||
|
|
||||||
private val setup = createSetup()
|
private val setup = createSetup()
|
||||||
|
|
|
@ -26,17 +26,19 @@ import io.papermc.paperweight.DownloadService
|
||||||
import io.papermc.paperweight.userdev.PaperweightUser
|
import io.papermc.paperweight.userdev.PaperweightUser
|
||||||
import io.papermc.paperweight.userdev.internal.setup.UserdevSetup
|
import io.papermc.paperweight.userdev.internal.setup.UserdevSetup
|
||||||
import io.papermc.paperweight.util.*
|
import io.papermc.paperweight.util.*
|
||||||
import io.papermc.paperweight.util.constants.USERDEV_SETUP_LOCK
|
import io.papermc.paperweight.util.constants.*
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
import java.time.Duration
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.*
|
||||||
|
import kotlin.streams.asSequence
|
||||||
import kotlin.system.measureTimeMillis
|
import kotlin.system.measureTimeMillis
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
|
|
||||||
private val paperweightHash: String by lazy { hashPaperweightJar() }
|
val paperweightHash: String by lazy { hashPaperweightJar() }
|
||||||
|
|
||||||
fun Path.siblingLogFile(): Path = withDifferentExtension("log")
|
fun Path.siblingLogFile(): Path = withDifferentExtension("log")
|
||||||
|
|
||||||
|
@ -154,7 +156,7 @@ fun interface HashFunction : () -> String {
|
||||||
|
|
||||||
private fun hashPaperweightJar(): String {
|
private fun hashPaperweightJar(): String {
|
||||||
val userdevShadowJar = Paths.get(PaperweightUser::class.java.protectionDomain.codeSource.location.toURI())
|
val userdevShadowJar = Paths.get(PaperweightUser::class.java.protectionDomain.codeSource.location.toURI())
|
||||||
return hash(userdevShadowJar)
|
return userdevShadowJar.sha256asHex()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <R> lockSetup(cache: Path, canBeNested: Boolean = false, action: () -> R): R {
|
fun <R> lockSetup(cache: Path, canBeNested: Boolean = false, action: () -> R): R {
|
||||||
|
@ -175,9 +177,56 @@ val Project.ci: Provider<Boolean>
|
||||||
.map { it.toBoolean() }
|
.map { it.toBoolean() }
|
||||||
.orElse(false)
|
.orElse(false)
|
||||||
|
|
||||||
|
private fun experimentalProp(name: String) = "paperweight.experimental.$name"
|
||||||
|
|
||||||
val Project.genSources: Boolean
|
val Project.genSources: Boolean
|
||||||
get() {
|
get() {
|
||||||
val ci = ci.get()
|
val ci = ci.get()
|
||||||
val prop = providers.gradleProperty("paperweight.experimental.genSources").orNull?.toBoolean()
|
val prop = providers.gradleProperty(experimentalProp("genSources")).orNull?.toBoolean()
|
||||||
return prop ?: !ci
|
return prop ?: !ci
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Project.sharedCaches: Boolean
|
||||||
|
get() = providers.gradleProperty(experimentalProp("sharedCaches")).orNull.toBoolean()
|
||||||
|
|
||||||
|
private fun deleteUnusedAfter(target: Project): Long =
|
||||||
|
target.providers.gradleProperty(experimentalProp("sharedCaches.deleteUnusedAfter"))
|
||||||
|
.map { value -> parseDuration(value) }
|
||||||
|
.orElse(Duration.ofDays(7))
|
||||||
|
.map { duration -> duration.toMillis() }
|
||||||
|
.get()
|
||||||
|
|
||||||
|
fun lastUsedFile(cacheDir: Path): Path = cacheDir.resolve(paperSetupOutput("last-used", "txt"))
|
||||||
|
|
||||||
|
fun cleanSharedCaches(target: Project, root: Path) {
|
||||||
|
if (!root.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val toDelete = Files.walk(root).use { stream ->
|
||||||
|
stream.asSequence()
|
||||||
|
.filter { it.name == "last-used.txt" }
|
||||||
|
.mapNotNull {
|
||||||
|
val dir = it.parent.parent // paperweight dir
|
||||||
|
val lastUsed = it.readText().toLong()
|
||||||
|
val since = System.currentTimeMillis() - lastUsed
|
||||||
|
val cutoff = deleteUnusedAfter(target)
|
||||||
|
if (since > cutoff) dir else null
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
for (path in toDelete) {
|
||||||
|
path.deleteRecursively()
|
||||||
|
|
||||||
|
// clean up empty parent directories
|
||||||
|
var parent: Path = path.parent
|
||||||
|
while (true) {
|
||||||
|
val entries = parent.listDirectoryEntries()
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
parent.deleteIfExists()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parent = parent.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue