Skip to content
109 changes: 108 additions & 1 deletion src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
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;
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;
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;

Expand Down Expand Up @@ -75,9 +83,19 @@ 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);

// 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.
if (neoForge == null) {
Expand Down Expand Up @@ -106,4 +124,93 @@ public void enable(
artifacts,
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.
* <p>
* 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.
* <p>
* 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.
*/
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<String>();

if (neoForgeVersion != null) {
var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\"");
fetchModuleDependencies(project, "net/neoforged/neoforge/" + neoForgeVersion
+ "/neoforge-" + neoForgeVersion + ".module", depPattern, dynamicModules);
// 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());
}
}
Comment thread
Gu-ZT marked this conversation as resolved.

// Apply the content filter now — before any dependency resolution uses
// the NeoForge repository. Each enable() call passes its own local set,
// so parallel project configuration is safe.
RepositoriesPlugin.applyContentFilter(project, dynamicModules);
}
Comment thread
Gu-ZT marked this conversation as resolved.

/**
* Downloads a single {@code .module} file from the NeoForged Maven and extracts
* {@code group:module} pairs from it.
* <p>
* 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<String> dynamicModules) {
if (project.getGradle().getStartParameter().isOffline()) {
return;
}

try {
var moduleUrl = URI.create(
"https://maven.neoforged.net/releases/" + path).toURL();

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);
Comment thread
Gu-ZT marked this conversation as resolved.
var matcher = depPattern.matcher(content);
while (matcher.find()) {
var group = matcher.group(1);
var module = matcher.group(2);
if ("*".equals(group) || "*".equals(module)) {
continue;
}
dynamicModules.add(group + ":" + module);
}
}
} catch (Exception e) {
LOG.warn(
"Failed to discover dependencies from {}: {}",
path, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package net.neoforged.moddevgradle.internal;

import java.util.Set;
import org.gradle.api.artifacts.repositories.RepositoryContentDescriptor;

/**
* Controls which modules Gradle may resolve from the NeoForged Maven.
* <p>
* The NeoForged Maven mirrors many Maven Central artifacts. Without a content filter,
* Gradle could resolve arbitrary third-party artifacts from it.
* <p>
* This filter is built from two layers:
* <ul>
* <li><b>Stable baseline</b> — 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.</li>
* <li><b>Dynamic modules</b> — 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<String>} keyed by {@code "group:module"}.
* This allows new Minecraft releases to work without a plugin update, provided
* their libraries are also mirrored.</li>
* </ul>
*/
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.
*/
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" },
};

/**
* 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
*/
public static void filter(RepositoryContentDescriptor descriptor, Set<String> dynamicModules) {
for (var entry : STABLE_MODULES) {
descriptor.includeModule(entry[0], entry[1]);
}

for (var coordinate : dynamicModules) {
var parts = coordinate.split(":", 2);
if (parts.length == 2) {
descriptor.includeModule(parts[0], parts[1]);
}
}
}
}
Loading
Loading