mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-04-03 05:57:42 -05:00
485 lines
18 KiB
Java
485 lines
18 KiB
Java
/*
|
|
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
|
*
|
|
* Copyright (c) 2016, 2017, 2018 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.providers;
|
|
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.gson.JsonParser;
|
|
import de.oceanlabs.mcp.mcinjector.adaptors.ParameterAnnotationFixer;
|
|
import net.fabricmc.loom.util.*;
|
|
import net.fabricmc.loom.util.function.FsPathConsumer;
|
|
import net.fabricmc.loom.util.srg.InnerClassRemapper;
|
|
import net.fabricmc.loom.util.srg.SpecialSourceExecutor;
|
|
import net.fabricmc.mapping.tree.TinyTree;
|
|
import net.fabricmc.tinyremapper.OutputConsumerPath;
|
|
import net.fabricmc.tinyremapper.TinyRemapper;
|
|
import net.minecraftforge.accesstransformer.TransformerProcessor;
|
|
import net.minecraftforge.binarypatcher.ConsoleTool;
|
|
import org.apache.commons.io.FileUtils;
|
|
import org.apache.commons.io.IOUtils;
|
|
import org.apache.commons.io.output.NullOutputStream;
|
|
import org.gradle.api.Project;
|
|
import org.gradle.api.logging.Logger;
|
|
import org.gradle.api.plugins.JavaPluginConvention;
|
|
import org.gradle.api.tasks.SourceSet;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.objectweb.asm.ClassReader;
|
|
import org.objectweb.asm.ClassVisitor;
|
|
import org.objectweb.asm.ClassWriter;
|
|
import org.objectweb.asm.tree.ClassNode;
|
|
import org.zeroturnaround.zip.ZipUtil;
|
|
|
|
import java.io.*;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.FileSystem;
|
|
import java.nio.file.*;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Locale;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
|
|
public class MinecraftPatchedProvider extends DependencyProvider {
|
|
private final MappingsProvider mappingsProvider;
|
|
private File minecraftClientSrgJar;
|
|
private File minecraftServerSrgJar;
|
|
private File minecraftClientPatchedSrgJar;
|
|
private File minecraftServerPatchedSrgJar;
|
|
private File minecraftClientPatchedOfficialJar;
|
|
private File minecraftServerPatchedOfficialJar;
|
|
private File minecraftMergedPatchedJar;
|
|
private File projectAtHash;
|
|
@Nullable
|
|
private File projectAt = null;
|
|
private boolean atDirty = false;
|
|
|
|
public MinecraftPatchedProvider(MappingsProvider mappingsProvider, Project project) {
|
|
super(project);
|
|
this.mappingsProvider = mappingsProvider;
|
|
}
|
|
|
|
public void initFiles() throws IOException {
|
|
projectAtHash = new File(getExtension().getProjectPersistentCache(), "at.sha256");
|
|
|
|
SourceSet main = getProject().getConvention().findPlugin(JavaPluginConvention.class).getSourceSets().getByName("main");
|
|
|
|
for (File srcDir : main.getResources().getSrcDirs()) {
|
|
File projectAt = new File(srcDir, "META-INF/accesstransformer.cfg");
|
|
|
|
if (projectAt.exists()) {
|
|
this.projectAt = projectAt;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isRefreshDeps() || !projectAtHash.exists()) {
|
|
writeAtHash();
|
|
atDirty = projectAt != null;
|
|
} else {
|
|
byte[] expected = com.google.common.io.Files.asByteSource(projectAtHash).read();
|
|
byte[] current = projectAt != null ? Checksum.sha256(projectAt) : Checksum.sha256("");
|
|
boolean mismatched = !Arrays.equals(current, expected);
|
|
|
|
if (mismatched) {
|
|
writeAtHash();
|
|
}
|
|
|
|
atDirty = mismatched && projectAt != null;
|
|
}
|
|
|
|
MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider();
|
|
PatchProvider patchProvider = getExtension().getPatchProvider();
|
|
String minecraftVersion = minecraftProvider.getMinecraftVersion();
|
|
String jarSuffix = "-patched-forge-" + patchProvider.forgeVersion;
|
|
minecraftProvider.setJarSuffix(jarSuffix);
|
|
|
|
File cache = usesProjectCache() ? getExtension().getProjectPersistentCache() : getExtension().getUserCache();
|
|
|
|
minecraftClientPatchedOfficialJar = new File(cache, "minecraft-" + minecraftVersion + "-client" + jarSuffix + ".jar");
|
|
minecraftServerPatchedOfficialJar = new File(cache, "minecraft-" + minecraftVersion + "-server" + jarSuffix + ".jar");
|
|
minecraftClientSrgJar = new File(cache, "minecraft-" + minecraftVersion + "-client-srg.jar");
|
|
minecraftServerSrgJar = new File(cache, "minecraft-" + minecraftVersion + "-server-srg.jar");
|
|
minecraftClientPatchedSrgJar = new File(cache, "minecraft-" + minecraftVersion + "-client-srg" + jarSuffix + ".jar");
|
|
minecraftServerPatchedSrgJar = new File(cache, "minecraft-" + minecraftVersion + "-server-srg" + jarSuffix + ".jar");
|
|
minecraftMergedPatchedJar = new File(cache, "minecraft-" + minecraftVersion + "-merged" + jarSuffix + ".jar");
|
|
|
|
if (isRefreshDeps()) {
|
|
cleanCache();
|
|
}
|
|
|
|
if (!minecraftClientSrgJar.exists() || !minecraftServerSrgJar.exists()
|
|
|| !minecraftClientPatchedSrgJar.exists() || !minecraftServerPatchedSrgJar.exists()
|
|
|| !minecraftMergedPatchedJar.exists()) {
|
|
minecraftClientSrgJar.delete();
|
|
minecraftServerSrgJar.delete();
|
|
minecraftClientPatchedSrgJar.delete();
|
|
minecraftServerPatchedSrgJar.delete();
|
|
minecraftMergedPatchedJar.delete();
|
|
}
|
|
}
|
|
|
|
public void cleanCache() {
|
|
for (File file : Arrays.asList(
|
|
projectAtHash,
|
|
minecraftClientSrgJar,
|
|
minecraftServerSrgJar,
|
|
minecraftClientPatchedSrgJar,
|
|
minecraftServerPatchedSrgJar,
|
|
minecraftClientPatchedOfficialJar,
|
|
minecraftServerPatchedOfficialJar,
|
|
minecraftMergedPatchedJar
|
|
)) {
|
|
file.delete();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception {
|
|
initFiles();
|
|
|
|
if (atDirty) {
|
|
getProject().getLogger().lifecycle(":found dirty access transformers");
|
|
}
|
|
|
|
if (atDirty || !minecraftClientPatchedOfficialJar.exists() || !minecraftServerPatchedOfficialJar.exists()) {
|
|
if (!minecraftClientSrgJar.exists() || !minecraftServerSrgJar.exists()) {
|
|
// Remap official jars to MCPConfig remapped srg jars
|
|
createSrgJars(getProject().getLogger());
|
|
}
|
|
|
|
if (atDirty || !minecraftClientPatchedSrgJar.exists() || !minecraftServerPatchedSrgJar.exists()) {
|
|
patchJars(getProject().getLogger());
|
|
injectForgeClasses(getProject().getLogger());
|
|
}
|
|
|
|
remapPatchedJars(getProject().getLogger());
|
|
}
|
|
|
|
if (atDirty || !minecraftMergedPatchedJar.exists()) {
|
|
mergeJars(getProject().getLogger());
|
|
}
|
|
}
|
|
|
|
private void writeAtHash() throws IOException {
|
|
try (FileOutputStream out = new FileOutputStream(projectAtHash)) {
|
|
if (projectAt != null) {
|
|
out.write(Checksum.sha256(projectAt));
|
|
} else {
|
|
out.write(Checksum.sha256(""));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void createSrgJars(Logger logger) throws Exception {
|
|
McpConfigProvider mcpProvider = getExtension().getMcpConfigProvider();
|
|
|
|
MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider();
|
|
|
|
String[] mappingsPath = {null};
|
|
if (!ZipUtil.handle(mcpProvider.getMcp(), "config.json", (in, zipEntry) -> {
|
|
mappingsPath[0] = new JsonParser().parse(new InputStreamReader(in)).getAsJsonObject().get("data").getAsJsonObject().get("mappings").getAsString();
|
|
})) {
|
|
throw new IllegalStateException("Failed to find 'config.json' in " + mcpProvider.getMcp().getAbsolutePath() + "!");
|
|
}
|
|
|
|
Path[] tmpSrg = {null};
|
|
if (!ZipUtil.handle(mcpProvider.getMcp(), mappingsPath[0], (in, zipEntry) -> {
|
|
tmpSrg[0] = Files.createTempFile(null, null);
|
|
try (BufferedWriter writer = Files.newBufferedWriter(tmpSrg[0])) {
|
|
IOUtils.copy(in, writer, StandardCharsets.UTF_8);
|
|
}
|
|
})) {
|
|
throw new IllegalStateException("Failed to find mappings '" + mappingsPath[0] + "' in " + mcpProvider.getMcp().getAbsolutePath() + "!");
|
|
}
|
|
|
|
File specialSourceJar = new File(getExtension().getUserCache(), "SpecialSource-1.8.3-shaded.jar");
|
|
DownloadUtil.downloadIfChanged(new URL("https://repo1.maven.org/maven2/net/md-5/SpecialSource/1.8.3/SpecialSource-1.8.3-shaded.jar"), specialSourceJar, getProject().getLogger(), true);
|
|
|
|
ThreadingUtils.run(() -> {
|
|
Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), mappingsProvider, "client", specialSourceJar, minecraftProvider.minecraftClientJar.toPath(), tmpSrg[0]), minecraftClientSrgJar.toPath());
|
|
}, () -> {
|
|
Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), mappingsProvider, "server", specialSourceJar, minecraftProvider.minecraftServerJar.toPath(), tmpSrg[0]), minecraftServerSrgJar.toPath());
|
|
});
|
|
}
|
|
|
|
private void fixParameterAnnotation(File jarFile) throws Exception {
|
|
getProject().getLogger().info(":fixing parameter annotations for " + jarFile.toString());
|
|
try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + jarFile.toURI()), ImmutableMap.of("create", false))) {
|
|
for (Path rootDir : fs.getRootDirectories()) {
|
|
for (Path file : (Iterable<? extends Path>) Files.walk(rootDir)::iterator) {
|
|
if (!file.toString().endsWith(".class")) continue;
|
|
byte[] bytes = Files.readAllBytes(file);
|
|
ClassReader reader = new ClassReader(bytes);
|
|
ClassNode node = new ClassNode();
|
|
ClassVisitor visitor = new ParameterAnnotationFixer(node, null);
|
|
reader.accept(visitor, 0);
|
|
|
|
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
|
node.accept(writer);
|
|
byte[] out = writer.toByteArray();
|
|
if (!Arrays.equals(bytes, out)) {
|
|
Files.delete(file);
|
|
Files.write(file, out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void injectForgeClasses(Logger logger) throws IOException {
|
|
logger.lifecycle(":injecting forge classes into minecraft");
|
|
ThreadingUtils.run(() -> {
|
|
copyAll(getExtension().getForgeUniversalProvider().getForge(), minecraftClientPatchedSrgJar);
|
|
copyUserdevFiles(getExtension().getForgeUserdevProvider().getUserdevJar(), minecraftClientPatchedSrgJar);
|
|
}, () -> {
|
|
copyAll(getExtension().getForgeUniversalProvider().getForge(), minecraftServerPatchedSrgJar);
|
|
copyUserdevFiles(getExtension().getForgeUserdevProvider().getUserdevJar(), minecraftServerPatchedSrgJar);
|
|
});
|
|
|
|
logger.lifecycle(":injecting loom classes into minecraft");
|
|
File injection = File.createTempFile("loom-injection", ".jar");
|
|
|
|
try (InputStream in = MinecraftProvider.class.getResourceAsStream("/inject/injection.jar")) {
|
|
FileUtils.copyInputStreamToFile(in, injection);
|
|
}
|
|
|
|
for (Environment environment : Environment.values()) {
|
|
String side = environment.side();
|
|
File target = environment.patchedSrgJar.apply(this);
|
|
walkFileSystems(injection, target, it -> !it.getFileName().toString().equals("MANIFEST.MF"), this::copyReplacing);
|
|
|
|
logger.lifecycle(":access transforming minecraft (" + side + ")");
|
|
|
|
File atJar = File.createTempFile("at" + side, ".jar");
|
|
File at = File.createTempFile("at" + side, ".cfg");
|
|
FileUtils.copyFile(target, atJar);
|
|
JarUtil.extractFile(atJar, "META-INF/accesstransformer.cfg", at);
|
|
String[] args = new String[]{
|
|
"--inJar", atJar.getAbsolutePath(),
|
|
"--outJar", target.getAbsolutePath(),
|
|
"--atFile", at.getAbsolutePath()
|
|
};
|
|
|
|
if (projectAt != null) {
|
|
args = Arrays.copyOf(args, args.length + 2);
|
|
args[args.length - 2] = "--atFile";
|
|
args[args.length - 1] = projectAt.getAbsolutePath();
|
|
}
|
|
|
|
TransformerProcessor.main(args);
|
|
};
|
|
}
|
|
|
|
private enum Environment {
|
|
CLIENT(provider -> provider.minecraftClientPatchedSrgJar, provider -> provider.minecraftClientPatchedOfficialJar),
|
|
SERVER(provider -> provider.minecraftServerPatchedSrgJar, provider -> provider.minecraftServerPatchedOfficialJar);
|
|
|
|
Function<MinecraftPatchedProvider, File> patchedSrgJar;
|
|
Function<MinecraftPatchedProvider, File> patchedOfficialJar;
|
|
|
|
Environment(Function<MinecraftPatchedProvider, File> patchedSrgJar, Function<MinecraftPatchedProvider, File> patchedOfficialJar) {
|
|
this.patchedSrgJar = patchedSrgJar;
|
|
this.patchedOfficialJar = patchedOfficialJar;
|
|
}
|
|
|
|
public String side() {
|
|
return name().toLowerCase(Locale.ROOT);
|
|
}
|
|
}
|
|
|
|
private void remapPatchedJars(Logger logger) throws Exception {
|
|
Path[] libraries = getExtension()
|
|
.getMinecraftProvider()
|
|
.getLibraryProvider()
|
|
.getLibraries()
|
|
.stream()
|
|
.map(File::toPath)
|
|
.toArray(Path[]::new);
|
|
|
|
ThreadingUtils.run(Arrays.asList(Environment.values()), environment -> {
|
|
logger.lifecycle(":remapping minecraft (TinyRemapper, " + environment.side() + ", srg -> official)");
|
|
TinyTree mappingsWithSrg = getExtension().getMappingsProvider().getMappingsWithSrg();
|
|
|
|
Path input = environment.patchedSrgJar.apply(this).toPath();
|
|
Path output = environment.patchedOfficialJar.apply(this).toPath();
|
|
|
|
Files.deleteIfExists(output);
|
|
|
|
TinyRemapper remapper = TinyRemapper.newRemapper()
|
|
.withMappings(TinyRemapperMappingsHelper.create(mappingsWithSrg, "srg", "official", true))
|
|
.withMappings(InnerClassRemapper.of(input, mappingsWithSrg, "srg", "official"))
|
|
.renameInvalidLocals(true)
|
|
.rebuildSourceFilenames(true)
|
|
.fixPackageAccess(true)
|
|
.build();
|
|
|
|
try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(output).build()) {
|
|
outputConsumer.addNonClassFiles(input);
|
|
|
|
remapper.readClassPath(libraries);
|
|
remapper.readInputs(input);
|
|
remapper.apply(outputConsumer);
|
|
} finally {
|
|
remapper.finish();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
private void patchJars(Logger logger) throws IOException {
|
|
logger.lifecycle(":patching jars");
|
|
|
|
PatchProvider patchProvider = getExtension().getPatchProvider();
|
|
patchJars(minecraftClientSrgJar, minecraftClientPatchedSrgJar, patchProvider.clientPatches);
|
|
patchJars(minecraftServerSrgJar, minecraftServerPatchedSrgJar, patchProvider.serverPatches);
|
|
ThreadingUtils.run(() -> {
|
|
copyMissingClasses(minecraftClientSrgJar, minecraftClientPatchedSrgJar);
|
|
fixParameterAnnotation(minecraftClientPatchedSrgJar);
|
|
}, () -> {
|
|
copyMissingClasses(minecraftServerSrgJar, minecraftServerPatchedSrgJar);
|
|
fixParameterAnnotation(minecraftServerPatchedSrgJar);
|
|
});
|
|
}
|
|
|
|
private void patchJars(File clean, File output, Path patches) throws IOException {
|
|
PrintStream previous = System.out;
|
|
try {
|
|
System.setOut(new PrintStream(new NullOutputStream()));
|
|
} catch (SecurityException ignored) {
|
|
}
|
|
ConsoleTool.main(new String[]{
|
|
"--clean", clean.getAbsolutePath(),
|
|
"--output", output.getAbsolutePath(),
|
|
"--apply", patches.toAbsolutePath().toString()
|
|
});
|
|
try {
|
|
System.setOut(previous);
|
|
} catch (SecurityException ignored) {
|
|
}
|
|
}
|
|
|
|
private void mergeJars(Logger logger) throws IOException {
|
|
// FIXME: Hack here: There are no server-only classes so we can just copy the client JAR.
|
|
FileUtils.copyFile(minecraftClientPatchedOfficialJar, minecraftMergedPatchedJar);
|
|
|
|
logger.lifecycle(":copying resources");
|
|
|
|
// Copy resources
|
|
MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider();
|
|
copyNonClassFiles(minecraftProvider.minecraftClientJar, minecraftMergedPatchedJar);
|
|
copyNonClassFiles(minecraftProvider.minecraftServerJar, minecraftMergedPatchedJar);
|
|
}
|
|
|
|
private void walkFileSystems(File source, File target, Predicate<Path> filter, Function<FileSystem, Iterable<Path>> toWalk, FsPathConsumer action)
|
|
throws IOException {
|
|
try (FileSystemUtil.FileSystemDelegate sourceFs = FileSystemUtil.getJarFileSystem(source, false);
|
|
FileSystemUtil.FileSystemDelegate targetFs = FileSystemUtil.getJarFileSystem(target, false)) {
|
|
for (Path sourceDir : toWalk.apply(sourceFs.get())) {
|
|
Path dir = sourceDir.toAbsolutePath();
|
|
Files.walk(dir)
|
|
.filter(Files::isRegularFile)
|
|
.filter(filter)
|
|
.forEach(it -> {
|
|
boolean root = dir.getParent() == null;
|
|
|
|
try {
|
|
Path relativeSource = root ? it : dir.relativize(it);
|
|
Path targetPath = targetFs.get().getPath(relativeSource.toString());
|
|
action.accept(sourceFs.get(), targetFs.get(), it, targetPath);
|
|
} catch (IOException e) {
|
|
throw new UncheckedIOException(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private void walkFileSystems(File source, File target, Predicate<Path> filter, FsPathConsumer action) throws IOException {
|
|
walkFileSystems(source, target, filter, FileSystem::getRootDirectories, action);
|
|
}
|
|
|
|
private void copyAll(File source, File target) throws IOException {
|
|
walkFileSystems(source, target, it -> true, this::copyReplacing);
|
|
}
|
|
|
|
private void copyMissingClasses(File source, File target) throws IOException {
|
|
walkFileSystems(source, target, it -> it.toString().endsWith(".class"), (sourceFs, targetFs, sourcePath, targetPath) -> {
|
|
if (Files.exists(targetPath)) return;
|
|
Path parent = targetPath.getParent();
|
|
|
|
if (parent != null) {
|
|
Files.createDirectories(parent);
|
|
}
|
|
|
|
Files.copy(sourcePath, targetPath);
|
|
});
|
|
}
|
|
|
|
private void copyNonClassFiles(File source, File target) throws IOException {
|
|
walkFileSystems(source, target, it -> !it.toString().endsWith(".class"), this::copyReplacing);
|
|
}
|
|
|
|
private void copyReplacing(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException {
|
|
Path parent = targetPath.getParent();
|
|
|
|
if (parent != null) {
|
|
Files.createDirectories(parent);
|
|
}
|
|
|
|
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
|
}
|
|
|
|
private void copyUserdevFiles(File source, File target) throws IOException {
|
|
walkFileSystems(source, target, file -> true, fs -> Collections.singleton(fs.getPath("inject")), (sourceFs, targetFs, sourcePath, targetPath) -> {
|
|
Path parent = targetPath.getParent();
|
|
|
|
if (parent != null) {
|
|
Files.createDirectories(parent);
|
|
}
|
|
|
|
Files.copy(sourcePath, targetPath);
|
|
});
|
|
}
|
|
|
|
public File getMergedJar() {
|
|
return minecraftMergedPatchedJar;
|
|
}
|
|
|
|
public boolean usesProjectCache() {
|
|
return projectAt != null;
|
|
}
|
|
|
|
public boolean isAtDirty() {
|
|
return atDirty;
|
|
}
|
|
|
|
@Override
|
|
public String getTargetConfig() {
|
|
return Constants.Configurations.MINECRAFT;
|
|
}
|
|
}
|