Debof spec context (#1424)

* Ahhhhh

* Injected interfaces working

* Access wideners working
This commit is contained in:
modmuss
2025-11-07 19:04:50 +00:00
committed by GitHub
parent 48f7aa66fb
commit e50fce87cc
20 changed files with 705 additions and 113 deletions

View File

@@ -357,6 +357,8 @@ tasks.register('writeActionsTestMatrix') {
def className = it.name.replace(".groovy", "")
testMatrix.add("net.fabricmc.loom.test.integration.${className}")
} else if (it.name.endsWith("noRemap")) {
testMatrix.add("net.fabricmc.loom.test.integration.noRemap.*")
}
}

View File

@@ -42,4 +42,6 @@ public interface ProcessorContext {
LazyCloseable<TinyRemapper> createRemapper(MappingsNamespace from, MappingsNamespace to);
MemoryMappingTree getMappings();
boolean disableObfuscation();
}

View File

@@ -24,12 +24,20 @@
package net.fabricmc.loom.api.processor;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import net.fabricmc.loom.configuration.processors.speccontext.ProjectView;
import net.fabricmc.loom.util.fmj.FabricModJson;
public interface SpecContext {
/**
* Returns a list of all the external mods that this project depends on regardless of configuration.
*/
List<FabricModJson> modDependencies();
List<FabricModJson> localMods();
@@ -47,4 +55,22 @@ public interface SpecContext {
default List<FabricModJson> allMods() {
return Stream.concat(modDependencies().stream(), localMods().stream()).toList();
}
// Returns all of the loom projects that are depended on in the main sourceset
// TODO make project isolation aware
static Stream<Project> getDependentProjects(ProjectView projectView) {
final Stream<Project> runtimeProjects = projectView.getLoomProjectDependencies(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
final Stream<Project> compileProjects = projectView.getLoomProjectDependencies(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
return Stream.concat(runtimeProjects, compileProjects)
.distinct();
}
// Sort to ensure stable caching
static List<FabricModJson> distinctSorted(List<FabricModJson> mods) {
return mods.stream()
.distinct()
.sorted(Comparator.comparing(FabricModJson::getId))
.toList();
}
}

View File

@@ -45,4 +45,6 @@ public interface AccessWidenerEntry {
String getSortKey();
void read(ClassTweakerVisitor visitor, LazyCloseable<TinyRemapper> remapper) throws IOException;
void readOfficial(ClassTweakerVisitor visitor) throws IOException;
}

View File

@@ -129,18 +129,31 @@ public class AccessWidenerJarProcessor implements MinecraftJarProcessor<AccessWi
@Override
public void processJar(Path jar, AccessWidenerJarProcessor.Spec spec, ProcessorContext context) throws IOException {
ClassTweaker classTweaker = getClassTweaker(spec, context);
AccessWidenerTransformer transformer = new AccessWidenerTransformer(classTweaker);
transformer.apply(jar);
}
private ClassTweaker getClassTweaker(AccessWidenerJarProcessor.Spec spec, ProcessorContext context) throws IOException {
final List<AccessWidenerEntry> accessWideners = spec.accessWidenersForContext(context);
final var accessWidener = ClassTweaker.newInstance();
final var classTweaker = ClassTweaker.newInstance();
if (context.disableObfuscation()) {
for (AccessWidenerEntry widener : accessWideners) {
widener.readOfficial(classTweaker);
}
return classTweaker;
}
try (LazyCloseable<TinyRemapper> remapper = context.createRemapper(MappingsNamespace.INTERMEDIARY, MappingsNamespace.NAMED)) {
for (AccessWidenerEntry widener : accessWideners) {
widener.read(accessWidener, remapper);
widener.read(classTweaker, remapper);
}
}
AccessWidenerTransformer transformer = new AccessWidenerTransformer(accessWidener);
transformer.apply(jar);
return classTweaker;
}
@Override

View File

@@ -32,6 +32,7 @@ import org.jetbrains.annotations.Nullable;
import net.fabricmc.classtweaker.api.ClassTweakerReader;
import net.fabricmc.classtweaker.api.visitor.ClassTweakerVisitor;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.LazyCloseable;
import net.fabricmc.loom.util.fmj.ModEnvironment;
@@ -48,6 +49,19 @@ public record LocalAccessWidenerEntry(Path path, String hash) implements AccessW
reader.read(Files.readAllBytes(path), null);
}
@Override
public void readOfficial(ClassTweakerVisitor visitor) throws IOException {
final byte[] data = Files.readAllBytes(path);
final ClassTweakerReader.Header header = ClassTweakerReader.readHeader(data);
if (!header.getNamespace().equals(MappingsNamespace.OFFICIAL.toString())) {
throw new IOException("Expected official namespace for access widener entry, found: " + header.getNamespace());
}
var reader = ClassTweakerReader.create(visitor);
reader.read(data, null);
}
@Override
public ModEnvironment environment() {
return ModEnvironment.UNIVERSAL;

View File

@@ -85,6 +85,24 @@ public record ModAccessWidenerEntry(FabricModJson mod, String path, ModEnvironme
reader.read(data, mod.getId());
}
@Override
public void readOfficial(ClassTweakerVisitor visitor) throws IOException {
if (transitiveOnly) {
// Filter for only transitive rules
visitor = new TransitiveOnlyFilter(visitor);
}
final byte[] data = readRaw();
final ClassTweakerReader.Header header = ClassTweakerReader.readHeader(data);
if (!header.getNamespace().equals(MappingsNamespace.OFFICIAL.toString())) {
throw new IOException("Expected official namespace for access widener entry, found: " + header.getNamespace());
}
var reader = ClassTweakerReader.create(visitor);
reader.read(data, mod.getId());
}
private static ClassTweakerRemapperVisitor getRemapper(ClassTweakerVisitor visitor, TinyRemapper tinyRemapper) {
return new ClassTweakerRemapperVisitor(
visitor,

View File

@@ -111,6 +111,20 @@ public abstract class InterfaceInjectionProcessor implements MinecraftJarProcess
@Override
public void processJar(Path jar, Spec spec, ProcessorContext context) throws IOException {
List<InjectedInterface> injectedInterfaces = getInjectedInterfaces(spec, context);
try {
ZipUtils.transform(jar, getTransformers(injectedInterfaces));
} catch (IOException e) {
throw new RuntimeException("Failed to apply interface injections to " + jar, e);
}
}
private List<InjectedInterface> getInjectedInterfaces(Spec spec, ProcessorContext context) throws IOException {
if (context.disableObfuscation()) {
return spec.injectedInterfaces();
}
// Remap from intermediary->named
final MemoryMappingTree mappings = context.getMappings();
final int intermediaryIndex = mappings.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString());
@@ -128,11 +142,7 @@ public abstract class InterfaceInjectionProcessor implements MinecraftJarProcess
tinyRemapper.get().getEnvironment().getRemapper()
))
.toList();
try {
ZipUtils.transform(jar, getTransformers(remappedInjectedInterfaces));
} catch (IOException e) {
throw new RuntimeException("Failed to apply interface injections to " + jar, e);
}
return remappedInjectedInterfaces;
}
}

View File

@@ -45,6 +45,8 @@ import net.fabricmc.loom.api.processor.MappingProcessorContext;
import net.fabricmc.loom.api.processor.MinecraftJarProcessor;
import net.fabricmc.loom.api.processor.ProcessorContext;
import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.configuration.processors.speccontext.DeobfSpecContext;
import net.fabricmc.loom.configuration.processors.speccontext.RemappedSpecContext;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
@@ -69,9 +71,9 @@ public final class MinecraftJarProcessorManager {
SpecContext specContext;
if (extension.disableObfuscation()) {
specContext = SpecContextDebofImpl.create();
specContext = DeobfSpecContext.create(project);
} else {
specContext = SpecContextRemappedImpl.create(project);
specContext = RemappedSpecContext.create(project);
}
return MinecraftJarProcessorManager.create(processors, specContext);

View File

@@ -65,4 +65,9 @@ public record ProcessorContextImpl(ConfigContext configContext, MinecraftJar min
LoomGradleExtension extension = LoomGradleExtension.get(configContext().project());
return extension.getMappingConfiguration().getMappingsService(configContext().project(), configContext().serviceFactory()).getMappingTree();
}
@Override
public boolean disableObfuscation() {
return configContext().extension().disableObfuscation();
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.processors.speccontext;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.tasks.SourceSet;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.LoomConfigurations;
import net.fabricmc.loom.util.Strings;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
/**
* This mess is created out of the need to resolve the compile/runtime dependencies, without using the actual runtime/compile classpath.
* As we need to add the minecraft jar to it later (based on the compile/runtime dependencies).
*/
public record DebofConfiguration(String name, List<Function<SourceSet, String>> configurationFunctions) {
public static final DebofConfiguration COMPILE = new DebofConfiguration("compile", List.of(
SourceSet::getImplementationConfigurationName,
SourceSet::getApiConfigurationName,
SourceSet::getCompileOnlyConfigurationName
));
public static final DebofConfiguration RUNTIME = new DebofConfiguration("runtime", List.of(
SourceSet::getImplementationConfigurationName,
SourceSet::getApiConfigurationName,
SourceSet::getRuntimeOnlyConfigurationName
));
public static final List<DebofConfiguration> ALL = List.of(COMPILE, RUNTIME);
public Configuration getConfiguration(Project project, TargetSourceSet targetSourceSet) {
return project.getConfigurations().getByName(resolveConfigurationName(targetSourceSet));
}
public List<Configuration> getConfigurations(Project project) {
ConfigurationContainer configurations = project.getConfigurations();
return TargetSourceSet.applicable(project).stream()
.map(this::resolveConfigurationName)
.map(configurations::getByName)
.toList();
}
private String resolveConfigurationName(TargetSourceSet targetSourceSet) {
return "%s%sDependencyResolve".formatted(targetSourceSet.name, Strings.capitalize(name()));
}
public static void create(Project project) {
for (DebofConfiguration debofConfiguration : ALL) {
debofConfiguration.createResolveConfiguration(project);
}
}
public void createResolveConfiguration(Project project) {
ConfigurationContainer configurations = project.getConfigurations();
for (TargetSourceSet target : TargetSourceSet.applicable(project)) {
SourceSet sourceSet = target.getSourceSet(project);
configurations.register(resolveConfigurationName(target), c -> {
LoomConfigurations.Role.RESOLVABLE.apply(c);
for (Function<SourceSet, String> configProvider : configurationFunctions()) {
Configuration sourceConfig = configurations.getByName(configProvider.apply(sourceSet));
c.extendsFrom(sourceConfig);
}
});
}
}
public enum TargetSourceSet {
MAIN(SourceSet.MAIN_SOURCE_SET_NAME, e -> Boolean.TRUE),
CLIENT("client", LoomGradleExtension::areEnvironmentSourceSetsSplit);
private final String name;
private final Function<LoomGradleExtension, Boolean> enabled;
TargetSourceSet(String name, Function<LoomGradleExtension, Boolean> enabled) {
this.name = name;
this.enabled = enabled;
}
public SourceSet getSourceSet(Project project) {
return SourceSetHelper.getSourceSetByName(this.name, project);
}
public static List<TargetSourceSet> applicable(Project project) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
return Arrays.stream(values())
.filter(targetSourceSet -> targetSourceSet.enabled.apply(extension))
.toList();
}
}
}

View File

@@ -22,27 +22,39 @@
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.processors;
package net.fabricmc.loom.configuration.processors.speccontext;
import java.util.List;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.util.fmj.FabricModJson;
public interface DeobfProjectView extends ProjectView {
FileCollection getDependencies(DebofConfiguration debofConfiguration, DebofConfiguration.TargetSourceSet targetSourceSet);
// TODO debof - fixme
public record SpecContextDebofImpl(List<FabricModJson> modDependencies,
List<FabricModJson> localMods) implements SpecContext {
public static SpecContext create() {
return new SpecContextDebofImpl(List.of(), List.of());
}
FileCollection getFullClasspath();
@Override
public List<FabricModJson> modDependenciesCompileRuntime() {
return List.of();
}
class Impl extends AbstractProjectView implements DeobfProjectView {
protected Impl(Project project) {
super(project);
}
@Override
public List<FabricModJson> modDependenciesCompileRuntimeClient() {
return List.of();
@Override
public FileCollection getDependencies(DebofConfiguration debofConfiguration, DebofConfiguration.TargetSourceSet targetSourceSet) {
return debofConfiguration.getConfiguration(project, targetSourceSet);
}
@Override
public FileCollection getFullClasspath() {
ConfigurableFileCollection classpath = project.files();
for (DebofConfiguration debofConfiguration : DebofConfiguration.ALL) {
for (Configuration configuration : debofConfiguration.getConfigurations(project)) {
classpath.from(configuration);
}
}
return classpath;
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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.processors.speccontext;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.util.AsyncCache;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
import net.fabricmc.loom.util.fmj.FabricModJsonHelpers;
public record DeobfSpecContext(List<FabricModJson> modDependencies,
List<FabricModJson> localMods,
// Mods that are in the following configurations: [runtimeClasspath, compileClasspath] or [runtimeClientClasspath, compileClientClasspath]
// These are mods that will be used to transform both the client and server jars
List<FabricModJson> modDependenciesCompileRuntime,
// Here we want mods that are ONLY in [runtimeClientClasspath, compileClientClasspath] and not [runtimeClasspath, compileClasspath]
// These mods will be excluded from transforming the server jar
List<FabricModJson> modDependenciesCompileRuntimeClient
) implements SpecContext {
public static DeobfSpecContext create(Project project) {
DebofConfiguration.create(project);
return create(new DeobfProjectView.Impl(project));
}
public static DeobfSpecContext create(DeobfProjectView projectView) {
AsyncCache<List<FabricModJson>> fmjCache = new AsyncCache<>();
List<FabricModJson> dependentMods = getDependentMods(projectView, fmjCache);
Map<String, FabricModJson> mods = dependentMods.stream()
.collect(HashMap::new, (map, mod) -> map.put(mod.getId(), mod), Map::putAll);
FileCollection mainRuntimeClasspath = projectView.getDependencies(DebofConfiguration.RUNTIME, DebofConfiguration.TargetSourceSet.MAIN);
FileCollection mainCompileClasspath = projectView.getDependencies(DebofConfiguration.COMPILE, DebofConfiguration.TargetSourceSet.MAIN);
// All mods in both [runtimeClasspath, compileClasspath]
Set<String> mainTransformingModIds = common(
getModIds(mainRuntimeClasspath, fmjCache),
getModIds(mainCompileClasspath, fmjCache)
);
// All mods in both [runtimeClientClasspath, compileClientClasspath]
Set<String> clientTransformingModIds;
if (projectView.areEnvironmentSourceSetsSplit()) {
FileCollection clientRuntimeClasspath = projectView.getDependencies(DebofConfiguration.RUNTIME, DebofConfiguration.TargetSourceSet.CLIENT);
FileCollection clientCompileClasspath = projectView.getDependencies(DebofConfiguration.COMPILE, DebofConfiguration.TargetSourceSet.CLIENT);
clientTransformingModIds = common(
getModIds(clientRuntimeClasspath, fmjCache),
getModIds(clientCompileClasspath, fmjCache)
);
} else {
clientTransformingModIds = Set.of();
}
return new DeobfSpecContext(
dependentMods,
projectView.getMods(),
getMods(mods, combine(mainTransformingModIds, clientTransformingModIds)),
getMods(mods, onlyInLeft(clientTransformingModIds, mainTransformingModIds))
);
}
// Returns a list of all the mods that the current project depends on
private static List<FabricModJson> getDependentMods(DeobfProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
var futures = new ArrayList<CompletableFuture<List<FabricModJson>>>();
Set<File> artifacts = projectView.getFullClasspath().getFiles();
for (File artifact : artifacts) {
futures.add(fmjCache.get(artifact.toPath().toAbsolutePath().toString(), () -> {
return FabricModJsonFactory.createFromZipOptional(artifact.toPath())
.map(List::of)
.orElseGet(List::of);
}));
}
if (!projectView.disableProjectDependantMods()) {
// Add all the dependent projects
for (Project dependentProject : SpecContext.getDependentProjects(projectView).toList()) {
futures.add(fmjCache.get(dependentProject.getPath(), () -> FabricModJsonHelpers.getModsInProject(dependentProject)));
}
}
return SpecContext.distinctSorted(AsyncCache.joinList(futures));
}
// Returns a list of mod ids in a given configuration
private static Set<String> getModIds(FileCollection configuration, AsyncCache<List<FabricModJson>> fmjCache) {
var futures = new ArrayList<CompletableFuture<List<FabricModJson>>>();
Set<File> artifacts = configuration.getFiles();
for (File artifact : artifacts) {
futures.add(fmjCache.get(artifact.toPath().toAbsolutePath().toString(), () -> {
return FabricModJsonFactory.createFromZipOptional(artifact.toPath())
.map(List::of)
.orElseGet(List::of);
}));
}
return SpecContext.distinctSorted(AsyncCache.joinList(futures)).stream()
.map(FabricModJson::getId)
.collect(HashSet::new, Set::add, Set::addAll);
}
private static List<FabricModJson> getMods(Map<String, FabricModJson> mods, Set<String> ids) {
List<FabricModJson> result = new ArrayList<>();
for (String id : ids) {
result.add(Objects.requireNonNull(mods.get(id), "Could not find mod with id: " + id));
}
return result;
}
private static Set<String> common(Set<String> a, Set<String> b) {
Set<String> copy = new HashSet<>(a);
copy.retainAll(b);
return copy;
}
private static Set<String> combine(Set<String> a, Set<String> b) {
Set<String> copy = new HashSet<>(a);
copy.addAll(b);
return copy;
}
private static Set<String> onlyInLeft(Set<String> left, Set<String> right) {
Set<String> copy = new HashSet<>(left);
copy.removeAll(right);
return copy;
}
}

View File

@@ -22,12 +22,9 @@
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.processors;
package net.fabricmc.loom.configuration.processors.speccontext;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.gradle.api.Project;
@@ -36,25 +33,23 @@ import org.gradle.api.artifacts.ProjectDependency;
import org.gradle.api.attributes.Usage;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonHelpers;
import net.fabricmc.loom.util.gradle.GradleUtils;
// Used to abstract out the Gradle API usage to ease unit testing.
public interface SpecContextProjectView {
LoomGradleExtension extension();
public interface ProjectView {
// Returns a list of Loom Projects found in the specified Configuration
Stream<Project> getLoomProjectDependencies(String name);
Function<RemapConfigurationSettings, Stream<Path>> resolveArtifacts(ArtifactUsage artifactUsage);
// Returns the mods defined in the current project
List<FabricModJson> getMods();
boolean disableProjectDependantMods();
boolean areEnvironmentSourceSetsSplit();
enum ArtifactUsage {
RUNTIME(Usage.JAVA_RUNTIME),
COMPILE(Usage.JAVA_API);
@@ -64,9 +59,21 @@ public interface SpecContextProjectView {
ArtifactUsage(String gradleUsage) {
this.gradleUsage = gradleUsage;
}
public String getGradleUsage() {
return gradleUsage;
}
}
record Impl(Project project, LoomGradleExtension extension) implements SpecContextProjectView {
abstract class AbstractProjectView implements ProjectView {
protected final Project project;
protected final LoomGradleExtension extension;
protected AbstractProjectView(Project project) {
this.project = project;
this.extension = LoomGradleExtension.get(project);
}
@Override
public Stream<Project> getLoomProjectDependencies(String name) {
final Configuration configuration = project.getConfigurations().getByName(name);
@@ -77,18 +84,6 @@ public interface SpecContextProjectView {
.filter(GradleUtils::isLoomProject);
}
@Override
public Function<RemapConfigurationSettings, Stream<Path>> resolveArtifacts(ArtifactUsage artifactUsage) {
final Usage usage = project.getObjects().named(Usage.class, artifactUsage.gradleUsage);
return settings -> {
final Configuration configuration = settings.getSourceConfiguration().get().copyRecursive();
configuration.setCanBeConsumed(false);
configuration.attributes(attributes -> attributes.attribute(Usage.USAGE_ATTRIBUTE, usage));
return configuration.resolve().stream().map(File::toPath);
};
}
@Override
public List<FabricModJson> getMods() {
return FabricModJsonHelpers.getModsInProject(project);
@@ -101,5 +96,10 @@ public interface SpecContextProjectView {
return extension.isProjectIsolationActive()
|| GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS);
}
@Override
public boolean areEnvironmentSourceSetsSplit() {
return extension.areEnvironmentSourceSetsSplit();
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.processors.speccontext;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.gradle.api.NamedDomainObjectList;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Usage;
import net.fabricmc.loom.api.RemapConfigurationSettings;
public interface RemappedProjectView extends ProjectView {
Function<RemapConfigurationSettings, Stream<Path>> resolveArtifacts(ArtifactUsage artifactUsage);
NamedDomainObjectList<RemapConfigurationSettings> getRemapConfigurations();
List<RemapConfigurationSettings> getCompileRemapConfigurations();
List<RemapConfigurationSettings> getRuntimeRemapConfigurations();
class Impl extends AbstractProjectView implements RemappedProjectView {
public Impl(Project project) {
super(project);
}
@Override
public Function<RemapConfigurationSettings, Stream<Path>> resolveArtifacts(ArtifactUsage artifactUsage) {
final Usage usage = project.getObjects().named(Usage.class, artifactUsage.getGradleUsage());
return settings -> {
final Configuration configuration = settings.getSourceConfiguration().get().copyRecursive();
configuration.setCanBeConsumed(false);
configuration.attributes(attributes -> attributes.attribute(Usage.USAGE_ATTRIBUTE, usage));
return configuration.resolve().stream().map(File::toPath);
};
}
@Override
public NamedDomainObjectList<RemapConfigurationSettings> getRemapConfigurations() {
return extension.getRemapConfigurations();
}
@Override
public List<RemapConfigurationSettings> getCompileRemapConfigurations() {
return extension.getCompileRemapConfigurations();
}
@Override
public List<RemapConfigurationSettings> getRuntimeRemapConfigurations() {
return extension.getRuntimeRemapConfigurations();
}
}
}

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.processors;
package net.fabricmc.loom.configuration.processors.speccontext;
import java.io.File;
import java.nio.file.Path;
@@ -42,7 +42,6 @@ import org.gradle.api.plugins.JavaPlugin;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
@@ -56,29 +55,29 @@ import net.fabricmc.loom.util.fmj.FabricModJsonHelpers;
* @param localMods Mods found in the current project.
* @param compileRuntimeMods Dependent mods found in both the compile and runtime classpath.
*/
public record SpecContextRemappedImpl(
public record RemappedSpecContext(
List<FabricModJson> modDependencies,
List<FabricModJson> localMods,
List<ModHolder> compileRuntimeMods) implements SpecContext {
public static SpecContextRemappedImpl create(Project project) {
return create(new SpecContextProjectView.Impl(project, LoomGradleExtension.get(project)));
public static RemappedSpecContext create(Project project) {
return create(new RemappedProjectView.Impl(project));
}
@VisibleForTesting
public static SpecContextRemappedImpl create(SpecContextProjectView projectView) {
public static RemappedSpecContext create(RemappedProjectView projectView) {
AsyncCache<List<FabricModJson>> fmjCache = new AsyncCache<>();
return new SpecContextRemappedImpl(
return new RemappedSpecContext(
getDependentMods(projectView, fmjCache),
projectView.getMods(),
getCompileRuntimeMods(projectView, fmjCache)
);
}
// Reruns a list of mods found on both the compile and/or runtime classpaths
private static List<FabricModJson> getDependentMods(SpecContextProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
// Returns a list of mods found on both the compile and/or runtime classpaths
private static List<FabricModJson> getDependentMods(RemappedProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
var futures = new ArrayList<CompletableFuture<List<FabricModJson>>>();
for (RemapConfigurationSettings entry : projectView.extension().getRemapConfigurations()) {
for (RemapConfigurationSettings entry : projectView.getRemapConfigurations()) {
final Set<File> artifacts = entry.getSourceConfiguration().get().resolve();
for (File artifact : artifacts) {
@@ -92,24 +91,16 @@ public record SpecContextRemappedImpl(
if (!projectView.disableProjectDependantMods()) {
// Add all the dependent projects
for (Project dependentProject : getDependentProjects(projectView).toList()) {
for (Project dependentProject : SpecContext.getDependentProjects(projectView).toList()) {
futures.add(fmjCache.get(dependentProject.getPath(), () -> FabricModJsonHelpers.getModsInProject(dependentProject)));
}
}
return distinctSorted(AsyncCache.joinList(futures));
}
private static Stream<Project> getDependentProjects(SpecContextProjectView projectView) {
final Stream<Project> runtimeProjects = projectView.getLoomProjectDependencies(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
final Stream<Project> compileProjects = projectView.getLoomProjectDependencies(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
return Stream.concat(runtimeProjects, compileProjects)
.distinct();
return SpecContext.distinctSorted(AsyncCache.joinList(futures));
}
// Returns a list of mods that are on both to compile and runtime classpath
private static List<ModHolder> getCompileRuntimeMods(SpecContextProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
private static List<ModHolder> getCompileRuntimeMods(RemappedProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(projectView, fmjCache));
for (Project dependentProject : getCompileRuntimeProjectDependencies(projectView).toList()) {
@@ -126,32 +117,32 @@ public record SpecContextRemappedImpl(
}
// Returns a list of jar mods that are found on the compile and runtime remapping configurations
private static List<ModHolder> getCompileRuntimeModsFromRemapConfigs(SpecContextProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
private static List<ModHolder> getCompileRuntimeModsFromRemapConfigs(RemappedProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache) {
// A set of mod ids from all remap configurations that are considered for dependency transforms.
final Set<String> runtimeModIds = getModIds(
SpecContextProjectView.ArtifactUsage.RUNTIME,
ProjectView.ArtifactUsage.RUNTIME,
projectView,
fmjCache,
projectView.extension().getRuntimeRemapConfigurations().stream()
projectView.getRuntimeRemapConfigurations().stream()
.filter(settings -> settings.getApplyDependencyTransforms().get())
);
// A set of mod ids that are found on one or more remap configurations that target the common source set.
// Null when split source sets are not enabled, meaning all mods are common.
final Set<String> commonRuntimeModIds = projectView.extension().areEnvironmentSourceSetsSplit() ? getModIds(
SpecContextProjectView.ArtifactUsage.RUNTIME,
final Set<String> commonRuntimeModIds = projectView.areEnvironmentSourceSetsSplit() ? getModIds(
ProjectView.ArtifactUsage.RUNTIME,
projectView,
fmjCache,
projectView.extension().getRuntimeRemapConfigurations().stream()
projectView.getRuntimeRemapConfigurations().stream()
.filter(settings -> settings.getSourceSet().map(sourceSet -> !sourceSet.getName().equals(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME)).get())
.filter(settings -> settings.getApplyDependencyTransforms().get()))
: null;
Stream<FabricModJson> compileMods = getMods(
SpecContextProjectView.ArtifactUsage.COMPILE,
ProjectView.ArtifactUsage.COMPILE,
projectView,
fmjCache,
projectView.extension().getCompileRemapConfigurations().stream()
projectView.getCompileRemapConfigurations().stream()
.filter(settings -> settings.getApplyDependencyTransforms().get()));
return compileMods
@@ -164,13 +155,13 @@ public record SpecContextRemappedImpl(
.toList();
}
private static Stream<FabricModJson> getMods(SpecContextProjectView.ArtifactUsage artifactUsage, SpecContextProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
private static Stream<FabricModJson> getMods(ProjectView.ArtifactUsage artifactUsage, RemappedProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
return stream.flatMap(projectView.resolveArtifacts(artifactUsage))
.map(modFromZip(fmjCache))
.filter(Objects::nonNull);
}
private static Set<String> getModIds(SpecContextProjectView.ArtifactUsage artifactUsage, SpecContextProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
private static Set<String> getModIds(ProjectView.ArtifactUsage artifactUsage, RemappedProjectView projectView, AsyncCache<List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
return getMods(artifactUsage, projectView, fmjCache, stream)
.map(FabricModJson::getId)
.collect(Collectors.toSet());
@@ -188,7 +179,7 @@ public record SpecContextRemappedImpl(
}
// Returns a list of Loom Projects found in both the runtime and compile classpath
private static Stream<Project> getCompileRuntimeProjectDependencies(SpecContextProjectView projectView) {
private static Stream<Project> getCompileRuntimeProjectDependencies(ProjectView projectView) {
if (projectView.disableProjectDependantMods()) {
return Stream.empty();
}
@@ -200,14 +191,6 @@ public record SpecContextRemappedImpl(
.filter(compileProjects::contains); // Use the intersection of the two configurations.
}
// Sort to ensure stable caching
private static List<FabricModJson> distinctSorted(List<FabricModJson> mods) {
return mods.stream()
.distinct()
.sorted(Comparator.comparing(FabricModJson::getId))
.toList();
}
@Override
public List<FabricModJson> modDependenciesCompileRuntime() {
return compileRuntimeMods.stream()

View File

@@ -0,0 +1,120 @@
/*
* 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.test.integration.noRemap
import java.nio.file.Path
import org.intellij.lang.annotations.Language
import spock.lang.Specification
import spock.lang.TempDir
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import net.fabricmc.loom.test.util.MockMavenServerTrait
import net.fabricmc.loom.util.ZipUtils
import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class DebofDependenciesTest extends Specification implements GradleProjectTestTrait, MockMavenServerTrait {
@TempDir
Path tempDir
@Unroll
def "apply interface injection"() {
setup:
def dep = tempDir.resolve("mod.jar")
ZipUtils.add(dep, "fabric.mod.json", FMJ)
ZipUtils.add(dep, "test.accesswidener", AW)
mavenHelper("loom.test", "test", "1.0.0").copyToMaven(dep, null)
def gradle = gradleProject(project: "minimalBaseNoRemap", version: PRE_RELEASE_GRADLE)
gradle.buildGradle << repositoriesBlock
gradle.buildGradle << '''
dependencies {
minecraft 'com.mojang:minecraft:25w45a_unobfuscated'
implementation 'loom.test:test:1.0.0'
}
'''
def pkg = new File(gradle.projectDir, "src/main/java/example/")
pkg.mkdirs()
new File(pkg, "Test.java").text = Test
new File(pkg, "InjectedInterface.java").text = InjectedInterface
when:
def result = gradle.run(task: "build")
then:
result.task(":build").outcome == SUCCESS
}
@Language("JSON")
private static final String FMJ = """
{
"schemaVersion": 1,
"id": "testmod",
"version": "1",
"name": "Test Mod",
"accessWidener": "test.accesswidener",
"custom": {
"loom:injected_interfaces": {
"net/minecraft/resources/Identifier": ["example/InjectedInterface"]
}
}
}
"""
@Language("Access Widener")
private static final String AW = """
accessWidener\tv2\tofficial
transitive-accessible field net/minecraft/resources/Identifier path Ljava/lang/String;
""".stripIndent().trim()
@Language("JAVA")
private static final String Test = """
package example;
import net.minecraft.resources.Identifier;
public class Test {
public static void main(String[] args) {
Identifier id = Identifier.fromNamespaceAndPath("loom", "test");
id.testCompiles(); // Test iface injection
String path = id.path; // Test AW
}
}
"""
@Language("JAVA")
private static final String InjectedInterface = """
package example;
public interface InjectedInterface {
default void testCompiles() {
}
}
"""
}

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration
package net.fabricmc.loom.test.integration.noRemap
import org.intellij.lang.annotations.Language
import spock.lang.Specification
@@ -33,9 +33,9 @@ import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class NotObfuscatedTest extends Specification implements GradleProjectTestTrait {
class SimpleDebofTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "Not Obfuscated"() {
def "build"() {
setup:
def gradle = gradleProject(project: "minimalBaseNoRemap", version: PRE_RELEASE_GRADLE)
gradle.buildGradle << '''

View File

@@ -36,11 +36,11 @@ import org.gradle.api.artifacts.Configuration
import spock.lang.Specification
import spock.lang.TempDir
import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.loom.api.RemapConfigurationSettings
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec
import net.fabricmc.loom.configuration.processors.SpecContextProjectView
import net.fabricmc.loom.configuration.processors.SpecContextRemappedImpl
import net.fabricmc.loom.configuration.processors.speccontext.ProjectView
import net.fabricmc.loom.configuration.processors.speccontext.RemappedProjectView
import net.fabricmc.loom.configuration.processors.speccontext.RemappedSpecContext
import net.fabricmc.loom.test.util.GradleTestUtil
import net.fabricmc.loom.util.ZipUtils
import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator
@@ -54,8 +54,7 @@ class SpecContextTest extends Specification {
Path tempDir
Project project
LoomGradleExtension extension
SpecContextProjectView projectView
RemappedProjectView projectView
NamedDomainObjectList<RemapConfigurationSettings> remapConfigurations
RemapConfigurationSettings implementation
@@ -67,14 +66,12 @@ class SpecContextTest extends Specification {
void setup() {
project = GradleTestUtil.mockProject()
extension = LoomGradleExtension.get(project)
projectView = mock(SpecContextProjectView.class)
projectView = mock(RemappedProjectView.class)
remapConfigurations = project.getObjects().namedDomainObjectList(RemapConfigurationSettings.class)
when(projectView.extension()).thenReturn(extension)
when(extension.getRemapConfigurations()).thenReturn(remapConfigurations)
when(projectView.resolveArtifacts(SpecContextProjectView.ArtifactUsage.RUNTIME)).thenReturn(resolve(runtimeArtifacts))
when(projectView.resolveArtifacts(SpecContextProjectView.ArtifactUsage.COMPILE)).thenReturn(resolve(apiArtifacts))
when(projectView.getRemapConfigurations()).thenReturn(remapConfigurations)
when(projectView.resolveArtifacts(ProjectView.ArtifactUsage.RUNTIME)).thenReturn(resolve(runtimeArtifacts))
when(projectView.resolveArtifacts(ProjectView.ArtifactUsage.COMPILE)).thenReturn(resolve(apiArtifacts))
implementation = createConfigurationSettings("implementation")
runtimeOnly = createConfigurationSettings("runtimeOnly")
@@ -85,8 +82,8 @@ class SpecContextTest extends Specification {
compileOnly
])
when(extension.getCompileRemapConfigurations()).thenReturn([implementation, compileOnly])
when(extension.getRuntimeRemapConfigurations()).thenReturn([implementation, runtimeOnly])
when(projectView.getCompileRemapConfigurations()).thenReturn([implementation, compileOnly])
when(projectView.getRuntimeRemapConfigurations()).thenReturn([implementation, runtimeOnly])
}
def "Empty"() {
@@ -98,7 +95,7 @@ class SpecContextTest extends Specification {
)
when:
def specContext = SpecContextRemappedImpl.create(projectView)
def specContext = RemappedSpecContext.create(projectView)
then:
specContext.modDependencies().size() == 0
@@ -117,7 +114,7 @@ class SpecContextTest extends Specification {
)
when:
def specContext = SpecContextRemappedImpl.create(projectView)
def specContext = RemappedSpecContext.create(projectView)
then:
specContext.modDependencies().size() == 1
@@ -136,7 +133,7 @@ class SpecContextTest extends Specification {
)
when:
def specContext = SpecContextRemappedImpl.create(projectView)
def specContext = RemappedSpecContext.create(projectView)
then:
specContext.modDependencies().size() == 1
@@ -155,7 +152,7 @@ class SpecContextTest extends Specification {
)
when:
def specContext = SpecContextRemappedImpl.create(projectView)
def specContext = RemappedSpecContext.create(projectView)
then:
specContext.modDependencies().size() == 1
@@ -175,7 +172,7 @@ class SpecContextTest extends Specification {
)
when:
def specContext = SpecContextRemappedImpl.create(projectView)
def specContext = RemappedSpecContext.create(projectView)
then:
specContext.modDependencies().size() == 1

View File

@@ -27,9 +27,11 @@ package net.fabricmc.loom.test.util
import io.javalin.Javalin
import org.apache.commons.io.IOUtils
import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper
trait MockMavenServerTrait {
public final int mavenServerPort = 9876
public final File testMavenDir = File.createTempDir()
public static final File testMavenDir = File.createTempDir()
private Javalin server
@SuppressWarnings('unused')
@@ -87,6 +89,21 @@ trait MockMavenServerTrait {
"${mavenServerPort}"
}
String getRepositoriesBlock() {
"""
repositories {
maven {
url = "http://localhost:${port()}/"
allowInsecureProtocol = true
}
}
"""
}
LocalMavenHelper mavenHelper(String group, String name, String version) {
return new LocalMavenHelper(group, name, version, null, getMavenDirectory().toPath(), null)
}
String getLatestSnapshotVersion(String group, String artifact, String version) {
File file = new File(getMavenDirectory(), "${group.replace('.', '/')}/${artifact}/${version}/maven-metadata.xml")
def root = new groovy.xml.XmlSlurper().parseText(file.text)