From 09a4831f25079373ad86ecadde9550f9c439bf01 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 20 Oct 2025 22:46:36 +0100 Subject: [PATCH] Support classpath groups when using configure on demand. (#1392) * Support classpath groups when using configure on demand. * Cleanup * Work around Gradle 8.14 issue * Another fix * Rename plugin * Fix plugin versioning * Add some docs * More fixes * Ensure backwards compatible. --- build.gradle | 42 +++-- .../loom/LoomCompanionGradlePlugin.java | 44 +++++ .../net/fabricmc/loom/LoomGradlePlugin.java | 3 + .../net/fabricmc/loom/api/ModSettings.java | 53 +++++- .../configuration/CompileConfiguration.java | 28 +-- .../configuration/LoomConfigurations.java | 4 +- .../classpathgroups/ClasspathGroup.java | 53 ++++++ .../ExternalClasspathGroup.java | 30 ++++ .../ExternalClasspathGroupDTO.java | 87 ++++++++++ .../loom/task/launch/ExportClasspathTask.java | 93 ++++++++++ .../task/launch/GenerateDLIConfigTask.java | 37 ++-- .../task/service/ClasspathGroupService.java | 162 ++++++++++++++++++ .../net/fabricmc/loom/util/Constants.java | 5 + .../loom/util/gradle/GradleUtils.java | 3 +- .../loom/util/gradle/SourceSetHelper.java | 15 +- .../test/integration/MultiProjectTest.groovy | 15 ++ .../projects/multiproject/build.gradle | 7 +- .../multiproject/javalib/build.gradle | 4 + .../projects/multiproject/settings.gradle | 3 +- 19 files changed, 611 insertions(+), 77 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/LoomCompanionGradlePlugin.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/classpathgroups/ClasspathGroup.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroup.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroupDTO.java create mode 100644 src/main/java/net/fabricmc/loom/task/launch/ExportClasspathTask.java create mode 100644 src/main/java/net/fabricmc/loom/task/service/ClasspathGroupService.java create mode 100644 src/test/resources/projects/multiproject/javalib/build.gradle diff --git a/build.gradle b/build.gradle index aaf025ef..7e68d615 100644 --- a/build.gradle +++ b/build.gradle @@ -237,6 +237,10 @@ gradlePlugin { id = 'fabric-loom' implementationClass = 'net.fabricmc.loom.LoomGradlePlugin' } + fabricLoomCompanion { + id = 'net.fabricmc.fabric-loom-companion' + implementationClass = 'net.fabricmc.loom.LoomCompanionGradlePlugin' + } } } @@ -292,25 +296,27 @@ publishing { from components.java } - // Manually crate the plugin marker for snapshot versions - snapshotPlugin(MavenPublication) { publication -> - groupId = 'fabric-loom' - artifactId = 'fabric-loom.gradle.plugin' - version = baseVersion + '-SNAPSHOT' + gradlePlugin.plugins.forEach { plugin -> + // Manually crate the plugin marker for snapshot versions + it.create(plugin.id + "SnapshotMarker", MavenPublication) { publication -> + groupId = plugin.id + artifactId = plugin.id + '.gradle.plugin' + version = baseVersion + '-SNAPSHOT' - pom.withXml({ - // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin - Element root = asElement() - Document document = root.getOwnerDocument() - Node dependencies = root.appendChild(document.createElement('dependencies')) - Node dependency = dependencies.appendChild(document.createElement('dependency')) - Node groupId = dependency.appendChild(document.createElement('groupId')) - groupId.setTextContent('net.fabricmc') - Node artifactId = dependency.appendChild(document.createElement('artifactId')) - artifactId.setTextContent('fabric-loom') - Node version = dependency.appendChild(document.createElement('version')) - version.setTextContent(baseVersion + '-SNAPSHOT') - }) + pom.withXml({ + // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin + Element root = asElement() + Document document = root.getOwnerDocument() + Node dependencies = root.appendChild(document.createElement('dependencies')) + Node dependency = dependencies.appendChild(document.createElement('dependency')) + Node groupId = dependency.appendChild(document.createElement('groupId')) + groupId.setTextContent('net.fabricmc') + Node artifactId = dependency.appendChild(document.createElement('artifactId')) + artifactId.setTextContent('fabric-loom') + Node version = dependency.appendChild(document.createElement('version')) + version.setTextContent(project.version) + }) + } } } } diff --git a/src/main/java/net/fabricmc/loom/LoomCompanionGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomCompanionGradlePlugin.java new file mode 100644 index 00000000..18a6ca99 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/LoomCompanionGradlePlugin.java @@ -0,0 +1,44 @@ +/* + * 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; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.jetbrains.annotations.NotNull; + +import net.fabricmc.loom.configuration.LoomConfigurations; +import net.fabricmc.loom.task.launch.ExportClasspathTask; +import net.fabricmc.loom.util.Constants; + +public class LoomCompanionGradlePlugin implements Plugin { + public static final String NAME = "net.fabricmc.fabric-loom-companion"; + + @Override + public void apply(@NotNull Project project) { + var exportClassPathTask = project.getTasks().register(Constants.Task.EXPORT_CLASSPATH, ExportClasspathTask.class); + project.getConfigurations().register(Constants.Configurations.EXPORTED_CLASSPATH, LoomConfigurations.Role.CONSUMABLE::apply); + project.artifacts(artifactHandler -> artifactHandler.add(Constants.Configurations.EXPORTED_CLASSPATH, exportClassPathTask)); + } +} diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index c253d5ea..05a2cdeb 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -50,6 +50,7 @@ import net.fabricmc.loom.task.RemapTaskConfiguration; import net.fabricmc.loom.util.LibraryLocationLogger; public class LoomGradlePlugin implements Plugin { + public static final String NAME = "fabric-loom"; public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public static final String LOOM_VERSION = Objects.requireNonNullElse(LoomGradlePlugin.class.getPackage().getImplementationVersion(), "0.0.0+unknown"); @@ -92,5 +93,7 @@ public class LoomGradlePlugin implements Plugin { for (Class jobClass : SETUP_JOBS) { project.getObjects().newInstance(jobClass).run(); } + + project.apply(Map.of("plugin", LoomCompanionGradlePlugin.NAME)); } } diff --git a/src/main/java/net/fabricmc/loom/api/ModSettings.java b/src/main/java/net/fabricmc/loom/api/ModSettings.java index 7df3134c..d40c0e46 100644 --- a/src/main/java/net/fabricmc/loom/api/ModSettings.java +++ b/src/main/java/net/fabricmc/loom/api/ModSettings.java @@ -24,6 +24,10 @@ package net.fabricmc.loom.api; +import java.io.File; +import java.util.List; +import java.util.Map; + import javax.inject.Inject; import org.gradle.api.Named; @@ -35,6 +39,8 @@ import org.gradle.api.provider.ListProperty; import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.ApiStatus; +import net.fabricmc.loom.LoomCompanionGradlePlugin; +import net.fabricmc.loom.configuration.classpathgroups.ExternalClasspathGroup; import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.gradle.SourceSetReference; @@ -49,7 +55,7 @@ public abstract class ModSettings implements Named { @Inject public ModSettings() { - getModSourceSets().finalizeValueOnRead(); + getExternalGroups().finalizeValueOnRead(); getModFiles().finalizeValueOnRead(); } @@ -82,18 +88,46 @@ public abstract class ModSettings implements Named { /** * Add {@link SourceSet}'s output directories from the supplied project to be grouped with the named mod. + * @deprecated Replaced with {@link #sourceSet(String, String)} to avoid passing a project reference. */ + @Deprecated public void sourceSet(SourceSet sourceSet, Project project) { - getModSourceSets().add(new SourceSetReference(sourceSet, project)); + ensureCompanion(project); + + sourceSet(sourceSet.getName(), project.getPath()); } /** * Add {@link SourceSet}'s output directories from the supplied project to be grouped with the named mod. * * @param name the name of the source set + * @deprecated Replaced with {@link #sourceSet(String, String)} to avoid passing a project reference. */ + @Deprecated public void sourceSet(String name, Project project) { - sourceSet(SourceSetHelper.getSourceSetByName(name, project), project); + ensureCompanion(project); + + sourceSet(name, project.getPath()); + } + + /** + * Add {@link SourceSet}'s output directories from the supplied project to be grouped with the named mod. + * + *

If the other project is not a Loom project you must apply the `net.fabricmc.fabric-loom-companion` plugin. + * + * @param sourceSetName the name of the source set + * @param projectPath the path of the project the source set belongs to + */ + public void sourceSet(String sourceSetName, String projectPath) { + if (projectPath.equals(getProject().getPath())) { + // Shortcut for source sets in our own project. + SourceSetReference ref = new SourceSetReference(SourceSetHelper.getSourceSetByName(sourceSetName, getProject()), getProject()); + List classpath = SourceSetHelper.getClasspath(ref); + getModFiles().from(classpath); + return; + } + + getExternalGroups().add(new ExternalClasspathGroup(projectPath, sourceSetName)); } /** @@ -114,11 +148,10 @@ public abstract class ModSettings implements Named { } /** - * List of classpath directories, used to populate the `fabric.classPathGroups` Fabric Loader system property. - * Use the {@link ModSettings#sourceSet} methods to add to this. + * List of {@link ExternalClasspathGroup} that will later be resolved to populate the classpath groups from another Gradle project. */ @ApiStatus.Internal - public abstract ListProperty getModSourceSets(); + public abstract ListProperty getExternalGroups(); @Inject public abstract Project getProject(); @@ -127,4 +160,12 @@ public abstract class ModSettings implements Named { public String toString() { return "ModSettings '" + getName() + "'"; } + + private void ensureCompanion(Project project) { + if (project == getProject()) { + return; + } + + project.apply(Map.of("plugin", LoomCompanionGradlePlugin.NAME)); + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 6c050bd0..249a4f6c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -26,7 +26,6 @@ package net.fabricmc.loom.configuration; import static net.fabricmc.loom.util.Constants.Configurations; -import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; @@ -36,12 +35,13 @@ import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.inject.Inject; +import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.plugins.JavaPlugin; @@ -71,6 +71,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMapped import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.extension.MixinExtension; +import net.fabricmc.loom.task.service.ClasspathGroupService; import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.ProcessUtil; @@ -266,15 +267,22 @@ public abstract class CompileConfiguration implements Runnable { } getProject().getTasks().named(JavaPlugin.TEST_TASK_NAME, Test.class, test -> { - String classPathGroups = extension.getMods().stream() - .map(modSettings -> - SourceSetHelper.getClasspath(modSettings, getProject()).stream() - .map(File::getAbsolutePath) - .collect(Collectors.joining(File.pathSeparator)) - ) - .collect(Collectors.joining(File.pathSeparator+File.pathSeparator));; + test.getInputs().property("LoomClassPathGroups", ClasspathGroupService.create(getProject())); + test.doFirst(new Action() { + @Override + public void execute(Task task) { + try (ScopedServiceFactory serviceFactory = new ScopedServiceFactory()) { + var options = (ClasspathGroupService.Options) task.getInputs().getProperties().get("LoomClassPathGroups"); + ClasspathGroupService classpathGroupService = serviceFactory.get(options); - test.systemProperty("fabric.classPathGroups", classPathGroups); + if (classpathGroupService.hasGroups()) { + test.systemProperty("fabric.classPathGroups", classpathGroupService.getClasspathGroupsPropertyValue()); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to get classpath groups", e); + } + } + }); }); } diff --git a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java index 72fa65a3..3586d4ce 100644 --- a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java +++ b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java @@ -175,7 +175,7 @@ public abstract class LoomConfigurations implements Runnable { getConfigurations().getByName(a, configuration -> configuration.extendsFrom(getConfigurations().getByName(b))); } - enum Role { + public enum Role { NONE(false, false), CONSUMABLE(true, false), RESOLVABLE(false, true); @@ -188,7 +188,7 @@ public abstract class LoomConfigurations implements Runnable { this.canBeResolved = canBeResolved; } - void apply(Configuration configuration) { + public void apply(Configuration configuration) { configuration.setCanBeConsumed(canBeConsumed); configuration.setCanBeResolved(canBeResolved); } diff --git a/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ClasspathGroup.java b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ClasspathGroup.java new file mode 100644 index 00000000..ae383520 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ClasspathGroup.java @@ -0,0 +1,53 @@ +/* + * 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.classpathgroups; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import net.fabricmc.loom.api.ModSettings; + +public record ClasspathGroup(List paths, List externalGroups) implements Serializable { + public static List fromModSettings(Set modSettings) { + return modSettings.stream().map(s -> new ClasspathGroup(getPaths(s), s.getExternalGroups().get())).toList(); + } + + // TODO remove this constructor when updating to Gradle 9.0, works around an issue where config cache cannot serialize immutable lists + public ClasspathGroup(List paths, List externalGroups) { + this.paths = new ArrayList<>(paths); + this.externalGroups = new ArrayList<>(externalGroups); + } + + private static List getPaths(ModSettings modSettings) { + return modSettings.getModFiles() + .getFiles() + .stream() + .map(File::getAbsolutePath) + .toList(); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroup.java b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroup.java new file mode 100644 index 00000000..780da174 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroup.java @@ -0,0 +1,30 @@ +/* + * 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.classpathgroups; + +import java.io.Serializable; + +public record ExternalClasspathGroup(String projectPath, String sourceSetName) implements Serializable { +} diff --git a/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroupDTO.java b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroupDTO.java new file mode 100644 index 00000000..38e02448 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/classpathgroups/ExternalClasspathGroupDTO.java @@ -0,0 +1,87 @@ +/* + * 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.classpathgroups; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.util.gradle.SourceSetHelper; +import net.fabricmc.loom.util.gradle.SourceSetReference; + +/** + * This object is exported by projects as a json file to be consumed by others to correctly populate the classpath groups. + */ +public record ExternalClasspathGroupDTO(String projectPath, Map> classpaths) implements Serializable { + public static ExternalClasspathGroupDTO createFromProject(Project project) { + SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(project); + + Map> classpaths = new HashMap<>(); + + for (SourceSet sourceSet : sourceSets) { + SourceSetReference ref = new SourceSetReference(sourceSet, project); + List classpath = SourceSetHelper.getClasspath(ref); + classpaths.put(sourceSet.getName(), classpath.stream().map(File::getAbsolutePath).toList()); + } + + return new ExternalClasspathGroupDTO(project.getPath(), Collections.unmodifiableMap(classpaths)); + } + + public static Map resolveExternal(Set files) { + Map map = new HashMap<>(); + + for (File file : files) { + String json; + + try { + json = Files.readString(file.toPath()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read external classpath group file: " + file, e); + } + + ExternalClasspathGroupDTO dto = LoomGradlePlugin.GSON.fromJson(json, ExternalClasspathGroupDTO.class); + map.put(dto.projectPath(), dto); + } + + return Collections.unmodifiableMap(map); + } + + public List getForSourceSet(String sourceSetName) { + return Objects.requireNonNull(classpaths.get(sourceSetName), "No classpath found for source set: " + sourceSetName); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/launch/ExportClasspathTask.java b/src/main/java/net/fabricmc/loom/task/launch/ExportClasspathTask.java new file mode 100644 index 00000000..72eafccd --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/launch/ExportClasspathTask.java @@ -0,0 +1,93 @@ +/* + * 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.task.launch; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import javax.inject.Inject; + +import org.gradle.api.UncheckedIOException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; + +import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.configuration.classpathgroups.ExternalClasspathGroupDTO; +import net.fabricmc.loom.task.AbstractLoomTask; + +public abstract class ExportClasspathTask extends AbstractLoomTask { + @Input + public abstract Property getClasspathDtoJson(); + + @OutputFile + public abstract RegularFileProperty getOutput(); + + @Inject + protected abstract WorkerExecutor getWorkerExecutor(); + + @Inject + public ExportClasspathTask() { + getClasspathDtoJson().set(getProject() + .provider(() -> ExternalClasspathGroupDTO.createFromProject(getProject())) + .map(LoomGradlePlugin.GSON::toJson)); + getOutput().set(getProject().getLayout().getBuildDirectory().file("export_classpath.json")); + } + + @TaskAction + public void run() { + final WorkQueue workQueue = getWorkerExecutor().noIsolation(); + workQueue.submit(ExportClassPathWorkAction.class, p -> { + p.getClasspathDtoJson().set(getClasspathDtoJson()); + p.getOutput().set(getOutput()); + }); + } + + protected interface ExportClassPathWorkParameters extends WorkParameters { + Property getClasspathDtoJson(); + RegularFileProperty getOutput(); + } + + public abstract static class ExportClassPathWorkAction implements WorkAction { + @Override + public void execute() { + File outputFile = getParameters().getOutput().getAsFile().get(); + String json = getParameters().getClasspathDtoJson().get(); + + try { + Files.writeString(outputFile.toPath(), json); + } catch (IOException e) { + throw new UncheckedIOException("Failed to write classpath groups", e); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java index 004209dc..a6ab0b26 100644 --- a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java +++ b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java @@ -42,6 +42,7 @@ import org.gradle.api.logging.configuration.ConsoleOutput; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; @@ -51,7 +52,8 @@ import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraftProvider; import net.fabricmc.loom.task.AbstractLoomTask; -import net.fabricmc.loom.util.gradle.SourceSetHelper; +import net.fabricmc.loom.task.service.ClasspathGroupService; +import net.fabricmc.loom.util.service.ScopedServiceFactory; public abstract class GenerateDLIConfigTask extends AbstractLoomTask { @Input @@ -69,10 +71,6 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask { @Input protected abstract Property getANSISupportedIDE(); - @Input - @Optional - protected abstract Property getClassPathGroups(); - @Input protected abstract Property getLog4jConfigPaths(); @@ -96,16 +94,16 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask { @OutputFile protected abstract RegularFileProperty getDevLauncherConfig(); + @Nested + protected abstract Property getClasspathGroupOptions(); + public GenerateDLIConfigTask() { getVersionInfoJson().set(LoomGradlePlugin.GSON.toJson(getExtension().getMinecraftProvider().getVersionInfo())); getMinecraftVersion().set(getExtension().getMinecraftProvider().minecraftVersion()); getSplitSourceSets().set(getExtension().areEnvironmentSourceSetsSplit()); getANSISupportedIDE().set(ansiSupportedIde(getProject())); getPlainConsole().set(getProject().getGradle().getStartParameter().getConsoleOutput() == ConsoleOutput.Plain); - - if (!getExtension().getMods().isEmpty()) { - getClassPathGroups().set(buildClassPathGroups(getProject())); - } + getClasspathGroupOptions().set(ClasspathGroupService.create(getProject())); getLog4jConfigPaths().set(getAllLog4JConfigFiles(getProject())); @@ -152,8 +150,12 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask { launchConfig.property("fabric.gameJarPath", getCommonGameJarPath().get()); } - if (getClassPathGroups().isPresent()) { - launchConfig.property("fabric.classPathGroups", getClassPathGroups().get()); + try (ScopedServiceFactory serviceFactory = new ScopedServiceFactory()) { + ClasspathGroupService classpathGroupService = serviceFactory.get(getClasspathGroupOptions()); + + if (classpathGroupService.hasGroups()) { + launchConfig.property("fabric.classPathGroups", classpathGroupService.getClasspathGroupsPropertyValue()); + } } //Enable ansi by default for idea and vscode when gradle is not ran with plain console. @@ -180,19 +182,6 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask { }; } - /** - * See: https://github.com/FabricMC/fabric-loader/pull/585. - */ - private static String buildClassPathGroups(Project project) { - return LoomGradleExtension.get(project).getMods().stream() - .map(modSettings -> - SourceSetHelper.getClasspath(modSettings, project).stream() - .map(File::getAbsolutePath) - .collect(Collectors.joining(File.pathSeparator)) - ) - .collect(Collectors.joining(File.pathSeparator+File.pathSeparator)); - } - private static boolean ansiSupportedIde(Project project) { File rootDir = project.getRootDir(); return new File(rootDir, ".vscode").exists() diff --git a/src/main/java/net/fabricmc/loom/task/service/ClasspathGroupService.java b/src/main/java/net/fabricmc/loom/task/service/ClasspathGroupService.java new file mode 100644 index 00000000..c3af919d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/service/ClasspathGroupService.java @@ -0,0 +1,162 @@ +/* + * 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.task.service; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.ModSettings; +import net.fabricmc.loom.configuration.classpathgroups.ClasspathGroup; +import net.fabricmc.loom.configuration.classpathgroups.ExternalClasspathGroup; +import net.fabricmc.loom.configuration.classpathgroups.ExternalClasspathGroupDTO; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.Lazy; +import net.fabricmc.loom.util.service.Service; +import net.fabricmc.loom.util.service.ServiceFactory; +import net.fabricmc.loom.util.service.ServiceType; + +public class ClasspathGroupService extends Service { + public static ServiceType TYPE = new ServiceType(Options.class, ClasspathGroupService.class); + + public interface Options extends Service.Options { + @Input + @Optional + ListProperty getClasspathGroups(); + + @InputFiles + @Optional + ConfigurableFileCollection getExternalClasspathGroups(); + } + + public static Provider create(Project project) { + return TYPE.create(project, options -> { + LoomGradleExtension extension = LoomGradleExtension.get(project); + NamedDomainObjectContainer modSettings = extension.getMods(); + + if (modSettings.isEmpty()) { + return; + } + + options.getClasspathGroups().set(ClasspathGroup.fromModSettings(modSettings)); + + if (!hasExternalClasspathGroups(modSettings)) { + return; + } + + List externalDependencies = getExternalDependencies(project, modSettings); + Configuration externalClasspathGroups = project.getConfigurations().detachedConfiguration(externalDependencies.toArray(new Dependency[0])); + options.getExternalClasspathGroups().from(externalClasspathGroups); + }); + } + + private static boolean hasExternalClasspathGroups(Set modSettings) { + return modSettings.stream() + .anyMatch(s -> + s.getExternalGroups().isPresent() + && !s.getExternalGroups().get().isEmpty() + ); + } + + private static List getExternalDependencies(Project project, Set modSettings) { + List requiredProjects = modSettings.stream() + .flatMap(s -> s.getExternalGroups().get().stream()) + .map(ExternalClasspathGroup::projectPath) + .distinct() + .toList(); + + List dependencies = new ArrayList<>(); + + for (String projectPath : requiredProjects) { + Dependency externalDependency = project.getDependencies() + .project(Map.of( + "path", projectPath, + "configuration", Constants.Configurations.EXPORTED_CLASSPATH + )); + dependencies.add(externalDependency); + } + + return Collections.unmodifiableList(dependencies); + } + + private final Supplier> externalClasspathGroups = Lazy.of(() -> ExternalClasspathGroupDTO.resolveExternal(getOptions().getExternalClasspathGroups().getFiles())); + + public ClasspathGroupService(Options options, ServiceFactory serviceFactory) { + super(options, serviceFactory); + } + + public List getClasspath(ClasspathGroup classpathGroup) { + final List paths = new ArrayList<>(); + + for (ExternalClasspathGroup externalGroup : classpathGroup.externalGroups()) { + ExternalClasspathGroupDTO dto = externalClasspathGroups.get().get(externalGroup.projectPath()); + + if (dto == null) { + throw new IllegalStateException("Could not find resolved external classpath group for project: " + externalGroup.projectPath()); + } + + paths.addAll(dto.getForSourceSet(externalGroup.sourceSetName())); + } + + paths.addAll(classpathGroup.paths()); + + return paths.stream().map(File::new).toList(); + } + + /** + * See: https://github.com/FabricMC/fabric-loader/pull/585. + */ + public String getClasspathGroupsPropertyValue() { + return getOptions().getClasspathGroups().get() + .stream() + .map(group -> + getClasspath(group).stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining(File.pathSeparator)) + ) + .collect(Collectors.joining(File.pathSeparator+File.pathSeparator)); + } + + public boolean hasGroups() { + return getOptions().getClasspathGroups().isPresent() && !getOptions().getClasspathGroups().get().isEmpty(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 24294c0d..c051f3c1 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -90,6 +90,10 @@ public class Constants { * Mods to be used by {@link net.fabricmc.loom.task.prod.AbstractProductionRunTask} tasks by default. */ public static final String PRODUCTION_RUNTIME_MODS = "productionRuntimeMods"; + /** + * Used to query classpath data across project boundaries. + */ + public static final String EXPORTED_CLASSPATH = "loomExportedClasspath"; private Configurations() { } @@ -125,6 +129,7 @@ public class Constants { public static final class Task { public static final String PROCESS_INCLUDE_JARS = "processIncludeJars"; + public static final String EXPORT_CLASSPATH = "exportClasspath"; private Task() { } diff --git a/src/main/java/net/fabricmc/loom/util/gradle/GradleUtils.java b/src/main/java/net/fabricmc/loom/util/gradle/GradleUtils.java index 0303aa63..1cfcc2b7 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/GradleUtils.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/GradleUtils.java @@ -33,6 +33,7 @@ import org.gradle.api.invocation.Gradle; import org.gradle.api.provider.Provider; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.LoomGradlePlugin; public final class GradleUtils { private GradleUtils() { @@ -59,7 +60,7 @@ public final class GradleUtils { } public static boolean isLoomProject(Project project) { - return project.getPluginManager().hasPlugin("fabric-loom"); + return project.getPluginManager().hasPlugin(LoomGradlePlugin.NAME); } public static Provider getBooleanPropertyProvider(Project project, String key) { diff --git a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java index e605648c..4ab589e0 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java @@ -52,7 +52,6 @@ import org.jetbrains.annotations.VisibleForTesting; import org.xml.sax.InputSource; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.api.ModSettings; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.util.Constants; @@ -113,18 +112,8 @@ public final class SourceSetHelper { return it.hasNext() ? it.next().getProject() : null; } - public static List getClasspath(ModSettings modSettings, Project project) { - final List files = new ArrayList<>(); - - files.addAll(modSettings.getModSourceSets().get().stream() - .flatMap(sourceSet -> getClasspath(sourceSet, project).stream()) - .toList()); - files.addAll(modSettings.getModFiles().getFiles()); - - return Collections.unmodifiableList(files); - } - - public static List getClasspath(SourceSetReference reference, Project project) { + public static List getClasspath(SourceSetReference reference) { + final Project project = reference.project(); final List classpath = getGradleClasspath(reference, project); classpath.addAll(getIdeaClasspath(reference, project)); diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/MultiProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/MultiProjectTest.groovy index f163703d..78083ded 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/MultiProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/MultiProjectTest.groovy @@ -57,4 +57,19 @@ class MultiProjectTest extends Specification implements GradleProjectTestTrait { where: version << STANDARD_TEST_VERSIONS } + + @Unroll + def "classpath groups (gradle #version)"() { + setup: + def gradle = gradleProject(project: "multiproject", version: version) + + when: + def result = gradle.run(tasks: [":generateDLIConfig",]) + + then: + result.task(":generateDLIConfig").outcome == SUCCESS + + where: + version << STANDARD_TEST_VERSIONS + } } diff --git a/src/test/resources/projects/multiproject/build.gradle b/src/test/resources/projects/multiproject/build.gradle index ae3e637a..1e414ec3 100644 --- a/src/test/resources/projects/multiproject/build.gradle +++ b/src/test/resources/projects/multiproject/build.gradle @@ -5,6 +5,8 @@ plugins { } allprojects { + if (it.path == ":javalib") return; + apply plugin: "fabric-loom" version = "1.0.0" @@ -42,13 +44,14 @@ allprojects { loom { mods { core { - sourceSet project(':core').sourceSets.main + sourceSet("main", ":core") } example { - sourceSet project(':example').sourceSets.main + sourceSet("main", ":example") } root { sourceSet sourceSets.main + sourceSet("main", ":javalib") } } } diff --git a/src/test/resources/projects/multiproject/javalib/build.gradle b/src/test/resources/projects/multiproject/javalib/build.gradle new file mode 100644 index 00000000..1fb413eb --- /dev/null +++ b/src/test/resources/projects/multiproject/javalib/build.gradle @@ -0,0 +1,4 @@ +plugins { + id 'java-library' + id 'net.fabricmc.fabric-loom-companion' +} \ No newline at end of file diff --git a/src/test/resources/projects/multiproject/settings.gradle b/src/test/resources/projects/multiproject/settings.gradle index df4ff932..e0e1c14b 100644 --- a/src/test/resources/projects/multiproject/settings.gradle +++ b/src/test/resources/projects/multiproject/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = "fabric-example-mod" include 'core' -include 'example' \ No newline at end of file +include 'example' +include 'javalib' \ No newline at end of file