diff --git a/src/main/java/dev/architectury/loom/forge/tool/AccessTransformerService.java b/src/main/java/dev/architectury/loom/forge/tool/AccessTransformerService.java new file mode 100644 index 00000000..22b0db31 --- /dev/null +++ b/src/main/java/dev/architectury/loom/forge/tool/AccessTransformerService.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, AccessTransformerService.class); + + public interface Options extends Service.Options { + @InputFiles + ConfigurableFileCollection getAccessTransformers(); + + @Input + Property getMainClass(); + + @Classpath + ConfigurableFileCollection getClasspath(); + + @Nested + Property getToolServiceOptions(); + } + + public static Provider 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 createOptionsForLoaderAts(Project project, TempFiles tempFiles) { + final Provider> 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 extractAccessTransformers(Path jar, UserdevConfig.AccessTransformerLocation location, TempFiles tempFiles) throws IOException { + final List 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 getAccessTransformerPaths(FileSystemUtil.Delegate fs, UserdevConfig.AccessTransformerLocation location) throws IOException { + return location.visitIo(directory -> { + Path dirPath = fs.getPath(directory); + + try (Stream 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 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()); + }); + } +} diff --git a/src/main/java/dev/architectury/loom/forge/tool/ForgeToolExecutor.java b/src/main/java/dev/architectury/loom/forge/tool/ForgeToolExecutor.java index c793cd23..b17233c9 100644 --- a/src/main/java/dev/architectury/loom/forge/tool/ForgeToolExecutor.java +++ b/src/main/java/dev/architectury/loom/forge/tool/ForgeToolExecutor.java @@ -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 getExecutable(); @Input + @Optional ListProperty getProgramArgs(); @Input + @Optional ListProperty getJvmArgs(); @Input + @Optional Property getMainClass(); @Classpath diff --git a/src/main/java/dev/architectury/loom/forge/tool/ForgeToolService.java b/src/main/java/dev/architectury/loom/forge/tool/ForgeToolService.java new file mode 100644 index 00000000..5e4df3ba --- /dev/null +++ b/src/main/java/dev/architectury/loom/forge/tool/ForgeToolService.java @@ -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 { + public static final ServiceType 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 getBaseSettings(); + + @Inject + ExecOperations getExecOperations(); + } + + public static Provider 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 configurator) { + return getOptions().getExecOperations().javaexec(spec -> { + ForgeToolExecutor.applyToSpec(getOptions().getBaseSettings().get(), spec); + configurator.execute(spec); + }); + } +} diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index 6bfd378b..49483cf2 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -215,4 +215,6 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { default Path getPlatformMappingFile() { return getMappingConfiguration().getPlatformMappingFile(this); } + + boolean manualRefreshDeps(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/accesstransformer/AccessTransformerJarProcessor.java b/src/main/java/net/fabricmc/loom/configuration/accesstransformer/AccessTransformerJarProcessor.java index c7f81bce..8611b91c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/accesstransformer/AccessTransformerJarProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/accesstransformer/AccessTransformerJarProcessor.java @@ -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 { private static final Logger LOGGER = Logging.getLogger(AccessTransformerJarProcessor.class); @@ -114,16 +113,14 @@ public class AccessTransformerJarProcessor implements MinecraftJarProcessor { - 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 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; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ConfigValue.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ConfigValue.java index d288910f..f804fd29 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ConfigValue.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ConfigValue.java @@ -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. */ diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java index 7015bcc6..705d8b68 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java @@ -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 extractAccessTransformers(Path jar, UserdevConfig.AccessTransformerLocation location, TempFiles tempFiles) throws IOException { - final List 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 getAccessTransformerPaths(FileSystemUtil.Delegate fs, UserdevConfig.AccessTransformerLocation location) throws IOException { - return location.visitIo(directory -> { - Path dirPath = fs.getPath(directory); - - try (Stream 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() { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigFunction.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigFunction.java index fd26e867..0a072e72 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigFunction.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigFunction.java @@ -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 args, List jvmArgs, @Nullable String repo) { +public record McpConfigFunction(String version, List args, List 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 { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigStep.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigStep.java index 2b9295a1..97e05e1d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigStep.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigStep.java @@ -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 config) { +public record McpConfigStep(String type, String name, Map config) implements Serializable { private static final String TYPE_KEY = "type"; private static final String NAME_KEY = "name"; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutor.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutor.java index 6e0ffa4d..94e3a45d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutor.java @@ -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 { + public static final ServiceType 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 steps; - private final DependencySet dependencySet; - private final Map functions; - private final Map config = new HashMap<>(); + private final Map config; private final Map 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 getStepLogicOptions(); + + /** + * The requested steps. + */ + @Input + ListProperty 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 getInitialConfig(); + + // Download settings + @Input + Property getOffline(); + + @Input + Property getManualRefreshDeps(); + + // Services + @Nested + Property 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 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 stepNames = dependencySet.buildExecutionSet(); - dependencySet.clear(); - List 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 steps) throws IOException { - extraConfig.clear(); - + List 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 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 configurator) { - ForgeToolValueSource.exec(project, configurator); - } - - @Override - public Set 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 configurator) { + final ForgeToolService toolService = getServiceFactory().get(getOptions().getToolServiceOptions()); + toolService.exec(configurator); } } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutorBuilder.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutorBuilder.java new file mode 100644 index 00000000..e0f8bc0b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpExecutorBuilder.java @@ -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 steps; + private final DependencySet dependencySet; + private final Map functions; + private final Map 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 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 build() throws IOException { + SortedSet stepNames = dependencySet.buildExecutionSet(); + dependencySet.clear(); + List 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 getStepLogic(String name, String type) { + if (stepLogicProvider != null) { + final @Nullable Provider 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 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 files = project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES).resolve(); + return project.files(files); + })::get); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ConstantLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ConstantLogic.java index 964c85fb..3b2301c6 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ConstantLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ConstantLogic.java @@ -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; +public final class ConstantLogic extends StepLogic { + public static final ServiceType TYPE = new ServiceType<>(Options.class, ConstantLogic.class); - public ConstantLogic(Supplier path) { - this.path = path; + public interface Options extends Service.Options { + @Input + Property getFile(); + } + + public static Provider createOptions(SetupContext context, Supplier 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())); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/DownloadManifestFileLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/DownloadManifestFileLogic.java index 2f2531c1..09e5723f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/DownloadManifestFileLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/DownloadManifestFileLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, DownloadManifestFileLogic.class); - public DownloadManifestFileLogic(MinecraftVersionMeta.Download download) { - this.download = download; + public interface Options extends Service.Options { + @Input + Property getDownload(); + } + + public static Provider 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")); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/FunctionLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/FunctionLogic.java index fe67eb65..0162728a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/FunctionLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/FunctionLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, FunctionLogic.class); - public FunctionLogic(McpConfigFunction function) { - this.function = function; + public interface Options extends Service.Options { + @Input + Property getFunction(); + + @InputFile + RegularFileProperty getToolJar(); + } + + public static Provider createOptions(SetupContext context, McpConfigFunction function) { + return TYPE.create(context.project(), options -> { + options.getFunction().set(function); + final Provider 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(); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/InjectLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/InjectLogic.java index 60fd852a..1dec5da1 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/InjectLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/InjectLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, InjectLogic.class); + + public interface Options extends Service.Options { + } + + public static Provider 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"))); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ListLibrariesLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ListLibrariesLogic.java index 12721dfc..480cdc8e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ListLibrariesLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/ListLibrariesLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, ListLibrariesLogic.class); + + public interface Options extends Service.Options { + @InputFiles + ConfigurableFileCollection getMinecraftLibraries(); + } + + public static Provider 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()); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/NoOpLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/NoOpLogic.java index ce54f333..e09a30dc 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/NoOpLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/NoOpLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(Options.class, NoOpLogic.class); + + public interface Options extends Service.Options { + } + + public static Provider 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 { } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/PatchLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/PatchLogic.java index 1e4fe546..5fe39df0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/PatchLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/PatchLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(PatchLogic.Options.class, PatchLogic.class); + + public interface Options extends Service.Options { + } + + public static Provider 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"))); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StepLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StepLogic.java index 0b7908ca..132a1477 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StepLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StepLogic.java @@ -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 extends Service { + 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 configurator); - Set getMinecraftLibraries(); + void javaexec(Action configurator); default List resolve(List configValues) { return CollectionUtil.map(configValues, this::resolve); } } + public interface SetupContext { + Project project(); + Path downloadFile(String url) throws IOException; + Path downloadDependency(String notation); + Provider getMinecraftLibraries(); + } + @FunctionalInterface - interface Provider { - Optional getStepLogic(String name, String type); + public interface StepLogicProvider { + @Nullable Provider getStepLogic(SetupContext context, String name, String type); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StripLogic.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StripLogic.java index cf8df4d5..30be0c01 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StripLogic.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/StripLogic.java @@ -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 { + public static final ServiceType TYPE = new ServiceType<>(StripLogic.Options.class, StripLogic.class); + + public interface Options extends Service.Options { + } + + public static Provider 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 filter = Files.readAllLines(context.mappings(), StandardCharsets.UTF_8).stream() diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java index 5325cc38..d8a58a89 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java @@ -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()); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 3974279a..b0f9e2fe 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -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"); } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java index e9a33ccd..ef4e630d 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java @@ -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 getForgeSourcesOptions(); + protected abstract Property getForgeSourcesOptions(); + + @Nested + protected abstract Property getMcpExecutorOptions(); + + @Nested + protected abstract Property getAccessTransformerOptions(); + + @Nested + protected abstract Property getToolServiceOptions(); + + @Nested + protected abstract Property getSourceRemapperOptions(); + + @Nested + protected abstract Property getSasOptions(); + + @Input + protected abstract Property getPatchPathInZip(); + + @Input + protected abstract Property getPatchesOriginalPrefix(); + + @Input + protected abstract Property getPatchesModifiedPrefix(); + + @Internal + protected abstract Property 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 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 sass = userdevProvider.getConfig().sass(); + final List sass = getSasOptions().get().getSass().get(); final List 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 getSass(); + + @Classpath + ConfigurableFileCollection getClasspath(); + } }