Skip to content

Split build-logic into four artifacts, align Adventure per target, command merge policy - #40

Merged
THEROER merged 11 commits into
mainfrom
feat/configurable-modrinth-synthesis
Jul 31, 2026
Merged

Split build-logic into four artifacts, align Adventure per target, command merge policy#40
THEROER merged 11 commits into
mainfrom
feat/configurable-modrinth-synthesis

Conversation

@THEROER

@THEROER THEROER commented Jul 31, 2026

Copy link
Copy Markdown
Owner

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 reference PlayerHeadObjectContents, 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, so compileOnly cannot isolate a toolchain; only a per-artifact split can.
  • fix(build-logic): scope matrix configuration to a project-qualified task:fabric-bundle:build was configuring every platform.
  • chore(build-logic): declare caching behaviour on the release and smoke tasks-p build-logic build failed validatePlugins.

Library

  • feat(commands): merge policy for shared command rootsMergePolicy (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 mapsend("{action}: {reason}", "create 'x'", "boom") formatted as a map.

Release

  • chore(release): version 1.27.4, build-logic plugins 0.1.10.

Artifact split

Artifact Carries Extra toolchain
magicutils-build-logic matrix/target/publish/release/smoke, module + bundle plugins, consumer-common none
magicutils-build-logic-fabric fabric-module, fabric-bundle, consumer-fabric Fabric Loom
magicutils-build-logic-neoforge neoforge-bundle, consumer-neoforge ModDevGradle
magicutils-build-logic-jvm consumer-bukkit, consumer-velocity, consumer-bungee jpenilla run-*

Nothing changes for consumers: they still apply plugins by id, and every marker points at the right artifact.

Verification

  • Isolation checked on a real resolve from mavenLocal, not just by construction: 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.
  • Same result inside the composite build: :neoforge-bundle gets project ':build-logic:neoforge' and no Loom; :fabric-bundle gets Loom; :core gets neither.
  • ./gradlew test green; ./gradlew -p build-logic build green; -Ptarget=mc262 :fabric-bundle:build :bukkit-bundle:build green.
  • Adventure resolves to 5.2.0 on mc262 and 4.26.1 on mc12110.

THEROER and others added 11 commits July 11, 2026 17:24
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
THEROER merged commit a37e1a9 into main Jul 31, 2026
8 checks passed
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
@THEROER
THEROER deleted the feat/configurable-modrinth-synthesis branch July 31, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant