Unpick remapping (#1318)

* Unpick remapping

* More work

* Better tests

* Copilot had some good ideas for once.
This commit is contained in:
modmuss
2025-06-20 20:05:27 +01:00
committed by GitHub
parent ba1cd12413
commit 9b76a353ec
15 changed files with 751 additions and 13 deletions

View File

@@ -41,6 +41,7 @@ import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
@@ -65,7 +66,7 @@ import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.extension.mixin.MixinExtension;
public class TinyRemapperService extends Service<TinyRemapperService.Options> implements Closeable {
public class TinyRemapperService extends Service<TinyRemapperService.Options> implements TinyRemapperServiceInterface, Closeable {
public static final ServiceType<Options, TinyRemapperService> TYPE = new ServiceType<>(Options.class, TinyRemapperService.class);
public interface Options extends Service.Options {
@@ -116,6 +117,24 @@ public class TinyRemapperService extends Service<TinyRemapperService.Options> im
});
}
public static Provider<Options> createSimple(Project project, Provider<String> from, Provider<String> to) {
return TYPE.create(project, options -> {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final ConfigurationContainer configurations = project.getConfigurations();
final FileCollection classpath = configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)
.minus(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES))
.minus(configurations.getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES));
options.getFrom().set(from);
options.getTo().set(to);
options.getMappings().add(MappingsService.createOptionsWithProjectMappings(project, options.getFrom(), options.getTo()));
options.getUselegacyMixinAP().set(true);
options.getClasspath().from(classpath);
options.getKnownIndyBsms().set(extension.getKnownIndyBsms().get().stream().sorted().toList());
options.getRemapperExtensions().set(extension.getRemapperExtensions());
});
}
private TinyRemapper tinyRemapper;
@Nullable
private KotlinRemapperClassloader kotlinRemapperClassloader;
@@ -178,11 +197,13 @@ public class TinyRemapperService extends Service<TinyRemapperService.Options> im
return tag;
}
@Override
public TinyRemapper getTinyRemapperForRemapping() {
isRemapping = true;
return Objects.requireNonNull(tinyRemapper, "Tiny remapper has not been setup");
}
@Override
public TinyRemapper getTinyRemapperForInputs() {
if (isRemapping) {
throw new IllegalStateException("Cannot read inputs as remapping has already started");

View File

@@ -0,0 +1,35 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 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.service;
import org.jetbrains.annotations.VisibleForTesting;
import net.fabricmc.tinyremapper.TinyRemapper;
@VisibleForTesting
public interface TinyRemapperServiceInterface {
TinyRemapper getTinyRemapperForRemapping();
TinyRemapper getTinyRemapperForInputs();
}

View File

@@ -0,0 +1,152 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 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.service;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import daomephsta.unpick.constantmappers.datadriven.parser.v3.UnpickV3Reader;
import daomephsta.unpick.constantmappers.datadriven.parser.v3.UnpickV3Remapper;
import daomephsta.unpick.constantmappers.datadriven.parser.v3.UnpickV3Writer;
import daomephsta.unpick.constantmappers.datadriven.tree.UnpickV3Visitor;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Nested;
import org.objectweb.asm.commons.Remapper;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
import net.fabricmc.loom.util.JarPackageIndex;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.api.TrClass;
import net.fabricmc.tinyremapper.api.TrField;
public class UnpickRemapperService extends Service<UnpickRemapperService.Options> {
public static final ServiceType<Options, UnpickRemapperService> TYPE = new ServiceType<>(Options.class, UnpickRemapperService.class);
public interface Options extends Service.Options {
@Nested
Property<TinyRemapperService.Options> getTinyRemapper();
}
public static Provider<Options> createOptions(Project project, UnpickMetadata.V2 metadata) {
return TYPE.create(project, options -> {
options.getTinyRemapper().set(TinyRemapperService.createSimple(project,
project.provider(metadata::namespace),
project.provider(MappingsNamespace.NAMED::toString)
));
});
}
public UnpickRemapperService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
/**
* Return the remapped definitions.
*/
public String remap(File input) throws IOException {
TinyRemapperServiceInterface tinyRemapperService = getServiceFactory().get(getOptions().getTinyRemapper());
TinyRemapper tinyRemapper = tinyRemapperService.getTinyRemapperForRemapping();
List<Path> classpath = getOptions().getTinyRemapper().get().getClasspath().getFiles().stream().map(File::toPath).toList();
JarPackageIndex packageIndex = JarPackageIndex.create(classpath);
return doRemap(input, tinyRemapper, packageIndex);
}
private String doRemap(File input, TinyRemapper remapper, JarPackageIndex packageIndex) throws IOException {
try (Reader fileReader = new BufferedReader(new FileReader(input));
var reader = new UnpickV3Reader(fileReader)) {
var writer = new UnpickV3Writer();
reader.accept(new UnpickRemapper(writer, remapper, packageIndex));
return writer.getOutput().replace(System.lineSeparator(), "\n");
}
}
private static final class UnpickRemapper extends UnpickV3Remapper {
private final TinyRemapper tinyRemapper;
private final Remapper remapper;
private final JarPackageIndex jarPackageIndex;
private UnpickRemapper(UnpickV3Visitor downstream, TinyRemapper tinyRemapper, JarPackageIndex jarPackageIndex) {
super(downstream);
this.tinyRemapper = tinyRemapper;
this.remapper = tinyRemapper.getEnvironment().getRemapper();
this.jarPackageIndex = jarPackageIndex;
}
@Override
protected String mapClassName(String className) {
return remapper.map(className);
}
@Override
protected String mapFieldName(String className, String fieldName, String fieldDesc) {
return remapper.mapFieldName(className, fieldName, fieldDesc);
}
@Override
protected String mapMethodName(String className, String methodName, String methodDesc) {
return remapper.mapMethodName(className, methodName, methodDesc);
}
// Return all classes in the given package, not recursively.
@Override
protected List<String> getClassesInPackage(String pkg) {
return jarPackageIndex.packages().getOrDefault(pkg, Collections.emptyList())
.stream()
.map(className -> pkg + "." + className)
.toList();
}
@Override
protected String getFieldDesc(String className, String fieldName) {
TrClass trClass = tinyRemapper.getEnvironment().getClass(className);
if (trClass == null) {
return null;
}
for (TrField trField : trClass.getFields()) {
if (trField.getName().equals(fieldName)) {
return trField.getDesc();
}
}
return null;
}
}
}

View File

@@ -24,10 +24,12 @@
package net.fabricmc.loom.task.service;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -49,6 +51,8 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.ClassReader;
@@ -86,6 +90,10 @@ public class UnpickService extends Service<UnpickService.Options> {
@InputFile
RegularFileProperty getUnpickDefinitions();
@Optional
@Nested
Property<UnpickRemapperService.Options> getUnpickRemapperService();
@InputFiles
ConfigurableFileCollection getUnpickConstantJar();
@@ -113,8 +121,7 @@ public class UnpickService extends Service<UnpickService.Options> {
if (unpickMetadata instanceof UnpickMetadata.V2 v2) {
if (!Objects.equals(v2.namespace(), MappingsNamespace.NAMED.toString())) {
// TODO!
throw new IllegalStateException("Unpick metadata with a namespace other than named is not yet supported");
options.getUnpickRemapperService().set(UnpickRemapperService.createOptions(project, v2));
}
}
@@ -144,13 +151,11 @@ public class UnpickService extends Service<UnpickService.Options> {
Stream.of(inputJar),
Stream.ofNullable(existingClasses)
).flatMap(Function.identity()).toList();
final Path unpickDefinitionsPath = getOptions().getUnpickDefinitions().getAsFile().get().toPath();
final Path outputJar = getOptions().getUnpickOutputJar().get().getAsFile().toPath();
Files.deleteIfExists(outputJar);
try (ZipFsClasspath zipFsClasspath = ZipFsClasspath.create(classpath);
InputStream unpickDefinitions = Files.newInputStream(unpickDefinitionsPath)) {
InputStream unpickDefinitions = getUnpickDefinitionsInputStream()) {
IClassResolver classResolver = zipFsClasspath.createClassResolver().chain(ClassResolvers.classpath());
ConstantUninliner uninliner = ConstantUninliner.builder()
.logger(JAVA_LOGGER)
@@ -169,10 +174,31 @@ public class UnpickService extends Service<UnpickService.Options> {
return outputJar;
}
private InputStream getUnpickDefinitionsInputStream() throws IOException {
final Path unpickDefinitionsPath = getOptions().getUnpickDefinitions().getAsFile().get().toPath();
if (getOptions().getUnpickRemapperService().isPresent()) {
LOGGER.info("Remapping unpick definitions: {}", unpickDefinitionsPath);
UnpickRemapperService unpickRemapperService = getServiceFactory().get(getOptions().getUnpickRemapperService());
String remapped = unpickRemapperService.remap(unpickDefinitionsPath.toFile());
return new ByteArrayInputStream(remapped.getBytes(StandardCharsets.UTF_8));
}
LOGGER.debug("Using unpick definitions: {}", unpickDefinitionsPath);
return Files.newInputStream(unpickDefinitionsPath);
}
public String getUnpickCacheKey() {
return Checksum.of(List.of(
Checksum.of(getOptions().getUnpickDefinitions().getAsFile().get()),
Checksum.of(getOptions().getUnpickConstantJar())
Checksum.of(getOptions().getUnpickConstantJar()),
Checksum.of(getOptions().getUnpickRemapperService()
.flatMap(options -> options.getTinyRemapper()
.flatMap(TinyRemapperService.Options::getFrom))
.getOrElse("named"))
)).sha256().hex();
}

View File

@@ -0,0 +1,100 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 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;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* An index of all packages and the classes directly contained within.
*/
public record JarPackageIndex(Map<String, List<String>> packages) {
public static JarPackageIndex create(List<Path> jars) {
Map<String, List<String>> packages = jars.stream()
.map(jar -> CompletableFuture.supplyAsync(() -> {
try {
List<String> classes = getClasses(jar);
return groupClassesByPackage(classes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}, Executors.newVirtualThreadPerTaskExecutor()))
.map(CompletableFuture::join)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(existing, newValues) -> {
existing.addAll(newValues);
return existing;
}
));
return new JarPackageIndex(packages);
}
private static List<String> getClasses(Path jar) throws IOException {
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar, false);
Stream<Path> walk = Files.walk(fs.getRoot())) {
return walk
.filter(Files::isRegularFile)
.map(Path::toString)
.filter(className -> className.endsWith(".class"))
.map(className -> className.startsWith("/") ? className.substring(1) : className)
.toList();
}
}
private static Map<String, List<String>> groupClassesByPackage(List<String> classes) {
return classes.stream()
.filter(className -> className.endsWith(".class")) // Ensure it's a class file
.collect(Collectors.groupingBy(
JarPackageIndex::extractPackageName,
Collectors.mapping(
JarPackageIndex::extractClassName,
Collectors.toList()
)
));
}
// Returns the package name from a class name, e.g., "com/example/MyClass.class" -> "com.example"
private static String extractPackageName(String className) {
int lastSlashIndex = className.lastIndexOf('/');
return lastSlashIndex == -1 ? "" : className.substring(0, lastSlashIndex).replace("/", ".");
}
private static String extractClassName(String className) {
int lastSlashIndex = className.lastIndexOf('/');
String simpleName = lastSlashIndex == -1 ? className : className.substring(lastSlashIndex + 1);
return simpleName.endsWith(".class") ? simpleName.substring(0, simpleName.length() - 6) : simpleName;
}
}

View File

@@ -31,13 +31,15 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import org.jetbrains.annotations.VisibleForTesting;
import net.fabricmc.loom.util.gradle.GradleTypeAdapter;
/**
* An implementation of {@link ServiceFactory} that creates services scoped to the factory instance.
* When the factory is closed, all services created by it are closed and discarded.
*/
public final class ScopedServiceFactory implements ServiceFactory, Closeable {
public class ScopedServiceFactory implements ServiceFactory, Closeable {
private final Map<Service.Options, Service<?>> servicesIdentityMap = new IdentityHashMap<>();
private final Map<String, Service<?>> servicesJsonMap = new HashMap<>();
@@ -60,7 +62,7 @@ public final class ScopedServiceFactory implements ServiceFactory, Closeable {
return service;
}
service = createService(options, this);
service = createService(options, getEffectiveServiceFactory());
servicesIdentityMap.put(options, service);
servicesJsonMap.put(key, service);
@@ -68,6 +70,11 @@ public final class ScopedServiceFactory implements ServiceFactory, Closeable {
return service;
}
@VisibleForTesting
protected ServiceFactory getEffectiveServiceFactory() {
return this;
}
private static <O extends Service.Options, S extends Service<O>> S createService(O options, ServiceFactory serviceFactory) {
// We need to create the service from the provided options
final Class<? extends S> serviceClass;