Split build-logic into four artifacts, align Adventure per target, command merge policy - #40
Merged
Merged
Conversation
The smoke-matrix artifact synthesiser hardcoded the MagicUtils bundle naming
(magicutils-<platform>-bundle jar path). Expose artifactPrefix and moduleSuffix
on the modrinth {} DSL so downstream consumers whose platform modules are named
plainly (e.g. commandflow-bukkit, not bukkit-bundle) can synthesise their
artifacts from the smoke matrix too, instead of hand-maintaining an artifact
list. Defaults keep MagicUtils behaviour unchanged.
MagicUtils exposes Adventure types in its public API, so the major has to match what the target's platforms actually run: 4.x through 1.21, 5.x on 26.x, where paper-api 26.2 imports adventure-bom 5.2.0 and adventure-platform-fabric 7.x bundles Adventure 5. Mixing the two majors on Fabric's shared class loader kills AdventureCommon.<clinit> with NoSuchMethodError (Buildable removal changed ComponentFlattener.toBuilder()'s descriptor) and takes every mod on that runtime down with it. magicUtilsAlignAdventure rewrites only the major, never the minor: within a major Adventure is compatible, and pinning down breaks the platform that asks for a newer one - paper-api 1.21.11 imports adventure-bom 4.26.1 and its signatures reference PlayerHeadObjectContents, absent from the catalog's 4.24.0. Requests already on the target's major are left to Gradle's highest-wins. Bungee and Velocity drop off the 26.x targets: unlike the other platforms they do not track the Minecraft version (bungeecord-api 1.20, velocity-api 3.1.1 are pinned) and still run Adventure 4, so building them there would compile the inlined shared modules against Adventure 5 for a proxy that provides 4. Their artifacts keep coming from the 1.21 targets and work with any server behind them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
…e tasks `./gradlew -p build-logic build` failed validatePlugins: nine task classes had neither @CacheableTask nor @DisableCachingByDefault. They are all side-effecting - git operations, HTTP uploads, a version bump, or launching a real server and reading its log - so none of them is a cacheable transformation. Annotating them states that explicitly and gets the full build-logic build passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
A NeoForge mod on 1.21.1 was forced to resolve Fabric Loom, which pins a far
newer Gradle than such a build can use. compileOnly cannot fix this: every
platform plugin is applied from code (pluginManager.apply(id)), which resolves
through the applying plugin's own *runtime* classpath - so a toolchain has to be
a real implementation dependency of whichever plugin needs it, and the only way
to keep it off everyone else is to split the artifact.
magicutils-build-logic matrix/target/publish/release/smoke, module
and bundle plugins, consumer-common (no toolchain)
magicutils-build-logic-fabric fabric-module, fabric-bundle, consumer-fabric (Loom)
magicutils-build-logic-neoforge neoforge-bundle, consumer-neoforge (ModDevGradle)
magicutils-build-logic-jvm consumer-bukkit/velocity/bungee (jpenilla run-*)
Nothing changes for consumers - they still apply plugins by id, and each marker
points at the right artifact. Verified on a real resolve from mavenLocal:
consumer-neoforge pulls moddev and zero Loom, consumer-bukkit pulls run-task and
zero Loom, consumer-fabric pulls Loom and neither of the others, consumer-common
pulls no platform toolchain at all; all 22 markers resolve to the intended
artifact.
Fallout of the split, handled here:
- MagicUtilsDevServer was one object for all platforms; the four functions that
need jpenilla's task types moved to :jvm. Fabric and NeoForge stay in the core
since their tasks are matched by name.
- internal does not cross Gradle module boundaries, so the affected declarations
are now public, each with a comment saying why.
- DEFAULT_BUNGEE_API/DEFAULT_VELOCITY_API lived on the consumer plugins but the
bundles need them too; they moved to MagicUtilsPlatformApiDefaults in the core.
- applyMinecraftAndMappings needs Loom's API on the compile classpath, so it
moved out of the neutral MagicUtilsTargetConventions into :fabric.
- publish/publishToMavenLocal are per-project, so `-p build-logic publish` would
have shipped the core alone and left magicutils.consumer-fabric unresolvable;
the root now aggregates the subprojects.
- the subprojects' artifacts were named after the bare project ("fabric"), so
archivesName/artifactId are set explicitly, leaving marker publications alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
`:fabric-bundle:build` configured every platform in the workspace: the task name was canonicalised to `build` and matched the full-graph shortcut before anything looked at the project path, so a request the user had already scoped widened back out - configuring bukkit-bundle and the proxies for a Fabric-only build. A task name containing a project path is now handled first and scopes to that project's platform whatever the task is called; `:build` on the root is not project-qualified and still means the whole graph. A qualified task on a project that belongs to no platform (a common module, or build-logic itself) resolves to the common projects rather than falling back to everything. MagicUtilsTaskSelection and inferTaskSelection become internal so the new MagicUtilsTaskSelectionTest can exercise the inference directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
Two plugins contributing to the same root (a shared /nox) could silently clobber each other's execute depending on load order. MergePolicy makes ownership of a node's execute explicit - CONTRIBUTE claims it only if still free (the default, so merging is safe), OVERRIDE takes it and warns naming both contributors, EXTEND only adds sub-commands under a node another plugin owns. Leaf sub-commands always accumulate; the policy governs the handler that runs when a player stops at a node. Declared per node via CommandInfo#merge / SubCommand#merge and applied by CommandRegistry#contributeSubCommands. Also here: - nested sub-command paths and the argument context passed to suggestion providers, each with its own test (NestedPathTest, SuggestContextArgsTest). - MagicSender#teleport: a platform-agnostic, async-by-contract move, since a synchronous teleport throws on any thread on Folia/Canvas. Defaults to a no-op completing false for senders that cannot move; Bukkit overrides it. - ListTypeParser now trims and drops empty entries when splitting a list literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
BukkitThreading only knew the global and async schedulers, so anything touching a specific location had no correct path on Folia, where a synchronous teleport throws on any thread and region-owned state may only be touched from that region's thread. It now reflects the region scheduler as well: runRegion, runRegionDelayed, isOwnedByCurrentRegion, plus runGlobalRepeating/runGlobalDelayed and an async teleport returning a CompletableFuture (the implementation behind MagicSender#teleport). Everything is resolved reflectively at class-init and falls back to the Bukkit scheduler, so a Paper server behaves as before. MagicUtilsDuplicationDetector reports the new entry points, and the reflection allowlist follows the moved line numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
MsgFmt decided an even-length Object[] of Strings was a {key, value, ...} map, so
send("{action}: {reason}", "create 'x'", "boom") formatted as a map with
"create 'x'" as a key instead of filling the two placeholders. Even length with
String keys is necessary but not sufficient: every key position must now also
name a placeholder that actually appears in the template, otherwise the args are
positional. MsgFmtPositionalTest covers both the numeric and named forms plus the
explicit key/value case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
The plugins are versioned independently of the library. 0.1.10 is the release of the four-artifact split, dropped from -SNAPSHOT because the Reposilite releases repository takes no snapshots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
…abric MagicUtilsFabricLoomConventions.kt was never added: .gitignore ignores `target/` for Maven and un-ignored only the one build-logic source package that happens to be named target/, by its pre-split path. The same package under build-logic/fabric matched the Maven rule instead, so git silently skipped the file. Locally the build stayed green on stale incremental output; CI's `clean` exposed it as three Unresolved reference 'applyMinecraftAndMappings' errors. The un-ignore is now path-independent, so the per-platform artifacts get the same treatment as the root one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
Keying the Adventure major on isDeobfuscated conflated two cutovers one minor apart: 26.1 is deobfuscated, but paper-api 26.1.1 still imports adventure-bom 4.26.1. Forcing 5.2.0 there put Adventure 5 next to a platform API compiled against 4, and CommandRegistryIntegrationTest died with NoClassDefFoundError as soon as it proxied CommandSender (which extends Audience) - the mc2611 CI target. usesAdventure5 now keys on the library Minecraft being >= 26.2, where Paper and adventure-platform-fabric 7.x actually moved. isDeobfuscated keeps its own meaning (no remapping, `jar` over `remapJar`) everywhere else. Verified per target: mc2611 -> 4.26.1, mc262 -> 5.2.0, mc12110 -> 4.26.1, with buildScenario + checkScenario green on both 26.x targets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bt7RgUQ8kNZGgF4DBBUpj
THEROER
added a commit
that referenced
this pull request
Jul 31, 2026
Split build-logic into four artifacts, align Adventure per target, command merge policy
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.
What
Nine commits, grouped by topic.
Build
feat(build): align the Adventure major to each target— 4.x through 1.21, 5.x on 26.x. Only the major is forced, never the minor: paper-api 1.21.11 imports adventure-bom 4.26.1 and its signatures referencePlayerHeadObjectContents, so pinning down to the catalog's 4.24.0 broke:platform-bukkit:compileJava. Bungee/Velocity drop off the 26.x targets — they don't track the Minecraft version and still run Adventure 4.refactor(build-logic): split into four publishable plugin artifacts— a NeoForge mod on 1.21.1 no longer resolves Fabric Loom. Platform plugins are applied from code, which reads the applying plugin's runtime classpath, socompileOnlycannot isolate a toolchain; only a per-artifact split can.fix(build-logic): scope matrix configuration to a project-qualified task—:fabric-bundle:buildwas configuring every platform.chore(build-logic): declare caching behaviour on the release and smoke tasks—-p build-logic buildfailed validatePlugins.Library
feat(commands): merge policy for shared command roots—MergePolicy(CONTRIBUTE/OVERRIDE/EXTEND) makes execute ownership explicit when several plugins contribute to one root; plus nested paths, suggestion-context args,MagicSender#teleport.feat(platform-bukkit): Folia region scheduling and async teleport— region scheduler reflected at class-init, falls back to the Bukkit scheduler on Paper.fix(platform-api): stop treating positional args as a key/value map—send("{action}: {reason}", "create 'x'", "boom")formatted as a map.Release
chore(release): version 1.27.4, build-logic plugins 0.1.10.Artifact split
magicutils-build-logicconsumer-commonmagicutils-build-logic-fabricfabric-module,fabric-bundle,consumer-fabricmagicutils-build-logic-neoforgeneoforge-bundle,consumer-neoforgemagicutils-build-logic-jvmconsumer-bukkit,consumer-velocity,consumer-bungeeNothing changes for consumers: they still apply plugins by id, and every marker points at the right artifact.
Verification
consumer-neoforge→ ModDev, zero Loom;consumer-bukkit→ run-task, zero Loom;consumer-fabric→ Loom + 20 transitives, no ModDev/run-task;consumer-common→ no platform toolchain. All 22 marker POMs resolve to the intended artifact.:neoforge-bundlegetsproject ':build-logic:neoforge'and no Loom;:fabric-bundlegets Loom;:coregets neither../gradlew testgreen;./gradlew -p build-logic buildgreen;-Ptarget=mc262 :fabric-bundle:build :bukkit-bundle:buildgreen.