diff --git a/build.gradle b/build.gradle index 8dada2b7..d0ee384f 100644 --- a/build.gradle +++ b/build.gradle @@ -127,6 +127,7 @@ dependencies { implementation ('org.cadixdev:lorenz-asm:0.5.3') implementation ('de.oceanlabs.mcp:mcinjector:3.8.0') implementation ('com.opencsv:opencsv:5.4') + implementation ('net.minecraftforge:DiffPatch:2.0.7') // Testing testImplementation(gradleTestKit()) diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java index 782da709..659c84e0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java @@ -31,7 +31,10 @@ import java.util.List; import org.gradle.api.Project; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.forge.MinecraftPatchedProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraftProvider; +import net.fabricmc.loom.task.GenerateForgePatchedSourcesTask; import net.fabricmc.loom.task.GenerateSourcesTask; import net.fabricmc.loom.util.Constants; @@ -82,5 +85,16 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration { + task.setDescription("Decompile Minecraft using Forge's toolchain."); + task.setGroup(Constants.TaskGroup.FABRIC); + + task.getInputJar().set(MinecraftPatchedProvider.get(project).getMinecraftSrgJar().toFile()); + task.getRuntimeJar().set(inputJar); + }); + } } } 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 ec7ae141..e699c269 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 @@ -70,8 +70,7 @@ import org.objectweb.asm.tree.ClassNode; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.accesstransformer.AccessTransformerJarProcessor; -import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpConfigData; -import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpConfigStep; +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.minecraft.ForgeMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; @@ -177,10 +176,8 @@ public class MinecraftPatchedProvider { if (Files.notExists(minecraftSrgJar)) { this.dirty = true; - McpConfigData data = getExtension().getMcpConfigProvider().getData(); - List steps = data.steps().get(type.mcpId); - McpExecutor executor = new McpExecutor(project, minecraftProvider, Files.createTempDirectory("loom-mcp"), steps, data.functions()); - Path output = executor.executeUpTo("rename"); + McpExecutor executor = createMcpExecutor(Files.createTempDirectory("loom-mcp")); + Path output = executor.enqueue("rename").execute(); Files.copy(output, minecraftSrgJar); } @@ -544,6 +541,15 @@ public class MinecraftPatchedProvider { } } + public McpExecutor createMcpExecutor(Path cache) { + McpConfigProvider provider = getExtension().getMcpConfigProvider(); + return new McpExecutor(project, minecraftProvider, cache, provider, type.mcpId); + } + + public Path getMinecraftSrgJar() { + return minecraftSrgJar; + } + public Path getMinecraftPatchedSrgJar() { return minecraftPatchedSrgJar; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/ConfigValue.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/ConfigValue.java index baa919b1..a72ee987 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/ConfigValue.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/ConfigValue.java @@ -38,7 +38,6 @@ import java.util.function.Function; public sealed interface ConfigValue { String OUTPUT = "output"; String PREVIOUS_OUTPUT_SUFFIX = "Output"; - String SRG_MAPPINGS_NAME = "mappings"; /** * A special config value that is the path to a log file if absent. */ diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/DependencySet.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/DependencySet.java new file mode 100644 index 00000000..a1373359 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/DependencySet.java @@ -0,0 +1,107 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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 static net.fabricmc.loom.configuration.providers.forge.mcpconfig.ConfigValue.PREVIOUS_OUTPUT_SUFFIX; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import net.fabricmc.loom.util.function.CollectionUtil; + +public final class DependencySet { + private final Map allSteps; + private final List stepNames; + private final List> skipRules = new ArrayList<>(); + private final Set steps = new HashSet<>(); + private Predicate ignoreDependenciesFilter = data -> false; + + public DependencySet(List allSteps) { + this.allSteps = allSteps.stream().collect(Collectors.toMap(McpConfigStep::name, Function.identity())); + this.stepNames = CollectionUtil.map(allSteps, McpConfigStep::name); + } + + public void clear() { + steps.clear(); + } + + public void add(String step) { + if (!allSteps.containsKey(step)) { + return; + } + + steps.add(step); + } + + public void skip(String step) { + skip(data -> data.name().equals(step)); + } + + public void skip(Predicate rule) { + skipRules.add(rule); + } + + public void setIgnoreDependenciesFilter(Predicate ignoreDependenciesFilter) { + this.ignoreDependenciesFilter = ignoreDependenciesFilter; + } + + public SortedSet buildExecutionSet() { + SortedSet steps = new TreeSet<>(Comparator.comparingInt(stepNames::indexOf)); + Queue queue = new ArrayDeque<>(this.steps); + + while (!queue.isEmpty()) { + String step = queue.remove(); + McpConfigStep data = allSteps.get(step); + if (!allSteps.containsKey(step) || skipRules.stream().anyMatch(rule -> rule.test(data))) continue; + steps.add(step); + + if (!ignoreDependenciesFilter.test(allSteps.get(step))) { + allSteps.get(step).config().values().forEach(value -> { + if (value instanceof ConfigValue.Variable var) { + String name = var.name(); + + if (name.endsWith(PREVIOUS_OUTPUT_SUFFIX) && name.length() > PREVIOUS_OUTPUT_SUFFIX.length()) { + String substep = name.substring(0, name.length() - PREVIOUS_OUTPUT_SUFFIX.length()); + queue.offer(substep); + } + } + }); + } + } + + return steps; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigData.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigData.java index 9a0bf0a2..26afb5d3 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/McpConfigData.java @@ -35,14 +35,22 @@ import com.google.gson.JsonObject; /** * Data extracted from the MCPConfig JSON file. * + * @param data the value of the {@code data} property * @param mappingsPath the path to srg mappings inside the MCP zip * @param official the value of the {@code official} property * @param steps the MCP step definitions by environment type * @param functions the MCP function definitions by name */ -public record McpConfigData(String mappingsPath, boolean official, Map> steps, Map functions) { +public record McpConfigData( + JsonObject data, + String mappingsPath, + boolean official, + Map> steps, + Map functions +) { public static McpConfigData fromJson(JsonObject json) { - String mappingsPath = json.getAsJsonObject("data").get("mappings").getAsString(); + JsonObject data = json.getAsJsonObject("data"); + String mappingsPath = data.get("mappings").getAsString(); boolean official = json.has("official") && json.getAsJsonPrimitive("official").getAsBoolean(); JsonObject stepsJson = json.getAsJsonObject("steps"); @@ -65,6 +73,6 @@ public record McpConfigData(String mappingsPath, boolean official, Map new RuntimeException("Could not resolve MCPConfig")).toPath(); - if (!Files.exists(mcp) || !Files.exists(configJson) || refreshDeps()) { + if (!Files.exists(mcp) || !Files.exists(unpacked) || refreshDeps()) { Files.copy(mcpZip, mcp, StandardCopyOption.REPLACE_EXISTING); - Files.write(configJson, ZipUtils.unpack(mcp, "config.json"), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + // Delete existing files + if (Files.exists(unpacked)) { + Files.walkFileTree(unpacked, new DeletingFileVisitor()); + } + + Files.createDirectory(unpacked); + ZipUtils.unpackAll(mcp, unpacked); } JsonObject json; @@ -70,27 +76,19 @@ public class McpConfigProvider extends DependencyProvider { data = McpConfigData.fromJson(json); } - private void init(String version) throws IOException { + private void init(String version) { Path dir = getMinecraftProvider().dir("mcp/" + version).toPath(); mcp = dir.resolve("mcp.zip"); - configJson = dir.resolve("mcp-config.json"); - mappings = dir.resolve("mcp-config-mappings.txt"); - - if (refreshDeps()) { - Files.deleteIfExists(mappings); - } + unpacked = dir.resolve("unpacked"); + configJson = unpacked.resolve("config.json"); } public Path getMappings() { - if (Files.notExists(mappings)) { - try { - Files.write(mappings, ZipUtils.unpack(getMcp(), getMappingsPath()), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - } catch (IOException e) { - throw new IllegalStateException("Failed to find mappings '" + getMappingsPath() + "' in " + getMcp() + "!"); - } - } + return unpacked.resolve(getMappingsPath()); + } - return mappings; + public Path getUnpackedZip() { + return unpacked; } public Path getMcp() { 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 aa66b533..a0492536 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 @@ -32,13 +32,18 @@ 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.Stopwatch; import com.google.common.hash.Hashing; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.logging.LogLevel; @@ -50,8 +55,10 @@ import net.fabricmc.loom.LoomGradleExtension; 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; @@ -66,15 +73,55 @@ public final class McpExecutor { 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 extraConfig = new HashMap<>(); + private @Nullable StepLogic.Provider stepLogicProvider = null; - public McpExecutor(Project project, MinecraftProvider minecraftProvider, Path cache, List steps, Map functions) { + public McpExecutor(Project project, MinecraftProvider minecraftProvider, Path cache, McpConfigProvider provider, String environment) { this.project = project; this.minecraftProvider = minecraftProvider; this.cache = cache; - this.steps = steps; - this.functions = functions; + 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()); + + addDefaultFiles(provider, environment); + } + + 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 { @@ -105,8 +152,8 @@ public final class McpExecutor { return resolve(step, valueFromStep); } - if (name.equals(ConfigValue.SRG_MAPPINGS_NAME)) { - return LoomGradleExtension.get(project).getSrgProvider().getSrg().toAbsolutePath().toString(); + if (config.containsKey(name)) { + return config.get(name); } else if (extraConfig.containsKey(name)) { return extraConfig.get(name); } else if (name.equals(ConfigValue.LOG)) { @@ -117,35 +164,78 @@ public final class McpExecutor { }); } - public Path executeUpTo(String step) throws IOException { + /** + * 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(); - // Find the total number of steps we need to execute. - int totalSteps = CollectionUtil.find(steps, s -> s.name().equals(step)) - .map(s -> steps.indexOf(s) + 1) - .orElse(steps.size()); + int totalSteps = steps.size(); int currentStepIndex = 0; project.getLogger().log(STEP_LOG_LEVEL, ":executing {} MCP steps", totalSteps); for (McpConfigStep currentStep : steps) { currentStepIndex++; - StepLogic stepLogic = getStepLogic(currentStep.type()); + StepLogic stepLogic = getStepLogic(currentStep.name(), currentStep.type()); project.getLogger().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()); - - if (currentStep.name().equals(step)) { - break; - } } return Path.of(extraConfig.get(ConfigValue.OUTPUT)); } - private StepLogic getStepLogic(String type) { + /** + * 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()); @@ -154,6 +244,8 @@ public final class McpExecutor { 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)); @@ -178,8 +270,7 @@ public final class McpExecutor { @Override public Path setOutput(String fileName) throws IOException { - createStepCache(step.name()); - return setOutput(getStepCache(step.name()).resolve(fileName)); + return setOutput(cache().resolve(fileName)); } @Override @@ -190,6 +281,11 @@ public final class McpExecutor { return output; } + @Override + public Path cache() throws IOException { + return createStepCache(step.name()); + } + @Override public Path mappings() { return LoomGradleExtension.get(project).getMcpConfigProvider().getMappings(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/package-info.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/package-info.java index f982dc14..c61e8f83 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/package-info.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/package-info.java @@ -24,7 +24,5 @@ /** * A simple implementation for executing MCPConfig steps. - * Doesn't support all steps, just the ones up to {@code rename} - * and all custom functions. */ package net.fabricmc.loom.configuration.providers.forge.mcpconfig; 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 new file mode 100644 index 00000000..7bd432dd --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/InjectLogic.java @@ -0,0 +1,62 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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.steplogic; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Iterator; +import java.util.stream.Stream; + +import net.fabricmc.loom.configuration.providers.forge.mcpconfig.ConfigValue; +import net.fabricmc.loom.util.FileSystemUtil; + +public final class InjectLogic implements StepLogic { + @Override + public void execute(ExecutionContext context) throws IOException { + Path injectedFiles = Path.of(context.resolve(new ConfigValue.Variable("inject"))); + Path input = Path.of(context.resolve(new ConfigValue.Variable("input"))); + Path output = context.setOutput("output.jar"); + Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING); + + try (FileSystemUtil.Delegate targetFs = FileSystemUtil.getJarFileSystem(output, false)) { + FileSystem fs = targetFs.get(); + + try (Stream paths = Files.walk(injectedFiles)) { + Iterator iter = paths.filter(Files::isRegularFile).iterator(); + + while (iter.hasNext()) { + Path from = iter.next(); + Path relative = injectedFiles.relativize(from); + Path to = fs.getPath(relative.toString().replace(relative.getFileSystem().getSeparator(), "/")); + Files.createDirectories(to.getParent()); + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + } + } + } + } +} 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 new file mode 100644 index 00000000..06bd3aab --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/mcpconfig/steplogic/PatchLogic.java @@ -0,0 +1,60 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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.steplogic; + +import java.io.IOException; +import java.nio.file.Path; + +import codechicken.diffpatch.cli.CliOperation; +import codechicken.diffpatch.cli.PatchOperation; +import codechicken.diffpatch.util.LoggingOutputStream; +import codechicken.diffpatch.util.PatchMode; +import org.gradle.api.logging.LogLevel; + +import net.fabricmc.loom.configuration.providers.forge.mcpconfig.ConfigValue; + +public final class PatchLogic implements StepLogic { + @Override + public void execute(ExecutionContext context) throws IOException { + Path input = Path.of(context.resolve(new ConfigValue.Variable("input"))); + Path patches = Path.of(context.resolve(new ConfigValue.Variable("patches"))); + Path output = context.setOutput("output.jar"); + Path rejects = context.cache().resolve("rejects"); + + CliOperation.Result result = PatchOperation.builder() + .logTo(new LoggingOutputStream(context.logger(), LogLevel.INFO)) + .basePath(input) + .patchesPath(patches) + .outputPath(output) + .mode(PatchMode.OFFSET) + .rejectsPath(rejects) + .build() + .operate(); + + if (result.exit != 0) { + throw new RuntimeException("Could not patch " + input + "; rejects saved to " + rejects.toAbsolutePath()); + } + } +} 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 267e447d..ad0b93bc 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 @@ -28,6 +28,7 @@ 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 org.gradle.api.Action; @@ -48,10 +49,15 @@ public interface StepLogic { return stepName; } + default boolean hasNoContext() { + return false; + } + interface ExecutionContext { Logger logger(); Path setOutput(String fileName) throws IOException; Path setOutput(Path output); + Path cache() throws IOException; /** Mappings extracted from {@code data.mappings} in the MCPConfig JSON. */ Path mappings(); String resolve(ConfigValue value); @@ -64,4 +70,9 @@ public interface StepLogic { return CollectionUtil.map(configValues, this::resolve); } } + + @FunctionalInterface + interface Provider { + Optional getStepLogic(String name, String type); + } } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java new file mode 100644 index 00000000..b490d7f3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/GenerateForgePatchedSourcesTask.java @@ -0,0 +1,137 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import codechicken.diffpatch.cli.CliOperation; +import codechicken.diffpatch.cli.PatchOperation; +import codechicken.diffpatch.util.LoggingOutputStream; +import codechicken.diffpatch.util.PatchMode; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +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.steplogic.ConstantLogic; +import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; +import net.fabricmc.loom.util.SourceRemapper; + +public abstract class GenerateForgePatchedSourcesTask extends AbstractLoomTask { + /** + * The SRG Minecraft file produced by the MCP executor. + */ + @InputFile + public abstract RegularFileProperty getInputJar(); + + /** + * The runtime Minecraft file. + */ + @InputFile + public abstract RegularFileProperty getRuntimeJar(); + + /** + * The source jar. + */ + @OutputFile + public abstract RegularFileProperty getOutputJar(); + + public GenerateForgePatchedSourcesTask() { + getOutputs().upToDateWhen((o) -> false); + getOutputJar().fileProvider(getProject().provider(() -> GenerateSourcesTask.getMappedJarFileWithSuffix(getRuntimeJar(), "-sources.jar"))); + } + + @TaskAction + public void run() throws IOException { + Path cache = Files.createTempDirectory("loom-decompilation"); + // Step 1: decompile and patch with MCP patches + Path rawDecompiled = decompileAndPatch(cache); + // Step 2: patch with Forge patches + getLogger().lifecycle(":applying Forge patches"); + Path patched = sourcePatch(cache, rawDecompiled); + // Step 3: remap + remap(patched); + // Step 4: add Forge's own sources + ForgeSourcesRemapper.addForgeSources(getProject(), getOutputJar().get().getAsFile().toPath()); + } + + private Path decompileAndPatch(Path cache) 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(() -> getInputJar().get().getAsFile().toPath())); + } + + return Optional.empty(); + }); + mcp.enqueue("decompile"); + mcp.enqueue("patch"); + return mcp.execute(); + } + + private Path sourcePatch(Path cache, Path rawDecompiled) throws IOException { + ForgeUserdevProvider userdev = getExtension().getForgeUserdevProvider(); + String patchPathInZip = userdev.getJson().getAsJsonPrimitive("patches").getAsString(); + 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()) + .patchesPrefix(patchPathInZip) + .outputPath(output) + .mode(PatchMode.ACCESS) + .rejectsPath(rejects) + .aPrefix(userdev.getJson().getAsJsonPrimitive("patchesOriginalPrefix").getAsString()) + .bPrefix(userdev.getJson().getAsJsonPrimitive("patchesModifiedPrefix").getAsString()) + .build() + .operate(); + + if (result.exit != 0) { + throw new RuntimeException("Could not patch " + rawDecompiled + "; rejects saved to " + rejects.toAbsolutePath()); + } + + return output; + } + + private void remap(Path input) { + SourceRemapper remapper = new SourceRemapper(getProject(), "srg", "named"); + remapper.scheduleRemapSources(input.toFile(), getOutputJar().get().getAsFile(), false, true, () -> { + }); + remapper.remapAll(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java index b7f37e33..f3461fb8 100644 --- a/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java +++ b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java @@ -55,6 +55,24 @@ public final class CollectionUtil { return Optional.empty(); } + /** + * Finds the index of the first element matching the predicate. + * + * @param list the list to be searched + * @param filter the predicate to be matched + * @param the element type + * @return the index of the first matching element, or -1 if none match + */ + public static int indexOf(List list, Predicate filter) { + for (int i = 0; i < list.size(); i++) { + if (filter.test(list.get(i))) { + return i; + } + } + + return -1; + } + /** * Transforms the collection with a function. * diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy index dc266e31..e242fedf 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/forge/ForgeTest.groovy @@ -41,7 +41,7 @@ class ForgeTest extends Specification implements GradleProjectTestTrait { .replace('@MAPPINGS@', mappings) when: - def result = gradle.run(task: "build", args: "") + def result = gradle.run(task: "build") then: result.task(":build").outcome == SUCCESS diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/forge/PatchedDecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/forge/PatchedDecompileTest.groovy new file mode 100644 index 00000000..e68f011d --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/forge/PatchedDecompileTest.groovy @@ -0,0 +1,57 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.integration.forge + +import net.fabricmc.loom.test.util.GradleProjectTestTrait +import spock.lang.Specification +import spock.lang.Unroll + +import static net.fabricmc.loom.test.LoomTestConstants.DEFAULT_GRADLE +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class PatchedDecompileTest extends Specification implements GradleProjectTestTrait { + @Unroll + def "decompile #mcVersion #forgeVersion"() { + setup: + def gradle = gradleProject(project: "forge/simple", version: DEFAULT_GRADLE) + gradle.buildGradle.text = gradle.buildGradle.text.replace('@MCVERSION@', mcVersion) + .replace('@FORGEVERSION@', forgeVersion) + .replace('@MAPPINGS@', 'loom.officialMojangMappings()') + + when: + def result = gradle.run(task: "genForgePatchedSources") + + then: + result.task(":genForgePatchedSources").outcome == SUCCESS + + where: + mcVersion | forgeVersion + '1.19.2' | "43.1.1" + '1.18.1' | "39.0.63" + '1.17.1' | "37.0.67" + '1.16.5' | "36.2.4" + '1.14.4' | "28.2.23" + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/forge/DependencySetTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/forge/DependencySetTest.groovy new file mode 100644 index 00000000..d2ae5e42 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/forge/DependencySetTest.groovy @@ -0,0 +1,90 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.forge + +import net.fabricmc.loom.configuration.providers.forge.mcpconfig.ConfigValue +import net.fabricmc.loom.configuration.providers.forge.mcpconfig.DependencySet +import net.fabricmc.loom.configuration.providers.forge.mcpconfig.McpConfigStep +import spock.lang.Shared +import spock.lang.Specification + +class DependencySetTest extends Specification { + /* + orphanA + orphanB + root + -> childA1 -> childA2 --> childAB + -> childB / + */ + @Shared List allSteps = [ + new McpConfigStep('foo', 'orphanA', [:]), + new McpConfigStep('foo', 'orphanB', [:]), + new McpConfigStep('bar', 'root', [:]), + new McpConfigStep('bar', 'childA1', [input: ConfigValue.of('{rootOutput}')]), + new McpConfigStep('bar', 'childA2', [input: ConfigValue.of('{childA1Output}')]), + new McpConfigStep('bar', 'childB', [input: ConfigValue.of('{rootOutput}')]), + new McpConfigStep( + 'bar', 'childAB', + [inputA: ConfigValue.of('{childA2Output}'), inputB: ConfigValue.of("{childBOutput}")] + ), + ] + + DependencySet dependencySet = new DependencySet(allSteps) + + def "single child"() { + when: + dependencySet.add('childAB') + def executedSteps = dependencySet.buildExecutionSet() + then: + executedSteps.toList() == ['root', 'childA1', 'childA2', 'childB', 'childAB'] + } + + def "multiple children"() { + when: + dependencySet.add('childA1') + dependencySet.add('orphanB') + def executedSteps = dependencySet.buildExecutionSet() + then: + executedSteps.toList() == ['orphanB', 'root', 'childA1'] + } + + def "skip rule"() { + when: + dependencySet.add('childAB') + dependencySet.skip('childA2') + def executedSteps = dependencySet.buildExecutionSet() + then: + executedSteps.toList() == ['root', 'childB', 'childAB'] + } + + def "ignore dependencies filter"() { + when: + dependencySet.add('childAB') + dependencySet.ignoreDependenciesFilter = { it.name() == 'childA2' } + def executedSteps = dependencySet.buildExecutionSet() + then: + executedSteps.toList() == ['root', 'childA2', 'childB', 'childAB'] + } +}