Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CleanerX-Paper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ repositories {
}

dependencies {
testImplementation(libs.aether.api)
testImplementation(libs.snakeyaml)
compileOnly(libs.paper.api)
compileOnly(libs.aether.api)
compileOnly(libs.snakeyaml)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,101 @@ import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.graph.Dependency
import org.eclipse.aether.repository.RemoteRepository
import org.yaml.snakeyaml.Yaml
import pl.syntaxdevteam.cleanerx.FastStatsBridge
import java.io.IOException
import java.io.InputStream
import java.util.logging.Level
import java.util.logging.Logger

/** Reads repository assignments from the same runtime manifest format as PunisherX. */
@Suppress("UnstableApiUsage", "unused")
class SyntaxLoader : PluginLoader {

companion object {
private val LOGGER: Logger = Logger.getLogger(SyntaxLoader::class.java.name)
}

override fun classloader(pluginClasspath: PluginClasspathBuilder) {
val resolver = MavenLibraryResolver()

resolveLibraries().map { DefaultArtifact(it) }
.forEach { artifact -> resolver.addDependency(Dependency(artifact, null)) }

resolver.addRepository(RemoteRepository.Builder("syntaxdevteam-snapshots", "default", "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/").build())
resolver.addRepository(RemoteRepository.Builder("syntaxdevteam-releases", "default", "https://nexus.syntaxdevteam.pl/repository/maven-releases/").build())
resolver.addRepository(RemoteRepository.Builder("paper", "default", "https://repo.papermc.io/repository/maven-public/").build())
resolver.addRepository(RemoteRepository.Builder("central-mirror", "default", "https://repo.papermc.io/repository/maven-central/").build())
resolver.addRepository(RemoteRepository.Builder("jitpack", "default", "https://jitpack.io/").build())
resolver.addRepository(RemoteRepository.Builder("codemc-snapshots", "default", "https://repo.codemc.io/repository/maven-snapshots/").build())
resolver.addRepository(RemoteRepository.Builder("faststatsReleases", "default", "https://repo.faststats.dev/releases").build())

pluginClasspath.addLibrary(resolver)
val config = try {
val input = SyntaxLoader::class.java.classLoader.getResourceAsStream("paper-libraries.yml")
?: error("paper-libraries.yml not found in plugin JAR")
input.use(::readLibraryConfig)
} catch (failure: Exception) {
throw IllegalStateException("Cannot configure CleanerX runtime libraries from paper-libraries.yml", failure)
}
val resolvers = linkedMapOf<String, MavenLibraryResolver>()
for (library in config.libraries) {
val repositoryId = selectRepository(library, config)
val resolver = resolvers.getOrPut(repositoryId) {
MavenLibraryResolver().apply {
repositoriesFor(repositoryId, config).forEach(::addRepository)
}
}
resolver.addDependency(Dependency(DefaultArtifact(library.coordinate), null))
}
resolvers.values.forEach(pluginClasspath::addLibrary)
}

private fun resolveLibraries(): List<String> {
return try {
readLibraryListFromYaml()
} catch (e: IOException) {
LOGGER.log(Level.SEVERE, "Failed to resolve libraries from paper-libraries.yml", e)
FastStatsBridge.trackBootstrapError(e)
emptyList()
internal fun readLibraryConfig(input: InputStream): LibraryConfig {
val document = Yaml().load<Any?>(input) as? Map<*, *>
?: error("paper-libraries.yml must contain a mapping")
val repositoryData = document["repositories"] as? Map<*, *>
?: error("paper-libraries.yml must define repositories")
val repositories = linkedMapOf<String, RemoteRepository>()
for ((key, value) in repositoryData) {
val id = (key as? String)?.trim().orEmpty()
val configuredUrl = (value as? String)?.trim().orEmpty()
require(id.isNotEmpty() && configuredUrl.isNotEmpty()) { "Invalid repository entry: $key" }
val url = if (configuredUrl == "MAVEN_CENTRAL_DEFAULT_MIRROR") {
MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR
} else {
configuredUrl
}
require(url.startsWith("https://") || url.startsWith("http://")) { "Invalid repository URL for $id" }
repositories[id] = RemoteRepository.Builder(id, "default", url).build()
}
require(repositories.containsKey("central")) { "Missing central repository in paper-libraries.yml" }
val libraryData = document["libraries"] as? List<*>
?: error("paper-libraries.yml must define libraries")
require(libraryData.isNotEmpty()) { "No runtime libraries defined in paper-libraries.yml" }
val libraries = libraryData.mapIndexed { index, entry ->
val library = when (entry) {
is String -> LibrarySpec(entry.trim())
is Map<*, *> -> {
val coordinate = entry["coordinate"] as? String
?: error("Missing coordinate in library entry ${index + 1}")
val repository = entry["repository"]
require(repository == null || repository is String) { "Invalid repository in library entry ${index + 1}" }
LibrarySpec(coordinate.trim(), repository?.trim())
}
else -> error("Invalid library entry ${index + 1}")
}
require(library.coordinate.isNotEmpty() && !library.coordinate.contains("\${")) {
"Invalid or unexpanded library coordinate: ${library.coordinate}"
}
DefaultArtifact(library.coordinate)
library
}
return LibraryConfig(repositories, libraries).also { config ->
config.libraries.forEach { selectRepository(it, config) }
}
}

private fun readLibraryListFromYaml(): List<String> {
val yaml = Yaml()
val inputStream: InputStream? = SyntaxLoader::class.java.classLoader.getResourceAsStream("paper-libraries.yml")

if (inputStream == null) {
LOGGER.warning("paper-libraries.yml not found in the classpath.")
return emptyList()
internal fun selectRepository(library: LibrarySpec, config: LibraryConfig): String {
val repositoryId = library.repository ?: when {
library.coordinate.substringBefore(':').let { it == "pl.syntaxdevteam" || it.startsWith("pl.syntaxdevteam.") } ->
if (library.coordinate.endsWith("-SNAPSHOT")) "syntaxdevteam-snapshots" else "syntaxdevteam-releases"
else -> "central"
}
require(config.repositories.containsKey(repositoryId)) {
"Unknown repository '$repositoryId' for ${library.coordinate}"
}
return repositoryId
}

val data: Map<String, List<String>> = yaml.load(inputStream)
return data["libraries"] ?: emptyList()
internal fun repositoriesFor(repositoryId: String, config: LibraryConfig): List<RemoteRepository> {
val selected = config.repositories.getValue(repositoryId)
// The selected repository also needs Central for transitive dependencies.
return if (repositoryId == "central") listOf(selected)
else listOf(selected, config.repositories.getValue("central"))
}

internal data class LibraryConfig(
val repositories: Map<String, RemoteRepository>,
val libraries: List<LibrarySpec>
)

internal data class LibrarySpec(val coordinate: String, val repository: String? = null)
}
12 changes: 11 additions & 1 deletion CleanerX-Paper/src/main/resources/paper-libraries.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
repositories:
syntaxdevteam-snapshots: "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/"
syntaxdevteam-releases: "https://nexus.syntaxdevteam.pl/repository/maven-releases/"
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
paper: "https://repo.papermc.io/repository/maven-public/"
jitpack: "https://jitpack.io/"
codemc-snapshots: "https://repo.codemc.io/repository/maven-snapshots/"
faststatsReleases: "https://repo.faststats.dev/releases"

libraries:
# Basic dependencies
- org.eclipse.aether:aether-api:${aetherVersion}
Expand All @@ -8,4 +17,5 @@ libraries:
- pl.syntaxdevteam:syntaxcore:${syntaxcoreVersion}
- pl.syntaxdevteam:messageHandler-paper:${messagehandlerVersion}
# FastStats dependencies
- dev.faststats.metrics:bukkit:${faststatsVersion}
- coordinate: dev.faststats.metrics:bukkit:${faststatsVersion}
repository: faststatsReleases
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package pl.syntaxdevteam.cleanerx.loader

import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
import java.io.ByteArrayInputStream

class SyntaxLoaderTest {
private val loader = SyntaxLoader()
private val repositories = """
repositories:
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
syntaxdevteam-snapshots: "https://example.com/snapshots/"
syntaxdevteam-releases: "https://example.com/releases/"
custom: "https://example.com/custom/"
""".trimIndent()

private fun read(libraries: String) = loader.readLibraryConfig(
ByteArrayInputStream((repositories + "\n" + libraries.trimIndent()).toByteArray())
)

@Test
fun routesCentralAndSyntaxDevTeamLibraries() {
val config = read("""
libraries:
- com.github.ben-manes.caffeine:caffeine:3.2.4
- net.kyori:adventure-nbt:5.2.0
- pl.syntaxdevteam:syntaxcore:1.4.1-R0.1-SNAPSHOT
- pl.syntaxdevteam.licencex:SyntaxLicenceX:0.1.2-SNAPSHOT
- pl.syntaxdevteam:syntaxcore:1.4.1
""")
assertEquals(
listOf("central", "central", "syntaxdevteam-snapshots", "syntaxdevteam-snapshots", "syntaxdevteam-releases"),
config.libraries.map { loader.selectRepository(it, config) }
)
}

@Test
fun explicitRepositoryWinsAndKeepsCentralForTransitives() {
val config = read("""
libraries:
- coordinate: pl.syntaxdevteam:example:1.0-SNAPSHOT
repository: custom
""")
val id = loader.selectRepository(config.libraries.single(), config)
assertEquals("custom", id)
assertEquals(listOf("custom", "central"), loader.repositoriesFor(id, config).map { it.id })
assertEquals(MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR, config.repositories.getValue("central").url)
assertEquals(listOf("central"), loader.repositoriesFor("central", config).map { it.id })
}

@Test
fun syntaxDevTeamResolverAlsoGetsCentral() {
val config = read("""
libraries:
- pl.syntaxdevteam:syntaxcore:1.4.1-R0.1-SNAPSHOT
""")
assertEquals(
listOf("syntaxdevteam-snapshots", "central"),
loader.repositoriesFor(loader.selectRepository(config.libraries.single(), config), config).map { it.id }
)
}

@Test
fun rejectsUnknownRepositoryBeforeBootstrap() {
reject("""
libraries:
- coordinate: com.example:library:1.0
repository: missing
""")
}

@Test
fun rejectsUnexpandedAndMalformedLibraryEntries() {
reject("libraries: ['com.example:library:" + '$' + "{version}']")
reject("libraries: [42]")
reject("libraries: [{repository: custom}]")
reject("libraries: [{coordinate: 'com.example:library:1.0', repository: 42}]")
reject("libraries: []")
}

@Test
fun rejectsMissingCentralRepository() {
try {
loader.readLibraryConfig(ByteArrayInputStream("repositories: {}\nlibraries: ['com.example:library:1.0']".toByteArray()))
fail("Missing Central must stop bootstrap")
} catch (failure: IllegalArgumentException) {
assertTrue(failure.message.orEmpty().contains("central"))
}
}

@Test
fun packagedManifestHasResolvedVersionsAndValidRepositories() {
val input = javaClass.classLoader.getResourceAsStream("paper-libraries.yml")
?: error("Processed runtime manifest is missing")
val config = input.use(loader::readLibraryConfig)
assertTrue(config.libraries.isNotEmpty())
config.libraries.forEach { library ->
assertTrue(!library.coordinate.contains("\${"))
assertTrue(loader.repositoriesFor(loader.selectRepository(library, config), config).isNotEmpty())
}
}

private fun reject(libraries: String) {
try {
read(libraries)
} catch (_: IllegalArgumentException) {
return
} catch (_: IllegalStateException) {
return
}
fail("Invalid runtime manifest must stop bootstrap: $libraries")
}
}
41 changes: 41 additions & 0 deletions docs/runtime-libraries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Runtime library loading

The Paper `SyntaxLoader` uses the same manifest structure as PunisherX:
`repositories` maps repository IDs to URLs, and `libraries` contains either
`group:artifact:version` strings or mappings with `coordinate` and `repository`.
Gradle still expands versions from the existing build configuration.

`central: "MAVEN_CENTRAL_DEFAULT_MIRROR"` is resolved by the loader through
Paper's `MavenLibraryResolver.MAVEN_CENTRAL_DEFAULT_MIRROR`, including the server's
configured mirror. Do not use the obsolete Paper `/repository/maven-central/` URL.

Libraries without an explicit repository use Central, except SyntaxDevTeam
groups (`pl.syntaxdevteam` and its subgroups), which use the snapshots or releases
repository according to their version. An explicit `repository` always wins. FastStats explicitly uses `faststatsReleases`.
Each selected repository gets its own resolver with Central available for
transitive dependencies. Unknown repository IDs, invalid entries, missing manifests and
unexpanded placeholders stop bootstrap with a descriptive error.

Example:

```yaml
repositories:
central: "MAVEN_CENTRAL_DEFAULT_MIRROR"
syntaxdevteam-snapshots: "https://nexus.syntaxdevteam.pl/repository/maven-snapshots/"
syntaxdevteam-releases: "https://nexus.syntaxdevteam.pl/repository/maven-releases/"
custom: "https://example.com/maven/"
libraries:
- com.github.ben-manes.caffeine:caffeine:3.2.4
- coordinate: com.example:library:1.0.0
repository: custom
```

This change preserves library versions and the Paper/Folia plugin lifecycle.
It only changes dependency bootstrap and manifest validation.

## Verification — 2026-10-11

`test build` passed (37 tests, including seven loader regression tests;
no failures or errors). The packaged Paper JAR contains the expanded manifest
with repository assignments and no obsolete Central URL or unresolved versions.
A live Paper/Folia server start was not repeated.
Loading