From 93ac9738d686a92eaaee8619aae88407c87527e9 Mon Sep 17 00:00:00 2001 From: owengregson Date: Wed, 8 Jul 2026 20:47:18 -0700 Subject: [PATCH 1/2] =?UTF-8?q?build(deps):=20modernize=20the=20whole=20sh?= =?UTF-8?q?aded=20stack=20=E2=80=94=20DBCP2,=20Adventure=205,=20mysql=209,?= =?UTF-8?q?=20H2=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship the newest version of every shaded dependency and let the jvmdg pipeline (that already downgrades our own bytecode) downgrade it too, with a build-high variant tweak for deps that declare a high JVM floor. First-party v52/v57/v61 tiers untouched (options.release stays 17); the only new tier is versions/21 for the relocated Adventure-5 originals. - HikariCP 4.0.3 → Apache Commons DBCP2 2.14.0 (latest). DBCP2's newest release is Java-8-native, so it drops into the v52 base tier with no jvmdg tricks — REMOVES the HikariCP pin. StorageBoot swaps HikariConfig/HikariDataSource → BasicDataSource (maxTotal, setUrl, close()-quietly). Its optional slf4j/cglib bridges and the JTA-managed datasource are guarded/dead: strip the managed package + servlet-container cleaner (javax/ HARD), ignore org/slf4j+net/sf/cglib. - slf4j-nop pin REMOVED — DBCP2 logs via bundled commons-logging. - Adventure 4.26.1 → 5.2.0 (build-high): 5.x pins jvm.version=21; raise the consumer TargetJvmVersion to 21 (variant selection only), jvmdg downgrades its v65 bytecode to v52 and keeps v65 originals under versions/21. Zero .java change. verifyDowngrade extended to accept the versions/21 modern-dep tier (rejects any first-party leak or ≤v61 class there). - mysql-connector-j 8.0.33 → 9.7.0 (io/opentelemetry guarded-optional ignored). - H2 1.4.200 → 2.2.224 — the latest Java-8-native H2 line (2.3.x needs Java 11 and jvmdg can't resolve its excluded tools/Server on downgrade). Schema + MODE=MySQL clears 2.x reserved words with no NON_KEYWORDS (full-DDL storage tests pass). Beta: fresh DB, no 1.4.200 on-disk migration. Full clean build + all 7 gates + full test suite green. Jar 10.68 → 11.73 MB. Co-Authored-By: Claude Fable 5 --- build.gradle.kts | 16 ++++ core/build.gradle.kts | 76 ++++++++++++++----- .../factions/core/storage/H2Dialect.java | 11 +-- .../factions/core/storage/StorageBoot.java | 47 +++++++----- docs/design/per-version-dependencies.md | 36 +++++++-- gradle/libs.versions.toml | 45 ++++++----- 6 files changed, 161 insertions(+), 70 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f1a172c..20ea10c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,22 @@ subprojects { options.compilerArgs.add("-parameters") } + // BUILD-HIGH dependency consumption (does NOT change first-party bytecode). A modern + // shaded dep may publish Gradle Module Metadata pinning a high runtime floor via + // org.gradle.jvm.version — Adventure 5.x declares 21 — and Gradle's variant selection + // would then REJECT it against a release-17 consumer. We build on JDK 25 and hand every + // shaded dep to JVMDowngrader, which lowers its bytecode to the v52 base tier (keeping the + // pristine original under META-INF/versions/ for JVMs that can run it), so the consumer + // can honestly claim JVM-21 capability for variant SELECTION. This raises ONLY the + // TargetJvmVersion attribute on resolvable classpaths; options.release stays 17, so + // first-party bytecode remains v61 and the v52/v57/v61 tier model is untouched. The dep's + // own high-Java bytecode is what jvmdg downgrades — we never down-version the dependency. + configurations.matching { it.isCanBeResolved }.configureEach { + attributes { + attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21) + } + } + tasks.withType().configureEach { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 88fe96a..653c105 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -100,8 +100,8 @@ dependencies { implementation(libs.adventure.api) { exclude(group = "org.jetbrains", module = "annotations") } implementation(libs.adventure.minimessage) { exclude(group = "org.jetbrains", module = "annotations") } implementation(libs.adventure.serializer.legacy) { exclude(group = "org.jetbrains", module = "annotations") } - implementation(libs.hikaricp) - implementation(libs.slf4j.nop) // provides the relocated slf4j binder (see catalog note) + implementation(libs.commons.dbcp2) // connection pool (Java-8-native latest; pulls commons-pool2 + commons-logging) + implementation(libs.commons.pool2) implementation(libs.h2) implementation(libs.mysql) implementation(libs.bstats.bukkit) @@ -136,11 +136,12 @@ tasks.shadowJar { relocate("net.kyori", "dev.fablemc.factions.lib.adventure") relocate("org.bstats", "dev.fablemc.factions.lib.bstats") - relocate("com.zaxxer.hikari", "dev.fablemc.factions.lib.hikari") + relocate("org.apache.commons.dbcp2", "dev.fablemc.factions.lib.dbcp2") + relocate("org.apache.commons.pool2", "dev.fablemc.factions.lib.pool2") + // DBCP2/pool2 log via commons-logging — shade it too (no slf4j binding needed). + relocate("org.apache.commons.logging", "dev.fablemc.factions.lib.commonslogging") relocate("org.h2", "dev.fablemc.factions.lib.h2") relocate("com.mysql", "dev.fablemc.factions.lib.mysql") - // HikariCP 4.x transitively needs slf4j-api — shade it too (AM-10). - relocate("org.slf4j", "dev.fablemc.factions.lib.slf4j") // AM-10: the relocated JDBC drivers use explicit driverClassName; drop the service // file, and strip third-party Multi-Release trees before the downgrade input. @@ -176,10 +177,18 @@ tasks.shadowJar { exclude("**/h2/tools/Shell*") // references server/web ConnectionInfo exclude("**/h2/util/DbDriverActivator*") // org.osgi BundleActivator exclude("**/h2/util/OsgiDataSourceFactory*") // org.osgi.service.jdbc - exclude("**/hikari/metrics/prometheus/**") // io.prometheus (not linked by core) - exclude("**/hikari/hibernate/**") // org.hibernate ConnectionProvider - exclude("**/hikari/util/JavassistProxyFactory*") // org.javassist (build-time proxy gen) exclude("**/mysql/cj/jdbc/integration/**") // com.mchange c3p0 ConnectionTester + // commons-logging's servlet-container cleanup listener implements javax.servlet.* — javax/ is + // an un-ignorable HARD verifyJdk8Api tier (route 1, like the H2 web console), and Paper is not + // a servlet container, so strip it. (Its optional slf4j/cglib bridges stay bundled but dead — + // route 2, ignored below — since org/slf4j and net/sf/cglib ARE ignorable external optionals.) + // NB shadow filters on the SOURCE path, so match the un-relocated org/apache/commons/logging. + exclude("**/commons/logging/impl/ServletContextCleaner*") + // DBCP2's optional MANAGED (XA/JTA) datasource pulls javax.transaction, which in turn references + // javax.enterprise/interceptor (CDI) — un-ignorable javax/ HARD packages. We use the plain + // BasicDataSource, never the managed pool, so strip that package and the bundled JTA API. + exclude("**/dbcp2/managed/**") + exclude("javax/transaction/**") // AuthenticationOciClient stays BUNDLED (route 2): it names only com.oracle.bmc // externally, which verifyJdk8Api ignores as a guarded-optional SDK (never linked on the // native/basic-auth path). Excluding it would dangle NativeAuthenticationProvider's ref. @@ -476,7 +485,8 @@ abstract class VerifyRelocationTask : DefaultTask() { @TaskAction fun run() { val libPrefix = "dev/fablemc/factions/lib/" - val tokens = listOf("net/kyori", "com/zaxxer", "org/h2", "com/mysql", "org/slf4j") + val tokens = listOf("net/kyori", "org/apache/commons/dbcp2", "org/apache/commons/pool2", + "org/apache/commons/logging", "org/h2", "com/mysql") val entryViolations = mutableListOf() val refViolations = mutableListOf() ZipFile(jarFile.get().asFile).use { zip -> @@ -538,6 +548,7 @@ abstract class VerifyDowngradeTask : DefaultTask() { val baseBytesByLogical = mutableMapOf() val allEntryNames = hashSetOf() val v13JvmdgRefs = sortedSetOf() + var v21DepClasses = 0 JarFile(file).use { jar -> val mr = jar.manifest?.mainAttributes?.getValue("Multi-Release") @@ -574,8 +585,24 @@ abstract class VerifyDowngradeTask : DefaultTask() { v13JvmdgRefs.addAll(MegaJarScan.tokensWithPrefix(text, jvmdgRuntime)) if (name == "META-INF/versions/13/$sentinel") v13SentinelMajor = major } + name.startsWith("META-INF/versions/21/") -> { + // Modern-dependency tier (build-high). jvmdg keeps a shaded dep's ORIGINAL + // post-v61 bytecode here when the dep was built on a newer JDK than the + // first-party v61 floor (Adventure 5.x is Java-21 / v65): only 21+ JVMs load + // it, everyone else runs the v52 base copy jvmdg produced. Valid ONLY if it + // carries relocated third-party (lib.*) classes and NO first-party (which must + // stay in the 13/17 tiers), with genuinely post-v61 bytecode — assert that + // shape rather than blanket-accepting versions/21, so a real leak still fails. + if (MegaJarScan.isFirstParty(logical)) { + problems.add("versions/21 carries first-party class $logical (first-party must stay in versions/13-17)") + } else if (major <= 61) { + problems.add("versions/21 dep class $logical is v$major (not a post-v61 modern-dependency tier)") + } else { + v21DepClasses++ + } + } name.startsWith("META-INF/versions/") -> - problems.add("unexpected versioned tier entry $name (only versions/13 and versions/17 are expected)") + problems.add("unexpected versioned tier entry $name (only versions/13, versions/17 and the versions/21 modern-dep tier are expected)") else -> { if (major > 52) problems.add("base entry $logical is v$major (>52)") if (MegaJarScan.isFirstParty(logical)) { @@ -623,7 +650,8 @@ abstract class VerifyDowngradeTask : DefaultTask() { problems.take(30).joinToString("\n") { " - $it" }) report.get().asFile.writeText("ok\n") logger.lifecycle("[verifyDowngrade] OK — base ≤ v52; ${v13FirstParty.size} first-party class(es) forked to v57 under versions/13; " + - "${v17FirstParty.size} to v61 under versions/17; sentinel forked 52/57/61; no reflective-record token; versions/13 jvmdg refs resolve.") + "${v17FirstParty.size} to v61 under versions/17; sentinel forked 52/57/61; no reflective-record token; versions/13 jvmdg refs resolve" + + (if (v21DepClasses > 0) "; $v21DepClasses post-v61 modern-dep class(es) under versions/21 (build-high dep originals)." else ".")) } } @@ -922,14 +950,22 @@ val serverProvidedIgnores = listOf( "net/md_5", "net/milkbowl", "com/sk89q", "me/clip", "org/dynmap", "com/earth2me", "github/scarsz", "com/griefcraft", "org/apache", "org/yaml", // Guarded-optional third-party integrations HARD-referenced by BUNDLED classes of the - // shaded storage libs, but never LINKED by the embedded-DB/JDBC path: HikariPool casts - // to the Dropwizard/Micrometer metric registries only when one is configured - // (string-guarded); H2's ValueGeometry touches JTS only when the optional JTS lib is - // present; the MySQL OCI auth plugin (AuthenticationOciClient) reads com.oracle.bmc only - // when authenticationPlugins names it. All external SDKs never on our path, so never a - // runtime miss — the Mental com/viaversion precedent (a guarded optional integration - // ignored rather than bundled). The allowlist stays EMPTY. - "com/codahale", "io/micrometer", "org/locationtech", "com/oracle", + // shaded storage libs, but never LINKED by the embedded-DB/JDBC path: H2's ValueGeometry + // touches JTS only when the optional JTS lib is present; the MySQL OCI auth plugin + // (AuthenticationOciClient) reads com.oracle.bmc only when authenticationPlugins names it; + // the MySQL OpenTelemetry handler (cj.otel, new in connector-j 9.x) touches io.opentelemetry + // only when its -guarded Class.forName("io.opentelemetry.api.GlobalOpenTelemetry") + // succeeds — absent, NativeSession falls back to NoopTelemetryHandler and the handler's + // io.opentelemetry-typed members never link. All external SDKs never on our path, so never a + // runtime miss — the Mental com/viaversion precedent (a guarded optional integration ignored + // rather than bundled). The allowlist stays EMPTY. (DBCP2/pool2 carry no such optional SDK ref.) + "org/locationtech", "com/oracle", "io/opentelemetry", + // commons-logging bundles optional bridges to slf4j (Slf4jLogFactory) and commons-pool2 an + // optional cglib proxy source — both guarded (commons-logging discovers a Log impl in a + // try/catch and we ship no slf4j; pool2 defaults to JDK proxies and we ship no cglib), so the + // classes are dead and their org/slf4j + net/sf/cglib references never link. Ignorable external + // optionals (route 2), same as the OCI/OTel SDKs above. + "org/slf4j", "net/sf/cglib", ) // A relocated FIRST-PARTY class we were FORCED to strip from the shaded jar (route (1) in @@ -946,7 +982,7 @@ val gateReports = layout.buildDirectory.dir("verify-gates") val verifyRelocation = tasks.register("verifyRelocation") { group = "verification" - description = "Fails if net/kyori, com/zaxxer, org/h2, com/mysql or org/slf4j survives outside the lib prefix." + description = "Fails if net/kyori, org/apache/commons/{dbcp2,pool2,logging}, org/h2 or com/mysql survives outside the lib prefix." jarFile.set(canonicalJar) report.set(gateReports.map { it.file("relocation.txt") }) } diff --git a/core/src/main/java/dev/fablemc/factions/core/storage/H2Dialect.java b/core/src/main/java/dev/fablemc/factions/core/storage/H2Dialect.java index 977e024..46dee10 100644 --- a/core/src/main/java/dev/fablemc/factions/core/storage/H2Dialect.java +++ b/core/src/main/java/dev/fablemc/factions/core/storage/H2Dialect.java @@ -1,10 +1,11 @@ package dev.fablemc.factions.core.storage; /** - * The H2 backend dialect (AM-10). Upserts rewrite to H2's {@code MERGE INTO … KEY(…)} form, - * which — unlike {@code ON DUPLICATE KEY UPDATE} — H2 1.4.200 implements natively. The JDBC URL - * uses {@code MODE=MySQL;DB_CLOSE_DELAY=-1} and pool size 1 (single-writer file DB); it does - * NOT use {@code NON_KEYWORDS} (that flag is H2 2.x-only — AM-10). + * The H2 backend dialect. Upserts rewrite to H2's {@code MERGE INTO … KEY(…)} form, which — unlike + * {@code ON DUPLICATE KEY UPDATE} — H2 implements natively. The JDBC URL uses + * {@code MODE=MySQL;DB_CLOSE_DELAY=-1} and pool size 1 (single-writer file DB). Backed by H2 + * 2.2.224 (the latest Java-8-native line); the schema + MySQL mode clears 2.x's stricter reserved + * words without needing the {@code NON_KEYWORDS} flag (the full-DDL storage tests confirm it). * *

Owning thread(s): stateless. Mutability: immutable singleton. */ @@ -35,7 +36,7 @@ public String driverClassName() { /** * Builds the H2 file-DB URL: {@code jdbc:h2:file:;MODE=MySQL;DB_CLOSE_DELAY=-1}. No - * {@code NON_KEYWORDS} flag (AM-10 — that is H2 2.x-only and would fail on the pinned 1.4.200). + * {@code NON_KEYWORDS} flag is needed — the schema clears H2 2.x's reserved words in MySQL mode. */ public static String fileUrl(String absolutePath) { return "jdbc:h2:file:" + absolutePath + ";MODE=MySQL;DB_CLOSE_DELAY=-1"; diff --git a/core/src/main/java/dev/fablemc/factions/core/storage/StorageBoot.java b/core/src/main/java/dev/fablemc/factions/core/storage/StorageBoot.java index 788a6a2..64754c3 100644 --- a/core/src/main/java/dev/fablemc/factions/core/storage/StorageBoot.java +++ b/core/src/main/java/dev/fablemc/factions/core/storage/StorageBoot.java @@ -14,8 +14,7 @@ import java.util.function.ToIntFunction; import java.util.logging.Logger; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.dbcp2.BasicDataSource; import dev.fablemc.factions.core.storage.load.BaselineLoader; import dev.fablemc.factions.kernel.config.ConfigImage; @@ -39,14 +38,14 @@ */ public final class StorageBoot implements AutoCloseable { - private final HikariDataSource dataSource; + private final BasicDataSource dataSource; private final SqlDialect dialect; private final String backendLabel; private final AdvisoryLock lock; private final StorageProjector projector; private final BaselineLoader loader; - private StorageBoot(HikariDataSource dataSource, SqlDialect dialect, String backendLabel, + private StorageBoot(BasicDataSource dataSource, SqlDialect dialect, String backendLabel, AdvisoryLock lock, StorageProjector projector, BaselineLoader loader) { this.dataSource = dataSource; this.dialect = dialect; @@ -95,7 +94,7 @@ public static StorageBoot open(StorageConfigView view, String mysqlPassword, Fil String label = mysql ? "MySQL" : "H2"; String dbKey = mysql ? view.mysqlDatabase() : view.h2File(); - HikariDataSource ds = buildPool(view, dialect, mysqlPassword, dataFolder, mysql); + BasicDataSource ds = buildPool(view, dialect, mysqlPassword, dataFolder, mysql); AdvisoryLock lock = null; try { try (Connection conn = ds.getConnection()) { @@ -111,11 +110,11 @@ public static StorageBoot open(StorageConfigView view, String mysqlPassword, Fil return new StorageBoot(ds, dialect, label, lock, projector, loader); } catch (SQLException ex) { releaseLock(lock); - ds.close(); + closeQuietly(ds); throw new StorageException("storage boot failed", ex); } catch (RuntimeException ex) { releaseLock(lock); // finding #17: a post-lock failure must release the lock, not leak it - ds.close(); + closeQuietly(ds); throw ex; } } @@ -237,25 +236,35 @@ public void close() { try { lock.close(); } finally { - dataSource.close(); + closeQuietly(dataSource); } } - private static HikariDataSource buildPool(StorageConfigView view, SqlDialect dialect, + private static BasicDataSource buildPool(StorageConfigView view, SqlDialect dialect, String mysqlPassword, File dataFolder, boolean mysql) { - HikariConfig cfg = new HikariConfig(); - cfg.setPoolName("fable-storage-pool"); - cfg.setDriverClassName(dialect.driverClassName()); + BasicDataSource ds = new BasicDataSource(); + // Explicit relocated driver class name (AM-10): the shaded jar excludes the JDBC service + // file, so the pool must be told which driver to load rather than discover it. + ds.setDriverClassName(dialect.driverClassName()); if (mysql) { - cfg.setJdbcUrl(MySqlDialect.url(view.mysqlHost(), view.mysqlPort(), view.mysqlDatabase())); - cfg.setUsername(view.mysqlUsername()); - cfg.setPassword(mysqlPassword == null ? "" : mysqlPassword); - cfg.setMaximumPoolSize(Math.max(1, view.mysqlPoolSize())); + ds.setUrl(MySqlDialect.url(view.mysqlHost(), view.mysqlPort(), view.mysqlDatabase())); + ds.setUsername(view.mysqlUsername()); + ds.setPassword(mysqlPassword == null ? "" : mysqlPassword); + ds.setMaxTotal(Math.max(1, view.mysqlPoolSize())); } else { - cfg.setJdbcUrl(h2Url(view.h2File(), dataFolder)); - cfg.setMaximumPoolSize(1); // single-writer file/mem DB (AM-10) + ds.setUrl(h2Url(view.h2File(), dataFolder)); + ds.setMaxTotal(1); // single-writer file/mem DB (AM-10) + } + return ds; + } + + /** Closes a DBCP2 pool, swallowing the checked close exception (best-effort cleanup path). */ + private static void closeQuietly(BasicDataSource ds) { + try { + ds.close(); + } catch (SQLException ignored) { + // boot-failure / shutdown cleanup: a pool that won't close cleanly is dropped anyway } - return new HikariDataSource(cfg); } /** Builds the H2 JDBC URL: a {@code mem:} handle passes through; otherwise a file DB under the data folder. */ diff --git a/docs/design/per-version-dependencies.md b/docs/design/per-version-dependencies.md index f5c9f17..9bca7c4 100644 --- a/docs/design/per-version-dependencies.md +++ b/docs/design/per-version-dependencies.md @@ -1,13 +1,33 @@ -# Evaluation — per-version dependency bundling +# Dependency modernization — what shipped and why + +**Status: DONE (v1 beta.3).** Wave 5 asked whether the mega-jar could run newer dependencies +across the whole 1.7.10→26.x range. The original question — *bundle a different version per +Minecraft/Java version* — is unsound (see "Why naïve Multi-Release override does NOT work" +below: MR overrides can't version-swap a library's class graph). But the project owner's +reframing works and is now **implemented**: ship a **single, newest** version of each dependency +and let the same jvmdg pipeline that downgrades our own bytecode downgrade *it* too — plus a +`build-high` variant-selection tweak for deps that publish a high `org.gradle.jvm.version`. + +## Shipped dependency set (beta.3) + +| Dependency | was | now | how | +|---|---|---|---| +| Connection pool | HikariCP 4.0.3 (pinned) | **Apache Commons DBCP2 2.14.0** (latest) | migrated (`StorageBoot`): DBCP2's newest release is Java-8-native, so it drops into the v52 base tier with no jvmdg tricks — **removes the HikariCP pin** | +| Logging binder | slf4j-nop 1.7.30 (pinned) | **gone** | DBCP2 logs via commons-logging (bundled) — **removes the slf4j pin** | +| Text | Adventure 4.x | **Adventure 5.2.0** (latest) | build-high: 5.x declares jvm.version=21; raise the consumer TargetJvmVersion to 21, jvmdg downgrades its v65 bytecode to v52 and keeps the v65 originals under `META-INF/versions/21`. Zero `.java` change | +| MySQL driver | 8.0.33 | **9.7.0** (latest) | consumed as-is; the optional OpenTelemetry handler is ``-guarded and ignored | +| Embedded DB | H2 1.4.200 (pinned) | **H2 2.2.224** | the latest **Java-8-native** H2 line (2.3.x needs Java 11 and jvmdg can't resolve its excluded `tools/Server` on downgrade). Our schema + MODE=MySQL clears 2.x's reserved words with no `NON_KEYWORDS`. Beta: no on-disk migration from the 1.4.200 format — a fresh DB is created | +| bStats | 3.2.1 | 3.2.1 | already latest | + +First-party v52/v57/v61 tiers are untouched (`options.release` stays 17); the only new tier is +`versions/21`, carrying the relocated Adventure-5 originals for Java-21+ JVMs. Jar 10.68 → 11.73 MB. +Full build + all 7 gates + tests green. The single remaining hold is H2 at 2.2.224 rather than +2.3.x — a principled Java-8-floor hold, the H2 analog of "use the newest that stays on the base +tier," exactly like DBCP2. -**Status: DECISION (v1 beta).** Question raised in wave 5: can the mega-jar carry -*different dependency versions per Minecraft/Java version* — newest deps for modern -servers, last-supported deps for legacy — the way it already carries per-version -**first-party** bytecode (v52 / v57 / v61 tiers)? +--- -**Verdict: not for v1. Keep the single pinned Java-8-line stack (AM-10). The mechanism -that would make it safe is documented below as a post-v1 option, to be revisited only when -a *concrete* modern-only dependency win is identified.** +## Appendix — why the *per-version bundling* framing was rejected --- diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97d5924..ba16f76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,18 +12,30 @@ paper-folia = "1.20.4-R0.1-SNAPSHOT" paper-modern = "1.20.6-R0.1-SNAPSHOT" # Shaded + relocated into the FableFactions jar (dev.fablemc.factions.lib.adventure) -# so a self-contained Adventure — the legacy-string serializer especially — is -# present below Paper 1.16.5 where net.kyori is absent from the server. If -# verifyJdk8Api later flags an adventure class as using a post-Java-8 API, drop to -# 4.14.0 (the last line proven Java-8-clean) and record it in the build notes. -adventure = "4.26.1" +# so a self-contained Adventure — the legacy-string serializer especially — is present +# below Paper 1.16.5 where net.kyori is absent from the server. Adventure 5.x publishes +# Gradle Module Metadata pinning org.gradle.jvm.version=21; it is consumed via the +# BUILD-HIGH path (root build.gradle.kts raises the consumer TargetJvmVersion to 21), +# and jvmdg downgrades its Java-21/v65 bytecode to the v52 base tree while keeping the +# pristine v65 originals under META-INF/versions/21 for Java-21+ JVMs. Our MiniMessage/ +# Component/LegacyComponentSerializer call sites are stable across 4→5 (no .java change). +adventure = "5.2.0" -# Java-8-line storage stack, pinned (AM-10): HikariCP 4.0.3 is the last Java-8 line, -# H2 1.4.200 is Java-8 compatible (NO NON_KEYWORDS URL flag — that is H2 2.x-only), -# mysql-connector-j 8.0.33. All three shaded + relocated under dev.fablemc.factions.lib.*. -hikari = "4.0.3" -h2 = "1.4.200" -mysql = "8.0.33" +# Storage stack, shaded + relocated under dev.fablemc.factions.lib.*. The connection pool is +# Apache Commons DBCP2 (with commons-pool2): its LATEST releases still target Java 8, so they +# drop straight into the v52 base tier with no jvmdg downgrade — unlike HikariCP, whose 5.x+ +# line needs Java 11 + unshimmable JDBC-4.3 methods. DBCP2 logs via commons-logging, so this +# also removed the slf4j pin. mysql-connector-j 9.x is consumed as-is (its Java-9+ bytecode is +# jvmdg-downgraded; the optional OpenTelemetry handler is -guarded). H2 1.4.200 stays +# on the Java-8 line (2.x changes the on-disk format + SQL dialect — a data migration, deferred). +commons-dbcp2 = "2.14.0" +commons-pool2 = "2.13.1" +# H2 2.2.224 is the last Java-8-native H2 line (2.3.x moved to a Java-11 floor); like DBCP2 it +# drops into the v52 base tier with no jvmdg downgrade. Our schema + MODE=MySQL survives 2.x's +# stricter reserved-word set as-is (the full-DDL storage tests pass without NON_KEYWORDS). (Beta: +# no on-disk migration from the old 1.4.200 format — a fresh DB is created, per the project owner.) +h2 = "2.2.224" +mysql = "9.7.0" bstats = "3.2.1" # compileOnly ONLY (soft-dep, never shaded): the typed FableExpansion subclass needs the @@ -53,13 +65,10 @@ adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } adventure-serializer-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } -hikaricp = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } -# HikariCP logs via slf4j-api (shaded transitively); slf4j-api's LoggerFactory statically -# references org.slf4j.impl.Static*Binder, which a binding must provide. Bundle the no-op -# binding (relocated with the rest of slf4j) so those references resolve in-jar for -# verifyJdk8Api and pool logging is silent rather than a startup warning. Pinned to the -# slf4j-api line HikariCP 4.0.3 pulls (1.7.x). -slf4j-nop = { module = "org.slf4j:slf4j-nop", version = "1.7.30" } +# Apache Commons DBCP2 connection pool + its commons-pool2 backend; both pull commons-logging +# transitively (all Java-8-native, so no jvmdg shim and no slf4j binding to bundle). +commons-dbcp2 = { module = "org.apache.commons:commons-dbcp2", version.ref = "commons-dbcp2" } +commons-pool2 = { module = "org.apache.commons:commons-pool2", version.ref = "commons-pool2" } h2 = { module = "com.h2database:h2", version.ref = "h2" } mysql = { module = "com.mysql:mysql-connector-j", version.ref = "mysql" } bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } From cda0349ba1213c0a3c24c6ff55c65d7fc2bcc3b6 Mon Sep 17 00:00:00 2001 From: owengregson Date: Wed, 8 Jul 2026 21:00:02 -0700 Subject: [PATCH 2/2] build(deps): hold Adventure at 4.x, keep DBCP2/H2-2.2.224/mysql-9; raise heap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (real environment) exposed that Adventure 5.x's build-high path forces jvmdg to shim far more modern-JDK APIs, and its stub set composes non-deterministically with the versions/13 + versions/21 tiers — green locally, but verifyJdk8Api flagged ~17 missing base-tree jvmdg stubs in CI. Revert the Adventure 5 + build- high pieces (TargetJvmVersion tweak, versions/21 verifyDowngrade handler); hold Adventure at the latest 4.x (Java-8-native, base tier) until a deterministic multi-tier stub merge lands. The other modernizations stand: - HikariCP → Apache Commons DBCP2 2.14.0 (HikariCP + slf4j pins removed) - H2 1.4.200 → 2.2.224 (latest Java-8-native line) - mysql-connector-j 8.0.33 → 9.7.0 The larger dep set (H2 2.2.224 especially) pushed jvmdg's shade past its internal timeout under the full parallel build; raise the daemon heap 3g → 6g so it completes comfortably (ubuntu-latest runners have 16g). Clean build reproducibly green; full test suite green. Co-Authored-By: Claude Fable 5 --- build.gradle.kts | 16 --------------- core/build.gradle.kts | 22 ++------------------- docs/design/per-version-dependencies.md | 26 ++++++++++++++++++------- gradle.properties | 2 +- gradle/libs.versions.toml | 18 ++++++++--------- 5 files changed, 31 insertions(+), 53 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 20ea10c..f1a172c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,22 +32,6 @@ subprojects { options.compilerArgs.add("-parameters") } - // BUILD-HIGH dependency consumption (does NOT change first-party bytecode). A modern - // shaded dep may publish Gradle Module Metadata pinning a high runtime floor via - // org.gradle.jvm.version — Adventure 5.x declares 21 — and Gradle's variant selection - // would then REJECT it against a release-17 consumer. We build on JDK 25 and hand every - // shaded dep to JVMDowngrader, which lowers its bytecode to the v52 base tier (keeping the - // pristine original under META-INF/versions/ for JVMs that can run it), so the consumer - // can honestly claim JVM-21 capability for variant SELECTION. This raises ONLY the - // TargetJvmVersion attribute on resolvable classpaths; options.release stays 17, so - // first-party bytecode remains v61 and the v52/v57/v61 tier model is untouched. The dep's - // own high-Java bytecode is what jvmdg downgrades — we never down-version the dependency. - configurations.matching { it.isCanBeResolved }.configureEach { - attributes { - attribute(org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21) - } - } - tasks.withType().configureEach { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 653c105..4dccf77 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -548,7 +548,6 @@ abstract class VerifyDowngradeTask : DefaultTask() { val baseBytesByLogical = mutableMapOf() val allEntryNames = hashSetOf() val v13JvmdgRefs = sortedSetOf() - var v21DepClasses = 0 JarFile(file).use { jar -> val mr = jar.manifest?.mainAttributes?.getValue("Multi-Release") @@ -585,24 +584,8 @@ abstract class VerifyDowngradeTask : DefaultTask() { v13JvmdgRefs.addAll(MegaJarScan.tokensWithPrefix(text, jvmdgRuntime)) if (name == "META-INF/versions/13/$sentinel") v13SentinelMajor = major } - name.startsWith("META-INF/versions/21/") -> { - // Modern-dependency tier (build-high). jvmdg keeps a shaded dep's ORIGINAL - // post-v61 bytecode here when the dep was built on a newer JDK than the - // first-party v61 floor (Adventure 5.x is Java-21 / v65): only 21+ JVMs load - // it, everyone else runs the v52 base copy jvmdg produced. Valid ONLY if it - // carries relocated third-party (lib.*) classes and NO first-party (which must - // stay in the 13/17 tiers), with genuinely post-v61 bytecode — assert that - // shape rather than blanket-accepting versions/21, so a real leak still fails. - if (MegaJarScan.isFirstParty(logical)) { - problems.add("versions/21 carries first-party class $logical (first-party must stay in versions/13-17)") - } else if (major <= 61) { - problems.add("versions/21 dep class $logical is v$major (not a post-v61 modern-dependency tier)") - } else { - v21DepClasses++ - } - } name.startsWith("META-INF/versions/") -> - problems.add("unexpected versioned tier entry $name (only versions/13, versions/17 and the versions/21 modern-dep tier are expected)") + problems.add("unexpected versioned tier entry $name (only versions/13 and versions/17 are expected)") else -> { if (major > 52) problems.add("base entry $logical is v$major (>52)") if (MegaJarScan.isFirstParty(logical)) { @@ -650,8 +633,7 @@ abstract class VerifyDowngradeTask : DefaultTask() { problems.take(30).joinToString("\n") { " - $it" }) report.get().asFile.writeText("ok\n") logger.lifecycle("[verifyDowngrade] OK — base ≤ v52; ${v13FirstParty.size} first-party class(es) forked to v57 under versions/13; " + - "${v17FirstParty.size} to v61 under versions/17; sentinel forked 52/57/61; no reflective-record token; versions/13 jvmdg refs resolve" + - (if (v21DepClasses > 0) "; $v21DepClasses post-v61 modern-dep class(es) under versions/21 (build-high dep originals)." else ".")) + "${v17FirstParty.size} to v61 under versions/17; sentinel forked 52/57/61; no reflective-record token; versions/13 jvmdg refs resolve.") } } diff --git a/docs/design/per-version-dependencies.md b/docs/design/per-version-dependencies.md index 9bca7c4..9927fd7 100644 --- a/docs/design/per-version-dependencies.md +++ b/docs/design/per-version-dependencies.md @@ -14,16 +14,28 @@ and let the same jvmdg pipeline that downgrades our own bytecode downgrade *it* |---|---|---|---| | Connection pool | HikariCP 4.0.3 (pinned) | **Apache Commons DBCP2 2.14.0** (latest) | migrated (`StorageBoot`): DBCP2's newest release is Java-8-native, so it drops into the v52 base tier with no jvmdg tricks — **removes the HikariCP pin** | | Logging binder | slf4j-nop 1.7.30 (pinned) | **gone** | DBCP2 logs via commons-logging (bundled) — **removes the slf4j pin** | -| Text | Adventure 4.x | **Adventure 5.2.0** (latest) | build-high: 5.x declares jvm.version=21; raise the consumer TargetJvmVersion to 21, jvmdg downgrades its v65 bytecode to v52 and keeps the v65 originals under `META-INF/versions/21`. Zero `.java` change | | MySQL driver | 8.0.33 | **9.7.0** (latest) | consumed as-is; the optional OpenTelemetry handler is ``-guarded and ignored | | Embedded DB | H2 1.4.200 (pinned) | **H2 2.2.224** | the latest **Java-8-native** H2 line (2.3.x needs Java 11 and jvmdg can't resolve its excluded `tools/Server` on downgrade). Our schema + MODE=MySQL clears 2.x's reserved words with no `NON_KEYWORDS`. Beta: no on-disk migration from the 1.4.200 format — a fresh DB is created | | bStats | 3.2.1 | 3.2.1 | already latest | - -First-party v52/v57/v61 tiers are untouched (`options.release` stays 17); the only new tier is -`versions/21`, carrying the relocated Adventure-5 originals for Java-21+ JVMs. Jar 10.68 → 11.73 MB. -Full build + all 7 gates + tests green. The single remaining hold is H2 at 2.2.224 rather than -2.3.x — a principled Java-8-floor hold, the H2 analog of "use the newest that stays on the base -tier," exactly like DBCP2. +| Text | Adventure 4.26.1 | **held at 4.26.1** (latest 4.x) | see below | + +First-party v52/v57/v61 tiers are untouched (`options.release` stays 17), and the jar stays a clean +base/v13/v17 Multi-Release with no extra tier. Full build + all 7 gates + tests green (the larger +dep set needed the daemon heap raised to 6g so jvmdg's shade stays under its internal timeout). + +### Held: Adventure 5.x (deferred, not rejected) + +Adventure 5.2.0 IS consumable via build-high (jvm.version=21 → jvmdg downgrade) with **zero +`.java` change** — proven green locally. But 5.x pulls in many more modern-JDK APIs +(`StringConcatFactory`, `HexFormat`, `RandomGenerator`, `MatchException`, `Locale`…), and jvmdg's +shim-stub set for them composes **non-deterministically** with the `versions/13` + `versions/21` +tiers: it passed locally but `verifyJdk8Api` flagged ~17 missing base-tree jvmdg stubs in CI (a +different environment/ordering surfaced the gap). The `mergeTiers` dedup even reported repeated +`versions/13` stub entries. Root cause is the three-way tier composition (base v52 / v57 versions-13 +/ v65 versions-21) not producing a complete, deterministic jvmdg-stub closure. Fixing it needs a +deterministic multi-tier stub-merge (compute the full stub closure per tier and inject it +explicitly), which is its own build-surgery task. Until then Adventure stays on the latest 4.x line +(Java-8-native, base tier) — the other four dependencies modernized without it. --- diff --git a/gradle.properties b/gradle.properties index acca3ab..4efb92c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,4 @@ org.gradle.caching=true # capture); shadow/jvmdg/run-paper are compatible as of the pinned versions. org.gradle.configuration-cache=true -org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba16f76..467d14a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,15 +11,15 @@ paper-floor = "1.13.2-R0.1-SNAPSHOT" paper-folia = "1.20.4-R0.1-SNAPSHOT" paper-modern = "1.20.6-R0.1-SNAPSHOT" -# Shaded + relocated into the FableFactions jar (dev.fablemc.factions.lib.adventure) -# so a self-contained Adventure — the legacy-string serializer especially — is present -# below Paper 1.16.5 where net.kyori is absent from the server. Adventure 5.x publishes -# Gradle Module Metadata pinning org.gradle.jvm.version=21; it is consumed via the -# BUILD-HIGH path (root build.gradle.kts raises the consumer TargetJvmVersion to 21), -# and jvmdg downgrades its Java-21/v65 bytecode to the v52 base tree while keeping the -# pristine v65 originals under META-INF/versions/21 for Java-21+ JVMs. Our MiniMessage/ -# Component/LegacyComponentSerializer call sites are stable across 4→5 (no .java change). -adventure = "5.2.0" +# Shaded + relocated into the FableFactions jar (dev.fablemc.factions.lib.adventure) so a +# self-contained Adventure — the legacy-string serializer especially — is present below Paper +# 1.16.5 where net.kyori is absent from the server. Held at the latest 4.x (Java-8-native, base +# tier). Adventure 5.x IS consumable via build-high (jvm.version=21 → downgrade), and the API +# needs no .java change — but 5.x pulls in far more modern-JDK APIs, and jvmdg's shim-stub set for +# them composes non-deterministically with the versions/13 + versions/21 tiers (green locally, but +# verifyJdk8Api flagged missing base-tree stubs in CI). Deferred until the multi-tier stub merge is +# made deterministic; the other deps modernized without it. +adventure = "4.26.1" # Storage stack, shaded + relocated under dev.fablemc.factions.lib.*. The connection pool is # Apache Commons DBCP2 (with commons-pool2): its LATEST releases still target Java 8, so they