Add genForgePatchedSources task (#98)

* Add genForgePatchedSources task

Closes #32. Also reworks the MCP executor system quite heavily:

- Instead of executing all steps up to `x`, it now resolves dependencies based on step inputs
- No-op steps are skipped
- Steps can be overridden with custom logic by callers
- You can add multiple desired steps to one round of execution
- All step types in MCPConfig 1.14-1.19 are supported now
- Variables that reference MCPConfig zip contents now work
  - Removed special case for `{mappings}` variable

Other:
- the MCPConfig zip is now completely extracted into the cache dir instead of just the config file
This commit is contained in:
Juuxel
2022-09-07 15:36:05 +03:00
committed by GitHub
parent f8603e11ef
commit ea9dc0baa5
17 changed files with 713 additions and 51 deletions

View File

@@ -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())

View File

@@ -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<Mapp
task.dependsOn(project.getTasks().named("genSourcesWithCfr"));
});
// TODO: Support for env-only jars?
if (extension.isForge() && extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.MERGED) {
project.getTasks().register("genForgePatchedSources", GenerateForgePatchedSourcesTask.class, task -> {
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);
});
}
}
}

View File

@@ -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<McpConfigStep> 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;
}

View File

@@ -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.
*/

View File

@@ -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<String, McpConfigStep> allSteps;
private final List<String> stepNames;
private final List<Predicate<McpConfigStep>> skipRules = new ArrayList<>();
private final Set<String> steps = new HashSet<>();
private Predicate<McpConfigStep> ignoreDependenciesFilter = data -> false;
public DependencySet(List<McpConfigStep> 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<McpConfigStep> rule) {
skipRules.add(rule);
}
public void setIgnoreDependenciesFilter(Predicate<McpConfigStep> ignoreDependenciesFilter) {
this.ignoreDependenciesFilter = ignoreDependenciesFilter;
}
public SortedSet<String> buildExecutionSet() {
SortedSet<String> steps = new TreeSet<>(Comparator.comparingInt(stepNames::indexOf));
Queue<String> 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;
}
}

View File

@@ -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<String, List<McpConfigStep>> steps, Map<String, McpConfigFunction> functions) {
public record McpConfigData(
JsonObject data,
String mappingsPath,
boolean official,
Map<String, List<McpConfigStep>> steps,
Map<String, McpConfigFunction> 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<String, L
functionsBuilder.put(key, McpConfigFunction.fromJson(functionsJson.getAsJsonObject(key)));
}
return new McpConfigData(mappingsPath, official, stepsBuilder.build(), functionsBuilder.build());
return new McpConfigData(data, mappingsPath, official, stepsBuilder.build(), functionsBuilder.build());
}
}

View File

@@ -24,12 +24,10 @@
package net.fabricmc.loom.configuration.providers.forge.mcpconfig;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
@@ -38,12 +36,13 @@ import org.gradle.api.Project;
import net.fabricmc.loom.configuration.DependencyInfo;
import net.fabricmc.loom.configuration.providers.forge.DependencyProvider;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.DeletingFileVisitor;
import net.fabricmc.loom.util.ZipUtils;
public class McpConfigProvider extends DependencyProvider {
private Path mcp;
private Path configJson;
private Path mappings;
private Path unpacked;
private McpConfigData data;
public McpConfigProvider(Project project) {
@@ -56,9 +55,16 @@ public class McpConfigProvider extends DependencyProvider {
Path mcpZip = dependency.resolveFile().orElseThrow(() -> 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() {

View File

@@ -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<McpConfigStep> steps;
private final DependencySet dependencySet;
private final Map<String, McpConfigFunction> functions;
private final Map<String, String> config = new HashMap<>();
private final Map<String, String> extraConfig = new HashMap<>();
private @Nullable StepLogic.Provider stepLogicProvider = null;
public McpExecutor(Project project, MinecraftProvider minecraftProvider, Path cache, List<McpConfigStep> steps, Map<String, McpConfigFunction> 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<String, JsonElement> entry : provider.getData().data().entrySet()) {
if (entry.getValue().isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), entry.getValue().getAsString());
} else if (entry.getValue().isJsonObject()) {
JsonObject json = entry.getValue().getAsJsonObject();
if (json.has(environment) && json.get(environment).isJsonPrimitive()) {
addDefaultFile(provider, entry.getKey(), json.getAsJsonPrimitive(environment).getAsString());
}
}
}
}
private void addDefaultFile(McpConfigProvider provider, String key, String value) {
Path path = provider.getUnpackedZip().resolve(value).toAbsolutePath();
if (!path.startsWith(provider.getUnpackedZip().toAbsolutePath())) {
// This is probably not what we're looking for since it falls outside the directory.
return;
} else if (Files.notExists(path)) {
// Not a real file, let's continue.
return;
}
addConfig(key, path.toString());
}
public void addConfig(String key, String value) {
config.put(key, value);
}
private Path getDownloadCache() throws IOException {
@@ -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<String> stepNames = dependencySet.buildExecutionSet();
dependencySet.clear();
List<McpConfigStep> toExecute = new ArrayList<>();
for (String stepName : stepNames) {
McpConfigStep step = CollectionUtil.find(steps, s -> s.name().equals(stepName))
.orElseThrow(() -> new NoSuchElementException("Step '" + stepName + "' not found in MCP config"));
toExecute.add(step);
}
return executeSteps(toExecute);
}
/**
* Executes the specified steps.
*
* @param steps the steps to execute
* @return the output file of the last executed step
*/
public Path executeSteps(List<McpConfigStep> steps) throws IOException {
extraConfig.clear();
// 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();

View File

@@ -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;

View File

@@ -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<Path> paths = Files.walk(injectedFiles)) {
Iterator<Path> 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);
}
}
}
}
}

View File

@@ -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<PatchOperation.PatchesSummary> 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());
}
}
}

View File

@@ -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<StepLogic> getStepLogic(String name, String type);
}
}

View File

@@ -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<PatchOperation.PatchesSummary> 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();
}
}

View File

@@ -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 <E> the element type
* @return the index of the first matching element, or -1 if none match
*/
public static <E> int indexOf(List<? extends E> list, Predicate<? super E> 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.
*

View File

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

View File

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

View File

@@ -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<McpConfigStep> 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']
}
}