diff --git a/build.gradle b/build.gradle index ff4a5b002..f8a21337e 100644 --- a/build.gradle +++ b/build.gradle @@ -440,44 +440,21 @@ List commonModdedArtifactEntries = [ 'assets/irisworldgen/icon.png' ] -// Frozen inventory of shipped classes that extend or implement an org.bukkit type. They come from -// core and VolmLib, sit on Bukkit-only code paths the mod loaders never resolve, and are inert dead -// weight rather than a boot hazard - excluding them means chasing the whole core reference graph. -// The verifier fails on anything NEW outside this set, which is the part that would be a regression. -// Shrinking the set (WP4 deletions, retypes) needs no edit here; growing it does. -Set knownBukkitSupertypeClasses = [ - 'art/arcane/iris/core/events/IrisEngineEvent.class', - 'art/arcane/iris/core/events/IrisLootEvent.class', - 'art/arcane/iris/core/events/IrisLootEvent$1.class', - 'art/arcane/iris/core/events/IrisLootEvent$2.class', - 'art/arcane/iris/core/link/data/ItemAdderDataProvider.class', - 'art/arcane/iris/core/link/data/MythicMobsDataProvider.class', - 'art/arcane/iris/core/link/data/OraxenDataProvider.class', - 'art/arcane/iris/core/link/MultiverseGuardListener.class', - 'art/arcane/iris/engine/framework/EngineAssignedWorldManager.class', - 'art/arcane/iris/engine/object/IrisEntity$BukkitOps$1.class', - 'art/arcane/iris/engine/object/LegacyTileData$SignTags$1.class', - 'art/arcane/iris/engine/platform/BukkitChunkGenerator.class', - 'art/arcane/iris/engine/platform/DummyBiomeProvider.class', - 'art/arcane/iris/engine/platform/DummyChunkGenerator.class', - 'art/arcane/iris/util/common/data/IrisCustomData.class', - 'art/arcane/iris/util/common/plugin/IrisService.class', - 'art/arcane/iris/util/common/plugin/VolmitPlugin.class', - 'art/arcane/iris/util/common/plugin/VolmitSender.class', - 'art/arcane/iris/util/common/plugin/chunk/ChunkTickets.class', - 'art/arcane/volmlib/util/board/BoardUpdateTask.class', - 'art/arcane/volmlib/util/bukkit/Events.class', - 'art/arcane/volmlib/util/data/Cuboid.class', - 'art/arcane/volmlib/util/data/InvertedBiomeGrid.class', - 'art/arcane/volmlib/util/data/IrisBiomeStorage.class', - 'art/arcane/volmlib/util/inventorygui/UIWindow.class' -] as Set +// The modded jars ship the Bukkit-only code paths inert - excluding them means chasing the whole core +// reference graph. What must not slip in is new Bukkit coupling in core, which core already declares +// in purity-allowlist.txt, so the baseline is read from there instead of frozen by hand. +File bukkitPurityAllowlist = layout.projectDirectory.file('core/purity-allowlist.txt').asFile +Set bukkitCoupledCoreClasses = bukkitPurityAllowlist.readLines() + .findAll { String line -> !line.isBlank() } + .collect { String line -> line.trim() - ~/\.java$/ } + .toSet() tasks.register('verifyFabricArtifact') { group = 'verification' dependsOn('fabricJar') File artifact = layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}").asFile inputs.file(artifact) + inputs.file(bukkitPurityAllowlist) doLast { List requiredEntries = commonModdedArtifactEntries + [ 'fabric.mod.json', @@ -485,7 +462,7 @@ tasks.register('verifyFabricArtifact') { 'irisworldgen.accesswidener', 'art/arcane/iris/fabric/IrisFabricBootstrap.class' ] - ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses) logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") } } @@ -495,13 +472,14 @@ tasks.register('verifyForgeArtifact') { dependsOn('forgeJar') File artifact = layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}").asFile inputs.file(artifact) + inputs.file(bukkitPurityAllowlist) doLast { List requiredEntries = commonModdedArtifactEntries + [ 'META-INF/mods.toml', 'META-INF/accesstransformer.cfg', 'art/arcane/iris/forge/IrisForgeBootstrap.class' ] - ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses) logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") } } @@ -511,13 +489,14 @@ tasks.register('verifyNeoforgeArtifact') { dependsOn('neoforgeJar') File artifact = layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}").asFile inputs.file(artifact) + inputs.file(bukkitPurityAllowlist) doLast { List requiredEntries = commonModdedArtifactEntries + [ 'META-INF/neoforge.mods.toml', 'META-INF/accesstransformer.cfg', 'art/arcane/iris/neoforge/IrisNeoForgeBootstrap.class' ] - ModdedArtifactVerifier.verify(artifact, requiredEntries, knownBukkitSupertypeClasses) + ModdedArtifactVerifier.verify(artifact, requiredEntries, bukkitCoupledCoreClasses) logger.lifecycle("Verified ${artifact.name} packaging, mixin compat, and nested jars") } } diff --git a/buildSrc/src/main/java/ModdedArtifactVerifier.java b/buildSrc/src/main/java/ModdedArtifactVerifier.java index 30c4305db..a77cd67fd 100644 --- a/buildSrc/src/main/java/ModdedArtifactVerifier.java +++ b/buildSrc/src/main/java/ModdedArtifactVerifier.java @@ -43,6 +43,8 @@ public final class ModdedArtifactVerifier { // Only the Bukkit platform binding is allowed to sit on org.bukkit types, and the modded jars // exclude it outright. Kept as an exemption so the same check can run over the Bukkit artifact. private static final String BUKKIT_PLATFORM_PREFIX = "art/arcane/iris/platform/bukkit/"; + // VolmLib is a separate library with its own build; Iris' core purity ratchet does not govern it. + private static final String SHARED_LIBRARY_PREFIX = "art/arcane/volmlib/"; private static final String BUKKIT_TYPE_PREFIX = "org/bukkit/"; private static final String FABRIC_METADATA = "fabric.mod.json"; private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml"; @@ -65,12 +67,14 @@ public final class ModdedArtifactVerifier { } /** - * @param bukkitSupertypeBaseline entry names (as they appear in the jar) that are known to - * extend or implement an {@code org.bukkit} type. New offenders - * outside this frozen set fail the build; entries that disappear - * from the artifact are not an error. + * @param bukkitCoupledCoreClasses top-level internal class names (no {@code .class} suffix, no + * nested-class part) whose source file is listed in + * core/purity-allowlist.txt. A class with an {@code org.bukkit} + * supertype is accepted only when it is one of those, sits under + * the Bukkit platform package, or comes from VolmLib. Anything + * else is new Bukkit coupling in core and fails the build. */ - public static void verify(File artifact, List requiredEntries, Set bukkitSupertypeBaseline) { + public static void verify(File artifact, List requiredEntries, Set bukkitCoupledCoreClasses) { if (!artifact.isFile()) { throw new GradleException("Missing modded Iris artifact: " + artifact.getAbsolutePath()); } @@ -129,7 +133,8 @@ public final class ModdedArtifactVerifier { } } if (!name.startsWith(BUKKIT_PLATFORM_PREFIX) - && !bukkitSupertypeBaseline.contains(name) + && !name.startsWith(SHARED_LIBRARY_PREFIX) + && !bukkitCoupledCoreClasses.contains(topLevelClass(name)) && hasBukkitSupertype(classFile)) { bukkitSupertypeClasses.add(name); } @@ -154,7 +159,8 @@ public final class ModdedArtifactVerifier { } if (!bukkitSupertypeClasses.isEmpty()) { throw new GradleException(artifact.getName() + " ships classes with an org.bukkit supertype outside " - + BUKKIT_PLATFORM_PREFIX + ": " + preview(bukkitSupertypeClasses)); + + BUKKIT_PLATFORM_PREFIX + " whose source is not declared Bukkit-coupled in" + + " core/purity-allowlist.txt: " + preview(bukkitSupertypeClasses)); } verifyNestedJarDeclarations(artifact, jar, shippedNestedJars); @@ -251,6 +257,12 @@ public final class ModdedArtifactVerifier { } } + private static String topLevelClass(String entryName) { + String internalName = entryName.substring(0, entryName.length() - ".class".length()); + int nested = internalName.indexOf('$'); + return nested < 0 ? internalName : internalName.substring(0, nested); + } + private static boolean hasBukkitSupertype(byte[] classFile) { ClassReader reader; try { diff --git a/buildSrc/src/test/java/ModdedArtifactVerifierTest.java b/buildSrc/src/test/java/ModdedArtifactVerifierTest.java index 42f4ec7ae..bfc13d7e9 100644 --- a/buildSrc/src/test/java/ModdedArtifactVerifierTest.java +++ b/buildSrc/src/test/java/ModdedArtifactVerifierTest.java @@ -253,14 +253,51 @@ public class ModdedArtifactVerifierTest { } @Test - public void acceptsBaselinedBukkitSupertype() throws Exception { - String entryName = "art/arcane/iris/engine/platform/BukkitChunkGenerator.class"; + public void acceptsBukkitSupertypeDeclaredInThePurityAllowlist() throws Exception { Map entries = validEntries(); - entries.put(entryName, classExtending("art/arcane/iris/engine/platform/BukkitChunkGenerator", - "org/bukkit/generator/ChunkGenerator")); + entries.put("art/arcane/iris/engine/platform/BukkitChunkGenerator.class", + classExtending("art/arcane/iris/engine/platform/BukkitChunkGenerator", + "org/bukkit/generator/ChunkGenerator")); File artifact = createArtifact(entries); - ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of(entryName)); + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, + Set.of("art/arcane/iris/engine/platform/BukkitChunkGenerator")); + } + + @Test + public void acceptsBukkitSupertypeOnNestedClassOfAllowlistedSource() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/engine/object/IrisEntity$BukkitOps$1.class", + classImplementing("art/arcane/iris/engine/object/IrisEntity$BukkitOps$1", + "org/bukkit/event/Listener")); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, + Set.of("art/arcane/iris/engine/object/IrisEntity")); + } + + @Test + public void acceptsBukkitSupertypeFromSharedLibrary() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/volmlib/util/bukkit/Events.class", + classImplementing("art/arcane/volmlib/util/bukkit/Events", "org/bukkit/event/Listener")); + File artifact = createArtifact(entries); + + ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); + } + + @Test + public void rejectsBukkitSupertypeMissingFromThePurityAllowlist() throws Exception { + Map entries = validEntries(); + entries.put("art/arcane/iris/core/link/data/OraxenDataProvider.class", + classImplementing("art/arcane/iris/core/link/data/OraxenDataProvider", + "org/bukkit/event/Listener")); + File artifact = createArtifact(entries); + + GradleException failure = assertThrows(GradleException.class, + () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, + Set.of("art/arcane/iris/core/link/data/ItemAdderDataProvider"))); + assertTrue(failure.getMessage().contains("core/purity-allowlist.txt")); } private Map validEntries() {