diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/JarSplitter.java b/src/main/java/net/fabricmc/loom/configuration/mods/JarSplitter.java new file mode 100644 index 00000000..91bc25bd --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/mods/JarSplitter.java @@ -0,0 +1,178 @@ +/* + * 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.mods; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.stream.Stream; + +import net.fabricmc.loom.task.AbstractRemapJarTask; +import net.fabricmc.loom.util.FileSystemUtil; + +public class JarSplitter { + final Path inputJar; + + public JarSplitter(Path inputJar) { + this.inputJar = inputJar; + } + + public boolean split(Path commonOutputJar, Path clientOutputJar) throws IOException { + try (FileSystemUtil.Delegate input = FileSystemUtil.getJarFileSystem(inputJar)) { + final Manifest manifest = input.fromInputStream(Manifest::new, AbstractRemapJarTask.MANIFEST_PATH); + final List clientEntries = readClientEntries(manifest); + + if (clientEntries.isEmpty()) { + // No client entries, just copy the input jar + Files.copy(inputJar, commonOutputJar); + return false; + } + + try (FileSystemUtil.Delegate commonOutput = FileSystemUtil.getJarFileSystem(commonOutputJar, true); + FileSystemUtil.Delegate clientOutput = FileSystemUtil.getJarFileSystem(clientOutputJar, true); + Stream walk = Files.walk(input.get().getPath("/"))) { + final Iterator iterator = walk.iterator(); + + while (iterator.hasNext()) { + final Path entry = iterator.next(); + + if (!Files.isRegularFile(entry)) { + continue; + } + + final Path relativePath = input.get().getPath("/").relativize(entry); + + if (relativePath.startsWith("META-INF")) { + if (isSignatureData(relativePath)) { + // Strip any signature data + continue; + } + } + + final String entryPath = relativePath.toString(); + + /* + Copy the manifest to both jars + - Remove signature data + - Remove split data as its already been split. + */ + if (entryPath.equals(AbstractRemapJarTask.MANIFEST_PATH)) { + final Manifest outManifest = new Manifest(manifest); + final Attributes attributes = outManifest.getMainAttributes(); + stripSignatureData(outManifest); + + attributes.remove(Attributes.Name.SIGNATURE_VERSION); + Objects.requireNonNull(attributes.remove(AbstractRemapJarTask.MANIFEST_SPLIT_ENV_NAME)); + Objects.requireNonNull(attributes.remove(AbstractRemapJarTask.MANIFEST_CLIENT_ENTRIES_NAME)); + + // TODO add an attribute to denote if the jar is common or client now + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + outManifest.write(out); + final byte[] manifestBytes = out.toByteArray(); + + writeBytes(manifestBytes, commonOutput.getPath(AbstractRemapJarTask.MANIFEST_PATH)); + writeBytes(manifestBytes, clientOutput.getPath(AbstractRemapJarTask.MANIFEST_PATH)); + + continue; + } + + final FileSystemUtil.Delegate target = clientEntries.contains(entryPath) ? clientOutput : commonOutput; + final Path outputEntry = target.getPath(entryPath); + final Path outputParent = outputEntry.getParent(); + + if (outputParent != null) { + Files.createDirectories(outputParent); + } + + Files.copy(entry, outputEntry, StandardCopyOption.COPY_ATTRIBUTES); + } + } + } + + return true; + } + + private List readClientEntries(Manifest manifest) { + final Attributes attributes = manifest.getMainAttributes(); + final String splitEnvValue = attributes.getValue(AbstractRemapJarTask.MANIFEST_SPLIT_ENV_KEY); + final String clientEntriesValue = attributes.getValue(AbstractRemapJarTask.MANIFEST_CLIENT_ENTRIES_KEY); + + if (splitEnvValue == null || !splitEnvValue.equals("true")) { + throw new UnsupportedOperationException("Cannot split jar that has not been built with a split env"); + } + + if (clientEntriesValue == null) { + throw new IllegalStateException("Split jar does not contain any client only classes"); + } + + return Arrays.stream(clientEntriesValue.split(";")).toList(); + } + + private boolean isSignatureData(Path path) { + final String fileName = path.getFileName().toString(); + return fileName.endsWith(".SF") + || fileName.endsWith(".DSA") + || fileName.endsWith(".RSA") + || fileName.startsWith("SIG-"); + } + + // Based off tiny-remapper's MetaInfFixer + private static void stripSignatureData(Manifest manifest) { + for (Iterator it = manifest.getEntries().values().iterator(); it.hasNext(); ) { + Attributes attrs = it.next(); + + for (Iterator it2 = attrs.keySet().iterator(); it2.hasNext(); ) { + Attributes.Name attrName = (Attributes.Name) it2.next(); + String name = attrName.toString(); + + if (name.endsWith("-Digest") || name.contains("-Digest-") || name.equals("Magic")) { + it2.remove(); + } + } + + if (attrs.isEmpty()) it.remove(); + } + } + + private static void writeBytes(byte[] bytes, Path path) throws IOException { + final Path parent = path.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + Files.write(path, bytes); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index dbc8edb4..053d8ec5 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +import java.util.jar.Attributes; import java.util.jar.Manifest; import javax.inject.Inject; @@ -62,6 +63,10 @@ import net.fabricmc.loom.util.ZipUtils; public abstract class AbstractRemapJarTask extends Jar { public static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; public static final String MANIFEST_NAMESPACE_KEY = "Fabric-Mapping-Namespace"; + public static final String MANIFEST_SPLIT_ENV_KEY = "Fabric-Loom-Split-Environment"; + public static final String MANIFEST_CLIENT_ENTRIES_KEY = "Fabric-Loom-Client-Only-Entries"; + public static final Attributes.Name MANIFEST_SPLIT_ENV_NAME = new Attributes.Name(MANIFEST_SPLIT_ENV_KEY); + public static final Attributes.Name MANIFEST_CLIENT_ENTRIES_NAME = new Attributes.Name(MANIFEST_CLIENT_ENTRIES_KEY); @InputFile public abstract RegularFileProperty getInputFile(); @@ -141,8 +146,8 @@ public abstract class AbstractRemapJarTask extends Jar { protected void applyClientOnlyManifestAttributes(AbstractRemapParams params, List entries) { params.getManifestAttributes().set(Map.of( - "Fabric-Loom-Split-Environment", "true", - "Fabric-Loom-Client-Only-Entries", String.join(";", entries) + MANIFEST_SPLIT_ENV_KEY, "true", + MANIFEST_CLIENT_ENTRIES_KEY, String.join(";", entries) )); } diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java index a031ffa0..0a097c83 100644 --- a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -26,6 +26,7 @@ package net.fabricmc.loom.util; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; @@ -38,8 +39,12 @@ import net.fabricmc.tinyremapper.FileSystemReference; public final class FileSystemUtil { public record Delegate(FileSystemReference reference) implements AutoCloseable, Supplier { + public Path getPath(String path, String... more) { + return get().getPath(path, more); + } + public byte[] readAllBytes(String path) throws IOException { - Path fsPath = get().getPath(path); + Path fsPath = getPath(path); if (Files.exists(fsPath)) { return Files.readAllBytes(fsPath); @@ -48,6 +53,12 @@ public final class FileSystemUtil { } } + public T fromInputStream(IOFunction function, String path, String... more) throws IOException { + try (InputStream inputStream = Files.newInputStream(getPath(path, more))) { + return function.apply(inputStream); + } + } + public String readString(String path) throws IOException { return new String(readAllBytes(path), StandardCharsets.UTF_8); } diff --git a/src/main/java/net/fabricmc/loom/util/IOFunction.java b/src/main/java/net/fabricmc/loom/util/IOFunction.java new file mode 100644 index 00000000..d1281b92 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/IOFunction.java @@ -0,0 +1,32 @@ +/* + * 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.util; + +import java.io.IOException; + +@FunctionalInterface +public interface IOFunction { + R apply(T t) throws IOException; +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/JarSplitterTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/JarSplitterTest.groovy new file mode 100644 index 00000000..a0a2a78a --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/JarSplitterTest.groovy @@ -0,0 +1,66 @@ +/* + * 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 + +import net.fabricmc.loom.configuration.mods.JarSplitter +import spock.lang.Specification + +class JarSplitterTest extends Specification { + public static final String INPUT_JAR_URL = "https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-lifecycle-events-v1/2.1.0%2B33fbc738a9/fabric-lifecycle-events-v1-2.1.0%2B33fbc738a9.jar" + + public static final File workingDir = new File("build/test/split") + + def "split jar"() { + given: + def inputJar = downloadJarIfNotExists(INPUT_JAR_URL, "input.jar") + def commonOutputJar = getFile("common.jar") + def clientOutputJar = getFile("client.jar") + + def jarSplitter = new JarSplitter(inputJar.toPath()) + when: + jarSplitter.split(commonOutputJar.toPath(), clientOutputJar.toPath()) + + then: + commonOutputJar.exists() + clientOutputJar.exists() + } + + File downloadJarIfNotExists(String url, String name) { + File dst = new File(workingDir, name) + + if (!dst.exists()) { + dst.parentFile.mkdirs() + dst << new URL(url).newInputStream() + } + + return dst + } + + File getFile(String name) { + File file = new File(workingDir, name) + file.delete() + return file + } +}