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.
This commit is contained in:
modmuss
2025-10-20 22:46:36 +01:00
committed by GitHub
parent b2c933d2c0
commit 09a4831f25
19 changed files with 611 additions and 77 deletions

View File

@@ -237,6 +237,10 @@ gradlePlugin {
id = 'fabric-loom' id = 'fabric-loom'
implementationClass = 'net.fabricmc.loom.LoomGradlePlugin' 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 from components.java
} }
// Manually crate the plugin marker for snapshot versions gradlePlugin.plugins.forEach { plugin ->
snapshotPlugin(MavenPublication) { publication -> // Manually crate the plugin marker for snapshot versions
groupId = 'fabric-loom' it.create(plugin.id + "SnapshotMarker", MavenPublication) { publication ->
artifactId = 'fabric-loom.gradle.plugin' groupId = plugin.id
version = baseVersion + '-SNAPSHOT' artifactId = plugin.id + '.gradle.plugin'
version = baseVersion + '-SNAPSHOT'
pom.withXml({ pom.withXml({
// Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin
Element root = asElement() Element root = asElement()
Document document = root.getOwnerDocument() Document document = root.getOwnerDocument()
Node dependencies = root.appendChild(document.createElement('dependencies')) Node dependencies = root.appendChild(document.createElement('dependencies'))
Node dependency = dependencies.appendChild(document.createElement('dependency')) Node dependency = dependencies.appendChild(document.createElement('dependency'))
Node groupId = dependency.appendChild(document.createElement('groupId')) Node groupId = dependency.appendChild(document.createElement('groupId'))
groupId.setTextContent('net.fabricmc') groupId.setTextContent('net.fabricmc')
Node artifactId = dependency.appendChild(document.createElement('artifactId')) Node artifactId = dependency.appendChild(document.createElement('artifactId'))
artifactId.setTextContent('fabric-loom') artifactId.setTextContent('fabric-loom')
Node version = dependency.appendChild(document.createElement('version')) Node version = dependency.appendChild(document.createElement('version'))
version.setTextContent(baseVersion + '-SNAPSHOT') version.setTextContent(project.version)
}) })
}
} }
} }
} }

View File

@@ -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<Project> {
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));
}
}

View File

@@ -50,6 +50,7 @@ import net.fabricmc.loom.task.RemapTaskConfiguration;
import net.fabricmc.loom.util.LibraryLocationLogger; import net.fabricmc.loom.util.LibraryLocationLogger;
public class LoomGradlePlugin implements Plugin<PluginAware> { public class LoomGradlePlugin implements Plugin<PluginAware> {
public static final String NAME = "fabric-loom";
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); 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"); 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<PluginAware> {
for (Class<? extends Runnable> jobClass : SETUP_JOBS) { for (Class<? extends Runnable> jobClass : SETUP_JOBS) {
project.getObjects().newInstance(jobClass).run(); project.getObjects().newInstance(jobClass).run();
} }
project.apply(Map.of("plugin", LoomCompanionGradlePlugin.NAME));
} }
} }

View File

@@ -24,6 +24,10 @@
package net.fabricmc.loom.api; package net.fabricmc.loom.api;
import java.io.File;
import java.util.List;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import org.gradle.api.Named; import org.gradle.api.Named;
@@ -35,6 +39,8 @@ import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.jetbrains.annotations.ApiStatus; 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.SourceSetHelper;
import net.fabricmc.loom.util.gradle.SourceSetReference; import net.fabricmc.loom.util.gradle.SourceSetReference;
@@ -49,7 +55,7 @@ public abstract class ModSettings implements Named {
@Inject @Inject
public ModSettings() { public ModSettings() {
getModSourceSets().finalizeValueOnRead(); getExternalGroups().finalizeValueOnRead();
getModFiles().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. * 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) { 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. * 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 * @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) { 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.
*
* <p>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<File> 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. * List of {@link ExternalClasspathGroup} that will later be resolved to populate the classpath groups from another Gradle project.
* Use the {@link ModSettings#sourceSet} methods to add to this.
*/ */
@ApiStatus.Internal @ApiStatus.Internal
public abstract ListProperty<SourceSetReference> getModSourceSets(); public abstract ListProperty<ExternalClasspathGroup> getExternalGroups();
@Inject @Inject
public abstract Project getProject(); public abstract Project getProject();
@@ -127,4 +160,12 @@ public abstract class ModSettings implements Named {
public String toString() { public String toString() {
return "ModSettings '" + getName() + "'"; return "ModSettings '" + getName() + "'";
} }
private void ensureCompanion(Project project) {
if (project == getProject()) {
return;
}
project.apply(Map.of("plugin", LoomCompanionGradlePlugin.NAME));
}
} }

View File

@@ -26,7 +26,6 @@ package net.fabricmc.loom.configuration;
import static net.fabricmc.loom.util.Constants.Configurations; import static net.fabricmc.loom.util.Constants.Configurations;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -36,12 +35,13 @@ import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.GradleException; import org.gradle.api.GradleException;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging; import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.JavaPlugin; 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.IntermediaryMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider;
import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.extension.MixinExtension;
import net.fabricmc.loom.task.service.ClasspathGroupService;
import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.ProcessUtil; 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 -> { getProject().getTasks().named(JavaPlugin.TEST_TASK_NAME, Test.class, test -> {
String classPathGroups = extension.getMods().stream() test.getInputs().property("LoomClassPathGroups", ClasspathGroupService.create(getProject()));
.map(modSettings -> test.doFirst(new Action<Task>() {
SourceSetHelper.getClasspath(modSettings, getProject()).stream() @Override
.map(File::getAbsolutePath) public void execute(Task task) {
.collect(Collectors.joining(File.pathSeparator)) try (ScopedServiceFactory serviceFactory = new ScopedServiceFactory()) {
) var options = (ClasspathGroupService.Options) task.getInputs().getProperties().get("LoomClassPathGroups");
.collect(Collectors.joining(File.pathSeparator+File.pathSeparator));; 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);
}
}
});
}); });
} }

View File

@@ -175,7 +175,7 @@ public abstract class LoomConfigurations implements Runnable {
getConfigurations().getByName(a, configuration -> configuration.extendsFrom(getConfigurations().getByName(b))); getConfigurations().getByName(a, configuration -> configuration.extendsFrom(getConfigurations().getByName(b)));
} }
enum Role { public enum Role {
NONE(false, false), NONE(false, false),
CONSUMABLE(true, false), CONSUMABLE(true, false),
RESOLVABLE(false, true); RESOLVABLE(false, true);
@@ -188,7 +188,7 @@ public abstract class LoomConfigurations implements Runnable {
this.canBeResolved = canBeResolved; this.canBeResolved = canBeResolved;
} }
void apply(Configuration configuration) { public void apply(Configuration configuration) {
configuration.setCanBeConsumed(canBeConsumed); configuration.setCanBeConsumed(canBeConsumed);
configuration.setCanBeResolved(canBeResolved); configuration.setCanBeResolved(canBeResolved);
} }

View File

@@ -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<String> paths, List<ExternalClasspathGroup> externalGroups) implements Serializable {
public static List<ClasspathGroup> fromModSettings(Set<ModSettings> 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<String> paths, List<ExternalClasspathGroup> externalGroups) {
this.paths = new ArrayList<>(paths);
this.externalGroups = new ArrayList<>(externalGroups);
}
private static List<String> getPaths(ModSettings modSettings) {
return modSettings.getModFiles()
.getFiles()
.stream()
.map(File::getAbsolutePath)
.toList();
}
}

View File

@@ -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 {
}

View File

@@ -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<String, List<String>> classpaths) implements Serializable {
public static ExternalClasspathGroupDTO createFromProject(Project project) {
SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(project);
Map<String, List<String>> classpaths = new HashMap<>();
for (SourceSet sourceSet : sourceSets) {
SourceSetReference ref = new SourceSetReference(sourceSet, project);
List<File> 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<String, ExternalClasspathGroupDTO> resolveExternal(Set<File> files) {
Map<String, ExternalClasspathGroupDTO> 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<String> getForSourceSet(String sourceSetName) {
return Objects.requireNonNull(classpaths.get(sourceSetName), "No classpath found for source set: " + sourceSetName);
}
}

View File

@@ -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<String> 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<String> getClasspathDtoJson();
RegularFileProperty getOutput();
}
public abstract static class ExportClassPathWorkAction implements WorkAction<ExportClassPathWorkParameters> {
@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);
}
}
}
}

View File

@@ -42,6 +42,7 @@ import org.gradle.api.logging.configuration.ConsoleOutput;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction; 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.MinecraftVersionMeta;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraftProvider;
import net.fabricmc.loom.task.AbstractLoomTask; 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 { public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
@Input @Input
@@ -69,10 +71,6 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
@Input @Input
protected abstract Property<Boolean> getANSISupportedIDE(); protected abstract Property<Boolean> getANSISupportedIDE();
@Input
@Optional
protected abstract Property<String> getClassPathGroups();
@Input @Input
protected abstract Property<String> getLog4jConfigPaths(); protected abstract Property<String> getLog4jConfigPaths();
@@ -96,16 +94,16 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
@OutputFile @OutputFile
protected abstract RegularFileProperty getDevLauncherConfig(); protected abstract RegularFileProperty getDevLauncherConfig();
@Nested
protected abstract Property<ClasspathGroupService.Options> getClasspathGroupOptions();
public GenerateDLIConfigTask() { public GenerateDLIConfigTask() {
getVersionInfoJson().set(LoomGradlePlugin.GSON.toJson(getExtension().getMinecraftProvider().getVersionInfo())); getVersionInfoJson().set(LoomGradlePlugin.GSON.toJson(getExtension().getMinecraftProvider().getVersionInfo()));
getMinecraftVersion().set(getExtension().getMinecraftProvider().minecraftVersion()); getMinecraftVersion().set(getExtension().getMinecraftProvider().minecraftVersion());
getSplitSourceSets().set(getExtension().areEnvironmentSourceSetsSplit()); getSplitSourceSets().set(getExtension().areEnvironmentSourceSetsSplit());
getANSISupportedIDE().set(ansiSupportedIde(getProject())); getANSISupportedIDE().set(ansiSupportedIde(getProject()));
getPlainConsole().set(getProject().getGradle().getStartParameter().getConsoleOutput() == ConsoleOutput.Plain); getPlainConsole().set(getProject().getGradle().getStartParameter().getConsoleOutput() == ConsoleOutput.Plain);
getClasspathGroupOptions().set(ClasspathGroupService.create(getProject()));
if (!getExtension().getMods().isEmpty()) {
getClassPathGroups().set(buildClassPathGroups(getProject()));
}
getLog4jConfigPaths().set(getAllLog4JConfigFiles(getProject())); getLog4jConfigPaths().set(getAllLog4JConfigFiles(getProject()));
@@ -152,8 +150,12 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
launchConfig.property("fabric.gameJarPath", getCommonGameJarPath().get()); launchConfig.property("fabric.gameJarPath", getCommonGameJarPath().get());
} }
if (getClassPathGroups().isPresent()) { try (ScopedServiceFactory serviceFactory = new ScopedServiceFactory()) {
launchConfig.property("fabric.classPathGroups", getClassPathGroups().get()); 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. //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) { private static boolean ansiSupportedIde(Project project) {
File rootDir = project.getRootDir(); File rootDir = project.getRootDir();
return new File(rootDir, ".vscode").exists() return new File(rootDir, ".vscode").exists()

View File

@@ -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<ClasspathGroupService.Options> {
public static ServiceType<Options, ClasspathGroupService> TYPE = new ServiceType<Options, ClasspathGroupService>(Options.class, ClasspathGroupService.class);
public interface Options extends Service.Options {
@Input
@Optional
ListProperty<ClasspathGroup> getClasspathGroups();
@InputFiles
@Optional
ConfigurableFileCollection getExternalClasspathGroups();
}
public static Provider<Options> create(Project project) {
return TYPE.create(project, options -> {
LoomGradleExtension extension = LoomGradleExtension.get(project);
NamedDomainObjectContainer<ModSettings> modSettings = extension.getMods();
if (modSettings.isEmpty()) {
return;
}
options.getClasspathGroups().set(ClasspathGroup.fromModSettings(modSettings));
if (!hasExternalClasspathGroups(modSettings)) {
return;
}
List<Dependency> externalDependencies = getExternalDependencies(project, modSettings);
Configuration externalClasspathGroups = project.getConfigurations().detachedConfiguration(externalDependencies.toArray(new Dependency[0]));
options.getExternalClasspathGroups().from(externalClasspathGroups);
});
}
private static boolean hasExternalClasspathGroups(Set<ModSettings> modSettings) {
return modSettings.stream()
.anyMatch(s ->
s.getExternalGroups().isPresent()
&& !s.getExternalGroups().get().isEmpty()
);
}
private static List<Dependency> getExternalDependencies(Project project, Set<ModSettings> modSettings) {
List<String> requiredProjects = modSettings.stream()
.flatMap(s -> s.getExternalGroups().get().stream())
.map(ExternalClasspathGroup::projectPath)
.distinct()
.toList();
List<Dependency> 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<Map<String, ExternalClasspathGroupDTO>> externalClasspathGroups = Lazy.of(() -> ExternalClasspathGroupDTO.resolveExternal(getOptions().getExternalClasspathGroups().getFiles()));
public ClasspathGroupService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
public List<File> getClasspath(ClasspathGroup classpathGroup) {
final List<String> 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();
}
}

View File

@@ -90,6 +90,10 @@ public class Constants {
* Mods to be used by {@link net.fabricmc.loom.task.prod.AbstractProductionRunTask} tasks by default. * Mods to be used by {@link net.fabricmc.loom.task.prod.AbstractProductionRunTask} tasks by default.
*/ */
public static final String PRODUCTION_RUNTIME_MODS = "productionRuntimeMods"; 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() { private Configurations() {
} }
@@ -125,6 +129,7 @@ public class Constants {
public static final class Task { public static final class Task {
public static final String PROCESS_INCLUDE_JARS = "processIncludeJars"; public static final String PROCESS_INCLUDE_JARS = "processIncludeJars";
public static final String EXPORT_CLASSPATH = "exportClasspath";
private Task() { private Task() {
} }

View File

@@ -33,6 +33,7 @@ import org.gradle.api.invocation.Gradle;
import org.gradle.api.provider.Provider; import org.gradle.api.provider.Provider;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin;
public final class GradleUtils { public final class GradleUtils {
private GradleUtils() { private GradleUtils() {
@@ -59,7 +60,7 @@ public final class GradleUtils {
} }
public static boolean isLoomProject(Project project) { public static boolean isLoomProject(Project project) {
return project.getPluginManager().hasPlugin("fabric-loom"); return project.getPluginManager().hasPlugin(LoomGradlePlugin.NAME);
} }
public static Provider<Boolean> getBooleanPropertyProvider(Project project, String key) { public static Provider<Boolean> getBooleanPropertyProvider(Project project, String key) {

View File

@@ -52,7 +52,6 @@ import org.jetbrains.annotations.VisibleForTesting;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.ModSettings;
import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils;
import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Constants;
@@ -113,18 +112,8 @@ public final class SourceSetHelper {
return it.hasNext() ? it.next().getProject() : null; return it.hasNext() ? it.next().getProject() : null;
} }
public static List<File> getClasspath(ModSettings modSettings, Project project) { public static List<File> getClasspath(SourceSetReference reference) {
final List<File> files = new ArrayList<>(); final Project project = reference.project();
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<File> getClasspath(SourceSetReference reference, Project project) {
final List<File> classpath = getGradleClasspath(reference, project); final List<File> classpath = getGradleClasspath(reference, project);
classpath.addAll(getIdeaClasspath(reference, project)); classpath.addAll(getIdeaClasspath(reference, project));

View File

@@ -57,4 +57,19 @@ class MultiProjectTest extends Specification implements GradleProjectTestTrait {
where: where:
version << STANDARD_TEST_VERSIONS 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
}
} }

View File

@@ -5,6 +5,8 @@ plugins {
} }
allprojects { allprojects {
if (it.path == ":javalib") return;
apply plugin: "fabric-loom" apply plugin: "fabric-loom"
version = "1.0.0" version = "1.0.0"
@@ -42,13 +44,14 @@ allprojects {
loom { loom {
mods { mods {
core { core {
sourceSet project(':core').sourceSets.main sourceSet("main", ":core")
} }
example { example {
sourceSet project(':example').sourceSets.main sourceSet("main", ":example")
} }
root { root {
sourceSet sourceSets.main sourceSet sourceSets.main
sourceSet("main", ":javalib")
} }
} }
} }

View File

@@ -0,0 +1,4 @@
plugins {
id 'java-library'
id 'net.fabricmc.fabric-loom-companion'
}

View File

@@ -1,4 +1,5 @@
rootProject.name = "fabric-example-mod" rootProject.name = "fabric-example-mod"
include 'core' include 'core'
include 'example' include 'example'
include 'javalib'