From a1b3bfdb584156d4ba11fd2c9594374bb25bb406 Mon Sep 17 00:00:00 2001 From: shartte Date: Sat, 28 Dec 2024 12:31:28 +0100 Subject: [PATCH] Imperative Configuration #200) Co-authored-by: Matyrobbrt <65940752+Matyrobbrt@users.noreply.github.com> Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com> --- BREAKING_CHANGES.md | 5 + LEGACY.md | 26 +- legacytest/build.gradle | 4 +- legacytest/forge/build.gradle | 4 +- .../legacyforge/dsl/LegacyForgeExtension.java | 61 ++ .../dsl/LegacyForgeModdingSettings.java | 84 ++ ...scation.java => ObfuscationExtension.java} | 45 +- .../internal/LegacyForgeModDevPlugin.java | 192 ++-- .../moddevgradle/dsl/ModDevExtension.java | 161 ++++ .../dsl/ModdingVersionSettings.java | 64 ++ .../moddevgradle/dsl/NeoForgeExtension.java | 192 +--- .../neoforged/moddevgradle/dsl/UnitTest.java | 7 +- .../internal/ArtifactNamingStrategy.java | 16 + .../moddevgradle/internal/Branding.java | 2 +- .../internal/DataFileCollections.java | 128 +++ .../internal/EclipseIntegration.java | 6 +- .../moddevgradle/internal/IdeIntegration.java | 9 +- .../internal/IntelliJIntegration.java | 8 +- .../internal/ModDevArtifactsWorkflow.java | 352 +++++++ .../moddevgradle/internal/ModDevPlugin.java | 863 ++---------------- .../internal/ModDevRunWorkflow.java | 494 ++++++++++ .../internal/ModdingDependencies.java | 67 ++ .../moddevgradle/internal/NeoDevFacade.java | 17 +- .../internal/PrepareRunOrTest.java | 3 +- .../internal/WorkflowArtifact.java | 16 + .../internal/jarjar/JarJarPlugin.java | 29 + .../jarjar/ResolvedJarJarArtifact.java | 4 +- .../internal/utils/VersionCapabilities.java | 13 +- .../neoforged/moddevgradle/tasks/JarJar.java | 2 +- .../AbstractProjectBuilderTest.java | 57 ++ .../functional/AbstractFunctionalTest.java | 6 +- .../functional/GroovyScriptTest.java | 22 + .../functional/KotlinScriptTest.java | 24 + .../AccessTransformerConventionTest.java | 1 + .../internal/ModDevPluginTest.java | 120 +++ .../legacyforge/LegacyModDevPluginTest.java | 122 +++ .../legacyforge/dsl/MixinMappingTest.java | 12 +- 37 files changed, 2147 insertions(+), 1091 deletions(-) create mode 100644 src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeExtension.java create mode 100644 src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeModdingSettings.java rename src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/{Obfuscation.java => ObfuscationExtension.java} (86%) create mode 100644 src/main/java/net/neoforged/moddevgradle/dsl/ModDevExtension.java create mode 100644 src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/ArtifactNamingStrategy.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/DataFileCollections.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/ModdingDependencies.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java create mode 100644 src/main/java/net/neoforged/moddevgradle/internal/jarjar/JarJarPlugin.java create mode 100644 src/test/java/net/neoforged/moddevgradle/AbstractProjectBuilderTest.java create mode 100644 src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java create mode 100644 src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 64ca1b38..c891f342 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -6,6 +6,11 @@ please refer to the changelog, which can be found on the [project page](https:// The breaking changes in this major version should not affect most projects. Nonetheless, every single breaking change is documented here, along with a suggested fix. +- Modding now needs to be enabled before dependencies are made available, and the NeoForge/NeoForm versions will + be fixed at the point in time when modding is enabled. Setting `neoForge.version` or `neoForge.neoFormVersion` will + enable modding when those properties are set. For more advanced use cases, the `neoForge.enable { ... }` block can + be used, i.e. to not enable modding for the `main` source set. You can only enable modding once for one version + of NeoForge/NeoForm per project. - Changes to access transformer and interface injection data publishing. - `accessTransformers.publish` and `interfaceInjectionData.publish` syntax was changed. - `accessTransformers.published` and `interfaceInjectionData.published` were removed. diff --git a/LEGACY.md b/LEGACY.md index 655bb463..71b04784 100644 --- a/LEGACY.md +++ b/LEGACY.md @@ -13,9 +13,9 @@ plugins { id 'net.neoforged.moddev.legacyforge' version '2.0.28-beta' } -neoForge { +legacyForge { // Develop against MinecraftForge version 47.3.0 for 1.20.1 (the versions can be found at https://files.minecraftforge.net/) - version = "1.20.1-47.3.0" + forgeVersion = "1.20.1-47.3.0" // Validate AT files and raise errors when they have invalid targets // This option is false by default, but turning it on is recommended @@ -91,6 +91,18 @@ obfuscation { } ``` +## Vanilla Mode + +You can get dependencies for Vanilla Minecraft added to your project by using the `mcpVersion` property instead of +setting the `forgeVersion` property. + +```groovy +legacyForge { + // This adds Minecraft 1.20.1 as a dependency to the main source set. + mcpVersion = "1.20.1" +} +``` + ## Mixins You need to create so-called "refmaps" for Mixin, which convert the names you used to declare injection points and reference other parts of Minecraft code to the names used at runtime (SRG). @@ -126,10 +138,10 @@ jar { } ``` -## Effects of applying the legacy plugin -When applied, the legacy plugin will change the base NeoForm and NeoForge artifact coordinates of the `neoForge` extension to -`de.oceanlabs.mcp:mcp_config` and `net.minecraftforge:forge`. -It will also trigger the creation of various intermediary (SRG) to named (official) mapping files used by various parts of the toolchain, such as -mod reobfuscation and runtime naming services. +## Effects of enabling legacy forge modding + +Enabling modding in the legacyForge extension triggers the creation of various intermediary (SRG) to named (official) mapping files used by various parts of the toolchain, such as +mod reobfuscation and runtime naming services. + Reobfuscation to the intermediary mappings will automatically be configured for the `jar` task, the non-obfuscated jar will have a `-dev` classifier and will not be published in favour of the reobfuscated variant. diff --git a/legacytest/build.gradle b/legacytest/build.gradle index 95b0ef83..b537b4b9 100644 --- a/legacytest/build.gradle +++ b/legacytest/build.gradle @@ -12,8 +12,8 @@ java { } } -neoForge { - neoFormVersion = '1.19.2' +legacyForge { + mcpVersion = '1.19.2' } publishing { diff --git a/legacytest/forge/build.gradle b/legacytest/forge/build.gradle index 67a4847b..f465bbda 100644 --- a/legacytest/forge/build.gradle +++ b/legacytest/forge/build.gradle @@ -30,8 +30,8 @@ dependencies { modImplementation('curse.maven:applied-energistics-2-223794:5641282') } -neoForge { - version = '1.20.1-47.3.0' +legacyForge { + version = '1.20.1-47.3.12' runs { client { client() diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeExtension.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeExtension.java new file mode 100644 index 00000000..aa0d0140 --- /dev/null +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeExtension.java @@ -0,0 +1,61 @@ +package net.neoforged.moddevgradle.legacyforge.dsl; + +import net.neoforged.moddevgradle.dsl.DataFileCollection; +import net.neoforged.moddevgradle.dsl.ModDevExtension; +import net.neoforged.moddevgradle.legacyforge.internal.LegacyForgeModDevPlugin; +import org.gradle.api.Action; +import org.gradle.api.Project; + +import javax.inject.Inject; + +/** + * This is the top-level {@code legacyForge} extension, used to configure the moddev plugin. + */ +public abstract class LegacyForgeExtension extends ModDevExtension { + private final Project project; + + @Inject + public LegacyForgeExtension(Project project, + DataFileCollection accessTransformers, + DataFileCollection interfaceInjectionData) { + super(project, accessTransformers, interfaceInjectionData); + this.project = project; + } + + /** + * Enables modding for the main source-set using the given Forge version. + *

+ * Shorthand for: + * + * enable { forgeVersion = '...' } + * + */ + public void setVersion(String version) { + enable(settings -> { + settings.setForgeVersion(version); + }); + } + + /** + * Enables modding for the main source-set in Vanilla-mode. + *

+ * Shorthand for: + * + * enable { mcpVersion = '...' } + * + */ + public void setMcpVersion(String version) { + enable(settings -> { + settings.setMcpVersion(version); + }); + } + + public void enable(Action customizer) { + var plugin = project.getPlugins().getPlugin(LegacyForgeModDevPlugin.class); + + var settings = project.getObjects().newInstance(LegacyForgeModdingSettings.class); + customizer.execute(settings); + + plugin.enable(project, settings, this); + } +} diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeModdingSettings.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeModdingSettings.java new file mode 100644 index 00000000..a004c936 --- /dev/null +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/LegacyForgeModdingSettings.java @@ -0,0 +1,84 @@ +package net.neoforged.moddevgradle.legacyforge.dsl; + +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import java.util.HashSet; +import java.util.Set; + +public abstract class LegacyForgeModdingSettings { + @Nullable + private String neoForgeVersion; + + @Nullable + private String forgeVersion; + + @Nullable + private String mcpVersion; + + private Set enabledSourceSets = new HashSet<>(); + + @Inject + public LegacyForgeModdingSettings(Project project) { + // By default, enable modding deps only for the main source set + var sourceSets = ExtensionUtils.getSourceSets(project); + var mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + enabledSourceSets.add(mainSourceSet); + } + + public @Nullable String getNeoForgeVersion() { + return neoForgeVersion; + } + + public @Nullable String getForgeVersion() { + return forgeVersion; + } + + public @Nullable String getMcpVersion() { + return mcpVersion; + } + + /** + * NeoForge version number. You have to set either this, {@link #setForgeVersion} or {@link #setMcpVersion}. + * Only NeoForge for Minecraft 1.20.1 is supported when using this plugin. + */ + public void setNeoForgeVersion(String version) { + this.neoForgeVersion = version; + } + + /** + * Minecraft Forge version. You have to set either this, {@link #setNeoForgeVersion} or {@link #setMcpVersion}. + */ + public void setForgeVersion(String version) { + this.forgeVersion = version; + } + + /** + * You can set this property to a version of MCP + * to either override the version used in the version of Forge you set, or to compile against + * Vanilla artifacts that have no Forge code added. + */ + public void setMcpVersion(String version) { + this.mcpVersion = version; + } + + /** + * Contains the list of source sets for which access to Minecraft classes should be configured. + * Defaults to the main source set, but can also be set to an empty list. + */ + + /** + * Contains the list of source sets for which access to Minecraft classes should be configured. + * Defaults to the main source set, but can also be set to an empty list. + */ + public Set getEnabledSourceSets() { + return enabledSourceSets; + } + + public void setEnabledSourceSets(Set enabledSourceSets) { + this.enabledSourceSets = enabledSourceSets; + } +} diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/Obfuscation.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/ObfuscationExtension.java similarity index 86% rename from src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/Obfuscation.java rename to src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/ObfuscationExtension.java index 39a56f70..a27bb342 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/Obfuscation.java +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/dsl/ObfuscationExtension.java @@ -5,6 +5,7 @@ import net.neoforged.moddevgradle.legacyforge.tasks.RemapOperation; import org.apache.commons.lang3.StringUtils; import org.gradle.api.Action; +import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ExternalModuleDependency; @@ -12,7 +13,7 @@ import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.component.AdhocComponentWithVariants; import org.gradle.api.file.FileCollection; -import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; @@ -23,41 +24,53 @@ import javax.inject.Inject; import java.util.List; -public abstract class Obfuscation { +public abstract class ObfuscationExtension { private final Project project; - private final Provider officialToSrg; - private final Provider mappingsCsv; private final Configuration autoRenamingToolRuntime; private final Configuration installerToolsRuntime; private final FileCollection extraMixinMappings; @Inject - public Obfuscation(Project project, - Provider officialToSrg, - Provider mappingsCsv, - Configuration autoRenamingToolRuntime, - Configuration installerToolsRuntime, - FileCollection extraMixinMappings) { + public ObfuscationExtension(Project project, + Configuration autoRenamingToolRuntime, + Configuration installerToolsRuntime, + FileCollection extraMixinMappings) { this.project = project; - this.officialToSrg = officialToSrg; - this.mappingsCsv = mappingsCsv; this.autoRenamingToolRuntime = autoRenamingToolRuntime; this.installerToolsRuntime = installerToolsRuntime; this.extraMixinMappings = extraMixinMappings; } + private Provider assertConfigured(Provider provider) { + return provider.orElse(project.provider(() -> { + throw new InvalidUserCodeException("Please enable modding by setting legacyForge.version or calling legacyForge.enable()"); + })); + } + + /** + * Format is TSRG. + */ + @ApiStatus.Internal + public abstract RegularFileProperty getNamedToSrgMappings(); + + /** + * Format is a ZIP file containing CSV files with mapping data. + */ + @ApiStatus.Internal + public abstract RegularFileProperty getSrgToNamedMappings(); + @ApiStatus.Internal public void configureNamedToSrgOperation(RemapOperation operation) { operation.getToolType().set(RemapOperation.ToolType.ART); operation.getToolClasspath().from(autoRenamingToolRuntime); - operation.getMappings().from(officialToSrg); + operation.getMappings().from(assertConfigured(getNamedToSrgMappings())); } @ApiStatus.Internal public void configureSrgToNamedOperation(RemapOperation operation) { operation.getToolType().set(RemapOperation.ToolType.INSTALLER_TOOLS); operation.getToolClasspath().from(installerToolsRuntime); - operation.getMappings().from(mappingsCsv); + operation.getMappings().from(assertConfigured(getSrgToNamedMappings())); } /** @@ -68,7 +81,8 @@ public void configureSrgToNamedOperation(RemapOperation operation) { * @return a provider of the created task */ public TaskProvider reobfuscate(TaskProvider jar, SourceSet sourceSet) { - return reobfuscate(jar, sourceSet, ignored -> {}); + return reobfuscate(jar, sourceSet, ignored -> { + }); } /** @@ -82,7 +96,6 @@ public TaskProvider reobfuscate(TaskProvider reobfuscate(TaskProvider jar, SourceSet sourceSet, Action configuration) { - var reobf = project.getTasks().register("reobf" + StringUtils.capitalize(jar.getName()), RemapJar.class, task -> { task.getInput().set(jar.flatMap(AbstractArchiveTask::getArchiveFile)); task.getDestinationDirectory().convention(task.getProject().getLayout().getBuildDirectory().dir("libs")); diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java index 2b88b739..138e57c9 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java @@ -1,26 +1,36 @@ package net.neoforged.moddevgradle.legacyforge.internal; -import net.neoforged.moddevgradle.dsl.NeoForgeExtension; +import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin; +import net.neoforged.moddevgradle.internal.ArtifactNamingStrategy; +import net.neoforged.moddevgradle.internal.Branding; +import net.neoforged.moddevgradle.internal.DataFileCollections; +import net.neoforged.moddevgradle.internal.ModdingDependencies; +import net.neoforged.moddevgradle.internal.jarjar.JarJarPlugin; import net.neoforged.moddevgradle.internal.LegacyForgeFacade; -import net.neoforged.moddevgradle.internal.ModDevPlugin; +import net.neoforged.moddevgradle.internal.ModDevArtifactsWorkflow; +import net.neoforged.moddevgradle.internal.ModDevRunWorkflow; +import net.neoforged.moddevgradle.internal.RepositoriesPlugin; +import net.neoforged.moddevgradle.internal.WorkflowArtifact; import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import net.neoforged.moddevgradle.internal.utils.VersionCapabilities; +import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeExtension; +import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeModdingSettings; import net.neoforged.moddevgradle.legacyforge.dsl.MixinExtension; -import net.neoforged.moddevgradle.legacyforge.dsl.Obfuscation; -import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; +import net.neoforged.moddevgradle.legacyforge.dsl.ObfuscationExtension; +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.type.ArtifactTypeDefinition; import org.gradle.api.attributes.Attribute; -import org.gradle.api.file.RegularFile; +import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.jvm.tasks.Jar; -import org.gradle.jvm.toolchain.JavaLanguageVersion; -import org.gradle.jvm.toolchain.JavaLauncher; -import org.gradle.jvm.toolchain.JavaToolchainService; import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.URI; import java.util.Map; @@ -28,6 +38,12 @@ @ApiStatus.Internal public class LegacyForgeModDevPlugin implements Plugin { + private static final Logger LOG = LoggerFactory.getLogger(LegacyForgeModDevPlugin.class); + + public static final String MIXIN_EXTENSION = "mixin"; + public static final String OBFUSCATION_EXTENSION = "obfuscation"; + public static final String LEGACYFORGE_EXTENSION = "legacyForge"; + public static final Attribute REMAPPED = Attribute.of("net.neoforged.moddevgradle.legacy.remapped", Boolean.class); public static final String CONFIGURATION_TOOL_ART = "autoRenamingToolRuntime"; @@ -35,7 +51,19 @@ public class LegacyForgeModDevPlugin implements Plugin { @Override public void apply(Project project) { - project.getPlugins().apply(ModDevPlugin.class); + project.getPlugins().apply(JavaLibraryPlugin.class); + project.getPlugins().apply(NeoFormRuntimePlugin.class); + project.getPlugins().apply(MinecraftDependenciesPlugin.class); + project.getPlugins().apply(JarJarPlugin.class); + + // TODO: Introduce a LegacyRepositoryPLugin to still allow repo management in settings.gradle + // Do not apply the repositories automatically if they have been applied at the settings-level. + // It's still possible to apply them manually, though. + if (!project.getGradle().getPlugins().hasPlugin(RepositoriesPlugin.class)) { + project.getPlugins().apply(RepositoriesPlugin.class); + } else { + LOG.info("Not enabling NeoForged repositories since they were applied at the settings level"); + } project.getRepositories().maven(repo -> { repo.setName("MinecraftForge"); @@ -66,35 +94,107 @@ public void apply(Project project) { spec.getDependencies().add(depFactory.create("net.neoforged.installertools:installertools:2.1.10:fatjar")); }); - // We use this directory to store intermediate files used during moddev - var modDevBuildDir = project.getLayout().getBuildDirectory().dir("moddev"); - var namedToIntermediate = modDevBuildDir.map(d -> d.file("namedToIntermediate.tsrg")); - var intermediateToNamed = modDevBuildDir.map(d -> d.file("intermediateToNamed.srg")); - var mappingsCsv = modDevBuildDir.map(d -> d.file("intermediateToNamed.zip")); - // This collection is used to share the files added by mixin with the obfuscation extension var extraMixinMappings = project.files(); + var obf = project.getExtensions().create(OBFUSCATION_EXTENSION, ObfuscationExtension.class, project, autoRenamingToolRuntime, installerToolsRuntime, extraMixinMappings); + project.getExtensions().create(MIXIN_EXTENSION, MixinExtension.class, project, obf.getNamedToSrgMappings(), extraMixinMappings); + + configureDependencyRemapping(project, obf); + + var dataFileCollections = DataFileCollections.create(project); + project.getExtensions().create( + LEGACYFORGE_EXTENSION, + LegacyForgeExtension.class, + project, + dataFileCollections.accessTransformers().extension(), + dataFileCollections.interfaceInjectionData().extension() + ); + } + + public void enable(Project project, LegacyForgeModdingSettings settings, LegacyForgeExtension extension) { + var depFactory = project.getDependencyFactory(); + + var forgeVersion = settings.getForgeVersion(); + var neoForgeVersion = settings.getNeoForgeVersion(); + var mcpVersion = settings.getMcpVersion(); + + ModdingDependencies dependencies; + ArtifactNamingStrategy artifactNamingStrategy; + VersionCapabilities versionCapabilities; + if (forgeVersion != null || neoForgeVersion != null) { + // All settings are mutually exclusive + if (forgeVersion != null && neoForgeVersion != null || mcpVersion != null) { + throw new InvalidUserCodeException("Specifying a Forge version is mutually exclusive with NeoForge or MCP"); + } + + var artifactPrefix = "forge-" + forgeVersion; + // We have to ensure that client resources are named "client-extra" and *do not* contain forge- + // otherwise FML might pick up the client resources as the main Minecraft jar. + artifactNamingStrategy = (artifact) -> { + if (artifact == WorkflowArtifact.CLIENT_RESOURCES) { + return "client-extra-" + forgeVersion + ".jar"; + } else { + return artifactPrefix + artifact.defaultSuffix + ".jar"; + } + }; + + versionCapabilities = VersionCapabilities.ofForgeVersion(forgeVersion); + + String groupId = forgeVersion != null ? "net.minecraftforge" : "net.neoforged"; + var neoForge = depFactory.create(groupId + ":forge:" + forgeVersion); + var neoForgeNotation = groupId + ":forge:" + forgeVersion + ":userdev"; + dependencies = ModdingDependencies.create(neoForge, neoForgeNotation, null, null, versionCapabilities); + } else if (mcpVersion != null) { + artifactNamingStrategy = ArtifactNamingStrategy.createDefault("vanilla-" + mcpVersion); + versionCapabilities = VersionCapabilities.ofMinecraftVersion(mcpVersion); + + var neoForm = depFactory.create("de.oceanlabs.mcp:mcp_config:" + mcpVersion); + var neoFormNotation = "de.oceanlabs.mcp:mcp_config:" + mcpVersion + "@zip"; + dependencies = ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation); + } else { + throw new InvalidUserCodeException("You must specify a Forge, NeoForge or MCP version"); + } + + var configurations = project.getConfigurations(); + + var artifacts = ModDevArtifactsWorkflow.create( + project, + settings.getEnabledSourceSets(), + Branding.MDG, + extension, + dependencies, + artifactNamingStrategy, + configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), + configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), + versionCapabilities + ); - var obf = project.getExtensions().create("obfuscation", Obfuscation.class, project, namedToIntermediate, mappingsCsv, autoRenamingToolRuntime, installerToolsRuntime, extraMixinMappings); - var mixin = project.getExtensions().create("mixin", MixinExtension.class, project, namedToIntermediate, extraMixinMappings); + var runs = ModDevRunWorkflow.create( + project, + Branding.MDG, + artifacts, + extension.getRuns() + ); - project.getExtensions().configure(NeoForgeExtension.class, extension -> { - extension.getNeoForgeArtifact().set(extension.getVersion().map(version -> "net.minecraftforge:forge:" + version)); - extension.getNeoFormArtifact().set(extension.getNeoFormVersion().map(version -> "de.oceanlabs.mcp:mcp_config:" + version)); + // Configure the mixin and obfuscation extensions + var mixin = ExtensionUtils.getExtension(project, MIXIN_EXTENSION, MixinExtension.class); + var obf = ExtensionUtils.getExtension(project, OBFUSCATION_EXTENSION, ObfuscationExtension.class); - extension.getAdditionalMinecraftArtifacts().put("namedToIntermediaryMapping", namedToIntermediate.map(RegularFile::getAsFile)); - extension.getAdditionalMinecraftArtifacts().put("intermediaryToNamedMapping", intermediateToNamed.map(RegularFile::getAsFile)); - extension.getAdditionalMinecraftArtifacts().put("csvMapping", mappingsCsv.map(RegularFile::getAsFile)); + // We use this directory to store intermediate files used during moddev + var namedToIntermediate = artifacts.requestAdditionalMinecraftArtifact("namedToIntermediaryMapping", "namedToIntermediate.tsrg"); + obf.getNamedToSrgMappings().set(namedToIntermediate); + var intermediateToNamed = artifacts.requestAdditionalMinecraftArtifact("intermediaryToNamedMapping", "intermediateToNamed.srg"); + var mappingsCsv = artifacts.requestAdditionalMinecraftArtifact("csvMapping", "intermediateToNamed.zip"); + obf.getSrgToNamedMappings().set(mappingsCsv); - extension.getRuns().configureEach(run -> { - LegacyForgeFacade.configureRun(project, run); + extension.getRuns().configureEach(run -> { + LegacyForgeFacade.configureRun(project, run); - // Mixin needs the intermediate (SRG) -> named (Mojang, MCP) mapping file in SRG (TSRG is not supported) to be able to ignore the refmaps of dependencies - run.getSystemProperties().put("mixin.env.remapRefMap", "true"); - run.getSystemProperties().put("mixin.env.refMapRemappingFile", intermediateToNamed.map(f -> f.getAsFile().getAbsolutePath())); + // Mixin needs the intermediate (SRG) -> named (Mojang, MCP) mapping file in SRG (TSRG is not supported) to be able to ignore the refmaps of dependencies + run.getSystemProperties().put("mixin.env.remapRefMap", "true"); + run.getSystemProperties().put("mixin.env.refMapRemappingFile", intermediateToNamed.map(f -> f.getAsFile().getAbsolutePath())); - run.getProgramArguments().addAll(mixin.getConfigs().map(cfgs -> cfgs.stream().flatMap(config -> Stream.of("--mixin.config", config)).toList())); - }); + run.getProgramArguments().addAll(mixin.getConfigs().map(cfgs -> cfgs.stream().flatMap(config -> Stream.of("--mixin.config", config)).toList())); }); var reobfJar = obf.reobfuscate( @@ -105,25 +205,22 @@ public void apply(Project project) { project.getTasks().named("assemble", assemble -> assemble.dependsOn(reobfJar)); // Forge expects the mapping csv files on the root classpath - project.getConfigurations().getByName(ModDevPlugin.CONFIGURATION_RUNTIME_DEPENDENCIES) + artifacts.runtimeDependencies() .getDependencies().add(project.getDependencyFactory().create(project.files(mappingsCsv))); // 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. // MDG already adds cilent-extra, but the forge jar is missing. - project.getConfigurations().getByName("additionalRuntimeClasspath") - .extendsFrom(project.getConfigurations().getByName(ModDevPlugin.CONFIGURATION_RUNTIME_DEPENDENCIES)) + runs.getAdditionalClasspath() + .extendsFrom(artifacts.runtimeDependencies()) .exclude(Map.of("group", "net.neoforged", "module", "DevLaunch")); - project.getDependencies().attributesSchema(schema -> schema.attribute(REMAPPED)); - project.getDependencies().getArtifactTypes().named("jar", a -> a.getAttributes().attribute(REMAPPED, false)); - var remapDeps = project.getConfigurations().create("remappingDependencies", spec -> { spec.setDescription("An internal configuration that contains the Minecraft dependencies, used for remapping mods"); spec.setCanBeConsumed(false); spec.setCanBeDeclared(false); spec.setCanBeResolved(true); - spec.extendsFrom(project.getConfigurations().getByName(ModDevPlugin.CONFIGURATION_RUNTIME_DEPENDENCIES)); + spec.extendsFrom(artifacts.runtimeDependencies()); }); project.getDependencies().registerTransform(RemappingTransform.class, params -> { @@ -138,24 +235,11 @@ public void apply(Project project) { .attribute(REMAPPED, true) .attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE); }); + } - // Set the right Java version - project.getTasks().withType(CreateMinecraftArtifacts.class).configureEach(task -> { - var extension = ExtensionUtils.getExtension(project, NeoForgeExtension.NAME, NeoForgeExtension.class); - var toolchainService = ExtensionUtils.getExtension(project, "javaToolchains", JavaToolchainService.class); - task.getToolsJavaExecutable().set( - toolchainService.launcherFor(spec -> { - spec.getLanguageVersion().set( - extension.getVersion().map(VersionCapabilities::ofForgeVersion) - .orElse(extension.getNeoFormVersion().map(VersionCapabilities::ofNeoFormVersion)) - .map(VersionCapabilities::javaVersion) - .map(JavaLanguageVersion::of) - ); - }) - .map(JavaLauncher::getExecutablePath) - .map(f -> f.getAsFile().getAbsolutePath()) - ); - }); + private static void configureDependencyRemapping(Project project, ObfuscationExtension obf) { + project.getDependencies().attributesSchema(schema -> schema.attribute(REMAPPED)); + project.getDependencies().getArtifactTypes().named("jar", a -> a.getAttributes().attribute(REMAPPED, false)); obf.createRemappingConfiguration(project.getConfigurations().getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME)); obf.createRemappingConfiguration(project.getConfigurations().getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME)); diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/ModDevExtension.java b/src/main/java/net/neoforged/moddevgradle/dsl/ModDevExtension.java new file mode 100644 index 00000000..5ccfee1b --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/dsl/ModDevExtension.java @@ -0,0 +1,161 @@ +package net.neoforged.moddevgradle.dsl; + +import net.neoforged.moddevgradle.internal.Branding; +import net.neoforged.moddevgradle.internal.IdeIntegration; +import net.neoforged.moddevgradle.internal.ModDevArtifactsWorkflow; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; + +import javax.inject.Inject; +import java.io.File; + +public abstract class ModDevExtension { + private final NamedDomainObjectContainer mods; + private final NamedDomainObjectContainer runs; + private final Parchment parchment; + + private final Project project; + private final DataFileCollection accessTransformers; + private final DataFileCollection interfaceInjectionData; + + @Inject + public ModDevExtension(Project project, + DataFileCollection accessTransformers, + DataFileCollection interfaceInjectionData) { + mods = project.container(ModModel.class); + runs = project.container(RunModel.class, name -> project.getObjects().newInstance(RunModel.class, name, project, mods)); + parchment = project.getObjects().newInstance(Parchment.class); + this.project = project; + this.accessTransformers = accessTransformers; + this.interfaceInjectionData = interfaceInjectionData; + getValidateAccessTransformers().convention(false); + + // Make sync tasks run + var ideIntegration = IdeIntegration.of(project, Branding.MDG); + ideIntegration.runTaskOnProjectSync(getIdeSyncTasks()); + } + + /** + * The list of additional access transformers that should be applied to the Minecraft source code. + *

+ * If you do not set this property, the plugin will look for an access transformer file at + * {@code META-INF/accesstransformer.cfg} relative to your main source sets resource directories. + * + * @see Access Transformer File Format + */ + public void accessTransformers(Action action) { + action.execute(accessTransformers); + } + + public DataFileCollection getAccessTransformers() { + return accessTransformers; + } + + /** + * Replaces current access transformers. + */ + public void setAccessTransformers(Object... paths) { + getAccessTransformers().getFiles().setFrom(paths); + } + + /** + * The data-files describing additional interface implementation declarations to be added to + * Minecraft classes. + *

+ * This is an advanced property: Injecting interfaces in your development environment using this property will not implement + * the interfaces in your published mod. You have to use Mixin or ASM to do that. + * + * @see Interface Injection Data Format + */ + public void interfaceInjectionData(Action action) { + action.execute(interfaceInjectionData); + } + + public DataFileCollection getInterfaceInjectionData() { + return interfaceInjectionData; + } + + /** + * Replaces current interface injection data files. + */ + public void setInterfaceInjectionData(Object... paths) { + getInterfaceInjectionData().getFiles().setFrom(paths); + } + + /** + * Enable access transformer validation, raising fatal errors if an AT targets a member that doesn't exist. + *

+ * Default {@code false}
+ */ + public abstract Property getValidateAccessTransformers(); + + public NamedDomainObjectContainer getMods() { + return mods; + } + + public void mods(Action> action) { + action.execute(mods); + } + + public NamedDomainObjectContainer getRuns() { + return runs; + } + + public void runs(Action> action) { + action.execute(runs); + } + + public Parchment getParchment() { + return parchment; + } + + public void parchment(Action action) { + action.execute(parchment); + } + + /** + * The tasks to be run when the IDE reloads the Gradle project. + */ + public abstract ListProperty> getIdeSyncTasks(); + + /** + * Configures the given task to be run when the IDE reloads the Gradle project. + */ + public void ideSyncTask(TaskProvider task) { + this.getIdeSyncTasks().add(task); + } + + /** + * Configures the given task to be run when the IDE reloads the Gradle project. + */ + public void ideSyncTask(Task task) { + this.getIdeSyncTasks().add(task.getProject().getTasks().named(task.getName())); + } + + /** + * Used to request additional Minecraft artifacts from NFRT for advanced usage scenarios. + *

+ * Maps a result name to the file it should be written to. + * The result names are specific to the NeoForm process that is being used in the background and may change between + * NeoForge versions. + */ + public abstract MapProperty getAdditionalMinecraftArtifacts(); + + /** + * Adds the necessary dependencies to develop a Minecraft mod to additional source sets. + * If you do not specify a source set when you enable modding, the dependencies are automatically added + * to the main source set. + */ + public void addModdingDependenciesTo(SourceSet sourceSet) { + ModDevArtifactsWorkflow.get(project).addToSourceSet(sourceSet); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java new file mode 100644 index 00000000..08c61f3c --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java @@ -0,0 +1,64 @@ +package net.neoforged.moddevgradle.dsl; + +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import java.util.HashSet; +import java.util.Set; + +public abstract class ModdingVersionSettings { + @Nullable + private String version; + + @Nullable + private String neoFormVersion; + + private Set enabledSourceSets = new HashSet<>(); + + @Inject + public ModdingVersionSettings(Project project) { + // By default, enable modding deps only for the main source set + var sourceSets = ExtensionUtils.getSourceSets(project); + var mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + enabledSourceSets.add(mainSourceSet); + } + + public @Nullable String getVersion() { + return version; + } + + public @Nullable String getNeoFormVersion() { + return neoFormVersion; + } + + /** + * NeoForge version number. You have to set either this or {@link #setNeoFormVersion}. + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * You can set this property to a version of NeoForm + * to either override the version used in the version of NeoForge you set, or to compile against + * Vanilla artifacts that have no NeoForge code added. + */ + public void setNeoFormVersion(String version) { + this.neoFormVersion = version; + } + + /** + * Contains the list of source sets for which access to Minecraft classes should be configured. + * Defaults to the main source set, but can also be set to an empty list. + */ + public Set getEnabledSourceSets() { + return enabledSourceSets; + } + + public void setEnabledSourceSets(Set enabledSourceSets) { + this.enabledSourceSets = enabledSourceSets; + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/NeoForgeExtension.java b/src/main/java/net/neoforged/moddevgradle/dsl/NeoForgeExtension.java index 312d7a54..04984dfc 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/NeoForgeExtension.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/NeoForgeExtension.java @@ -3,175 +3,64 @@ import net.neoforged.moddevgradle.internal.ModDevPlugin; import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import org.gradle.api.Action; -import org.gradle.api.GradleException; -import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.provider.ListProperty; -import org.gradle.api.provider.MapProperty; -import org.gradle.api.provider.Property; import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskProvider; import javax.inject.Inject; -import java.io.File; +import java.util.List; /** * This is the top-level {@code neoForge} extension, used to configure the moddev plugin. */ -public abstract class NeoForgeExtension { +public abstract class NeoForgeExtension extends ModDevExtension { public static final String NAME = "neoForge"; private final Project project; - private final NamedDomainObjectContainer mods; - private final NamedDomainObjectContainer runs; - private final Parchment parchment; private final UnitTest unitTest; - private final DataFileCollection accessTransformers; - private final DataFileCollection interfaceInjectionData; - @Inject public NeoForgeExtension(Project project, DataFileCollection accessTransformers, DataFileCollection interfaceInjectionData) { + super(project, accessTransformers, interfaceInjectionData); this.project = project; - mods = project.container(ModModel.class); - runs = project.container(RunModel.class, name -> project.getObjects().newInstance(RunModel.class, name, project, mods)); - parchment = project.getObjects().newInstance(Parchment.class); unitTest = project.getObjects().newInstance(UnitTest.class); - getNeoForgeArtifact().convention(getVersion().map(version -> "net.neoforged:neoforge:" + version)); - getNeoFormArtifact().convention(getNeoFormVersion().map(version -> "net.neoforged:neoform:" + version)); - - this.accessTransformers = accessTransformers; - this.interfaceInjectionData = interfaceInjectionData; - getValidateAccessTransformers().convention(false); unitTest.getLoadedMods().convention(getMods()); } /** - * Adds the necessary dependencies to develop a Minecraft mod to the given source set. - * The plugin automatically adds these dependencies to the main source set. - */ - public void addModdingDependenciesTo(SourceSet sourceSet) { - var configurations = project.getConfigurations(); - var sourceSets = ExtensionUtils.getSourceSets(project); - if (!sourceSets.contains(sourceSet)) { - throw new GradleException("Cannot add to the source set in another project."); - } - - configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()) - .extendsFrom(configurations.getByName(ModDevPlugin.CONFIGURATION_RUNTIME_DEPENDENCIES)); - configurations.getByName(sourceSet.getCompileClasspathConfigurationName()) - .extendsFrom(configurations.getByName(ModDevPlugin.CONFIGURATION_COMPILE_DEPENDENCIES)); - } - - /** - * NeoForge version number. You have to set either this, {@link #getNeoFormVersion()} - * or {@link #getNeoFormArtifact()}. - */ - public abstract Property getVersion(); - - /** - * You can set this property to a version of NeoForm - * to either override the version used in the version of NeoForge you set, or to compile against - * Vanilla artifacts that have no NeoForge code added. - *

- * This property is mutually exclusive with {@link #getNeoFormArtifact()}. - */ - public abstract Property getNeoFormVersion(); - - /** - * Is derived automatically from {@link #getVersion()}. - * - * @return Maven artifact coordinate (group:module:version) - */ - public abstract Property getNeoForgeArtifact(); - - /** - * Derived automatically from the {@link #getNeoFormVersion()}. - * You can override this property to use i.e. MCP for up to 1.20.1. - *

- * This property is mutually exclusive with {@link #getNeoForgeArtifact()}. - * - * @return Maven artifact coordinate (group:module:version) - */ - public abstract Property getNeoFormArtifact(); - - /** - * The list of additional access transformers that should be applied to the Minecraft source code. - *

- * If you do not set this property, the plugin will look for an access transformer file at - * {@code META-INF/accesstransformer.cfg} relative to your main source sets resource directories. + * Enables modding on the main source set with the given NeoForge version. * - * @see Access Transformer File Format + * Shorthand for: + * + * enable { version = '...' } + * */ - public void accessTransformers(Action action) { - action.execute(accessTransformers); - } - - public DataFileCollection getAccessTransformers() { - return accessTransformers; + public void setVersion(String version) { + enable(settings -> { + settings.setVersion(version); + }); } /** - * Replaces current access transformers. - */ - public void setAccessTransformers(Object... paths) { - getAccessTransformers().getFiles().setFrom(paths); - } - - /** - * The data-files describing additional interface implementation declarations to be added to - * Minecraft classes. - *

- * This is an advanced property: Injecting interfaces in your development environment using this property will not implement - * the interfaces in your published mod. You have to use Mixin or ASM to do that. + * Enables the Vanilla-only mode of ModDevGradle. * - * @see Interface Injection Data Format + * Shorthand for: + * + * enable { neoFormVersion = '...' } + * */ - public void interfaceInjectionData(Action action) { - action.execute(interfaceInjectionData); + public void setNeoFormVersion(String version) { + enable(settings -> { + settings.setNeoFormVersion(version); + }); } - public DataFileCollection getInterfaceInjectionData() { - return interfaceInjectionData; - } + public void enable(Action customizer) { + var modDevPlugin = project.getPlugins().getPlugin(ModDevPlugin.class); - /** - * Replaces current interface injection data files. - */ - public void setInterfaceInjectionData(Object... paths) { - getInterfaceInjectionData().getFiles().setFrom(paths); - } - - /** - * Enable access transformer validation, raising fatal errors if an AT targets a member that doesn't exist. - *

- * Default {@code false}
- */ - public abstract Property getValidateAccessTransformers(); - - public NamedDomainObjectContainer getMods() { - return mods; - } - - public void mods(Action> action) { - action.execute(mods); - } + var settings = project.getObjects().newInstance(ModdingVersionSettings.class); + customizer.execute(settings); - public NamedDomainObjectContainer getRuns() { - return runs; - } - - public void runs(Action> action) { - action.execute(runs); - } - - public Parchment getParchment() { - return parchment; - } - - public void parchment(Action action) { - action.execute(parchment); + modDevPlugin.enable(project, settings, this); } public UnitTest getUnitTest() { @@ -181,33 +70,4 @@ public UnitTest getUnitTest() { public void unitTest(Action action) { action.execute(unitTest); } - - - /** - * The tasks to be run when the IDE reloads the Gradle project. - */ - public abstract ListProperty> getIdeSyncTasks(); - - /** - * Configures the given task to be run when the IDE reloads the Gradle project. - */ - public void ideSyncTask(TaskProvider task) { - this.getIdeSyncTasks().add(task); - } - - /** - * Configures the given task to be run when the IDE reloads the Gradle project. - */ - public void ideSyncTask(Task task) { - this.getIdeSyncTasks().add(task.getProject().getTasks().named(task.getName())); - } - - /** - * Used to request additional Minecraft artifacts from NFRT for advanced usage scenarios. - *

- * Maps a result name to the file it should be written to. - * The result names are specific to the NeoForm process that is being used in the background and may change between - * NeoForge versions. - */ - public abstract MapProperty getAdditionalMinecraftArtifacts(); } diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/UnitTest.java b/src/main/java/net/neoforged/moddevgradle/dsl/UnitTest.java index e987f94c..5e9442c1 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/UnitTest.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/UnitTest.java @@ -1,6 +1,6 @@ package net.neoforged.moddevgradle.dsl; -import net.neoforged.moddevgradle.internal.ModDevPlugin; +import net.neoforged.moddevgradle.internal.ModDevRunWorkflow; import org.gradle.api.Project; import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; @@ -22,7 +22,10 @@ public UnitTest(Project project) { * Enables the integration. */ public void enable() { - project.getPlugins().getPlugin(ModDevPlugin.class).setupTestTask(); + ModDevRunWorkflow.get(project).configureTesting( + getTestedMod(), + getLoadedMods() + ); } /** diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ArtifactNamingStrategy.java b/src/main/java/net/neoforged/moddevgradle/internal/ArtifactNamingStrategy.java new file mode 100644 index 00000000..b22a44d9 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/ArtifactNamingStrategy.java @@ -0,0 +1,16 @@ +package net.neoforged.moddevgradle.internal; + +import org.jetbrains.annotations.ApiStatus; + +@FunctionalInterface +@ApiStatus.Internal +public interface ArtifactNamingStrategy { + static ArtifactNamingStrategy createDefault(String artifactFilenamePrefix) { + return (artifact) -> { + // It's helpful to be able to differentiate the Vanilla jar and the NeoForge jar in classic multiloader setups. + return artifactFilenamePrefix + artifact.defaultSuffix + ".jar"; + }; + } + + String getFilename(WorkflowArtifact artifact); +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/Branding.java b/src/main/java/net/neoforged/moddevgradle/internal/Branding.java index 9b07605f..5efcac01 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/Branding.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/Branding.java @@ -6,7 +6,7 @@ * @param publicTaskGroup Use this group for tasks that are considered to be part of the user-interface of MDG. * @param internalTaskGroup Use this group for tasks that are considered to be an implementation detail of MDG. */ -record Branding(String publicTaskGroup, String internalTaskGroup) { +public record Branding(String publicTaskGroup, String internalTaskGroup) { public static final Branding MDG = new Branding("mod development", "mod development/internal"); public static final Branding NEODEV = new Branding("neoforge development", "neoforge development/internal"); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/DataFileCollections.java b/src/main/java/net/neoforged/moddevgradle/internal/DataFileCollections.java new file mode 100644 index 00000000..896ec937 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/DataFileCollections.java @@ -0,0 +1,128 @@ +package net.neoforged.moddevgradle.internal; + +import net.neoforged.moddevgradle.dsl.DataFileCollection; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurablePublishArtifact; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.Category; +import org.gradle.api.component.AdhocComponentWithVariants; +import org.gradle.api.tasks.SourceSet; +import org.jetbrains.annotations.ApiStatus; + +import java.io.File; +import java.util.function.Consumer; + +/** + * Access Transformers and Interface Injection Data are treated in a common way as "collections of data files", + * which can be declared via a {@link DataFileCollection DSL}, and have an associated configuration for internal + * use by the plugin and the publication of these files. + *

+ * This factory constructs these pairs. + */ +@ApiStatus.Internal +public record DataFileCollections(CollectionWrapper accessTransformers, + CollectionWrapper interfaceInjectionData) { + public static final String CONFIGURATION_ACCESS_TRANSFORMERS = "accessTransformers"; + + public static final String CONFIGURATION_INTERFACE_INJECTION_DATA = "interfaceInjectionData"; + + /** + * Constructs the default data file collections for access transformers and intrface injection data + * with sensible defaults. + */ + public static DataFileCollections create(Project project) { + // Create an access transformer configuration + var accessTransformers = createCollection( + project, + CONFIGURATION_ACCESS_TRANSFORMERS, + "AccessTransformers to widen visibility of Minecraft classes/fields/methods", + "accesstransformer" + ); + accessTransformers.extension().getFiles().convention(project.provider(() -> { + var collection = project.getObjects().fileCollection(); + + // Only return this when it actually exists + var mainSourceSet = ExtensionUtils.getSourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); + for (var resources : mainSourceSet.getResources().getSrcDirs()) { + var defaultPath = new File(resources, "META-INF/accesstransformer.cfg"); + if (project.file(defaultPath).exists()) { + return collection.from(defaultPath.getAbsolutePath()); + } + } + + return collection; + })); + + // Create a configuration for grabbing interface injection data + var interfaceInjectionData = createCollection( + project, + CONFIGURATION_INTERFACE_INJECTION_DATA, + "Interface injection data adds extend/implements clauses for interfaces to Minecraft code at development time", + "interfaceinjection" + ); + + return new DataFileCollections(accessTransformers, interfaceInjectionData); + } + + public record CollectionWrapper(DataFileCollection extension, Configuration configuration) { + } + + private static CollectionWrapper createCollection(Project project, String name, String description, String category) { + var configuration = project.getConfigurations().create(name, spec -> { + spec.setDescription(description); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + spec.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.CATEGORY_ATTRIBUTE.getType(), category)); + }); + }); + + var elementsConfiguration = project.getConfigurations().create(name + "Elements", spec -> { + spec.setDescription("Published data files for " + name); + spec.setCanBeConsumed(true); + spec.setCanBeResolved(false); + spec.attributes(attributes -> { + attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.CATEGORY_ATTRIBUTE.getType(), category)); + }); + }); + + // Set up the variant publishing conditionally + var java = (AdhocComponentWithVariants) project.getComponents().getByName("java"); + java.addVariantsFromConfiguration(elementsConfiguration, variant -> { + // This should be invoked lazily, so checking if the artifacts are empty is fine: + // "The details object used to determine what to do with a configuration variant **when publishing**." + if (variant.getConfigurationVariant().getArtifacts().isEmpty()) { + variant.skip(); + } + }); + + var depFactory = project.getDependencyFactory(); + Consumer publishCallback = new Consumer<>() { + ConfigurablePublishArtifact firstArtifact; + int artifactCount; + + @Override + public void accept(Object artifactNotation) { + elementsConfiguration.getDependencies().add(depFactory.create(project.files(artifactNotation))); + project.getArtifacts().add(elementsConfiguration.getName(), artifactNotation, artifact -> { + if (firstArtifact == null) { + firstArtifact = artifact; + artifact.setClassifier(category); + artifactCount = 1; + } else { + if (artifactCount == 1) { + firstArtifact.setClassifier(category + artifactCount); + } + artifact.setClassifier(category + (++artifactCount)); + } + }); + } + }; + + var extension = project.getObjects().newInstance(DataFileCollection.class, publishCallback); + configuration.getDependencies().add(depFactory.create(extension.getFiles())); + + return new CollectionWrapper(extension, configuration); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java index 3a7e7324..ba9b6410 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java @@ -11,9 +11,7 @@ import org.gradle.api.Project; import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; -import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.TaskProvider; import org.gradle.plugins.ide.eclipse.EclipsePlugin; import org.gradle.plugins.ide.eclipse.model.Classpath; @@ -86,8 +84,8 @@ public void configureRuns(Map> prepareRunTask } @Override - public void configureTesting(SetProperty loadedMods, - Property testedMod, + public void configureTesting(Provider> loadedMods, + Provider testedMod, Provider runArgsDir, File gameDirectory, Provider programArgsFile, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java index 17d3ad51..7ae3ebf1 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java @@ -8,20 +8,19 @@ import org.gradle.api.Task; import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; -import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.TaskProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.Map; +import java.util.Set; /** * Implementing classes are responsible for registering {@code ideSyncTask} with their IDE. */ -sealed abstract class IdeIntegration permits IntelliJIntegration, EclipseIntegration, NoIdeIntegration { +public sealed abstract class IdeIntegration permits IntelliJIntegration, EclipseIntegration, NoIdeIntegration { private static final Logger LOG = LoggerFactory.getLogger(IdeIntegration.class); /** @@ -95,8 +94,8 @@ public final void runTaskOnProjectSync(Object task) { void configureRuns(Map> prepareRunTasks, Iterable runs) { } - void configureTesting(SetProperty loadedMods, - Property testedMod, + void configureTesting(Provider> loadedMods, + Provider testedMod, Provider runArgsDir, File gameDirectory, Provider programArgsFile, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java index 0f8ed346..35bf723e 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java @@ -11,9 +11,7 @@ import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; import org.gradle.api.plugins.ExtensionAware; -import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskProvider; import org.gradle.internal.DefaultTaskExecutionRequest; @@ -112,8 +110,8 @@ public void configureRuns(Map> prepareRunTask } @Override - public void configureTesting(SetProperty loadedMods, - Property testedMod, + public void configureTesting(Provider> loadedMods, + Provider testedMod, Provider runArgsDir, File gameDirectory, Provider programArgsFile, @@ -141,7 +139,7 @@ public void configureTesting(SetProperty loadedMods, if (intelliJRunConfigurations != null) { intelliJRunConfigurations.defaults(JUnit.class, jUnitDefaults -> { // $MODULE_WORKING_DIR$ is documented here: https://www.jetbrains.com/help/idea/absolute-path-variables.html - jUnitDefaults.setWorkingDirectory("$MODULE_WORKING_DIR$/" + ModDevPlugin.JUNIT_GAME_DIR); + jUnitDefaults.setWorkingDirectory("$MODULE_WORKING_DIR$/" + ModDevRunWorkflow.JUNIT_GAME_DIR); jUnitDefaults.setVmParameters( // The FML JUnit plugin uses this system property to read a file containing the program arguments needed to launch // NOTE: IntelliJ does not support $MODULE_WORKING_DIR$ in VM Arguments diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java new file mode 100644 index 00000000..924427a7 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java @@ -0,0 +1,352 @@ +package net.neoforged.moddevgradle.internal; + +import net.neoforged.minecraftdependencies.MinecraftDistribution; +import net.neoforged.moddevgradle.dsl.ModDevExtension; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.internal.utils.VersionCapabilities; +import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; +import net.neoforged.nfrtgradle.DownloadAssets; +import org.gradle.api.GradleException; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Named; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.attributes.AttributeContainer; +import org.gradle.api.attributes.Category; +import org.gradle.api.attributes.DocsType; +import org.gradle.api.attributes.Usage; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFile; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * The workflow needed to produce artifacts and assets for compiling and running a mod. + */ +@ApiStatus.Internal +public record ModDevArtifactsWorkflow( + Project project, + ModdingDependencies dependencies, + VersionCapabilities versionCapabilities, + TaskProvider createArtifacts, + TaskProvider downloadAssets, + Configuration runtimeDependencies, + Configuration compileDependencies, + Provider modDevBuildDir, + Provider artifactsBuildDir +) { + + private static final String EXTENSION_NAME = "__internal_modDevArtifactsWorkflow"; + + public static ModDevArtifactsWorkflow get(Project project) { + var result = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevArtifactsWorkflow.class); + if (result == null) { + throw new IllegalStateException("Mod development has not been enabled yet for project " + project); + } + return result; + } + + public static ModDevArtifactsWorkflow create(Project project, + Collection enabledSourceSets, + Branding branding, + ModDevExtension extension, + ModdingDependencies moddingDependencies, + ArtifactNamingStrategy artifactNamingStrategy, + Configuration accessTransformers, + Configuration interfaceInjectionData, + VersionCapabilities versionCapabilities + ) { + if (project.getExtensions().findByName(EXTENSION_NAME) != null) { + throw new InvalidUserCodeException("You cannot enable modding in the same project twice."); + } + + var ideIntegration = IdeIntegration.of(project, branding); + + // We use this directory to store intermediate files used during moddev + var modDevBuildDir = project.getLayout().getBuildDirectory().dir("moddev"); + var artifactsBuildDir = project.getLayout().getBuildDirectory().dir("moddev/artifacts"); + + var createManifestConfigurations = configureArtifactManifestConfigurations( + project, + moddingDependencies.neoForgeDependency(), + moddingDependencies.neoFormDependency() + ); + + var dependencyFactory = project.getDependencyFactory(); + var configurations = project.getConfigurations(); + var tasks = project.getTasks(); + var javaExtension = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); + + // Users can theoretically compile their mods at higher java versions than used by Minecraft, + // but it's more important to default the common user to the right Java version. + var javaToolchainService = ExtensionUtils.getExtension(project, "javaToolchains", JavaToolchainService.class); + + // Try to give people at least a fighting chance to run on the correct java version + var toolchainSpec = javaExtension.getToolchain(); + try { + toolchainSpec.getLanguageVersion().convention(JavaLanguageVersion.of(versionCapabilities.javaVersion())); + } catch (IllegalStateException e) { + // We tried our best, but the java version was already resolved and is thus finalized + // this can occur if any dependency resolution happens since it reads this version for the attributes + } + + var parchment = extension.getParchment(); + var parchmentData = configurations.create("parchmentData", spec -> { + spec.setDescription("Data used to add parameter names and javadoc to Minecraft sources"); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.setTransitive(false); // Expect a single result + spec.getDependencies().addLater(parchment.getParchmentArtifact().map(dependencyFactory::create)); + }); + + // it has to contain client-extra to be loaded by FML, and it must be added to the legacy CP + var createArtifacts = tasks.register("createMinecraftArtifacts", CreateMinecraftArtifacts.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Creates the NeoForge and Minecraft artifacts by invoking NFRT."); + for (var configuration : createManifestConfigurations) { + task.addArtifactsToManifest(configuration); + } + + // NFRT needs access to a JDK of the right version to be able to correctly decompile and recompile the code + task.getToolsJavaExecutable().set(javaToolchainService + .launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(versionCapabilities.javaVersion()))) + .map(javaLauncher -> javaLauncher.getExecutablePath().getAsFile().getAbsolutePath()) + ); + + task.getAccessTransformers().from(accessTransformers); + task.getInterfaceInjectionData().from(interfaceInjectionData); + task.getValidateAccessTransformers().set(extension.getValidateAccessTransformers()); + task.getParchmentData().from(parchmentData); + task.getParchmentEnabled().set(parchment.getEnabled()); + task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); + + Function> artifactPathStrategy = artifact -> + artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); + + task.getCompiledArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED)); + task.getCompiledWithSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED_WITH_SOURCES)); + task.getSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.SOURCES)); + task.getResourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.CLIENT_RESOURCES)); + + task.getNeoForgeArtifact().set(moddingDependencies.neoForgeDependencyNotation()); + task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); + task.getAdditionalResults().putAll(extension.getAdditionalMinecraftArtifacts()); + }); + ideIntegration.runTaskOnProjectSync(createArtifacts); + + var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> { + // Not in the internal group in case someone wants to "preload" the asset before they go offline + task.setGroup(branding.publicTaskGroup()); + task.setDescription("Downloads the Minecraft assets and asset index needed to run a Minecraft client or generate client-side resources."); + // While downloadAssets does not require *all* of the dependencies, it does need NeoForge/NeoForm to benefit + // from any caching/overrides applied to these dependencies in Gradle + for (var configuration : createManifestConfigurations) { + task.addArtifactsToManifest(configuration); + } + task.getAssetPropertiesFile().set(modDevBuildDir.map(dir -> dir.file("minecraft_assets.properties"))); + task.getNeoForgeArtifact().set(moddingDependencies.neoForgeDependencyNotation()); + task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); + }); + + // For IntelliJ, we attach a combined sources+classes artifact which enables an "Attach Sources..." link for IJ users + // Otherwise, attaching sources is a pain for IJ users. + Provider minecraftClassesArtifact; + if (ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { + minecraftClassesArtifact = createArtifacts.map(task -> project.files(task.getCompiledWithSourcesArtifact())); + } else { + minecraftClassesArtifact = createArtifacts.map(task -> project.files(task.getCompiledArtifact())); + } + + // Name of the configuration in which we place the required dependencies to develop mods for use in the runtime-classpath. + // We cannot use "runtimeOnly", since the contents of that are published. + var runtimeDependencies = configurations.create("modDevRuntimeDependencies", config -> { + config.setDescription("The runtime dependencies to develop a mod for, including Minecraft classes and modding platform classes."); + config.setCanBeResolved(false); + config.setCanBeConsumed(false); + + config.getDependencies().addLater(minecraftClassesArtifact.map(dependencyFactory::create)); + config.getDependencies().addLater(createArtifacts.map(task -> project.files(task.getResourcesArtifact())).map(dependencyFactory::create)); + // Technically, the Minecraft dependencies do not strictly need to be on the classpath because they are pulled from the legacy class path. + // However, we do it anyway because this matches production environments, and allows launch proxies such as DevLogin to use Minecraft's libraries. + config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); + }); + + // Configuration in which we place the required dependencies to develop mods for use in the compile-classpath. + // While compile only is not published, we also use a configuration here to be consistent. + var compileDependencies = configurations.create("modDevCompileDependencies", config -> { + config.setDescription("The compile-time dependencies to develop a mod, including Minecraft and modding platform classes."); + config.setCanBeResolved(false); + config.setCanBeConsumed(false); + config.getDependencies().addLater(minecraftClassesArtifact.map(dependencyFactory::create)); + config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); + }); + + // For IDEs that support it, link the source/binary artifacts if we use separated ones + if (!ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { + ideIntegration.attachSources( + Map.of( + createArtifacts.get().getCompiledArtifact(), + createArtifacts.get().getSourcesArtifact() + ) + ); + } + + var result = new ModDevArtifactsWorkflow( + project, + moddingDependencies, + versionCapabilities, + createArtifacts, + downloadAssets, + runtimeDependencies, + compileDependencies, + modDevBuildDir, + artifactsBuildDir + ); + + project.getExtensions().add(ModDevArtifactsWorkflow.class, EXTENSION_NAME, result); + + for (var sourceSets : enabledSourceSets) { + result.addToSourceSet(sourceSets); + } + + return result; + } + + /** + * Collects all dependencies needed by the NeoFormRuntime + */ + private static List configureArtifactManifestConfigurations( + Project project, + @Nullable ModuleDependency moddingPlatformDependency, + @Nullable ModuleDependency recompilableMinecraftWorkflowDependency + ) { + var configurations = project.getConfigurations(); + + var configurationPrefix = "neoFormRuntimeDependencies"; + + var result = new ArrayList(); + + // Gradle prevents us from having dependencies with "incompatible attributes" in the same configuration. + // What constitutes incompatible cannot be overridden on a per-configuration basis. + var neoForgeClassesAndData = configurations.create(configurationPrefix + "NeoForgeClasses", spec -> { + spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge classes)"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + if (moddingPlatformDependency != null) { + spec.getDependencies().add(moddingPlatformDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-moddev-bundle"))); + } + + // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode + if (recompilableMinecraftWorkflowDependency != null) { + spec.getDependencies().add(recompilableMinecraftWorkflowDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoform"))); + } + }); + result.add(neoForgeClassesAndData); + + if (moddingPlatformDependency != null) { + var neoForgeSources = configurations.create(configurationPrefix + "NeoForgeSources", spec -> { + spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge sources)"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + spec.getDependencies().add(moddingPlatformDependency); + spec.attributes(attributes -> { + setNamedAttribute(project, attributes, Category.CATEGORY_ATTRIBUTE, Category.DOCUMENTATION); + setNamedAttribute(project, attributes, DocsType.DOCS_TYPE_ATTRIBUTE, DocsType.SOURCES); + }); + }); + result.add(neoForgeSources); + } + + // Compile-time dependencies used by NeoForm, NeoForge and Minecraft. + // Also includes any classes referenced by compiled Minecraft code (used by decompilers, renamers, etc.) + var compileClasspath = configurations.create(configurationPrefix + "CompileClasspath", spec -> { + spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (Classpath)"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + if (moddingPlatformDependency != null) { + spec.getDependencies().add(moddingPlatformDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-dependencies"))); + } + if (recompilableMinecraftWorkflowDependency != null) { + // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode + spec.getDependencies().add(recompilableMinecraftWorkflowDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoform-dependencies"))); + } + spec.attributes(attributes -> { + setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_API); + setNamedAttribute(project, attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT); + }); + }); + result.add(compileClasspath); + + // Runtime-time dependencies used by NeoForm, NeoForge and Minecraft. + var runtimeClasspath = configurations.create(configurationPrefix + "RuntimeClasspath", spec -> { + spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (Classpath)"); + spec.setCanBeConsumed(false); + spec.setCanBeResolved(true); + if (moddingPlatformDependency != null) { + spec.getDependencies().add(moddingPlatformDependency); // Universal Jar + spec.getDependencies().add(moddingPlatformDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-dependencies"))); + } + // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode + if (recompilableMinecraftWorkflowDependency != null) { + spec.getDependencies().add(recompilableMinecraftWorkflowDependency.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoform-dependencies"))); + } + spec.attributes(attributes -> { + setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); + setNamedAttribute(project, attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT); + }); + }); + result.add(runtimeClasspath); + + return result; + } + + /** + * Adds the compile-time and runtime-dependencies needed to compile mod code to the source-set of the given name. + */ + public void addToSourceSet(SourceSet sourceSet) { + var configurations = project.getConfigurations(); + var sourceSets = ExtensionUtils.getSourceSets(project); + if (!sourceSets.contains(sourceSet)) { + throw new GradleException("Cannot add to the source set in another project: " + sourceSet); + } + + configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()).extendsFrom(runtimeDependencies); + configurations.getByName(sourceSet.getCompileClasspathConfigurationName()).extendsFrom(compileDependencies); + } + + public Provider requestAdditionalMinecraftArtifact(String id, String filename) { + return requestAdditionalMinecraftArtifact(id, artifactsBuildDir.map(dir -> dir.file(filename))); + } + + public Provider requestAdditionalMinecraftArtifact(String id, Provider path) { + createArtifacts.configure(task -> task.getAdditionalResults().put(id, path.map(RegularFile::getAsFile))); + return project.getLayout().file( + createArtifacts.flatMap(task -> task.getAdditionalResults().getting(id)) + ); + } + + 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/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index f28a2f91..1977a86b 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -1,62 +1,19 @@ package net.neoforged.moddevgradle.internal; import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin; -import net.neoforged.minecraftdependencies.MinecraftDistribution; -import net.neoforged.moddevgradle.dsl.DataFileCollection; -import net.neoforged.moddevgradle.dsl.InternalModelHelper; -import net.neoforged.moddevgradle.dsl.ModModel; +import net.neoforged.moddevgradle.dsl.ModDevExtension; +import net.neoforged.moddevgradle.dsl.ModdingVersionSettings; import net.neoforged.moddevgradle.dsl.NeoForgeExtension; -import net.neoforged.moddevgradle.dsl.RunModel; -import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.internal.jarjar.JarJarPlugin; import net.neoforged.moddevgradle.internal.utils.VersionCapabilities; -import net.neoforged.moddevgradle.tasks.JarJar; -import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; -import net.neoforged.nfrtgradle.DownloadAssets; import net.neoforged.nfrtgradle.NeoFormRuntimePlugin; -import org.gradle.api.DomainObjectCollection; -import org.gradle.api.Named; +import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.Task; -import org.gradle.api.artifacts.ConfigurablePublishArtifact; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ExternalModuleDependency; -import org.gradle.api.attributes.Attribute; -import org.gradle.api.attributes.AttributeContainer; -import org.gradle.api.attributes.Category; -import org.gradle.api.attributes.DocsType; -import org.gradle.api.attributes.Usage; -import org.gradle.api.component.AdhocComponentWithVariants; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.Directory; -import org.gradle.api.file.RegularFile; -import org.gradle.api.model.ObjectFactory; +import org.gradle.api.artifacts.ModuleDependency; import org.gradle.api.plugins.JavaLibraryPlugin; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.provider.SetProperty; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.api.tasks.bundling.AbstractArchiveTask; -import org.gradle.api.tasks.testing.Test; -import org.gradle.jvm.toolchain.JavaLanguageVersion; -import org.gradle.jvm.toolchain.JavaToolchainService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.event.Level; - -import javax.inject.Inject; -import java.io.File; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; /** * The main plugin class. @@ -64,38 +21,12 @@ public class ModDevPlugin implements Plugin { private static final Logger LOG = LoggerFactory.getLogger(ModDevPlugin.class); - /** - * This must be relative to the project directory since we can only set this to the same project-relative - * directory across all subprojects due to IntelliJ limitations. - */ - static final String JUNIT_GAME_DIR = "build/minecraft-junit"; - - /** - * Name of the configuration in which we place the required dependencies to develop mods for use in the runtime-classpath. - * We cannot use "runtimeOnly", since the contents of that are published. - */ - public static final String CONFIGURATION_RUNTIME_DEPENDENCIES = "neoForgeRuntimeDependencies"; - - /** - * Name of the configuration in which we place the required dependencies to develop mods for use in the compile-classpath. - * While compile only is not published, we also use a configuration here to be consistent. - */ - public static final String CONFIGURATION_COMPILE_DEPENDENCIES = "neoForgeCompileDependencies"; - - private final ObjectFactory objectFactory; - - private Runnable configureTesting = null; - - @Inject - public ModDevPlugin(ObjectFactory objectFactory) { - this.objectFactory = objectFactory; - } - @Override public void apply(Project project) { project.getPlugins().apply(JavaLibraryPlugin.class); project.getPlugins().apply(NeoFormRuntimePlugin.class); project.getPlugins().apply(MinecraftDependenciesPlugin.class); + project.getPlugins().apply(JarJarPlugin.class); // Do not apply the repositories automatically if they have been applied at the settings-level. // It's still possible to apply them manually, though. @@ -104,757 +35,77 @@ public void apply(Project project) { } else { LOG.info("Not enabling NeoForged repositories since they were applied at the settings level"); } - var javaExtension = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); - - var configurations = project.getConfigurations(); - var layout = project.getLayout(); - var tasks = project.getTasks(); - - var ideIntegration = IdeIntegration.of(project, Branding.MDG); - - // We use this directory to store intermediate files used during moddev - var modDevBuildDir = layout.getBuildDirectory().dir("moddev"); - - // Create an access transformer configuration - var accessTransformers = dataFileConfiguration( - project, - "accessTransformers", - "AccessTransformers to widen visibility of Minecraft classes/fields/methods", - "accesstransformer" - ); - accessTransformers.extension.getFiles().convention(project.provider(() -> { - var collection = project.getObjects().fileCollection(); - - // Only return this when it actually exists - var mainSourceSet = ExtensionUtils.getSourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); - for (var resources : mainSourceSet.getResources().getSrcDirs()) { - var defaultPath = new File(resources, "META-INF/accesstransformer.cfg"); - if (project.file(defaultPath).exists()) { - return collection.from(defaultPath.getAbsolutePath()); - } - } - - return collection; - })); - // Create a configuration for grabbing interface injection data - var interfaceInjectionData = dataFileConfiguration( - project, - "interfaceInjectionData", - "Interface injection data adds extend/implements clauses for interfaces to Minecraft code at development time", - "interfaceinjection" - ); - - var extension = project.getExtensions().create( + var dataFileCollections = DataFileCollections.create(project); + project.getExtensions().create( NeoForgeExtension.NAME, NeoForgeExtension.class, - accessTransformers.extension, - interfaceInjectionData.extension + dataFileCollections.accessTransformers().extension(), + dataFileCollections.interfaceInjectionData().extension() ); - ideIntegration.runTaskOnProjectSync(extension.getIdeSyncTasks()); - var dependencyFactory = project.getDependencyFactory(); - - Provider versionCapabilities = extension.getVersion().map(v -> { - // Temporary until we have imperative configuration - if (project.getPlugins().hasPlugin("net.neoforged.moddev.legacyforge")) { - return VersionCapabilities.ofForgeVersion(v); - } else { - return VersionCapabilities.ofNeoForgeVersion(v); - } - }) - .orElse(extension.getNeoFormVersion().map(VersionCapabilities::ofNeoFormVersion)) - .orElse(VersionCapabilities.latest()); - - // When a NeoForge version is specified, we use the dependencies published by that, and otherwise - // we fall back to a potentially specified NeoForm version, which allows us to run in "Vanilla" mode. - var neoForgeModDevLibrariesDependency = extension.getNeoForgeArtifact().map(artifactId -> { - return dependencyFactory.create(artifactId) - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-dependencies"); - }); - }).orElse(extension.getNeoFormArtifact().map(artifact -> { - return dependencyFactory.create(artifact) - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoform-dependencies"); - }); - })); - - var createManifestConfigurations = configureArtifactManifestConfigurations(project, extension); - - // Add a filtered parchment repository automatically if enabled - var parchment = extension.getParchment(); - var parchmentData = configurations.create("parchmentData", spec -> { - spec.setDescription("Data used to add parameter names and javadoc to Minecraft sources"); - spec.setCanBeResolved(true); - spec.setCanBeConsumed(false); - spec.setTransitive(false); // Expect a single result - spec.getDependencies().addLater(parchment.getParchmentArtifact().map(project.getDependencyFactory()::create)); - }); - - // it has to contain client-extra to be loaded by FML, and it must be added to the legacy CP - var createArtifacts = tasks.register("createMinecraftArtifacts", CreateMinecraftArtifacts.class, task -> { - task.setGroup(Branding.MDG.internalTaskGroup()); - task.setDescription("Creates the NeoForge and Minecraft artifacts by invoking NFRT."); - for (var configuration : createManifestConfigurations) { - task.addArtifactsToManifest(configuration); - } + } - task.getAccessTransformers().from(accessTransformers.configuration); - task.getInterfaceInjectionData().from(interfaceInjectionData.configuration); - task.getValidateAccessTransformers().set(extension.getValidateAccessTransformers()); - task.getParchmentData().from(parchmentData); - task.getParchmentEnabled().set(parchment.getEnabled()); - task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); + public void enable( + Project project, + ModdingVersionSettings settings, + ModDevExtension extension + ) { + var neoForgeVersion = settings.getVersion(); + var neoFormVersion = settings.getNeoFormVersion(); + if (neoForgeVersion == null && neoFormVersion == null) { + throw new InvalidUserCodeException("You must specify at least a NeoForge or a NeoForm version for vanilla-only mode"); + } - var minecraftArtifactsDir = modDevBuildDir.map(dir -> dir.dir("artifacts")); - Function> jarPathFactory = suffix -> { - return minecraftArtifactsDir.zip( - // It's helpful to be able to differentiate the Vanilla jar and the NeoForge jar in classic multiloader setups. - extension.getNeoForgeArtifact().map(art -> { - var split = art.split(":", 3); - return split[1] + "-" + split[2]; - }) - .orElse(extension.getNeoFormArtifact().map(v -> "vanilla-" + v.split(":", 3)[2])), - (dir, prefix) -> dir.file(prefix + "-minecraft" + suffix + ".jar")); - }; - task.getCompiledArtifact().set(jarPathFactory.apply("")); - task.getCompiledWithSourcesArtifact().set(jarPathFactory.apply("-merged")); - task.getSourcesArtifact().set(jarPathFactory.apply("-sources")); - task.getResourcesArtifact().set(minecraftArtifactsDir.zip( - extension.getNeoForgeArtifact().map(art -> { - var split = art.split(":", 3); - return split[2] + "-" + split[1]; - }) - .orElse(extension.getNeoFormArtifact().map(v -> "vanilla-" + v.split(":", 3)[2])), - // To support older versions of FML, which pick up the Minecraft jar by looking on the LCP for "forge-", - // we have to ensure client-extra does *not* start with "forge-". - (dir, prefix) -> dir.file("client-extra-aka-minecraft-resources-" + prefix + ".jar") - )); + var dependencyFactory = project.getDependencyFactory(); - task.getNeoForgeArtifact().set(getNeoForgeUserDevDependencyNotation(extension)); - task.getNeoFormArtifact().set(getNeoFormDataDependencyNotation(extension)); - task.getAdditionalResults().putAll(extension.getAdditionalMinecraftArtifacts()); - }); - ideIntegration.runTaskOnProjectSync(createArtifacts); + ModuleDependency neoForge = null; + String neoForgeNotation = null; + if (neoForgeVersion != null) { + neoForge = dependencyFactory.create("net.neoforged:neoforge:" + neoForgeVersion); + neoForgeNotation = "net.neoforged:neoforge:" + neoForgeVersion + ":userdev"; + } - var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> { - // Not in the internal group in case someone wants to "preload" the asset before they go offline - task.setGroup(Branding.MDG.publicTaskGroup()); - task.setDescription("Downloads the Minecraft assets and asset index needed to run a Minecraft client or generate client-side resources."); - // While downloadAssets does not require *all* of the dependencies, it does need NeoForge/NeoForm to benefit - // from any caching/overrides applied to these dependencies in Gradle - for (var configuration : createManifestConfigurations) { - task.addArtifactsToManifest(configuration); - } - task.getAssetPropertiesFile().set(modDevBuildDir.map(dir -> dir.file("minecraft_assets.properties"))); - task.getNeoForgeArtifact().set(getNeoForgeUserDevDependencyNotation(extension)); - task.getNeoFormArtifact().set(getNeoFormDataDependencyNotation(extension)); - }); + ModuleDependency neoForm = null; + String neoFormNotation = null; + if (neoFormVersion != null) { + neoForm = dependencyFactory.create("net.neoforged:neoform:" + neoFormVersion); + neoFormNotation = "net.neoforged:neoform:" + neoFormVersion + "@zip"; + } - // For IntelliJ, we attach a combined sources+classes artifact which enables an "Attach Sources..." link for IJ users - // Otherwise, attaching sources is a pain for IJ users. - Provider minecraftClassesArtifact; - if (ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { - minecraftClassesArtifact = createArtifacts.map(task -> project.files(task.getCompiledWithSourcesArtifact())); + // When a NeoForge version is specified, we use the dependencies published by that, and otherwise + // we fall back to a potentially specified NeoForm version, which allows us to run in "Vanilla" mode. + ArtifactNamingStrategy artifactNamingStrategy; + if (neoForge != null) { + artifactNamingStrategy = ArtifactNamingStrategy.createDefault("neoforge-" + neoForgeVersion); } else { - minecraftClassesArtifact = createArtifacts.map(task -> project.files(task.getCompiledArtifact())); + artifactNamingStrategy = ArtifactNamingStrategy.createDefault("vanilla-" + neoFormVersion); } - configurations.create(CONFIGURATION_RUNTIME_DEPENDENCIES, config -> { - config.setDescription("The runtime dependencies to develop a mod for NeoForge, including Minecraft classes."); - config.setCanBeResolved(false); - config.setCanBeConsumed(false); - - config.getDependencies().addLater(minecraftClassesArtifact.map(dependencyFactory::create)); - config.getDependencies().addLater(createArtifacts.map(task -> project.files(task.getResourcesArtifact())).map(dependencyFactory::create)); - // Technically the Minecraft dependencies do not strictly need to be on the classpath because they are pulled from the legacy class path. - // However, we do it anyway because this matches production environments, and allows launch proxies such as DevLogin to use Minecraft's libraries. - config.getDependencies().addLater(neoForgeModDevLibrariesDependency); - }); - - configurations.create(CONFIGURATION_COMPILE_DEPENDENCIES, config -> { - config.setDescription("The compile-time dependencies to develop a mod for NeoForge, including Minecraft classes."); - config.setCanBeResolved(false); - config.setCanBeConsumed(false); - config.getDependencies().addLater(minecraftClassesArtifact.map(dependencyFactory::create)); - config.getDependencies().addLater(neoForgeModDevLibrariesDependency); - }); - - var sourceSets = ExtensionUtils.getSourceSets(project); - extension.addModdingDependenciesTo(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)); - - // Try to give people at least a fighting chance to run on the correct java version - project.afterEvaluate(ignored -> { - var toolchainSpec = javaExtension.getToolchain(); - try { - toolchainSpec.getLanguageVersion().convention(versionCapabilities.map(VersionCapabilities::javaVersion).map(JavaLanguageVersion::of)); - } catch (IllegalStateException e) { - // We tried our best - } - }); - - // Let's try to get the userdev JSON out of the universal jar - // I don't like having to use a configuration for this... - var userDevConfigOnly = project.getConfigurations().create("neoForgeConfigOnly", spec -> { - spec.setDescription("Resolves exclusively the NeoForge userdev JSON for configuring runs"); - spec.setCanBeResolved(true); - spec.setCanBeConsumed(false); - spec.setTransitive(false); - spec.getDependencies().addLater(extension.getNeoForgeArtifact().map(artifact -> { - return dependencyFactory.create(artifact) - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-moddev-config"); - }); - })); - }); - - 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); + var configurations = project.getConfigurations(); - spec.getDependencies().addLater(neoForgeModDevLibrariesDependency); - addClientResources(project, spec, createArtifacts); - }); + var versionCapabilities = neoForgeVersion != null ? VersionCapabilities.ofNeoForgeVersion(neoForgeVersion) + : VersionCapabilities.ofNeoFormVersion(neoFormVersion); - // This defines the module path for runs - // NOTE: When running in vanilla mode, this provider is undefined and will not result in an actual dependency - var modulePathDependency = extension.getNeoForgeArtifact().map(artifactId -> { - return dependencyFactory.create(artifactId) - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-moddev-module-path"); - }) - // TODO: this is ugly; maybe make the configuration transitive in neoforge, or fix the SJH dep. - .exclude(Map.of("group", "org.jetbrains", "module", "annotations")); - }); + var dependencies = neoForge != null ? ModdingDependencies.create(neoForge, neoForgeNotation, neoForm, neoFormNotation, versionCapabilities) + : ModdingDependencies.createVanillaOnly(neoForm, neoFormNotation); - setupRuns( + var artifacts = ModDevArtifactsWorkflow.create( project, + settings.getEnabledSourceSets(), Branding.MDG, - modDevBuildDir, - extension.getRuns(), - userDevConfigOnly, - modulePath -> modulePath.getDependencies().addLater(modulePathDependency), - legacyClassPath -> legacyClassPath.extendsFrom(additionalClasspath), - downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile), + extension, + dependencies, + artifactNamingStrategy, + configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), + configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), versionCapabilities ); - setupJarJar(project); - - configureTesting = () -> { - // Weirdly enough, testCompileOnly extends from compileOnlyApi, and not compileOnly - configurations.named(JavaPlugin.TEST_COMPILE_ONLY_CONFIGURATION_NAME).configure(configuration -> { - configuration.getDependencies().addLater(minecraftClassesArtifact.map(dependencyFactory::create)); - configuration.getDependencies().addLater(neoForgeModDevLibrariesDependency); - }); - - var testFixtures = configurations.create("neoForgeTestFixtures", config -> { - config.setDescription("Additional JUnit helpers provided by NeoForge"); - config.setCanBeResolved(false); - config.setCanBeConsumed(false); - config.getDependencies().addLater(extension.getNeoForgeArtifact().map(artifact -> { - return dependencyFactory.create(artifact) - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-moddev-test-fixtures"); - }); - })); - }); - - configurations.getByName(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME, files -> { - files.extendsFrom(configurations.getByName(CONFIGURATION_RUNTIME_DEPENDENCIES)); - files.extendsFrom(testFixtures); - }); - - setupTestTask( - project, - Branding.MDG, - userDevConfigOnly, - tasks.named("test", Test.class), - extension.getUnitTest().getLoadedMods(), - extension.getUnitTest().getTestedMod(), - modDevBuildDir, - modulePath -> modulePath.getDependencies().addLater(modulePathDependency), - spec -> { - spec.getDependencies().addLater(neoForgeModDevLibrariesDependency); - addClientResources(project, spec, createArtifacts); - }, - downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile) - ); - }; - - // For IDEs that support it, link the source/binary artifacts if we use separated ones - if (!ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { - ideIntegration.attachSources( - Map.of( - createArtifacts.get().getCompiledArtifact(), - createArtifacts.get().getSourcesArtifact() - ) - ); - } - } - - // FML searches for client resources on the legacy classpath - private static void addClientResources(Project project, Configuration spec, TaskProvider createArtifacts) { - // FML searches for client resources on the legacy classpath - spec.getDependencies().add( - project.getDependencyFactory().create( - project.files(createArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact)) - ) + ModDevRunWorkflow.create( + project, + Branding.MDG, + artifacts, + extension.getRuns() ); } - - private static Provider getNeoFormDataDependencyNotation(NeoForgeExtension extension) { - return extension.getNeoFormArtifact().map(art -> art + "@zip"); - } - - private static Provider getNeoForgeUserDevDependencyNotation(NeoForgeExtension extension) { - return extension.getNeoForgeArtifact().map(art -> art + ":userdev"); - } - - /** - * Collects all dependencies needed by the NeoFormRuntime - */ - private List configureArtifactManifestConfigurations(Project project, NeoForgeExtension extension) { - var configurations = project.getConfigurations(); - var dependencyFactory = project.getDependencyFactory(); - - var configurationPrefix = "neoFormRuntimeDependencies"; - - Provider neoForgeDependency = extension.getNeoForgeArtifact().map(dependencyFactory::create); - Provider neoFormDependency = extension.getNeoFormArtifact().map(dependencyFactory::create); - - // Gradle prevents us from having dependencies with "incompatible attributes" in the same configuration. - // What constitutes incompatible cannot be overridden on a per-configuration basis. - var neoForgeClassesAndData = configurations.create(configurationPrefix + "NeoForgeClasses", spec -> { - spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge classes)"); - spec.setCanBeConsumed(false); - spec.setCanBeResolved(true); - spec.getDependencies().addLater(neoForgeDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-moddev-bundle"); - }))); - - // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode - spec.getDependencies().addLater(neoFormDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoform"); - }))); - }); - - // This configuration is empty when running in Vanilla-mode. - var neoForgeSources = configurations.create(configurationPrefix + "NeoForgeSources", spec -> { - spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (NeoForge sources)"); - spec.setCanBeConsumed(false); - spec.setCanBeResolved(true); - spec.getDependencies().addLater(neoForgeDependency); - spec.attributes(attributes -> { - setNamedAttribute(project, attributes, Category.CATEGORY_ATTRIBUTE, Category.DOCUMENTATION); - setNamedAttribute(project, attributes, DocsType.DOCS_TYPE_ATTRIBUTE, DocsType.SOURCES); - }); - }); - - // Compile-time dependencies used by NeoForm, NeoForge and Minecraft. - // Also includes any classes referenced by compiled Minecraft code (used by decompilers, renamers, etc.) - var compileClasspath = configurations.create(configurationPrefix + "CompileClasspath", spec -> { - spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (Classpath)"); - spec.setCanBeConsumed(false); - spec.setCanBeResolved(true); - spec.getDependencies().addLater(neoForgeDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-dependencies"); - }))); - // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode - spec.getDependencies().addLater(neoFormDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoform-dependencies"); - }))); - spec.attributes(attributes -> { - setNamedAttribute(attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_API); - setNamedAttribute(attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT); - }); - }); - - // Runtime-time dependencies used by NeoForm, NeoForge and Minecraft. - var runtimeClasspath = configurations.create(configurationPrefix + "RuntimeClasspath", spec -> { - spec.setDescription("Dependencies needed for running NeoFormRuntime for the selected NeoForge/NeoForm version (Classpath)"); - spec.setCanBeConsumed(false); - spec.setCanBeResolved(true); - spec.getDependencies().addLater(neoForgeDependency); // Universal Jar - spec.getDependencies().addLater(neoForgeDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoforge-dependencies"); - }))); - // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode - spec.getDependencies().addLater(neoFormDependency.map(dependency -> dependency.copy() - .capabilities(caps -> { - caps.requireCapability("net.neoforged:neoform-dependencies"); - }))); - spec.attributes(attributes -> { - setNamedAttribute(attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); - setNamedAttribute(attributes, MinecraftDistribution.ATTRIBUTE, MinecraftDistribution.CLIENT); - }); - }); - - return List.of(neoForgeClassesAndData, neoForgeSources, compileClasspath, runtimeClasspath); - } - - static void setupRuns(Project project, - Branding branding, - Provider argFileDir, - DomainObjectCollection runs, - Object runTemplatesSourceFile, - Consumer configureModulePath, - Consumer configureLegacyClasspath, - Provider assetPropertiesFile, - Provider versionCapabilities - ) { - var ideIntegration = IdeIntegration.of(project, branding); - - // Create a configuration to resolve DevLaunch and DevLogin without leaking them to consumers - var supplyDevLogin = project.provider(() -> runs.stream().anyMatch(model -> model.getDevLogin().get())); - var devLaunchConfig = project.getConfigurations().create("devLaunchConfig", spec -> { - spec.setDescription("This configuration is used to inject DevLaunch and optionally DevLogin into the runtime classpaths of runs."); - spec.getDependencies().add(project.getDependencyFactory().create(RunUtils.DEV_LAUNCH_GAV)); - spec.getDependencies().addAllLater(supplyDevLogin.map( - supply -> supply ? List.of(project.getDependencyFactory().create(RunUtils.DEV_LOGIN_GAV)) : List.of())); - }); - - // Create an empty task similar to "assemble" which can be used to generate all launch scripts at once - var createLaunchScriptsTask = project.getTasks().register("createLaunchScripts", Task.class, task -> { - task.setGroup(branding.publicTaskGroup()); - task.setDescription("Creates batch files/shell scripts to launch the game from outside of Gradle (i.e. Renderdoc, NVidia Nsight, etc.)"); - }); - - Map> prepareRunTasks = new IdentityHashMap<>(); - runs.all(run -> { - var prepareRunTask = setupRunInGradle( - project, - branding, - argFileDir, - run, - runTemplatesSourceFile, - configureModulePath, - configureLegacyClasspath, - assetPropertiesFile, - devLaunchConfig, - versionCapabilities, - createLaunchScriptsTask - ); - prepareRunTasks.put(run, prepareRunTask); - }); - ideIntegration.configureRuns(prepareRunTasks, runs); - } - - /** - * @param runTemplatesFile See {@link ConfigurableFileCollection#from(Object...)}. This must ultimately resolve - * to a single file that is - * @param configureLegacyClasspath Callback to add entries to the legacy classpath. - * @param assetPropertiesFile File that contains the asset properties file produced by NFRT. - * @param createLaunchScriptsTask - */ - private static TaskProvider setupRunInGradle( - Project project, - Branding branding, - Provider argFileDir, - RunModel run, - Object runTemplatesFile, - Consumer configureModulePath, - Consumer configureLegacyClasspath, // TODO: can be removed in favor of directly passing a configuration for the moddev libraries - Provider assetPropertiesFile, - Configuration devLaunchConfig, - Provider versionCapabilities, - TaskProvider createLaunchScriptsTask) { - var ideIntegration = IdeIntegration.of(project, branding); - var configurations = project.getConfigurations(); - var javaExtension = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); - var tasks = project.getTasks(); - - var runtimeClasspathConfig = run.getSourceSet().map(SourceSet::getRuntimeClasspathConfigurationName) - .map(configurations::getByName); - - // Sucks, but what can you do... Only at the end do we actually know which source set this run will use - project.afterEvaluate(ignored -> { - runtimeClasspathConfig.get().extendsFrom(devLaunchConfig); - }); - - var type = RunUtils.getRequiredType(project, run); - - var modulePathConfiguration = project.getConfigurations().create(InternalModelHelper.nameOfRun(run, "", "modulesOnly"), spec -> { - spec.setDescription("Libraries that should be placed on the JVMs boot module path for run " + run.getName() + "."); - spec.setCanBeResolved(true); - spec.setCanBeConsumed(false); - spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); - 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); - }); - 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 prepareRunTask = tasks.register(InternalModelHelper.nameOfRun(run, "prepare", "run"), PrepareRun.class, task -> { - task.setGroup(branding.internalTaskGroup()); - task.setDescription("Prepares all files needed to launch the " + run.getName() + " Minecraft run."); - - task.getGameDirectory().set(run.getGameDirectory()); - task.getVmArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.VMARGS)); - task.getProgramArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.PROGRAMARGS)); - task.getLog4jConfigFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.LOG4J_CONFIG)); - task.getRunType().set(run.getType()); - task.getRunTypeTemplatesSource().from(runTemplatesFile); - task.getModules().from(modulePathConfiguration); - task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile()); - task.getAssetProperties().set(assetPropertiesFile); - task.getSystemProperties().set(run.getSystemProperties().map(props -> { - props = new HashMap<>(props); - return props; - })); - task.getMainClass().set(run.getMainClass()); - task.getProgramArguments().set(run.getProgramArguments()); - task.getJvmArguments().set(run.getJvmArguments()); - task.getGameLogLevel().set(run.getLogLevel()); - task.getDevLogin().set(run.getDevLogin()); - task.getVersionCapabilities().set(versionCapabilities); - }); - ideIntegration.runTaskOnProjectSync(prepareRunTask); - - var launchScriptTask = tasks.register(InternalModelHelper.nameOfRun(run, "create", "launchScript"), CreateLaunchScriptTask.class, task -> { - task.setGroup(branding.internalTaskGroup()); - task.setDescription("Creates a bash/shell-script to launch the " + run.getName() + " Minecraft run from outside Gradle or your IDE."); - - task.getWorkingDirectory().set(run.getGameDirectory().map(d -> d.getAsFile().getAbsolutePath())); - task.getRuntimeClasspath().setFrom(runtimeClasspathConfig); - task.getLaunchScript().set(RunUtils.getLaunchScript(argFileDir, run)); - task.getClasspathArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.CLASSPATH)); - task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); - task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath())); - task.getEnvironment().set(run.getEnvironment()); - task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); - }); - createLaunchScriptsTask.configure(task -> task.dependsOn(launchScriptTask)); - - tasks.register(InternalModelHelper.nameOfRun(run, "run", ""), RunGameTask.class, task -> { - task.setGroup(branding.publicTaskGroup()); - task.setDescription("Runs the " + run.getName() + " Minecraft run configuration."); - - // Launch with the Java version used in the project - var toolchainService = ExtensionUtils.findExtension(project, "javaToolchains", JavaToolchainService.class); - task.getJavaLauncher().set(toolchainService.launcherFor(spec -> spec.getLanguageVersion().set(javaExtension.getToolchain().getLanguageVersion()))); - // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. - // This records a dependency on compiling and processing the resources of the source set. - task.getClasspathProvider().from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); - task.getGameDirectory().set(run.getGameDirectory()); - - task.getEnvironmentProperty().set(run.getEnvironment()); - task.jvmArgs(RunUtils.getArgFileParameter(prepareRunTask.get().getVmArgsFile().get()).replace("\\", "\\\\")); - task.getMainClass().set(RunUtils.DEV_LAUNCH_MAIN_CLASS); - task.args(RunUtils.getArgFileParameter(prepareRunTask.get().getProgramArgsFile().get()).replace("\\", "\\\\")); - // Of course we need the arg files to be up-to-date ;) - task.dependsOn(prepareRunTask); - task.dependsOn(run.getTasksBefore()); - - task.getJvmArgumentProviders().add(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); - }); - - return prepareRunTask; - } - - public void setupTestTask() { - if (configureTesting == null) { - throw new IllegalStateException("Unit testing was already enabled once!"); - } - configureTesting.run(); - configureTesting = null; - } - - /** - * @see #setupRunInGradle for a description of the parameters - */ - static void setupTestTask(Project project, - Branding branding, - Object runTemplatesSourceFile, - TaskProvider testTask, - SetProperty loadedMods, - Property testedMod, - Provider argFileDir, - Consumer configureModulePath, - Consumer configureLegacyClasspath, - Provider assetPropertiesFile - ) { - var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR); - - var ideIntegration = IdeIntegration.of(project, branding); - - var tasks = project.getTasks(); - var configurations = project.getConfigurations(); - - var testRuntimeClasspath = configurations.getByName(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME); - - var neoForgeModDevModules = project.getConfigurations().create("neoForgeTestModules", spec -> { - spec.setDescription("Libraries that should be placed on the JVMs boot module path for unit tests."); - spec.setCanBeResolved(true); - spec.setCanBeConsumed(false); - spec.shouldResolveConsistentlyWith(testRuntimeClasspath); - 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); - }); - - var vmArgsFile = runArgsDir.map(dir -> dir.file("vmArgs.txt")); - var programArgsFile = runArgsDir.map(dir -> dir.file("programArgs.txt")); - var log4j2ConfigFile = runArgsDir.map(dir -> dir.file("log4j2.xml")); - var prepareTask = tasks.register("prepareNeoForgeTestFiles", PrepareTest.class, task -> { - task.setGroup(branding.internalTaskGroup()); - task.setDescription("Prepares all files needed to run the JUnit test task."); - task.getGameDirectory().set(gameDirectory); - task.getVmArgsFile().set(vmArgsFile); - task.getProgramArgsFile().set(programArgsFile); - task.getLog4jConfigFile().set(log4j2ConfigFile); - task.getRunTypeTemplatesSource().from(runTemplatesSourceFile); - task.getModules().from(neoForgeModDevModules); - task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile()); - task.getAssetProperties().set(assetPropertiesFile); - task.getGameLogLevel().set(Level.INFO); - }); - - // Ensure the test files are written on sync so that users who use IDE-only tests can run them - ideIntegration.runTaskOnProjectSync(prepareTask); - - testTask.configure(task -> { - task.dependsOn(prepareTask); - - // The FML JUnit plugin uses this system property to read a - // file containing the program arguments needed to launch - task.systemProperty("fml.junit.argsfile", programArgsFile.get().getAsFile().getAbsolutePath()); - task.jvmArgs(RunUtils.getArgFileParameter(vmArgsFile.get())); - - var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod); - task.getJvmArgumentProviders().add(modFoldersProvider); - }); - - project.afterEvaluate(p -> { - // Test tasks don't have a provider-based property for working directory, so we need to afterEvaluate it. - testTask.configure(task -> task.setWorkingDir(gameDirectory)); - }); - - ideIntegration.configureTesting(loadedMods, testedMod, runArgsDir, gameDirectory, programArgsFile, vmArgsFile); - } - - private static void setupJarJar(Project project) { - SourceSetContainer sourceSets = ExtensionUtils.getExtension(project, "sourceSets", SourceSetContainer.class); - sourceSets.all(sourceSet -> { - var jarJarTask = JarJar.registerWithConfiguration(project, sourceSet.getTaskName(null, "jarJar")); - jarJarTask.configure(task -> task.setGroup(Branding.MDG.internalTaskGroup())); - - // The target jar task for this source set might not exist, and #named(String) requires the task to exist - var jarTaskName = sourceSet.getJarTaskName(); - project.getTasks().withType(AbstractArchiveTask.class).named(name -> name.equals(jarTaskName)).configureEach(task -> { - task.from(jarJarTask); - }); - }); - } - - record DataFileCollectionWrapper(DataFileCollection extension, Configuration configuration) { - } - - private static DataFileCollectionWrapper dataFileConfiguration(Project project, String name, String description, String category) { - var configuration = project.getConfigurations().create(name, spec -> { - spec.setDescription(description); - spec.setCanBeConsumed(false); - spec.setCanBeResolved(true); - spec.attributes(attributes -> setNamedAttribute(project, attributes, Category.CATEGORY_ATTRIBUTE, category)); - }); - - var elementsConfiguration = project.getConfigurations().create(name + "Elements", spec -> { - spec.setDescription("Published data files for " + name); - spec.setCanBeConsumed(true); - spec.setCanBeResolved(false); - spec.attributes(attributes -> setNamedAttribute(project, attributes, Category.CATEGORY_ATTRIBUTE, category)); - }); - - // Set up the variant publishing conditionally - var java = (AdhocComponentWithVariants) project.getComponents().getByName("java"); - java.addVariantsFromConfiguration(elementsConfiguration, variant -> { - // This should be invoked lazily, so checking if the artifacts are empty is fine: - // "The details object used to determine what to do with a configuration variant **when publishing**." - if (variant.getConfigurationVariant().getArtifacts().isEmpty()) { - variant.skip(); - } - }); - - var depFactory = project.getDependencyFactory(); - Consumer publishCallback = new Consumer<>() { - ConfigurablePublishArtifact firstArtifact; - int artifactCount; - - @Override - public void accept(Object artifactNotation) { - elementsConfiguration.getDependencies().add(depFactory.create(project.files(artifactNotation))); - project.getArtifacts().add(elementsConfiguration.getName(), artifactNotation, artifact -> { - if (firstArtifact == null) { - firstArtifact = artifact; - artifact.setClassifier(category); - artifactCount = 1; - } else { - if (artifactCount == 1) { - firstArtifact.setClassifier(category + artifactCount); - } - artifact.setClassifier(category + (++artifactCount)); - } - }); - } - }; - - var extension = project.getObjects().newInstance(DataFileCollection.class, publishCallback); - configuration.getDependencies().add(depFactory.create(extension.getFiles())); - - return new DataFileCollectionWrapper(extension, configuration); - } - - private void setNamedAttribute(AttributeContainer attributes, Attribute attribute, String value) { - attributes.attribute(attribute, objectFactory.named(attribute.getType(), value)); - } - - 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/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java new file mode 100644 index 00000000..ad63efa2 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -0,0 +1,494 @@ +package net.neoforged.moddevgradle.internal; + +import net.neoforged.minecraftdependencies.MinecraftDistribution; +import net.neoforged.moddevgradle.dsl.InternalModelHelper; +import net.neoforged.moddevgradle.dsl.ModModel; +import net.neoforged.moddevgradle.dsl.RunModel; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.internal.utils.VersionCapabilities; +import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; +import net.neoforged.nfrtgradle.DownloadAssets; +import org.gradle.api.DomainObjectCollection; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Named; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.attributes.Attribute; +import org.gradle.api.attributes.AttributeContainer; +import org.gradle.api.attributes.Usage; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFile; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.plugins.jvm.JvmTestSuite; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.testing.Test; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.testing.base.TestingExtension; +import org.jetbrains.annotations.Nullable; +import org.slf4j.event.Level; + +import java.io.File; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * After modding has been enabled, this will be attached as an extension to the project. + */ +public class ModDevRunWorkflow { + private static final String EXTENSION_NAME = "__internal_modDevRunWorkflow"; + + /** + * This must be relative to the project directory since we can only set this to the same project-relative + * directory across all subprojects due to IntelliJ limitations. + */ + static final String JUNIT_GAME_DIR = "build/minecraft-junit"; + + private final Project project; + private final Branding branding; + @Nullable + private final ModuleDependency modulePathDependency; + @Nullable + private final ModuleDependency testFixturesDependency; + private final ModuleDependency gameLibrariesDependency; + private final Configuration additionalClasspath; + private final Configuration userDevConfigOnly; + + /** + * @param gameLibrariesDependency A module dependency that represents the library dependencies of the game. + * This module can be depended on with different usage attributes, which allow it + * to expose different sets of libraries for use in compiling code or at runtime + * (apiElements vs. runtimeElements). + */ + private ModDevRunWorkflow(Project project, + Branding branding, + ModDevArtifactsWorkflow artifactsWorkflow, + @Nullable ModuleDependency modulePathDependency, + @Nullable ModuleDependency runTypesConfigDependency, + @Nullable ModuleDependency testFixturesDependency, + ModuleDependency gameLibrariesDependency, + DomainObjectCollection runs, + VersionCapabilities versionCapabilities) { + this.project = project; + this.branding = branding; + this.modulePathDependency = modulePathDependency; + this.testFixturesDependency = testFixturesDependency; + this.gameLibrariesDependency = gameLibrariesDependency; + + var configurations = project.getConfigurations(); + + // Let's try to get the userdev JSON out of the universal jar + // I don't like having to use a configuration for this... + userDevConfigOnly = configurations.create("neoForgeConfigOnly", spec -> { + spec.setDescription("Resolves exclusively the NeoForge userdev JSON for configuring runs"); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.setTransitive(false); + if (runTypesConfigDependency != null) { + spec.getDependencies().add(runTypesConfigDependency); + } + }); + + 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()); + }); + + setupRuns( + project, + branding, + artifactsWorkflow.modDevBuildDir(), + runs, + userDevConfigOnly, + modulePath -> { + if (modulePathDependency != null) { + modulePath.getDependencies().add(modulePathDependency); + } + }, + legacyClassPath -> legacyClassPath.extendsFrom(additionalClasspath), + artifactsWorkflow.downloadAssets().flatMap(DownloadAssets::getAssetPropertiesFile), + versionCapabilities + ); + } + + public static ModDevRunWorkflow get(Project project) { + var workflow = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevRunWorkflow.class); + if (workflow == null) { + throw new InvalidUserCodeException("Please enable the modding plugin first by setting a version"); + } + return workflow; + } + + public static ModDevRunWorkflow create(Project project, + Branding branding, + ModDevArtifactsWorkflow artifactsWorkflow, + DomainObjectCollection runs) { + + var dependencies = artifactsWorkflow.dependencies(); + var versionCapabilites = artifactsWorkflow.versionCapabilities(); + + var workflow = new ModDevRunWorkflow( + project, + branding, + artifactsWorkflow, + dependencies.modulePathDependency(), + dependencies.runTypesConfigDependency(), + dependencies.testFixturesDependency(), + dependencies.gameLibrariesDependency(), + runs, + versionCapabilites + ); + + project.getExtensions().add(EXTENSION_NAME, workflow); + + return workflow; + } + + public void configureTesting(Provider testedMod, Provider> loadedMods) { + var testing = project.getExtensions().getByType(TestingExtension.class); + var testSuite = (JvmTestSuite) testing.getSuites().getByName("test"); + var testSourceSet = testSuite.getSources(); + + var artifactsWorkflow = ModDevArtifactsWorkflow.get(project); + artifactsWorkflow.addToSourceSet(testSourceSet); + + var configurations = project.getConfigurations(); + + // If test fixtures are available for the current workflow, add them to runtime only + if (testFixturesDependency != null) { + configurations.getByName(testSourceSet.getRuntimeOnlyConfigurationName(), configuration -> { + configuration.getDependencies().add(testFixturesDependency); + }); + } + + if (testSuite.getTargets().size() > 1) { + // NOTE: We can implement support for multiple test tasks later if someone is adamant about it + throw new InvalidUserCodeException("MDG currently only supports test suites with a single test task."); + } + + for (var target : testSuite.getTargets()) { + setupTestTask( + project, + branding, + userDevConfigOnly, + target.getTestTask(), + loadedMods, + testedMod, + artifactsWorkflow.modDevBuildDir(), + modulePath -> { + if (modulePathDependency != null) { + modulePath.getDependencies().add(modulePathDependency); + } + }, + legacyClassPath -> { + legacyClassPath.getDependencies().add(gameLibrariesDependency); + addClientResources(project, legacyClassPath, artifactsWorkflow.createArtifacts()); + }, + artifactsWorkflow.downloadAssets().flatMap(DownloadAssets::getAssetPropertiesFile) + ); + } + } + + // FML searches for client resources on the legacy classpath + private static void addClientResources(Project project, Configuration spec, TaskProvider createArtifacts) { + spec.getDependencies().add( + project.getDependencyFactory().create( + project.files(createArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact)) + ) + ); + } + + public static void setupRuns( + Project project, + Branding branding, + Provider argFileDir, + DomainObjectCollection runs, + Object runTemplatesSourceFile, + Consumer configureModulePath, + Consumer configureLegacyClasspath, + Provider assetPropertiesFile, + VersionCapabilities versionCapabilities + ) { + var dependencyFactory = project.getDependencyFactory(); + var ideIntegration = IdeIntegration.of(project, branding); + + // Create a configuration to resolve DevLaunch and DevLogin without leaking them to consumers + var supplyDevLogin = project.provider(() -> runs.stream().anyMatch(model -> model.getDevLogin().get())); + var devLaunchConfig = project.getConfigurations().create("devLaunchConfig", spec -> { + spec.setDescription("This configuration is used to inject DevLaunch and optionally DevLogin into the runtime classpaths of runs."); + spec.getDependencies().add(dependencyFactory.create(RunUtils.DEV_LAUNCH_GAV)); + spec.getDependencies().addAllLater(supplyDevLogin.map( + supply -> supply ? List.of(dependencyFactory.create(RunUtils.DEV_LOGIN_GAV)) : List.of())); + }); + + // Create an empty task similar to "assemble" which can be used to generate all launch scripts at once + var createLaunchScriptsTask = project.getTasks().register("createLaunchScripts", Task.class, task -> { + task.setGroup(branding.publicTaskGroup()); + task.setDescription("Creates batch files/shell scripts to launch the game from outside of Gradle (i.e. Renderdoc, NVidia Nsight, etc.)"); + }); + + Map> prepareRunTasks = new IdentityHashMap<>(); + runs.all(run -> { + var prepareRunTask = setupRunInGradle( + project, + branding, + argFileDir, + run, + runTemplatesSourceFile, + configureModulePath, + configureLegacyClasspath, + assetPropertiesFile, + devLaunchConfig, + versionCapabilities, + createLaunchScriptsTask + ); + prepareRunTasks.put(run, prepareRunTask); + }); + ideIntegration.configureRuns(prepareRunTasks, runs); + } + + /** + * @param runTemplatesFile See {@link ConfigurableFileCollection#from(Object...)}. This must ultimately resolve + * to a single file that is + * @param configureLegacyClasspath Callback to add entries to the legacy classpath. + * @param assetPropertiesFile File that contains the asset properties file produced by NFRT. + */ + private static TaskProvider setupRunInGradle( + Project project, + Branding branding, + Provider argFileDir, + RunModel run, + Object runTemplatesFile, + Consumer configureModulePath, + Consumer configureLegacyClasspath, // TODO: can be removed in favor of directly passing a configuration for the moddev libraries + Provider assetPropertiesFile, + Configuration devLaunchConfig, + VersionCapabilities versionCapabilities, + TaskProvider createLaunchScriptsTask) { + var ideIntegration = IdeIntegration.of(project, branding); + var configurations = project.getConfigurations(); + var javaExtension = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); + var tasks = project.getTasks(); + + var runtimeClasspathConfig = run.getSourceSet().map(SourceSet::getRuntimeClasspathConfigurationName) + .map(configurations::getByName); + + // Sucks, but what can you do... Only at the end do we actually know which source set this run will use + project.afterEvaluate(ignored -> { + runtimeClasspathConfig.get().extendsFrom(devLaunchConfig); + }); + + var type = RunUtils.getRequiredType(project, run); + + var modulePathConfiguration = project.getConfigurations().create(InternalModelHelper.nameOfRun(run, "", "modulesOnly"), spec -> { + spec.setDescription("Libraries that should be placed on the JVMs boot module path for run " + run.getName() + "."); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); + 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); + }); + 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 prepareRunTask = tasks.register(InternalModelHelper.nameOfRun(run, "prepare", "run"), PrepareRun.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Prepares all files needed to launch the " + run.getName() + " Minecraft run."); + + task.getGameDirectory().set(run.getGameDirectory()); + task.getVmArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.VMARGS)); + task.getProgramArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.PROGRAMARGS)); + task.getLog4jConfigFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.LOG4J_CONFIG)); + task.getRunType().set(run.getType()); + task.getRunTypeTemplatesSource().from(runTemplatesFile); + task.getModules().from(modulePathConfiguration); + task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile()); + task.getAssetProperties().set(assetPropertiesFile); + task.getSystemProperties().set(run.getSystemProperties().map(props -> { + props = new HashMap<>(props); + return props; + })); + task.getMainClass().set(run.getMainClass()); + task.getProgramArguments().set(run.getProgramArguments()); + task.getJvmArguments().set(run.getJvmArguments()); + task.getGameLogLevel().set(run.getLogLevel()); + task.getDevLogin().set(run.getDevLogin()); + task.getVersionCapabilities().set(versionCapabilities); + }); + ideIntegration.runTaskOnProjectSync(prepareRunTask); + + var launchScriptTask = tasks.register(InternalModelHelper.nameOfRun(run, "create", "launchScript"), CreateLaunchScriptTask.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Creates a bash/shell-script to launch the " + run.getName() + " Minecraft run from outside Gradle or your IDE."); + + task.getWorkingDirectory().set(run.getGameDirectory().map(d -> d.getAsFile().getAbsolutePath())); + task.getRuntimeClasspath().setFrom(runtimeClasspathConfig); + task.getLaunchScript().set(RunUtils.getLaunchScript(argFileDir, run)); + task.getClasspathArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.CLASSPATH)); + task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); + task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath())); + task.getEnvironment().set(run.getEnvironment()); + task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); + }); + createLaunchScriptsTask.configure(task -> task.dependsOn(launchScriptTask)); + + tasks.register(InternalModelHelper.nameOfRun(run, "run", ""), RunGameTask.class, task -> { + task.setGroup(branding.publicTaskGroup()); + task.setDescription("Runs the " + run.getName() + " Minecraft run configuration."); + + // Launch with the Java version used in the project + var toolchainService = ExtensionUtils.findExtension(project, "javaToolchains", JavaToolchainService.class); + task.getJavaLauncher().set(toolchainService.launcherFor(spec -> spec.getLanguageVersion().set(javaExtension.getToolchain().getLanguageVersion()))); + // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. + // This records a dependency on compiling and processing the resources of the source set. + task.getClasspathProvider().from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); + task.getGameDirectory().set(run.getGameDirectory()); + + task.getEnvironmentProperty().set(run.getEnvironment()); + task.jvmArgs(RunUtils.getArgFileParameter(prepareRunTask.get().getVmArgsFile().get()).replace("\\", "\\\\")); + task.getMainClass().set(RunUtils.DEV_LAUNCH_MAIN_CLASS); + task.args(RunUtils.getArgFileParameter(prepareRunTask.get().getProgramArgsFile().get()).replace("\\", "\\\\")); + // Of course we need the arg files to be up-to-date ;) + task.dependsOn(prepareRunTask); + task.dependsOn(run.getTasksBefore()); + + task.getJvmArgumentProviders().add(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); + }); + + return prepareRunTask; + } + + /** + * @see #setupRunInGradle for a description of the parameters + */ + static void setupTestTask(Project project, + Branding branding, + Object runTemplatesSourceFile, + TaskProvider testTask, + Provider> loadedMods, + Provider testedMod, + Provider argFileDir, + Consumer configureModulePath, + Consumer configureLegacyClasspath, + Provider assetPropertiesFile + ) { + var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR); + + var ideIntegration = IdeIntegration.of(project, branding); + + var tasks = project.getTasks(); + var configurations = project.getConfigurations(); + + var testRuntimeClasspath = configurations.getByName(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + + var neoForgeModDevModules = project.getConfigurations().create("neoForgeTestModules", spec -> { + spec.setDescription("Libraries that should be placed on the JVMs boot module path for unit tests."); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.shouldResolveConsistentlyWith(testRuntimeClasspath); + 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); + }); + + var vmArgsFile = runArgsDir.map(dir -> dir.file("vmArgs.txt")); + var programArgsFile = runArgsDir.map(dir -> dir.file("programArgs.txt")); + var log4j2ConfigFile = runArgsDir.map(dir -> dir.file("log4j2.xml")); + var prepareTask = tasks.register("prepareNeoForgeTestFiles", PrepareTest.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Prepares all files needed to run the JUnit test task."); + task.getGameDirectory().set(gameDirectory); + task.getVmArgsFile().set(vmArgsFile); + task.getProgramArgsFile().set(programArgsFile); + task.getLog4jConfigFile().set(log4j2ConfigFile); + task.getRunTypeTemplatesSource().from(runTemplatesSourceFile); + task.getModules().from(neoForgeModDevModules); + task.getLegacyClasspathFile().set(writeLcpTask.get().getLegacyClasspathFile()); + task.getAssetProperties().set(assetPropertiesFile); + task.getGameLogLevel().set(Level.INFO); + }); + + // Ensure the test files are written on sync so that users who use IDE-only tests can run them + ideIntegration.runTaskOnProjectSync(prepareTask); + + testTask.configure(task -> { + task.dependsOn(prepareTask); + + // The FML JUnit plugin uses this system property to read a + // file containing the program arguments needed to launch + task.systemProperty("fml.junit.argsfile", programArgsFile.get().getAsFile().getAbsolutePath()); + task.jvmArgs(RunUtils.getArgFileParameter(vmArgsFile.get())); + + var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod); + task.getJvmArgumentProviders().add(modFoldersProvider); + }); + + project.afterEvaluate(p -> { + // Test tasks don't have a provider-based property for working directory, so we need to afterEvaluate it. + testTask.configure(task -> task.setWorkingDir(gameDirectory)); + }); + + 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/ModdingDependencies.java b/src/main/java/net/neoforged/moddevgradle/internal/ModdingDependencies.java new file mode 100644 index 00000000..424175d8 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModdingDependencies.java @@ -0,0 +1,67 @@ +package net.neoforged.moddevgradle.internal; + +import net.neoforged.moddevgradle.internal.utils.VersionCapabilities; +import org.gradle.api.artifacts.ModuleDependency; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public record ModdingDependencies( + @Nullable ModuleDependency neoForgeDependency, + @Nullable String neoForgeDependencyNotation, + @Nullable ModuleDependency neoFormDependency, + @Nullable String neoFormDependencyNotation, + ModuleDependency gameLibrariesDependency, + @Nullable ModuleDependency modulePathDependency, + @Nullable ModuleDependency runTypesConfigDependency, + @Nullable ModuleDependency testFixturesDependency +) { + + public static ModdingDependencies create(ModuleDependency neoForge, + String neoForgeNotation, + @Nullable ModuleDependency neoForm, + @Nullable String neoFormNotation, + VersionCapabilities versionCapabilities) { + var runTypesDataDependency = neoForge.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-moddev-config")); + var modulePathDependency = neoForge.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-moddev-module-path")) + // TODO: this is ugly; maybe make the configuration transitive in neoforge, or fix the SJH dep. + .exclude(Map.of("group", "org.jetbrains", "module", "annotations")); + var librariesDependency = neoForge.copy() + .capabilities(c -> c.requireCapability("net.neoforged:neoforge-dependencies")); + + ModuleDependency testFixturesDependency = null; + if (versionCapabilities.testFixtures()) { + testFixturesDependency = neoForge.copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-moddev-test-fixtures")); + } + + return new ModdingDependencies( + neoForge, + neoForgeNotation, + neoForm, + neoFormNotation, + librariesDependency, + modulePathDependency, + runTypesDataDependency, + testFixturesDependency + ); + } + + public static ModdingDependencies createVanillaOnly(ModuleDependency neoForm, String neoFormNotation) { + var librariesDependency = neoForm.copy() + .capabilities(c -> c.requireCapability("net.neoforged:neoform-dependencies")); + + return new ModdingDependencies( + null, + null, + neoForm, + neoFormNotation, + librariesDependency, + null, + null, + null + ); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java index 68c61f70..6758c606 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java @@ -8,12 +8,11 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; -import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.testing.Test; +import java.util.Set; import java.util.function.Consumer; /** @@ -35,7 +34,7 @@ public static void setupRuns(Project project, Consumer configureAdditionalClasspath, Provider assetPropertiesFile ) { - ModDevPlugin.setupRuns( + ModDevRunWorkflow.setupRuns( project, Branding.NEODEV, argFileDir, @@ -45,7 +44,7 @@ public static void setupRuns(Project project, configureAdditionalClasspath, assetPropertiesFile, // This overload of the method was only used by NeoForge 1.21.3 - project.provider(() -> VersionCapabilities.ofMinecraftVersion("1.21.3")) + VersionCapabilities.ofMinecraftVersion("1.21.3") ); } @@ -58,7 +57,7 @@ public static void setupRuns(Project project, Provider assetPropertiesFile, Provider neoFormVersion ) { - ModDevPlugin.setupRuns( + ModDevRunWorkflow.setupRuns( project, Branding.NEODEV, argFileDir, @@ -67,7 +66,7 @@ public static void setupRuns(Project project, configureModulePath, configureAdditionalClasspath, assetPropertiesFile, - neoFormVersion.map(VersionCapabilities::ofNeoFormVersion) + neoFormVersion.map(VersionCapabilities::ofNeoFormVersion).getOrElse(VersionCapabilities.latest()) ); } @@ -75,13 +74,13 @@ public static void setupTestTask(Project project, Provider argFileDir, TaskProvider testTask, Object runTemplatesSourceFile, - SetProperty loadedMods, - Property testedMod, + Provider> loadedMods, + Provider testedMod, Consumer configureModulePath, Consumer configureAdditionalClasspath, Provider assetPropertiesFile ) { - ModDevPlugin.setupTestTask( + ModDevRunWorkflow.setupTestTask( project, Branding.NEODEV, runTemplatesSourceFile, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index dc6cc7de..a5825ad5 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -97,7 +97,8 @@ abstract class PrepareRunOrTest extends DefaultTask { /** * Only used when {@link #getRunTypeTemplatesSource()} is empty, - * to know whether the associated Minecraft version requires one or two data runs. + * to know whether the associated Minecraft version has separate entrypoints for generating resource- and + * data packs. * Defaults to latest. */ @Input diff --git a/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java b/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java new file mode 100644 index 00000000..c1a7f795 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java @@ -0,0 +1,16 @@ +package net.neoforged.moddevgradle.internal; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public enum WorkflowArtifact { + COMPILED(""), + COMPILED_WITH_SOURCES("-merged"), + SOURCES("-sources"), + CLIENT_RESOURCES("-client-extra-aka-minecraft-resources"); + public final String defaultSuffix; + + WorkflowArtifact(String defaultSuffix) { + this.defaultSuffix = defaultSuffix; + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/jarjar/JarJarPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/jarjar/JarJarPlugin.java new file mode 100644 index 00000000..885cefd7 --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/internal/jarjar/JarJarPlugin.java @@ -0,0 +1,29 @@ +package net.neoforged.moddevgradle.internal.jarjar; + +import net.neoforged.moddevgradle.internal.Branding; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.tasks.JarJar; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.api.tasks.compile.JavaCompile; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class JarJarPlugin implements Plugin { + @Override + public void apply(Project project) { + SourceSetContainer sourceSets = ExtensionUtils.getExtension(project, "sourceSets", SourceSetContainer.class); + sourceSets.all(sourceSet -> { + var jarJarTask = JarJar.registerWithConfiguration(project, sourceSet.getTaskName(null, "jarJar")); + jarJarTask.configure(task -> task.setGroup(Branding.MDG.internalTaskGroup())); + + // The target jar task for this source set might not exist, and #named(String) requires the task to exist + var jarTaskName = sourceSet.getJarTaskName(); + project.getTasks().withType(AbstractArchiveTask.class).named(name -> name.equals(jarTaskName)).configureEach(task -> { + task.from(jarJarTask); + }); + }); + } +} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/jarjar/ResolvedJarJarArtifact.java b/src/main/java/net/neoforged/moddevgradle/internal/jarjar/ResolvedJarJarArtifact.java index 7a774e92..3539a4ea 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/jarjar/ResolvedJarJarArtifact.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/jarjar/ResolvedJarJarArtifact.java @@ -50,7 +50,7 @@ public ContainedVersion createContainedVersion() { } public ContainedJarMetadata createContainerMetadata() { - return new ContainedJarMetadata(createContainedJarIdentifier(), createContainedVersion(), "META-INF/jarjar/"+embeddedFilename, isObfuscated(file)); + return new ContainedJarMetadata(createContainedJarIdentifier(), createContainedVersion(), "META-INF/jarjar/" + embeddedFilename, isObfuscated(file)); } @InputFile @@ -85,7 +85,7 @@ public String getArtifact() { } private static boolean isObfuscated(final File dependency) { - try(final JarFile jarFile = new JarFile(dependency)) { + try (final JarFile jarFile = new JarFile(dependency)) { final Manifest manifest = jarFile.getManifest(); return manifest.getMainAttributes().containsKey("Obfuscated-By"); } catch (IOException e) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilities.java b/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilities.java index f249e136..197900de 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilities.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/utils/VersionCapabilities.java @@ -11,16 +11,18 @@ * Models the changing capabilities of the modding platform and Vanilla, which we tie to the Minecraft version. * @param javaVersion Which Java version Vanilla uses to compile and run. * @param splitDataRuns Whether Vanilla has separate main classes for generating client and server data. + * @param testFixtures If the NeoForge version for this Minecraft version supports test fixtures. */ -public record VersionCapabilities(int javaVersion, boolean splitDataRuns) implements Serializable { +public record VersionCapabilities(int javaVersion, boolean splitDataRuns, boolean testFixtures) implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(VersionCapabilities.class); - private static final VersionCapabilities LATEST = new VersionCapabilities(21, true); + private static final VersionCapabilities LATEST = new VersionCapabilities(21, true, true); private static final Pattern NEOFORGE_PATTERN = Pattern.compile("^(\\d+\\.\\d+)\\.\\d+(|-.*)$"); private static final int MC_24W45A_INDEX = getReferenceVersionIndex("24w45a"); private static final int MC_24W14A_INDEX = getReferenceVersionIndex("24w14a"); + private static final int MC_1_20_4_INDEX = getReferenceVersionIndex("1.20.4"); private static final int MC_1_18_PRE2_INDEX = getReferenceVersionIndex("1.18-pre2"); private static final int MC_21W19A_INDEX = getReferenceVersionIndex("21w19a"); @@ -41,8 +43,9 @@ public static VersionCapabilities ofMinecraftVersion(String minecraftVersion) { public static VersionCapabilities ofVersionIndex(int versionIndex) { var javaVersion = getJavaVersion(versionIndex); var splitData = hasSplitDataEntrypoints(versionIndex); + var testFixtures = hasTestFixtures(versionIndex); - return new VersionCapabilities(javaVersion, splitData); + return new VersionCapabilities(javaVersion, splitData, testFixtures); } static int getJavaVersion(int versionIndex) { @@ -61,6 +64,10 @@ static boolean hasSplitDataEntrypoints(int versionIndex) { return versionIndex <= MC_24W45A_INDEX; } + static boolean hasTestFixtures(int versionIndex) { + return versionIndex <= MC_1_20_4_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); diff --git a/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java b/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java index 4218ae6f..f7a855ae 100644 --- a/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java +++ b/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java @@ -94,7 +94,7 @@ public static TaskProvider registerWithConfiguration(Project project, St // jvm version. We could copy DefaultJvmFeature, and search for the target version of the compile task, // but this is difficult - we only have a feature name, not the linked source set. For this reason, we use // the toolchain version, which is the most likely to be correct. - attributes.attributeProvider(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, javaPlugin.getToolchain().getLanguageVersion().map(JavaLanguageVersion::asInt)); + attributes.attributeProvider(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, javaPlugin.getToolchain().getLanguageVersion().orElse(JavaLanguageVersion.current()).map(JavaLanguageVersion::asInt)); attributes.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME)); attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR)); attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY)); diff --git a/src/test/java/net/neoforged/moddevgradle/AbstractProjectBuilderTest.java b/src/test/java/net/neoforged/moddevgradle/AbstractProjectBuilderTest.java new file mode 100644 index 00000000..327585f7 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/AbstractProjectBuilderTest.java @@ -0,0 +1,57 @@ +package net.neoforged.moddevgradle; + +import org.assertj.core.api.AbstractListAssert; +import org.assertj.core.api.ObjectAssert; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ExternalModuleDependency; +import org.gradle.api.artifacts.FileCollectionDependency; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class AbstractProjectBuilderTest { + protected Project project; + + protected final AbstractListAssert, String, ObjectAssert> assertThatDependencies(String configurationName) { + var configuration = project.getConfigurations().getByName(configurationName); + return assertThat(configuration.getAllDependencies()) + .extracting(this::describeDependency); + } + + protected final String describeDependency(Dependency dependency) { + if (dependency instanceof FileCollectionDependency fileCollectionDependency) { + return fileCollectionDependency.getFiles().getFiles() + .stream() + .map(f -> project.getProjectDir().toPath().relativize(f.toPath()).toString().replace('\\', '/')) + .collect(Collectors.joining(";")); + } else if (dependency instanceof ExternalModuleDependency moduleDependency) { + return moduleDependency.getGroup() + + ":" + moduleDependency.getName() + + ":" + moduleDependency.getVersion() + + formatCapabilities(moduleDependency); + } else { + return dependency.toString(); + } + } + + protected final String formatCapabilities(ExternalModuleDependency moduleDependency) { + var capabilities = moduleDependency.getRequestedCapabilities(); + if (capabilities.isEmpty()) { + return ""; + } + + var mainVersion = moduleDependency.getVersion(); + return "[" + + capabilities.stream().map(cap -> { + if (Objects.equals(mainVersion, cap.getVersion()) || cap.getVersion() == null) { + return cap.getGroup() + ":" + cap.getName(); + } else { + return cap.getGroup() + ":" + cap.getName() + ":" + cap.getVersion(); + } + }).collect(Collectors.joining(",")) + "]"; + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java index 1427611e..792459d8 100644 --- a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java +++ b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java @@ -48,7 +48,11 @@ void writeGroovyBuildScript(@Language("gradle") String text, Object... args) thr writeFile(buildFile, interpolateTemplate(text, args)); } - private static String interpolateTemplate(@Language("gradle") String text, Object[] args) { + void writeKotlinBuildScript(@Language("kotlin") String text, Object... args) throws IOException { + writeFile(buildFile, interpolateTemplate(text, args)); + } + + private static String interpolateTemplate(String text, Object[] args) { var m = Pattern.compile("\\{(\\d+|[A-Z_]+)}"); var body = m.matcher(text).replaceAll(matchResult -> { Object arg; diff --git a/src/test/java/net/neoforged/moddevgradle/functional/GroovyScriptTest.java b/src/test/java/net/neoforged/moddevgradle/functional/GroovyScriptTest.java index a0702b91..150d803d 100644 --- a/src/test/java/net/neoforged/moddevgradle/functional/GroovyScriptTest.java +++ b/src/test/java/net/neoforged/moddevgradle/functional/GroovyScriptTest.java @@ -22,6 +22,28 @@ public void testApplyInEmptyProject() throws IOException { """; writeFile(buildFile, buildFileContent); + BuildResult result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir) + .withArguments("tasks", "--all") + .build(); + + assertThat(result.getOutput()).doesNotContain("createMinecraftArtifacts"); + assertEquals(TaskOutcome.SUCCESS, result.task(":tasks").getOutcome()); + } + + @Test + public void testApplyInEmptyProjectAndEnable() throws IOException { + writeFile(settingsFile, "rootProject.name = 'hello-world'"); + writeGroovyBuildScript(""" + plugins { + id "net.neoforged.moddev" + } + neoForge { + version = "{DEFAULT_NEOFORGE_VERSION}" + } + """); + BuildResult result = GradleRunner.create() .withPluginClasspath() .withProjectDir(testProjectDir) diff --git a/src/test/java/net/neoforged/moddevgradle/functional/KotlinScriptTest.java b/src/test/java/net/neoforged/moddevgradle/functional/KotlinScriptTest.java index 1b2cafa1..0e311686 100644 --- a/src/test/java/net/neoforged/moddevgradle/functional/KotlinScriptTest.java +++ b/src/test/java/net/neoforged/moddevgradle/functional/KotlinScriptTest.java @@ -23,6 +23,30 @@ public void testApplyInEmptyProject() throws IOException { """; writeFile(buildFile, buildFileContent); + BuildResult result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir) + .withArguments("tasks", "--all") + .build(); + + assertThat(result.getOutput()).doesNotContain("createMinecraftArtifacts"); + assertEquals(TaskOutcome.SUCCESS, result.task(":tasks").getOutcome()); + } + + @Test + public void testApplyInEmptyProjectAndEnable() throws IOException { + writeFile(settingsFile, """ + rootProject.name = "hello-world"; + """); + writeKotlinBuildScript(""" + plugins { + id("net.neoforged.moddev") + } + neoForge { + version = "{DEFAULT_NEOFORGE_VERSION}" + } + """); + BuildResult result = GradleRunner.create() .withPluginClasspath() .withProjectDir(testProjectDir) diff --git a/src/test/java/net/neoforged/moddevgradle/internal/AccessTransformerConventionTest.java b/src/test/java/net/neoforged/moddevgradle/internal/AccessTransformerConventionTest.java index 3a0c3e7c..41117846 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/AccessTransformerConventionTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/AccessTransformerConventionTest.java @@ -24,6 +24,7 @@ void setup() { project.getPlugins().apply(ModDevPlugin.class); extension = ExtensionUtils.getExtension(project, "neoForge", NeoForgeExtension.class); + extension.setVersion("1.2.3"); } @Test diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java new file mode 100644 index 00000000..ad58c391 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java @@ -0,0 +1,120 @@ +package net.neoforged.moddevgradle.internal; + +import net.neoforged.moddevgradle.AbstractProjectBuilderTest; +import net.neoforged.moddevgradle.dsl.NeoForgeExtension; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ModDevPluginTest extends AbstractProjectBuilderTest { + private final NeoForgeExtension extension; + private final SourceSet mainSourceSet; + private final SourceSet testSourceSet; + + public ModDevPluginTest() { + project = ProjectBuilder.builder().build(); + project.getPlugins().apply(ModDevPlugin.class); + + extension = ExtensionUtils.getExtension(project, "neoForge", NeoForgeExtension.class); + + var sourceSets = ExtensionUtils.getSourceSets(project); + mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + testSourceSet = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME); + + // Set the Java version to the currently running Java to make it use that + var java = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); + java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); + } + + @Test + void testModdingCannotBeEnabledTwice() { + extension.setVersion("1.2.3"); + var e = assertThrows(InvalidUserCodeException.class, () -> extension.setVersion("1.2.3")); + assertThat(e).hasMessage("You cannot enable modding in the same project twice."); + } + + @Test + void testEnableVanillaOnlyMode() { + extension.setNeoFormVersion("1.2.3"); + + assertThatDependencies(mainSourceSet.getCompileClasspathConfigurationName()) + .containsOnly( + "build/moddev/artifacts/vanilla-1.2.3.jar", + "net.neoforged:neoform:1.2.3[net.neoforged:neoform-dependencies]" + ); + assertThatDependencies(mainSourceSet.getRuntimeClasspathConfigurationName()) + .containsOnly( + "build/moddev/artifacts/vanilla-1.2.3.jar", + "build/moddev/artifacts/vanilla-1.2.3-client-extra-aka-minecraft-resources.jar", + "net.neoforged:neoform:1.2.3[net.neoforged:neoform-dependencies]" + ); + } + + @Test + void testEnableForTestSourceSetOnly() { + extension.enable(settings -> { + settings.setVersion("1.2.3"); + settings.setEnabledSourceSets(Set.of(testSourceSet)); + }); + + // Both the compile and runtime classpath of the main source set had no dependencies added + assertThatDependencies(mainSourceSet.getCompileClasspathConfigurationName()).isEmpty(); + assertThatDependencies(mainSourceSet.getRuntimeClasspathConfigurationName()).isEmpty(); + + // While the test classpath should have modding dependencies + assertContainsModdingCompileDependencies("1.2.3", testSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", testSourceSet.getRuntimeClasspathConfigurationName()); + } + + @Test + void testAddModdingDependenciesTo() { + extension.setVersion("1.2.3"); + + // Initially, only the main source set should have the dependencies + assertContainsModdingCompileDependencies("1.2.3", mainSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", mainSourceSet.getRuntimeClasspathConfigurationName()); + assertThatDependencies(testSourceSet.getCompileClasspathConfigurationName()).isEmpty(); + assertThatDependencies(testSourceSet.getRuntimeClasspathConfigurationName()).isEmpty(); + + // Now add it to the test source set too + extension.addModdingDependenciesTo(testSourceSet); + + assertContainsModdingCompileDependencies("1.2.3", testSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", testSourceSet.getRuntimeClasspathConfigurationName()); + } + + private void assertContainsModdingCompileDependencies(String version, String configurationName) { + assertThatDependencies(configurationName) + .containsOnly( + "build/moddev/artifacts/neoforge-" + version + ".jar", + "net.neoforged:neoforge:" + version + "[net.neoforged:neoforge-dependencies]" + ); + } + + private void assertContainsModdingRuntimeDependencies(String version, String configurationName) { + var configuration = project.getConfigurations().getByName(configurationName); + + var dependentTasks = configuration.getBuildDependencies().getDependencies(null); + assertThat(dependentTasks) + .extracting(Task::getName) + .containsOnly("createMinecraftArtifacts"); + + assertThatDependencies(configurationName) + .containsOnly( + "build/moddev/artifacts/neoforge-" + version + ".jar", + "build/moddev/artifacts/neoforge-" + version + "-client-extra-aka-minecraft-resources.jar", + "net.neoforged:neoforge:" + version + "[net.neoforged:neoforge-dependencies]" + ); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java new file mode 100644 index 00000000..17e13eb8 --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/legacyforge/LegacyModDevPluginTest.java @@ -0,0 +1,122 @@ +package net.neoforged.moddevgradle.legacyforge; + +import net.neoforged.moddevgradle.AbstractProjectBuilderTest; +import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; +import net.neoforged.moddevgradle.legacyforge.dsl.LegacyForgeExtension; +import net.neoforged.moddevgradle.legacyforge.internal.LegacyForgeModDevPlugin; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LegacyModDevPluginTest extends AbstractProjectBuilderTest { + private final LegacyForgeExtension extension; + private final SourceSet mainSourceSet; + private final SourceSet testSourceSet; + + public LegacyModDevPluginTest() { + project = ProjectBuilder.builder().build(); + project.getPlugins().apply(LegacyForgeModDevPlugin.class); + + extension = ExtensionUtils.getExtension(project, "legacyForge", LegacyForgeExtension.class); + + var sourceSets = ExtensionUtils.getSourceSets(project); + mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + testSourceSet = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME); + + // Set the Java version to the currently running Java to make it use that + var java = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); + java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); + } + + @Test + void testModdingCannotBeEnabledTwice() { + extension.setVersion("1.2.3"); + var e = assertThrows(InvalidUserCodeException.class, () -> extension.setVersion("1.2.3")); + assertThat(e).hasMessage("You cannot enable modding in the same project twice."); + } + + @Test + void testEnableVanillaOnlyMode() { + extension.setMcpVersion("1.17.1"); + + assertThatDependencies(mainSourceSet.getCompileClasspathConfigurationName()) + .containsOnly( + "build/moddev/artifacts/vanilla-1.17.1.jar", + "de.oceanlabs.mcp:mcp_config:1.17.1[net.neoforged:neoform-dependencies]" + ); + assertThatDependencies(mainSourceSet.getRuntimeClasspathConfigurationName()) + .containsOnly( + "build/moddev/artifacts/vanilla-1.17.1.jar", + "build/moddev/artifacts/vanilla-1.17.1-client-extra-aka-minecraft-resources.jar", + "de.oceanlabs.mcp:mcp_config:1.17.1[net.neoforged:neoform-dependencies]", + "build/moddev/artifacts/intermediateToNamed.zip" + ); + } + + @Test + void testEnableForTestSourceSetOnly() { + extension.enable(settings -> { + settings.setForgeVersion("1.2.3"); + settings.setEnabledSourceSets(Set.of(testSourceSet)); + }); + + // Both the compile and runtime classpath of the main source set had no dependencies added + assertThatDependencies(mainSourceSet.getCompileClasspathConfigurationName()).isEmpty(); + assertThatDependencies(mainSourceSet.getRuntimeClasspathConfigurationName()).isEmpty(); + + // While the test classpath should have modding dependencies + assertContainsModdingCompileDependencies("1.2.3", testSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", testSourceSet.getRuntimeClasspathConfigurationName()); + } + + @Test + void testAddModdingDependenciesTo() { + extension.setVersion("1.2.3"); + + // Initially, only the main source set should have the dependencies + assertContainsModdingCompileDependencies("1.2.3", mainSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", mainSourceSet.getRuntimeClasspathConfigurationName()); + assertThatDependencies(testSourceSet.getCompileClasspathConfigurationName()).isEmpty(); + assertThatDependencies(testSourceSet.getRuntimeClasspathConfigurationName()).isEmpty(); + + // Now add it to the test source set too + extension.addModdingDependenciesTo(testSourceSet); + + assertContainsModdingCompileDependencies("1.2.3", testSourceSet.getCompileClasspathConfigurationName()); + assertContainsModdingRuntimeDependencies("1.2.3", testSourceSet.getRuntimeClasspathConfigurationName()); + } + + private void assertContainsModdingCompileDependencies(String version, String configurationName) { + assertThatDependencies(configurationName) + .containsOnly( + "build/moddev/artifacts/forge-" + version + ".jar", + "net.minecraftforge:forge:" + version + "[net.neoforged:neoforge-dependencies]" + ); + } + + private void assertContainsModdingRuntimeDependencies(String version, String configurationName) { + var configuration = project.getConfigurations().getByName(configurationName); + + var dependentTasks = configuration.getBuildDependencies().getDependencies(null); + assertThat(dependentTasks) + .extracting(Task::getName) + .containsOnly("createMinecraftArtifacts"); + + assertThatDependencies(configurationName) + .containsOnly( + "build/moddev/artifacts/forge-" + version + ".jar", + "build/moddev/artifacts/client-extra-1.2.3.jar", + "build/moddev/artifacts/intermediateToNamed.zip", + "net.minecraftforge:forge:" + version + "[net.neoforged:neoforge-dependencies]" + ); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/legacyforge/dsl/MixinMappingTest.java b/src/test/java/net/neoforged/moddevgradle/legacyforge/dsl/MixinMappingTest.java index b16b163c..f7716036 100644 --- a/src/test/java/net/neoforged/moddevgradle/legacyforge/dsl/MixinMappingTest.java +++ b/src/test/java/net/neoforged/moddevgradle/legacyforge/dsl/MixinMappingTest.java @@ -19,20 +19,24 @@ public void testMixinMappingsArePropagatedToObfuscationTasks() { var project = ProjectBuilder.builder().build(); project.getPlugins().apply(LegacyForgeModDevPlugin.class); - var obfuscation = ExtensionUtils.getExtension(project, "obfuscation", Obfuscation.class); + var obfuscation = ExtensionUtils.getExtension(project, LegacyForgeModDevPlugin.OBFUSCATION_EXTENSION, ObfuscationExtension.class); var sourceSets = ExtensionUtils.getSourceSets(project); - var mixinExtension = ExtensionUtils.getExtension(project, "mixin", MixinExtension.class); + var mixinExtension = ExtensionUtils.getExtension(project, LegacyForgeModDevPlugin.MIXIN_EXTENSION, MixinExtension.class); var mainSourceSet = sourceSets.getByName("main"); mixinExtension.add(mainSourceSet, "testmod.refmap.json"); var someJarTask = project.getTasks().register("someJar", Jar.class); var customRemapJarTask = obfuscation.reobfuscate(someJarTask, mainSourceSet).get(); - var remapJarTask = (RemapJar) project.getTasks().getByName("reobfJar"); + + var remapJarTask = (RemapJar) project.getTasks().getByName("reobfSomeJar"); // The main named->intermediary mappings for the game - var namedToIntermediary = project.getLayout().getBuildDirectory().file("moddev/namedToIntermediate.tsrg").get().getAsFile(); + var namedToIntermediary = project.getLayout().getBuildDirectory().file("moddev/artifacts/namedToIntermediate.tsrg").get().getAsFile(); var mixinApMappings = project.getLayout().getBuildDirectory().file("mixin/testmod.refmap.json.mappings.tsrg").get().getAsFile(); + // Enable modding to actually wire up the tasks + ExtensionUtils.getExtension(project, LegacyForgeModDevPlugin.LEGACYFORGE_EXTENSION, LegacyForgeExtension.class).setVersion("1.20.1-47.11"); + // The mapping file produced by the Mixin AP should be added as an input to both Jar tasks. var otherMappings = customRemapJarTask.getRemapOperation().getMappings().getFiles(); assertThat(otherMappings).containsOnly(namedToIntermediary, mixinApMappings);