From 454e32ece76f5c4c376ece644793f954d3e19582 Mon Sep 17 00:00:00 2001 From: modmuss Date: Wed, 1 Jan 2025 14:25:54 +0000 Subject: [PATCH] Refactor Fabric API extension (#1238) * Refactor Fabric API extension * Fix * Fix * Even more cleanup --- .../net/fabricmc/loom/LoomGradlePlugin.java | 5 +- .../api/fabricapi/DataGenerationSettings.java | 70 ++++ .../api/fabricapi/FabricApiExtension.java | 61 ++++ .../configuration/FabricApiExtension.java | 344 ------------------ .../fabricapi/FabricApiAbstractSourceSet.java | 110 ++++++ .../fabricapi/FabricApiDataGeneration.java | 145 ++++++++ .../fabricapi/FabricApiExtensionImpl.java | 67 ++++ .../fabricapi/FabricApiVersions.java | 157 ++++++++ .../test/unit/FabricApiExtensionTest.groovy | 6 +- 9 files changed, 616 insertions(+), 349 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java create mode 100644 src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java delete mode 100644 src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index 89cdbf7a4..144c548fd 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -34,9 +34,10 @@ import org.gradle.api.plugins.PluginAware; import net.fabricmc.loom.api.LoomGradleExtensionAPI; +import net.fabricmc.loom.api.fabricapi.FabricApiExtension; import net.fabricmc.loom.bootstrap.BootstrappedPlugin; import net.fabricmc.loom.configuration.CompileConfiguration; -import net.fabricmc.loom.configuration.FabricApiExtension; +import net.fabricmc.loom.configuration.fabricapi.FabricApiExtensionImpl; import net.fabricmc.loom.configuration.LoomConfigurations; import net.fabricmc.loom.configuration.MavenPublication; import net.fabricmc.loom.configuration.ide.idea.IdeaConfiguration; @@ -85,7 +86,7 @@ public void apply(Project project) { // Setup extensions project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project)); - project.getExtensions().create("fabricApi", FabricApiExtension.class); + project.getExtensions().create(FabricApiExtension.class, "fabricApi", FabricApiExtensionImpl.class); for (Class jobClass : SETUP_JOBS) { project.getObjects().newInstance(jobClass).run(); diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java new file mode 100644 index 000000000..4b86632c4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java @@ -0,0 +1,70 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.api.fabricapi; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; + +/** + * Represents the settings for data generation. + */ +public interface DataGenerationSettings { + /** + * Contains the output directory where generated data files will be stored. + */ + RegularFileProperty getOutputDirectory(); + + /** + * Contains a boolean indicating whether a run configuration should be created for the data generation process. + */ + Property getCreateRunConfiguration(); + + /** + * Contains a boolean property indicating whether a new source set should be created for the data generation process. + */ + Property getCreateSourceSet(); + + /** + * Contains a string property representing the mod ID associated with the data generation process. + * + *

This must be set when {@link #getCreateRunConfiguration()} is set. + */ + Property getModId(); + + /** + * Contains a boolean property indicating whether strict validation is enabled. + */ + Property getStrictValidation(); + + /** + * Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset. + */ + Property getAddToResources(); + + /** + * Contains a boolean property indicating whether data generation will be compiled and ran with the client. + */ + Property getClient(); +} diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java new file mode 100644 index 000000000..e05f7062f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java @@ -0,0 +1,61 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.api.fabricapi; + +import org.gradle.api.Action; +import org.gradle.api.artifacts.Dependency; + +/** + * A gradle extension with specific functionality related to Fabric API. + */ +public interface FabricApiExtension { + /** + * Get a {@link Dependency} for a given Fabric API module. + * + * @param moduleName The name of the module. + * @param fabricApiVersion The main Fabric API version. + * @return A {@link Dependency} for the module. + */ + Dependency module(String moduleName, String fabricApiVersion); + + /** + * Get the version of a Fabric API module. + * @param moduleName The name of the module. + * @param fabricApiVersion The main Fabric API version. + * @return The version of the module. + */ + String moduleVersion(String moduleName, String fabricApiVersion); + + /** + * Configuration data generation using the default settings. + */ + void configureDataGeneration(); + + /** + * Configuration data generation using the specified settings. + * @param action An action to configure specific data generation settings. See {@link DataGenerationSettings} for more information. + */ + void configureDataGeneration(Action action); +} diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java deleted file mode 100644 index 26d6b271a..000000000 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2020-2023 FabricMC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package net.fabricmc.loom.configuration; - -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -import org.gradle.api.Action; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.jvm.tasks.Jar; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; -import net.fabricmc.loom.util.download.DownloadException; -import net.fabricmc.loom.util.fmj.FabricModJson; -import net.fabricmc.loom.util.fmj.FabricModJsonFactory; -import net.fabricmc.loom.util.gradle.SourceSetHelper; - -public abstract class FabricApiExtension { - @Inject - public abstract Project getProject(); - - private static final String DATAGEN_SOURCESET_NAME = "datagen"; - - private static final HashMap> moduleVersionCache = new HashMap<>(); - private static final HashMap> deprecatedModuleVersionCache = new HashMap<>(); - - public Dependency module(String moduleName, String fabricApiVersion) { - return getProject().getDependencies() - .create(getDependencyNotation(moduleName, fabricApiVersion)); - } - - public String moduleVersion(String moduleName, String fabricApiVersion) { - String moduleVersion = moduleVersionCache - .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions) - .get(moduleName); - - if (moduleVersion == null) { - moduleVersion = deprecatedModuleVersionCache - .computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions) - .get(moduleName); - } - - if (moduleVersion == null) { - throw new RuntimeException("Failed to find module version for module: " + moduleName); - } - - return moduleVersion; - } - - /** - * Configure data generation with the default options. - */ - public void configureDataGeneration() { - configureDataGeneration(dataGenerationSettings -> { }); - } - - /** - * Configure data generation with custom options. - */ - public void configureDataGeneration(Action action) { - final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); - final TaskContainer taskContainer = getProject().getTasks(); - - DataGenerationSettings settings = getProject().getObjects().newInstance(DataGenerationSettings.class); - settings.getOutputDirectory().set(getProject().file("src/main/generated")); - settings.getCreateRunConfiguration().convention(true); - settings.getCreateSourceSet().convention(false); - settings.getStrictValidation().convention(false); - settings.getAddToResources().convention(true); - settings.getClient().convention(false); - - action.execute(settings); - - final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); - final File outputDirectory = settings.getOutputDirectory().getAsFile().get(); - - if (settings.getAddToResources().get()) { - mainSourceSet.resources(files -> { - // Add the src/main/generated to the main sourceset's resources. - Set srcDirs = new HashSet<>(files.getSrcDirs()); - srcDirs.add(outputDirectory); - files.setSrcDirs(srcDirs); - }); - } - - // Exclude the cache dir from the output jar to ensure reproducibility. - taskContainer.getByName(JavaPlugin.JAR_TASK_NAME, task -> { - Jar jar = (Jar) task; - jar.exclude(".cache/**"); - }); - - if (settings.getCreateSourceSet().get()) { - final boolean isClientAndSplit = extension.areEnvironmentSourceSetsSplit() && settings.getClient().get(); - - SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(getProject()); - - // Create the new datagen sourceset, depend on the main or client sourceset. - SourceSet dataGenSourceSet = sourceSets.create(DATAGEN_SOURCESET_NAME, sourceSet -> { - dependsOn(sourceSet, mainSourceSet); - - if (isClientAndSplit) { - dependsOn(sourceSet, SourceSetHelper.getSourceSetByName(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME, getProject())); - } - }); - - settings.getModId().convention(getProject().provider(() -> { - try { - final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), dataGenSourceSet); - - if (fabricModJson == null) { - throw new RuntimeException("Could not find a fabric.mod.json file in the data source set or a value for DataGenerationSettings.getModId()"); - } - - return fabricModJson.getId(); - } catch (IOException e) { - throw new org.gradle.api.UncheckedIOException("Failed to read mod id from the datagen source set.", e); - } - })); - - extension.getMods().create(settings.getModId().get(), mod -> { - // Create a classpath group for this mod. Assume that the main sourceset is already in a group. - mod.sourceSet(DATAGEN_SOURCESET_NAME); - }); - - extension.createRemapConfigurations(sourceSets.getByName(DATAGEN_SOURCESET_NAME)); - } - - if (settings.getCreateRunConfiguration().get()) { - extension.getRunConfigs().create("datagen", run -> { - run.inherit(extension.getRunConfigs().getByName(settings.getClient().get() ? "client" : "server")); - run.setConfigName("Data Generation"); - - run.property("fabric-api.datagen"); - run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); - run.runDir("build/datagen"); - - if (settings.getModId().isPresent()) { - run.property("fabric-api.datagen.modid", settings.getModId().get()); - } - - if (settings.getStrictValidation().get()) { - run.property("fabric-api.datagen.strict-validation", "true"); - } - - if (settings.getCreateSourceSet().get()) { - run.source(DATAGEN_SOURCESET_NAME); - } - }); - - // Add the output directory as an output allowing the task to be skipped. - getProject().getTasks().named("runDatagen", task -> { - task.getOutputs().dir(outputDirectory); - }); - } - } - - public interface DataGenerationSettings { - /** - * Contains the output directory where generated data files will be stored. - */ - RegularFileProperty getOutputDirectory(); - - /** - * Contains a boolean indicating whether a run configuration should be created for the data generation process. - */ - Property getCreateRunConfiguration(); - - /** - * Contains a boolean property indicating whether a new source set should be created for the data generation process. - */ - Property getCreateSourceSet(); - - /** - * Contains a string property representing the mod ID associated with the data generation process. - * - *

This must be set when {@link #getCreateRunConfiguration()} is set. - */ - Property getModId(); - - /** - * Contains a boolean property indicating whether strict validation is enabled. - */ - Property getStrictValidation(); - - /** - * Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset. - */ - Property getAddToResources(); - - /** - * Contains a boolean property indicating whether data generation will be compiled and ran with the client. - */ - Property getClient(); - } - - private String getDependencyNotation(String moduleName, String fabricApiVersion) { - return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); - } - - private Map getApiModuleVersions(String fabricApiVersion) { - try { - return populateModuleVersionMap(getApiMavenPom(fabricApiVersion)); - } catch (PomNotFoundException e) { - throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion); - } - } - - private Map getDeprecatedApiModuleVersions(String fabricApiVersion) { - try { - return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion)); - } catch (PomNotFoundException e) { - // Not all fabric-api versions have deprecated modules, return an empty map to cache this fact. - return Collections.emptyMap(); - } - } - - private Map populateModuleVersionMap(File pomFile) { - try { - DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); - Document pom = docBuilder.parse(pomFile); - - Map versionMap = new HashMap<>(); - - NodeList dependencies = ((Element) pom.getElementsByTagName("dependencies").item(0)).getElementsByTagName("dependency"); - - for (int i = 0; i < dependencies.getLength(); i++) { - Element dep = (Element) dependencies.item(i); - Element artifact = (Element) dep.getElementsByTagName("artifactId").item(0); - Element version = (Element) dep.getElementsByTagName("version").item(0); - - if (artifact == null || version == null) { - throw new RuntimeException("Failed to find artifact or version"); - } - - versionMap.put(artifact.getTextContent(), version.getTextContent()); - } - - return versionMap; - } catch (Exception e) { - throw new RuntimeException("Failed to parse " + pomFile.getName(), e); - } - } - - private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException { - return getPom("fabric-api", fabricApiVersion); - } - - private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException { - return getPom("fabric-api-deprecated", fabricApiVersion); - } - - private File getPom(String name, String version) throws PomNotFoundException { - final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); - final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version)); - - try { - extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name)) - .defaultCache() - .downloadPath(mavenPom.toPath()); - } catch (DownloadException e) { - if (e.getStatusCode() == 404) { - throw new PomNotFoundException(e); - } - - throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e); - } - - return mavenPom; - } - - private static class PomNotFoundException extends Exception { - PomNotFoundException(Throwable cause) { - super(cause); - } - } - - private static void extendsFrom(Project project, String name, String extendsFrom) { - final ConfigurationContainer configurations = project.getConfigurations(); - - configurations.named(name, configuration -> { - configuration.extendsFrom(configurations.getByName(extendsFrom)); - }); - } - - private void dependsOn(SourceSet sourceSet, SourceSet other) { - sourceSet.setCompileClasspath( - sourceSet.getCompileClasspath() - .plus(other.getOutput()) - ); - - sourceSet.setRuntimeClasspath( - sourceSet.getRuntimeClasspath() - .plus(other.getOutput()) - ); - - extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); - extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); - } -} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java new file mode 100644 index 000000000..6ffdce0c9 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java @@ -0,0 +1,110 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.fabricapi; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; +import net.fabricmc.loom.util.fmj.FabricModJson; +import net.fabricmc.loom.util.fmj.FabricModJsonFactory; +import net.fabricmc.loom.util.gradle.SourceSetHelper; + +abstract class FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + protected abstract String getSourceSetName(); + + protected void configureSourceSet(Property modId, boolean isClient) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); + + final boolean isClientAndSplit = extension.areEnvironmentSourceSetsSplit() && isClient; + + SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(getProject()); + + // Create the new datagen sourceset, depend on the main or client sourceset. + SourceSet dataGenSourceSet = sourceSets.create(getSourceSetName(), sourceSet -> { + dependsOn(sourceSet, mainSourceSet); + + if (isClientAndSplit) { + dependsOn(sourceSet, SourceSetHelper.getSourceSetByName(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME, getProject())); + } + }); + + modId.convention(getProject().provider(() -> { + try { + final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), dataGenSourceSet); + + if (fabricModJson == null) { + throw new RuntimeException("Could not find a fabric.mod.json file in the data source set or a value for DataGenerationSettings.getModId()"); + } + + return fabricModJson.getId(); + } catch (IOException e) { + throw new org.gradle.api.UncheckedIOException("Failed to read mod id from the datagen source set.", e); + } + })); + + extension.getMods().create(modId.get(), mod -> { + // Create a classpath group for this mod. Assume that the main sourceset is already in a group. + mod.sourceSet(getSourceSetName()); + }); + + extension.createRemapConfigurations(sourceSets.getByName(getSourceSetName())); + } + + private static void extendsFrom(Project project, String name, String extendsFrom) { + final ConfigurationContainer configurations = project.getConfigurations(); + + configurations.named(name, configuration -> { + configuration.extendsFrom(configurations.getByName(extendsFrom)); + }); + } + + private void dependsOn(SourceSet sourceSet, SourceSet other) { + sourceSet.setCompileClasspath( + sourceSet.getCompileClasspath() + .plus(other.getOutput()) + ); + + sourceSet.setRuntimeClasspath( + sourceSet.getRuntimeClasspath() + .plus(other.getOutput()) + ); + + extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); + extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java new file mode 100644 index 000000000..836df8569 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java @@ -0,0 +1,145 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.fabricapi; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.jvm.tasks.Jar; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.fabricapi.DataGenerationSettings; +import net.fabricmc.loom.util.gradle.SourceSetHelper; + +public abstract class FabricApiDataGeneration extends FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + @Inject + public FabricApiDataGeneration() { + } + + @Override + protected String getSourceSetName() { + return "datagen"; + } + + void configureDataGeneration(Action action) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final TaskContainer taskContainer = getProject().getTasks(); + + DataGenerationSettings settings = getProject().getObjects().newInstance(DataGenerationSettings.class); + settings.getOutputDirectory().set(getProject().file("src/main/generated")); + settings.getCreateRunConfiguration().convention(true); + settings.getCreateSourceSet().convention(false); + settings.getStrictValidation().convention(false); + settings.getAddToResources().convention(true); + settings.getClient().convention(false); + + action.execute(settings); + + final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); + final File outputDirectory = settings.getOutputDirectory().getAsFile().get(); + + if (settings.getAddToResources().get()) { + mainSourceSet.resources(files -> { + // Add the src/main/generated to the main sourceset's resources. + Set srcDirs = new HashSet<>(files.getSrcDirs()); + srcDirs.add(outputDirectory); + files.setSrcDirs(srcDirs); + }); + } + + // Exclude the cache dir from the output jar to ensure reproducibility. + taskContainer.getByName(JavaPlugin.JAR_TASK_NAME, task -> { + Jar jar = (Jar) task; + jar.exclude(".cache/**"); + }); + + if (settings.getCreateSourceSet().get()) { + configureSourceSet(settings.getModId(), settings.getClient().get()); + } + + if (settings.getCreateRunConfiguration().get()) { + extension.getRunConfigs().create("datagen", run -> { + run.inherit(extension.getRunConfigs().getByName(settings.getClient().get() ? "client" : "server")); + run.setConfigName("Data Generation"); + + run.property("fabric-api.datagen"); + run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); + run.runDir("build/datagen"); + + if (settings.getModId().isPresent()) { + run.property("fabric-api.datagen.modid", settings.getModId().get()); + } + + if (settings.getStrictValidation().get()) { + run.property("fabric-api.datagen.strict-validation", "true"); + } + + if (settings.getCreateSourceSet().get()) { + run.source(getSourceSetName()); + } + }); + + // Add the output directory as an output allowing the task to be skipped. + getProject().getTasks().named("runDatagen", task -> { + task.getOutputs().dir(outputDirectory); + }); + } + } + + private static void extendsFrom(Project project, String name, String extendsFrom) { + final ConfigurationContainer configurations = project.getConfigurations(); + + configurations.named(name, configuration -> { + configuration.extendsFrom(configurations.getByName(extendsFrom)); + }); + } + + private void dependsOn(SourceSet sourceSet, SourceSet other) { + sourceSet.setCompileClasspath( + sourceSet.getCompileClasspath() + .plus(other.getOutput()) + ); + + sourceSet.setRuntimeClasspath( + sourceSet.getRuntimeClasspath() + .plus(other.getOutput()) + ); + + extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); + extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java new file mode 100644 index 000000000..c19a1d8fb --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java @@ -0,0 +1,67 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2023 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.fabricapi; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.model.ObjectFactory; + +import net.fabricmc.loom.api.fabricapi.DataGenerationSettings; +import net.fabricmc.loom.api.fabricapi.FabricApiExtension; + +public abstract class FabricApiExtensionImpl implements FabricApiExtension { + @Inject + protected abstract ObjectFactory getObjectFactory(); + + private final FabricApiVersions versions; + private final FabricApiDataGeneration dataGeneration; + + public FabricApiExtensionImpl() { + versions = getObjectFactory().newInstance(FabricApiVersions.class); + dataGeneration = getObjectFactory().newInstance(FabricApiDataGeneration.class); + } + + @Override + public Dependency module(String moduleName, String fabricApiVersion) { + return versions.module(moduleName, fabricApiVersion); + } + + @Override + public String moduleVersion(String moduleName, String fabricApiVersion) { + return versions.moduleVersion(moduleName, fabricApiVersion); + } + + @Override + public void configureDataGeneration() { + configureDataGeneration(dataGenerationSettings -> { }); + } + + @Override + public void configureDataGeneration(Action action) { + dataGeneration.configureDataGeneration(action); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java new file mode 100644 index 000000000..937a07337 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java @@ -0,0 +1,157 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.configuration.fabricapi; + +import java.io.File; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.util.download.DownloadException; + +public abstract class FabricApiVersions { + @Inject + protected abstract Project getProject(); + + private final HashMap> moduleVersionCache = new HashMap<>(); + private final HashMap> deprecatedModuleVersionCache = new HashMap<>(); + + public Dependency module(String moduleName, String fabricApiVersion) { + return getProject().getDependencies() + .create(getDependencyNotation(moduleName, fabricApiVersion)); + } + + public String moduleVersion(String moduleName, String fabricApiVersion) { + String moduleVersion = moduleVersionCache + .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions) + .get(moduleName); + + if (moduleVersion == null) { + moduleVersion = deprecatedModuleVersionCache + .computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions) + .get(moduleName); + } + + if (moduleVersion == null) { + throw new RuntimeException("Failed to find module version for module: " + moduleName); + } + + return moduleVersion; + } + + private String getDependencyNotation(String moduleName, String fabricApiVersion) { + return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); + } + + private Map getApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion); + } + } + + private Map getDeprecatedApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + // Not all fabric-api versions have deprecated modules, return an empty map to cache this fact. + return Collections.emptyMap(); + } + } + + private Map populateModuleVersionMap(File pomFile) { + try { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + Document pom = docBuilder.parse(pomFile); + + Map versionMap = new HashMap<>(); + + NodeList dependencies = ((Element) pom.getElementsByTagName("dependencies").item(0)).getElementsByTagName("dependency"); + + for (int i = 0; i < dependencies.getLength(); i++) { + Element dep = (Element) dependencies.item(i); + Element artifact = (Element) dep.getElementsByTagName("artifactId").item(0); + Element version = (Element) dep.getElementsByTagName("version").item(0); + + if (artifact == null || version == null) { + throw new RuntimeException("Failed to find artifact or version"); + } + + versionMap.put(artifact.getTextContent(), version.getTextContent()); + } + + return versionMap; + } catch (Exception e) { + throw new RuntimeException("Failed to parse " + pomFile.getName(), e); + } + } + + private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api", fabricApiVersion); + } + + private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api-deprecated", fabricApiVersion); + } + + private File getPom(String name, String version) throws PomNotFoundException { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version)); + + try { + extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name)) + .defaultCache() + .downloadPath(mavenPom.toPath()); + } catch (DownloadException e) { + if (e.getStatusCode() == 404) { + throw new PomNotFoundException(e); + } + + throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e); + } + + return mavenPom; + } + + private static class PomNotFoundException extends Exception { + PomNotFoundException(Throwable cause) { + super(cause); + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy index 860e65ebd..0e4b5403b 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy @@ -27,13 +27,13 @@ package net.fabricmc.loom.test.unit import org.gradle.api.Project import spock.lang.Specification -import net.fabricmc.loom.configuration.FabricApiExtension +import net.fabricmc.loom.configuration.fabricapi.FabricApiVersions import net.fabricmc.loom.test.util.GradleTestUtil class FabricApiExtensionTest extends Specification { def "get module version"() { when: - def fabricApi = new FabricApiExtension() { + def fabricApi = new FabricApiVersions() { Project project = GradleTestUtil.mockProject() } def version = fabricApi.moduleVersion(moduleName, apiVersion) @@ -51,7 +51,7 @@ class FabricApiExtensionTest extends Specification { def "unknown module"() { when: - def fabricApi = new FabricApiExtension() { + def fabricApi = new FabricApiVersions() { Project project = GradleTestUtil.mockProject() } fabricApi.moduleVersion("fabric-api-unknown", apiVersion)