Skip to content

Commit 225b93f

Browse files
committed
stage2/lw: Support extracting MixinExtras versions from third-party jars
This way if a third-party mod ships a MixinExtras version newer than anything any EssentialLoader-using mod ships, we'll correctly use the newer version from the third-party mod instead of simply the latest (but older) version from all our jars. E.g. MixinBooter currently ships MixinExtras 0.5.0-rc.1, so if an EssentialLoader-using mod very reasonably ships 0.4.1, we'd simply be loading 0.4.1 prior to this commit.
1 parent 0faedcb commit 225b93f

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/Loader.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.google.gson.JsonObject;
77
import com.google.gson.JsonPrimitive;
88
import gg.essential.loader.stage2.relaunch.Relaunch;
9+
import gg.essential.loader.stage2.util.MixinExtrasExtractor;
910
import net.minecraft.launchwrapper.ITweaker;
1011
import net.minecraft.launchwrapper.Launch;
1112
import org.apache.logging.log4j.LogManager;
@@ -217,6 +218,7 @@ private JarInfo load(JarInfo parent, Path jar, JsonObject descriptor, Map<String
217218
assert(jar.isAbsolute());
218219

219220
boolean hasStage1 = false;
221+
String embeddedMixinExtrasVersion;
220222

221223
try (FileSystem fileSystem = FileSystems.newFileSystem(jar, (ClassLoader) null)) {
222224
Path modJsonPath = fileSystem.getPath(ESSENTIAL_MOD_JSON);
@@ -231,6 +233,8 @@ private JarInfo load(JarInfo parent, Path jar, JsonObject descriptor, Map<String
231233
hasStage1 = true;
232234
checkForNewerLoaderInStage1(stage1Path);
233235
}
236+
237+
embeddedMixinExtrasVersion = MixinExtrasExtractor.readMixinExtrasVersion(jar, fileSystem);
234238
} catch (IOException e) {
235239
throw new RuntimeException(e);
236240
}
@@ -256,6 +260,35 @@ private JarInfo load(JarInfo parent, Path jar, JsonObject descriptor, Map<String
256260

257261
if (descriptor == null) {
258262
// This mod doesn't appear to be using Essential Loader
263+
264+
// If it ships a MixinExtras though, we'll extract that and add it to our mods so we don't overwrite newer
265+
// MixinExtras versions with an old version shipped by another Essential Loader-using mod.
266+
if (embeddedMixinExtrasVersion != null) {
267+
try {
268+
// We're adding a suffix to the version so that
269+
// 1. it is clearer where it came from
270+
// 2. if we have the same version as a proper Jar-in-Jar, we'll prefer using that
271+
String version = embeddedMixinExtrasVersion
272+
+ ".from."
273+
+ jar.getFileName().toString().replaceAll("[^A-Za-z0-9]", "_");
274+
275+
Path extractedPath = Files.createTempFile("mixinextras-" + version + "-", ".jar");
276+
extractedPath.toFile().deleteOnExit();
277+
278+
LOGGER.debug("Extracting MixinExtras from {} to {}", jar, extractedPath);
279+
MixinExtrasExtractor.extractMixinExtras(jar, extractedPath, embeddedMixinExtrasVersion);
280+
281+
JarInfo info = new JarInfo();
282+
info.path = extractedPath;
283+
info.id = "io.github.llamalad7:mixinextras-common";
284+
info.version = version;
285+
if (parent != null) parent.children.add(info);
286+
allVersions.put(info.id + ":" + version, info);
287+
return info;
288+
} catch (Exception e) {
289+
LOGGER.error("Failed to extract MixinExtras from {}", jar, e);
290+
}
291+
}
259292
return null;
260293
}
261294

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package gg.essential.loader.stage2.util;
2+
3+
import org.apache.logging.log4j.LogManager;
4+
import org.apache.logging.log4j.Logger;
5+
import org.objectweb.asm.ClassReader;
6+
import org.objectweb.asm.Opcodes;
7+
import org.objectweb.asm.tree.AbstractInsnNode;
8+
import org.objectweb.asm.tree.ClassNode;
9+
import org.objectweb.asm.tree.FieldInsnNode;
10+
import org.objectweb.asm.tree.FieldNode;
11+
import org.objectweb.asm.tree.LdcInsnNode;
12+
import org.objectweb.asm.tree.MethodNode;
13+
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.io.OutputStream;
17+
import java.nio.file.FileSystem;
18+
import java.nio.file.FileSystems;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.StandardOpenOption;
22+
import java.util.jar.JarOutputStream;
23+
import java.util.jar.Manifest;
24+
import java.util.stream.Stream;
25+
26+
public class MixinExtrasExtractor {
27+
private static final Logger LOGGER = LogManager.getLogger(MixinExtrasExtractor.class);
28+
29+
private static final String MIXINEXTRAS_PACKAGE_PATH = "com/llamalad7/mixinextras";
30+
private static final String MIXIN_EXTRAS_VERSION_CLASS = MIXINEXTRAS_PACKAGE_PATH + "/service/MixinExtrasVersion.class";
31+
32+
public static String readMixinExtrasVersion(Path jar, FileSystem jarFileSystem) {
33+
try {
34+
Path classFilePath = jarFileSystem.getPath(MIXIN_EXTRAS_VERSION_CLASS);
35+
if (!Files.exists(classFilePath)) return null;
36+
37+
byte[] bytes = Files.readAllBytes(classFilePath);
38+
ClassNode classNode = new ClassNode();
39+
ClassReader classReader = new ClassReader(bytes);
40+
classReader.accept(classNode, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
41+
42+
FieldNode lastEnumField = null;
43+
for (FieldNode field : classNode.fields) {
44+
if ((field.access & Opcodes.ACC_ENUM) != 0) {
45+
lastEnumField = field;
46+
}
47+
}
48+
if (lastEnumField == null) throw new UnsupportedOperationException("Failed to find any enum entries");
49+
50+
MethodNode clinit = null;
51+
for (MethodNode method : classNode.methods) {
52+
if (method.name.equals("<clinit>")) {
53+
clinit = method;
54+
break;
55+
}
56+
}
57+
if (clinit == null) throw new UnsupportedOperationException("Failed to find static initializer");
58+
59+
AbstractInsnNode insn = clinit.instructions.getFirst();
60+
61+
// Search for field assignment
62+
while (insn != null) {
63+
if (insn instanceof FieldInsnNode && ((FieldInsnNode) insn).name.equals(lastEnumField.name)) {
64+
break;
65+
}
66+
insn = insn.getNext();
67+
}
68+
if (insn == null) throw new UnsupportedOperationException("Failed to find enum field initializer");
69+
70+
// Search backwards for the version string
71+
while (insn != null) {
72+
if (insn instanceof LdcInsnNode && ((LdcInsnNode) insn).cst instanceof String) {
73+
return (String) ((LdcInsnNode) insn).cst;
74+
}
75+
insn = insn.getPrevious();
76+
}
77+
78+
throw new UnsupportedOperationException("Failed to find version argument");
79+
} catch (Exception e) {
80+
LOGGER.error("Failed to determine version of MixinExtras in {}", jar, e);
81+
return null;
82+
}
83+
}
84+
85+
public static void extractMixinExtras(Path sourceJar, Path extractedJar, String version) throws IOException {
86+
// Create manifest file
87+
// One is implicitly required by LaunchClassLoader, otherwise won't be declaring `Package`s for the classes in
88+
// the jar, and MixinExtras initialization code will consequently NPE.
89+
Manifest manifest = new Manifest();
90+
// and while we're at it, may as well set the correct version
91+
manifest.getMainAttributes().putValue("Implementation-Version", version);
92+
93+
// Create empty jar file
94+
try (OutputStream out = Files.newOutputStream(extractedJar, StandardOpenOption.TRUNCATE_EXISTING)) {
95+
new JarOutputStream(out, manifest).close();
96+
}
97+
98+
// Copy MixinExtras package from source jar to our new jar
99+
try (FileSystem srcFs = FileSystems.newFileSystem(sourceJar, (ClassLoader) null)) {
100+
try (FileSystem dstFs = FileSystems.newFileSystem(extractedJar, (ClassLoader) null)) {
101+
// Note: These are `toAbsolutePath`ed as a workaround for ZipFileSystem sometimes returning absolute
102+
// `Path`s from `walk` even when the `start` `Path` is relative.
103+
Path srcRoot = srcFs.getPath(MIXINEXTRAS_PACKAGE_PATH).toAbsolutePath();
104+
Path dstRoot = dstFs.getPath(MIXINEXTRAS_PACKAGE_PATH).toAbsolutePath();
105+
106+
try (Stream<Path> stream = Files.walk(srcRoot)) {
107+
for (Path src : (Iterable<? extends Path>) stream::iterator) {
108+
src = src.toAbsolutePath(); // necessary because *see note above*
109+
Path dst = dstRoot.resolve(srcRoot.relativize(src));
110+
Files.createDirectories(dst.getParent());
111+
Files.copy(src, dst);
112+
}
113+
}
114+
}
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)