Remap dependencies?
Hello, I was wondering if libby can optionally remap libraries for use with Paper after they've been downloaded?
See https://github.com/WesJD/AnvilGUI/issues/372
As I know not possible
I did it in my AnvilGUI addon.
Basically, Paper does expose an API to remap libraries. However, it only remaps the libraries if it has paperweight-mappings-namespace in its Manifest File.
In my implementation, I have to make a copy of the library and add the required line in its Manifest file.
You can check my implementation here: https://github.com/BetterGUI-MC/AnvilGUI/blob/0c1978df7195802628d89b5e3a32f465db583f75/src/main/java/me/hsgamer/bettergui/anvilgui/LibLoader.java
Related to this issue https://github.com/PaperMC/Paper/issues/10713
Unfortunately they marked that as not-planned.
I didn't know about the paperweight-mappings-namespace, we will probably look at that too once the v2 is nearly completed.
Thank you, @HSGamer, that works for the time being. Looking forward to v2!
It's possible to do:
- Resolve
modifierfield of typeFunction<byte[], byte[]>in the ClassLoader returned byorg.bukkit.plugin.java.LibraryLoader.LIBRARY_LOADER_FACTORY#apply(URL[], URlClassLoader) - Pass the bytes of any class file through that function to obtain a remapped version.
- If you care about caching the result, be sure to determine if the classes need remapping. (If the server jar/build changes)
I don't think this really needs to be implemented in Libby itself.
Perhaps add a rewriter(BiFunction<String, byte[], byte[]>) to LibraryBuilder to allow remapping classes for any purpose?
Where the String is the class name or path, and the rest is the input/output bytes of the class.
My implementation for inspiration
@SuppressWarnings({"UnstableApiUsage", "CallToPrintStackTrace"})
public class PaperLibraryRemapper {
private static final Logger LOGGER = Logger.getLogger(PaperLibraryRemapper.class.getName());
public static final boolean IS_MOJANG_MAPPED = resolveIsMojangMapped();
public static final Function<byte[], byte[]> CLASS_REWRITER = resolveClassRewriter();
/**
* Remaps and rewrites a jar to fix compatibility with Mojang mapped Paper servers.
*
* <p>This method always assumes the server is Mojang mapped
* <br>While it is possible that a Paper server is using Reobf mappings, this is only the
* case if someone manually (and intentionally) builds it that way.
*
* <p>This method will cache remapped jars until they are invalid, as determined by {@link #requiresRemapping(Path, Path)}
*
* @param path the path of the library to remap
* @param librarySavePath the path of the library root folder (used to print a more friendly message to {@link Logger log})
* @param log the logger to write to
* @return the path of the Remapped jar
* @throws RuntimeException if remapping fails for any reason
*/
public static Path remap(Path path, Path librarySavePath, Logger log) {
try {
Path remapped = path.resolveSibling("remapped-" + path.getFileName());
// only remap if required
if (!requiresRemapping(path, remapped))
return remapped;
log.info("Remapping: " + librarySavePath.getParent().relativize(path));
// REMAPPER could change the original, so we need to copy it.
// It could also return a different path as well, so we need to handle that too.
Path remappedTmp = remapped.resolveSibling("remapped-tmp-" + path.getFileName());
Path remappedTmp2 = null;
try (JarOutputStream outJar = new JarOutputStream(new BufferedOutputStream(Files.newOutputStream(remapped, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)))) {
Files.copy(path, remappedTmp, StandardCopyOption.REPLACE_EXISTING);
remappedTmp2 = LibraryLoader.REMAPPER.apply(List.of(remappedTmp)).getFirst();
try (JarFile inJar = new JarFile(remappedTmp2.toFile())) {
var inEntries = inJar.entries();
while (inEntries.hasMoreElements()) {
JarEntry in = inEntries.nextElement();
byte[] data;
try (var is = inJar.getInputStream(in)) {
data = is.readAllBytes();
}
String inName = in.getName();
if (inName.startsWith("META-INF/")) {
if (inName.endsWith(".SF") || inName.endsWith(".DSA") || inName.endsWith(".RSA"))
continue; // skip signature files to avoid verification issues
} else if (inName.endsWith(".class") && CLASS_REWRITER != null) {
data = CLASS_REWRITER.apply(data); // rewrite classes using Paper's class rewriter
}
JarEntry out = new JarEntry(inName);
out.setComment(in.getComment());
out.setMethod(in.getMethod());
outJar.putNextEntry(out);
outJar.write(data);
outJar.closeEntry();
}
}
} catch (Throwable t) {
Files.deleteIfExists(remapped);
} finally {
Files.deleteIfExists(remappedTmp);
if (remappedTmp2 != null)
Files.deleteIfExists(remappedTmp2);
}
return remapped;
} catch (Throwable t) {
throw new RuntimeException("Failed to remap library: " + path, t);
}
}
/**
* Determines if the remapped path is either out of date or isn't a file
*
* @param original the original non-remapped file
* @param remapped the path to the remapped file (even if it doesn't exist)
* @return <p>
* true if any of the following conditions are met:
* <ul>
* <li>remapped doesn't exist</li>
* <li>original file doesn't exist</li>
* <li>server jar location couldn't be resolved</li>
* <li>server jar doesn't exist</li>
* <li>original is newer than remapped</li>
* <li>server jar is newer than remapped</li>
* <li>error while reading the {@link BasicFileAttributes} of the above files</li>
* </ul>
* </p>
*/
public static boolean requiresRemapping(Path original, Path remapped) {
if (!Files.isRegularFile(original) || !Files.isRegularFile(remapped))
return true;
if (BukkitUtil.SERVER_JAR == null || !Files.isRegularFile(BukkitUtil.SERVER_JAR))
return true;
try {
BasicFileAttributes serverAttrs = Files.readAttributes(BukkitUtil.SERVER_JAR, BasicFileAttributes.class);
BasicFileAttributes originalAttrs = Files.readAttributes(original, BasicFileAttributes.class);
BasicFileAttributes remappedAttrs = Files.readAttributes(remapped, BasicFileAttributes.class);
return isNewer(serverAttrs, remappedAttrs) || isNewer(originalAttrs, remappedAttrs);
} catch (Throwable t) {}
return true;
}
/**
* Determines if {@link BasicFileAttributes a} is newer than {@link BasicFileAttributes b}
*
* <p>{@link BasicFileAttributes a} is considered newer if either it's modification time, or creation time (whichever is greater)
* is greater than the modification time, or creation time (whichever is greater) of {@link BasicFileAttributes b}
*
* @param a the possibly newer attributes
* @param b the possibly older attributes
* @return true if {@link BasicFileAttributes a} is newer than {@link BasicFileAttributes b}
*/
public static boolean isNewer(BasicFileAttributes a, BasicFileAttributes b) {
var aCre = a.creationTime().toInstant();
var aMod = a.lastModifiedTime().toInstant();
var aTime = aCre.isAfter(aMod) ? aCre : aMod;
var bCre = b.creationTime().toInstant();
var bMod = b.lastModifiedTime().toInstant();
var bTime = bCre.isAfter(bMod) ? bCre : bMod;
return aTime.isAfter(bTime);
}
private static boolean resolveIsMojangMapped() {
var mappingEnvClass = Reflection.clazz("io.papermc.paper.util.MappingEnvironment");
if (mappingEnvClass == null)
return false;
var reobfMethod = Reflection.declaredMethod("reobf", mappingEnvClass);
return reobfMethod == null
|| Modifier.isStatic(reobfMethod.getModifiers())
|| !reobfMethod.getReturnType().equals(boolean.class)
|| !(boolean) Reflection.invoke(reobfMethod);
}
private static Function<byte[], byte[]> resolveClassRewriter() {
// steal the class rewriter from Paper's BytecodeModifyingURLClassLoader
// this is used to replace references to java's reflection with paper's remaping reflection system
try (URLClassLoader urlLoader = new URLClassLoader(new URL[0])) {
ClassLoader modifierLoader = LibraryLoader.LIBRARY_LOADER_FACTORY.apply(new URL[0], urlLoader);
Field f = Reflection.declaredField("modifier", modifierLoader.getClass());
return Reflection.get(f, modifierLoader);
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "Failed to find class rewriter. Reflective nms lookups in libraries may fail", t);
return null;
}
}
}
Hope that helps.
Hello! Here you can get my class, currently used on ItemsAdder to load FastNBT. Hope it helps <3
Something I forgot to post here. I've made a sample extension of the BukkitLibraryManager with the remap trick: https://gist.github.com/HSGamer/82b4e72b8eaf9cac41289cba51116497