diff --git a/README.md b/README.md
index c58ebc4c..85d6d468 100644
--- a/README.md
+++ b/README.md
@@ -323,6 +323,11 @@ To decrease the likelihood of conflicts if no explicit module name is set,
we prefix the filename of embedded subprojects with the group id.
### External Dependencies: Runs
+As of Minecraft 1.21.9, external dependencies do not need special handling anymore to be loaded in runs.
+
+
+Show information for 1.21.8 and older Minecraft versions
+
External dependencies will only be loaded in your runs if they are mods (with a `META-INF/neoforge.mods.toml` file),
or if they have the `FMLModType` entry set in their `META-INF/MANIFEST.MF` file.
Usually, Java libraries do not fit either of these requirements,
@@ -340,6 +345,7 @@ dependencies {
_Advanced_: The additional runtime classpath can be configured per-run.
For example, to add a dependency to the `client` run only, it can be added to `clientAdditionalRuntimeClasspath`.
+
### Isolated Source Sets
diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java
index 6c0fdbbb..4ccaf0f3 100644
--- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java
+++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java
@@ -59,7 +59,6 @@ public class ModDevRunWorkflow {
@Nullable
private final ModuleDependency testFixturesDependency;
private final ModuleDependency gameLibrariesDependency;
- private final Configuration additionalClasspath;
private final Configuration userDevConfigOnly;
/**
@@ -97,19 +96,32 @@ private ModDevRunWorkflow(Project project,
}
});
- additionalClasspath = configurations.create("additionalRuntimeClasspath", spec -> {
- spec.setDescription("Contains dependencies of every run, that should not be considered boot classpath modules.");
- spec.setCanBeResolved(true);
- spec.setCanBeConsumed(false);
-
- spec.getDependencies().add(gameLibrariesDependency);
- addClientResources(project, spec, artifactsWorkflow.createArtifacts());
- if (!versionCapabilities.modLocatorRework()) {
- // Forge expects to find the Forge and client-extra jar on the legacy classpath
- // Newer FML versions also search for it on the java.class.path.
- spec.getDependencies().addLater(artifactsWorkflow.minecraftClassesDependency());
- }
- });
+ Consumer configureLegacyClasspath;
+ if (versionCapabilities.legacyClasspath()) {
+ var additionalClasspath = configurations.create("additionalRuntimeClasspath", spec -> {
+ spec.setDescription("Contains dependencies of every run, that should not be considered boot classpath modules.");
+ spec.setCanBeResolved(true);
+ spec.setCanBeConsumed(false);
+
+ spec.getDependencies().add(gameLibrariesDependency);
+ addClientResources(project, spec, artifactsWorkflow.createArtifacts());
+ if (!versionCapabilities.modLocatorRework()) {
+ // Forge expects to find the Forge and client-extra jar on the legacy classpath
+ // Newer FML versions also search for it on the java.class.path.
+ spec.getDependencies().addLater(artifactsWorkflow.minecraftClassesDependency());
+ }
+ });
+ configureLegacyClasspath = legacyClassPath -> legacyClassPath.extendsFrom(additionalClasspath);
+ } else {
+ // Create the configuration but disallow adding anything to it, to notify users about potential mistakes.
+ // We might decide to remove it entirely in the future
+ var additionalClasspath = configurations.create("additionalRuntimeClasspath");
+ forbidAdditionalRuntimeDependencies(additionalClasspath, versionCapabilities);
+
+ configureLegacyClasspath = legacyClassPath -> {
+ throw new IllegalStateException("There is no legacy classpath for Minecraft " + versionCapabilities.minecraftVersion());
+ };
+ }
setupRuns(
project,
@@ -122,11 +134,24 @@ private ModDevRunWorkflow(Project project,
modulePath.getDependencies().add(modulePathDependency);
}
},
- legacyClassPath -> legacyClassPath.extendsFrom(additionalClasspath),
+ configureLegacyClasspath,
artifactsWorkflow.downloadAssets().flatMap(DownloadAssets::getAssetPropertiesFile),
versionCapabilities);
}
+ private static void forbidAdditionalRuntimeDependencies(Configuration configuration, VersionCapabilitiesInternal versionCapabilities) {
+ // We cannot use withDependencies() since the configuration should never get resolved,
+ // but we want to inform the user anyway.
+ configuration.getDependencies().all(dependency -> {
+ throw new IllegalStateException(String.format(
+ "Tried to add a dependency to configuration %s, but there is no additional classpath anymore for Minecraft %s. "
+ + "Add the dependency to a standard configuration such as implementation or runtimeOnly. Dependency: %s",
+ configuration,
+ versionCapabilities.minecraftVersion(),
+ dependency));
+ });
+ }
+
public static ModDevRunWorkflow get(Project project) {
var workflow = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevRunWorkflow.class);
if (workflow == null) {
@@ -198,7 +223,8 @@ public void configureTesting(Provider testedMod, Provider setupRunInGradle(
configureModulePath.accept(spec);
});
- var legacyClasspathConfiguration = configurations.create(InternalModelHelper.nameOfRun(run, "", "legacyClasspath"), spec -> {
- spec.setDescription("Contains all dependencies of the " + run.getName() + " run that should not be considered boot classpath modules.");
- spec.setCanBeResolved(true);
- spec.setCanBeConsumed(false);
- spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get());
- spec.attributes(attributes -> {
- attributes.attributeProvider(MinecraftDistribution.ATTRIBUTE, type.map(t -> {
- var name = t.equals("client") || t.equals("data") || t.equals("clientData") ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER;
- return project.getObjects().named(MinecraftDistribution.class, name);
- }));
- setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME);
+ Provider legacyClasspathFile;
+ if (versionCapabilities.legacyClasspath()) {
+ var legacyClasspathConfiguration = configurations.create(InternalModelHelper.nameOfRun(run, "", "legacyClasspath"), spec -> {
+ spec.setDescription("Contains all dependencies of the " + run.getName() + " run that should not be considered boot classpath modules.");
+ spec.setCanBeResolved(true);
+ spec.setCanBeConsumed(false);
+ spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get());
+ spec.attributes(attributes -> {
+ attributes.attributeProvider(MinecraftDistribution.ATTRIBUTE, type.map(t -> {
+ var name = t.equals("client") || t.equals("data") || t.equals("clientData") ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER;
+ return project.getObjects().named(MinecraftDistribution.class, name);
+ }));
+ setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME);
+ });
+ configureLegacyClasspath.accept(spec);
+ spec.extendsFrom(run.getAdditionalRuntimeClasspathConfiguration());
});
- configureLegacyClasspath.accept(spec);
- spec.extendsFrom(run.getAdditionalRuntimeClasspathConfiguration());
- });
- var writeLcpTask = tasks.register(InternalModelHelper.nameOfRun(run, "write", "legacyClasspath"), WriteLegacyClasspath.class, writeLcp -> {
- writeLcp.setGroup(branding.internalTaskGroup());
- writeLcp.setDescription("Writes the legacyClasspath file for the " + run.getName() + " Minecraft run, containing all dependencies that shouldn't be considered boot modules.");
- writeLcp.getLegacyClasspathFile().set(argFileDir.map(dir -> dir.file(InternalModelHelper.nameOfRun(run, "", "legacyClasspath") + ".txt")));
- writeLcp.addEntries(legacyClasspathConfiguration);
- });
+ var writeLcpTask = tasks.register(InternalModelHelper.nameOfRun(run, "write", "legacyClasspath"), WriteLegacyClasspath.class, writeLcp -> {
+ writeLcp.setGroup(branding.internalTaskGroup());
+ writeLcp.setDescription("Writes the legacyClasspath file for the " + run.getName() + " Minecraft run, containing all dependencies that shouldn't be considered boot modules.");
+ writeLcp.getLegacyClasspathFile().set(argFileDir.map(dir -> dir.file(InternalModelHelper.nameOfRun(run, "", "legacyClasspath") + ".txt")));
+ writeLcp.addEntries(legacyClasspathConfiguration);
+ });
+ legacyClasspathFile = writeLcpTask.get().getLegacyClasspathFile();
+ } else {
+ // Disallow adding dependencies to the additional classpath configuration since it would have no effect.
+ forbidAdditionalRuntimeDependencies(run.getAdditionalRuntimeClasspathConfiguration(), versionCapabilities);
+ legacyClasspathFile = null;
+ }
var prepareRunTask = tasks.register(InternalModelHelper.nameOfRun(run, "prepare", "run"), PrepareRun.class, task -> {
task.setGroup(branding.internalTaskGroup());
@@ -337,7 +371,9 @@ private static TaskProvider setupRunInGradle(
task.getRunType().set(run.getType());
task.getRunTypeTemplatesSource().from(runTemplatesFile);
task.getModules().from(modulePathConfiguration);
- task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile());
+ if (legacyClasspathFile != null) {
+ task.getLegacyClasspathFile().set(legacyClasspathFile);
+ }
task.getAssetProperties().set(assetPropertiesFile);
task.getSystemProperties().set(run.getSystemProperties().map(props -> {
props = new HashMap<>(props);
@@ -405,7 +441,8 @@ static void setupTestTask(Project project,
Provider argFileDir,
Consumer configureModulePath,
Consumer configureLegacyClasspath,
- Provider assetPropertiesFile) {
+ Provider assetPropertiesFile,
+ VersionCapabilitiesInternal versionCapabilities) {
var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR);
var ideIntegration = IdeIntegration.of(project, branding);
@@ -423,27 +460,33 @@ static void setupTestTask(Project project,
configureModulePath.accept(spec);
});
- var legacyClasspathConfiguration = configurations.create("neoForgeTestLibraries", spec -> {
- spec.setDescription("Contains the legacy classpath of unit tests.");
- spec.setCanBeResolved(true);
- spec.setCanBeConsumed(false);
- spec.shouldResolveConsistentlyWith(testRuntimeClasspath);
- spec.attributes(attributes -> {
- setNamedAttribute(project, attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT);
- setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME);
- });
- configureLegacyClasspath.accept(spec);
- });
-
// Place files for junit runtime in a subdirectory to avoid conflicting with other runs
var runArgsDir = argFileDir.map(dir -> dir.dir("junit"));
- var writeLcpTask = tasks.register("writeNeoForgeTestClasspath", WriteLegacyClasspath.class, writeLcp -> {
- writeLcp.setGroup(branding.internalTaskGroup());
- writeLcp.setDescription("Writes the legacyClasspath file for the test run, containing all dependencies that shouldn't be considered boot modules.");
- writeLcp.getLegacyClasspathFile().convention(runArgsDir.map(dir -> dir.file("legacyClasspath.txt")));
- writeLcp.addEntries(legacyClasspathConfiguration);
- });
+ Provider legacyClasspathFile;
+ if (versionCapabilities.legacyClasspath()) {
+ var legacyClasspathConfiguration = configurations.create("neoForgeTestLibraries", spec -> {
+ spec.setDescription("Contains the legacy classpath of unit tests.");
+ spec.setCanBeResolved(true);
+ spec.setCanBeConsumed(false);
+ spec.shouldResolveConsistentlyWith(testRuntimeClasspath);
+ spec.attributes(attributes -> {
+ setNamedAttribute(project, attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT);
+ setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME);
+ });
+ configureLegacyClasspath.accept(spec);
+ });
+
+ var writeLcpTask = tasks.register("writeNeoForgeTestClasspath", WriteLegacyClasspath.class, writeLcp -> {
+ writeLcp.setGroup(branding.internalTaskGroup());
+ writeLcp.setDescription("Writes the legacyClasspath file for the test run, containing all dependencies that shouldn't be considered boot modules.");
+ writeLcp.getLegacyClasspathFile().convention(runArgsDir.map(dir -> dir.file("legacyClasspath.txt")));
+ writeLcp.addEntries(legacyClasspathConfiguration);
+ });
+ legacyClasspathFile = writeLcpTask.get().getLegacyClasspathFile();
+ } else {
+ legacyClasspathFile = null;
+ }
var vmArgsFile = runArgsDir.map(dir -> dir.file("vmArgs.txt"));
var programArgsFile = runArgsDir.map(dir -> dir.file("programArgs.txt"));
@@ -457,7 +500,9 @@ static void setupTestTask(Project project,
task.getLog4jConfigFile().set(log4j2ConfigFile);
task.getRunTypeTemplatesSource().from(runTemplatesSourceFile);
task.getModules().from(neoForgeModDevModules);
- task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile());
+ if (legacyClasspathFile != null) {
+ task.getLegacyClasspathFile().set(legacyClasspathFile);
+ }
task.getAssetProperties().set(assetPropertiesFile);
task.getGameLogLevel().set(Level.INFO);
});
@@ -485,10 +530,6 @@ static void setupTestTask(Project project,
ideIntegration.configureTesting(loadedMods, testedMod, runArgsDir, gameDirectory, programArgsFile, vmArgsFile);
}
- public Configuration getAdditionalClasspath() {
- return additionalClasspath;
- }
-
private static void setNamedAttribute(Project project, AttributeContainer attributes, Attribute attribute, String value) {
attributes.attribute(attribute, project.getObjects().named(attribute.getType(), value));
}
diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java
index 272dfe3f..24692694 100644
--- a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java
+++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java
@@ -83,7 +83,33 @@ public static void setupTestTask(Project project,
argFileDir,
configureModulePath,
configureAdditionalClasspath,
- assetPropertiesFile);
+ assetPropertiesFile,
+ // This overload of the method was only used by NeoForge before 1.21.9
+ VersionCapabilitiesInternal.ofMinecraftVersion("1.21.8"));
+ }
+
+ public static void setupTestTask(Project project,
+ Provider argFileDir,
+ TaskProvider testTask,
+ Object runTemplatesSourceFile,
+ Provider> loadedMods,
+ Provider testedMod,
+ Consumer configureModulePath,
+ Consumer configureAdditionalClasspath,
+ Provider assetPropertiesFile,
+ Provider neoFormVersion) {
+ ModDevRunWorkflow.setupTestTask(
+ project,
+ Branding.NEODEV,
+ runTemplatesSourceFile,
+ testTask,
+ loadedMods,
+ testedMod,
+ argFileDir,
+ configureModulePath,
+ configureAdditionalClasspath,
+ assetPropertiesFile,
+ neoFormVersion.map(VersionCapabilitiesInternal::ofNeoFormVersion).getOrElse(VersionCapabilitiesInternal.latest()));
}
public static void runTaskOnProjectSync(Project project, Object task) {
diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java
index 0543e684..116bb72f 100644
--- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java
+++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java
@@ -89,6 +89,7 @@ abstract class PrepareRunOrTest extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
+ @Optional
abstract RegularFileProperty getLegacyClasspathFile();
@Classpath
diff --git a/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilitiesInternal.java b/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilitiesInternal.java
index 4921a31f..3cfcb31c 100644
--- a/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilitiesInternal.java
+++ b/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilitiesInternal.java
@@ -16,7 +16,7 @@
* @param testFixtures If the NeoForge version for this Minecraft version supports test fixtures.
*/
public record VersionCapabilitiesInternal(String minecraftVersion, int javaVersion, boolean splitDataRuns,
- boolean testFixtures, boolean modLocatorRework) implements VersionCapabilities, Serializable {
+ boolean testFixtures, boolean modLocatorRework, boolean legacyClasspath) implements VersionCapabilities, Serializable {
private static final Logger LOG = Logging.getLogger(VersionCapabilitiesInternal.class);
@@ -26,6 +26,7 @@ public record VersionCapabilitiesInternal(String minecraftVersion, int javaVersi
// Strips NeoForm timestamp suffixes OR dynamic version markers
private static final Pattern NEOFORM_PATTERN = Pattern.compile("^(.*)-(?:\\+|\\d{8}\\.\\d{6})$");
+ private static final int MC_1_21_9_INDEX = getReferenceVersionIndex("1.21.9");
private static final int MC_24W45A_INDEX = getReferenceVersionIndex("24w45a");
private static final int MC_1_20_5_INDEX = getReferenceVersionIndex("1.20.5");
private static final int MC_24W14A_INDEX = getReferenceVersionIndex("24w14a");
@@ -56,8 +57,9 @@ public static VersionCapabilitiesInternal ofVersionIndex(int versionIndex, Strin
var splitData = hasSplitDataEntrypoints(versionIndex);
var testFixtures = hasTestFixtures(versionIndex);
var modLocatorRework = hasModLocatorRework(versionIndex);
+ var legacyClasspath = hasLegacyClasspath(versionIndex);
- return new VersionCapabilitiesInternal(minecraftVersion, javaVersion, splitData, testFixtures, modLocatorRework);
+ return new VersionCapabilitiesInternal(minecraftVersion, javaVersion, splitData, testFixtures, modLocatorRework, legacyClasspath);
}
static int getJavaVersion(int versionIndex) {
@@ -84,6 +86,10 @@ static boolean hasModLocatorRework(int versionIndex) {
return versionIndex <= MC_1_20_5_INDEX;
}
+ static boolean hasLegacyClasspath(int versionIndex) {
+ return versionIndex > MC_1_21_9_INDEX;
+ }
+
static int indexOfNeoForgeVersion(String version) {
// NeoForge omits the "1." at the start of the Minecraft version and just adds an incrementing last digit
var matcher = NEOFORGE_PATTERN.matcher(version);
@@ -168,6 +174,7 @@ public VersionCapabilitiesInternal withMinecraftVersion(String minecraftVersion)
javaVersion,
splitDataRuns,
testFixtures,
- modLocatorRework);
+ modLocatorRework,
+ legacyClasspath);
}
}