feat: split into modules, replace the dependency resolver, add build plugins - #2
Merged
Conversation
Turns the single-module build into a multi-module one. The root project becomes an aggregator that publishes nothing itself, and the extension system moves unchanged into its own subproject. The published artifact keeps its coordinates and its content: CloudNet v4 compiles against net.onelitefeather:minestom-extensions, so the packages net.minestom.server.extensions.* and net.hollowcube.minestom.extensions are a public contract and none of them are touched here. The sources are moved with git mv, so the history follows them. The whole maven-publish setup - POM metadata, licences, SCM, the OneLiteFeather repository and its credentials - is declared once in the root build instead of being copied per module. Only name, description and artifactId differ. The java-platform BOM deliberately does not match the java-library block, so it never gets a sources or javadoc jar. All modules inherit the single version from gradle.properties, including its "# x-release-please-version" marker, and are always released together.
Adds net.onelitefeather:minestom-extensions-processor, an annotation processor that writes the extension descriptor at compile time instead of leaving it to be maintained by hand. There is deliberately no entrypoint() element: the entrypoint is the annotated class itself, so it cannot drift away from a rename or a package move. It is written as the binary name rather than the canonical one, because ExtensionManager resolves it with Class.forName(), which needs com.example.Outer$Inner for a nested class. The module has no runtime dependencies at all - only javax.annotation .processing from the JDK. That is why NAME_REGEX is duplicated here rather than imported: depending on minestom-extensions would put Minestom on the processor path of every extension build. For the same reason the JSON is written by hand instead of with Gson. Non-ASCII characters are escaped as \uXXXX. The Filer encodes with javac's -encoding while the runtime reads with the JVM default charset, so a build that is not UTF-8 would otherwise turn an author name into mojibake, or drop characters it cannot represent. Validation that would otherwise only fail at server startup happens at compile time: the name against NAME_REGEX, a single entrypoint per compilation, that the class extends Extension (skipped when Extension is not on the classpath), and that repository URLs are http(s).
Nothing linked the names the processor writes to the fields the runtime reads. The processor deliberately does not depend on minestom-extensions, and Gson silently ignores JSON members it does not recognise - so renaming a field in DiscoveredExtension would break every extension at runtime while leaving the entire build green. extension-descriptor-contract.json is the exact output of the processor for a fully populated @ExtensionInfo. The processor module copies it in and asserts it still reproduces it byte for byte; the core module deserializes it into the real DiscoveredExtension and asserts every field arrives, including the nested externalDependencies structure. Verified by mutation: renaming the field and every use of it - which compiles cleanly - turns the core suite red instead of shipping a silently broken descriptor.
DependencyGetter is compiled from Kotlin and references
kotlin/jvm/internal/Intrinsics 48 times, but the Kotlin stdlib is nowhere
in its dependency tree. Any extension declaring externalDependencies
therefore died on the very first line of loadDependencies:
java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
at net.minestom.dependencies.maven.MavenRepository.<clinit>
And because that is an Error rather than an Exception, the catch in
loadDependencies never saw it: instead of a clean MISSING_DEPENDENCIES the
server start was torn down. The feature has been broken, not degraded.
Replaced with Maven Artifact Resolver - the resolver Maven itself uses,
and the same approach Paper takes for its plugin library loader. It also
gets the project off JitPack, drops the transitive footprint from 52
artifacts to 28 (the old chain pulled in jsoup, maven-invoker,
plexus-compiler-javac and an embedded Maven), and removes an unmaintained
2020 jar that still advertised jCenter.
The wiring goes through the deprecated MavenRepositorySystemUtils
.newServiceLocator() on purpose. The modern RepositorySystemSupplier
builds a resolver whose descriptor reader does not interpret POMs:
readArtifactDescriptor returns zero dependencies and transitive resolution
silently resolves to nothing. That is documented on the class.
The public API is unchanged - loadDependencies and addDependencyFile are
private, and MavenDependencyResolver is package-private.
Writing @externaldependency("com.google.guava:guava:33.4.0-jre") means the version lives in the source as well as in the build, and the two drift apart. The plugin lets the build state it once: dependencies { extensionLibrary("com.google.guava:guava:33.4.0-jre") } extensionLibrary is declaration-only - on no compile or runtime classpath, nothing from it is bundled. It exists purely to describe what the extension resolves for itself at startup, so those jars must not leak into a consumer's classpath. A coordinate without an explicit group and version fails the build: the server resolves from coordinates alone, with no access to the build's version catalog or platforms. The task writes to a file of its own rather than over the processor's copy, and the jar takes that one instead. Editing in place would make the task's own output its next input, and the consequence is worse than a missed up-to-date check: removing an extensionLibrary would leave the stale coordinate in the descriptor forever, because compileJava stays up to date and never regenerates the file the task reads. There is a test for exactly that, and one asserting the jar carries a single descriptor. Dropping the processor's copy from the jar uses eachFile rather than exclude(), because an exclude on the jar spec applies to every source including the replacement, leaving no descriptor at all. Anything already declared in @ExtensionInfo is kept, and wins on a clash.
The Maven counterpart to the Gradle plugin. The version may be left out
and is then taken from the project's dependencies, which is the point:
the coordinate is named once in the plugin config and versioned once in
<dependencies>, instead of being repeated in the annotation.
<externalDependency>org.apache.commons:commons-lang3</externalDependency>
Which dependencies to record is stated explicitly. Deriving them from the
provided scope was tried and rejected: everything a Minestom extension
compiles against is provided - Minestom, minestom-extensions, the
annotation processor itself - and an end-to-end run produced a descriptor
instructing the server to download the annotation processor.
Like the Gradle plugin, the goal must not merge into its own previous
output, or a coordinate removed from the POM would linger forever on any
build without clean. It keeps a pristine copy of what the processor wrote
beside the build output and works from that, refreshing it whenever the
class output differs from what the goal last wrote.
META-INF/maven/plugin.xml is maintained by hand: a Maven build would
generate it with maven-plugin-plugin, and the Gradle equivalent calls
ProjectDependency.getDependencyProject(), which Gradle 9 removed, with no
newer release. PluginDescriptorTest compares the descriptor against the
mojo's fields, types, goal and phase, because a drifting descriptor fails
in the user's build - Maven either reports a parameter as unknown or
silently never injects it.
The reusable workflow's default path filter anchors sources at `src/**`, which stopped matching anything once they moved into `minestom-extensions/src/` and the other module directories - so a change touching only sources would no longer trigger a PR build. Widened to `**/src/**`. `**/*.java` on its own is not enough: non-Java files under a module's src/ would still be missed, in particular minestom-extensions-processor's META-INF/services registration, which is the thing that makes the annotation processor discoverable at all.
Brings the README up to the multi-module layout: a table of the five artifacts, BOM-based install snippets for Gradle (Kotlin and Groovy) and Maven, a section on generating extension.json with @ExtensionInfo, and one on declaring runtime libraries from the build. Two of the install instructions were wrong and are fixed here, both reproduced against real consumer projects built from the snippets: The Gradle block did not resolve at all. `annotationProcessor` extends no other configuration, so a platform imported into `implementation` never reaches it and the versionless processor dependency had nothing to resolve against - "Could not find ...:minestom-extensions-processor:" with an empty version. It needs its own platform line. The Maven instructions were wrong for the Java version this project requires. Since JDK 23 javac no longer runs annotation processors found only on the compile classpath, so the documented provided-scope setup produced a green build and a jar with no descriptor in it, failing only at server startup with "Missing extension.json in extension <jar>". <annotationProcessorPaths> is now the documented configuration rather than an aside. Also notes that Extension#getLogger() needs slf4j-api on the consumer's compile classpath, since the core artifact only carries it at runtime, and warns against pointing extensions at repo1.maven.org directly, which the Maven Central terms of service do not allow for shipped software.
Documents what the build looks like now and, more importantly, the three places where a well-meant change quietly breaks something: - the packages under minestom-extensions/ are a public contract with CloudNet and must not be renamed, - MavenDependencyResolver's deprecated service-locator wiring is deliberate - the modern supplier silently resolves no transitive dependencies, - the build plugins write to a separate file on purpose, and the Maven plugin's descriptor is hand-maintained and covered by a test.
The Maven coordinates in the README are documentation that silently rots. Two custom managers keep them current: one for the group:artifact:version examples, one for the maven-compiler-plugin version in the Maven setup snippet. The first deliberately matches the generated-output example as well as the inputs, so the descriptor shown in the README cannot drift away from the annotation that is supposed to produce it. This project's own artifacts are excluded - they are versioned by release-please, and the README shows them as placeholders. Everything lands in one grouped docs PR rather than a stream of individual ones. Validated with renovate-config-validator; both patterns are RE2-safe, which matters because that is what Renovate uses in production.
This comment has been minimized.
This comment has been minimized.
Test results 18 files 18 suites 1m 35s ⏱️ Results for commit 884bc87. ♻️ This comment has been updated with latest results. |
The test compares the processor's output against the checked-in contract byte for byte, and the processor always writes LF. On Windows, Git's autocrlf hands out a CRLF working copy, so the comparison failed on line endings alone: ExtensionInfoProcessorTest > the generated descriptor matches the contract shared with minestom-extensions FAILED strip() only trims the ends, not the line endings in between. Fixed at the source with a .gitattributes that checks the repository out as LF everywhere, keeping CRLF only where Windows needs it (.bat/.cmd) and LF where sh needs it (gradlew). The test additionally normalises line endings, so it also survives a working copy created before this file existed, or an editor that rewrites the contract on save. What the contract is about is field names, order, indentation and escaping - not the checkout's line ending policy. Verified by converting the contract to CRLF and running the suite: red before, green after.
Both build plugins now record the project version, so the compiler
argument users had to wire up by hand is no longer needed:
tasks.withType<JavaCompile> {
options.compilerArgs.add("-Aminestom.extension.version=${project.version}")
}
The build wins over @ExtensionInfo(version = ...), which matches what the
existing -Aminestom.extension.version already did. Opt out with
useProjectVersion = false to keep the version in the annotation.
Setting it from the task rather than through a compiler argument is
deliberate: it also applies on an incremental build where compileJava is
up to date and the processor does not run at all.
The Gradle side ignores an unset project version. Gradle defaults it to
the string "unspecified", and writing that into a descriptor would replace
a perfectly good annotation value with a placeholder.
Verified end to end for Maven in both directions: with the compiler
argument removed, useProjectVersion=false keeps the annotation's 1.0.0
and true writes the POM's 2.5.0.
v2.2.0 through v2.4.0 only added workflows - docker-publish, gradle-docker-context and pr-lint - so nothing this repository calls changed behaviour. Checked against the workflows CHANGELOG. release-please stays inline for now: the reusable release-please.yml forwards none of the action's outputs, and the publish job is gated on `needs.release-please.outputs.release_created`. Switching over as-is would silently skip publishing on every release. OneLiteFeatherNET/workflows#21 adds those outputs; this repository can move once that is released.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Splits the project into five published modules, replaces the runtime dependency resolver, and adds optional build plugins for Gradle and Maven.
Modules
minestom-extensionsminestom-extensions-processor@ExtensionInfoand the annotation processor generatingextension.json.minestom-extensions-gradle-pluginminestom-extensions-maven-pluginminestom-extensions-bomCloudNet v4 compatibility
The core artifact is reproduced byte for byte. All five sources moved as pure renames (
R100), the generated POM and Gradle module metadata diff empty against a build ofmain, every jar entry and CRC matches, andjavapover the five public classes is identical.groupId,artifactId, version and dependency scopes are unchanged.externalDependencieswas broken, not just outdatedDependencyGetteris Kotlin-compiled and referenceskotlin/jvm/internal/Intrinsics48 times, but the Kotlin stdlib is nowhere in its dependency tree. Any extension declaringexternalDependenciesdied on the first line ofloadDependencies:That is an
Error, not anException, so thecatchinloadDependenciesnever saw it — instead of a cleanMISSING_DEPENDENCIES, the server start was torn down. This affects every release so far, independently of this PR.Replaced with Maven Artifact Resolver, the resolver Maven itself uses and the same approach Paper takes for its plugin library loader. Side effects: off JitPack, and the transitive footprint drops from 52 artifacts to 28 — the old chain pulled in jsoup, maven-invoker, plexus-compiler-javac and an embedded Maven.
One trap worth knowing: the modern
RepositorySystemSupplierbuilds a resolver whose descriptor reader does not interpret POMs.readArtifactDescriptorreturns zero dependencies and transitive resolution silently yields nothing. The deprecatedMavenRepositorySystemUtils.newServiceLocator()is therefore deliberate and documented on the class.Two documentation bugs fixed
Both reproduced against real consumer projects built from the README snippets:
annotationProcessorextends no other configuration, so a platform imported intoimplementationnever reaches it.provided-scope setup produced a green build with no descriptor in the jar, failing only at server startup.Notes for review
DescriptorContractTestpins the descriptor field names toDiscoveredExtension(nothing else links them — Gson ignores unknown members, so a rename would break every extension at runtime with a green build), and both plugins have a regression test for the removed-dependency case below.plugin.xmlis hand-maintained —maven-plugin-developmentcalls an API Gradle 9 removed and has no newer release.PluginDescriptorTestcompares it against the mojo so it cannot drift.provided; an early version produced a descriptor telling the server to download the annotation processor.Known issue, not addressed here
Maven consumers of
minestom-extensionsfail atmvn packageonshrinkwrap-resolver-depchain:jar:3.1.4.DependencyGetter:v1.0.1declares it without<type>pom</type>, so Maven looks for a jar that does not exist. This disappears with the resolver replacement, but is worth knowing for existing releases.