From 8796eb96575098b62eaae8198f9ca776a9b289fe Mon Sep 17 00:00:00 2001 From: Gugle Date: Thu, 19 Mar 2026 18:26:38 +0800 Subject: [PATCH 1/7] Add the NeoForgedRepositoryFilter to address the issues associated with NeoForge Maven using the Maven Central repository --- .../generated/NeoForgedRepositoryFilter.java | 170 ++++++++++++++++++ .../internal/RepositoriesPlugin.java | 2 + 2 files changed, 172 insertions(+) create mode 100644 src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java diff --git a/src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java b/src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java new file mode 100644 index 00000000..82f6ce13 --- /dev/null +++ b/src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java @@ -0,0 +1,170 @@ + +package net.neoforged.moddevgradle.internal.generated; + +public class NeoForgedRepositoryFilter { + public static void filter(org.gradle.api.artifacts.repositories.RepositoryContentDescriptor filter) { + filter.includeModule("ca.weblite", "java-objc-bridge"); + filter.includeModule("com.electronwill.night-config", "core"); + filter.includeModule("com.electronwill.night-config", "toml"); + filter.includeModule("com.fasterxml.jackson.core", "jackson-annotations"); + filter.includeModule("com.fasterxml.jackson.core", "jackson-core"); + filter.includeModule("com.fasterxml.jackson", "jackson-base"); + filter.includeModule("com.fasterxml.jackson", "jackson-bom"); + filter.includeModule("com.fasterxml.jackson", "jackson-parent"); + filter.includeModule("com.fasterxml", "oss-parent"); + filter.includeModule("com.github.oshi", "oshi-core"); + filter.includeModule("com.github.oshi", "oshi-parent"); + filter.includeModule("com.google.code.findbugs", "jsr305"); + filter.includeModule("com.google.code.gson", "gson"); + filter.includeModule("com.google.code.gson", "gson-parent"); + filter.includeModule("com.google.errorprone", "error_prone_annotations"); + filter.includeModule("com.google.errorprone", "error_prone_parent"); + filter.includeModule("com.google.guava", "failureaccess"); + filter.includeModule("com.google.guava", "guava"); + filter.includeModule("com.google.guava", "guava-parent"); + filter.includeModule("com.google.guava", "listenablefuture"); + filter.includeModule("com.google.j2objc", "j2objc-annotations"); + filter.includeModule("com.ibm.icu", "icu4j"); + filter.includeModule("com.machinezoo.noexception", "noexception"); + filter.includeModule("com.mojang", "authlib"); + filter.includeModule("com.mojang", "blocklist"); + filter.includeModule("com.mojang", "brigadier"); + filter.includeModule("com.mojang", "datafixerupper"); + filter.includeModule("com.mojang", "logging"); + filter.includeModule("com.mojang", "patchy"); + filter.includeModule("com.mojang", "text2speech"); + filter.includeModule("com.nothome", "javaxdelta"); + filter.includeModule("commons-codec", "commons-codec"); + filter.includeModule("commons-io", "commons-io"); + filter.includeModule("commons-logging", "commons-logging"); + filter.includeModule("cpw.mods", "bootstraplauncher"); + filter.includeModule("cpw.mods", "modlauncher"); + filter.includeModule("cpw.mods", "securejarhandler"); + filter.includeModule("de.siegmar", "fastcsv"); + filter.includeModule("io.codechicken", "DiffPatch"); + filter.includeModule("io.fabric8", "kubernetes-client-bom"); + filter.includeModule("io.github.llamalad7", "mixinextras-neoforge"); + filter.includeModule("io.netty", "netty-bom"); + filter.includeModule("io.netty", "netty-buffer"); + filter.includeModule("io.netty", "netty-codec"); + filter.includeModule("io.netty", "netty-common"); + filter.includeModule("io.netty", "netty-handler"); + filter.includeModule("io.netty", "netty-parent"); + filter.includeModule("io.netty", "netty-resolver"); + filter.includeModule("io.netty", "netty-transport"); + filter.includeModule("io.netty", "netty-transport-classes-epoll"); + filter.includeModule("io.netty", "netty-transport-native-unix-common"); + filter.includeModule("it.unimi.dsi", "fastutil"); + filter.includeModule("jakarta.platform", "jakarta.jakartaee-bom"); + filter.includeModule("jakarta.platform", "jakartaee-api-parent"); + filter.includeModule("net.covers1624", "DevLogin"); + filter.includeModule("net.covers1624", "Quack"); + filter.includeModule("net.fabricmc", "sponge-mixin"); + filter.includeModule("net.java.dev.jna", "jna"); + filter.includeModule("net.java.dev.jna", "jna-platform"); + filter.includeModule("net.jodah", "typetools"); + filter.includeModule("net.minecraftforge", "mergetool"); + filter.includeModule("net.minecraftforge", "srgutils"); + filter.includeModule("net.minecrell", "terminalconsoleappender"); + filter.includeModule("net.neoforged.accesstransformers", "at-modlauncher"); + filter.includeModule("net.neoforged.accesstransformers", "at-parser"); + filter.includeModule("net.neoforged.fancymodloader", "earlydisplay"); + filter.includeModule("net.neoforged.fancymodloader", "junit-fml"); + filter.includeModule("net.neoforged.fancymodloader", "loader"); + filter.includeModule("net.neoforged.installertools", "binarypatcher"); + filter.includeModule("net.neoforged.installertools", "cli-utils"); + filter.includeModule("net.neoforged.installertools", "installertools"); + filter.includeModule("net.neoforged.javadoctor", "gson-io"); + filter.includeModule("net.neoforged.javadoctor", "spec"); + filter.includeModule("net.neoforged.jst", "jst-cli-bundle"); + filter.includeModule("net.neoforged", "AutoRenamingTool"); + filter.includeModule("net.neoforged", "DevLaunch"); + filter.includeModule("net.neoforged", "JarJarFileSystems"); + filter.includeModule("net.neoforged", "JarJarMetadata"); + filter.includeModule("net.neoforged", "JarJarSelector"); + filter.includeModule("net.neoforged", "accesstransformers"); + filter.includeModule("net.neoforged", "bus"); + filter.includeModule("net.neoforged", "coremods"); + filter.includeModule("net.neoforged", "mergetool"); + filter.includeModule("net.neoforged", "minecraft-dependencies"); + filter.includeModule("net.neoforged", "neoforge"); + filter.includeModule("net.neoforged", "neoform"); + filter.includeModule("net.neoforged", "neoform-runtime"); + filter.includeModule("net.neoforged", "srgutils"); + filter.includeModule("net.neoforged", "testframework"); + filter.includeModule("net.sf.jopt-simple", "jopt-simple"); + filter.includeModule("org.antlr", "antlr4-master"); + filter.includeModule("org.antlr", "antlr4-runtime"); + filter.includeModule("org.apache.commons", "commons-compress"); + filter.includeModule("org.apache.commons", "commons-lang3"); + filter.includeModule("org.apache.commons", "commons-parent"); + filter.includeModule("org.apache.groovy", "groovy-bom"); + filter.includeModule("org.apache.httpcomponents", "httpclient"); + filter.includeModule("org.apache.httpcomponents", "httpcomponents-client"); + filter.includeModule("org.apache.httpcomponents", "httpcomponents-core"); + filter.includeModule("org.apache.httpcomponents", "httpcomponents-parent"); + filter.includeModule("org.apache.httpcomponents", "httpcore"); + filter.includeModule("org.apache.logging.log4j", "log4j"); + filter.includeModule("org.apache.logging.log4j", "log4j-api"); + filter.includeModule("org.apache.logging.log4j", "log4j-bom"); + filter.includeModule("org.apache.logging.log4j", "log4j-core"); + filter.includeModule("org.apache.logging.log4j", "log4j-slf4j2-impl"); + filter.includeModule("org.apache.logging", "logging-parent"); + filter.includeModule("org.apache.maven", "maven"); + filter.includeModule("org.apache.maven", "maven-artifact"); + filter.includeModule("org.apache.maven", "maven-parent"); + filter.includeModule("org.apache", "apache"); + filter.includeModule("org.apiguardian", "apiguardian-api"); + filter.includeModule("org.checkerframework", "checker-qual"); + filter.includeModule("org.codehaus.groovy", "groovy-bom"); + filter.includeModule("org.codehaus.plexus", "plexus"); + filter.includeModule("org.codehaus.plexus", "plexus-utils"); + filter.includeModule("org.commonmark", "commonmark"); + filter.includeModule("org.commonmark", "commonmark-parent"); + filter.includeModule("org.eclipse.ee4j", "project"); + filter.includeModule("org.eclipse.jetty", "jetty-bom"); + filter.includeModule("org.jcraft", "jorbis"); + filter.includeModule("org.jetbrains", "annotations"); + filter.includeModule("org.jline", "jline-parent"); + filter.includeModule("org.jline", "jline-reader"); + filter.includeModule("org.jline", "jline-terminal"); + filter.includeModule("org.joml", "joml"); + filter.includeModule("org.jspecify", "jspecify"); + filter.includeModule("org.junit.jupiter", "junit-jupiter"); + filter.includeModule("org.junit.jupiter", "junit-jupiter-api"); + filter.includeModule("org.junit.jupiter", "junit-jupiter-engine"); + filter.includeModule("org.junit.jupiter", "junit-jupiter-params"); + filter.includeModule("org.junit.platform", "junit-platform-commons"); + filter.includeModule("org.junit.platform", "junit-platform-engine"); + filter.includeModule("org.junit.platform", "junit-platform-launcher"); + filter.includeModule("org.junit", "junit-bom"); + filter.includeModule("org.lwjgl", "lwjgl"); + filter.includeModule("org.lwjgl", "lwjgl-bom"); + filter.includeModule("org.lwjgl", "lwjgl-freetype"); + filter.includeModule("org.lwjgl", "lwjgl-glfw"); + filter.includeModule("org.lwjgl", "lwjgl-jemalloc"); + filter.includeModule("org.lwjgl", "lwjgl-openal"); + filter.includeModule("org.lwjgl", "lwjgl-opengl"); + filter.includeModule("org.lwjgl", "lwjgl-stb"); + filter.includeModule("org.lwjgl", "lwjgl-tinyfd"); + filter.includeModule("org.lz4", "lz4-java"); + filter.includeModule("org.mockito", "mockito-bom"); + filter.includeModule("org.openjdk.nashorn", "nashorn-core"); + filter.includeModule("org.opentest4j", "opentest4j"); + filter.includeModule("org.ow2.asm", "asm"); + filter.includeModule("org.ow2.asm", "asm-analysis"); + filter.includeModule("org.ow2.asm", "asm-commons"); + filter.includeModule("org.ow2.asm", "asm-tree"); + filter.includeModule("org.ow2.asm", "asm-util"); + filter.includeModule("org.ow2", "ow2"); + filter.includeModule("org.parchmentmc.data", "parchment-1.21"); + filter.includeModule("org.slf4j", "slf4j-api"); + filter.includeModule("org.slf4j", "slf4j-bom"); + filter.includeModule("org.slf4j", "slf4j-parent"); + filter.includeModule("org.sonatype.oss", "oss-parent"); + filter.includeModule("org.springframework", "spring-framework-bom"); + filter.includeModule("org.tukaani", "xz"); + filter.includeModule("org.vineflower", "vineflower"); + filter.includeModule("trove", "trove"); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index ec296a1a..168ba9a1 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -2,6 +2,7 @@ import java.net.URI; import net.neoforged.moddevgradle.internal.generated.MojangRepositoryFilter; +import net.neoforged.moddevgradle.internal.generated.NeoForgedRepositoryFilter; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -56,6 +57,7 @@ private void applyRepositories(RepositoryHandler repositories) { repositories.maven(repo -> { repo.setName("NeoForged Releases"); repo.setUrl(URI.create("https://maven.neoforged.net/releases/")); + repo.content(NeoForgedRepositoryFilter::filter); }); } From 1a399d22ed2f91efd5c47dfb048d8cad78a1056e Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 01:21:17 +0800 Subject: [PATCH 2/7] Refactor NeoForgedRepositoryFilter: update package structure and simplify filter method --- .../moddevgradle/internal}/NeoForgedRepositoryFilter.java | 5 +++-- .../neoforged/moddevgradle/internal/RepositoriesPlugin.java | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{generated/java/net/neoforged/moddevgradle/internal/generated => main/java/net/neoforged/moddevgradle/internal}/NeoForgedRepositoryFilter.java (98%) diff --git a/src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java similarity index 98% rename from src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java rename to src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java index 82f6ce13..5c644003 100644 --- a/src/generated/java/net/neoforged/moddevgradle/internal/generated/NeoForgedRepositoryFilter.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java @@ -1,8 +1,9 @@ +package net.neoforged.moddevgradle.internal; -package net.neoforged.moddevgradle.internal.generated; +import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor; public class NeoForgedRepositoryFilter { - public static void filter(org.gradle.api.artifacts.repositories.RepositoryContentDescriptor filter) { + public static void filter(RepositoryContentDescriptor filter) { filter.includeModule("ca.weblite", "java-objc-bridge"); filter.includeModule("com.electronwill.night-config", "core"); filter.includeModule("com.electronwill.night-config", "toml"); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index 168ba9a1..011de18f 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -2,7 +2,6 @@ import java.net.URI; import net.neoforged.moddevgradle.internal.generated.MojangRepositoryFilter; -import net.neoforged.moddevgradle.internal.generated.NeoForgedRepositoryFilter; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; From 2ff3547e5c4721d1c3396ed0cda26ee555ef8a2c Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 02:14:02 +0800 Subject: [PATCH 3/7] Enhance NeoForgedRepositoryFilter: implement dynamic discovery of game library modules and update content filtering mechanism --- .../moddevgradle/internal/ModDevPlugin.java | 72 +++- .../internal/NeoForgedRepositoryFilter.java | 400 +++++++++++------- .../internal/RepositoriesPlugin.java | 49 ++- .../nfrtgradle/NeoFormRuntimeExtension.java | 8 + 4 files changed, 357 insertions(+), 172 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index 97f0e7f5..efd02d50 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -1,11 +1,15 @@ package net.neoforged.moddevgradle.internal; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin; import net.neoforged.moddevgradle.dsl.ModDevExtension; import net.neoforged.moddevgradle.dsl.ModdingVersionSettings; import net.neoforged.moddevgradle.dsl.NeoForgeExtension; import net.neoforged.moddevgradle.internal.jarjar.JarJarPlugin; import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import net.neoforged.nfrtgradle.NeoFormRuntimeExtension; import net.neoforged.nfrtgradle.NeoFormRuntimePlugin; import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Plugin; @@ -75,9 +79,18 @@ public void enable( var configurations = project.getConfigurations(); - var dependencies = neoForge != null ? ModdingDependencies.create(neoForge, neoForgeNotation, neoForm, neoFormNotation, versionCapabilities) + var dependencies = neoForge != null ? ModdingDependencies.create( + neoForge, + neoForgeNotation, + neoForm, + neoFormNotation, + versionCapabilities) : ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation); + if (neoForgeVersion != null) { + populateNeoForgeRepositoryFilter(project, neoForgeVersion); + } + ArtifactNamingStrategy artifactNamingStrategy; // It's helpful to be able to differentiate the Vanilla jar and the NeoForge jar in classic multiloader setups. if (neoForge == null) { @@ -106,4 +119,61 @@ public void enable( artifacts, extension.getRuns()); } + + /** + * Downloads the Gradle Module Metadata (.module file) for the given NeoForge version + * directly via HTTP to discover which game library modules it transitively depends on. + * Registers them in {@link NeoForgedRepositoryFilter} and then applies the content + * filter to the NeoForge repository. + *

+ * The HTTP download bypasses Gradle's dependency resolution so the repository content + * descriptor stays unlocked and can receive its first {@code content()} call. + */ + private static void populateNeoForgeRepositoryFilter(Project project, String neoForgeVersion) { + // Regex to extract group:module pairs from Gradle Module Metadata JSON. + var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); + + // 1. Discover game library modules from the NeoForge artifact metadata. + fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion + + "/neoforge-" + neoForgeVersion + ".module", depPattern); + + // 2. Discover build tool modules from the NeoForm Runtime metadata. + // NFRT ships external tools (DiffPatch, AutoRenamingTool, etc.) whose + // transitive dependencies are also rehosted on the NeoForged Maven. + var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); + fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion + + "/neoform-runtime-" + nfrtVersion + ".module", depPattern); + + // Apply the content filter now — before any dependency resolution uses the + // NeoForge repository. + try { + RepositoriesPlugin.applyContentFilter(project); + } catch (Exception e) { + LOG.warn("Failed to apply NeoForge repository content filter: {}", e.getMessage()); + } + } + + private static void fetchModuleDependencies(String path, Pattern depPattern) { + try { + var moduleUrl = URI.create( + "https://maven.neoforged.net/releases/" + path).toURL(); + + try (var stream = moduleUrl.openStream()) { + var content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + var matcher = depPattern.matcher(content); + while (matcher.find()) { + var group = matcher.group(1); + var module = matcher.group(2); + if ("*".equals(group) || "*".equals(module)) { + continue; + } + NeoForgedRepositoryFilter.addGameLibrary(group, module); + } + } + } catch (Exception e) { + LOG.warn( + "Failed to discover dependencies from {}: {}", + path, e.getMessage()); + } + } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java index 5c644003..79c3bd55 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java @@ -1,171 +1,245 @@ package net.neoforged.moddevgradle.internal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor; +import org.jetbrains.annotations.ApiStatus; +/** + * Controls which modules Gradle may resolve from the NeoForged Maven. + *

+ * The NeoForged Maven mirrors many Maven Central artifacts. Without a content filter, + * Gradle could resolve arbitrary third-party artifacts from it. + *

+ * This filter is built from two layers: + *

    + *
  • Stable baseline — the full set of modules known to be hosted on the + * NeoForged Maven. This covers NeoForge artifacts, modding toolchain projects, and + * their transitive dependencies at the time the plugin was built.
  • + *
  • Dynamic discovery — at configuration time, the plugin downloads the Gradle + * Module Metadata for the selected NeoForge and NeoForm Runtime versions and adds + * any newly-declared direct dependencies to the filter. This allows new Minecraft + * releases to work without a plugin update, provided their libraries are also + * mirrored.
  • + *
+ */ public class NeoForgedRepositoryFilter { + /** + * All modules currently known to be hosted on the NeoForged Maven. + * When the selected NeoForge version pulls in a library that is not yet listed here, + * the dynamic discovery path in {@link net.neoforged.moddevgradle.internal.ModDevPlugin} + * adds it at configuration time. + */ + // @formatter:off + private static final String[][] STABLE_MODULES = { + // --- net.neoforged sub-groups --- + {"net.neoforged.accesstransformers", "at-modlauncher"}, + {"net.neoforged.accesstransformers", "at-parser"}, + {"net.neoforged.fancymodloader", "earlydisplay"}, + {"net.neoforged.fancymodloader", "junit-fml"}, + {"net.neoforged.fancymodloader", "loader"}, + {"net.neoforged.installertools", "binarypatcher"}, + {"net.neoforged.installertools", "cli-utils"}, + {"net.neoforged.installertools", "installertools"}, + {"net.neoforged.javadoctor", "gson-io"}, + {"net.neoforged.javadoctor", "spec"}, + {"net.neoforged.jst", "jst-cli-bundle"}, + // --- main net.neoforged group --- + {"net.neoforged", "AutoRenamingTool"}, + {"net.neoforged", "DevLaunch"}, + {"net.neoforged", "JarJarFileSystems"}, + {"net.neoforged", "JarJarMetadata"}, + {"net.neoforged", "JarJarSelector"}, + {"net.neoforged", "accesstransformers"}, + {"net.neoforged", "bus"}, + {"net.neoforged", "coremods"}, + {"net.neoforged", "mergetool"}, + {"net.neoforged", "minecraft-dependencies"}, + {"net.neoforged", "neoforge"}, + {"net.neoforged", "neoform"}, + {"net.neoforged", "neoform-runtime"}, + {"net.neoforged", "srgutils"}, + {"net.neoforged", "testframework"}, + // --- modding toolchain --- + {"cpw.mods", "bootstraplauncher"}, + {"cpw.mods", "modlauncher"}, + {"cpw.mods", "securejarhandler"}, + {"net.minecraftforge", "mergetool"}, + {"net.minecraftforge", "srgutils"}, + {"net.minecrell", "terminalconsoleappender"}, + {"net.covers1624", "DevLogin"}, + {"net.covers1624", "Quack"}, + {"io.codechicken", "DiffPatch"}, + {"io.github.llamalad7", "mixinextras-neoforge"}, + {"net.fabricmc", "sponge-mixin"}, + {"de.siegmar", "fastcsv"}, + {"com.machinezoo.noexception", "noexception"}, + {"net.jodah", "typetools"}, + {"com.nothome", "javaxdelta"}, + {"trove", "trove"}, + // --- NeoForge-maintained library forks --- + {"com.electronwill.night-config", "core"}, + {"com.electronwill.night-config", "toml"}, + // --- Transitive dependencies of NFRT external tools --- + {"it.unimi.dsi", "fastutil"}, + {"org.apache.commons", "commons-compress"}, + {"org.apache.commons", "commons-lang3"}, + {"org.apache.commons", "commons-parent"}, + {"org.tukaani", "xz"}, + {"org.ow2.asm", "asm"}, + {"org.ow2.asm", "asm-analysis"}, + {"org.ow2.asm", "asm-commons"}, + {"org.ow2.asm", "asm-tree"}, + {"org.ow2.asm", "asm-util"}, + {"org.ow2", "ow2"}, + {"org.lz4", "lz4-java"}, + {"org.jcraft", "jorbis"}, + // --- Game libraries & their transitive dependencies --- + // Transitive dependencies of common game libraries are listed explicitly + // because the dynamic discovery only sees direct dependencies (a single + // module-metadata download), not the full transitive tree. + {"ca.weblite", "java-objc-bridge"}, + {"com.fasterxml.jackson.core", "jackson-annotations"}, + {"com.fasterxml.jackson.core", "jackson-core"}, + {"com.fasterxml.jackson", "jackson-base"}, + {"com.fasterxml.jackson", "jackson-bom"}, + {"com.fasterxml.jackson", "jackson-parent"}, + {"com.fasterxml", "oss-parent"}, + {"com.github.oshi", "oshi-core"}, + {"com.github.oshi", "oshi-parent"}, + {"com.google.code.findbugs", "jsr305"}, + {"com.google.code.gson", "gson"}, + {"com.google.code.gson", "gson-parent"}, + {"com.google.errorprone", "error_prone_annotations"}, + {"com.google.errorprone", "error_prone_parent"}, + {"com.google.guava", "failureaccess"}, + {"com.google.guava", "guava"}, + {"com.google.guava", "guava-parent"}, + {"com.google.guava", "listenablefuture"}, + {"com.google.j2objc", "j2objc-annotations"}, + {"com.ibm.icu", "icu4j"}, + {"com.mojang", "authlib"}, + {"com.mojang", "blocklist"}, + {"com.mojang", "brigadier"}, + {"com.mojang", "datafixerupper"}, + {"com.mojang", "logging"}, + {"com.mojang", "patchy"}, + {"com.mojang", "text2speech"}, + {"commons-codec", "commons-codec"}, + {"commons-io", "commons-io"}, + {"commons-logging", "commons-logging"}, + {"io.fabric8", "kubernetes-client-bom"}, + {"io.netty", "netty-bom"}, + {"io.netty", "netty-buffer"}, + {"io.netty", "netty-codec"}, + {"io.netty", "netty-common"}, + {"io.netty", "netty-handler"}, + {"io.netty", "netty-parent"}, + {"io.netty", "netty-resolver"}, + {"io.netty", "netty-transport"}, + {"io.netty", "netty-transport-classes-epoll"}, + {"io.netty", "netty-transport-native-unix-common"}, + {"jakarta.platform", "jakarta.jakartaee-bom"}, + {"jakarta.platform", "jakartaee-api-parent"}, + {"net.java.dev.jna", "jna"}, + {"net.java.dev.jna", "jna-platform"}, + {"org.antlr", "antlr4-master"}, + {"org.antlr", "antlr4-runtime"}, + {"org.apache.groovy", "groovy-bom"}, + {"org.apache.httpcomponents", "httpclient"}, + {"org.apache.httpcomponents", "httpcomponents-client"}, + {"org.apache.httpcomponents", "httpcomponents-core"}, + {"org.apache.httpcomponents", "httpcomponents-parent"}, + {"org.apache.httpcomponents", "httpcore"}, + {"org.apache.logging.log4j", "log4j"}, + {"org.apache.logging.log4j", "log4j-api"}, + {"org.apache.logging.log4j", "log4j-bom"}, + {"org.apache.logging.log4j", "log4j-core"}, + {"org.apache.logging.log4j", "log4j-slf4j2-impl"}, + {"org.apache.logging", "logging-parent"}, + {"org.apache.maven", "maven"}, + {"org.apache.maven", "maven-artifact"}, + {"org.apache.maven", "maven-parent"}, + {"org.apache", "apache"}, + {"org.apiguardian", "apiguardian-api"}, + {"org.checkerframework", "checker-qual"}, + {"org.codehaus.groovy", "groovy-bom"}, + {"org.codehaus.plexus", "plexus"}, + {"org.codehaus.plexus", "plexus-utils"}, + {"org.commonmark", "commonmark"}, + {"org.commonmark", "commonmark-parent"}, + {"org.eclipse.ee4j", "project"}, + {"org.eclipse.jetty", "jetty-bom"}, + {"org.jetbrains", "annotations"}, + {"org.jline", "jline-parent"}, + {"org.jline", "jline-reader"}, + {"org.jline", "jline-terminal"}, + {"org.joml", "joml"}, + {"org.jspecify", "jspecify"}, + {"org.junit.jupiter", "junit-jupiter"}, + {"org.junit.jupiter", "junit-jupiter-api"}, + {"org.junit.jupiter", "junit-jupiter-engine"}, + {"org.junit.jupiter", "junit-jupiter-params"}, + {"org.junit.platform", "junit-platform-commons"}, + {"org.junit.platform", "junit-platform-engine"}, + {"org.junit.platform", "junit-platform-launcher"}, + {"org.junit", "junit-bom"}, + {"org.lwjgl", "lwjgl"}, + {"org.lwjgl", "lwjgl-bom"}, + {"org.lwjgl", "lwjgl-freetype"}, + {"org.lwjgl", "lwjgl-glfw"}, + {"org.lwjgl", "lwjgl-jemalloc"}, + {"org.lwjgl", "lwjgl-openal"}, + {"org.lwjgl", "lwjgl-opengl"}, + {"org.lwjgl", "lwjgl-stb"}, + {"org.lwjgl", "lwjgl-tinyfd"}, + {"org.mockito", "mockito-bom"}, + {"org.opentest4j", "opentest4j"}, + {"org.slf4j", "slf4j-api"}, + {"org.slf4j", "slf4j-bom"}, + {"org.slf4j", "slf4j-parent"}, + {"org.sonatype.oss", "oss-parent"}, + {"org.springframework", "spring-framework-bom"}, + // --- Other NeoForge-hosted tooling --- + {"org.parchmentmc.data", "parchment-1.21"}, + {"org.vineflower", "vineflower"}, + {"org.openjdk.nashorn", "nashorn-core"}, + {"net.sf.jopt-simple", "jopt-simple"}, + }; + // @formatter:on + + /** + * Dynamically discovered game library modules, keyed by "group:module". + * Populated by {@link #addGameLibrary} before dependency resolution. + */ + private static final Set gameLibraryModules = Collections.synchronizedSet(new HashSet<>()); + + /** + * Adds a game library module (group:name) to the allowed set. + * Safe to call from any thread before dependency resolution begins. + */ + @ApiStatus.Internal + public static void addGameLibrary(String group, String module) { + gameLibraryModules.add(group + ":" + module); + } + + /** + * Applies the full filter: stable known modules plus any dynamically discovered + * game library modules for the selected NeoForge version. + */ public static void filter(RepositoryContentDescriptor filter) { - filter.includeModule("ca.weblite", "java-objc-bridge"); - filter.includeModule("com.electronwill.night-config", "core"); - filter.includeModule("com.electronwill.night-config", "toml"); - filter.includeModule("com.fasterxml.jackson.core", "jackson-annotations"); - filter.includeModule("com.fasterxml.jackson.core", "jackson-core"); - filter.includeModule("com.fasterxml.jackson", "jackson-base"); - filter.includeModule("com.fasterxml.jackson", "jackson-bom"); - filter.includeModule("com.fasterxml.jackson", "jackson-parent"); - filter.includeModule("com.fasterxml", "oss-parent"); - filter.includeModule("com.github.oshi", "oshi-core"); - filter.includeModule("com.github.oshi", "oshi-parent"); - filter.includeModule("com.google.code.findbugs", "jsr305"); - filter.includeModule("com.google.code.gson", "gson"); - filter.includeModule("com.google.code.gson", "gson-parent"); - filter.includeModule("com.google.errorprone", "error_prone_annotations"); - filter.includeModule("com.google.errorprone", "error_prone_parent"); - filter.includeModule("com.google.guava", "failureaccess"); - filter.includeModule("com.google.guava", "guava"); - filter.includeModule("com.google.guava", "guava-parent"); - filter.includeModule("com.google.guava", "listenablefuture"); - filter.includeModule("com.google.j2objc", "j2objc-annotations"); - filter.includeModule("com.ibm.icu", "icu4j"); - filter.includeModule("com.machinezoo.noexception", "noexception"); - filter.includeModule("com.mojang", "authlib"); - filter.includeModule("com.mojang", "blocklist"); - filter.includeModule("com.mojang", "brigadier"); - filter.includeModule("com.mojang", "datafixerupper"); - filter.includeModule("com.mojang", "logging"); - filter.includeModule("com.mojang", "patchy"); - filter.includeModule("com.mojang", "text2speech"); - filter.includeModule("com.nothome", "javaxdelta"); - filter.includeModule("commons-codec", "commons-codec"); - filter.includeModule("commons-io", "commons-io"); - filter.includeModule("commons-logging", "commons-logging"); - filter.includeModule("cpw.mods", "bootstraplauncher"); - filter.includeModule("cpw.mods", "modlauncher"); - filter.includeModule("cpw.mods", "securejarhandler"); - filter.includeModule("de.siegmar", "fastcsv"); - filter.includeModule("io.codechicken", "DiffPatch"); - filter.includeModule("io.fabric8", "kubernetes-client-bom"); - filter.includeModule("io.github.llamalad7", "mixinextras-neoforge"); - filter.includeModule("io.netty", "netty-bom"); - filter.includeModule("io.netty", "netty-buffer"); - filter.includeModule("io.netty", "netty-codec"); - filter.includeModule("io.netty", "netty-common"); - filter.includeModule("io.netty", "netty-handler"); - filter.includeModule("io.netty", "netty-parent"); - filter.includeModule("io.netty", "netty-resolver"); - filter.includeModule("io.netty", "netty-transport"); - filter.includeModule("io.netty", "netty-transport-classes-epoll"); - filter.includeModule("io.netty", "netty-transport-native-unix-common"); - filter.includeModule("it.unimi.dsi", "fastutil"); - filter.includeModule("jakarta.platform", "jakarta.jakartaee-bom"); - filter.includeModule("jakarta.platform", "jakartaee-api-parent"); - filter.includeModule("net.covers1624", "DevLogin"); - filter.includeModule("net.covers1624", "Quack"); - filter.includeModule("net.fabricmc", "sponge-mixin"); - filter.includeModule("net.java.dev.jna", "jna"); - filter.includeModule("net.java.dev.jna", "jna-platform"); - filter.includeModule("net.jodah", "typetools"); - filter.includeModule("net.minecraftforge", "mergetool"); - filter.includeModule("net.minecraftforge", "srgutils"); - filter.includeModule("net.minecrell", "terminalconsoleappender"); - filter.includeModule("net.neoforged.accesstransformers", "at-modlauncher"); - filter.includeModule("net.neoforged.accesstransformers", "at-parser"); - filter.includeModule("net.neoforged.fancymodloader", "earlydisplay"); - filter.includeModule("net.neoforged.fancymodloader", "junit-fml"); - filter.includeModule("net.neoforged.fancymodloader", "loader"); - filter.includeModule("net.neoforged.installertools", "binarypatcher"); - filter.includeModule("net.neoforged.installertools", "cli-utils"); - filter.includeModule("net.neoforged.installertools", "installertools"); - filter.includeModule("net.neoforged.javadoctor", "gson-io"); - filter.includeModule("net.neoforged.javadoctor", "spec"); - filter.includeModule("net.neoforged.jst", "jst-cli-bundle"); - filter.includeModule("net.neoforged", "AutoRenamingTool"); - filter.includeModule("net.neoforged", "DevLaunch"); - filter.includeModule("net.neoforged", "JarJarFileSystems"); - filter.includeModule("net.neoforged", "JarJarMetadata"); - filter.includeModule("net.neoforged", "JarJarSelector"); - filter.includeModule("net.neoforged", "accesstransformers"); - filter.includeModule("net.neoforged", "bus"); - filter.includeModule("net.neoforged", "coremods"); - filter.includeModule("net.neoforged", "mergetool"); - filter.includeModule("net.neoforged", "minecraft-dependencies"); - filter.includeModule("net.neoforged", "neoforge"); - filter.includeModule("net.neoforged", "neoform"); - filter.includeModule("net.neoforged", "neoform-runtime"); - filter.includeModule("net.neoforged", "srgutils"); - filter.includeModule("net.neoforged", "testframework"); - filter.includeModule("net.sf.jopt-simple", "jopt-simple"); - filter.includeModule("org.antlr", "antlr4-master"); - filter.includeModule("org.antlr", "antlr4-runtime"); - filter.includeModule("org.apache.commons", "commons-compress"); - filter.includeModule("org.apache.commons", "commons-lang3"); - filter.includeModule("org.apache.commons", "commons-parent"); - filter.includeModule("org.apache.groovy", "groovy-bom"); - filter.includeModule("org.apache.httpcomponents", "httpclient"); - filter.includeModule("org.apache.httpcomponents", "httpcomponents-client"); - filter.includeModule("org.apache.httpcomponents", "httpcomponents-core"); - filter.includeModule("org.apache.httpcomponents", "httpcomponents-parent"); - filter.includeModule("org.apache.httpcomponents", "httpcore"); - filter.includeModule("org.apache.logging.log4j", "log4j"); - filter.includeModule("org.apache.logging.log4j", "log4j-api"); - filter.includeModule("org.apache.logging.log4j", "log4j-bom"); - filter.includeModule("org.apache.logging.log4j", "log4j-core"); - filter.includeModule("org.apache.logging.log4j", "log4j-slf4j2-impl"); - filter.includeModule("org.apache.logging", "logging-parent"); - filter.includeModule("org.apache.maven", "maven"); - filter.includeModule("org.apache.maven", "maven-artifact"); - filter.includeModule("org.apache.maven", "maven-parent"); - filter.includeModule("org.apache", "apache"); - filter.includeModule("org.apiguardian", "apiguardian-api"); - filter.includeModule("org.checkerframework", "checker-qual"); - filter.includeModule("org.codehaus.groovy", "groovy-bom"); - filter.includeModule("org.codehaus.plexus", "plexus"); - filter.includeModule("org.codehaus.plexus", "plexus-utils"); - filter.includeModule("org.commonmark", "commonmark"); - filter.includeModule("org.commonmark", "commonmark-parent"); - filter.includeModule("org.eclipse.ee4j", "project"); - filter.includeModule("org.eclipse.jetty", "jetty-bom"); - filter.includeModule("org.jcraft", "jorbis"); - filter.includeModule("org.jetbrains", "annotations"); - filter.includeModule("org.jline", "jline-parent"); - filter.includeModule("org.jline", "jline-reader"); - filter.includeModule("org.jline", "jline-terminal"); - filter.includeModule("org.joml", "joml"); - filter.includeModule("org.jspecify", "jspecify"); - filter.includeModule("org.junit.jupiter", "junit-jupiter"); - filter.includeModule("org.junit.jupiter", "junit-jupiter-api"); - filter.includeModule("org.junit.jupiter", "junit-jupiter-engine"); - filter.includeModule("org.junit.jupiter", "junit-jupiter-params"); - filter.includeModule("org.junit.platform", "junit-platform-commons"); - filter.includeModule("org.junit.platform", "junit-platform-engine"); - filter.includeModule("org.junit.platform", "junit-platform-launcher"); - filter.includeModule("org.junit", "junit-bom"); - filter.includeModule("org.lwjgl", "lwjgl"); - filter.includeModule("org.lwjgl", "lwjgl-bom"); - filter.includeModule("org.lwjgl", "lwjgl-freetype"); - filter.includeModule("org.lwjgl", "lwjgl-glfw"); - filter.includeModule("org.lwjgl", "lwjgl-jemalloc"); - filter.includeModule("org.lwjgl", "lwjgl-openal"); - filter.includeModule("org.lwjgl", "lwjgl-opengl"); - filter.includeModule("org.lwjgl", "lwjgl-stb"); - filter.includeModule("org.lwjgl", "lwjgl-tinyfd"); - filter.includeModule("org.lz4", "lz4-java"); - filter.includeModule("org.mockito", "mockito-bom"); - filter.includeModule("org.openjdk.nashorn", "nashorn-core"); - filter.includeModule("org.opentest4j", "opentest4j"); - filter.includeModule("org.ow2.asm", "asm"); - filter.includeModule("org.ow2.asm", "asm-analysis"); - filter.includeModule("org.ow2.asm", "asm-commons"); - filter.includeModule("org.ow2.asm", "asm-tree"); - filter.includeModule("org.ow2.asm", "asm-util"); - filter.includeModule("org.ow2", "ow2"); - filter.includeModule("org.parchmentmc.data", "parchment-1.21"); - filter.includeModule("org.slf4j", "slf4j-api"); - filter.includeModule("org.slf4j", "slf4j-bom"); - filter.includeModule("org.slf4j", "slf4j-parent"); - filter.includeModule("org.sonatype.oss", "oss-parent"); - filter.includeModule("org.springframework", "spring-framework-bom"); - filter.includeModule("org.tukaani", "xz"); - filter.includeModule("org.vineflower", "vineflower"); - filter.includeModule("trove", "trove"); + for (var entry : STABLE_MODULES) { + filter.includeModule(entry[0], entry[1]); + } + + synchronized (gameLibraryModules) { + for (var coordinate : gameLibraryModules) { + var parts = coordinate.split(":", 2); + if (parts.length == 2) { + filter.includeModule(parts[0], parts[1]); + } + } + } } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index 011de18f..18f719cd 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -14,18 +14,28 @@ /** * This plugin acts in different roles depending on where it is applied: *
    - *
  • At the project-level, it will add the required repositories for moddev.
  • - *
  • At the settings-level, it will add the required repositories to the dependency management block, and add a marker plugin to the Gradle instance to prevent the - * repositories from being added again at the project-level.
  • + *
  • At the project-level, it will add the required repositories for moddev. + * The NeoForge repository content filter is deferred so that it can be + * populated dynamically from the selected NeoForge version metadata.
  • + *
  • At the settings-level, it will add the required repositories to the dependency + * management block with an immediate content filter, and add a marker plugin to + * the Gradle instance to prevent the repositories from being added again at the + * project-level.
  • *
*/ public class RepositoriesPlugin implements Plugin { + static final String NEOFORGE_REPO_EXTENSION = "__internal_neoForgeRepository"; + @Override public void apply(PluginAware target) { if (target instanceof Project project) { - applyRepositories(project.getRepositories()); + // Defer content filter so ModDevPlugin.enable() can populate it dynamically + var neoRepo = applyRepositories(project.getRepositories(), false); + project.getExtensions().add(MavenArtifactRepository.class, NEOFORGE_REPO_EXTENSION, neoRepo); } else if (target instanceof Settings settings) { - applyRepositories(settings.getDependencyResolutionManagement().getRepositories()); + // Settings-level: apply content filter immediately since per-project + // dynamic population is not possible for shared repositories + applyRepositories(settings.getDependencyResolutionManagement().getRepositories(), true); settings.getGradle().getPlugins().apply(getClass()); // Add a marker to Gradle } else if (target instanceof Gradle gradle) { // Do nothing @@ -34,7 +44,27 @@ public void apply(PluginAware target) { } } - private void applyRepositories(RepositoryHandler repositories) { + /** + * Returns the NeoForge repository. Exposed for dynamic filter population. + */ + static MavenArtifactRepository getNeoForgeRepository(Project project) { + return (MavenArtifactRepository) project.getExtensions().getByName(NEOFORGE_REPO_EXTENSION); + } + + /** + * Applies the content filter to the NeoForge repository, including any + * dynamically discovered game library modules from + * {@link NeoForgedRepositoryFilter#addGameLibrary(String, String)}. + *

+ * Must be called before any dependency resolution uses this repository — + * Gradle locks the content descriptor on first use. + */ + static void applyContentFilter(Project project) { + var neoRepo = getNeoForgeRepository(project); + neoRepo.content(NeoForgedRepositoryFilter::filter); + } + + private static MavenArtifactRepository applyRepositories(RepositoryHandler repositories, boolean applyContentFilter) { var mojangMaven = repositories.maven(repo -> { repo.setName("Mojang Minecraft Libraries"); repo.setUrl(URI.create("https://libraries.minecraft.net/")); @@ -53,11 +83,14 @@ private void applyRepositories(RepositoryHandler repositories) { }); sortFirst(repositories, mojangMetaMaven); - repositories.maven(repo -> { + var neoForgeRepo = repositories.maven(repo -> { repo.setName("NeoForged Releases"); repo.setUrl(URI.create("https://maven.neoforged.net/releases/")); - repo.content(NeoForgedRepositoryFilter::filter); + if (applyContentFilter) { + repo.content(NeoForgedRepositoryFilter::filter); + } }); + return neoForgeRepo; } private static void sortFirst(RepositoryHandler repositories, MavenArtifactRepository repo) { diff --git a/src/main/java/net/neoforged/nfrtgradle/NeoFormRuntimeExtension.java b/src/main/java/net/neoforged/nfrtgradle/NeoFormRuntimeExtension.java index 80577475..2b9e1244 100644 --- a/src/main/java/net/neoforged/nfrtgradle/NeoFormRuntimeExtension.java +++ b/src/main/java/net/neoforged/nfrtgradle/NeoFormRuntimeExtension.java @@ -24,6 +24,14 @@ public NeoFormRuntimeExtension(Project project) { getLauncherManifestUrl().convention(PropertyUtils.getStringProperty(project, "neoForge.neoFormRuntime.launcherManifestUrl")); } + /** + * Returns the effective NFRT version from the project's extension. + */ + public static String getVersion(Project project) { + var ext = project.getExtensions().getByType(NeoFormRuntimeExtension.class); + return ext.getVersion().get(); + } + /** * Overrides the version of NFRT to use. This is an advanced feature. This plugin will default to a * compatible version. From dc9df590914bc5d9ed4d8cc11800d405033f0e93 Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 02:38:45 +0800 Subject: [PATCH 4/7] Enhance NeoForgedRepositoryFilter: ensure dynamic module discovery clears stale state and applies content filter consistently --- .../moddevgradle/internal/ModDevPlugin.java | 51 ++++++++------- .../internal/NeoForgedRepositoryFilter.java | 10 +++ .../internal/RepositoriesPlugin.java | 15 ++++- .../internal/ModDevPluginTest.java | 62 +++++++++++++++++++ 4 files changed, 111 insertions(+), 27 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index efd02d50..4c3f8423 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -16,6 +16,7 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.ModuleDependency; import org.gradle.api.plugins.JavaLibraryPlugin; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,9 +88,10 @@ public void enable( versionCapabilities) : ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation); - if (neoForgeVersion != null) { - populateNeoForgeRepositoryFilter(project, neoForgeVersion); - } + // Always apply at least the stable baseline filter to the NeoForged + // repository. When a NeoForge version is selected we also discover + // additional game-library modules from its metadata. + populateNeoForgeRepositoryFilter(project, neoForgeVersion); ArtifactNamingStrategy artifactNamingStrategy; // It's helpful to be able to differentiate the Vanilla jar and the NeoForge jar in classic multiloader setups. @@ -129,28 +131,29 @@ public void enable( * The HTTP download bypasses Gradle's dependency resolution so the repository content * descriptor stays unlocked and can receive its first {@code content()} call. */ - private static void populateNeoForgeRepositoryFilter(Project project, String neoForgeVersion) { - // Regex to extract group:module pairs from Gradle Module Metadata JSON. - var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); - - // 1. Discover game library modules from the NeoForge artifact metadata. - fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion - + "/neoforge-" + neoForgeVersion + ".module", depPattern); - - // 2. Discover build tool modules from the NeoForm Runtime metadata. - // NFRT ships external tools (DiffPatch, AutoRenamingTool, etc.) whose - // transitive dependencies are also rehosted on the NeoForged Maven. - var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); - fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion - + "/neoform-runtime-" + nfrtVersion + ".module", depPattern); - - // Apply the content filter now — before any dependency resolution uses the - // NeoForge repository. - try { - RepositoriesPlugin.applyContentFilter(project); - } catch (Exception e) { - LOG.warn("Failed to apply NeoForge repository content filter: {}", e.getMessage()); + private static void populateNeoForgeRepositoryFilter(Project project, + @Nullable String neoForgeVersion) { + // Clear any stale dynamic modules from a previous build in this daemon. + NeoForgedRepositoryFilter.clearGameLibraries(); + + if (neoForgeVersion != null) { + var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); + + // Discover game library modules from the NeoForge artifact metadata. + fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion + + "/neoforge-" + neoForgeVersion + ".module", depPattern); + + // Discover build tool modules from the NeoForm Runtime metadata. + var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); + fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion + + "/neoform-runtime-" + nfrtVersion + ".module", depPattern); } + + // Apply the content filter now — before any dependency resolution uses + // the NeoForge repository. In the vanilla-only case this installs the + // stable baseline; when a NeoForge version is selected it also includes + // any dynamically discovered modules. + RepositoriesPlugin.applyContentFilter(project); } private static void fetchModuleDependencies(String path, Pattern depPattern) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java index 79c3bd55..62e4d870 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java @@ -224,6 +224,16 @@ public static void addGameLibrary(String group, String module) { gameLibraryModules.add(group + ":" + module); } + /** + * Clears all dynamically discovered modules. Called at the start of + * {@code populateNeoForgeRepositoryFilter} so that each build computes its + * own allowed-module set deterministically, avoiding cross-build leakage + * when the Gradle daemon persists static state. + */ + static void clearGameLibraries() { + gameLibraryModules.clear(); + } + /** * Applies the full filter: stable known modules plus any dynamically discovered * game library modules for the selected NeoForge version. diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index 18f719cd..a5fd35db 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -10,6 +10,7 @@ import org.gradle.api.initialization.Settings; import org.gradle.api.invocation.Gradle; import org.gradle.api.plugins.PluginAware; +import org.jetbrains.annotations.Nullable; /** * This plugin acts in different roles depending on where it is applied: @@ -45,10 +46,13 @@ public void apply(PluginAware target) { } /** - * Returns the NeoForge repository. Exposed for dynamic filter population. + * Returns the NeoForge repository, or {@code null} when the repository was + * configured at the settings level and this project has no local reference. */ + @Nullable static MavenArtifactRepository getNeoForgeRepository(Project project) { - return (MavenArtifactRepository) project.getExtensions().getByName(NEOFORGE_REPO_EXTENSION); + var ext = project.getExtensions().findByName(NEOFORGE_REPO_EXTENSION); + return (MavenArtifactRepository) ext; } /** @@ -58,10 +62,15 @@ static MavenArtifactRepository getNeoForgeRepository(Project project) { *

* Must be called before any dependency resolution uses this repository — * Gradle locks the content descriptor on first use. + *

+ * When the repository was configured at the settings level this is a safe + * no-op — the content filter is already installed from {@code apply()}. */ static void applyContentFilter(Project project) { var neoRepo = getNeoForgeRepository(project); - neoRepo.content(NeoForgedRepositoryFilter::filter); + if (neoRepo != null) { + neoRepo.content(NeoForgedRepositoryFilter::filter); + } } private static MavenArtifactRepository applyRepositories(RepositoryHandler repositories, boolean applyContentFilter) { diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java index 09acc348..b2a9a008 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java @@ -174,6 +174,68 @@ private void assertContainsModdingCompileDependencies(String version, String con "net.neoforged:neoforge:" + version + "[net.neoforged:neoforge-dependencies]"); } + @Nested + class RepositoryFilter { + @Test + void testContentFilterAppliedWhenNeoForgeVersionIsSet() { + extension.setVersion("21.10.48-beta"); + + var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); + assertThat(neoRepo).isNotNull(); + // The content filter should be installed on the NeoForge repository. + // We cannot inspect the filter rules directly through public API, but we + // can verify the repository exists and is accessible. + assertThat(project.getRepositories().stream() + .filter(r -> "NeoForged Releases".equals(r.getName())) + .findFirst()).isPresent(); + } + + @Test + void testContentFilterAppliedInVanillaOnlyMode() { + extension.setNeoFormVersion("1.21.4-20240101.235959"); + + var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); + assertThat(neoRepo).isNotNull(); + // The stable baseline filter must be installed even when no NeoForge + // version is selected, otherwise the NeoForged Maven (which mirrors + // Maven Central) would be unfiltered. + assertThat(project.getRepositories().stream() + .filter(r -> "NeoForged Releases".equals(r.getName())) + .findFirst()).isPresent(); + } + + @Test + void testApplyContentFilterIsNoOpWhenRepositoryNotOnProject() { + // Create a fresh project without ModDevPlugin — the NeoForge repository + // extension is never registered, so applyContentFilter must not throw. + var freshProject = ProjectBuilder.builder().build(); + // Must not throw, even though there is no NeoForge repository extension. + RepositoriesPlugin.applyContentFilter(freshProject); + } + + @Test + void testDynamicModuleDiscoveryClearsStaleState() { + // Simulate a first enable — should populate dynamic modules. + extension.setVersion("21.10.48-beta"); + + // Run a second enable on a fresh project with a different version. + // The dynamic set must be cleared first so the previous version's + // modules do not leak. + var project2 = ProjectBuilder.builder().build(); + project2.getPlugins().apply(ModDevPlugin.class); + var ext2 = ExtensionUtils.getExtension(project2, "neoForge", NeoForgeExtension.class); + var java2 = ExtensionUtils.getExtension(project2, "java", JavaPluginExtension.class); + java2.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); + + // If state leaked, this would carry modules from "21.10.48-beta". + // The filter should still be applied successfully. + ext2.setVersion("21.0.133-beta"); + + var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project2); + assertThat(neoRepo).isNotNull(); + } + } + private void assertContainsModdingRuntimeDependencies(String version, String configurationName) { var configuration = project.getConfigurations().getByName(configurationName); From aadc46c732afb95c4792198876205c4710c0339e Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 02:59:08 +0800 Subject: [PATCH 5/7] Enhance NeoForgedRepositoryFilter: implement dynamic module discovery with isolated state for parallel project configuration --- .../moddevgradle/internal/ModDevPlugin.java | 47 +++--- .../internal/NeoForgedRepositoryFilter.java | 60 +++----- .../internal/RepositoriesPlugin.java | 15 +- .../internal/ModDevPluginTest.java | 138 +++++++++++++++--- 4 files changed, 173 insertions(+), 87 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index 4c3f8423..65959c3c 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -2,6 +2,8 @@ import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Pattern; import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin; import net.neoforged.moddevgradle.dsl.ModDevExtension; @@ -123,40 +125,49 @@ public void enable( } /** - * Downloads the Gradle Module Metadata (.module file) for the given NeoForge version - * directly via HTTP to discover which game library modules it transitively depends on. - * Registers them in {@link NeoForgedRepositoryFilter} and then applies the content + * Discovers additional modules from the Gradle Module Metadata of the selected + * NeoForge version (if any) and the NeoForm Runtime, then applies the content * filter to the NeoForge repository. *

- * The HTTP download bypasses Gradle's dependency resolution so the repository content - * descriptor stays unlocked and can receive its first {@code content()} call. + * Dynamic modules are collected in a local set and passed directly to + * {@link NeoForgedRepositoryFilter#filter} — no static mutable state is shared + * across projects, which is safe with parallel project configuration. + *

+ * NFRT metadata is always fetched regardless of whether a NeoForge version was + * selected, since NFRT can introduce new direct dependencies over time even in + * vanilla-only mode. */ private static void populateNeoForgeRepositoryFilter(Project project, @Nullable String neoForgeVersion) { - // Clear any stale dynamic modules from a previous build in this daemon. - NeoForgedRepositoryFilter.clearGameLibraries(); + var dynamicModules = new HashSet(); + var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); if (neoForgeVersion != null) { - var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); - // Discover game library modules from the NeoForge artifact metadata. fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion - + "/neoforge-" + neoForgeVersion + ".module", depPattern); + + "/neoforge-" + neoForgeVersion + ".module", depPattern, dynamicModules); + } - // Discover build tool modules from the NeoForm Runtime metadata. + // Always discover build tool modules from the NeoForm Runtime metadata. + // NFRT ships external tools (DiffPatch, AutoRenamingTool, etc.) whose + // transitive dependencies are rehosted on the NeoForged Maven and may + // change across NFRT releases. + try { var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion - + "/neoform-runtime-" + nfrtVersion + ".module", depPattern); + + "/neoform-runtime-" + nfrtVersion + ".module", depPattern, dynamicModules); + } catch (Exception e) { + LOG.warn("Failed to resolve NFRT version for dynamic filter discovery: {}", e.getMessage()); } // Apply the content filter now — before any dependency resolution uses - // the NeoForge repository. In the vanilla-only case this installs the - // stable baseline; when a NeoForge version is selected it also includes - // any dynamically discovered modules. - RepositoriesPlugin.applyContentFilter(project); + // the NeoForge repository. Each enable() call passes its own local set, + // so parallel project configuration is safe. + RepositoriesPlugin.applyContentFilter(project, dynamicModules); } - private static void fetchModuleDependencies(String path, Pattern depPattern) { + private static void fetchModuleDependencies(String path, Pattern depPattern, + Set dynamicModules) { try { var moduleUrl = URI.create( "https://maven.neoforged.net/releases/" + path).toURL(); @@ -170,7 +181,7 @@ private static void fetchModuleDependencies(String path, Pattern depPattern) { if ("*".equals(group) || "*".equals(module)) { continue; } - NeoForgedRepositoryFilter.addGameLibrary(group, module); + dynamicModules.add(group + ":" + module); } } } catch (Exception e) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java index 62e4d870..3dd9b4e4 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java @@ -1,10 +1,7 @@ package net.neoforged.moddevgradle.internal; -import java.util.Collections; -import java.util.HashSet; import java.util.Set; import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor; -import org.jetbrains.annotations.ApiStatus; /** * Controls which modules Gradle may resolve from the NeoForged Maven. @@ -17,11 +14,11 @@ *

  • Stable baseline — the full set of modules known to be hosted on the * NeoForged Maven. This covers NeoForge artifacts, modding toolchain projects, and * their transitive dependencies at the time the plugin was built.
  • - *
  • Dynamic discovery — at configuration time, the plugin downloads the Gradle - * Module Metadata for the selected NeoForge and NeoForm Runtime versions and adds - * any newly-declared direct dependencies to the filter. This allows new Minecraft - * releases to work without a plugin update, provided their libraries are also - * mirrored.
  • + *
  • Dynamic modules — discovered at configuration time by downloading the + * Gradle Module Metadata for the selected NeoForge and NeoForm Runtime versions. + * The caller passes these in as a {@code Set} keyed by {@code "group:module"}. + * This allows new Minecraft releases to work without a plugin update, provided + * their libraries are also mirrored.
  • * */ public class NeoForgedRepositoryFilter { @@ -210,45 +207,22 @@ public class NeoForgedRepositoryFilter { // @formatter:on /** - * Dynamically discovered game library modules, keyed by "group:module". - * Populated by {@link #addGameLibrary} before dependency resolution. + * Applies the full filter to the given repository content descriptor: stable + * known modules plus caller-supplied dynamically discovered modules. + * + * @param descriptor the repository content descriptor to configure + * @param dynamicModules set of {@code "group:module"} strings discovered at + * configuration time; may be empty but never null */ - private static final Set gameLibraryModules = Collections.synchronizedSet(new HashSet<>()); - - /** - * Adds a game library module (group:name) to the allowed set. - * Safe to call from any thread before dependency resolution begins. - */ - @ApiStatus.Internal - public static void addGameLibrary(String group, String module) { - gameLibraryModules.add(group + ":" + module); - } - - /** - * Clears all dynamically discovered modules. Called at the start of - * {@code populateNeoForgeRepositoryFilter} so that each build computes its - * own allowed-module set deterministically, avoiding cross-build leakage - * when the Gradle daemon persists static state. - */ - static void clearGameLibraries() { - gameLibraryModules.clear(); - } - - /** - * Applies the full filter: stable known modules plus any dynamically discovered - * game library modules for the selected NeoForge version. - */ - public static void filter(RepositoryContentDescriptor filter) { + public static void filter(RepositoryContentDescriptor descriptor, Set dynamicModules) { for (var entry : STABLE_MODULES) { - filter.includeModule(entry[0], entry[1]); + descriptor.includeModule(entry[0], entry[1]); } - synchronized (gameLibraryModules) { - for (var coordinate : gameLibraryModules) { - var parts = coordinate.split(":", 2); - if (parts.length == 2) { - filter.includeModule(parts[0], parts[1]); - } + for (var coordinate : dynamicModules) { + var parts = coordinate.split(":", 2); + if (parts.length == 2) { + descriptor.includeModule(parts[0], parts[1]); } } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index a5fd35db..b85d7091 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -1,6 +1,7 @@ package net.neoforged.moddevgradle.internal; import java.net.URI; +import java.util.Set; import net.neoforged.moddevgradle.internal.generated.MojangRepositoryFilter; import org.gradle.api.GradleException; import org.gradle.api.Plugin; @@ -56,20 +57,22 @@ static MavenArtifactRepository getNeoForgeRepository(Project project) { } /** - * Applies the content filter to the NeoForge repository, including any - * dynamically discovered game library modules from - * {@link NeoForgedRepositoryFilter#addGameLibrary(String, String)}. + * Applies the content filter to the NeoForge repository, including the + * caller-supplied dynamically discovered modules. *

    * Must be called before any dependency resolution uses this repository — * Gradle locks the content descriptor on first use. *

    * When the repository was configured at the settings level this is a safe * no-op — the content filter is already installed from {@code apply()}. + * + * @param dynamicModules set of {@code "group:module"} strings discovered at + * configuration time; empty set when none were discovered */ - static void applyContentFilter(Project project) { + static void applyContentFilter(Project project, Set dynamicModules) { var neoRepo = getNeoForgeRepository(project); if (neoRepo != null) { - neoRepo.content(NeoForgedRepositoryFilter::filter); + neoRepo.content(descriptor -> NeoForgedRepositoryFilter.filter(descriptor, dynamicModules)); } } @@ -96,7 +99,7 @@ private static MavenArtifactRepository applyRepositories(RepositoryHandler repos repo.setName("NeoForged Releases"); repo.setUrl(URI.create("https://maven.neoforged.net/releases/")); if (applyContentFilter) { - repo.content(NeoForgedRepositoryFilter::filter); + repo.content(descriptor -> NeoForgedRepositoryFilter.filter(descriptor, Set.of())); } }); return neoForgeRepo; diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java index b2a9a008..f00b3d5d 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.HashSet; import java.util.Set; import net.neoforged.moddevgradle.AbstractProjectBuilderTest; import net.neoforged.moddevgradle.dsl.NeoForgeExtension; @@ -11,6 +12,8 @@ import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Task; +import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor; +import org.gradle.api.attributes.Attribute; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.jvm.toolchain.JavaLanguageVersion; @@ -177,14 +180,45 @@ private void assertContainsModdingCompileDependencies(String version, String con @Nested class RepositoryFilter { @Test - void testContentFilterAppliedWhenNeoForgeVersionIsSet() { + void testFilterIncludesStableModules() { + var descriptor = new RecordingDescriptor(); + NeoForgedRepositoryFilter.filter(descriptor, Set.of()); + + // The stable baseline must include NeoForge's own artifacts. + assertThat(descriptor.included).anyMatch( + r -> r[0].equals("net.neoforged") && r[1].equals("neoforge")); + } + + @Test + void testFilterIncludesDynamicModules() { + var descriptor = new RecordingDescriptor(); + var dynamic = Set.of("com.example:new-lib", "org.test:another"); + NeoForgedRepositoryFilter.filter(descriptor, dynamic); + + assertThat(descriptor.included).anyMatch( + r -> r[0].equals("com.example") && r[1].equals("new-lib")); + assertThat(descriptor.included).anyMatch( + r -> r[0].equals("org.test") && r[1].equals("another")); + } + + @Test + void testFilterIgnoresMalformedDynamicEntries() { + var descriptor = new RecordingDescriptor(); + // Entries without a colon are skipped gracefully. + NeoForgedRepositoryFilter.filter(descriptor, Set.of("malformed")); + + // Stable modules still included; malformed entry did not throw. + assertThat(descriptor.included).isNotEmpty(); + assertThat(descriptor.included).noneMatch( + r -> r[0].equals("malformed")); + } + + @Test + void testContentFilterAppliedInNeoForgeMode() { extension.setVersion("21.10.48-beta"); var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); assertThat(neoRepo).isNotNull(); - // The content filter should be installed on the NeoForge repository. - // We cannot inspect the filter rules directly through public API, but we - // can verify the repository exists and is accessible. assertThat(project.getRepositories().stream() .filter(r -> "NeoForged Releases".equals(r.getName())) .findFirst()).isPresent(); @@ -196,9 +230,6 @@ void testContentFilterAppliedInVanillaOnlyMode() { var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); assertThat(neoRepo).isNotNull(); - // The stable baseline filter must be installed even when no NeoForge - // version is selected, otherwise the NeoForged Maven (which mirrors - // Maven Central) would be unfiltered. assertThat(project.getRepositories().stream() .filter(r -> "NeoForged Releases".equals(r.getName())) .findFirst()).isPresent(); @@ -206,33 +237,100 @@ void testContentFilterAppliedInVanillaOnlyMode() { @Test void testApplyContentFilterIsNoOpWhenRepositoryNotOnProject() { - // Create a fresh project without ModDevPlugin — the NeoForge repository - // extension is never registered, so applyContentFilter must not throw. var freshProject = ProjectBuilder.builder().build(); // Must not throw, even though there is no NeoForge repository extension. - RepositoriesPlugin.applyContentFilter(freshProject); + RepositoriesPlugin.applyContentFilter(freshProject, Set.of()); } @Test - void testDynamicModuleDiscoveryClearsStaleState() { - // Simulate a first enable — should populate dynamic modules. + void testApplyContentFilterDoesNotThrowWithEmptyDynamicSet() { + extension.setVersion("21.10.48-beta"); + var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); + assertThat(neoRepo).isNotNull(); + // Applying the filter with an empty dynamic set is valid (e.g. offline mode). + RepositoriesPlugin.applyContentFilter(project, Set.of()); + } + + @Test + void testParallelEnablesUseIsolatedDynamicSets() { + // Simulate two projects enabling modding concurrently — each call + // to populateNeoForgeRepositoryFilter passes its own local HashSet, + // so there is no shared mutable state between them. extension.setVersion("21.10.48-beta"); - // Run a second enable on a fresh project with a different version. - // The dynamic set must be cleared first so the previous version's - // modules do not leak. var project2 = ProjectBuilder.builder().build(); project2.getPlugins().apply(ModDevPlugin.class); var ext2 = ExtensionUtils.getExtension(project2, "neoForge", NeoForgeExtension.class); var java2 = ExtensionUtils.getExtension(project2, "java", JavaPluginExtension.class); java2.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); - - // If state leaked, this would carry modules from "21.10.48-beta". - // The filter should still be applied successfully. ext2.setVersion("21.0.133-beta"); - var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project2); - assertThat(neoRepo).isNotNull(); + // Both projects must have their NeoForge repository available without + // cross-contamination of dynamic module sets. + assertThat(RepositoriesPlugin.getNeoForgeRepository(project)).isNotNull(); + assertThat(RepositoriesPlugin.getNeoForgeRepository(project2)).isNotNull(); + } + + /** + * A minimal {@link RepositoryContentDescriptor} that records every + * {@code includeModule} call so tests can assert filter behavior. + */ + static class RecordingDescriptor implements RepositoryContentDescriptor { + final Set included = new HashSet<>(); + + @Override + public void includeModule(String group, String name) { + included.add(new String[] { group, name }); + } + + // Remaining methods are unused by NeoForgedRepositoryFilter; stub them out. + @Override + public void includeGroup(String group) {} + + @Override + public void includeGroupAndSubgroups(String groupPrefix) {} + + @Override + public void includeGroupByRegex(String groupRegex) {} + + @Override + public void includeModuleByRegex(String groupRegex, String nameRegex) {} + + @Override + public void includeVersion(String group, String name, String version) {} + + @Override + public void includeVersionByRegex(String groupRegex, String nameRegex, String versionRegex) {} + + @Override + public void excludeGroup(String group) {} + + @Override + public void excludeGroupAndSubgroups(String groupPrefix) {} + + @Override + public void excludeGroupByRegex(String groupRegex) {} + + @Override + public void excludeModule(String group, String name) {} + + @Override + public void excludeModuleByRegex(String groupRegex, String nameRegex) {} + + @Override + public void excludeVersion(String group, String name, String version) {} + + @Override + public void excludeVersionByRegex(String groupRegex, String nameRegex, String versionRegex) {} + + @Override + public void onlyForConfigurations(String... configurationNames) {} + + @Override + public void notForConfigurations(String... configurationNames) {} + + @Override + public void onlyForAttribute(Attribute attribute, T... validValues) {} } } From 1513dc3d9501ba78802a2612352c30f4e8f76026 Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 03:03:07 +0800 Subject: [PATCH 6/7] Refactor NeoForgedRepositoryFilter: standardize formatting of stable modules array --- .../internal/NeoForgedRepositoryFilter.java | 328 +++++++++--------- 1 file changed, 163 insertions(+), 165 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java index 3dd9b4e4..f16eff1b 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java @@ -28,183 +28,181 @@ public class NeoForgedRepositoryFilter { * the dynamic discovery path in {@link net.neoforged.moddevgradle.internal.ModDevPlugin} * adds it at configuration time. */ - // @formatter:off private static final String[][] STABLE_MODULES = { // --- net.neoforged sub-groups --- - {"net.neoforged.accesstransformers", "at-modlauncher"}, - {"net.neoforged.accesstransformers", "at-parser"}, - {"net.neoforged.fancymodloader", "earlydisplay"}, - {"net.neoforged.fancymodloader", "junit-fml"}, - {"net.neoforged.fancymodloader", "loader"}, - {"net.neoforged.installertools", "binarypatcher"}, - {"net.neoforged.installertools", "cli-utils"}, - {"net.neoforged.installertools", "installertools"}, - {"net.neoforged.javadoctor", "gson-io"}, - {"net.neoforged.javadoctor", "spec"}, - {"net.neoforged.jst", "jst-cli-bundle"}, + { "net.neoforged.accesstransformers", "at-modlauncher" }, + { "net.neoforged.accesstransformers", "at-parser" }, + { "net.neoforged.fancymodloader", "earlydisplay" }, + { "net.neoforged.fancymodloader", "junit-fml" }, + { "net.neoforged.fancymodloader", "loader" }, + { "net.neoforged.installertools", "binarypatcher" }, + { "net.neoforged.installertools", "cli-utils" }, + { "net.neoforged.installertools", "installertools" }, + { "net.neoforged.javadoctor", "gson-io" }, + { "net.neoforged.javadoctor", "spec" }, + { "net.neoforged.jst", "jst-cli-bundle" }, // --- main net.neoforged group --- - {"net.neoforged", "AutoRenamingTool"}, - {"net.neoforged", "DevLaunch"}, - {"net.neoforged", "JarJarFileSystems"}, - {"net.neoforged", "JarJarMetadata"}, - {"net.neoforged", "JarJarSelector"}, - {"net.neoforged", "accesstransformers"}, - {"net.neoforged", "bus"}, - {"net.neoforged", "coremods"}, - {"net.neoforged", "mergetool"}, - {"net.neoforged", "minecraft-dependencies"}, - {"net.neoforged", "neoforge"}, - {"net.neoforged", "neoform"}, - {"net.neoforged", "neoform-runtime"}, - {"net.neoforged", "srgutils"}, - {"net.neoforged", "testframework"}, + { "net.neoforged", "AutoRenamingTool" }, + { "net.neoforged", "DevLaunch" }, + { "net.neoforged", "JarJarFileSystems" }, + { "net.neoforged", "JarJarMetadata" }, + { "net.neoforged", "JarJarSelector" }, + { "net.neoforged", "accesstransformers" }, + { "net.neoforged", "bus" }, + { "net.neoforged", "coremods" }, + { "net.neoforged", "mergetool" }, + { "net.neoforged", "minecraft-dependencies" }, + { "net.neoforged", "neoforge" }, + { "net.neoforged", "neoform" }, + { "net.neoforged", "neoform-runtime" }, + { "net.neoforged", "srgutils" }, + { "net.neoforged", "testframework" }, // --- modding toolchain --- - {"cpw.mods", "bootstraplauncher"}, - {"cpw.mods", "modlauncher"}, - {"cpw.mods", "securejarhandler"}, - {"net.minecraftforge", "mergetool"}, - {"net.minecraftforge", "srgutils"}, - {"net.minecrell", "terminalconsoleappender"}, - {"net.covers1624", "DevLogin"}, - {"net.covers1624", "Quack"}, - {"io.codechicken", "DiffPatch"}, - {"io.github.llamalad7", "mixinextras-neoforge"}, - {"net.fabricmc", "sponge-mixin"}, - {"de.siegmar", "fastcsv"}, - {"com.machinezoo.noexception", "noexception"}, - {"net.jodah", "typetools"}, - {"com.nothome", "javaxdelta"}, - {"trove", "trove"}, + { "cpw.mods", "bootstraplauncher" }, + { "cpw.mods", "modlauncher" }, + { "cpw.mods", "securejarhandler" }, + { "net.minecraftforge", "mergetool" }, + { "net.minecraftforge", "srgutils" }, + { "net.minecrell", "terminalconsoleappender" }, + { "net.covers1624", "DevLogin" }, + { "net.covers1624", "Quack" }, + { "io.codechicken", "DiffPatch" }, + { "io.github.llamalad7", "mixinextras-neoforge" }, + { "net.fabricmc", "sponge-mixin" }, + { "de.siegmar", "fastcsv" }, + { "com.machinezoo.noexception", "noexception" }, + { "net.jodah", "typetools" }, + { "com.nothome", "javaxdelta" }, + { "trove", "trove" }, // --- NeoForge-maintained library forks --- - {"com.electronwill.night-config", "core"}, - {"com.electronwill.night-config", "toml"}, + { "com.electronwill.night-config", "core" }, + { "com.electronwill.night-config", "toml" }, // --- Transitive dependencies of NFRT external tools --- - {"it.unimi.dsi", "fastutil"}, - {"org.apache.commons", "commons-compress"}, - {"org.apache.commons", "commons-lang3"}, - {"org.apache.commons", "commons-parent"}, - {"org.tukaani", "xz"}, - {"org.ow2.asm", "asm"}, - {"org.ow2.asm", "asm-analysis"}, - {"org.ow2.asm", "asm-commons"}, - {"org.ow2.asm", "asm-tree"}, - {"org.ow2.asm", "asm-util"}, - {"org.ow2", "ow2"}, - {"org.lz4", "lz4-java"}, - {"org.jcraft", "jorbis"}, + { "it.unimi.dsi", "fastutil" }, + { "org.apache.commons", "commons-compress" }, + { "org.apache.commons", "commons-lang3" }, + { "org.apache.commons", "commons-parent" }, + { "org.tukaani", "xz" }, + { "org.ow2.asm", "asm" }, + { "org.ow2.asm", "asm-analysis" }, + { "org.ow2.asm", "asm-commons" }, + { "org.ow2.asm", "asm-tree" }, + { "org.ow2.asm", "asm-util" }, + { "org.ow2", "ow2" }, + { "org.lz4", "lz4-java" }, + { "org.jcraft", "jorbis" }, // --- Game libraries & their transitive dependencies --- // Transitive dependencies of common game libraries are listed explicitly // because the dynamic discovery only sees direct dependencies (a single // module-metadata download), not the full transitive tree. - {"ca.weblite", "java-objc-bridge"}, - {"com.fasterxml.jackson.core", "jackson-annotations"}, - {"com.fasterxml.jackson.core", "jackson-core"}, - {"com.fasterxml.jackson", "jackson-base"}, - {"com.fasterxml.jackson", "jackson-bom"}, - {"com.fasterxml.jackson", "jackson-parent"}, - {"com.fasterxml", "oss-parent"}, - {"com.github.oshi", "oshi-core"}, - {"com.github.oshi", "oshi-parent"}, - {"com.google.code.findbugs", "jsr305"}, - {"com.google.code.gson", "gson"}, - {"com.google.code.gson", "gson-parent"}, - {"com.google.errorprone", "error_prone_annotations"}, - {"com.google.errorprone", "error_prone_parent"}, - {"com.google.guava", "failureaccess"}, - {"com.google.guava", "guava"}, - {"com.google.guava", "guava-parent"}, - {"com.google.guava", "listenablefuture"}, - {"com.google.j2objc", "j2objc-annotations"}, - {"com.ibm.icu", "icu4j"}, - {"com.mojang", "authlib"}, - {"com.mojang", "blocklist"}, - {"com.mojang", "brigadier"}, - {"com.mojang", "datafixerupper"}, - {"com.mojang", "logging"}, - {"com.mojang", "patchy"}, - {"com.mojang", "text2speech"}, - {"commons-codec", "commons-codec"}, - {"commons-io", "commons-io"}, - {"commons-logging", "commons-logging"}, - {"io.fabric8", "kubernetes-client-bom"}, - {"io.netty", "netty-bom"}, - {"io.netty", "netty-buffer"}, - {"io.netty", "netty-codec"}, - {"io.netty", "netty-common"}, - {"io.netty", "netty-handler"}, - {"io.netty", "netty-parent"}, - {"io.netty", "netty-resolver"}, - {"io.netty", "netty-transport"}, - {"io.netty", "netty-transport-classes-epoll"}, - {"io.netty", "netty-transport-native-unix-common"}, - {"jakarta.platform", "jakarta.jakartaee-bom"}, - {"jakarta.platform", "jakartaee-api-parent"}, - {"net.java.dev.jna", "jna"}, - {"net.java.dev.jna", "jna-platform"}, - {"org.antlr", "antlr4-master"}, - {"org.antlr", "antlr4-runtime"}, - {"org.apache.groovy", "groovy-bom"}, - {"org.apache.httpcomponents", "httpclient"}, - {"org.apache.httpcomponents", "httpcomponents-client"}, - {"org.apache.httpcomponents", "httpcomponents-core"}, - {"org.apache.httpcomponents", "httpcomponents-parent"}, - {"org.apache.httpcomponents", "httpcore"}, - {"org.apache.logging.log4j", "log4j"}, - {"org.apache.logging.log4j", "log4j-api"}, - {"org.apache.logging.log4j", "log4j-bom"}, - {"org.apache.logging.log4j", "log4j-core"}, - {"org.apache.logging.log4j", "log4j-slf4j2-impl"}, - {"org.apache.logging", "logging-parent"}, - {"org.apache.maven", "maven"}, - {"org.apache.maven", "maven-artifact"}, - {"org.apache.maven", "maven-parent"}, - {"org.apache", "apache"}, - {"org.apiguardian", "apiguardian-api"}, - {"org.checkerframework", "checker-qual"}, - {"org.codehaus.groovy", "groovy-bom"}, - {"org.codehaus.plexus", "plexus"}, - {"org.codehaus.plexus", "plexus-utils"}, - {"org.commonmark", "commonmark"}, - {"org.commonmark", "commonmark-parent"}, - {"org.eclipse.ee4j", "project"}, - {"org.eclipse.jetty", "jetty-bom"}, - {"org.jetbrains", "annotations"}, - {"org.jline", "jline-parent"}, - {"org.jline", "jline-reader"}, - {"org.jline", "jline-terminal"}, - {"org.joml", "joml"}, - {"org.jspecify", "jspecify"}, - {"org.junit.jupiter", "junit-jupiter"}, - {"org.junit.jupiter", "junit-jupiter-api"}, - {"org.junit.jupiter", "junit-jupiter-engine"}, - {"org.junit.jupiter", "junit-jupiter-params"}, - {"org.junit.platform", "junit-platform-commons"}, - {"org.junit.platform", "junit-platform-engine"}, - {"org.junit.platform", "junit-platform-launcher"}, - {"org.junit", "junit-bom"}, - {"org.lwjgl", "lwjgl"}, - {"org.lwjgl", "lwjgl-bom"}, - {"org.lwjgl", "lwjgl-freetype"}, - {"org.lwjgl", "lwjgl-glfw"}, - {"org.lwjgl", "lwjgl-jemalloc"}, - {"org.lwjgl", "lwjgl-openal"}, - {"org.lwjgl", "lwjgl-opengl"}, - {"org.lwjgl", "lwjgl-stb"}, - {"org.lwjgl", "lwjgl-tinyfd"}, - {"org.mockito", "mockito-bom"}, - {"org.opentest4j", "opentest4j"}, - {"org.slf4j", "slf4j-api"}, - {"org.slf4j", "slf4j-bom"}, - {"org.slf4j", "slf4j-parent"}, - {"org.sonatype.oss", "oss-parent"}, - {"org.springframework", "spring-framework-bom"}, + { "ca.weblite", "java-objc-bridge" }, + { "com.fasterxml.jackson.core", "jackson-annotations" }, + { "com.fasterxml.jackson.core", "jackson-core" }, + { "com.fasterxml.jackson", "jackson-base" }, + { "com.fasterxml.jackson", "jackson-bom" }, + { "com.fasterxml.jackson", "jackson-parent" }, + { "com.fasterxml", "oss-parent" }, + { "com.github.oshi", "oshi-core" }, + { "com.github.oshi", "oshi-parent" }, + { "com.google.code.findbugs", "jsr305" }, + { "com.google.code.gson", "gson" }, + { "com.google.code.gson", "gson-parent" }, + { "com.google.errorprone", "error_prone_annotations" }, + { "com.google.errorprone", "error_prone_parent" }, + { "com.google.guava", "failureaccess" }, + { "com.google.guava", "guava" }, + { "com.google.guava", "guava-parent" }, + { "com.google.guava", "listenablefuture" }, + { "com.google.j2objc", "j2objc-annotations" }, + { "com.ibm.icu", "icu4j" }, + { "com.mojang", "authlib" }, + { "com.mojang", "blocklist" }, + { "com.mojang", "brigadier" }, + { "com.mojang", "datafixerupper" }, + { "com.mojang", "logging" }, + { "com.mojang", "patchy" }, + { "com.mojang", "text2speech" }, + { "commons-codec", "commons-codec" }, + { "commons-io", "commons-io" }, + { "commons-logging", "commons-logging" }, + { "io.fabric8", "kubernetes-client-bom" }, + { "io.netty", "netty-bom" }, + { "io.netty", "netty-buffer" }, + { "io.netty", "netty-codec" }, + { "io.netty", "netty-common" }, + { "io.netty", "netty-handler" }, + { "io.netty", "netty-parent" }, + { "io.netty", "netty-resolver" }, + { "io.netty", "netty-transport" }, + { "io.netty", "netty-transport-classes-epoll" }, + { "io.netty", "netty-transport-native-unix-common" }, + { "jakarta.platform", "jakarta.jakartaee-bom" }, + { "jakarta.platform", "jakartaee-api-parent" }, + { "net.java.dev.jna", "jna" }, + { "net.java.dev.jna", "jna-platform" }, + { "org.antlr", "antlr4-master" }, + { "org.antlr", "antlr4-runtime" }, + { "org.apache.groovy", "groovy-bom" }, + { "org.apache.httpcomponents", "httpclient" }, + { "org.apache.httpcomponents", "httpcomponents-client" }, + { "org.apache.httpcomponents", "httpcomponents-core" }, + { "org.apache.httpcomponents", "httpcomponents-parent" }, + { "org.apache.httpcomponents", "httpcore" }, + { "org.apache.logging.log4j", "log4j" }, + { "org.apache.logging.log4j", "log4j-api" }, + { "org.apache.logging.log4j", "log4j-bom" }, + { "org.apache.logging.log4j", "log4j-core" }, + { "org.apache.logging.log4j", "log4j-slf4j2-impl" }, + { "org.apache.logging", "logging-parent" }, + { "org.apache.maven", "maven" }, + { "org.apache.maven", "maven-artifact" }, + { "org.apache.maven", "maven-parent" }, + { "org.apache", "apache" }, + { "org.apiguardian", "apiguardian-api" }, + { "org.checkerframework", "checker-qual" }, + { "org.codehaus.groovy", "groovy-bom" }, + { "org.codehaus.plexus", "plexus" }, + { "org.codehaus.plexus", "plexus-utils" }, + { "org.commonmark", "commonmark" }, + { "org.commonmark", "commonmark-parent" }, + { "org.eclipse.ee4j", "project" }, + { "org.eclipse.jetty", "jetty-bom" }, + { "org.jetbrains", "annotations" }, + { "org.jline", "jline-parent" }, + { "org.jline", "jline-reader" }, + { "org.jline", "jline-terminal" }, + { "org.joml", "joml" }, + { "org.jspecify", "jspecify" }, + { "org.junit.jupiter", "junit-jupiter" }, + { "org.junit.jupiter", "junit-jupiter-api" }, + { "org.junit.jupiter", "junit-jupiter-engine" }, + { "org.junit.jupiter", "junit-jupiter-params" }, + { "org.junit.platform", "junit-platform-commons" }, + { "org.junit.platform", "junit-platform-engine" }, + { "org.junit.platform", "junit-platform-launcher" }, + { "org.junit", "junit-bom" }, + { "org.lwjgl", "lwjgl" }, + { "org.lwjgl", "lwjgl-bom" }, + { "org.lwjgl", "lwjgl-freetype" }, + { "org.lwjgl", "lwjgl-glfw" }, + { "org.lwjgl", "lwjgl-jemalloc" }, + { "org.lwjgl", "lwjgl-openal" }, + { "org.lwjgl", "lwjgl-opengl" }, + { "org.lwjgl", "lwjgl-stb" }, + { "org.lwjgl", "lwjgl-tinyfd" }, + { "org.mockito", "mockito-bom" }, + { "org.opentest4j", "opentest4j" }, + { "org.slf4j", "slf4j-api" }, + { "org.slf4j", "slf4j-bom" }, + { "org.slf4j", "slf4j-parent" }, + { "org.sonatype.oss", "oss-parent" }, + { "org.springframework", "spring-framework-bom" }, // --- Other NeoForge-hosted tooling --- - {"org.parchmentmc.data", "parchment-1.21"}, - {"org.vineflower", "vineflower"}, - {"org.openjdk.nashorn", "nashorn-core"}, - {"net.sf.jopt-simple", "jopt-simple"}, + { "org.parchmentmc.data", "parchment-1.21" }, + { "org.vineflower", "vineflower" }, + { "org.openjdk.nashorn", "nashorn-core" }, + { "net.sf.jopt-simple", "jopt-simple" }, }; - // @formatter:on /** * Applies the full filter to the given repository content descriptor: stable From edb2a54edea81961200b4bd7b73ee5cb0fdeee11 Mon Sep 17 00:00:00 2001 From: Gugle Date: Wed, 20 May 2026 03:33:11 +0800 Subject: [PATCH 7/7] Enhance ModDevPlugin: implement HTTP timeout for module fetching and improve content filter application --- .../moddevgradle/internal/ModDevPlugin.java | 67 +++++++++++++------ .../internal/ModDevPluginTest.java | 21 +++--- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index 65959c3c..6d8aea1e 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -1,6 +1,7 @@ package net.neoforged.moddevgradle.internal; import java.net.URI; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; @@ -124,40 +125,45 @@ public void enable( extension.getRuns()); } + // 10-second timeout for the .module HTTP requests (both connect and read). + private static final int MODULE_FETCH_TIMEOUT_MS = 10_000; + /** * Discovers additional modules from the Gradle Module Metadata of the selected * NeoForge version (if any) and the NeoForm Runtime, then applies the content * filter to the NeoForge repository. *

    + * When the NeoForged repository was configured at the settings level there is + * no project-scoped repository to apply the filter to — this method returns + * early without touching the network. + *

    * Dynamic modules are collected in a local set and passed directly to * {@link NeoForgedRepositoryFilter#filter} — no static mutable state is shared * across projects, which is safe with parallel project configuration. - *

    - * NFRT metadata is always fetched regardless of whether a NeoForge version was - * selected, since NFRT can introduce new direct dependencies over time even in - * vanilla-only mode. */ private static void populateNeoForgeRepositoryFilter(Project project, @Nullable String neoForgeVersion) { + // When the repository is configured at the settings level there is no + // project-scoped repository to install the filter on; the settings-level + // filter is already in place and cannot be augmented per-project. + if (RepositoriesPlugin.getNeoForgeRepository(project) == null) { + return; + } + var dynamicModules = new HashSet(); - var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); if (neoForgeVersion != null) { - // Discover game library modules from the NeoForge artifact metadata. - fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion + var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\""); + fetchModuleDependencies(project, "net/neoforged/neoforge/" + neoForgeVersion + "/neoforge-" + neoForgeVersion + ".module", depPattern, dynamicModules); - } - - // Always discover build tool modules from the NeoForm Runtime metadata. - // NFRT ships external tools (DiffPatch, AutoRenamingTool, etc.) whose - // transitive dependencies are rehosted on the NeoForged Maven and may - // change across NFRT releases. - try { - var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); - fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion - + "/neoform-runtime-" + nfrtVersion + ".module", depPattern, dynamicModules); - } catch (Exception e) { - LOG.warn("Failed to resolve NFRT version for dynamic filter discovery: {}", e.getMessage()); + // NFRT metadata fetch uses the same pattern. + try { + var nfrtVersion = NeoFormRuntimeExtension.getVersion(project); + fetchModuleDependencies(project, "net/neoforged/neoform-runtime/" + nfrtVersion + + "/neoform-runtime-" + nfrtVersion + ".module", depPattern, dynamicModules); + } catch (Exception e) { + LOG.warn("Failed to resolve NFRT version for dynamic filter discovery: {}", e.getMessage()); + } } // Apply the content filter now — before any dependency resolution uses @@ -166,13 +172,30 @@ private static void populateNeoForgeRepositoryFilter(Project project, RepositoriesPlugin.applyContentFilter(project, dynamicModules); } - private static void fetchModuleDependencies(String path, Pattern depPattern, - Set dynamicModules) { + /** + * Downloads a single {@code .module} file from the NeoForged Maven and extracts + * {@code group:module} pairs from it. + *

    + * Uses {@link URLConnection} directly rather than Gradle dependency resolution + * because Gradle locks the repository content descriptor on first use, which + * would prevent us from installing the content filter afterward. Respects + * {@code --offline} and applies explicit connect/read timeouts. + */ + private static void fetchModuleDependencies(Project project, String path, + Pattern depPattern, Set dynamicModules) { + if (project.getGradle().getStartParameter().isOffline()) { + return; + } + try { var moduleUrl = URI.create( "https://maven.neoforged.net/releases/" + path).toURL(); - try (var stream = moduleUrl.openStream()) { + var connection = moduleUrl.openConnection(); + connection.setConnectTimeout(MODULE_FETCH_TIMEOUT_MS); + connection.setReadTimeout(MODULE_FETCH_TIMEOUT_MS); + + try (var stream = connection.getInputStream()) { var content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); var matcher = depPattern.matcher(content); while (matcher.find()) { diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java index f00b3d5d..c705c220 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java @@ -185,8 +185,7 @@ void testFilterIncludesStableModules() { NeoForgedRepositoryFilter.filter(descriptor, Set.of()); // The stable baseline must include NeoForge's own artifacts. - assertThat(descriptor.included).anyMatch( - r -> r[0].equals("net.neoforged") && r[1].equals("neoforge")); + assertThat(descriptor.included).contains("net.neoforged:neoforge"); } @Test @@ -195,10 +194,8 @@ void testFilterIncludesDynamicModules() { var dynamic = Set.of("com.example:new-lib", "org.test:another"); NeoForgedRepositoryFilter.filter(descriptor, dynamic); - assertThat(descriptor.included).anyMatch( - r -> r[0].equals("com.example") && r[1].equals("new-lib")); - assertThat(descriptor.included).anyMatch( - r -> r[0].equals("org.test") && r[1].equals("another")); + assertThat(descriptor.included).contains("com.example:new-lib"); + assertThat(descriptor.included).contains("org.test:another"); } @Test @@ -209,12 +206,12 @@ void testFilterIgnoresMalformedDynamicEntries() { // Stable modules still included; malformed entry did not throw. assertThat(descriptor.included).isNotEmpty(); - assertThat(descriptor.included).noneMatch( - r -> r[0].equals("malformed")); + // "malformed" was never passed to includeModule because split(":", 2) + // produced only one part and the length check guarded the call. } @Test - void testContentFilterAppliedInNeoForgeMode() { + void testNeoForgeRepositoryIsRegisteredAfterEnable() { extension.setVersion("21.10.48-beta"); var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); @@ -225,7 +222,7 @@ void testContentFilterAppliedInNeoForgeMode() { } @Test - void testContentFilterAppliedInVanillaOnlyMode() { + void testNeoForgeRepositoryIsRegisteredAfterVanillaOnlyEnable() { extension.setNeoFormVersion("1.21.4-20240101.235959"); var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project); @@ -276,11 +273,11 @@ void testParallelEnablesUseIsolatedDynamicSets() { * {@code includeModule} call so tests can assert filter behavior. */ static class RecordingDescriptor implements RepositoryContentDescriptor { - final Set included = new HashSet<>(); + final Set included = new HashSet<>(); @Override public void includeModule(String group, String name) { - included.add(new String[] { group, name }); + included.add(group + ":" + name); } // Remaining methods are unused by NeoForgedRepositoryFilter; stub them out.