Skip to content

Commit dc9df59

Browse files
committed
Enhance NeoForgedRepositoryFilter: ensure dynamic module discovery clears stale state and applies content filter consistently
1 parent 2ff3547 commit dc9df59

4 files changed

Lines changed: 111 additions & 27 deletions

File tree

src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.gradle.api.Project;
1717
import org.gradle.api.artifacts.ModuleDependency;
1818
import org.gradle.api.plugins.JavaLibraryPlugin;
19+
import org.jetbrains.annotations.Nullable;
1920
import org.slf4j.Logger;
2021
import org.slf4j.LoggerFactory;
2122

@@ -87,9 +88,10 @@ public void enable(
8788
versionCapabilities)
8889
: ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation);
8990

90-
if (neoForgeVersion != null) {
91-
populateNeoForgeRepositoryFilter(project, neoForgeVersion);
92-
}
91+
// Always apply at least the stable baseline filter to the NeoForged
92+
// repository. When a NeoForge version is selected we also discover
93+
// additional game-library modules from its metadata.
94+
populateNeoForgeRepositoryFilter(project, neoForgeVersion);
9395

9496
ArtifactNamingStrategy artifactNamingStrategy;
9597
// 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(
129131
* The HTTP download bypasses Gradle's dependency resolution so the repository content
130132
* descriptor stays unlocked and can receive its first {@code content()} call.
131133
*/
132-
private static void populateNeoForgeRepositoryFilter(Project project, String neoForgeVersion) {
133-
// Regex to extract group:module pairs from Gradle Module Metadata JSON.
134-
var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\"");
135-
136-
// 1. Discover game library modules from the NeoForge artifact metadata.
137-
fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion
138-
+ "/neoforge-" + neoForgeVersion + ".module", depPattern);
139-
140-
// 2. Discover build tool modules from the NeoForm Runtime metadata.
141-
// NFRT ships external tools (DiffPatch, AutoRenamingTool, etc.) whose
142-
// transitive dependencies are also rehosted on the NeoForged Maven.
143-
var nfrtVersion = NeoFormRuntimeExtension.getVersion(project);
144-
fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion
145-
+ "/neoform-runtime-" + nfrtVersion + ".module", depPattern);
146-
147-
// Apply the content filter now — before any dependency resolution uses the
148-
// NeoForge repository.
149-
try {
150-
RepositoriesPlugin.applyContentFilter(project);
151-
} catch (Exception e) {
152-
LOG.warn("Failed to apply NeoForge repository content filter: {}", e.getMessage());
134+
private static void populateNeoForgeRepositoryFilter(Project project,
135+
@Nullable String neoForgeVersion) {
136+
// Clear any stale dynamic modules from a previous build in this daemon.
137+
NeoForgedRepositoryFilter.clearGameLibraries();
138+
139+
if (neoForgeVersion != null) {
140+
var depPattern = Pattern.compile("\"group\":\\s*\"([^\"]+)\",\\s*\"module\":\\s*\"([^\"]+)\"");
141+
142+
// Discover game library modules from the NeoForge artifact metadata.
143+
fetchModuleDependencies("net/neoforged/neoforge/" + neoForgeVersion
144+
+ "/neoforge-" + neoForgeVersion + ".module", depPattern);
145+
146+
// Discover build tool modules from the NeoForm Runtime metadata.
147+
var nfrtVersion = NeoFormRuntimeExtension.getVersion(project);
148+
fetchModuleDependencies("net/neoforged/neoform-runtime/" + nfrtVersion
149+
+ "/neoform-runtime-" + nfrtVersion + ".module", depPattern);
153150
}
151+
152+
// Apply the content filter now — before any dependency resolution uses
153+
// the NeoForge repository. In the vanilla-only case this installs the
154+
// stable baseline; when a NeoForge version is selected it also includes
155+
// any dynamically discovered modules.
156+
RepositoriesPlugin.applyContentFilter(project);
154157
}
155158

156159
private static void fetchModuleDependencies(String path, Pattern depPattern) {

src/main/java/net/neoforged/moddevgradle/internal/NeoForgedRepositoryFilter.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ public static void addGameLibrary(String group, String module) {
224224
gameLibraryModules.add(group + ":" + module);
225225
}
226226

227+
/**
228+
* Clears all dynamically discovered modules. Called at the start of
229+
* {@code populateNeoForgeRepositoryFilter} so that each build computes its
230+
* own allowed-module set deterministically, avoiding cross-build leakage
231+
* when the Gradle daemon persists static state.
232+
*/
233+
static void clearGameLibraries() {
234+
gameLibraryModules.clear();
235+
}
236+
227237
/**
228238
* Applies the full filter: stable known modules plus any dynamically discovered
229239
* game library modules for the selected NeoForge version.

src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.gradle.api.initialization.Settings;
1111
import org.gradle.api.invocation.Gradle;
1212
import org.gradle.api.plugins.PluginAware;
13+
import org.jetbrains.annotations.Nullable;
1314

1415
/**
1516
* This plugin acts in different roles depending on where it is applied:
@@ -45,10 +46,13 @@ public void apply(PluginAware target) {
4546
}
4647

4748
/**
48-
* Returns the NeoForge repository. Exposed for dynamic filter population.
49+
* Returns the NeoForge repository, or {@code null} when the repository was
50+
* configured at the settings level and this project has no local reference.
4951
*/
52+
@Nullable
5053
static MavenArtifactRepository getNeoForgeRepository(Project project) {
51-
return (MavenArtifactRepository) project.getExtensions().getByName(NEOFORGE_REPO_EXTENSION);
54+
var ext = project.getExtensions().findByName(NEOFORGE_REPO_EXTENSION);
55+
return (MavenArtifactRepository) ext;
5256
}
5357

5458
/**
@@ -58,10 +62,15 @@ static MavenArtifactRepository getNeoForgeRepository(Project project) {
5862
* <p>
5963
* Must be called before any dependency resolution uses this repository —
6064
* Gradle locks the content descriptor on first use.
65+
* <p>
66+
* When the repository was configured at the settings level this is a safe
67+
* no-op — the content filter is already installed from {@code apply()}.
6168
*/
6269
static void applyContentFilter(Project project) {
6370
var neoRepo = getNeoForgeRepository(project);
64-
neoRepo.content(NeoForgedRepositoryFilter::filter);
71+
if (neoRepo != null) {
72+
neoRepo.content(NeoForgedRepositoryFilter::filter);
73+
}
6574
}
6675

6776
private static MavenArtifactRepository applyRepositories(RepositoryHandler repositories, boolean applyContentFilter) {

src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,68 @@ private void assertContainsModdingCompileDependencies(String version, String con
174174
"net.neoforged:neoforge:" + version + "[net.neoforged:neoforge-dependencies]");
175175
}
176176

177+
@Nested
178+
class RepositoryFilter {
179+
@Test
180+
void testContentFilterAppliedWhenNeoForgeVersionIsSet() {
181+
extension.setVersion("21.10.48-beta");
182+
183+
var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project);
184+
assertThat(neoRepo).isNotNull();
185+
// The content filter should be installed on the NeoForge repository.
186+
// We cannot inspect the filter rules directly through public API, but we
187+
// can verify the repository exists and is accessible.
188+
assertThat(project.getRepositories().stream()
189+
.filter(r -> "NeoForged Releases".equals(r.getName()))
190+
.findFirst()).isPresent();
191+
}
192+
193+
@Test
194+
void testContentFilterAppliedInVanillaOnlyMode() {
195+
extension.setNeoFormVersion("1.21.4-20240101.235959");
196+
197+
var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project);
198+
assertThat(neoRepo).isNotNull();
199+
// The stable baseline filter must be installed even when no NeoForge
200+
// version is selected, otherwise the NeoForged Maven (which mirrors
201+
// Maven Central) would be unfiltered.
202+
assertThat(project.getRepositories().stream()
203+
.filter(r -> "NeoForged Releases".equals(r.getName()))
204+
.findFirst()).isPresent();
205+
}
206+
207+
@Test
208+
void testApplyContentFilterIsNoOpWhenRepositoryNotOnProject() {
209+
// Create a fresh project without ModDevPlugin — the NeoForge repository
210+
// extension is never registered, so applyContentFilter must not throw.
211+
var freshProject = ProjectBuilder.builder().build();
212+
// Must not throw, even though there is no NeoForge repository extension.
213+
RepositoriesPlugin.applyContentFilter(freshProject);
214+
}
215+
216+
@Test
217+
void testDynamicModuleDiscoveryClearsStaleState() {
218+
// Simulate a first enable — should populate dynamic modules.
219+
extension.setVersion("21.10.48-beta");
220+
221+
// Run a second enable on a fresh project with a different version.
222+
// The dynamic set must be cleared first so the previous version's
223+
// modules do not leak.
224+
var project2 = ProjectBuilder.builder().build();
225+
project2.getPlugins().apply(ModDevPlugin.class);
226+
var ext2 = ExtensionUtils.getExtension(project2, "neoForge", NeoForgeExtension.class);
227+
var java2 = ExtensionUtils.getExtension(project2, "java", JavaPluginExtension.class);
228+
java2.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current());
229+
230+
// If state leaked, this would carry modules from "21.10.48-beta".
231+
// The filter should still be applied successfully.
232+
ext2.setVersion("21.0.133-beta");
233+
234+
var neoRepo = RepositoriesPlugin.getNeoForgeRepository(project2);
235+
assertThat(neoRepo).isNotNull();
236+
}
237+
}
238+
177239
private void assertContainsModdingRuntimeDependencies(String version, String configurationName) {
178240
var configuration = project.getConfigurations().getByName(configurationName);
179241

0 commit comments

Comments
 (0)