Make McpExecutor and GenerateForgePatchedSourcesTask config cache-compatible

- Added ForgeToolService to run tools in tasks
- Added AccessTransformerService to simplify applying ATs and
  to make the tool config cache-compatible
- Split McpExecutor into an execution stage (McpE) and
  a setup stage (McpExecutorBuilder)
- McpExecutor is now a service
- All StepLogic implementations are now services
This commit is contained in:
Juuz
2025-08-18 19:03:03 +03:00
parent 43c6662661
commit 57e853aa13
23 changed files with 1042 additions and 452 deletions

View File

@@ -0,0 +1,155 @@
package dev.architectury.loom.forge.tool;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import dev.architectury.loom.forge.UserdevConfig;
import dev.architectury.loom.util.TempFiles;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.DependencyDownloader;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* A service that executes the access transformer tool.
* The tool information and the AT files are specified in the options.
*/
public final class AccessTransformerService extends Service<AccessTransformerService.Options> {
public static final ServiceType<Options, AccessTransformerService> TYPE = new ServiceType<>(Options.class, AccessTransformerService.class);
public interface Options extends Service.Options {
@InputFiles
ConfigurableFileCollection getAccessTransformers();
@Input
Property<String> getMainClass();
@Classpath
ConfigurableFileCollection getClasspath();
@Nested
Property<ForgeToolService.Options> getToolServiceOptions();
}
public static Provider<Options> createOptions(Project project, Object atFiles) {
return TYPE.create(project, options -> {
LoomVersions accessTransformer = chooseAccessTransformer(project);
String mainClass = accessTransformer.equals(LoomVersions.ACCESS_TRANSFORMERS_NEO)
? "net.neoforged.accesstransformer.cli.TransformerProcessor"
: "net.minecraftforge.accesstransformer.TransformerProcessor";
FileCollection classpath = new DependencyDownloader(project)
.add(accessTransformer.mavenNotation())
.add(LoomVersions.ASM.mavenNotation())
.platform(LoomVersions.ACCESS_TRANSFORMERS_LOG4J_BOM.mavenNotation())
.download();
options.getMainClass().set(mainClass);
options.getAccessTransformers().from(atFiles);
options.getClasspath().from(classpath);
options.getToolServiceOptions().set(ForgeToolService.createOptions(project));
});
}
public static Provider<Options> createOptionsForLoaderAts(Project project, TempFiles tempFiles) {
final Provider<List<String>> atFiles = project.provider(() -> {
LoomGradleExtension extension = LoomGradleExtension.get(project);
Path userdevJar = extension.getForgeUserdevProvider().getUserdevJar().toPath();
return extractAccessTransformers(userdevJar, extension.getForgeUserdevProvider().getConfig().ats(), tempFiles);
});
return createOptions(project, atFiles);
}
private static List<String> extractAccessTransformers(Path jar, UserdevConfig.AccessTransformerLocation location, TempFiles tempFiles) throws IOException {
final List<String> extracted = new ArrayList<>();
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) {
for (Path atFile : getAccessTransformerPaths(fs, location)) {
byte[] atBytes;
try {
atBytes = Files.readAllBytes(atFile);
} catch (NoSuchFileException e) {
continue;
}
Path tmpFile = tempFiles.file("at-conf", ".cfg");
Files.write(tmpFile, atBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
extracted.add(tmpFile.toAbsolutePath().toString());
}
}
return extracted;
}
private static List<Path> getAccessTransformerPaths(FileSystemUtil.Delegate fs, UserdevConfig.AccessTransformerLocation location) throws IOException {
return location.visitIo(directory -> {
Path dirPath = fs.getPath(directory);
try (Stream<Path> paths = Files.list(dirPath)) {
return paths.toList();
}
}, paths -> paths.stream().map(fs::getPath).toList());
}
public AccessTransformerService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
private static LoomVersions chooseAccessTransformer(Project project) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
boolean serverBundleMetadataPresent = extension.getMinecraftProvider().getServerBundleMetadata() != null;
if (!serverBundleMetadataPresent) {
return LoomVersions.ACCESS_TRANSFORMERS;
} else if (extension.isNeoForge()) {
MinecraftVersionMeta.JavaVersion javaVersion = extension.getMinecraftProvider().getVersionInfo().javaVersion();
if (javaVersion != null && javaVersion.majorVersion() >= 21) {
return LoomVersions.ACCESS_TRANSFORMERS_NEO;
}
}
return LoomVersions.ACCESS_TRANSFORMERS_NEW;
}
public void execute(Path input, Path output) throws IOException {
final List<String> args = new ArrayList<>();
args.add("--inJar");
args.add(input.toAbsolutePath().toString());
args.add("--outJar");
args.add(output.toAbsolutePath().toString());
for (File atFile : getOptions().getAccessTransformers().getFiles()) {
args.add("--atFile");
args.add(atFile.getAbsolutePath());
}
final ForgeToolService toolService = getServiceFactory().get(getOptions().getToolServiceOptions());
toolService.exec(spec -> {
spec.getMainClass().set(getOptions().getMainClass());
spec.setArgs(args);
spec.setClasspath(getOptions().getClasspath());
});
}
}

View File

@@ -15,8 +15,10 @@ import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;
import org.gradle.process.JavaExecSpec;
import org.jetbrains.annotations.Nullable;
/**
@@ -42,6 +44,13 @@ public abstract class ForgeToolExecutor {
settings.getExecutable().set(JavaExecutableFetcher.getJavaToolchainExecutable(project));
settings.getShowVerboseStdout().set(shouldShowVerboseStdout(project));
settings.getShowVerboseStderr().set(shouldShowVerboseStderr(project));
// call this to ensure the fields aren't null
settings.getProgramArgs();
settings.getJvmArgs();
settings.getMainClass();
settings.getExecClasspath();
return settings;
}
@@ -66,39 +75,46 @@ public abstract class ForgeToolExecutor {
}
public static ExecResult exec(ExecOperations execOperations, Settings settings) {
return execOperations.javaexec(spec -> {
final @Nullable String executable = settings.getExecutable().getOrNull();
if (executable != null) spec.setExecutable(executable);
spec.getMainClass().set(settings.getMainClass());
spec.setArgs(settings.getProgramArgs().get());
spec.setJvmArgs(settings.getJvmArgs().get());
spec.setClasspath(settings.getExecClasspath());
return execOperations.javaexec(spec -> applyToSpec(settings, spec));
}
if (settings.getShowVerboseStdout().get()) {
spec.setStandardOutput(System.out);
} else {
spec.setStandardOutput(NullOutputStream.NULL_OUTPUT_STREAM);
}
static void applyToSpec(Settings settings, JavaExecSpec spec) {
final @Nullable String executable = settings.getExecutable().getOrNull();
if (executable != null) spec.setExecutable(executable);
final @Nullable String mainClass = settings.getMainClass().getOrNull();
if (mainClass != null) spec.getMainClass().set(mainClass);
spec.setArgs(settings.getProgramArgs().get());
spec.setJvmArgs(settings.getJvmArgs().get());
spec.setClasspath(settings.getExecClasspath());
if (settings.getShowVerboseStderr().get()) {
spec.setErrorOutput(System.err);
} else {
spec.setErrorOutput(NullOutputStream.NULL_OUTPUT_STREAM);
}
});
if (settings.getShowVerboseStdout().get()) {
spec.setStandardOutput(System.out);
} else {
spec.setStandardOutput(NullOutputStream.INSTANCE);
}
if (settings.getShowVerboseStderr().get()) {
spec.setErrorOutput(System.err);
} else {
spec.setErrorOutput(NullOutputStream.INSTANCE);
}
}
public interface Settings {
@Input
@Optional
Property<String> getExecutable();
@Input
@Optional
ListProperty<String> getProgramArgs();
@Input
@Optional
ListProperty<String> getJvmArgs();
@Input
@Optional
Property<String> getMainClass();
@Classpath

View File

@@ -0,0 +1,58 @@
package dev.architectury.loom.forge.tool;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Nested;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;
import org.gradle.process.JavaExecSpec;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* A service that can execute Forge tools in tasks.
*/
public final class ForgeToolService extends Service<ForgeToolService.Options> {
public static final ServiceType<Options, ForgeToolService> TYPE = new ServiceType<>(Options.class, ForgeToolService.class);
public interface Options extends Service.Options {
/**
* The default settings from {@link ForgeToolExecutor}.
* It contains the verbosity and JVM toolchain options that are dependent on the project state.
*/
@Nested
Property<ForgeToolExecutor.Settings> getBaseSettings();
@Inject
ExecOperations getExecOperations();
}
public static Provider<Options> createOptions(Project project) {
return TYPE.create(project, options -> {
options.getBaseSettings().set(ForgeToolExecutor.getDefaultSettings(project));
});
}
public ForgeToolService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
/**
* Executes the tool specified in the spec.
*
* @param configurator an action that configures the spec
* @return the execution result
*/
public ExecResult exec(Action<? super JavaExecSpec> configurator) {
return getOptions().getExecOperations().javaexec(spec -> {
ForgeToolExecutor.applyToSpec(getOptions().getBaseSettings().get(), spec);
configurator.execute(spec);
});
}
}

View File

@@ -215,4 +215,6 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
default Path getPlatformMappingFile() {
return getMappingConfiguration().getPlatformMappingFile(this);
}
boolean manualRefreshDeps();
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -42,10 +42,9 @@ import com.google.common.hash.Hashing;
import com.google.common.io.MoreFiles;
import dev.architectury.at.AccessTransformSet;
import dev.architectury.at.io.AccessTransformFormats;
import dev.architectury.loom.forge.tool.ForgeToolValueSource;
import dev.architectury.loom.forge.tool.AccessTransformerService;
import dev.architectury.loom.util.TempFiles;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.jetbrains.annotations.Nullable;
@@ -58,10 +57,10 @@ import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.DependencyDownloader;
import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.service.ScopedServiceFactory;
public class AccessTransformerJarProcessor implements MinecraftJarProcessor<AccessTransformerJarProcessor.Spec> {
private static final Logger LOGGER = Logging.getLogger(AccessTransformerJarProcessor.class);
@@ -114,16 +113,14 @@ public class AccessTransformerJarProcessor implements MinecraftJarProcessor<Acce
@Override
public void processJar(Path jar, Spec spec, ProcessorContext context) throws IOException {
try (var tempFiles = new TempFiles()) {
try (var tempFiles = new TempFiles(); var serviceFactory = new ScopedServiceFactory()) {
LOGGER.lifecycle(":applying project access transformers");
final Path tempInput = tempFiles.file("input", ".jar");
Files.copy(jar, tempInput, StandardCopyOption.REPLACE_EXISTING);
final Path atPath = mergeAndRemapAccessTransformers(context, spec.accessTransformers(), tempFiles);
executeAt(project, tempInput, jar, args -> {
args.add("--atFile");
args.add(atPath.toAbsolutePath().toString());
});
final AccessTransformerService service = serviceFactory.get(AccessTransformerService.createOptions(project, atPath.toAbsolutePath()));
service.execute(tempInput, jar);
} catch (IOException e) {
throw ExceptionUtil.createDescriptiveWrapper(UncheckedIOException::new, "Could not access transform " + jar.toAbsolutePath(), e);
}
@@ -158,30 +155,6 @@ public class AccessTransformerJarProcessor implements MinecraftJarProcessor<Acce
return name;
}
public static void executeAt(Project project, Path input, Path output, AccessTransformerConfiguration configuration) throws IOException {
LoomVersions accessTransformer = chooseAccessTransformer(project);
String mainClass = accessTransformer == LoomVersions.ACCESS_TRANSFORMERS_NEO ? "net.neoforged.accesstransformer.cli.TransformerProcessor"
: "net.minecraftforge.accesstransformer.TransformerProcessor";
FileCollection classpath = new DependencyDownloader(project)
.add(accessTransformer.mavenNotation())
.add(LoomVersions.ASM.mavenNotation())
.platform(LoomVersions.ACCESS_TRANSFORMERS_LOG4J_BOM.mavenNotation())
.download();
List<String> args = new ArrayList<>();
args.add("--inJar");
args.add(input.toAbsolutePath().toString());
args.add("--outJar");
args.add(output.toAbsolutePath().toString());
configuration.apply(args);
ForgeToolValueSource.exec(project, spec -> {
spec.getMainClass().set(mainClass);
spec.setArgs(args);
spec.setClasspath(classpath);
});
}
private static LoomVersions chooseAccessTransformer(Project project) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
boolean serverBundleMetadataPresent = extension.getMinecraftProvider().getServerBundleMetadata() != null;

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -24,12 +24,14 @@
package net.fabricmc.loom.configuration.providers.forge;
import java.io.Serializable;
import com.mojang.serialization.Codec;
/**
* A string or a variable in a Forge configuration file, or an MCPConfig step or function.
*/
public sealed interface ConfigValue {
public sealed interface ConfigValue extends Serializable {
/**
* The variable that refers to the current MCP step's output path.
*/

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2020-2024 FabricMC
* Copyright (c) 2020-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
@@ -32,7 +32,6 @@ import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
@@ -54,6 +53,7 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import de.oceanlabs.mcp.mcinjector.adaptors.ParameterAnnotationFixer;
import dev.architectury.loom.forge.UserdevConfig;
import dev.architectury.loom.forge.tool.AccessTransformerService;
import dev.architectury.loom.forge.tool.ForgeToolValueSource;
import dev.architectury.loom.neoforge.SidedJarIndexGenerator;
import dev.architectury.loom.util.MappingOption;
@@ -71,9 +71,9 @@ import org.objectweb.asm.tree.ClassNode;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.configuration.accesstransformer.AccessTransformerJarProcessor;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpConfigProvider;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpExecutor;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpExecutorBuilder;
import net.fabricmc.loom.configuration.providers.forge.minecraft.ForgeMinecraftProvider;
import net.fabricmc.loom.configuration.providers.mappings.TinyMappingsService;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
@@ -84,6 +84,7 @@ import net.fabricmc.loom.util.ThreadingUtils;
import net.fabricmc.loom.util.TinyRemapperHelper;
import net.fabricmc.loom.util.ZipUtils;
import net.fabricmc.loom.util.function.FsPathConsumer;
import net.fabricmc.loom.util.service.ScopedServiceFactory;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.srg.CoreModClassRemapper;
import net.fabricmc.loom.util.srg.InnerClassRemapper;
@@ -189,9 +190,11 @@ public class MinecraftPatchedProvider {
if (Files.notExists(minecraftIntermediateJar)) {
this.dirty = true;
try (var tempFiles = new TempFiles()) {
McpExecutor executor = createMcpExecutor(tempFiles.directory("loom-mcp"));
Path output = executor.enqueue("rename").execute();
try (var tempFiles = new TempFiles(); var serviceFactory = new ScopedServiceFactory()) {
McpExecutorBuilder builder = createMcpExecutor(tempFiles.directory("loom-mcp"));
builder.enqueue("rename");
McpExecutor executor = serviceFactory.get(builder.build());
Path output = executor.execute();
Files.copy(output, minecraftIntermediateJar);
}
}
@@ -403,60 +406,15 @@ public class MinecraftPatchedProvider {
private void accessTransformForge() throws IOException {
Path input = minecraftPatchedIntermediateJar;
Path target = minecraftPatchedIntermediateAtJar;
accessTransform(project, input, target);
}
public static void accessTransform(Project project, Path input, Path target) throws IOException {
Stopwatch stopwatch = Stopwatch.createStarted();
project.getLogger().lifecycle(":access transforming minecraft");
LoomGradleExtension extension = LoomGradleExtension.get(project);
Path userdevJar = extension.getForgeUserdevProvider().getUserdevJar().toPath();
Files.deleteIfExists(target);
try (var tempFiles = new TempFiles()) {
AccessTransformerJarProcessor.executeAt(project, input, target, args -> {
for (String atFile : extractAccessTransformers(userdevJar, extension.getForgeUserdevProvider().getConfig().ats(), tempFiles)) {
args.add("--atFile");
args.add(atFile);
}
});
logger.lifecycle(":access transforming minecraft");
try (var tempFiles = new TempFiles(); var serviceFactory = new ScopedServiceFactory()) {
AccessTransformerService service = serviceFactory.get(AccessTransformerService.createOptionsForLoaderAts(project, tempFiles));
Files.deleteIfExists(target);
service.execute(input, target);
}
project.getLogger().lifecycle(":access transformed minecraft in " + stopwatch.stop());
}
private static List<String> extractAccessTransformers(Path jar, UserdevConfig.AccessTransformerLocation location, TempFiles tempFiles) throws IOException {
final List<String> extracted = new ArrayList<>();
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) {
for (Path atFile : getAccessTransformerPaths(fs, location)) {
byte[] atBytes;
try {
atBytes = Files.readAllBytes(atFile);
} catch (NoSuchFileException e) {
continue;
}
Path tmpFile = tempFiles.file("at-conf", ".cfg");
Files.write(tmpFile, atBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
extracted.add(tmpFile.toAbsolutePath().toString());
}
}
return extracted;
}
private static List<Path> getAccessTransformerPaths(FileSystemUtil.Delegate fs, UserdevConfig.AccessTransformerLocation location) throws IOException {
return location.visitIo(directory -> {
Path dirPath = fs.getPath(directory);
try (Stream<Path> paths = Files.list(dirPath)) {
return paths.toList();
}
}, paths -> paths.stream().map(fs::getPath).toList());
logger.lifecycle(":access transformed minecraft in " + stopwatch.stop());
}
private void remapPatchedJar(ServiceFactory serviceFactory) throws Exception {
@@ -669,9 +627,9 @@ public class MinecraftPatchedProvider {
}
}
public McpExecutor createMcpExecutor(Path cache) {
public McpExecutorBuilder createMcpExecutor(Path cache) {
McpConfigProvider provider = getExtension().getMcpConfigProvider();
return new McpExecutor(project, minecraftProvider, cache, provider, type.mcpId);
return new McpExecutorBuilder(project, minecraftProvider, cache, provider, type.mcpId);
}
public Path getMinecraftIntermediateJar() {

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -25,6 +25,7 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.List;
@@ -45,13 +46,13 @@ import net.fabricmc.loom.util.function.CollectionUtil;
* @param jvmArgs the JVM arguments
* @param repo the Maven repository to download the dependency from, or {@code null} if not specified
*/
public record McpConfigFunction(String version, List<ConfigValue> args, List<ConfigValue> jvmArgs, @Nullable String repo) {
public record McpConfigFunction(String version, List<ConfigValue> args, List<ConfigValue> jvmArgs, @Nullable String repo) implements Serializable {
private static final String VERSION_KEY = "version";
private static final String ARGS_KEY = "args";
private static final String JVM_ARGS_KEY = "jvmargs";
private static final String REPO_KEY = "repo";
public Path download(StepLogic.ExecutionContext executionContext) throws IOException {
public Path download(StepLogic.SetupContext executionContext) throws IOException {
if (repo != null) {
return executionContext.downloadFile(getDownloadUrl());
} else {

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -24,6 +24,7 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig;
import java.io.Serializable;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
@@ -31,7 +32,7 @@ import com.google.gson.JsonObject;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
public record McpConfigStep(String type, String name, Map<String, ConfigValue> config) {
public record McpConfigStep(String type, String name, Map<String, ConfigValue> config) implements Serializable {
private static final String TYPE_KEY = "type";
private static final String NAME_KEY = "name";

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -24,141 +24,101 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import com.google.common.base.Stopwatch;
import com.google.common.hash.Hashing;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.architectury.loom.forge.tool.ForgeToolExecutor;
import dev.architectury.loom.forge.tool.ForgeToolValueSource;
import dev.architectury.loom.forge.tool.ForgeToolService;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.process.JavaExecSpec;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.configuration.providers.forge.ForgeProvider;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.ConstantLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.DownloadManifestFileLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.FunctionLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.InjectLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.ListLibrariesLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.NoOpLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.PatchLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.StepLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.StripLogic;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.Download;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.loom.util.function.CollectionUtil;
import net.fabricmc.loom.util.gradle.GradleUtils;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
public final class McpExecutor {
/**
* Executes MCPConfig and NeoForm configs to build Minecraft jars on those platforms.
*/
public final class McpExecutor extends Service<McpExecutor.Options> {
public static final ServiceType<Options, McpExecutor> TYPE = new ServiceType<>(Options.class, McpExecutor.class);
private static final Logger LOGGER = Logging.getLogger(McpExecutor.class);
private static final LogLevel STEP_LOG_LEVEL = LogLevel.LIFECYCLE;
private final Project project;
private final MinecraftProvider minecraftProvider;
private final Path cache;
private final List<McpConfigStep> steps;
private final DependencySet dependencySet;
private final Map<String, McpConfigFunction> functions;
private final Map<String, String> config = new HashMap<>();
private final Map<String, String> config;
private final Map<String, String> extraConfig = new HashMap<>();
private @Nullable StepLogic.Provider stepLogicProvider = null;
public McpExecutor(Project project, MinecraftProvider minecraftProvider, Path cache, McpConfigProvider provider, String environment) {
this.project = project;
this.minecraftProvider = minecraftProvider;
this.cache = cache;
this.steps = provider.getData().steps().get(environment);
this.functions = provider.getData().functions();
this.dependencySet = new DependencySet(this.steps);
this.dependencySet.skip(step -> getStepLogic(step.name(), step.type()) instanceof NoOpLogic);
this.dependencySet.setIgnoreDependenciesFilter(step -> getStepLogic(step.name(), step.type()).hasNoContext());
public interface Options extends Service.Options {
// Steps
checkMinecraftVersion(provider);
addDefaultFiles(provider, environment);
/**
* The service options for the step logics of the requested steps.
*/
@Nested
MapProperty<String, Service.Options> getStepLogicOptions();
/**
* The requested steps.
*/
@Input
ListProperty<McpConfigStep> getStepsToExecute();
// Config data
/**
* Mappings extracted from {@code data.mappings} in the MCPConfig JSON.
*/
@InputFile
RegularFileProperty getMappings();
/**
* The initial config from the data files.
*/
@Input
MapProperty<String, String> getInitialConfig();
// Download settings
@Input
Property<Boolean> getOffline();
@Input
Property<Boolean> getManualRefreshDeps();
// Services
@Nested
Property<ForgeToolService.Options> getToolServiceOptions();
@Internal
DirectoryProperty getCache();
}
private void checkMinecraftVersion(McpConfigProvider provider) {
final String expected = provider.getData().version();
final String actual = minecraftProvider.minecraftVersion();
if (!expected.equals(actual)) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final ForgeProvider forgeProvider = extension.getForgeProvider();
final String message = "%s %s is not for Minecraft %s (expected: %s)."
.formatted(
extension.getPlatform().get().displayName(),
forgeProvider.getVersion().getCombined(),
actual,
expected
);
if (GradleUtils.getBooleanProperty(project, Constants.Properties.ALLOW_MISMATCHED_PLATFORM_VERSION)) {
project.getLogger().warn(message);
} else {
final String fullMessage = "%s\nYou can suppress this error by adding '%s = true' to gradle.properties."
.formatted(message, Constants.Properties.ALLOW_MISMATCHED_PLATFORM_VERSION);
throw new UnsupportedOperationException(fullMessage);
}
}
}
private void addDefaultFiles(McpConfigProvider provider, String environment) {
for (Map.Entry<String, JsonElement> entry : provider.getData().data().entrySet()) {
if (entry.getValue().isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), entry.getValue().getAsString());
} else if (entry.getValue().isJsonObject()) {
JsonObject json = entry.getValue().getAsJsonObject();
if (json.has(environment) && json.get(environment).isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), json.getAsJsonPrimitive(environment).getAsString());
}
}
}
}
private void addDefaultFile(McpConfigProvider provider, String key, String value) {
Path path = provider.getUnpackedZip().resolve(value).toAbsolutePath();
if (!path.startsWith(provider.getUnpackedZip().toAbsolutePath())) {
// This is probably not what we're looking for since it falls outside the directory.
return;
} else if (Files.notExists(path)) {
// Not a real file, let's continue.
return;
}
addConfig(key, path.toString());
}
public void addConfig(String key, String value) {
config.put(key, value);
}
private Path getDownloadCache() throws IOException {
Path downloadCache = cache.resolve("downloads");
Files.createDirectories(downloadCache);
return downloadCache;
public McpExecutor(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
this.config = new HashMap<>(options.getInitialConfig().get());
this.cache = options.getCache().get().getAsFile().toPath();
}
private Path getStepCache(String step) {
@@ -195,96 +155,34 @@ public final class McpExecutor {
});
}
/**
* Enqueues a step and its dependencies to be executed.
*
* @param step the name of the step
* @return this executor
*/
public McpExecutor enqueue(String step) {
dependencySet.add(step);
return this;
}
/**
* Executes all queued steps and their dependencies.
*
* @return the output file of the last executed step
*/
public Path execute() throws IOException {
SortedSet<String> stepNames = dependencySet.buildExecutionSet();
dependencySet.clear();
List<McpConfigStep> toExecute = new ArrayList<>();
for (String stepName : stepNames) {
McpConfigStep step = CollectionUtil.find(steps, s -> s.name().equals(stepName))
.orElseThrow(() -> new NoSuchElementException("Step '" + stepName + "' not found in MCP config"));
toExecute.add(step);
}
return executeSteps(toExecute);
}
/**
* Executes the specified steps.
*
* @param steps the steps to execute
* @return the output file of the last executed step
*/
public Path executeSteps(List<McpConfigStep> steps) throws IOException {
extraConfig.clear();
List<McpConfigStep> steps = getOptions().getStepsToExecute().get();
int totalSteps = steps.size();
int currentStepIndex = 0;
project.getLogger().log(STEP_LOG_LEVEL, ":executing {} MCP steps", totalSteps);
LOGGER.log(STEP_LOG_LEVEL, ":executing {} MCP steps", totalSteps);
for (McpConfigStep currentStep : steps) {
currentStepIndex++;
StepLogic stepLogic = getStepLogic(currentStep.name(), currentStep.type());
project.getLogger().log(STEP_LOG_LEVEL, ":step {}/{} - {}", currentStepIndex, totalSteps, stepLogic.getDisplayName(currentStep.name()));
StepLogic<?> stepLogic = getStepLogic(currentStep.name());
LOGGER.log(STEP_LOG_LEVEL, ":step {}/{} - {}", currentStepIndex, totalSteps, stepLogic.getDisplayName(currentStep.name()));
Stopwatch stopwatch = Stopwatch.createStarted();
stepLogic.execute(new ExecutionContextImpl(currentStep));
project.getLogger().log(STEP_LOG_LEVEL, ":{} done in {}", currentStep.name(), stopwatch.stop());
LOGGER.log(STEP_LOG_LEVEL, ":{} done in {}", currentStep.name(), stopwatch.stop());
}
return Path.of(extraConfig.get(ConfigValue.OUTPUT));
}
/**
* Sets the custom step logic provider of this executor.
*
* @param stepLogicProvider the provider, or null to disable
*/
public void setStepLogicProvider(@Nullable StepLogic.Provider stepLogicProvider) {
this.stepLogicProvider = stepLogicProvider;
}
private StepLogic getStepLogic(String name, String type) {
if (stepLogicProvider != null) {
final @Nullable StepLogic custom = stepLogicProvider.getStepLogic(name, type).orElse(null);
if (custom != null) return custom;
}
return switch (type) {
case "downloadManifest", "downloadJson" -> new NoOpLogic();
case "downloadClient" -> new ConstantLogic(() -> minecraftProvider.getMinecraftClientJar().toPath());
case "downloadServer" -> new ConstantLogic(() -> minecraftProvider.getMinecraftServerJar().toPath());
case "strip" -> new StripLogic();
case "listLibraries" -> new ListLibrariesLogic();
case "downloadClientMappings" -> new DownloadManifestFileLogic(minecraftProvider.getVersionInfo().download("client_mappings"));
case "downloadServerMappings" -> new DownloadManifestFileLogic(minecraftProvider.getVersionInfo().download("server_mappings"));
case "inject" -> new InjectLogic();
case "patch" -> new PatchLogic();
default -> {
if (functions.containsKey(type)) {
yield new FunctionLogic(functions.get(type));
}
throw new UnsupportedOperationException("MCP config step type: " + type);
}
};
private StepLogic<?> getStepLogic(String name) {
final Provider<Service.Options> options = getOptions().getStepLogicOptions().getting(name);
return (StepLogic<?>) getServiceFactory().get(options);
}
private class ExecutionContextImpl implements StepLogic.ExecutionContext {
@@ -296,7 +194,7 @@ public final class McpExecutor {
@Override
public Logger logger() {
return project.getLogger();
return LOGGER;
}
@Override
@@ -319,7 +217,7 @@ public final class McpExecutor {
@Override
public Path mappings() {
return LoomGradleExtension.get(project).getMcpConfigProvider().getMappings();
return getOptions().getMappings().get().getAsFile().toPath();
}
@Override
@@ -327,55 +225,31 @@ public final class McpExecutor {
return McpExecutor.this.resolve(step, value);
}
@Override
public Path downloadFile(String url) throws IOException {
Path path = getDownloadCache().resolve(Hashing.sha256().hashString(url, StandardCharsets.UTF_8).toString().substring(0, 24));
redirectAwareDownload(url, path);
return path;
}
@Override
public Path downloadDependency(String notation) {
final Dependency dependency = project.getDependencies().create(notation);
final Configuration configuration = project.getConfigurations().detachedConfiguration(dependency);
configuration.setTransitive(false);
return configuration.getSingleFile().toPath();
}
@Override
public DownloadBuilder downloadBuilder(String url) {
return LoomGradleExtension.get(project).download(url);
}
DownloadBuilder builder;
// Some of these files linked to the old Forge maven, let's follow the redirects to the new one.
private static void redirectAwareDownload(String urlString, Path path) throws IOException {
URL url = new URL(urlString);
if (url.getProtocol().equals("http")) {
url = new URL("https", url.getHost(), url.getPort(), url.getFile());
try {
builder = Download.create(url);
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to create downloader for: " + e);
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
redirectAwareDownload(connection.getHeaderField("Location"), path);
} else {
try (InputStream in = connection.getInputStream()) {
Files.copy(in, path);
}
if (getOptions().getOffline().get()) {
builder.offline();
}
if (getOptions().getManualRefreshDeps().get()) {
builder.forceDownload();
}
return builder;
}
@Override
public void javaexec(Action<? super ForgeToolExecutor.Settings> configurator) {
ForgeToolValueSource.exec(project, configurator);
}
@Override
public Set<File> getMinecraftLibraries() {
// (1.2) minecraftRuntimeLibraries contains the compile-time libraries as well.
return project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES).resolve();
public void javaexec(Action<? super JavaExecSpec> configurator) {
final ForgeToolService toolService = getServiceFactory().get(getOptions().getToolServiceOptions());
toolService.exec(configurator);
}
}
}

View File

@@ -0,0 +1,301 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-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.providers.forge.mcpconfig;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import com.google.common.base.Suppliers;
import com.google.common.hash.Hashing;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.architectury.loom.forge.tool.ForgeToolService;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Provider;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.providers.forge.ForgeProvider;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.ConstantLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.DownloadManifestFileLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.FunctionLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.InjectLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.ListLibrariesLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.NoOpLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.PatchLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.StepLogic;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.StripLogic;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.function.CollectionUtil;
import net.fabricmc.loom.util.gradle.GradleUtils;
import net.fabricmc.loom.util.service.Service;
/**
* Builds an {@link McpExecutor}'s {@linkplain McpExecutor.Options options} from the project state
* and enqueued steps.
*/
public final class McpExecutorBuilder {
private final Project project;
private final MinecraftProvider minecraftProvider;
private final Path cache;
private final List<McpConfigStep> steps;
private final DependencySet dependencySet;
private final Map<String, McpConfigFunction> functions;
private final Map<String, String> config = new HashMap<>();
private final StepLogic.SetupContext setupContext = new SetupContextImpl();
private @Nullable StepLogic.StepLogicProvider stepLogicProvider = null;
public McpExecutorBuilder(Project project, MinecraftProvider minecraftProvider, Path cache, McpConfigProvider provider, String environment) {
this.project = project;
this.minecraftProvider = minecraftProvider;
this.cache = cache;
this.steps = provider.getData().steps().get(environment);
this.functions = provider.getData().functions();
this.dependencySet = new DependencySet(this.steps);
this.dependencySet.skip(step -> isNoOp(step.type()));
checkMinecraftVersion(provider);
addDefaultFiles(provider, environment);
}
private void checkMinecraftVersion(McpConfigProvider provider) {
final String expected = provider.getData().version();
final String actual = minecraftProvider.minecraftVersion();
if (!expected.equals(actual)) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final ForgeProvider forgeProvider = extension.getForgeProvider();
final String message = "%s %s is not for Minecraft %s (expected: %s)."
.formatted(
extension.getPlatform().get().displayName(),
forgeProvider.getVersion().getCombined(),
actual,
expected
);
if (GradleUtils.getBooleanProperty(project, Constants.Properties.ALLOW_MISMATCHED_PLATFORM_VERSION)) {
project.getLogger().warn(message);
} else {
final String fullMessage = "%s\nYou can suppress this error by adding '%s = true' to gradle.properties."
.formatted(message, Constants.Properties.ALLOW_MISMATCHED_PLATFORM_VERSION);
throw new UnsupportedOperationException(fullMessage);
}
}
}
private void addDefaultFiles(McpConfigProvider provider, String environment) {
for (Map.Entry<String, JsonElement> entry : provider.getData().data().entrySet()) {
if (entry.getValue().isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), entry.getValue().getAsString());
} else if (entry.getValue().isJsonObject()) {
JsonObject json = entry.getValue().getAsJsonObject();
if (json.has(environment) && json.get(environment).isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), json.getAsJsonPrimitive(environment).getAsString());
}
}
}
}
private void addDefaultFile(McpConfigProvider provider, String key, String value) {
Path path = provider.getUnpackedZip().resolve(value).toAbsolutePath();
if (!path.startsWith(provider.getUnpackedZip().toAbsolutePath())) {
// This is probably not what we're looking for since it falls outside the directory.
return;
} else if (Files.notExists(path)) {
// Not a real file, let's continue.
return;
}
addConfig(key, path.toString());
}
public void addConfig(String key, String value) {
config.put(key, value);
}
private Path getDownloadCache() throws IOException {
Path downloadCache = cache.resolve("downloads");
Files.createDirectories(downloadCache);
return downloadCache;
}
/**
* Enqueues a step and its dependencies to be executed.
*
* @param step the name of the step
* @return this builder
*/
public McpExecutorBuilder enqueue(String step) {
dependencySet.add(step);
return this;
}
/**
* Builds options for an executor that runs all queued steps and their dependencies.
*
* @return the options
*/
public Provider<McpExecutor.Options> build() throws IOException {
SortedSet<String> stepNames = dependencySet.buildExecutionSet();
dependencySet.clear();
List<McpConfigStep> toExecute = new ArrayList<>();
for (String stepName : stepNames) {
McpConfigStep step = CollectionUtil.find(steps, s -> s.name().equals(stepName))
.orElseThrow(() -> new NoSuchElementException("Step '" + stepName + "' not found in MCP config"));
toExecute.add(step);
}
return McpExecutor.TYPE.create(project, options -> {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
for (McpConfigStep step : toExecute) {
options.getStepLogicOptions().put(step.name(), getStepLogic(step.name(), step.type()));
}
options.getStepsToExecute().set(toExecute);
options.getMappings().set(extension.getMcpConfigProvider().getMappings().toFile());
options.getInitialConfig().set(config);
options.getOffline().set(project.getGradle().getStartParameter().isOffline());
options.getManualRefreshDeps().set(extension.manualRefreshDeps());
options.getToolServiceOptions().set(ForgeToolService.createOptions(project));
options.getCache().set(cache.toFile());
});
}
/**
* Sets the custom step logic provider of this executor.
*
* @param stepLogicProvider the provider, or null to disable
*/
public void setStepLogicProvider(@Nullable StepLogic.StepLogicProvider stepLogicProvider) {
this.stepLogicProvider = stepLogicProvider;
}
private boolean isNoOp(String stepType) {
return "downloadManifest".equals(stepType) || "downloadJson".equals(stepType);
}
private Provider<? extends Service.Options> getStepLogic(String name, String type) {
if (stepLogicProvider != null) {
final @Nullable Provider<? extends Service.Options> custom = stepLogicProvider.getStepLogic(setupContext, name, type);
if (custom != null) return custom;
}
return switch (type) {
case "downloadManifest", "downloadJson" -> NoOpLogic.createOptions(setupContext);
case "downloadClient" -> ConstantLogic.createOptions(setupContext, () -> minecraftProvider.getMinecraftClientJar().toPath());
case "downloadServer" -> ConstantLogic.createOptions(setupContext, () -> minecraftProvider.getMinecraftServerJar().toPath());
case "strip" -> StripLogic.createOptions(setupContext);
case "listLibraries" -> ListLibrariesLogic.createOptions(setupContext);
case "downloadClientMappings" -> DownloadManifestFileLogic.createOptions(setupContext, minecraftProvider.getVersionInfo().download("client_mappings"));
case "downloadServerMappings" -> DownloadManifestFileLogic.createOptions(setupContext, minecraftProvider.getVersionInfo().download("server_mappings"));
case "inject" -> InjectLogic.createOptions(setupContext);
case "patch" -> PatchLogic.createOptions(setupContext);
default -> {
if (functions.containsKey(type)) {
yield FunctionLogic.createOptions(setupContext, functions.get(type));
}
throw new UnsupportedOperationException("MCP config step type: " + type);
}
};
}
private class SetupContextImpl implements StepLogic.SetupContext {
@Override
public Project project() {
return project;
}
@Override
public Path downloadFile(String url) throws IOException {
Path path = getDownloadCache().resolve(Hashing.sha256().hashString(url, StandardCharsets.UTF_8).toString().substring(0, 24));
// If the file is already downloaded, we don't need to do anything.
if (Files.exists(path)) return path;
redirectAwareDownload(url, path);
return path;
}
// Some of these files linked to the old Forge maven, let's follow the redirects to the new one.
private static void redirectAwareDownload(String urlString, Path path) throws IOException {
URL url = new URL(urlString);
if (url.getProtocol().equals("http")) {
url = new URL("https", url.getHost(), url.getPort(), url.getFile());
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
redirectAwareDownload(connection.getHeaderField("Location"), path);
} else {
try (InputStream in = connection.getInputStream()) {
Files.copy(in, path);
}
}
}
@Override
public Path downloadDependency(String notation) {
final Dependency dependency = project.getDependencies().create(notation);
final Configuration configuration = project.getConfigurations().detachedConfiguration(dependency);
configuration.setTransitive(false);
return configuration.getSingleFile().toPath();
}
@Override
public Provider<FileCollection> getMinecraftLibraries() {
return project().provider(Suppliers.memoize(() -> {
project.getLogger().lifecycle(":downloading minecraft libraries, this may take a while...");
// (1.2) minecraftRuntimeLibraries contains the compile-time libraries as well.
final Set<File> files = project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES).resolve();
return project.files(files);
})::get);
}
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -28,19 +28,38 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Supplier;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* A no-op step logic that is used for steps automatically executed by Loom earlier.
* This one returns a file.
*/
public final class ConstantLogic implements StepLogic {
private final Supplier<Path> path;
public final class ConstantLogic extends StepLogic<ConstantLogic.Options> {
public static final ServiceType<Options, ConstantLogic> TYPE = new ServiceType<>(Options.class, ConstantLogic.class);
public ConstantLogic(Supplier<Path> path) {
this.path = path;
public interface Options extends Service.Options {
@Input
Property<String> getFile();
}
public static Provider<Options> createOptions(SetupContext context, Supplier<Path> path) {
return TYPE.create(context.project(), options -> {
options.getFile().set(context.project().provider(() -> path.get().toAbsolutePath().toString()));
});
}
public ConstantLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
public void execute(ExecutionContext context) throws IOException {
context.setOutput(path.get());
context.setOutput(Path.of(getOptions().getFile().get()));
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -26,20 +26,37 @@ package net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic;
import java.io.IOException;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* Downloads a file from the Minecraft version metadata.
*/
public final class DownloadManifestFileLogic implements StepLogic {
private final MinecraftVersionMeta.Download download;
public final class DownloadManifestFileLogic extends StepLogic<DownloadManifestFileLogic.Options> {
public static final ServiceType<Options, DownloadManifestFileLogic> TYPE = new ServiceType<>(Options.class, DownloadManifestFileLogic.class);
public DownloadManifestFileLogic(MinecraftVersionMeta.Download download) {
this.download = download;
public interface Options extends Service.Options {
@Input
Property<MinecraftVersionMeta.Download> getDownload();
}
public static Provider<Options> createOptions(SetupContext context, MinecraftVersionMeta.Download download) {
return TYPE.create(context.project(), options -> options.getDownload().set(download));
}
public DownloadManifestFileLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
public void execute(ExecutionContext context) throws IOException {
MinecraftVersionMeta.Download download = getOptions().getDownload().get();
context.downloadBuilder(download.url())
.sha1(download.sha1())
.downloadPath(context.setOutput("output"));

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -24,21 +24,54 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.io.UncheckedIOException;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import com.google.common.base.Suppliers;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpConfigFunction;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* Runs a Forge tool configured by a {@linkplain McpConfigFunction function}.
*/
public final class FunctionLogic implements StepLogic {
private final McpConfigFunction function;
public final class FunctionLogic extends StepLogic<FunctionLogic.Options> {
public static final ServiceType<Options, FunctionLogic> TYPE = new ServiceType<>(Options.class, FunctionLogic.class);
public FunctionLogic(McpConfigFunction function) {
this.function = function;
public interface Options extends Service.Options {
@Input
Property<McpConfigFunction> getFunction();
@InputFile
RegularFileProperty getToolJar();
}
public static Provider<Options> createOptions(SetupContext context, McpConfigFunction function) {
return TYPE.create(context.project(), options -> {
options.getFunction().set(function);
final Provider<File> jar = context.project().provider(Suppliers.memoize(() -> {
try {
return function.download(context).toFile();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})::get);
options.getToolJar().set(context.project().getLayout().file(jar));
});
}
public FunctionLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
@@ -47,13 +80,15 @@ public final class FunctionLogic implements StepLogic {
// The other tools seem to work with the name containing .jar anyway.
// Technically, FG supports an "outputExtension" config value for steps, but it's not used in practice.
context.setOutput("output.jar");
Path jar = function.download(context);
McpConfigFunction function = getOptions().getFunction().get();
File jar = getOptions().getToolJar().get().getAsFile();
String mainClass;
try (JarFile jarFile = new JarFile(jar.toFile())) {
try (JarFile jarFile = new JarFile(jar)) {
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
} catch (IOException e) {
throw new IOException("Could not determine main class for " + jar.toAbsolutePath(), e);
throw new IOException("Could not determine main class for " + jar.getAbsolutePath(), e);
}
context.javaexec(spec -> {
@@ -66,6 +101,6 @@ public final class FunctionLogic implements StepLogic {
@Override
public String getDisplayName(String stepName) {
return stepName + " with " + function.version();
return stepName + " with " + getOptions().getFunction().get().version();
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -32,10 +32,28 @@ import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import java.util.stream.Stream;
import org.gradle.api.provider.Provider;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
public final class InjectLogic extends StepLogic<InjectLogic.Options> {
public static final ServiceType<Options, InjectLogic> TYPE = new ServiceType<>(Options.class, InjectLogic.class);
public interface Options extends Service.Options {
}
public static Provider<Options> createOptions(SetupContext context) {
return TYPE.create(context.project(), options -> { });
}
public InjectLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
public final class InjectLogic implements StepLogic {
@Override
public void execute(ExecutionContext context) throws IOException {
Path injectedFiles = Path.of(context.resolve(new ConfigValue.Variable("inject")));

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -29,16 +29,39 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.InputFiles;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* Lists the Minecraft libraries into the output file.
*/
public final class ListLibrariesLogic implements StepLogic {
public final class ListLibrariesLogic extends StepLogic<ListLibrariesLogic.Options> {
public static final ServiceType<Options, ListLibrariesLogic> TYPE = new ServiceType<>(Options.class, ListLibrariesLogic.class);
public interface Options extends Service.Options {
@InputFiles
ConfigurableFileCollection getMinecraftLibraries();
}
public static Provider<Options> createOptions(SetupContext context) {
return TYPE.create(context.project(), options -> {
options.getMinecraftLibraries().from(context.getMinecraftLibraries());
});
}
public ListLibrariesLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
public void execute(ExecutionContext context) throws IOException {
context.logger().lifecycle(":downloading minecraft libraries, this may take a while...");
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(context.setOutput("libraries.txt")))) {
for (File lib : context.getMinecraftLibraries()) {
for (File lib : getOptions().getMinecraftLibraries()) {
writer.println("-e=" + lib.getAbsolutePath());
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -26,10 +26,29 @@ package net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic;
import java.io.IOException;
import org.gradle.api.provider.Provider;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* A no-op step logic that is used for steps automatically executed by Loom earlier.
*/
public final class NoOpLogic implements StepLogic {
public final class NoOpLogic extends StepLogic<NoOpLogic.Options> {
public static final ServiceType<Options, NoOpLogic> TYPE = new ServiceType<>(Options.class, NoOpLogic.class);
public interface Options extends Service.Options {
}
public static Provider<Options> createOptions(SetupContext context) {
return TYPE.create(context.project(), options -> { });
}
public NoOpLogic(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
public void execute(ExecutionContext context) throws IOException {
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -32,10 +32,27 @@ import codechicken.diffpatch.cli.PatchOperation;
import codechicken.diffpatch.util.LoggingOutputStream;
import codechicken.diffpatch.util.PatchMode;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.provider.Provider;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
public final class PatchLogic extends StepLogic<PatchLogic.Options> {
public static final ServiceType<Options, PatchLogic> TYPE = new ServiceType<>(PatchLogic.Options.class, PatchLogic.class);
public interface Options extends Service.Options {
}
public static Provider<Options> createOptions(SetupContext context) {
return TYPE.create(context.project(), options -> { });
}
public PatchLogic(PatchLogic.Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
public final class PatchLogic implements StepLogic {
@Override
public void execute(ExecutionContext context) throws IOException {
Path input = Path.of(context.resolve(new ConfigValue.Variable("input")));

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -24,36 +24,39 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import dev.architectury.loom.forge.tool.ForgeToolExecutor;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.provider.Provider;
import org.gradle.process.JavaExecSpec;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.loom.util.function.CollectionUtil;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
/**
* The logic for executing a step. This corresponds to the {@code type} key in the step JSON format.
*/
public interface StepLogic {
void execute(ExecutionContext context) throws IOException;
public abstract class StepLogic<O extends Service.Options> extends Service<O> {
public StepLogic(O options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
default String getDisplayName(String stepName) {
public abstract void execute(ExecutionContext context) throws IOException;
public String getDisplayName(String stepName) {
return stepName;
}
default boolean hasNoContext() {
return false;
}
interface ExecutionContext {
public interface ExecutionContext {
Logger logger();
Path setOutput(String fileName) throws IOException;
Path setOutput(Path output);
@@ -61,19 +64,23 @@ public interface StepLogic {
/** Mappings extracted from {@code data.mappings} in the MCPConfig JSON. */
Path mappings();
String resolve(ConfigValue value);
Path downloadFile(String url) throws IOException;
Path downloadDependency(String notation);
DownloadBuilder downloadBuilder(String url);
void javaexec(Action<? super ForgeToolExecutor.Settings> configurator);
Set<File> getMinecraftLibraries();
void javaexec(Action<? super JavaExecSpec> configurator);
default List<String> resolve(List<ConfigValue> configValues) {
return CollectionUtil.map(configValues, this::resolve);
}
}
public interface SetupContext {
Project project();
Path downloadFile(String url) throws IOException;
Path downloadDependency(String notation);
Provider<FileCollection> getMinecraftLibraries();
}
@FunctionalInterface
interface Provider {
Optional<StepLogic> getStepLogic(String name, String type);
public interface StepLogicProvider {
@Nullable Provider<? extends Service.Options> getStepLogic(SetupContext context, String name, String type);
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -33,14 +33,32 @@ import java.nio.file.StandardCopyOption;
import java.util.Set;
import java.util.stream.Collectors;
import org.gradle.api.provider.Provider;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.ThreadingUtils;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
/**
* Strips certain classes from the jar.
*/
public final class StripLogic implements StepLogic {
public final class StripLogic extends StepLogic<StripLogic.Options> {
public static final ServiceType<Options, StripLogic> TYPE = new ServiceType<>(StripLogic.Options.class, StripLogic.class);
public interface Options extends Service.Options {
}
public static Provider<Options> createOptions(SetupContext context) {
return TYPE.create(context.project(), options -> { });
}
public StripLogic(StripLogic.Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
@Override
public void execute(ExecutionContext context) throws IOException {
Set<String> filter = Files.readAllLines(context.mappings(), StandardCharsets.UTF_8).stream()

View File

@@ -25,6 +25,7 @@
package net.fabricmc.loom.configuration.providers.minecraft;
import java.io.File;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -163,7 +164,7 @@ public record MinecraftVersionMeta(
}
}
public record Download(String path, String sha1, long size, String url) {
public record Download(String path, String sha1, long size, String url) implements Serializable {
public File relativeFile(File baseDirectory) {
Objects.requireNonNull(path(), "Cannot get relative file from a null path");
return new File(baseDirectory, path());

View File

@@ -298,7 +298,8 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
return builder;
}
private boolean manualRefreshDeps() {
@Override
public boolean manualRefreshDeps() {
return project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh");
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 FabricMC
* Copyright (c) 2022-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
@@ -25,12 +25,12 @@
package net.fabricmc.loom.task;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import codechicken.diffpatch.cli.CliOperation;
import codechicken.diffpatch.cli.PatchOperation;
@@ -38,32 +38,36 @@ import codechicken.diffpatch.util.LoggingOutputStream;
import codechicken.diffpatch.util.PatchMode;
import com.google.common.base.Stopwatch;
import dev.architectury.loom.forge.ForgeSourcesService;
import dev.architectury.loom.forge.tool.ForgeToolValueSource;
import dev.architectury.loom.forge.tool.AccessTransformerService;
import dev.architectury.loom.forge.tool.ForgeToolService;
import dev.architectury.loom.forge.tool.ForgeTools;
import dev.architectury.loom.util.TempFiles;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager;
import net.fabricmc.loom.configuration.providers.forge.ForgeUserdevProvider;
import net.fabricmc.loom.configuration.providers.forge.MinecraftPatchedProvider;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpExecutor;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpExecutorBuilder;
import net.fabricmc.loom.configuration.providers.forge.mcpconfig.steplogic.ConstantLogic;
import net.fabricmc.loom.task.service.MappingsService;
import net.fabricmc.loom.task.service.SourceRemapperService;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.DependencyDownloader;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.SourceRemapper;
import net.fabricmc.loom.util.service.ScopedServiceFactory;
import net.fabricmc.loom.util.service.ServiceFactory;
// TODO: NeoForge support
// TODO: Config cache support
public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
/**
* The SRG Minecraft file produced by the MCP executor.
@@ -83,35 +87,112 @@ public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
@OutputFile
public abstract RegularFileProperty getOutputJar();
@OutputFile
protected abstract RegularFileProperty getSideAnnotationStrippedMinecraftJar();
@Nested
public abstract Property<ForgeSourcesService.Options> getForgeSourcesOptions();
protected abstract Property<ForgeSourcesService.Options> getForgeSourcesOptions();
@Nested
protected abstract Property<McpExecutor.Options> getMcpExecutorOptions();
@Nested
protected abstract Property<AccessTransformerService.Options> getAccessTransformerOptions();
@Nested
protected abstract Property<ForgeToolService.Options> getToolServiceOptions();
@Nested
protected abstract Property<SourceRemapperService.Options> getSourceRemapperOptions();
@Nested
protected abstract Property<SasOptions> getSasOptions();
@Input
protected abstract Property<String> getPatchPathInZip();
@Input
protected abstract Property<String> getPatchesOriginalPrefix();
@Input
protected abstract Property<String> getPatchesModifiedPrefix();
@Internal
protected abstract Property<TempFiles> getTempFiles();
public GenerateForgePatchedSourcesTask() {
getOutputs().upToDateWhen((o) -> false);
getOutputJar().fileProvider(getProject().provider(() -> GenerateSourcesTask.getJarFileWithSuffix(getRuntimeJar(), "-sources.jar")));
getForgeSourcesOptions().convention(ForgeSourcesService.createOptions(getProject()));
final TempFiles tempFiles = new TempFiles();
getTempFiles().value(tempFiles).finalizeValue();
final Path cache;
try {
cache = tempFiles.directory("mcp-cache");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
getSideAnnotationStrippedMinecraftJar().set(cache.resolve("side-annotation-stripped.jar").toFile());
getMcpExecutorOptions().convention(getProject().provider(() -> {
MinecraftPatchedProvider patchedProvider = MinecraftPatchedProvider.get(getProject());
McpExecutorBuilder mcp = patchedProvider.createMcpExecutor(cache);
mcp.setStepLogicProvider((setupContext, name, type) -> {
if (name.equals("rename")) {
return ConstantLogic.createOptions(setupContext, () -> getSideAnnotationStrippedMinecraftJar().get().getAsFile().toPath());
}
return null;
});
mcp.enqueue("decompile");
mcp.enqueue("patch");
try {
return mcp.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}).flatMap(o -> o));
getAccessTransformerOptions().convention(AccessTransformerService.createOptionsForLoaderAts(getProject(), tempFiles));
getToolServiceOptions().convention(ForgeToolService.createOptions(getProject()));
final SasOptions sasOptions = getProject().getObjects().newInstance(SasOptions.class);
sasOptions.getUserdevJar().set(getExtension().getForgeUserdevProvider().getUserdevJar());
sasOptions.getSass().set(getExtension().getForgeUserdevProvider().getConfig().sass());
sasOptions.getClasspath().from(DependencyDownloader.download(getProject(), ForgeTools.SIDE_STRIPPER, false, false));
getSasOptions().set(sasOptions);
getPatchPathInZip().set(getExtension().getForgeUserdevProvider().getConfig().patches());
getPatchesOriginalPrefix().set(getExtension().getForgeUserdevProvider().getConfig().patchesOriginalPrefix().orElseThrow());
getPatchesModifiedPrefix().set(getExtension().getForgeUserdevProvider().getConfig().patchesModifiedPrefix().orElseThrow());
getSourceRemapperOptions().set(SourceRemapperService.TYPE.create(getProject(), sro -> {
sro.getMappings().set(MappingsService.createOptionsWithProjectMappings(
getProject(),
getProject().provider(() -> "srg"),
getProject().provider(() -> "named")
));
sro.getJavaCompileRelease().set(SourceRemapperService.getJavaCompileRelease(getProject()));
sro.getClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
}));
}
@TaskAction
public void run() throws IOException {
// Check that the jar is not processed
final @Nullable MinecraftJarProcessorManager jarProcessorManager = MinecraftJarProcessorManager.create(getProject());
if (jarProcessorManager != null) {
throw new UnsupportedOperationException("Cannot run Forge's patched decompilation with a processed Minecraft jar");
}
try (var tempFiles = new TempFiles(); var serviceFactory = new ScopedServiceFactory()) {
try (var tempFiles = getTempFiles().get(); var serviceFactory = new ScopedServiceFactory()) {
Path cache = tempFiles.directory("loom-decompilation");
// Transform game jar before decompiling
Path accessTransformed = cache.resolve("access-transformed.jar");
MinecraftPatchedProvider.accessTransform(getProject(), getInputJar().get().getAsFile().toPath(), accessTransformed);
Path sideAnnotationStripped = cache.resolve("side-annotation-stripped.jar");
stripSideAnnotations(accessTransformed, sideAnnotationStripped);
AccessTransformerService atService = serviceFactory.get(getAccessTransformerOptions());
atService.execute(getInputJar().get().getAsFile().toPath(), accessTransformed);
Path sideAnnotationStripped = getSideAnnotationStrippedMinecraftJar().get().getAsFile().toPath();
stripSideAnnotations(accessTransformed, sideAnnotationStripped, serviceFactory);
// Step 1: decompile and patch with MCP patches
Path rawDecompiled = decompileAndPatch(cache, sideAnnotationStripped);
Path rawDecompiled = decompileAndPatch(serviceFactory);
// Step 2: patch with Forge patches
getLogger().lifecycle(":applying Forge patches");
Path patched = sourcePatch(cache, rawDecompiled);
@@ -123,40 +204,26 @@ public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
}
}
private Path decompileAndPatch(Path cache, Path gameJar) throws IOException {
Path mcpCache = cache.resolve("mcp");
Files.createDirectory(mcpCache);
MinecraftPatchedProvider patchedProvider = MinecraftPatchedProvider.get(getProject());
McpExecutor mcp = patchedProvider.createMcpExecutor(mcpCache);
mcp.setStepLogicProvider((name, type) -> {
if (name.equals("rename")) {
return Optional.of(new ConstantLogic(() -> gameJar));
}
return Optional.empty();
});
mcp.enqueue("decompile");
mcp.enqueue("patch");
return mcp.execute();
private Path decompileAndPatch(ScopedServiceFactory serviceFactory) throws IOException {
final McpExecutor executor = serviceFactory.get(getMcpExecutorOptions());
return executor.execute();
}
private Path sourcePatch(Path cache, Path rawDecompiled) throws IOException {
ForgeUserdevProvider userdev = getExtension().getForgeUserdevProvider();
String patchPathInZip = userdev.getConfig().patches();
String patchPathInZip = getPatchPathInZip().get();
Path output = cache.resolve("patched.jar");
Path rejects = cache.resolve("rejects");
CliOperation.Result<PatchOperation.PatchesSummary> result = PatchOperation.builder()
.logTo(new LoggingOutputStream(getLogger(), LogLevel.INFO))
.basePath(rawDecompiled)
.patchesPath(userdev.getUserdevJar().toPath())
.patchesPath(getSasOptions().get().getUserdevJar().get().getAsFile().toPath())
.patchesPrefix(patchPathInZip)
.outputPath(output)
.mode(PatchMode.ACCESS)
.rejectsPath(rejects)
.aPrefix(userdev.getConfig().patchesOriginalPrefix().orElseThrow())
.bPrefix(userdev.getConfig().patchesModifiedPrefix().orElseThrow())
.aPrefix(getPatchesOriginalPrefix().get())
.bPrefix(getPatchesModifiedPrefix().get())
.build()
.operate();
@@ -167,23 +234,20 @@ public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
return output;
}
private void remap(Path input, ServiceFactory serviceFactory) {
SourceRemapper remapper = new SourceRemapper(getProject(), serviceFactory, "srg", "named");
remapper.scheduleRemapSources(input.toFile(), getOutputJar().get().getAsFile(), false, true, () -> {
});
remapper.remapAll();
private void remap(Path input, ServiceFactory serviceFactory) throws IOException {
final SourceRemapperService remapperService = serviceFactory.get(getSourceRemapperOptions());
remapperService.remapSourcesJar(input, getOutputJar().get().getAsFile().toPath());
}
private void stripSideAnnotations(Path input, Path output) throws IOException {
private void stripSideAnnotations(Path input, Path output, ServiceFactory serviceFactory) throws IOException {
final Stopwatch stopwatch = Stopwatch.createStarted();
getLogger().lifecycle(":stripping side annotations");
try (var tempFiles = new TempFiles()) {
final ForgeUserdevProvider userdevProvider = getExtension().getForgeUserdevProvider();
final List<String> sass = userdevProvider.getConfig().sass();
final List<String> sass = getSasOptions().get().getSass().get();
final List<Path> sasPaths = new ArrayList<>();
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(userdevProvider.getUserdevJar(), false)) {
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(getSasOptions().get().getUserdevJar().get().getAsFile(), false)) {
for (String sasPath : sass) {
try {
final Path from = fs.getPath(sasPath);
@@ -196,10 +260,9 @@ public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
}
}
final FileCollection classpath = DependencyDownloader.download(getProject(), ForgeTools.SIDE_STRIPPER, false, true);
ForgeToolValueSource.exec(getProject(), spec -> {
spec.setClasspath(classpath);
final ForgeToolService toolService = serviceFactory.get(getToolServiceOptions());
toolService.exec(spec -> {
spec.setClasspath(getSasOptions().get().getClasspath());
spec.args(
"--strip",
"--input", input.toAbsolutePath().toString(),
@@ -214,4 +277,15 @@ public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask {
getLogger().lifecycle(":side annotations stripped in " + stopwatch.stop());
}
public interface SasOptions {
@InputFile
RegularFileProperty getUserdevJar();
@Input
ListProperty<String> getSass();
@Classpath
ConfigurableFileCollection getClasspath();
}
}