Skip to content
Merged
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: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
javaVersion=25
mcVersion=26.3
group=dev.slne.surf.api
version=3.48.1
version=3.49.0
relocationPrefix=dev.slne.surf.api.libs
snapshot=false
2 changes: 1 addition & 1 deletion surf-api-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {

group = groupId
version = buildString {
append("2.4.1")
append("2.5.0")
if (snapshot) append("-SNAPSHOT")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,21 @@ enum class MinestomServerFeature(
* Calling `withXxx()` only puts the feature's module on the class path; the server still installs
* it in `surfMinestomServer { }`.
*
* By default, the server jar only contains the project's own code and the surf modules. Minestom
* and every other third-party library are downloaded into `libraries/` when the server starts, see
* [downloadLibraries].
* By default, the server jar only contains the project's own code, the surf modules and the
* dependencies declared in the `shaded` configuration. Minestom and every other third-party
* library are downloaded into `libraries/` when the server starts, see [downloadLibraries]. With
* [downloadSurfModules], the surf modules are downloaded as well.
*
* Dependencies from repositories that need credentials cannot be downloaded at runtime. Declare
* them in the `shaded` configuration to embed them in the server jar:
* ```
* dependencies {
* shaded(libs.private.plugin)
* }
* ```
* The whole resolved closure of a shaded dependency is embedded and therefore not downloaded, which
* may include public modules it depends on. `isTransitive = false` limits a declaration to the
* module itself.
*/
open class MinestomServerSurfExtension @Inject constructor(objects: ObjectFactory) :
CoreSurfExtension(objects) {
Expand All @@ -69,10 +81,21 @@ open class MinestomServerSurfExtension @Inject constructor(objects: ObjectFactor
* Whether third-party libraries are downloaded with gremlin when the server starts instead of
* being shaded into the server jar. They are fetched from the project's HTTP(S) Maven
* repositories, so libraries that only exist in `mavenLocal()` or need credentials have to be
* shaded. Surf modules (`dev.slne.surf*`) are always shaded.
* declared in the `shaded` configuration. Surf modules (`dev.slne.surf*`) are shaded unless
* [downloadSurfModules] is set.
*/
val downloadLibraries = objects.property<Boolean>().convention(true)

/**
* Whether surf modules (`dev.slne.surf*`, such as surf-api and the bundled plugins) are
* downloaded on the first start like third-party libraries instead of being shaded.
*
* Only the project's own modules, the bootstrap and the `shaded` dependencies stay in the
* server jar, which keeps it small. Requires [downloadLibraries]; without it the setting has no
* effect and every module is shaded.
*/
val downloadSurfModules = objects.property<Boolean>().convention(false)

/**
* The plugins from the `plugins` directory the server's own code uses, by ID.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import org.gradle.api.Project
import org.gradle.api.artifacts.ResolvedDependency
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.attributes.Attribute
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
Expand All @@ -29,11 +31,18 @@ import xyz.jpenilla.gremlin.gradle.WriteDependencySet
* Builds a runnable surf Minestom server as one executable server jar: the surf api, the
* requested features and the project's own code.
*
* Only the project's own modules, the surf modules and the bootstrap are shaded. Every other
* library is listed in the gremlin dependency set `dependencies.txt`. The bootstrap agent
* The server jar contains the project's own modules, the bootstrap, the surf modules and the
* modules declared in the `shaded` configuration. With
* [MinestomServerSurfExtension.downloadSurfModules] the surf modules are downloaded instead. Every
* other library is listed in the gremlin dependency set `dependencies.txt`. The bootstrap agent
* downloads these libraries into `libraries/`, relocates them like the server jar and appends
* them to the system class path before `main` runs.
*
* The `shaded` configuration is extended by `runtimeOnly` and embeds modules that cannot be
* downloaded, such as those of repositories that need credentials. The whole resolved closure of
* the dependencies declared in it is embedded, including public modules they depend on. A module
* of that closure is never downloaded, regardless of other paths to it.
*
* Manifest attributes written to the jar:
* - `Launcher-Agent-Class`: the bootstrap agent.
* - `Surf-Delegate-Agent-Classes`: delegate agents the bootstrap agent calls after the libraries
Expand Down Expand Up @@ -68,10 +77,18 @@ internal class MinestomServerSurfPlugin :
}

override fun Project.configure0() {
val shaded = configurations.create(SHADED) {
description = "Dependencies embedded in the server jar instead of being downloaded, " +
"for repositories that need credentials"
isCanBeConsumed = false
isCanBeResolved = true
}
configurations.named("runtimeOnly") { extendsFrom(shaded) }

dependencies {
add("implementation", Constants.MINESTOM)
add("runtimeOnly", "org.apache.logging.log4j:log4j-core:${Constants.LOG4J_VERSION}")
add("runtimeOnly", "org.apache.logging.log4j:log4j-slf4j2-impl:${Constants.LOG4J_VERSION}")
"implementation"(Constants.MINESTOM)
"runtimeOnly"("org.apache.logging.log4j:log4j-core:${Constants.LOG4J_VERSION}")
"runtimeOnly"("org.apache.logging.log4j:log4j-slf4j2-impl:${Constants.LOG4J_VERSION}")
}

tasks.named<ShadowJar>("shadowJar") {
Expand Down Expand Up @@ -137,6 +154,9 @@ internal class MinestomServerSurfPlugin :
if (extension.downloadLibraries.get()) {
configureLibraryDownload(extension)
} else {
if (extension.downloadSurfModules.get()) {
logger.warn("downloadSurfModules has no effect on ${project.path} while downloadLibraries is disabled")
}
tasks.named<ProcessResources>("processResources") { exclude(DEPENDENCIES_FILE) }
configureShadedAsmVersion()
}
Expand Down Expand Up @@ -202,11 +222,33 @@ internal class MinestomServerSurfPlugin :
}

/**
* Writes every third-party library, with the server jar's relocations, to `dependencies.txt`,
* the file the bootstrap downloads from. The project's own modules, the surf modules and the
* bootstrap stay shaded in the server jar.
* Writes every library that is not shaded, with the server jar's relocations, to
* `dependencies.txt`, the file the bootstrap downloads from. The [ShadingRules] decide which
* modules stay in the server jar.
*/
private fun Project.configureLibraryDownload(extension: MinestomServerSurfExtension) {
val runtimeClasspath = configurations.getByName("runtimeClasspath")
val shaded = configurations.named(SHADED) {
// Selects the same versions as the runtime graph and resolves the same variants
shouldResolveConsistentlyWith(runtimeClasspath)
attributes {
runtimeClasspath.attributes.keySet().forEach { key ->
@Suppress("UNCHECKED_CAST")
attribute(key as Attribute<Any>, runtimeClasspath.attributes.getAttribute(key)!!)
}
}
}
val rules = ShadingRules(
downloadSurfModules = extension.downloadSurfModules.get(),
embedded = shaded.map { configuration ->
configuration.incoming.resolutionResult.allComponents
.mapNotNull { component ->
(component.id as? ModuleComponentIdentifier)?.let { it.group to it.module }
}
.toSet()
},
)

configurations.named(RUNTIME_DOWNLOAD) {
extendsFrom(configurations.getByName("implementation"), configurations.getByName("runtimeOnly"))
shouldResolveConsistentlyWith(configurations.getByName("runtimeClasspath"))
Expand All @@ -215,8 +257,10 @@ internal class MinestomServerSurfPlugin :
tasks.named<WriteDependencySet>("writeDependencies") {
// Filtered per component, so that the libraries of shaded modules are still downloaded
dependencies.setFrom(configurations.named(RUNTIME_DOWNLOAD).map { configuration ->
// Resolves `shaded` before the artifact view, not during its resolution
rules.embeddedModules
configuration.incoming.artifactView {
componentFilter { id -> id !is ModuleComponentIdentifier || !isShaded(id.group, id.module) }
componentFilter { id -> id !is ModuleComponentIdentifier || !rules.isShaded(id.group, id.module) }
}.artifacts
})
forEachRelocation { from, to, excludes ->
Expand All @@ -232,7 +276,7 @@ internal class MinestomServerSurfPlugin :

tasks.named<ShadowJar>("shadowJar") {
dependencies {
exclude { dependency -> !dependency.isShaded() }
exclude { dependency -> !rules.isShaded(dependency) }
}
}
}
Expand Down Expand Up @@ -263,16 +307,35 @@ internal class MinestomServerSurfPlugin :
}
}

private fun ResolvedDependency.isShaded() =
isShaded(moduleGroup, moduleName) ||
moduleArtifacts.any { it.id.componentIdentifier is ProjectComponentIdentifier }

/**
* Whether the module stays in the server jar: surf modules (not the third-party forks under
* `dev.slne.forks`) and the [BOOTSTRAP_MODULES].
* Decides which modules stay in the server jar when the libraries are downloaded.
*
* A module is shaded when it is one of the [BOOTSTRAP_MODULES], a project dependency, part of
* the `shaded` closure or, unless [downloadSurfModules] is set, a surf module. Surf modules
* exclude the third-party forks under `dev.slne.forks`. Every component therefore has exactly
* one destination: the server jar or `dependencies.txt`.
*
* The `shaded` closure is resolved on first use, at execution time, and then kept.
*
* @param embedded `group` to `name` of every module in the resolution result of `shaded`
*/
private fun isShaded(group: String, module: String) =
group == "dev.slne.surf" || group.startsWith("dev.slne.surf.") || group to module in BOOTSTRAP_MODULES
private class ShadingRules(
private val downloadSurfModules: Boolean,
embedded: Provider<Set<Pair<String, String>>>,
) {
val embeddedModules: Set<Pair<String, String>> by lazy { embedded.get() }

fun isShaded(group: String, module: String): Boolean {
val id = group to module
return id in BOOTSTRAP_MODULES ||
id in embeddedModules ||
(!downloadSurfModules && (group == "dev.slne.surf" || group.startsWith("dev.slne.surf.")))
}

fun isShaded(dependency: ResolvedDependency) =
isShaded(dependency.moduleGroup, dependency.moduleName) ||
dependency.moduleArtifacts.any { it.id.componentIdentifier is ProjectComponentIdentifier }
}

/**
* Registers `runServer`, which runs the server jar with `java -jar` so that the manifest's
Expand Down Expand Up @@ -306,6 +369,7 @@ internal class MinestomServerSurfPlugin :
const val LOG4J_CONFIG = "log4j2.xml"

const val RUNTIME_DOWNLOAD = "runtimeDownload"
const val SHADED = "shaded"
const val JAR_RELOCATOR_RUNTIME = "jarRelocatorRuntime"
const val DEPENDENCIES_FILE = "dependencies.txt"
const val BOOTSTRAP_MODULE = "surf-api-minestom-server-bootstrap"
Expand All @@ -317,9 +381,11 @@ internal class MinestomServerSurfPlugin :
const val MIXIN_CONFIG = "mixins.surf-api-minestom-server.json"

/**
* What the bootstrap needs before anything is downloaded.
* What the bootstrap needs before anything is downloaded, independent of
* [MinestomServerSurfExtension.downloadSurfModules].
*/
val BOOTSTRAP_MODULES = setOf(
"dev.slne.surf.api" to BOOTSTRAP_MODULE,
"xyz.jpenilla" to "gremlin-runtime",
"org.jspecify" to "jspecify",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.slne.surf.api.gradle.platform.minestom.server

import org.gradle.kotlin.dsl.getByType
import org.gradle.testfixtures.ProjectBuilder
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class MinestomServerSurfPluginTest {

private val project = ProjectBuilder.builder().build().also {
it.plugins.apply(MinestomServerSurfPlugin::class.java)
}

@Test
fun `shaded configuration is resolvable but not consumable`() {
val shaded = project.configurations.getByName("shaded")

assertTrue(shaded.isCanBeResolved)
assertFalse(shaded.isCanBeConsumed)
assertNotNull(shaded.description)
}

@Test
fun `runtimeOnly extends shaded`() {
val shaded = project.configurations.getByName("shaded")

assertTrue(shaded in project.configurations.getByName("runtimeOnly").extendsFrom)
}

@Test
fun `downloadSurfModules is disabled by default and downloadLibraries enabled`() {
val extension = project.extensions.getByType<MinestomServerSurfExtension>()

assertFalse(extension.downloadSurfModules.get())
assertTrue(extension.downloadLibraries.get())
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
package dev.slne.surf.api.minestom.example

import dev.slne.surf.api.minestom.command.dsl.anyExecutor
import dev.slne.surf.api.minestom.command.dsl.commandAPICommand
import dev.slne.surf.api.minestom.command.dsl.integerArgument
import dev.slne.surf.api.minestom.command.dsl.playerArgument
import dev.slne.surf.api.minestom.command.dsl.playerExecutor
import dev.slne.surf.api.minestom.command.dsl.playerExecutorSuspend
import dev.slne.surf.api.minestom.command.dsl.signedMessageArgument
import dev.slne.surf.api.minestom.command.dsl.subcommand
import dev.slne.surf.api.minestom.command.dsl.*
import dev.slne.surf.api.minestom.coroutine.withEntity
import dev.slne.surf.api.minestom.highlight.BlockHighlights
import dev.slne.surf.api.minestom.server.SurfMinestomServer
import dev.slne.surf.api.minestom.visibility.PlayerVisibility
import kotlinx.coroutines.delay
import net.kyori.adventure.chat.SignedMessage
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import net.minestom.server.MinecraftServer
import net.minestom.server.entity.GameMode
import net.minestom.server.entity.Player
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.seconds

/**
Expand Down Expand Up @@ -70,9 +64,15 @@ internal object ExampleCommands {
}
}

// stop() waits until the plugins are disabled, which a tick thread must not wait for
commandAPICommand("stop") {
withPermission("example.stop")
anyExecutor { _, _ -> MinecraftServer.stopCleanly() }
anyExecutor { _, _ ->
Thread({
SurfMinestomServer.instance.stop()
exitProcess(0)
}, "example-stop").start()
}
}

// The message keeps the signature the client produced for the argument
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ fun main() {

maxPlayers = 20
listeners(ExampleListeners(world))

// Runs after the plugins and features are disabled, also when the process is terminated
onShutdown { LOGGER.info("Example server stopped") }
}

ExampleCommands.register()
Expand Down
Loading
Loading