Merge 1.11

This commit is contained in:
Juuz
2025-08-18 13:04:45 +03:00
164 changed files with 6611 additions and 897 deletions

View File

@@ -1,5 +1,6 @@
[*.{gradle,java,kotlin}]
[*.{gradle,groovy,java,kotlin}]
indent_style = tab
ij_continuation_indent_size = 8
ij_java_imports_layout = $*,|,java.**,|,javax.**,|,*,|,net.fabricmc.**
ij_java_class_count_to_use_import_on_demand = 999
ij_groovy_imports_layout = java.**,|,javax.**,|,*,|,net.fabricmc.**,|,$*

View File

@@ -1,5 +1,17 @@
name: Run Tests
on: [push, pull_request]
on:
push:
pull_request:
workflow_dispatch:
inputs:
extended_tests:
description: 'Extended tests'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
concurrency:
group: build-${{ github.event.pull_request.number || github.ref }}
@@ -24,6 +36,8 @@ jobs:
build/reports/checkstyle/*.xml
build_windows:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.extended_tests == 'true' }}
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
@@ -77,6 +91,7 @@ jobs:
- run: ./gradlew printActionsTestName --name="${{ matrix.test }}" test --tests ${{ matrix.test }} --stacktrace --warning-mode fail
env:
TEST_WARNING_MODE: fail
EXTENDED_TESTS: ${{ github.event.inputs.extended_tests }}
id: test
- uses: actions/upload-artifact@v4
@@ -91,6 +106,8 @@ jobs:
path: "*.hprof"
run_tests_windows:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.extended_tests == 'true' }}
needs: prepare_test_matrix
strategy:
@@ -98,7 +115,7 @@ jobs:
matrix:
test: ${{ fromJson(needs.prepare_test_matrix.outputs.matrix) }}
runs-on: windows-2022
runs-on: windows-2025
steps:
- uses: actions/checkout@v4
@@ -130,8 +147,8 @@ jobs:
strategy:
fail-fast: false
matrix:
java: [ 17, 21 ]
os: [ windows-2022, ubuntu-24.04, macos-14 ]
java: [ 21 ]
os: [ windows-2025, ubuntu-24.04, macos-15 ]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -18,12 +18,12 @@ tasks.withType(JavaCompile).configureEach {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "17"
jvmTarget = "21"
}
}
group = "dev.architectury"
def baseVersion = '1.10'
def baseVersion = '1.11'
def ENV = System.getenv()
def runNumber = ENV.GITHUB_RUN_NUMBER ?: "9999"
@@ -144,6 +144,9 @@ dependencies {
// source code remapping
implementation libs.fabric.mercury
implementation libs.fabric.unpick
implementation libs.fabric.unpick.utils
// Kotlin
implementation(libs.kotlin.metadata) {
transitive = false
@@ -176,6 +179,9 @@ dependencies {
}
testImplementation testLibs.mockito
testImplementation testLibs.java.debug
testImplementation testLibs.bcprov
testImplementation testLibs.bcutil
testImplementation testLibs.bcpkix
compileOnly runtimeLibs.jetbrains.annotations
testCompileOnly runtimeLibs.jetbrains.annotations
@@ -201,13 +207,13 @@ base {
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 17
it.options.release = 21
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
spotless {
@@ -362,6 +368,7 @@ publishing {
tasks.register('writeActionsTestMatrix') {
doLast {
def testMatrix = []
def extendedTests = Boolean.parseBoolean(System.getenv('EXTENDED_TESTS') ?: 'false')
file('src/test/groovy/net/fabricmc/loom/test/integration').traverse {
if (it.name.endsWith("Test.groovy")) {
if (it.name.endsWith("ReproducibleBuildTest.groovy")) {
@@ -369,7 +376,7 @@ tasks.register('writeActionsTestMatrix') {
return
}
if (it.name.endsWith("DebugLineNumbersTest.groovy")) {
if (it.name.endsWith("DebugLineNumbersTest.groovy") && !extendedTests) {
// Known flakey test
return
}
@@ -389,9 +396,6 @@ tasks.register('writeActionsTestMatrix') {
// Run all the unit tests together
testMatrix.add("net.fabricmc.loom.test.unit.*")
// Kotlin tests
testMatrix.add("net.fabricmc.loom.test.kotlin.*")
def json = groovy.json.JsonOutput.toJson(testMatrix)
def output = file("build/test_matrix.json")
output.parentFile.mkdir()

View File

@@ -1,6 +1,6 @@
[versions]
kotlin = "2.0.21"
asm = "9.7.1"
asm = "9.8"
commons-io = "2.15.1"
gson = "2.10.1"
guava = "33.0.0-jre"
@@ -12,6 +12,7 @@ mapping-io = "0.7.1"
lorenz-tiny = "4.0.2"
mercury = "0.1.4.17"
loom-native = "0.2.0"
unpick = "3.0.0-beta.9"
# Plugins
spotless = "6.25.0"
@@ -48,6 +49,8 @@ fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping
fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" }
fabric-mercury = { module = "dev.architectury:mercury", version.ref = "mercury" }
fabric-loom-nativelib = { module = "net.fabricmc:fabric-loom-native", version.ref = "loom-native" }
fabric-unpick = { module = "net.fabricmc.unpick:unpick", version.ref = "unpick" }
fabric-unpick-utils = { module = "net.fabricmc.unpick:unpick-format-utils", version.ref = "unpick" }
# Misc
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

View File

@@ -8,9 +8,12 @@ vineflower = "1.11.1"
mixin-compile-extensions = "0.6.0"
dev-launch-injector = "0.2.1+build.8"
terminal-console-appender = "1.3.0"
jetbrains-annotations = "25.0.0"
jetbrains-annotations = "26.0.2"
native-support = "1.0.1"
fabric-installer = "1.0.1"
fabric-installer = "1.0.3"
# Debug tools
renderdoc = "1.37"
# Forge Runtime depedencies
javax-annotations = "3.0.2"
@@ -36,6 +39,9 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j
native-support = { module = "net.fabricmc:fabric-loom-native-support", version.ref = "native-support" }
fabric-installer = { module = "net.fabricmc:fabric-installer", version.ref = "fabric-installer" }
# Debug tools
renderdoc = { module = "org.renderdoc:renderdoc", version.ref = "renderdoc" } # Not a maven dependency
# Forge Runtime depedencies
javax-annotations = { module = "com.google.code.findbugs:jsr305", version.ref = "javax-annotations" }
mixin-remapper-service = { module = "dev.architectury:architectury-mixin-remapper-service", version.ref = "forge-runtime" }

View File

@@ -1,13 +1,15 @@
[versions]
spock = "2.3-groovy-3.0"
junit = "5.11.3"
javalin = "6.3.0"
mockito = "5.14.2"
java-debug = "0.52.0"
junit = "5.12.2"
javalin = "6.6.0"
mockito = "5.17.0"
java-debug = "0.53.1"
mixin = "0.15.3+mixin.0.8.7"
bouncycastle = "1.80"
gradle-nightly = "8.14-20250208001853+0000"
fabric-loader = "0.16.9"
gradle-latest = "9.0.0-rc-1"
gradle-nightly = "9.1.0-20250620001442+0000"
fabric-loader = "0.16.14"
[libraries]
spock = { module = "org.spockframework:spock-core", version.ref = "spock" }
@@ -17,5 +19,9 @@ javalin = { module = "io.javalin:javalin", version.ref = "javalin" }
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
java-debug = { module = "com.microsoft.java:com.microsoft.java.debug.core", version.ref = "java-debug" }
mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" }
gradle-latest = { module = "org.gradle:dummy", version.ref = "gradle-latest" }
gradle-nightly = { module = "org.gradle:dummy", version.ref = "gradle-nightly" }
fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" }
bcutil = { module = "org.bouncycastle:bcutil-jdk18on", version.ref = "bouncycastle" }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -205,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -155,6 +155,11 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
boolean isProjectIsolationActive();
/**
* @return true when '--write-verification-metadata` is set
*/
boolean isCollectingDependencyVerificationMetadata();
// ===================
// Architectury Loom
// ===================

View File

@@ -27,10 +27,10 @@ package net.fabricmc.loom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.gradle.api.Plugin;
@@ -98,8 +98,8 @@ public class LoomGradlePlugin implements Plugin<PluginAware> {
LibraryLocationLogger.logLibraryVersions();
// Apply default plugins
project.apply(ImmutableMap.of("plugin", "java-library"));
project.apply(ImmutableMap.of("plugin", "eclipse"));
project.apply(Map.of("plugin", "java-library"));
project.apply(Map.of("plugin", "eclipse"));
// Setup extensions
project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project));

View File

@@ -40,6 +40,8 @@ import org.gradle.api.tasks.SourceSet;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
/**
* A {@link Named} object for configuring "proxy" configurations that remap artifacts.
*/
@@ -140,7 +142,7 @@ public abstract class RemapConfigurationSettings implements Named {
}
private Provider<Boolean> defaultDependencyTransforms() {
return getSourceSet().map(sourceSet -> sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME) || sourceSet.getName().equals("client"));
return getSourceSet().map(sourceSet -> sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME) || sourceSet.getName().equals(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME));
}
@Override

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2021 FabricMC
* Copyright (c) 2016-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
@@ -46,6 +46,8 @@ public interface MappingContext {
Supplier<MemoryMappingTree> intermediaryTree();
boolean isUsingIntermediateMappings();
MinecraftProvider minecraftProvider();
default String minecraftVersion() {
@@ -62,4 +64,6 @@ public interface MappingContext {
DownloadBuilder download(String url);
boolean refreshDeps();
boolean hasProperty(String property);
}

View File

@@ -34,9 +34,16 @@ public interface SpecContext {
List<FabricModJson> localMods();
// Returns mods that are both on the compile and runtime classpath
/**
* Return a set of mods that should be used for transforms, that target EITHER the common or client.
*/
List<FabricModJson> modDependenciesCompileRuntime();
/**
* Return a set of mods that should be used for transforms, that target ONLY the client.
*/
List<FabricModJson> modDependenciesCompileRuntimeClient();
default List<FabricModJson> allMods() {
return Stream.concat(modDependencies().stream(), localMods().stream()).toList();
}

View File

@@ -25,10 +25,10 @@
package net.fabricmc.loom.build.mixin;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
@@ -41,7 +41,7 @@ public class GroovyApInvoker extends AnnotationProcessorInvoker<GroovyCompile> {
public GroovyApInvoker(Project project) {
super(
project,
ImmutableList.of(),
List.of(),
getInvokerTasks(project),
AnnotationProcessorInvoker.GROOVY);
}

View File

@@ -25,10 +25,10 @@
package net.fabricmc.loom.build.mixin;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
@@ -42,7 +42,7 @@ public class ScalaApInvoker extends AnnotationProcessorInvoker<ScalaCompile> {
super(
project,
// Scala just uses the java AP configuration afaik. This of course assumes the java AP also gets configured.
ImmutableList.of(),
List.of(),
getInvokerTasks(project),
AnnotationProcessorInvoker.SCALA);
}

View File

@@ -39,7 +39,6 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import com.google.common.hash.Hashing;
import com.google.gson.JsonObject;
import org.apache.commons.io.FileUtils;
import org.gradle.api.artifacts.ArtifactView;
@@ -63,6 +62,7 @@ import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.task.AbstractLoomTask;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.ZipReprocessorUtil;
import net.fabricmc.loom.util.ZipUtils;
@@ -194,9 +194,7 @@ public abstract class NestableJarGenerationTask extends AbstractLoomTask {
// Fabric Loader can't handle modIds longer than 64 characters
if (modId.length() > 64) {
String hash = Hashing.sha256()
.hashString(modId, StandardCharsets.UTF_8)
.toString();
String hash = Checksum.of(modId).sha256().hex();
modId = modId.substring(0, 50) + hash.substring(0, 14);
}

View File

@@ -367,7 +367,7 @@ public abstract class CompileConfiguration implements Runnable {
private LockFile getLockFile() {
final LoomGradleExtension extension = LoomGradleExtension.get(getProject());
final Path cacheDirectory = extension.getFiles().getUserCache().toPath();
final String pathHash = Checksum.projectHash(getProject());
final String pathHash = Checksum.of(getProject()).sha1().hex();
return new LockFile(
cacheDirectory.resolve("." + pathHash + ".lock"),
"Lock for cache='%s', project='%s'".formatted(

View File

@@ -138,7 +138,6 @@ public abstract class LoomConfigurations implements Runnable {
register(Constants.Configurations.MAPPINGS, Role.RESOLVABLE);
register(Constants.Configurations.MAPPINGS_FINAL, Role.RESOLVABLE);
register(Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES, Role.RESOLVABLE);
register(Constants.Configurations.UNPICK_CLASSPATH, Role.RESOLVABLE);
register(Constants.Configurations.LOCAL_RUNTIME, Role.RESOLVABLE);
extendsFrom(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME, Constants.Configurations.LOCAL_RUNTIME);

View File

@@ -35,7 +35,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import com.google.common.collect.ImmutableMap;
import groovy.util.Node;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
@@ -54,7 +53,7 @@ import net.fabricmc.loom.util.gradle.GradleUtils;
public abstract class MavenPublication implements Runnable {
// ImmutableMap is needed since it guarantees ordering
// (compile must go before runtime, or otherwise dependencies might get the "weaker" runtime scope).
private static final Map<String, String> CONFIGURATION_TO_SCOPE = ImmutableMap.of(
private static final Map<String, String> CONFIGURATION_TO_SCOPE = Map.of(
JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME, "compile",
JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME, "runtime"
);

View File

@@ -25,7 +25,6 @@
package net.fabricmc.loom.configuration.accesswidener;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -40,11 +39,7 @@ import net.fabricmc.tinyremapper.TinyRemapper;
public record LocalAccessWidenerEntry(Path path, String hash) implements AccessWidenerEntry {
public static LocalAccessWidenerEntry create(Path path) {
try {
return new LocalAccessWidenerEntry(path, Checksum.sha1Hex(path));
} catch (IOException e) {
throw new UncheckedIOException("Failed to create LocalAccessWidenerEntry", e);
}
return new LocalAccessWidenerEntry(path, Checksum.of(path).sha1().hex());
}
@Override

View File

@@ -24,18 +24,12 @@
package net.fabricmc.loom.configuration.decompile;
import java.io.File;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraftProvider;
import net.fabricmc.loom.task.GenerateSourcesTask;
import net.fabricmc.loom.util.Constants;
public abstract class DecompileConfiguration<T extends MappedMinecraftProvider> {
static final String DEFAULT_DECOMPILER = "Vineflower";
@@ -55,15 +49,4 @@ public abstract class DecompileConfiguration<T extends MappedMinecraftProvider>
public abstract String getTaskName(MinecraftJar.Type type);
public abstract void afterEvaluation();
protected final void configureUnpick(GenerateSourcesTask task, File unpickOutputJar) {
final ConfigurationContainer configurations = task.getProject().getConfigurations();
task.getUnpickDefinitions().set(mappingConfiguration.getUnpickDefinitionsFile());
task.getUnpickOutputJar().set(unpickOutputJar);
task.getUnpickConstantJar().setFrom(configurations.getByName(Constants.Configurations.MAPPING_CONSTANTS));
task.getUnpickClasspath().setFrom(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
task.getUnpickClasspath().from(configurations.getByName(Constants.Configurations.MOD_COMPILE_CLASSPATH_MAPPED));
extension.getMinecraftJars(MappingsNamespace.NAMED).forEach(task.getUnpickClasspath()::from);
}
}

View File

@@ -24,7 +24,6 @@
package net.fabricmc.loom.configuration.decompile;
import java.io.File;
import java.util.List;
import org.gradle.api.Project;
@@ -66,11 +65,6 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration<Mapp
task.dependsOn(project.getTasks().named("validateAccessWidener"));
task.setDescription("Decompile minecraft using %s.".formatted(decompilerName));
task.setGroup(Constants.TaskGroup.FABRIC);
if (mappingConfiguration.hasUnpickDefinitions()) {
final File outputJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-unpicked.jar");
configureUnpick(task, outputJar);
}
});
});

View File

@@ -24,8 +24,6 @@
package net.fabricmc.loom.configuration.decompile;
import java.io.File;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.Task;
@@ -56,22 +54,12 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
final TaskProvider<Task> commonDecompileTask = createDecompileTasks("Common", task -> {
task.getInputJarName().set(commonJar.getName());
task.getSourcesOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", commonJar.getPath()));
if (mappingConfiguration.hasUnpickDefinitions()) {
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-common-unpicked.jar");
configureUnpick(task, unpickJar);
}
});
final TaskProvider<Task> clientOnlyDecompileTask = createDecompileTasks("ClientOnly", task -> {
task.getInputJarName().set(clientOnlyJar.getName());
task.getSourcesOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath()));
if (mappingConfiguration.hasUnpickDefinitions()) {
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-clientonly-unpicked.jar");
configureUnpick(task, unpickJar);
}
// Don't allow them to run at the same time.
task.mustRunAfter(commonDecompileTask);
});

View File

@@ -41,9 +41,9 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import groovy.xml.XmlUtil;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
@@ -51,9 +51,6 @@ import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedModuleVersion;
import org.gradle.api.tasks.SourceSet;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.InstallerData;
@@ -78,42 +75,7 @@ public class RunConfig {
public transient SourceSet sourceSet;
public Map<String, Object> environmentVariables;
public String projectName;
public Element genRuns(Element doc) {
Element root = this.addXml(doc, "component", ImmutableMap.of("name", "ProjectRunConfigurationManager"));
root = addXml(root, "configuration", ImmutableMap.of("default", "false", "name", configName, "type", "Application", "factoryName", "Application"));
this.addXml(root, "module", ImmutableMap.of("name", ideaModuleName));
this.addXml(root, "option", ImmutableMap.of("name", "MAIN_CLASS_NAME", "value", mainClass));
this.addXml(root, "option", ImmutableMap.of("name", "WORKING_DIRECTORY", "value", runDirIdeaUrl));
if (!vmArgs.isEmpty()) {
this.addXml(root, "option", ImmutableMap.of("name", "VM_PARAMETERS", "value", joinArguments(vmArgs)));
}
if (!programArgs.isEmpty()) {
this.addXml(root, "option", ImmutableMap.of("name", "PROGRAM_PARAMETERS", "value", joinArguments(programArgs)));
}
return root;
}
public Element addXml(Node parent, String name, Map<String, String> values) {
Document doc = parent.getOwnerDocument();
if (doc == null) {
doc = (Document) parent;
}
Element e = doc.createElement(name);
for (Map.Entry<String, String> entry : values.entrySet()) {
e.setAttribute(entry.getKey(), entry.getValue());
}
parent.appendChild(e);
return e;
}
public String folderName;
// Turns camelCase/PascalCase into Capital Case
// caseConversionExample -> Case Conversion Example
@@ -194,6 +156,7 @@ public class RunConfig {
runConfig.environmentVariables = new HashMap<>();
runConfig.environmentVariables.putAll(settings.getEnvironmentVariables());
runConfig.projectName = project.getName();
runConfig.folderName = settings.getIdeConfigFolder().getOrNull();
for (Consumer<RunConfig> consumer : extension.getSettingsPostEdit()) {
consumer.accept(runConfig);
@@ -228,6 +191,7 @@ public class RunConfig {
dummyConfig = dummyConfig.replace("%VM_ARGS%", joinArguments(vmArgs).replaceAll("\"", "&quot;"));
dummyConfig = dummyConfig.replace("%IDEA_ENV_VARS%", getEnvVars("<env name=\"%s\" value=\"%s\"/>"));
dummyConfig = dummyConfig.replace("%ECLIPSE_ENV_VARS%", getEnvVars("<mapEntry key=\"%s\" value=\"%s\"/>"));
dummyConfig = dummyConfig.replace("%IDEA_FOLDER_NAME%", folderName == null ? "" : "folderName=\"" + XmlUtil.escapeXml(folderName) + "\"");
return dummyConfig;
}

View File

@@ -54,7 +54,7 @@ import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.Platform;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
public class RunConfigSettings implements Named {
public abstract class RunConfigSettings implements Named {
/**
* Arguments for the JVM, such as system properties.
*/
@@ -465,6 +465,7 @@ public class RunConfigSettings implements Named {
defaultMainClass = parent.defaultMainClass;
source = parent.source;
ideConfigGenerated = parent.ideConfigGenerated;
getIdeConfigFolder().set(parent.getIdeConfigFolder());
}
public void makeRunDir() {
@@ -483,6 +484,15 @@ public class RunConfigSettings implements Named {
this.ideConfigGenerated = ideConfigGenerated;
}
/**
* Group this run config under the given folder.
*
* <p>This is currently only supported on IntelliJ IDEA.
*
* @return The property used to set the config folder.
*/
public abstract Property<String> getIdeConfigFolder();
@ApiStatus.Internal
@ApiStatus.Experimental
public Property<String> devLaunchMainClass() {

View File

@@ -100,10 +100,14 @@ public abstract class InterfaceInjectionProcessor implements MinecraftJarProcess
return null;
}
return new Spec(injectedInterfaces);
Set<String> clientOnlyModIds = context.modDependenciesCompileRuntimeClient().stream()
.map(FabricModJson::getId)
.collect(Collectors.toSet());
return new Spec(injectedInterfaces, clientOnlyModIds);
}
public record Spec(List<InjectedInterface> injectedInterfaces) implements MinecraftJarProcessor.Spec {
public record Spec(List<InjectedInterface> injectedInterfaces, Set<String> clientOnlyModIds) implements MinecraftJarProcessor.Spec {
}
@Override
@@ -115,6 +119,10 @@ public abstract class InterfaceInjectionProcessor implements MinecraftJarProcess
try (LazyCloseable<TinyRemapper> tinyRemapper = context.createRemapper(MappingsNamespace.INTERMEDIARY, MappingsNamespace.NAMED)) {
final List<InjectedInterface> remappedInjectedInterfaces = spec.injectedInterfaces().stream()
.filter(injectedInterface -> {
return context.includesClient() // The client jar depends on the server, so always apply all to it
|| !spec.clientOnlyModIds.contains(injectedInterface.modId()); // Or the mod is NOT only found on the client classpath, so we can apply it to the server jar
})
.map(injectedInterface -> remap(
injectedInterface,
s -> mappings.mapClassName(s, intermediaryIndex, namedIndex),

View File

@@ -69,7 +69,7 @@ public interface ArtifactRef {
}
public String version() {
return replaceIfNullOrEmpty(artifact.getModuleVersion().getId().getVersion(), () -> Checksum.truncatedSha256(artifact.getFile()));
return replaceIfNullOrEmpty(artifact.getModuleVersion().getId().getVersion(), () -> Checksum.of(artifact.getFile()).sha256().hex(10));
}
public String classifier() {

View File

@@ -38,7 +38,6 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import com.google.common.collect.ImmutableMap;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.FileCollectionDependency;
@@ -58,6 +57,8 @@ import org.gradle.api.tasks.SourceSet;
import org.gradle.jvm.JvmLibrary;
import org.gradle.language.base.artifact.SourcesArtifact;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin;
@@ -65,6 +66,7 @@ import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.configuration.RemapConfigurations;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.loom.configuration.mods.dependency.ModDependencyFactory;
import net.fabricmc.loom.configuration.mods.dependency.ModDependencyOptions;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants;
@@ -79,6 +81,8 @@ public class ModConfigurationRemapper {
// This can happen when the dependency is a FileCollectionDependency or from a flatDir repository.
public static final String MISSING_GROUP = "unspecified";
private static final Logger LOGGER = LoggerFactory.getLogger(ModConfigurationRemapper.class);
public static void supplyModConfigurations(Project project, ServiceFactory serviceFactory, String mappingsSuffix, LoomGradleExtension extension, SourceRemapper sourceRemapper) {
final DependencyHandler dependencies = project.getDependencies();
// The configurations where the source and remapped artifacts go.
@@ -98,7 +102,7 @@ public class ModConfigurationRemapper {
for (RemapConfigurationSettings entry : remapConfigurationSettings) {
// key: true if runtime, false if compile
final Map<Boolean, Boolean> envToEnabled = ImmutableMap.of(
final Map<Boolean, Boolean> envToEnabled = Map.of(
false, entry.getOnCompileClasspath().get(),
true, entry.getOnRuntimeClasspath().get()
);
@@ -135,6 +139,15 @@ public class ModConfigurationRemapper {
}
}
final ModDependencyOptions modDependencyOptions = ModDependencyOptions.create(project, ModDependencyOptions.class, options -> {
options.getMappings().set(mappingsSuffix);
options.getInlineRefmap().set(extension.getMixin().getInlineDependencyRefmaps());
});
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Mod dependency options: {}", modDependencyOptions.getJson());
}
// Round 1: Discovery
// Go through all the configs to find artifacts to remap and
// the installer data. The installer data has to be added before
@@ -177,7 +190,7 @@ public class ModConfigurationRemapper {
continue;
}
final ModDependency modDependency = ModDependencyFactory.create(artifact, artifactMetadata, remappedConfig, clientRemappedConfig, mappingsSuffix, project);
final ModDependency modDependency = ModDependencyFactory.create(artifact, artifactMetadata, remappedConfig, clientRemappedConfig, modDependencyOptions, project);
scheduleSourcesRemapping(project, sourceRemapper, modDependency);
modDependencies.add(modDependency);
}
@@ -263,7 +276,7 @@ public class ModConfigurationRemapper {
for (File artifact : files) {
final String name = getNameWithoutExtension(artifact.toPath());
final String version = replaceIfNullOrEmpty(dependency.getVersion(), () -> Checksum.truncatedSha256(artifact));
final String version = replaceIfNullOrEmpty(dependency.getVersion(), () -> Checksum.of(artifact).sha256().hex(10));
artifacts.add(new ArtifactRef.FileArtifactRef(artifact.toPath(), group, name, version));
}
}
@@ -333,7 +346,7 @@ public class ModConfigurationRemapper {
}
if (dependency.isCacheInvalid(project, "sources")) {
final Path output = dependency.getWorkingFile("sources");
final Path output = dependency.getWorkingFile(project, "sources");
sourceRemapper.scheduleRemapSources(sourcesInput.toFile(), output.toFile(), false, true, () -> {
try {

View File

@@ -37,6 +37,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -48,15 +49,19 @@ import dev.architectury.loom.util.MappingOption;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Usage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.loom.configuration.mods.extension.ModProcessorExtension;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.extension.RemapperExtensionHolder;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.IdentityBiMap;
import net.fabricmc.loom.util.LoggerFilter;
import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.Pair;
@@ -73,11 +78,12 @@ import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.NonClassCopyMode;
import net.fabricmc.tinyremapper.OutputConsumerPath;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.extension.mixin.MixinExtension;
public class ModProcessor {
private static final String toM = MappingsNamespace.NAMED.toString();
private static final Logger LOGGER = LoggerFactory.getLogger(ModProcessor.class);
private static final Pattern COPY_CONFIGURATION_PATTERN = Pattern.compile("^(.+)Copy[0-9]*$");
private final Project project;
@@ -92,7 +98,7 @@ public class ModProcessor {
public void processMods(List<ModDependency> remapList) throws IOException {
try {
project.getLogger().lifecycle(":remapping {} mods from {}", remapList.size(), describeConfiguration(sourceConfiguration));
LOGGER.info(":remapping {} mods from {}", remapList.size(), describeConfiguration(sourceConfiguration));
remapJars(remapList);
} catch (Exception e) {
throw new RuntimeException(String.format(Locale.ENGLISH, "Failed to remap %d mods", remapList.size()), e);
@@ -192,13 +198,21 @@ public class ModProcessor {
builder.extension(kotlinRemapperClassloader.getTinyRemapperExtension());
}
final Set<InputTag> remapMixins = new HashSet<>();
final boolean requiresStaticMixinRemap = remapList.stream()
.anyMatch(modDependency -> modDependency.getMetadata().mixinRemapType() == ArtifactMetadata.MixinRemapType.STATIC);
final IdentityBiMap<InputTag, ModDependency> inputTags = new IdentityBiMap<>();
final List<ModProcessorExtension> activeExtensions = ModProcessorExtension.EXTENSIONS.stream()
.filter(e -> remapList.stream().anyMatch(e::appliesTo))
.toList();
final ModProcessorExtension.Context context = new ModProcessorExtension.Context(fromM, toM, remapList);
if (requiresStaticMixinRemap) {
// Configure the mixin extension to remap mixins from mod jars that were remapped with the mixin extension.
builder.extension(new MixinExtension(remapMixins::contains));
for (ModProcessorExtension modProcessorExtension : activeExtensions) {
LOGGER.info("Applying mod processor extension: {}", modProcessorExtension.getClass().getSimpleName());
final Predicate<InputTag> applyPredicate = inputTag -> {
ModDependency mod = inputTags.getByKey(inputTag);
return mod != null && modProcessorExtension.appliesTo(mod);
};
builder.extension(modProcessorExtension.createExtension(context, applyPredicate));
}
for (RemapperExtensionHolder holder : extension.getRemapperExtensions().get()) {
@@ -209,14 +223,13 @@ public class ModProcessor {
remapper.readClassPath(extension.getMinecraftJars(IntermediaryNamespaces.runtimeIntermediaryNamespace(project)).toArray(Path[]::new));
final Map<ModDependency, InputTag> tagMap = new HashMap<>();
final Map<ModDependency, OutputConsumerPath> outputConsumerMap = new HashMap<>();
final Map<ModDependency, Pair<byte[], String>> accessWidenerMap = new HashMap<>();
for (RemapConfigurationSettings entry : extension.getRemapConfigurations()) {
for (File inputFile : entry.getSourceConfiguration().get().getFiles()) {
if (remapList.stream().noneMatch(info -> info.getInputFile().toFile().equals(inputFile))) {
project.getLogger().debug("Adding " + inputFile + " onto the remap classpath");
LOGGER.debug("Adding " + inputFile + " onto the remap classpath");
remapper.readClassPathAsync(inputFile.toPath());
}
}
@@ -225,23 +238,10 @@ public class ModProcessor {
for (ModDependency info : remapList) {
InputTag tag = remapper.createInputTag();
project.getLogger().debug("Adding " + info.getInputFile() + " as a remap input");
// Note: this is done at a jar level, not at the level of an individual mixin config.
// If a mod has multiple mixin configs, it's assumed that either all or none of them have refmaps.
if (info.getMetadata().mixinRemapType() == ArtifactMetadata.MixinRemapType.STATIC) {
if (!requiresStaticMixinRemap) {
// Should be impossible but stranger things have happened.
throw new IllegalStateException("Was not configured for static remap, but a mod required it?!");
}
project.getLogger().info("Remapping mixins in {} statically", info.getInputFile());
remapMixins.add(tag);
}
LOGGER.debug("Adding " + info.getInputFile() + " as a remap input");
inputTags.put(tag, info);
remapper.readInputsAsync(tag, info.getInputFile());
tagMap.put(info, tag);
Files.deleteIfExists(getRemappedOutput(info));
}
@@ -258,12 +258,12 @@ public class ModProcessor {
final AccessWidenerUtils.AccessWidenerData accessWidenerData = AccessWidenerUtils.readAccessWidenerData(dependency.getInputFile(), platform);
if (accessWidenerData != null) {
project.getLogger().debug("Remapping access widener in {}", dependency.getInputFile());
LOGGER.debug("Remapping access widener in {}", dependency.getInputFile());
byte[] remappedAw = AccessWidenerUtils.remapAccessWidener(accessWidenerData.content(), remapper.getEnvironment().getRemapper());
accessWidenerMap.put(dependency, new Pair<>(remappedAw, accessWidenerData.path()));
}
remapper.apply(outputConsumer, tagMap.get(dependency));
remapper.apply(outputConsumer, inputTags.getByValue(dependency));
} catch (Exception e) {
throw new RuntimeException("Failed to remap: " + dependency, e);
}
@@ -288,6 +288,12 @@ public class ModProcessor {
ZipUtils.replace(output, accessWidener.right(), accessWidener.left());
}
for (ModProcessorExtension modProcessorExtension : activeExtensions) {
if (modProcessorExtension.appliesTo(dependency)) {
modProcessorExtension.finalise(dependency, output);
}
}
stripNestedJars(output);
remapJarManifestEntries(output);
@@ -307,8 +313,8 @@ public class ModProcessor {
}
}
private static Path getRemappedOutput(ModDependency dependency) {
return dependency.getWorkingFile(null);
private Path getRemappedOutput(ModDependency dependency) {
return dependency.getWorkingFile(project, null);
}
private void remapJarManifestEntries(Path jar) throws IOException {

View File

@@ -34,7 +34,11 @@ import java.nio.file.StandardCopyOption;
import org.jetbrains.annotations.Nullable;
public record LocalMavenHelper(String group, String name, String version, @Nullable String baseClassifier, Path root) {
public record LocalMavenHelper(String group, String name, String version, @Nullable String baseClassifier, Path root, @Nullable String snapshotVersion) {
public LocalMavenHelper(String group, String name, String version, @Nullable String baseClassifier, Path root) {
this(group, name, version, baseClassifier, root, null);
}
public Path copyToMaven(Path artifact, @Nullable String classifier) throws IOException {
if (!artifact.getFileName().toString().endsWith(".jar")) {
throw new UnsupportedOperationException();
@@ -77,6 +81,13 @@ public record LocalMavenHelper(String group, String name, String version, @Nulla
}
private Path getDirectory() {
String version = this.version();
// When using a specific snapshot version the directory name should be the 1.0.0-SNAPSHOT version
if (this.snapshotVersion() != null) {
version = this.snapshotVersion();
}
return root.resolve("%s/%s/%s".formatted(group.replace(".", "/"), name, version));
}

View File

@@ -28,6 +28,8 @@ import java.io.IOException;
import java.nio.file.Path;
import org.gradle.api.Project;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.internal.artifacts.repositories.resolver.MavenUniqueSnapshotComponentIdentifier;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
@@ -37,23 +39,21 @@ import net.fabricmc.loom.configuration.mods.ArtifactRef;
public abstract sealed class ModDependency permits SplitModDependency, SimpleModDependency {
private final ArtifactRef artifact;
private final ArtifactMetadata metadata;
protected final String group;
protected final String name;
protected final String version;
private final String group;
private final String name;
private final String version;
@Nullable
protected final String classifier;
protected final String mappingsSuffix;
protected final Project project;
private final String classifier;
private final ModDependencyOptions options;
public ModDependency(ArtifactRef artifact, ArtifactMetadata metadata, String mappingsSuffix, Project project) {
public ModDependency(ArtifactRef artifact, ArtifactMetadata metadata, ModDependencyOptions options) {
this.artifact = artifact;
this.metadata = metadata;
this.group = artifact.group();
this.name = artifact.name();
this.version = artifact.version();
this.classifier = artifact.classifier();
this.mappingsSuffix = mappingsSuffix;
this.project = project;
this.options = options;
}
/**
@@ -71,10 +71,27 @@ public abstract sealed class ModDependency permits SplitModDependency, SimpleMod
*/
public abstract void applyToProject(Project project);
protected LocalMavenHelper createMaven(String name) {
/**
* Create a maven helper for the local cache.
* @param type The jar type, e.g "common" or "client" for split dependencies.
*/
protected LocalMavenHelper createMavenHelper(Project project, @Nullable String type) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final Path root = extension.getFiles().getRemappedModCache().toPath();
return new LocalMavenHelper(getRemappedGroup(), name, this.version, this.classifier, root);
final String fullName = getName() + (type != null ? "-" + type : "");
return new LocalMavenHelper(getGroup(), fullName, this.version, this.classifier, root, getSnapshotVersion());
}
private @Nullable String getSnapshotVersion() {
if (artifact instanceof ArtifactRef.ResolvedArtifactRef resolvedArtifactRef) {
ComponentIdentifier componentIdentifier = resolvedArtifactRef.artifact().getId().getComponentIdentifier();
if (componentIdentifier instanceof MavenUniqueSnapshotComponentIdentifier mavenUniqueId) {
return mavenUniqueId.getSnapshotVersion();
}
}
return null;
}
public ArtifactRef getInputArtifact() {
@@ -85,26 +102,34 @@ public abstract sealed class ModDependency permits SplitModDependency, SimpleMod
return metadata;
}
protected String getRemappedGroup() {
return getMappingsPrefix() + "." + group;
protected String getName() {
return "%s-%s".formatted(name, options.getCacheKey());
}
private String getMappingsPrefix() {
return mappingsSuffix.replace(".", "_").replace("-", "_").replace("+", "_");
protected String getGroup() {
return "remapped.%s".formatted(group);
}
protected String getVersion() {
return version;
}
public Path getInputFile() {
return artifact.path();
}
public Path getWorkingFile(@Nullable String classifier) {
public Path getWorkingFile(Project project, @Nullable String classifier) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final String fileName = classifier == null ? String.format("%s-%s-%s.jar", getRemappedGroup(), name, version)
: String.format("%s-%s-%s-%s.jar", getRemappedGroup(), name, version, classifier);
final String fileName = classifier == null ? String.format("%s-%s-%s.jar", getGroup(), getName(), version)
: String.format("%s-%s-%s-%s.jar", getGroup(), getName(), version, classifier);
return extension.getFiles().getProjectBuildCache().toPath().resolve("remapped_working").resolve(fileName);
}
public ModDependencyOptions getOptions() {
return options;
}
@Override
public String toString() {
return "ModDependency{" + "group='" + group + '\'' + ", name='" + name + '\'' + ", version='" + version + '\'' + ", classifier='" + classifier + '\'' + '}';

View File

@@ -41,7 +41,7 @@ import net.fabricmc.loom.util.AttributeHelper;
public class ModDependencyFactory {
private static final String TARGET_ATTRIBUTE_KEY = "loom-target";
public static ModDependency create(ArtifactRef artifact, ArtifactMetadata metadata, Configuration targetConfig, @Nullable Configuration targetClientConfig, String mappingsSuffix, Project project) {
public static ModDependency create(ArtifactRef artifact, ArtifactMetadata metadata, Configuration targetConfig, @Nullable Configuration targetClientConfig, ModDependencyOptions options, Project project) {
if (targetClientConfig != null && LoomGradleExtension.get(project).getSplitModDependencies().get()) {
final Optional<JarSplitter.Target> cachedTarget = readTarget(artifact);
JarSplitter.Target target;
@@ -54,11 +54,11 @@ public class ModDependencyFactory {
}
if (target != null) {
return new SplitModDependency(artifact, metadata, mappingsSuffix, targetConfig, targetClientConfig, target, project);
return new SplitModDependency(artifact, metadata, options, targetConfig, targetClientConfig, target, project);
}
}
return new SimpleModDependency(artifact, metadata, mappingsSuffix, targetConfig, project);
return new SimpleModDependency(artifact, metadata, options, targetConfig, project);
}
private static Optional<JarSplitter.Target> readTarget(ArtifactRef artifact) {

View File

@@ -0,0 +1,38 @@
/*
* 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.configuration.mods.dependency;
import org.gradle.api.provider.Property;
import net.fabricmc.loom.util.CacheKey;
/**
* Inputs used to process a mod dependency. The output jar is cached based on these properties.
*/
public abstract class ModDependencyOptions extends CacheKey {
public abstract Property<String> getMappings();
public abstract Property<Boolean> getInlineRefmap();
}

View File

@@ -40,10 +40,10 @@ public final class SimpleModDependency extends ModDependency {
private final Configuration targetConfig;
private final LocalMavenHelper maven;
public SimpleModDependency(ArtifactRef artifact, ArtifactMetadata metadata, String mappingsSuffix, Configuration targetConfig, Project project) {
super(artifact, metadata, mappingsSuffix, project);
public SimpleModDependency(ArtifactRef artifact, ArtifactMetadata metadata, ModDependencyOptions options, Configuration targetConfig, Project project) {
super(artifact, metadata, options);
this.targetConfig = Objects.requireNonNull(targetConfig);
this.maven = createMaven(name);
this.maven = createMavenHelper(project, null);
}
@Override

View File

@@ -48,13 +48,13 @@ public final class SplitModDependency extends ModDependency {
@Nullable
private final LocalMavenHelper clientMaven;
public SplitModDependency(ArtifactRef artifact, ArtifactMetadata metadata, String mappingsSuffix, Configuration targetCommonConfig, Configuration targetClientConfig, JarSplitter.Target target, Project project) {
super(artifact, metadata, mappingsSuffix, project);
public SplitModDependency(ArtifactRef artifact, ArtifactMetadata metadata, ModDependencyOptions options, Configuration targetCommonConfig, Configuration targetClientConfig, JarSplitter.Target target, Project project) {
super(artifact, metadata, options);
this.targetCommonConfig = Objects.requireNonNull(targetCommonConfig);
this.targetClientConfig = Objects.requireNonNull(targetClientConfig);
this.target = Objects.requireNonNull(target);
this.commonMaven = target.common() ? createMaven(name + "-common") : null;
this.clientMaven = target.client() ? createMaven(name + "-client") : null;
this.commonMaven = target.common() ? createMavenHelper(project, "common") : null;
this.clientMaven = target.client() ? createMavenHelper(project, "client") : null;
}
@Override
@@ -86,8 +86,8 @@ public final class SplitModDependency extends ModDependency {
// Split the jar into 2
case SPLIT -> {
final String suffix = variant == null ? "" : "-" + variant;
final Path commonTempJar = getWorkingFile("common" + suffix);
final Path clientTempJar = getWorkingFile("client" + suffix);
final Path commonTempJar = getWorkingFile(project, "common" + suffix);
final Path clientTempJar = getWorkingFile(project, "client" + suffix);
final JarSplitter splitter = new JarSplitter(path);
splitter.split(commonTempJar, clientTempJar);
@@ -114,15 +114,16 @@ public final class SplitModDependency extends ModDependency {
if (target == JarSplitter.Target.SPLIT) {
createModGroup(
project,
getCommonMaven().getOutputFile(null),
getClientMaven().getOutputFile(null)
);
}
}
private void createModGroup(Path commonJar, Path clientJar) {
private void createModGroup(Project project, Path commonJar, Path clientJar) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
final ModSettings modSettings = extension.getMods().maybeCreate(String.format("%s-%s-%s", getRemappedGroup(), name, version));
final ModSettings modSettings = extension.getMods().maybeCreate(String.format("%s-%s-%s", getGroup(), getName(), getVersion()));
modSettings.getModFiles().from(
commonJar.toFile(),
clientJar.toFile()

View File

@@ -0,0 +1,29 @@
/*
* 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.configuration.mods.dependency.refmap;
public interface MixinReferenceRemapper {
String remapReference(String mixinClassName, String reference);
}

View File

@@ -0,0 +1,69 @@
/*
* 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.configuration.mods.dependency.refmap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.fmj.mixin.MixinRefmap;
public record MixinReferenceRemapperImpl(Map<String, MixinRefmap.ReferenceMappingData> data) implements MixinReferenceRemapper {
private static final Logger LOGGER = LoggerFactory.getLogger(MixinReferenceRemapperImpl.class);
public static MixinReferenceRemapper createFromRefmaps(String from, String to, Stream<MixinRefmap> refmaps) {
MixinRefmap.NamespacePair namespaces = new MixinRefmap.NamespacePair(from, to);
Map<String, MixinRefmap.ReferenceMappingData> data = refmaps
.map(refmap -> refmap.getData(namespaces))
.filter(Objects::nonNull)
.map(MixinRefmap.MixinMappingData::data)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(existing, replacement) -> {
// TODO we could merge this, but it should never happen in practice
LOGGER.warn("Duplicate mixin reference mapping for {} in refmaps, using the first one", existing);
return existing;
}
));
return new MixinReferenceRemapperImpl(data);
}
@Override
public String remapReference(String mixinClassName, String reference) {
final MixinRefmap.ReferenceMappingData data = data().get(mixinClassName);
if (data != null) {
return data.remap(reference);
}
return reference;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.configuration.mods.dependency.refmap;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.configuration.mods.ArtifactMetadata;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
import net.fabricmc.loom.util.fmj.mixin.MixinConfiguration;
public class MixinRefmapInliner {
private static final Logger LOGGER = LoggerFactory.getLogger(MixinRefmapInliner.class);
public static MixinReferenceRemapper createRemapper(String from, String to, List<ModDependency> mods) throws IOException {
List<MixinConfiguration> mixinConfigurations = new ArrayList<>();
for (ModDependency mod : mods) {
if (mod.getMetadata().mixinRemapType() != ArtifactMetadata.MixinRemapType.MIXIN) {
continue;
}
FabricModJson fabricModJson = FabricModJsonFactory.createFromZipNullable(mod.getInputFile());
if (fabricModJson == null) {
LOGGER.warn("Failed to read fabric.mod.json from {}", mod.getInputFile());
continue;
}
try {
mixinConfigurations.addAll(MixinConfiguration.fromMod(fabricModJson));
} catch (IOException e) {
throw ExceptionUtil.createDescriptiveWrapper(IOException::new, "Failed to read mixin configuration from " + mod.getInputFile(), e);
}
}
return MixinReferenceRemapperImpl.createFromRefmaps(from, to, mixinConfigurations.stream().map(MixinConfiguration::refmap));
}
public static void removeRefmap(ModDependency modDependency, Path ouputPath) {
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.configuration.mods.dependency.refmap;
import java.util.function.Predicate;
import org.objectweb.asm.ClassVisitor;
import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.api.TrClass;
public record MixinRefmapInlinerApplyVisitorProvider(
MixinReferenceRemapper remapper,
// A set of input tags that do NOT need their refmaps inlined
Predicate<InputTag> staticRemappedMixins) implements TinyRemapper.ApplyVisitorProvider, TinyRemapper.Extension {
@Override
public ClassVisitor insertApplyVisitor(TrClass cls, ClassVisitor next) {
return new MixinRefmapInlinerClassVisitor(remapper, next);
}
@Override
public ClassVisitor insertApplyVisitor(TrClass cls, ClassVisitor next, InputTag[] inputTags) {
for (InputTag tag : inputTags) {
if (staticRemappedMixins.test(tag)) {
// No need to inline the refmaps for this tag, as we know this was originally a statically remapped mixin with no refmap
return next;
}
}
return new MixinRefmapInlinerClassVisitor(remapper, next);
}
@Override
public void attach(TinyRemapper.Builder builder) {
builder.extraPreApplyVisitor(this);
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.configuration.mods.dependency.refmap;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import net.fabricmc.loom.util.Constants;
public class MixinRefmapInlinerClassVisitor extends ClassVisitor {
private final MixinReferenceRemapper remapper;
private String className = null;
public MixinRefmapInlinerClassVisitor(MixinReferenceRemapper remapper, ClassVisitor classVisitor) {
super(Constants.ASM_VERSION, classVisitor);
this.remapper = remapper;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible);
return new RefmapInlinerAnnotationVisitor(annotationVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new RefmapInlinerMethodVisitor(methodVisitor);
}
private class RefmapInlinerMethodVisitor extends MethodVisitor {
private RefmapInlinerMethodVisitor(MethodVisitor methodVisitor) {
super(MixinRefmapInlinerClassVisitor.super.api, methodVisitor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible);
return new RefmapInlinerAnnotationVisitor(annotationVisitor);
}
}
private class RefmapInlinerAnnotationVisitor extends AnnotationVisitor {
private RefmapInlinerAnnotationVisitor(AnnotationVisitor annotationVisitor) {
super(MixinRefmapInlinerClassVisitor.super.api, annotationVisitor);
}
@Override
public void visit(String name, Object value) {
if (value instanceof String strValue) {
value = remapper.remapReference(className, strValue);
}
super.visit(name, value);
}
@Override
public AnnotationVisitor visitArray(String name) {
AnnotationVisitor annotationVisitor = super.visitArray(name);
return new RefmapInlinerAnnotationVisitor(annotationVisitor);
}
@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
AnnotationVisitor annotationVisitor = super.visitAnnotation(name, descriptor);
return new RefmapInlinerAnnotationVisitor(annotationVisitor);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.configuration.mods.extension;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Predicate;
import net.fabricmc.loom.configuration.mods.ArtifactMetadata;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.loom.configuration.mods.dependency.refmap.MixinReferenceRemapper;
import net.fabricmc.loom.configuration.mods.dependency.refmap.MixinRefmapInliner;
import net.fabricmc.loom.configuration.mods.dependency.refmap.MixinRefmapInlinerApplyVisitorProvider;
import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.TinyRemapper;
final class InlineRefmap implements ModProcessorExtension {
static final InlineRefmap INSTANCE = new InlineRefmap();
private InlineRefmap() {
}
@Override
public boolean appliesTo(ModDependency modDependency) {
return modDependency.getOptions().getInlineRefmap().get()
&& modDependency.getMetadata().mixinRemapType() == ArtifactMetadata.MixinRemapType.MIXIN;
}
@Override
public TinyRemapper.Extension createExtension(Context ctx, Predicate<InputTag> applyPredicate) throws IOException {
MixinReferenceRemapper refmapRemapper = MixinRefmapInliner.createRemapper(ctx.from(), ctx.to(), ctx.mods());
return new MixinRefmapInlinerApplyVisitorProvider(refmapRemapper, applyPredicate);
}
@Override
public void finalise(ModDependency modDependency, Path path) throws IOException {
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.configuration.mods.extension;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Predicate;
import net.fabricmc.loom.configuration.mods.ArtifactMetadata;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.loom.configuration.mods.dependency.refmap.MixinRefmapInliner;
import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.extension.mixin.MixinExtension;
final class MixinRemap implements ModProcessorExtension {
static final MixinRemap INSTANCE = new MixinRemap();
private MixinRemap() {
}
@Override
public boolean appliesTo(ModDependency modDependency) {
return modDependency.getMetadata().mixinRemapType() == ArtifactMetadata.MixinRemapType.STATIC;
}
@Override
public TinyRemapper.Extension createExtension(Context ctx, Predicate<InputTag> applyPredicate) {
return new MixinExtension(applyPredicate);
}
@Override
public void finalise(ModDependency modDependency, Path path) throws IOException {
MixinRefmapInliner.removeRefmap(modDependency, path);
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.configuration.mods.extension;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Predicate;
import net.fabricmc.loom.configuration.mods.dependency.ModDependency;
import net.fabricmc.tinyremapper.InputTag;
import net.fabricmc.tinyremapper.TinyRemapper;
/**
* An interface to aid with applying mod-specific remapping extensions.
*/
public interface ModProcessorExtension {
List<ModProcessorExtension> EXTENSIONS = List.of(
MixinRemap.INSTANCE,
InlineRefmap.INSTANCE
);
/**
* Return true if the extension applies to the given mod dependency.
*/
boolean appliesTo(ModDependency modDependency);
/**
* Create a TinyRemapper extension that uses the predicate to only apply to mods that match appliesTo.
*/
TinyRemapper.Extension createExtension(Context ctx, Predicate<InputTag> applyPredicate) throws IOException;
void finalise(ModDependency modDependency, Path path) throws IOException;
record Context(
String from,
String to,
List<ModDependency> mods) { }
}

View File

@@ -25,7 +25,6 @@
package net.fabricmc.loom.configuration.processors;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -113,11 +112,11 @@ public final class MinecraftJarProcessorManager {
public String getJarHash() {
//fabric-loom:mod-javadoc:-1289977000
return Checksum.sha1Hex(getCacheValue().getBytes(StandardCharsets.UTF_8)).substring(0, 10);
return Checksum.of(getCacheValue()).sha1().hex(10);
}
public String getSourceMappingsHash() {
return Checksum.sha1Hex(getCacheValue().getBytes(StandardCharsets.UTF_8));
return Checksum.of(getCacheValue()).sha1().hex();
}
public boolean requiresProcessingJar(Path jar) {

View File

@@ -124,7 +124,7 @@ public abstract class ModJavadocProcessor implements MinecraftJarProcessor<ModJa
try {
final byte[] data = fabricModJson.getSource().read(javaDocPath);
mappingsHash = Checksum.sha1Hex(data);
mappingsHash = Checksum.of(data).sha1().hex();
try (Reader reader = new InputStreamReader(new ByteArrayInputStream(data))) {
MappingReader.read(reader, mappings);

View File

@@ -48,6 +48,7 @@ import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.api.processor.SpecContext;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
@@ -59,10 +60,17 @@ import net.fabricmc.loom.util.gradle.GradleUtils;
* @param localMods Mods found in the current project.
* @param compileRuntimeMods Dependent mods found in both the compile and runtime classpath.
*/
public record SpecContextImpl(List<FabricModJson> modDependencies, List<FabricModJson> localMods, List<FabricModJson> compileRuntimeMods) implements SpecContext {
public record SpecContextImpl(
List<FabricModJson> modDependencies,
List<FabricModJson> localMods,
List<ModHolder> compileRuntimeMods) implements SpecContext {
public static SpecContextImpl create(Project project) {
final Map<String, List<FabricModJson>> fmjCache = new HashMap<>();
return new SpecContextImpl(getDependentMods(project, fmjCache), FabricModJsonHelpers.getModsInProject(project), getCompileRuntimeMods(project, fmjCache));
return new SpecContextImpl(
getDependentMods(project, fmjCache),
FabricModJsonHelpers.getModsInProject(project),
getCompileRuntimeMods(project, fmjCache)
);
}
// Reruns a list of mods found on both the compile and/or runtime classpaths
@@ -108,39 +116,68 @@ public record SpecContextImpl(List<FabricModJson> modDependencies, List<FabricMo
}
// Returns a list of mods that are on both to compile and runtime classpath
private static List<FabricModJson> getCompileRuntimeMods(Project project, Map<String, List<FabricModJson>> fmjCache) {
var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(project, fmjCache).toList());
private static List<ModHolder> getCompileRuntimeMods(Project project, Map<String, List<FabricModJson>> fmjCache) {
var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(project, fmjCache));
for (Project dependentProject : getCompileRuntimeProjectDependencies(project).toList()) {
mods.addAll(fmjCache.computeIfAbsent(dependentProject.getPath(), $ -> {
List<FabricModJson> projectMods = fmjCache.computeIfAbsent(dependentProject.getPath(), $ -> {
return FabricModJsonHelpers.getModsInProject(dependentProject);
}));
});
for (FabricModJson mod : projectMods) {
mods.add(new ModHolder(mod));
}
}
return Collections.unmodifiableList(mods);
}
// Returns a list of jar mods that are found on the compile and runtime remapping configurations
private static Stream<FabricModJson> getCompileRuntimeModsFromRemapConfigs(Project project, Map<String, List<FabricModJson>> fmjCache) {
private static List<ModHolder> getCompileRuntimeModsFromRemapConfigs(Project project, Map<String, List<FabricModJson>> fmjCache) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final Set<String> runtimeModIds = extension.getRuntimeRemapConfigurations().stream()
.filter(settings -> settings.getApplyDependencyTransforms().get())
.flatMap(resolveArtifacts(project, true))
.map(modFromZip(fmjCache))
.filter(Objects::nonNull)
.map(FabricModJson::getId)
.collect(Collectors.toSet());
return extension.getCompileRemapConfigurations().stream()
// A set of mod ids from all remap configurations that are considered for dependency transforms.
final Set<String> runtimeModIds = getModIds(
project,
fmjCache,
extension.getRuntimeRemapConfigurations().stream()
.filter(settings -> settings.getApplyDependencyTransforms().get())
.flatMap(resolveArtifacts(project, false))// Use the intersection of the two configurations.
.map(modFromZip(fmjCache))
.filter(Objects::nonNull)
);
// A set of mod ids that are found on one or more remap configurations that target the common source set.
// Null when split source sets are not enabled, meaning all mods are common.
final Set<String> commonModIds = extension.areEnvironmentSourceSetsSplit() ? getModIds(
project,
fmjCache,
extension.getRuntimeRemapConfigurations().stream()
.filter(settings -> settings.getSourceSet().map(sourceSet -> !sourceSet.getName().equals(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME)).get())
.filter(settings -> settings.getApplyDependencyTransforms().get()))
: null;
return getMods(
project,
fmjCache,
extension.getCompileRemapConfigurations().stream()
.filter(settings -> settings.getApplyDependencyTransforms().get()))
// Only check based on the modid, as there may be differing versions used between the compile and runtime classpath.
// We assume that the version used at runtime will be binary compatible with the version used to compile against.
// It's not perfect but better than silently not supplying the mod, and this could happen with regular API that you compile against anyway.
.filter(fabricModJson -> runtimeModIds.contains(fabricModJson.getId()))
.sorted(Comparator.comparing(FabricModJson::getId));
.sorted(Comparator.comparing(FabricModJson::getId))
.map(fabricModJson -> new ModHolder(fabricModJson, commonModIds == null || commonModIds.contains(fabricModJson.getId())))
.toList();
}
private static Stream<FabricModJson> getMods(Project project, Map<String, List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
return stream.flatMap(resolveArtifacts(project, true))
.map(modFromZip(fmjCache))
.filter(Objects::nonNull);
}
private static Set<String> getModIds(Project project, Map<String, List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
return getMods(project, fmjCache, stream)
.map(FabricModJson::getId)
.collect(Collectors.toSet());
}
private static Function<Path, @Nullable FabricModJson> modFromZip(Map<String, List<FabricModJson>> fmjCache) {
@@ -190,6 +227,22 @@ public record SpecContextImpl(List<FabricModJson> modDependencies, List<FabricMo
@Override
public List<FabricModJson> modDependenciesCompileRuntime() {
return compileRuntimeMods;
return compileRuntimeMods.stream()
.map(ModHolder::mod)
.toList();
}
@Override
public List<FabricModJson> modDependenciesCompileRuntimeClient() {
return compileRuntimeMods.stream()
.filter(modHolder -> !modHolder.common())
.map(ModHolder::mod)
.toList();
}
private record ModHolder(FabricModJson mod, boolean common) {
ModHolder(FabricModJson mod) {
this(mod, true);
}
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2018-2021 FabricMC
* Copyright (c) 2018-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
@@ -41,6 +41,7 @@ import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.loom.util.gradle.GradleUtils;
import net.fabricmc.loom.util.service.ScopedServiceFactory;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
@@ -85,6 +86,11 @@ public class GradleMappingContext implements MappingContext {
};
}
@Override
public boolean isUsingIntermediateMappings() {
return !(extension.getIntermediateMappingsProvider() instanceof NoOpIntermediateMappingsProvider);
}
@Override
public MinecraftProvider minecraftProvider() {
return extension.getMinecraftProvider();
@@ -110,6 +116,11 @@ public class GradleMappingContext implements MappingContext {
return extension.refreshDeps();
}
@Override
public boolean hasProperty(String property) {
return GradleUtils.getBooleanProperty(project, property);
}
public Project getProject() {
return project;
}

View File

@@ -25,7 +25,6 @@
package net.fabricmc.loom.configuration.providers.mappings;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -115,7 +114,7 @@ public abstract class IntermediaryMappingsProvider extends IntermediateMappingsP
if (!LoomGradleExtensionApiImpl.DEFAULT_INTERMEDIARY_URL.equals(urlRaw)) {
final String url = getIntermediaryUrl().get().formatted(encodedMcVersion);
return NAME + "-" + Checksum.sha1Hex(url.getBytes(StandardCharsets.UTF_8));
return NAME + "-" + Checksum.of(url).sha1().hex();
}
return NAME;

View File

@@ -47,6 +47,7 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper;
import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
import net.fabricmc.loom.configuration.providers.mappings.utils.AddConstructorMappingVisitor;
import net.fabricmc.loom.util.ZipUtils;
import net.fabricmc.mappingio.adapter.MappingDstNsReorder;
@@ -148,7 +149,7 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) {
return;
}
ZipUtils.add(mappingsFile, "extras/definitions.unpick", unpickData.definitions());
ZipUtils.add(mappingsFile, "extras/unpick.json", unpickData.metadata().asJson());
ZipUtils.add(mappingsFile, UnpickMetadata.UNPICK_DEFINITIONS_PATH, unpickData.definitions());
ZipUtils.add(mappingsFile, UnpickMetadata.UNPICK_METADATA_PATH, unpickData.rawMetadata());
}
}

View File

@@ -45,14 +45,12 @@ import java.util.Objects;
import com.google.common.base.Stopwatch;
import com.google.common.base.Supplier;
import com.google.gson.JsonObject;
import dev.architectury.loom.util.MappingOption;
import org.apache.tools.ant.util.StringUtils;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.provider.Provider;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +62,7 @@ import net.fabricmc.loom.configuration.providers.forge.ForgeMigratedMappingConfi
import net.fabricmc.loom.configuration.providers.forge.SrgProvider;
import net.fabricmc.loom.configuration.providers.mappings.tiny.MappingsMerger;
import net.fabricmc.loom.configuration.providers.mappings.tiny.TinyJarInfo;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.DeletingFileVisitor;
@@ -102,7 +101,7 @@ public class MappingConfiguration {
private final Map<MappingOption, Supplier<Path>> mappingOptions;
private final Path unpickDefinitions;
private boolean hasUnpickDefinitions;
@Nullable
private UnpickMetadata unpickMetadata;
private Map<String, String> signatureFixes;
@@ -250,15 +249,19 @@ public class MappingConfiguration {
}
public void applyToProject(Project project, DependencyInfo dependency) throws IOException {
if (hasUnpickDefinitions()) {
String notation = String.format("%s:%s:%s:constants",
if (unpickMetadata != null) {
if (unpickMetadata.hasConstants()) {
String notation = switch (unpickMetadata) {
case UnpickMetadata.V1 v1 -> String.format("%s:%s:%s:constants",
dependency.getDependency().getGroup(),
dependency.getDependency().getName(),
dependency.getDependency().getVersion()
);
case UnpickMetadata.V2 v2 -> Objects.requireNonNull(v2.constants());
};
project.getDependencies().add(Constants.Configurations.MAPPING_CONSTANTS, notation);
populateUnpickClasspath(project);
}
}
LoomGradleExtension extension = LoomGradleExtension.get(project);
@@ -441,8 +444,8 @@ public class MappingConfiguration {
}
private void extractUnpickDefinitions(FileSystem jar) throws IOException {
Path unpickPath = jar.getPath("extras/definitions.unpick");
Path unpickMetadataPath = jar.getPath("extras/unpick.json");
Path unpickPath = jar.getPath(UnpickMetadata.UNPICK_DEFINITIONS_PATH);
Path unpickMetadataPath = jar.getPath(UnpickMetadata.UNPICK_METADATA_PATH);
if (!Files.exists(unpickPath) || !Files.exists(unpickMetadataPath)) {
return;
@@ -450,8 +453,7 @@ public class MappingConfiguration {
Files.copy(unpickPath, unpickDefinitions, StandardCopyOption.REPLACE_EXISTING);
unpickMetadata = parseUnpickMetadata(unpickMetadataPath);
hasUnpickDefinitions = true;
unpickMetadata = UnpickMetadata.parse(unpickMetadataPath);
}
private void extractSignatureFixes(FileSystem jar) throws IOException {
@@ -467,40 +469,6 @@ public class MappingConfiguration {
}
}
private UnpickMetadata parseUnpickMetadata(Path input) throws IOException {
JsonObject jsonObject = LoomGradlePlugin.GSON.fromJson(Files.readString(input, StandardCharsets.UTF_8), JsonObject.class);
if (!jsonObject.has("version") || jsonObject.get("version").getAsInt() != 1) {
throw new UnsupportedOperationException("Unsupported unpick version");
}
return new UnpickMetadata(
jsonObject.get("unpickGroup").getAsString(),
jsonObject.get("unpickVersion").getAsString()
);
}
private void populateUnpickClasspath(Project project) {
String unpickCliName = "unpick-cli";
project.getDependencies().add(Constants.Configurations.UNPICK_CLASSPATH,
String.format("%s:%s:%s", unpickMetadata.unpickGroup, unpickCliName, unpickMetadata.unpickVersion)
);
// Unpick ships with a slightly older version of asm, ensure it runs with at least the same version as loom.
String[] asmDeps = new String[] {
"org.ow2.asm:asm:%s",
"org.ow2.asm:asm-tree:%s",
"org.ow2.asm:asm-commons:%s",
"org.ow2.asm:asm-util:%s"
};
for (String asm : asmDeps) {
project.getDependencies().add(Constants.Configurations.UNPICK_CLASSPATH,
asm.formatted(Opcodes.class.getPackage().getImplementationVersion())
);
}
}
private void suggestFieldNames(Path inputJar, Path oldMappings, Path newMappings) {
Command command = new CommandProposeFieldNames();
runCommand(command, inputJar.toFile().getAbsolutePath(),
@@ -554,7 +522,11 @@ public class MappingConfiguration {
}
public boolean hasUnpickDefinitions() {
return hasUnpickDefinitions;
return unpickMetadata != null;
}
public UnpickMetadata getUnpickMetadata() {
return Objects.requireNonNull(unpickMetadata, "Unpick metadata is not available");
}
@Nullable
@@ -562,10 +534,6 @@ public class MappingConfiguration {
return signatureFixes;
}
public String getBuildServiceName(String name, String from, String to) {
return "%s:%s:%s>%S".formatted(name, mappingsIdentifier(), from, to);
}
public Path getReplacedTarget(LoomGradleExtension loom, String namespace) {
if (namespace.equals("intermediary")) return getPlatformMappingFile(loom);
@@ -602,7 +570,4 @@ public class MappingConfiguration {
return tinyMappings;
}
}
public record UnpickMetadata(String unpickGroup, String unpickVersion) {
}
}

View File

@@ -25,37 +25,24 @@
package net.fabricmc.loom.configuration.providers.mappings.extras.unpick;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
@ApiStatus.Experimental
public interface UnpickLayer {
@Nullable
UnpickData getUnpickData() throws IOException;
record UnpickData(Metadata metadata, byte[] definitions) {
record UnpickData(UnpickMetadata metadata, byte[] rawMetadata, byte[] definitions) {
public static UnpickData read(Path metadataPath, Path definitionPath) throws IOException {
final byte[] definitions = Files.readAllBytes(definitionPath);
final Metadata metadata;
try (Reader reader = Files.newBufferedReader(metadataPath, StandardCharsets.UTF_8)) {
metadata = LoomGradlePlugin.GSON.fromJson(reader, Metadata.class);
}
return new UnpickData(metadata, definitions);
}
public record Metadata(int version, String unpickGroup, String unpickVersion) {
public String asJson() {
return LoomGradlePlugin.GSON.toJson(this);
}
final byte[] metadata = Files.readAllBytes(metadataPath);
return new UnpickData(UnpickMetadata.parse(metadataPath), metadata, definitions);
}
}
}

View File

@@ -36,6 +36,7 @@ import net.fabricmc.loom.api.mappings.layered.MappingLayer;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer;
import net.fabricmc.loom.configuration.providers.mappings.intermediary.IntermediaryMappingLayer;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.ZipUtils;
import net.fabricmc.mappingio.MappingReader;
@@ -52,9 +53,6 @@ public record FileMappingsLayer(
boolean unpick,
String mergeNamespace
) implements MappingLayer, UnpickLayer {
private static final String UNPICK_METADATA_PATH = "extras/unpick.json";
private static final String UNPICK_DEFINITIONS_PATH = "extras/definitions.unpick";
@Override
public void visit(MappingVisitor mappingVisitor) throws IOException {
// Bare file
@@ -102,8 +100,8 @@ public record FileMappingsLayer(
}
try (FileSystemUtil.Delegate fileSystem = FileSystemUtil.getJarFileSystem(path)) {
final Path unpickMetadata = fileSystem.get().getPath(UNPICK_METADATA_PATH);
final Path unpickDefinitions = fileSystem.get().getPath(UNPICK_DEFINITIONS_PATH);
final Path unpickMetadata = fileSystem.get().getPath(UnpickMetadata.UNPICK_METADATA_PATH);
final Path unpickDefinitions = fileSystem.get().getPath(UnpickMetadata.UNPICK_DEFINITIONS_PATH);
if (!Files.exists(unpickMetadata)) {
// No unpick in this zip

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
* Copyright (c) 2021-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
@@ -30,9 +30,11 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import org.gradle.api.logging.Logger;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.api.mappings.layered.MappingLayer;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
@@ -41,11 +43,14 @@ import net.fabricmc.loom.configuration.providers.mappings.utils.DstNameFilterMap
import net.fabricmc.mappingio.MappingVisitor;
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
import net.fabricmc.mappingio.format.proguard.ProGuardFileReader;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
public record MojangMappingLayer(String minecraftVersion,
Path clientMappings,
Path serverMappings,
boolean nameSyntheticMembers,
boolean dropNoneIntermediaryRoots,
@Nullable Supplier<MemoryMappingTree> intermediarySupplier,
Logger logger,
MojangMappingsSpec.SilenceLicenseOption silenceLicense) implements MappingLayer {
private static final Pattern SYNTHETIC_NAME_PATTERN = Pattern.compile("^(access|this|val\\$this|lambda\\$.*)\\$[0-9]+$");
@@ -55,12 +60,41 @@ public record MojangMappingLayer(String minecraftVersion,
printMappingsLicense(clientMappings);
}
if (!dropNoneIntermediaryRoots) {
logger().debug("Not attempting to drop none intermediary roots");
readMappings(mappingVisitor);
return;
}
logger().info("Attempting to drop none intermediary roots");
if (intermediarySupplier == null) {
// Using no-op intermediary mappings
readMappings(mappingVisitor);
return;
}
// Create a mapping tree with src: official dst: named, intermediary
MemoryMappingTree mappingTree = new MemoryMappingTree();
intermediarySupplier.get().accept(mappingTree);
readMappings(mappingTree);
// The following code first switches the src namespace to intermediary dropping any entries that don't have an intermediary name
// This removes any none root methods before switching it back to official
var officialSwitch = new MappingSourceNsSwitch(mappingVisitor, getSourceNamespace().toString(), false);
var intermediarySwitch = new MappingSourceNsSwitch(officialSwitch, MappingsNamespace.INTERMEDIARY.toString(), true);
mappingTree.accept(intermediarySwitch);
}
private void readMappings(MappingVisitor mappingVisitor) throws IOException {
// Filter out field names matching the pattern
DstNameFilterMappingVisitor nameFilter = new DstNameFilterMappingVisitor(mappingVisitor, SYNTHETIC_NAME_PATTERN);
var nameFilter = new DstNameFilterMappingVisitor(mappingVisitor, SYNTHETIC_NAME_PATTERN);
// Make official the source namespace
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(nameSyntheticMembers() ? mappingVisitor : nameFilter, MappingsNamespace.OFFICIAL.toString());
var nsSwitch = new MappingSourceNsSwitch(nameSyntheticMembers() ? mappingVisitor : nameFilter, MappingsNamespace.OFFICIAL.toString());
// Read both server and client mappings
try (BufferedReader clientBufferedReader = Files.newBufferedReader(clientMappings, StandardCharsets.UTF_8);
BufferedReader serverBufferedReader = Files.newBufferedReader(serverMappings, StandardCharsets.UTF_8)) {
ProGuardFileReader.read(clientBufferedReader, MappingsNamespace.NAMED.toString(), MappingsNamespace.OFFICIAL.toString(), nsSwitch);

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2021 FabricMC
* Copyright (c) 2016-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
@@ -30,6 +30,7 @@ import java.nio.file.Path;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.DownloadException;
public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean nameSyntheticMembers) implements MappingsSpec<MojangMappingLayer> {
@@ -103,6 +104,8 @@ public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean na
clientMappings,
serverMappings,
nameSyntheticMembers(),
context.hasProperty(Constants.Properties.DROP_NON_INTERMEDIATE_ROOT_METHODS),
context.isUsingIntermediateMappings() ? context.intermediaryTree() : null,
context.getLogger(),
silenceLicense()
);

View File

@@ -0,0 +1,105 @@
/*
* 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.configuration.providers.mappings.unpick;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradlePlugin;
public sealed interface UnpickMetadata permits UnpickMetadata.V1, UnpickMetadata.V2 {
String UNPICK_METADATA_PATH = "extras/unpick.json";
String UNPICK_DEFINITIONS_PATH = "extras/definitions.unpick";
boolean hasConstants();
/**
* @param unpickGroup Deprecated, always uses the version of unpick loom depends on.
* @param unpickVersion Deprecated, always uses the version of unpick loom depends on.
*/
record V1(@Deprecated String unpickGroup, @Deprecated String unpickVersion) implements UnpickMetadata {
@Override
public boolean hasConstants() {
return true;
}
}
/**
* Unpick metadata v2.
*
* @param namespace the mapping namespace of the unpick definitions
* @param constants An optional maven notation of the constants jar.
*/
record V2(String namespace, @Nullable String constants) implements UnpickMetadata {
@Override
public boolean hasConstants() {
return constants != null;
}
}
static UnpickMetadata parse(Path path) throws IOException {
JsonObject jsonObject = LoomGradlePlugin.GSON.fromJson(Files.readString(path, StandardCharsets.UTF_8), JsonObject.class);
if (!jsonObject.has("version")) {
throw new UnsupportedOperationException("Missing unpick metadata version");
}
int version = jsonObject.get("version").getAsInt();
switch (version) {
case 1 -> {
return new V1(
getString(jsonObject, "unpickGroup"),
getString(jsonObject, "unpickVersion")
);
}
case 2 -> {
return new V2(
getString(jsonObject, "namespace"),
getOptionalString(jsonObject, "constants")
);
}
default -> throw new UnsupportedOperationException("Unsupported unpick metadata version: %s. Please update loom.".formatted(version));
}
}
private static String getString(JsonObject jsonObject, String key) {
if (!jsonObject.has(key)) {
throw new UnsupportedOperationException("Missing unpick metadata %s".formatted(key));
}
return jsonObject.get(key).getAsString();
}
@Nullable
private static String getOptionalString(JsonObject jsonObject, String key) {
return jsonObject.has(key) ? jsonObject.get(key).getAsString() : null;
}
}

View File

@@ -29,8 +29,8 @@ import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import net.fabricmc.loom.api.mappings.layered.spec.FileSpec;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.api.mappings.layered.spec.FileSpec;
import net.fabricmc.loom.util.Checksum;
public class LocalFileSpec implements FileSpec {
@@ -48,7 +48,7 @@ public class LocalFileSpec implements FileSpec {
}
// Use the file hash as part of the spec, this means if the input file changes the mappings will be re-generated.
return Objects.hash(Arrays.hashCode(Checksum.sha256(file)), file.getAbsolutePath());
return Objects.hash(Arrays.hashCode(Checksum.of(file).sha256().digest()), file.getAbsolutePath());
}
@Override

View File

@@ -92,7 +92,6 @@ public class MergedMinecraftProvider extends MinecraftProvider {
File minecraftServerJar = getMinecraftServerJar();
if (getServerBundleMetadata() != null) {
extractBundledServerJar();
minecraftServerJar = getMinecraftExtractedServerJar();
}

View File

@@ -37,8 +37,6 @@ import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import com.google.common.collect.Sets;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.FileSystemUtil;
@@ -79,7 +77,7 @@ public class MinecraftJarSplitter implements AutoCloseable {
}
public static Set<String> getJarEntries(Path input) throws IOException {
Set<String> entries = Sets.newHashSet();
Set<String> entries = new HashSet<>();
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(input);
Stream<Path> walk = Files.walk(fs.get().getPath("/"))) {
@@ -154,17 +152,17 @@ public class MinecraftJarSplitter implements AutoCloseable {
this.clientEntries = clientEntries;
this.serverEntries = serverEntries;
this.commonEntries = Sets.newHashSet(clientEntries);
this.commonEntries = new HashSet<>(clientEntries);
this.commonEntries.retainAll(serverEntries);
this.commonEntries.addAll(sharedEntries);
this.commonEntries.removeAll(forcedClientEntries);
this.clientOnlyEntries = Sets.newHashSet(clientEntries);
this.clientOnlyEntries = new HashSet<>(clientEntries);
this.clientOnlyEntries.removeAll(serverEntries);
this.clientOnlyEntries.addAll(sharedEntries);
this.clientOnlyEntries.addAll(forcedClientEntries);
this.serverOnlyEntries = Sets.newHashSet(serverEntries);
this.serverOnlyEntries = new HashSet<>(serverEntries);
this.serverOnlyEntries.removeAll(clientEntries);
}
}

View File

@@ -31,6 +31,7 @@ import java.util.List;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.ModuleDependency;
@@ -94,6 +95,10 @@ public class MinecraftLibraryProvider {
if (provideServer) {
provideServerLibraries();
}
if (extension.isCollectingDependencyVerificationMetadata()) {
resolveAllLibraries();
}
}
private void provideClientLibraries() {
@@ -114,6 +119,22 @@ public class MinecraftLibraryProvider {
processLibraries.forEach(this::applyServerLibrary);
}
/**
* When Gradle is writing dependency verification metadata, we need to resolve all libraries across all platforms,
* to ensure that they are captured.
*/
private void resolveAllLibraries() {
project.getLogger().info("Resolving all libraries for dependency verification metadata generation");
final List<Library> libraries = MinecraftLibraryHelper.getAllLibraries(minecraftProvider.getVersionInfo());
Configuration detachedConfiguration = project.getConfigurations().detachedConfiguration(
libraries.stream()
.map(library -> project.getDependencies().create(library.mavenNotation()))
.toArray(Dependency[]::new)
);
detachedConfiguration.getFiles();
}
private List<Library> processLibraries(List<Library> libraries) {
final LibraryContext libraryContext = new LibraryContext(minecraftProvider.getVersionInfo(), getTargetRuntimeJavaVersion());
return processorManager.processLibraries(libraries, libraryContext);

View File

@@ -29,6 +29,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.base.Preconditions;
import org.gradle.api.JavaVersion;
@@ -41,9 +42,12 @@ import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.providers.BundleMetadata;
import net.fabricmc.loom.configuration.providers.minecraft.verify.MinecraftJarVerification;
import net.fabricmc.loom.configuration.providers.minecraft.verify.SignatureVerificationFailure;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.DownloadExecutor;
import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
import net.fabricmc.loom.util.gradle.GradleUtils;
import net.fabricmc.loom.util.gradle.ProgressGroup;
public abstract class MinecraftProvider {
@@ -93,10 +97,18 @@ public abstract class MinecraftProvider {
}
}
downloadJars();
boolean didDownload = downloadJars();
if (provideServer()) {
serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath());
if (serverBundleMetadata != null) {
extractBundledServerJar();
}
}
if (didDownload) {
verifyJars();
}
final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, configContext.project());
@@ -114,7 +126,35 @@ public abstract class MinecraftProvider {
}
}
private void downloadJars() throws IOException {
private void verifyJars() throws IOException, SignatureVerificationFailure {
if (GradleUtils.getBooleanProperty(getProject(), Constants.Properties.DISABLE_MINECRAFT_VERIFICATION)) {
LOGGER.info("Skipping Minecraft jar verification!");
return;
}
LOGGER.info("Verifying Minecraft jars");
MinecraftJarVerification verification = getProject().getObjects().newInstance(MinecraftJarVerification.class, minecraftVersion());
if (provideClient()) {
verification.verifyClientJar(minecraftClientJar.toPath());
}
if (provideServer()) {
if (serverBundleMetadata == null) {
verification.verifyServerJar(minecraftServerJar.toPath());
} else {
verification.verifyServerJar(getMinecraftExtractedServerJar().toPath());
}
}
LOGGER.info("Jar verification complete");
}
// Returns true when a file was downloaded
private boolean downloadJars() throws IOException {
AtomicBoolean didDownload = new AtomicBoolean(false);
try (ProgressGroup progressGroup = new ProgressGroup(getProject(), "Download Minecraft jars");
DownloadExecutor executor = new DownloadExecutor(2)) {
if (provideClient()) {
@@ -122,7 +162,12 @@ public abstract class MinecraftProvider {
getExtension().download(client.url())
.sha1(client.sha1())
.progress(new GradleDownloadProgressListener("Minecraft client", progressGroup::createProgressLogger))
.downloadPathAsync(minecraftClientJar.toPath(), executor);
.downloadPathAsync(minecraftClientJar.toPath(), executor)
.thenAccept(downloadResult -> {
if (downloadResult.didDownload()) {
didDownload.set(true);
}
});
}
if (provideServer()) {
@@ -130,9 +175,22 @@ public abstract class MinecraftProvider {
getExtension().download(server.url())
.sha1(server.sha1())
.progress(new GradleDownloadProgressListener("Minecraft server", progressGroup::createProgressLogger))
.downloadPathAsync(minecraftServerJar.toPath(), executor);
.downloadPathAsync(minecraftServerJar.toPath(), executor)
.thenAccept(downloadResult -> {
if (downloadResult.didDownload()) {
didDownload.set(true);
}
});
}
}
if (didDownload.get()) {
LOGGER.info("Downloaded new Minecraft jars");
return true;
}
LOGGER.info("Using cached Minecraft jars");
return false;
}
public final void extractBundledServerJar() throws IOException {

View File

@@ -143,14 +143,13 @@ public abstract class SingleJarMinecraftProvider extends MinecraftProvider {
}
@Override
public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception {
public Path getInputJar(SingleJarMinecraftProvider provider) {
BundleMetadata serverBundleMetadata = provider.getServerBundleMetadata();
if (serverBundleMetadata == null) {
return provider.getMinecraftServerJar().toPath();
}
provider.extractBundledServerJar();
return provider.getMinecraftExtractedServerJar().toPath();
}

View File

@@ -74,8 +74,6 @@ public final class SplitMinecraftProvider extends MinecraftProvider {
throw new UnsupportedOperationException("Only Minecraft versions using a bundled server jar can be split, please use a merged jar setup for this version of minecraft");
}
extractBundledServerJar();
final Path clientJar = getMinecraftClientJar().toPath();
final Path serverJar = getMinecraftExtractedServerJar().toPath();

View File

@@ -27,6 +27,7 @@ package net.fabricmc.loom.configuration.providers.minecraft.library;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -72,6 +73,35 @@ public class MinecraftLibraryHelper {
return Collections.unmodifiableList(libraries);
}
public static List<Library> getAllLibraries(MinecraftVersionMeta versionMeta) {
var libraries = new ArrayList<Library>();
for (MinecraftVersionMeta.Library library : versionMeta.libraries()) {
if (library.artifact() != null) {
Library mavenLib = Library.fromMaven(library.name(), Library.Target.COMPILE);
// Versions that have the natives on the classpath, attempt to target them as natives.
if (mavenLib.classifier() != null && mavenLib.classifier().startsWith("natives-")) {
mavenLib = mavenLib.withTarget(Library.Target.NATIVES);
}
libraries.add(mavenLib);
}
Map<String, MinecraftVersionMeta.Download> classifiers = library.downloads().classifiers();
if (classifiers == null) {
continue;
}
for (MinecraftVersionMeta.Download download : classifiers.values()) {
libraries.add(downloadToLibrary(download));
}
}
return Collections.unmodifiableList(libraries);
}
private static Library downloadToLibrary(MinecraftVersionMeta.Download download) {
final String path = download.path();
final Matcher matcher = NATIVES_PATTERN.matcher(path);

View File

@@ -0,0 +1,194 @@
/*
* 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.configuration.providers.minecraft.verify;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
/**
* A node in the certificate chain.
*/
public interface CertificateChain {
/**
* The certificate itself.
*/
X509Certificate certificate();
/**
* The issuer of this certificate, or null if this is a root certificate.
*/
@Nullable CertificateChain issuer();
/**
* The children of this certificate, or an empty list if this is a leaf certificate.
*/
List<CertificateChain> children();
/**
* Verify that this certificate chain matches exactly with another one.
* @param other the other certificate chain
*/
void verifyChainMatches(CertificateChain other) throws SignatureVerificationFailure;
/**
* Recursively visit all certificates in the chain, including this one.
*/
static void visitAll(CertificateChain chain, CertificateConsumer consumer) throws SignatureVerificationFailure {
consumer.accept(chain.certificate());
for (CertificateChain child : chain.children()) {
visitAll(child, consumer);
}
}
/**
* Load certificate chain from the classpath, returning the root certificate.
*/
static CertificateChain getRoot(String name) throws IOException {
try (InputStream is = JarVerifier.class.getClassLoader().getResourceAsStream("certs/" + name + ".cer")) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<X509Certificate> certificates = cf.generateCertificates(is).stream()
.map(c -> (X509Certificate) c)
.toList();
return getRoot(certificates);
} catch (CertificateException e) {
throw new RuntimeException("Failed to load certificate: " + name, e);
}
}
/**
* Takes an unordered collection of certificates and builds a tree structure.
*/
static CertificateChain getRoot(Collection<X509Certificate> certificates) {
Map<String, Impl> certificateNodes = new HashMap<>();
for (X509Certificate certificate : certificates) {
Impl node = new Impl();
node.certificate = certificate;
certificateNodes.put(certificate.getSubjectX500Principal().getName(), node);
}
for (X509Certificate certificate : certificates) {
String subject = certificate.getSubjectX500Principal().getName();
String issuer = certificate.getIssuerX500Principal().getName();
if (subject.equals(issuer)) {
continue; // self-signed
}
Impl parent = certificateNodes.get(issuer);
Impl self = certificateNodes.get(subject);
if (parent == self) {
throw new IllegalStateException("Certificate " + subject + " is its own issuer");
}
if (parent == null) {
throw new IllegalStateException("Certificate " + subject + " defines issuer " + issuer + " which is not in the chain");
}
parent.children.add(self);
self.issuer = parent;
}
List<Impl> roots = certificateNodes.values()
.stream()
.filter(node -> node.issuer == null)
.toList();
if (roots.size() != 1) {
throw new IllegalStateException("Expected exactly one root certificate, but found " + roots.size());
}
return roots.get(0);
}
@FunctionalInterface
interface CertificateConsumer {
void accept(X509Certificate certificate) throws SignatureVerificationFailure;
}
class Impl implements CertificateChain {
X509Certificate certificate;
@Nullable CertificateChain.Impl issuer;
List<CertificateChain> children = new ArrayList<>();
private Impl() {
}
@Override
public X509Certificate certificate() {
return certificate;
}
@Override
public @Nullable CertificateChain issuer() {
return issuer;
}
@Override
public List<CertificateChain> children() {
return children;
}
@Override
public void verifyChainMatches(CertificateChain other) throws SignatureVerificationFailure {
if (!this.certificate().equals(other.certificate())) {
throw new SignatureVerificationFailure("Certificate mismatch: " + this + " != " + other);
}
if (this.children().size() != other.children().size()) {
throw new SignatureVerificationFailure("Certificate mismatch: " + this + " has " + this.children().size() + " children, but " + other + " has " + other.children().size());
}
if (this.children.isEmpty()) {
// Fine, leaf certificate
return;
}
if (this.children.size() != 1) {
// TODO support this, not needed currently
throw new UnsupportedOperationException("Validating Certificate chain with multiple children is not supported");
}
this.children.get(0).verifyChainMatches(other.children().get(0));
}
@Override
public String toString() {
return certificate.getSubjectX500Principal().getName();
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.configuration.providers.minecraft.verify;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.gradle.api.Project;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.util.download.DownloadException;
public record CertificateRevocationList(Collection<X509CRL> crls, boolean downloadFailure) {
/**
* Hardcoded CRLs for Mojang's certificate, we don't want to add a large dependency just to parse this each time.
*/
public static final List<String> CSC3_2010 = List.of(
"http://crl.verisign.com/pca3-g5.crl",
"http://crl.verisign.com/pca3.crl",
"http://csc3-2010-crl.verisign.com/CSC3-2010.crl"
);
private static final Logger LOGGER = LoggerFactory.getLogger(CertificateRevocationList.class);
/**
* Attempt to download the CRL from the given URL, if we fail to get it its not the end of the world.
*/
public static CertificateRevocationList create(Project project, List<String> urls) throws IOException {
List<X509CRL> crls = new ArrayList<>();
boolean downloadFailure = false;
for (String url : urls) {
try {
crls.add(download(project, url));
} catch (DownloadException e) {
LOGGER.info("Failed to download CRL from {}: {}", url, e.getMessage());
LOGGER.info("Loom will not be able to verify the integrity of the minecraft jar signature");
downloadFailure = true;
}
}
return new CertificateRevocationList(crls, downloadFailure);
}
static X509CRL download(Project project, String url) throws IOException {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final String name = url.substring(url.lastIndexOf('/') + 1);
final Path path = extension.getFiles().getUserCache().toPath()
.resolve("crl")
.resolve(name);
LOGGER.info("Downloading CRL from {} to {}", url, path);
extension.download(url)
.allowInsecureProtocol()
.maxAge(Duration.ofDays(7)) // Cache the CRL for a week
.downloadPath(path);
return parse(path);
}
static X509CRL parse(Path path) throws IOException {
try (InputStream inStream = Files.newInputStream(path)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509CRL) cf.generateCRL(inStream);
} catch (CRLException | CertificateException e) {
throw new RuntimeException(e);
}
}
/**
* Verify that none of the certs in the chain are revoked.
* @throws SignatureVerificationFailure if the certificate is revoked
*/
public void verify(CertificateChain certificateChain) throws SignatureVerificationFailure {
CertificateChain.visitAll(certificateChain, this::verify);
}
private void verify(X509Certificate certificate) throws SignatureVerificationFailure {
for (X509CRL crl : crls) {
if (crl.isRevoked(certificate)) {
throw new SignatureVerificationFailure("Certificate " + certificate.getSubjectX500Principal().getName() + " is revoked");
}
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.configuration.providers.minecraft.verify;
import java.io.IOException;
import java.nio.file.Path;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.ZipReprocessorUtil;
public final class JarVerifier {
private static final Logger LOGGER = LoggerFactory.getLogger(JarVerifier.class);
private JarVerifier() {
}
public static void verify(Path jarPath, CertificateChain certificateChain) throws IOException, SignatureVerificationFailure {
Objects.requireNonNull(jarPath, "jarPath");
Objects.requireNonNull(certificateChain, "certificateChain");
if (certificateChain.issuer() != null) {
throw new IllegalStateException("Can only verify jars from a root certificate");
}
Set<X509Certificate> jarCertificates = new HashSet<>();
try (JarFile jarFile = new JarFile(jarPath.toFile(), true)) {
for (JarEntry jarEntry : Collections.list(jarFile.entries())) {
if (ZipReprocessorUtil.isSpecialFile(jarEntry.getName())
|| jarEntry.getName().equals("META-INF/MANIFEST.MF")
|| jarEntry.isDirectory()) {
continue;
}
try {
// Must read the entire entry to trigger the signature verification
byte[] bytes = jarFile.getInputStream(jarEntry).readAllBytes();
} catch (SecurityException e) {
throw new SignatureVerificationFailure("Jar entry " + jarEntry.getName() + " failed signature verification", e);
}
Certificate[] entryCertificates = jarEntry.getCertificates();
if (entryCertificates == null) {
throw new SignatureVerificationFailure("Jar entry " + jarEntry.getName() + " does not have a signature");
}
Arrays.stream(entryCertificates)
.map(c -> (X509Certificate) c)
.forEach(jarCertificates::add);
}
}
CertificateChain jarCertificateChain = CertificateChain.getRoot(jarCertificates);
jarCertificateChain.verifyChainMatches(certificateChain);
LOGGER.debug("Jar {} is signed by the expected certificate", jarPath);
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.configuration.providers.minecraft.verify;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
import net.fabricmc.loom.LoomGradlePlugin;
/**
* The know versions keep track of the versions that are signed using SHA1 or not signature at all.
* The maps are the Minecraft version to sha256 hash of the jar file.
*/
public record KnownVersions(
Map<String, String> client,
Map<String, String> server) {
public static final Supplier<KnownVersions> INSTANCE = Suppliers.memoize(KnownVersions::load);
private static KnownVersions load() {
try (InputStream is = KnownVersions.class.getClassLoader().getResourceAsStream("certs/known_versions.json");
Reader reader = new InputStreamReader(Objects.requireNonNull(is))) {
return LoomGradlePlugin.GSON.fromJson(reader, KnownVersions.class);
} catch (IOException e) {
throw new UncheckedIOException("Failed to load known versions", e);
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.configuration.providers.minecraft.verify;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import javax.inject.Inject;
import org.gradle.api.Project;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.Checksum;
public abstract class MinecraftJarVerification {
private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftJarVerification.class);
private final String minecraftVersion;
@Inject
protected abstract Project getProject();
@Inject
public MinecraftJarVerification(String minecraftVersion) {
this.minecraftVersion = minecraftVersion;
}
public void verifyClientJar(Path path) throws IOException, SignatureVerificationFailure {
verifyJarSignature(path, KnownJarType.CLIENT);
}
public void verifyServerJar(Path path) throws IOException, SignatureVerificationFailure {
verifyJarSignature(path, KnownJarType.SERVER);
}
private void verifyJarSignature(Path path, KnownJarType type) throws IOException, SignatureVerificationFailure {
CertificateChain chain = CertificateChain.getRoot("mojangcs");
CertificateRevocationList revocationList = CertificateRevocationList.create(getProject(), CertificateRevocationList.CSC3_2010);
try {
revocationList.verify(chain);
JarVerifier.verify(path, chain);
} catch (SignatureVerificationFailure e) {
if (isValidKnownVersion(path, minecraftVersion, type)) {
LOGGER.info("Minecraft {} signature verification failed, but is a known version", path.getFileName());
return;
}
LOGGER.error("Verification of Minecraft {} signature failed: {}", path.getFileName(), e.getMessage());
throw e;
}
}
private boolean isValidKnownVersion(Path path, String version, KnownJarType type) throws IOException, SignatureVerificationFailure {
Map<String, String> knownVersions = type.getKnownVersions();
String expectedHash = knownVersions.get(version);
if (expectedHash == null) {
return false;
}
LOGGER.info("Found executed hash ({}) for known version: {}", expectedHash, version);
Checksum.Result hash = Checksum.of(path).sha256();
if (hash.matchesStr(expectedHash)) {
LOGGER.info("Minecraft {} hash matches known version", path.getFileName());
return true;
}
throw new SignatureVerificationFailure("Hash mismatch for known Minecraft version " + version + ": expected " + expectedHash + ", got " + hash);
}
private enum KnownJarType {
CLIENT(KnownVersions::client),
SERVER(KnownVersions::server),;
private final Function<KnownVersions, Map<String, String>> knownVersions;
KnownJarType(Function<KnownVersions, Map<String, String>> knownVersions) {
this.knownVersions = knownVersions;
}
private Map<String, String> getKnownVersions() {
return knownVersions.apply(KnownVersions.INSTANCE.get());
}
}
}

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.configuration.providers.minecraft.verify;
public final class SignatureVerificationFailure extends Exception {
public SignatureVerificationFailure(String message) {
super(message);
}
public SignatureVerificationFailure(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -105,13 +105,13 @@ public record ClassEntry(String name, List<String> innerClasses, List<String> su
public String hash(Path root) throws IOException {
StringJoiner joiner = new StringJoiner(",");
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(name))));
joiner.add(Checksum.of(root.resolve(name)).sha256().hex());
for (String innerClass : innerClasses) {
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(innerClass))));
joiner.add(Checksum.of(root.resolve(innerClass)).sha256().hex());
}
return Checksum.sha256Hex(joiner.toString().getBytes());
return Checksum.of(joiner.toString()).sha256().hex();
}
/**
@@ -138,7 +138,7 @@ public record ClassEntry(String name, List<String> innerClasses, List<String> su
}
}
return Checksum.sha256Hex(joiner.toString().getBytes());
return Checksum.of(joiner.toString()).sha256().hex();
}
public String sourcesFileName() {

View File

@@ -46,7 +46,6 @@ public interface LoomFiles {
File getNativesDirectory(Project project);
File getDefaultLog4jConfigFile();
File getDevLauncherConfig();
File getUnpickLoggingConfigFile();
File getRemapClasspathFile();
File getGlobalMinecraftRepo();
File getLocalMinecraftRepo();

View File

@@ -84,11 +84,6 @@ public abstract class LoomFilesBaseImpl implements LoomFiles {
return new File(getProjectPersistentCache(), "launch.cfg");
}
@Override
public File getUnpickLoggingConfigFile() {
return new File(getProjectPersistentCache(), "unpick-logging.properties");
}
@Override
public File getRemapClasspathFile() {
return new File(getProjectPersistentCache(), "remapClasspath.txt");

View File

@@ -88,6 +88,7 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
private final ListProperty<LibraryProcessorManager.LibraryProcessorFactory> libraryProcessorFactories;
private final boolean configurationCacheActive;
private final boolean isolatedProjectsActive;
private final boolean isCollectingDependencyVerificationMetadata;
// +-------------------+
// | Architectury Loom |
@@ -127,6 +128,7 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
configurationCacheActive = getBuildFeatures().getConfigurationCache().getActive().get();
isolatedProjectsActive = getBuildFeatures().getIsolatedProjects().getActive().get();
isCollectingDependencyVerificationMetadata = !project.getGradle().getStartParameter().getWriteDependencyVerifications().isEmpty();
if (refreshDeps) {
project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower.");
@@ -348,6 +350,11 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
return isolatedProjectsActive;
}
@Override
public boolean isCollectingDependencyVerificationMetadata() {
return isCollectingDependencyVerificationMetadata;
}
@Override
public ForgeExtensionAPI getForge() {
ModPlatform.assertPlatform(this, ModPlatform.FORGE);

View File

@@ -33,6 +33,7 @@ import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.SourceSet;
@@ -89,4 +90,6 @@ public interface MixinExtension extends MixinExtensionAPI {
Collection<SourceSet> getMixinSourceSets();
void init();
Property<Boolean> getInlineDependencyRefmaps();
}

View File

@@ -46,11 +46,13 @@ import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.util.PatternSet;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinExtension {
private boolean isDefault;
private final Property<String> defaultRefmapName;
private final Property<Boolean> inlineDependencyRefmaps;
@Inject
public MixinExtensionImpl(Project project) {
@@ -59,6 +61,9 @@ public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinEx
this.defaultRefmapName = project.getObjects().property(String.class)
.convention(project.provider(this::getDefaultMixinRefmapName));
this.defaultRefmapName.finalizeValueOnRead();
this.inlineDependencyRefmaps = project.getObjects().property(Boolean.class)
.convention(false);
this.inlineDependencyRefmaps.finalizeValueOnRead();
}
@Override
@@ -146,4 +151,10 @@ public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinEx
}
});
}
@ApiStatus.Experimental
@Override
public Property<Boolean> getInlineDependencyRefmaps() {
return inlineDependencyRefmaps;
}
}

View File

@@ -95,6 +95,14 @@ public abstract class AbstractRemapJarTask extends Jar {
@Optional
public abstract Property<String> getClientOnlySourceSetName();
/**
* Optionally supply a single mapping file or jar file containing mappings to be used for remapping.
*/
@ApiStatus.Experimental
@InputFiles
@Optional
public abstract ConfigurableFileCollection getCustomMappings();
@Input
@Optional
@ApiStatus.Internal

View File

@@ -28,7 +28,6 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -37,9 +36,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -59,7 +56,6 @@ import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.services.ServiceReference;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Nested;
@@ -70,7 +66,6 @@ import org.gradle.api.tasks.UntrackedTask;
import org.gradle.api.tasks.options.Option;
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkQueue;
@@ -91,6 +86,7 @@ import net.fabricmc.loom.decompilers.cache.CachedData;
import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl;
import net.fabricmc.loom.decompilers.cache.CachedJarProcessor;
import net.fabricmc.loom.task.service.SourceMappingsService;
import net.fabricmc.loom.task.service.UnpickService;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.ExceptionUtil;
@@ -106,6 +102,7 @@ import net.fabricmc.loom.util.gradle.daemon.DaemonUtils;
import net.fabricmc.loom.util.ipc.IPCClient;
import net.fabricmc.loom.util.ipc.IPCServer;
import net.fabricmc.loom.util.service.ScopedServiceFactory;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
@UntrackedTask(because = "Manually invoked, has internal caching")
@@ -135,31 +132,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
@OutputFile
protected abstract ConfigurableFileCollection getClassesOutputJar(); // Single jar
// Unpick
@InputFile
@Optional
public abstract RegularFileProperty getUnpickDefinitions();
@InputFiles
@Optional
public abstract ConfigurableFileCollection getUnpickConstantJar();
@InputFiles
@Optional
public abstract ConfigurableFileCollection getUnpickClasspath();
@InputFiles
@Optional
@ApiStatus.Internal
public abstract ConfigurableFileCollection getUnpickRuntimeClasspath();
@OutputFile
@Optional
public abstract RegularFileProperty getUnpickOutputJar();
@OutputFile
protected abstract RegularFileProperty getUnpickLogConfig();
@Input
@Option(option = "use-cache", description = "Use the decompile cache")
@ApiStatus.Experimental
@@ -204,6 +176,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
@Nested
protected abstract Property<DaemonUtils.Context> getDaemonUtilsContext();
@Nested
@Optional
protected abstract Property<UnpickService.Options> getUnpickOptions();
// Prevent Gradle from running two gen sources tasks in parallel
@ServiceReference(SyncTaskBuildService.NAME)
abstract Property<SyncTaskBuildService> getSyncTask();
@@ -246,8 +222,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
getMinecraftCompileLibraries().from(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
getDecompileCacheFile().set(getExtension().getFiles().getDecompileCache(CACHE_VERSION));
getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH));
getUnpickLogConfig().set(getExtension().getFiles().getUnpickLoggingConfigFile());
getUseCache().convention(true);
getResetCache().convention(getExtension().refreshDeps());
@@ -259,6 +233,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
getDaemonUtilsContext().set(getProject().getObjects().newInstance(DaemonUtils.Context.class, getProject()));
getUnpickOptions().set(UnpickService.createOptions(this));
mustRunAfter(getProject().getTasks().withType(AbstractRemapJarTask.class));
}
@@ -270,11 +246,12 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements.");
}
try (ScopedServiceFactory serviceFactory = new ScopedServiceFactory()) {
if (!getUseCache().get()) {
getLogger().info("Not using decompile cache.");
try (var timer = new Timer("Decompiled sources")) {
runWithoutCache();
runWithoutCache(serviceFactory);
} catch (Exception e) {
ExceptionUtil.processException(e, getDaemonUtilsContext().get());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
@@ -306,21 +283,22 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
}
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(cacheFile, true)) {
runWithCache(fs.getRoot());
runWithCache(serviceFactory, fs.getRoot());
}
} catch (Exception e) {
ExceptionUtil.processException(e, getDaemonUtilsContext().get());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
}
}
}
private void runWithCache(Path cacheRoot) throws IOException {
private void runWithCache(ServiceFactory serviceFactory, Path cacheRoot) throws IOException {
final Path classesInputJar = getClassesInputJar().getSingleFile().toPath();
final Path sourcesOutputJar = getSourcesOutputJar().get().getAsFile().toPath();
final Path classesOutputJar = getClassesOutputJar().getSingleFile().toPath();
final var cacheRules = new CachedFileStoreImpl.CacheRules(getMaxCachedFiles().get(), Duration.ofDays(getMaxCacheFileAge().get()));
final var decompileCache = new CachedFileStoreImpl<>(cacheRoot, CachedData.SERIALIZER, cacheRules);
final String cacheKey = getCacheKey();
final String cacheKey = getCacheKey(serviceFactory);
final CachedJarProcessor cachedJarProcessor = new CachedJarProcessor(decompileCache, cacheKey);
final CachedJarProcessor.WorkRequest workRequest;
@@ -342,9 +320,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
Path workInputJar = workToDoJob.incomplete();
@Nullable Path existingClasses = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existingClasses() : null;
if (getUnpickDefinitions().isPresent()) {
if (usingUnpick()) {
try (var timer = new Timer("Unpick")) {
workInputJar = unpickJar(workInputJar, existingClasses);
UnpickService unpick = serviceFactory.get(getUnpickOptions());
workInputJar = unpick.unpickJar(workInputJar, existingClasses);
}
}
@@ -381,16 +360,17 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
}
}
private void runWithoutCache() throws IOException {
private void runWithoutCache(ServiceFactory serviceFactory) throws IOException {
final Path classesInputJar = getClassesInputJar().getSingleFile().toPath();
final Path sourcesOutputJar = getSourcesOutputJar().get().getAsFile().toPath();
final Path classesOutputJar = getClassesOutputJar().getSingleFile().toPath();
Path workClassesJar = classesInputJar;
if (getUnpickDefinitions().isPresent()) {
if (usingUnpick()) {
try (var timer = new Timer("Unpick")) {
workClassesJar = unpickJar(workClassesJar, null);
UnpickService unpick = serviceFactory.get(getUnpickOptions());
workClassesJar = unpick.unpickJar(workClassesJar, null);
}
}
@@ -427,24 +407,24 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
Files.move(tempJar, classesOutputJar, StandardCopyOption.REPLACE_EXISTING);
}
private String getCacheKey() {
private String getCacheKey(ServiceFactory serviceFactory) {
var sj = new StringJoiner(",");
sj.add(getDecompilerCheckKey());
sj.add(getUnpickCacheKey());
if (usingUnpick()) {
UnpickService unpick = serviceFactory.get(getUnpickOptions());
sj.add(unpick.getUnpickCacheKey());
}
getLogger().info("Decompile cache data: {}", sj);
try {
return Checksum.sha256Hex(sj.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return Checksum.of(sj.toString()).sha256().hex();
}
private String getDecompilerCheckKey() {
var sj = new StringJoiner(",");
sj.add(decompilerOptions.getDecompilerClassName().get());
sj.add(fileCollectionHash(decompilerOptions.getClasspath()));
sj.add(Checksum.of(decompilerOptions.getClasspath()).sha256().hex());
for (Map.Entry<String, String> entry : decompilerOptions.getOptions().get().entrySet()) {
sj.add(entry.getKey() + "=" + entry.getValue());
@@ -453,19 +433,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return sj.toString();
}
private String getUnpickCacheKey() {
if (!getUnpickDefinitions().isPresent()) {
return "";
}
var sj = new StringJoiner(",");
sj.add(fileHash(getUnpickDefinitions().getAsFile().get()));
sj.add(fileCollectionHash(getUnpickConstantJar()));
sj.add(fileCollectionHash(getUnpickRuntimeClasspath()));
return sj.toString();
}
@Nullable
private ClassLineNumbers runDecompileJob(Path inputJar, Path outputJar, @Nullable Path existingJar) throws IOException {
final Platform platform = Platform.CURRENT;
@@ -565,55 +532,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
}
}
private Path unpickJar(Path inputJar, @Nullable Path existingClasses) {
final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath();
final List<String> args = getUnpickArgs(inputJar, outputJar, existingClasses);
ExecResult result = getExecOperations().javaexec(spec -> {
spec.getMainClass().set("daomephsta.unpick.cli.Main");
spec.classpath(getUnpickRuntimeClasspath());
spec.args(args);
spec.systemProperty("java.util.logging.config.file", writeUnpickLogConfig().getAbsolutePath());
});
result.rethrowFailure();
return outputJar;
}
private List<String> getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingClasses) {
var fileArgs = new ArrayList<File>();
fileArgs.add(inputJar.toFile());
fileArgs.add(outputJar.toFile());
fileArgs.add(getUnpickDefinitions().get().getAsFile());
fileArgs.add(getUnpickConstantJar().getSingleFile());
for (File file : getUnpickClasspath()) {
fileArgs.add(file);
}
if (existingClasses != null) {
fileArgs.add(existingClasses.toFile());
}
return fileArgs.stream()
.map(File::getAbsolutePath)
.toList();
}
private File writeUnpickLogConfig() {
final File unpickLoggingConfigFile = getUnpickLogConfig().getAsFile().get();
try (InputStream is = GenerateSourcesTask.class.getClassLoader().getResourceAsStream("unpick-logging.properties")) {
Files.copy(Objects.requireNonNull(is), unpickLoggingConfigFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Failed to copy unpick logging config", e);
}
return unpickLoggingConfigFile;
}
private void remapLineNumbers(ClassLineNumbers lineNumbers, Path inputJar, Path outputJar) throws IOException {
Objects.requireNonNull(lineNumbers, "lineNumbers");
final var remapper = new LineNumberRemapper(lineNumbers);
@@ -689,6 +607,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return !Boolean.getBoolean("fabric.loom.genSources.debug");
}
private boolean usingUnpick() {
return getUnpickOptions().isPresent();
}
public interface DecompileParams extends WorkParameters {
Property<DecompilerOptions.Dto> getDecompilerOptions();
@@ -816,26 +738,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
}
}
private static String fileHash(File file) {
try {
return Checksum.sha256Hex(Files.readAllBytes(file.toPath()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String fileCollectionHash(FileCollection files) {
var sj = new StringJoiner(",");
files.getFiles()
.stream()
.sorted(Comparator.comparing(File::getAbsolutePath))
.map(GenerateSourcesTask::fileHash)
.forEach(sj::add);
return sj.toString();
}
public interface MappingsProcessor {
boolean transform(MemoryMappingTree mappings);
}

View File

@@ -24,13 +24,18 @@
package net.fabricmc.loom.task;
import java.io.File;
import javax.inject.Inject;
import com.google.common.base.Preconditions;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskOutputs;
import org.gradle.api.tasks.TaskProvider;
import net.fabricmc.loom.LoomGradleExtension;
@@ -41,6 +46,8 @@ import net.fabricmc.loom.task.launch.GenerateDLIConfigTask;
import net.fabricmc.loom.task.launch.GenerateLog4jConfigTask;
import net.fabricmc.loom.task.launch.GenerateRemapClasspathTask;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.Platform;
import net.fabricmc.loom.util.gradle.GradleUtils;
public abstract class LoomTasks implements Runnable {
@@ -132,17 +139,28 @@ public abstract class LoomTasks implements Runnable {
private void registerRunTasks() {
LoomGradleExtension extension = LoomGradleExtension.get(getProject());
final boolean renderDocSupported = RenderDocRunTask.isSupported(Platform.CURRENT);
Preconditions.checkArgument(extension.getRunConfigs().size() == 0, "Run configurations must not be registered before loom");
extension.getRunConfigs().whenObjectAdded(config -> {
getTasks().register(getRunConfigTaskName(config), RunGameTask.class, config).configure(t -> {
var runTask = getTasks().register(getRunConfigTaskName(config), RunGameTask.class, config);
runTask.configure(t -> {
t.setDescription("Starts the '" + config.getConfigName() + "' run configuration");
t.dependsOn(config.getEnvironment().equals("client") ? "configureClientLaunch" : "configureLaunch");
});
if (config.getName().equals("client") && renderDocSupported) {
getTasks().register("runClientRenderDoc", RenderDocRunTask.class, config);
}
});
if (renderDocSupported) {
configureRenderDocTasks();
}
extension.getRunConfigs().whenObjectRemoved(runConfigSettings -> {
getTasks().named(getRunConfigTaskName(runConfigSettings), task -> {
// Disable the task so it can't be run
@@ -170,7 +188,58 @@ public abstract class LoomTasks implements Runnable {
return;
}
extension.getRunConfigs().removeIf(settings -> settings.getName().equals(taskName));
extension.getRunConfigs().removeIf(settings -> settings.getName().equals(taskName)
|| settings.getName().equals(taskName + "RenderDoc"));
});
}
private void configureRenderDocTasks() {
final Platform.OperatingSystem operatingSystem = Platform.CURRENT.getOperatingSystem();
final String renderDocVersion = LoomVersions.RENDERDOC.version();
final String renderDocBaseName = operatingSystem.isWindows()
? "RenderDoc_%s_64".formatted(renderDocVersion)
: "renderdoc_%s".formatted(renderDocVersion);
final String renderDocFilename = operatingSystem.isWindows()
? "%s.zip".formatted(renderDocBaseName)
: "%s.tar.gz".formatted(renderDocBaseName);
final String renderDocUrl = "https://maven.fabricmc.net/org/renderdoc/%s".formatted(renderDocFilename);
final String executableExt = operatingSystem.isWindows() ? ".exe" : "";
var downloadRenderDoc = getTasks().register("downloadRenderDoc", DownloadTask.class, task -> {
task.setGroup(Constants.TaskGroup.FABRIC);
task.getUrl().set(renderDocUrl);
task.getOutput().set(getProject().getLayout().getBuildDirectory().file(renderDocFilename));
});
var extractRenderDoc = getTasks().register("extractRenderDoc", Sync.class, task -> {
task.setGroup(Constants.TaskGroup.FABRIC);
if (operatingSystem.isWindows()) {
task.from(getProject().zipTree(downloadRenderDoc.map(DownloadTask::getOutput)));
} else {
task.from(getProject().tarTree(downloadRenderDoc.map(DownloadTask::getOutput)));
}
task.into(getProject().getLayout().getBuildDirectory().dir("renderdoc"));
});
Provider<File> renderDocDir = extractRenderDoc.map(Sync::getOutputs)
.map(TaskOutputs::getFiles)
.map(FileCollection::getSingleFile)
.map(dir -> new File(dir, renderDocBaseName));
if (operatingSystem.isLinux()) {
renderDocDir = renderDocDir.map(dir -> new File(dir, "bin"));
}
Provider<File> renderDocCMD = renderDocDir.map(dir -> new File(dir, "renderdoccmd" + executableExt));
Provider<File> renderDocUI = renderDocDir.map(dir -> new File(dir, "qrenderdoc" + executableExt));
getTasks().register("startRenderDocUI", RenderDocRunUITask.class, task -> task.getRenderDocExecutable().fileProvider(renderDocUI));
getTasks().withType(RenderDocRunTask.class).configureEach(task -> {
task.getRenderDocExecutable().fileProvider(renderDocCMD);
});
}

View File

@@ -0,0 +1,93 @@
/*
* 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;
import java.io.File;
import javax.inject.Inject;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.process.CommandLineArgumentProvider;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.configuration.ide.RunConfigSettings;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.Platform;
public abstract class RenderDocRunTask extends RunGameTask {
private static final Logger LOGGER = LoggerFactory.getLogger(RenderDocRunTask.class);
@InputFile
public abstract RegularFileProperty getRenderDocExecutable();
@Input
public abstract ListProperty<String> getRenderDocArgs();
@Inject
protected abstract ExecOperations getExecOperations();
@Inject
public RenderDocRunTask(RunConfigSettings settings) {
super(settings);
setGroup(Constants.TaskGroup.FABRIC);
dependsOn("configureClientLaunch");
getRenderDocArgs().addAll("capture", "--wait-for-exit");
}
@Override
public void exec() {
ExecResult result = getExecOperations().exec(exec -> {
exec.workingDir(new File(getProjectDir().get(), getInternalRunDir().get()));
exec.environment(getInternalEnvironmentVars().get());
exec.commandLine(getRenderDocExecutable().get().getAsFile());
exec.args(getRenderDocArgs().get());
exec.args("--working-dir", new File(getProjectDir().get(), getInternalRunDir().get()));
exec.args(getJavaLauncher().get().getExecutablePath());
exec.args(getJvmArgs());
exec.args(getMainClass().get());
for (CommandLineArgumentProvider provider : getArgumentProviders()) {
exec.args(provider.asArguments());
}
LOGGER.info("Running command: {}", exec.getCommandLine());
});
result.assertNormalExitValue();
}
public static boolean isSupported(Platform platform) {
final Platform.OperatingSystem os = platform.getOperatingSystem();
final Platform.Architecture arch = platform.getArchitecture();
// RenderDoc does support 32-bit Windows, but I cannot be bothered to test/maintain it
return (os.isLinux() || os.isWindows()) && arch.isX64();
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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;
import java.io.IOException;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.TaskAction;
import net.fabricmc.loom.util.Constants;
public abstract class RenderDocRunUITask extends DefaultTask {
@InputFile
public abstract RegularFileProperty getRenderDocExecutable();
public RenderDocRunUITask() {
setGroup(Constants.TaskGroup.FABRIC);
}
@TaskAction
public void run() throws IOException {
ProcessBuilder builder = new ProcessBuilder()
.command(getRenderDocExecutable().getAsFile().get().getAbsolutePath());
builder.start();
// Allow to run in the background.
}
}

View File

@@ -25,6 +25,7 @@
package net.fabricmc.loom.task.service;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
@@ -35,9 +36,12 @@ import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.task.AbstractRemapJarTask;
import net.fabricmc.loom.util.TinyRemapperHelper;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
@@ -52,6 +56,8 @@ import net.fabricmc.tinyremapper.IMappingProvider;
public final class MappingsService extends Service<MappingsService.Options> implements Closeable {
public static ServiceType<Options, MappingsService> TYPE = new ServiceType<>(Options.class, MappingsService.class);
private static final Logger LOGGER = LoggerFactory.getLogger(TinyRemapperService.class);
// TODO use a nested TinyMappingsService instead of duplicating it
public interface Options extends Service.Options {
@InputFile
@@ -91,6 +97,36 @@ public final class MappingsService extends Service<MappingsService.Options> impl
return createOptions(project, LoomGradleExtension.get(project).getPlatformMappingFile(), from, to, false);
}
public static Provider<MappingsService.Options> createForRemapTask(AbstractRemapJarTask remapJarTask) {
final Project project = remapJarTask.getProject();
return project.provider(() -> {
if (remapJarTask.getCustomMappings().isEmpty()) {
LOGGER.debug("Using default project mappings for remapping");
return MappingsService.createOptionsWithProjectMappings(
project,
remapJarTask.getSourceNamespace(),
remapJarTask.getTargetNamespace()
).get();
}
// Custom mappings:
File mappingsFile = remapJarTask.getCustomMappings().getSingleFile();
if (mappingsFile.getName().endsWith(".zip") || mappingsFile.getName().endsWith(".jar")) {
mappingsFile = project.zipTree(mappingsFile).matching(patternFilterable -> patternFilterable.include("mappings/mappings.tiny")).getSingleFile();
}
LOGGER.info("Using custom mappings for remap task: {}", mappingsFile);
return MappingsService.createOptions(
project,
mappingsFile.toPath(),
remapJarTask.getSourceNamespace(), remapJarTask.getTargetNamespace(),
false)
.get();
});
}
public MappingsService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}

View File

@@ -29,8 +29,8 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
import org.cadixdev.lorenz.MappingSet;
import org.cadixdev.mercury.Mercury;
import org.cadixdev.mercury.remapper.MercuryRemapper;
@@ -181,7 +181,7 @@ public class MigrateMappingsService extends Service<MigrateMappingsService.Optio
}
} catch (IllegalDependencyNotation ignored) {
LOGGER.info("Could not locate mappings, presuming V2 Yarn");
return project.getConfigurations().detachedConfiguration(project.getDependencies().create(ImmutableMap.of("group", "net.fabricmc", "name", "yarn", "version", mappings, "classifier", "v2")));
return project.getConfigurations().detachedConfiguration(project.getDependencies().create(Map.of("group", "net.fabricmc", "name", "yarn", "version", mappings, "classifier", "v2")));
} catch (IOException e) {
throw new UncheckedIOException("Failed to resolve mappings", e);
}

View File

@@ -68,11 +68,7 @@ public final class SourceRemapperService extends Service<SourceRemapperService.O
public static Provider<Options> createOptions(RemapSourcesJarTask task) {
return TYPE.create(task.getProject(), o -> {
o.getMappings().set(MappingsService.createOptionsWithProjectMappings(
task.getProject(),
task.getSourceNamespace(),
task.getTargetNamespace()
));
o.getMappings().set(MappingsService.createForRemapTask(task));
o.getJavaCompileRelease().set(getJavaCompileRelease(task.getProject()));
o.getClasspath().from(task.getClasspath());
});

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;
@@ -51,6 +52,7 @@ import org.gradle.api.tasks.Optional;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.extension.RemapperExtensionHolder;
import net.fabricmc.loom.task.AbstractRemapJarTask;
@@ -66,7 +68,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 {
@@ -103,7 +105,7 @@ public class TinyRemapperService extends Service<TinyRemapperService.Options> im
options.getFrom().set(remapJarTask.getSourceNamespace());
options.getTo().set(remapJarTask.getTargetNamespace());
options.getMappings().add(MappingsService.createOptionsWithProjectMappings(project, options.getFrom(), options.getTo()));
options.getMappings().add(MappingsService.createForRemapTask(remapJarTask));
if (legacyMixin) {
options.getMixinApMappings().set(MixinAPMappingService.createOptions(project, options.getFrom(), options.getTo().map(to -> IntermediaryNamespaces.replaceMixinIntermediaryNamespace(project, to))));
@@ -117,6 +119,56 @@ public class TinyRemapperService extends Service<TinyRemapperService.Options> im
});
}
public static Provider<Options> createSimple(Project project, Provider<String> from, Provider<String> to, ClasspathLibraries classpathLibraries) {
return TYPE.create(project, options -> {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final FileCollection classpath = getRemapClasspath(project, from, classpathLibraries);
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 static FileCollection getRemapClasspath(Project project, Provider<String> from, ClasspathLibraries classpathLibraries) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final ConfigurationContainer configurations = project.getConfigurations();
if (from.get().equals(MappingsNamespace.INTERMEDIARY.toString())) {
ConfigurableFileCollection files = project.files(extension.getMinecraftJars(MappingsNamespace.INTERMEDIARY));
if (classpathLibraries == ClasspathLibraries.INCLUDE) {
files = files.from(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
}
return files;
}
if (classpathLibraries == ClasspathLibraries.INCLUDE) {
return configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
}
return configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)
.minus(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES))
.minus(configurations.getByName(Constants.Configurations.MINECRAFT_RUNTIME_LIBRARIES));
}
public enum ClasspathLibraries {
/**
* Default, in most cases the Minecraft libraries are not required as they are not obfuscated and do not need to be queried.
*/
EXCLUDE,
/**
* Uses more memory, but provides a complete index of all the classes within the libraries.
*/
INCLUDE
}
private TinyRemapper tinyRemapper;
@Nullable
private KotlinRemapperClassloader kotlinRemapperClassloader;
@@ -179,11 +231,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,171 @@
/*
* 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.lang.reflect.Field;
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.Type;
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),
TinyRemapperService.ClasspathLibraries.INCLUDE // Must include the full set of libraries on classpath so fields can be looked up. This does use a lot of memory however...
));
});
}
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.replace('.', '/')).replace('/', '.');
}
@Override
protected String mapFieldName(String className, String fieldName, String fieldDesc) {
return remapper.mapFieldName(className.replace('.', '/'), fieldName, fieldDesc);
}
@Override
protected String mapMethodName(String className, String methodName, String methodDesc) {
return remapper.mapMethodName(className.replace('.', '/'), 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.replace('.', '/'));
if (trClass != null) {
for (TrField trField : trClass.getFields()) {
if (trField.getName().equals(fieldName)) {
return trField.getDesc();
}
}
}
String fieldDesc = getFieldDescFromReflection(className, fieldName);
if (fieldDesc == null) {
throw new IllegalStateException("Could not find field " + fieldName + " in class " + className);
}
return fieldDesc;
}
private static String getFieldDescFromReflection(String className, String fieldName) {
try {
// Use the bootstrap class loader, which should only resolve classes from the JDK.
// Don't run the static initializer.
Class<?> clazz = Class.forName(className, false, null);
Field field = clazz.getDeclaredField(fieldName);
return Type.getDescriptor(field.getType());
} catch (ClassNotFoundException | NoSuchFieldException e) {
return null;
}
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* 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.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;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import daomephsta.unpick.api.ConstantUninliner;
import daomephsta.unpick.api.classresolvers.ClassResolvers;
import daomephsta.unpick.api.classresolvers.IClassResolver;
import daomephsta.unpick.api.constantgroupers.ConstantGroupers;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
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;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.configuration.providers.mappings.unpick.UnpickMetadata;
import net.fabricmc.loom.task.GenerateSourcesTask;
import net.fabricmc.loom.util.AsyncZipProcessor;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.SLF4JAdapterHandler;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
public class UnpickService extends Service<UnpickService.Options> {
private static final Logger LOGGER = LoggerFactory.getLogger(UnpickService.class);
private static final java.util.logging.Logger JAVA_LOGGER = java.util.logging.Logger.getLogger("loom-unpick-service");
static {
JAVA_LOGGER.setUseParentHandlers(false);
JAVA_LOGGER.addHandler(new SLF4JAdapterHandler(LOGGER, true));
}
public static final ServiceType<Options, UnpickService> TYPE = new ServiceType<>(Options.class, UnpickService.class);
public interface Options extends Service.Options {
@InputFile
RegularFileProperty getUnpickDefinitions();
@Optional
@Nested
Property<UnpickRemapperService.Options> getUnpickRemapperService();
@InputFiles
ConfigurableFileCollection getUnpickConstantJar();
@InputFiles
ConfigurableFileCollection getUnpickClasspath();
@OutputFile
RegularFileProperty getUnpickOutputJar();
@Input
Property<Boolean> getLenient();
}
public static Provider<Options> createOptions(GenerateSourcesTask task) {
final Project project = task.getProject();
return TYPE.maybeCreate(project, options -> {
LoomGradleExtension extension = LoomGradleExtension.get(project);
MappingConfiguration mappingConfiguration = extension.getMappingConfiguration();
if (!mappingConfiguration.hasUnpickDefinitions()) {
return false;
}
UnpickMetadata unpickMetadata = mappingConfiguration.getUnpickMetadata();
if (unpickMetadata instanceof UnpickMetadata.V2 v2) {
if (!Objects.equals(v2.namespace(), MappingsNamespace.NAMED.toString())) {
options.getUnpickRemapperService().set(UnpickRemapperService.createOptions(project, v2));
}
}
ConfigurationContainer configurations = project.getConfigurations();
File mappingsWorkingDir = mappingConfiguration.mappingsWorkingDir().toFile();
options.getUnpickDefinitions().set(mappingConfiguration.getUnpickDefinitionsFile());
options.getUnpickOutputJar().set(task.getInputJarName().map(s -> project.getLayout()
.dir(project.provider(() -> mappingsWorkingDir)).get().file(s + "-unpicked.jar")));
options.getUnpickConstantJar().setFrom(configurations.getByName(Constants.Configurations.MAPPING_CONSTANTS));
options.getUnpickClasspath().setFrom(configurations.getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
options.getUnpickClasspath().from(configurations.getByName(Constants.Configurations.MOD_COMPILE_CLASSPATH_MAPPED));
options.getLenient().set(unpickMetadata instanceof UnpickMetadata.V1);
extension.getMinecraftJars(MappingsNamespace.NAMED).forEach(options.getUnpickClasspath()::from);
return true;
});
}
public UnpickService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
}
public Path unpickJar(Path inputJar, @Nullable Path existingClasses) throws IOException {
final List<Path> classpath = Stream.of(
getOptions().getUnpickClasspath().getFiles().stream().map(File::toPath),
getOptions().getUnpickConstantJar().getFiles().stream().map(File::toPath),
Stream.of(inputJar),
Stream.ofNullable(existingClasses)
).flatMap(Function.identity()).toList();
final Path outputJar = getOptions().getUnpickOutputJar().get().getAsFile().toPath();
Files.deleteIfExists(outputJar);
try (ZipFsClasspath zipFsClasspath = ZipFsClasspath.create(classpath);
InputStream unpickDefinitions = getUnpickDefinitionsInputStream()) {
IClassResolver classResolver = zipFsClasspath.createClassResolver().chain(ClassResolvers.classpath());
ConstantUninliner uninliner = ConstantUninliner.builder()
.logger(JAVA_LOGGER)
.classResolver(classResolver)
.grouper(ConstantGroupers.dataDriven()
.logger(JAVA_LOGGER)
.lenient(getOptions().getLenient().get())
.classResolver(classResolver)
.mappingSource(unpickDefinitions)
.build())
.build();
AsyncZipProcessor.processEntries(inputJar, outputJar, new UnpickZipProcessor(uninliner));
}
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().getUnpickRemapperService()
.flatMap(options -> options.getTinyRemapper()
.flatMap(TinyRemapperService.Options::getFrom))
.getOrElse("named"))
)).sha256().hex();
}
private record UnpickZipProcessor(ConstantUninliner uninliner) implements AsyncZipProcessor {
@Override
public void processEntryAsync(Path input, Path output) throws IOException {
Files.createDirectories(output.getParent());
String fileName = input.toAbsolutePath().toString();
if (!fileName.endsWith(".class")) {
// Copy non-class files
Files.copy(input, output);
return;
}
ClassNode classNode = new ClassNode();
try (InputStream is = Files.newInputStream(input)) {
ClassReader reader = new ClassReader(is);
reader.accept(classNode, 0);
}
LOGGER.debug("Unpick class: {}", classNode.name);
uninliner.transform(classNode);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
Files.write(output, writer.toByteArray());
}
}
private record ZipFsClasspath(List<FileSystemUtil.Delegate> fileSystems) implements Closeable {
private ZipFsClasspath {
if (fileSystems.isEmpty()) {
throw new IllegalArgumentException("No resolvers provided");
}
}
public static ZipFsClasspath create(List<Path> classpath) throws IOException {
var fileSystems = new ArrayList<FileSystemUtil.Delegate>();
for (Path path : classpath) {
FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(path, false);
fileSystems.add(fs);
}
return new ZipFsClasspath(fileSystems);
}
public IClassResolver createClassResolver() {
IClassResolver resolver = ClassResolvers.fromDirectory(fileSystems.getFirst().getRoot());
for (int i = 1; i < fileSystems.size(); i++) {
resolver = resolver.chain(ClassResolvers.fromDirectory(fileSystems.get(i).getRoot()));
}
return resolver;
}
@Override
public void close() throws IOException {
for (FileSystemUtil.Delegate fileSystem : fileSystems) {
fileSystem.close();
}
}
}
}

View File

@@ -40,17 +40,17 @@ import java.util.concurrent.Executors;
public interface AsyncZipProcessor {
static void processEntries(Path inputZip, Path outputZip, AsyncZipProcessor processor) throws IOException {
try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputZip, false);
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputZip, true)) {
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputZip, true);
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {
final Path inRoot = inFs.get().getPath("/");
final Path outRoot = outFs.get().getPath("/");
List<CompletableFuture<Void>> futures = new ArrayList<>();
final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Files.walkFileTree(inRoot, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path inputFile, BasicFileAttributes attrs) throws IOException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
public FileVisitResult visitFile(Path inputFile, BasicFileAttributes attrs) {
final CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
final String rel = inRoot.relativize(inputFile).toString();
final Path outputFile = outRoot.resolve(rel);
@@ -58,8 +58,6 @@ public interface AsyncZipProcessor {
} catch (IOException e) {
throw new CompletionException(e);
}
return null;
}, executor);
futures.add(future);
@@ -67,7 +65,7 @@ public interface AsyncZipProcessor {
}
});
// Wait for all futures to complete
// Wait for all futures to complete, throwing the first exception if any
for (CompletableFuture<Void> future : futures) {
try {
future.join();
@@ -79,8 +77,6 @@ public interface AsyncZipProcessor {
throw new RuntimeException("Failed to process zip", e.getCause());
}
}
executor.shutdown();
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.util.function.Supplier;
import com.google.common.base.Suppliers;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.tasks.Internal;
import net.fabricmc.loom.util.gradle.GradleTypeAdapter;
/**
* A simple base class for creating cache keys. Extend this class and create abstract properties to be included in the cache key.
*/
public abstract class CacheKey {
private static final int CHECKSUM_LENGTH = 8;
private final transient Supplier<String> jsonSupplier = Suppliers.memoize(() -> GradleTypeAdapter.GSON.toJson(this));
private final transient Supplier<String> cacheKeySupplier = Suppliers.memoize(() -> Checksum.of(jsonSupplier.get()).sha1().hex(CHECKSUM_LENGTH));
public static <T> T create(Project project, Class<T> clazz, Action<T> action) {
T instance = project.getObjects().newInstance(clazz);
action.execute(instance);
return instance;
}
@Internal
public final String getJson() {
return jsonSupplier.get();
}
@Internal
public final String getCacheKey() {
return cacheKeySupplier.get();
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2020 FabricMC
* 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
@@ -26,88 +26,138 @@ package net.fabricmc.loom.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.file.FileCollection;
import org.jetbrains.annotations.NotNull;
public class Checksum {
private static final Logger log = Logging.getLogger(Checksum.class);
public static boolean equals(File file, String checksum) {
if (file == null || !file.exists()) {
return false;
public final class Checksum {
public static Checksum of(byte[] data) {
return new Checksum(digest -> digest.write(data));
}
public static Checksum of(String str) {
return new Checksum(digest -> digest.write(str));
}
public static Checksum of(File file) {
return of(file.toPath());
}
public static Checksum of(Path file) {
return new Checksum(digest -> {
try (InputStream is = Files.newInputStream(file)) {
is.transferTo(digest);
}
});
}
public static Checksum of(Project project) {
return of(project.getProjectDir().getAbsolutePath() + ":" + project.getPath());
}
public static Checksum of(FileCollection files) {
return new Checksum(os -> {
for (File file : files) {
try (InputStream is = Files.newInputStream(file.toPath())) {
is.transferTo(os);
}
}
});
}
public static Checksum of(List<Checksum> others) {
return new Checksum(os -> {
for (Checksum other : others) {
other.consumer.accept(os);
}
});
}
private final DataConsumer consumer;
private Checksum(DataConsumer consumer) {
this.consumer = consumer;
}
public Result sha1() {
return computeResult("SHA-1");
}
public Result sha256() {
return computeResult("SHA-256");
}
public Result md5() {
return computeResult("MD5");
}
private Result computeResult(String algorithm) {
MessageDigest digest;
try {
HashCode hash = Files.asByteSource(file).hash(Hashing.sha1());
String hashString = hash.toString();
log.debug("Checksum check: '" + hashString + "' == '" + checksum + "'?");
return hashString.equals(checksum);
digest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try (MessageDigestOutputStream os = new MessageDigestOutputStream(digest)) {
consumer.accept(os);
} catch (IOException e) {
e.printStackTrace();
throw new UncheckedIOException("Failed to compute checksum", e);
}
return false;
return new Result(digest.digest());
}
public static byte[] sha256(File file) {
try {
HashCode hash = Files.asByteSource(file).hash(Hashing.sha256());
return hash.asBytes();
} catch (IOException e) {
throw new UncheckedIOException("Failed to get file hash", e);
public record Result(byte[] digest) {
public String hex() {
return HexFormat.of().formatHex(digest());
}
public String hex(int length) {
return hex().substring(0, length);
}
public boolean matchesStr(String other) {
return hex().equalsIgnoreCase(other);
}
}
public static String sha256Hex(byte[] input) throws IOException {
HashCode hash = ByteSource.wrap(input).hash(Hashing.sha256());
return Checksum.toHex(hash.asBytes());
@FunctionalInterface
private interface DataConsumer {
void accept(MessageDigestOutputStream os) throws IOException;
}
public static String sha1Hex(Path path) throws IOException {
HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1());
return toHex(hash.asBytes());
private static class MessageDigestOutputStream extends OutputStream {
private final MessageDigest digest;
private MessageDigestOutputStream(MessageDigest digest) {
this.digest = digest;
}
public static String sha1Hex(byte[] input) {
try {
HashCode hash = ByteSource.wrap(input).hash(Hashing.sha1());
return toHex(hash.asBytes());
} catch (IOException e) {
throw new UncheckedIOException("Failed to hash", e);
}
@Override
public void write(int b) {
digest.update((byte) b);
}
public static String truncatedSha256(File file) {
try {
HashCode hash = Files.asByteSource(file).hash(Hashing.sha256());
return hash.toString().substring(0, 12);
} catch (IOException e) {
throw new UncheckedIOException("Failed to get file hash of " + file, e);
}
@Override
public void write(byte @NotNull[] b, int off, int len) {
digest.update(b, off, len);
}
public static byte[] sha256(String string) {
HashCode hash = Hashing.sha256().hashString(string, StandardCharsets.UTF_8);
return hash.asBytes();
}
public static String toHex(byte[] bytes) {
return BaseEncoding.base16().lowerCase().encode(bytes);
}
public static String projectHash(Project project) {
String str = project.getProjectDir().getAbsolutePath() + ":" + project.getPath();
String hex = sha1Hex(str.getBytes(StandardCharsets.UTF_8));
return hex.substring(hex.length() - 16);
public void write(String string) throws IOException {
write(string.getBytes(StandardCharsets.UTF_8));
}
}
}

View File

@@ -101,7 +101,6 @@ public class Constants {
*/
public static final String FORGE_RUNTIME_LIBRARY = "forgeRuntimeLibrary";
public static final String MAPPING_CONSTANTS = "mappingsConstants";
public static final String UNPICK_CLASSPATH = "unpick";
/**
* A configuration that behaves like {@code runtimeOnly} but is not
* exposed in {@code runtimeElements} to dependents. A bit like
@@ -176,6 +175,14 @@ public class Constants {
public static final String RUNTIME_JAVA_COMPATIBILITY_VERSION = "fabric.loom.runtimeJavaCompatibilityVersion";
public static final String DECOMPILE_CACHE_MAX_FILES = "fabric.loom.decompileCacheMaxFiles";
public static final String DECOMPILE_CACHE_MAX_AGE = "fabric.loom.decompileCacheMaxAge";
/**
* Skip the signature verification of the Minecraft jar after downloading it.
*/
public static final String DISABLE_MINECRAFT_VERIFICATION = "fabric.loom.disableMinecraftVerification";
/**
* When using the MojangMappingLayer this will remove names for non root methods by using the intermediary mappings.
*/
public static final String DROP_NON_INTERMEDIATE_ROOT_METHODS = "fabric.loom.dropNonIntermediateRootMethods";
public static final String ALLOW_MISMATCHED_PLATFORM_VERSION = "loom.allowMismatchedPlatformVersion";
public static final String IGNORE_DEPENDENCY_LOOM_VERSION_VALIDATION = "loom.ignoreDependencyLoomVersionValidation";
}

View File

@@ -0,0 +1,49 @@
/*
* 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.util.IdentityHashMap;
import java.util.Map;
public final class IdentityBiMap<K, V> {
private final Map<K, V> keyToValue = new IdentityHashMap<>();
private final Map<V, K> valueToKey = new IdentityHashMap<>();
public IdentityBiMap() {
}
public void put(K key, V value) {
keyToValue.put(key, value);
valueToKey.put(value, key);
}
public V getByKey(K key) {
return keyToValue.get(key);
}
public K getByValue(V value) {
return valueToKey.get(value);
}
}

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

@@ -53,6 +53,10 @@ public interface Platform {
boolean isArm();
boolean isRiscV();
default boolean isX64() {
return is64Bit() && !isArm() && !isRiscV();
}
}
Architecture getArchitecture();

View File

@@ -0,0 +1,64 @@
/*
* 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.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import org.slf4j.Logger;
public class SLF4JAdapterHandler extends Handler {
private final Logger logger;
private final boolean suppressWarnings;
public SLF4JAdapterHandler(Logger logger, boolean suppressWarnings) {
this.logger = logger;
this.suppressWarnings = suppressWarnings;
}
@Override
public void publish(LogRecord record) {
if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
logger.error(record.getMessage(), record.getThrown());
} else if (record.getLevel().intValue() >= Level.WARNING.intValue() && !suppressWarnings) {
logger.warn(record.getMessage(), record.getThrown());
} else if (record.getLevel().intValue() >= Level.INFO.intValue()) {
logger.info(record.getMessage(), record.getThrown());
} else if (record.getLevel().intValue() >= Level.FINER.intValue()) {
logger.debug(record.getMessage(), record.getThrown());
} else {
logger.trace(record.getMessage(), record.getThrown());
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
}

View File

@@ -120,7 +120,7 @@ public class SourceRemapper {
source = new File(destination.getAbsolutePath().substring(0, destination.getAbsolutePath().lastIndexOf('.')) + "-dev.jar");
try {
com.google.common.io.Files.move(destination, source);
Files.move(destination.toPath(), source.toPath());
} catch (IOException e) {
throw new RuntimeException("Could not rename " + destination.getName() + "!", e);
}

View File

@@ -31,7 +31,6 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import com.google.common.collect.ImmutableMap;
import dev.architectury.loom.util.MappingOption;
import org.gradle.api.Project;
@@ -50,11 +49,11 @@ import net.fabricmc.tinyremapper.TinyRemapper;
* Contains shortcuts to create tiny remappers using the mappings accessibly to the project.
*/
public final class TinyRemapperHelper {
public static final Map<String, String> JSR_TO_JETBRAINS = new ImmutableMap.Builder<String, String>()
.put("javax/annotation/Nullable", "org/jetbrains/annotations/Nullable")
.put("javax/annotation/Nonnull", "org/jetbrains/annotations/NotNull")
.put("javax/annotation/concurrent/Immutable", "org/jetbrains/annotations/Unmodifiable")
.build();
public static final Map<String, String> JSR_TO_JETBRAINS = Map.of(
"javax/annotation/Nullable", "org/jetbrains/annotations/Nullable",
"javax/annotation/Nonnull", "org/jetbrains/annotations/NotNull",
"javax/annotation/concurrent/Immutable", "org/jetbrains/annotations/Unmodifiable"
);
/**
* Matches the new local variable naming format introduced in 21w37a.

View File

@@ -46,7 +46,7 @@ public class ZipReprocessorUtil {
private static final String META_INF = "META-INF/";
// See https://docs.oracle.com/en/java/javase/20/docs/specs/jar/jar.html#signed-jar-file
private static boolean isSpecialFile(String zipEntryName) {
public static boolean isSpecialFile(String zipEntryName) {
if (!zipEntryName.startsWith(META_INF)) {
return false;
}

Some files were not shown because too many files have changed in this diff Show More