mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-03-28 04:07:01 -05:00
Merge 1.11
This commit is contained in:
@@ -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.**,|,$*
|
||||
|
||||
25
.github/workflows/test-push.yml
vendored
25
.github/workflows/test-push.yml
vendored
@@ -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:
|
||||
|
||||
22
build.gradle
22
build.gradle
@@ -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()
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
6
gradlew
vendored
@@ -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
4
gradlew.bat
vendored
@@ -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
|
||||
|
||||
@@ -155,6 +155,11 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
|
||||
|
||||
boolean isProjectIsolationActive();
|
||||
|
||||
/**
|
||||
* @return true when '--write-verification-metadata` is set
|
||||
*/
|
||||
boolean isCollectingDependencyVerificationMetadata();
|
||||
|
||||
// ===================
|
||||
// Architectury Loom
|
||||
// ===================
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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("\"", """));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 + '\'' + '}';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -92,7 +92,6 @@ public class MergedMinecraftProvider extends MinecraftProvider {
|
||||
File minecraftServerJar = getMinecraftServerJar();
|
||||
|
||||
if (getServerBundleMetadata() != null) {
|
||||
extractBundledServerJar();
|
||||
minecraftServerJar = getMinecraftExtractedServerJar();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -46,7 +46,6 @@ public interface LoomFiles {
|
||||
File getNativesDirectory(Project project);
|
||||
File getDefaultLog4jConfigFile();
|
||||
File getDevLauncherConfig();
|
||||
File getUnpickLoggingConfigFile();
|
||||
File getRemapClasspathFile();
|
||||
File getGlobalMinecraftRepo();
|
||||
File getLocalMinecraftRepo();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
93
src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java
Normal file
93
src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java
Normal 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();
|
||||
}
|
||||
}
|
||||
51
src/main/java/net/fabricmc/loom/task/RenderDocRunUITask.java
Normal file
51
src/main/java/net/fabricmc/loom/task/RenderDocRunUITask.java
Normal 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.
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
270
src/main/java/net/fabricmc/loom/task/service/UnpickService.java
Normal file
270
src/main/java/net/fabricmc/loom/task/service/UnpickService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
59
src/main/java/net/fabricmc/loom/util/CacheKey.java
Normal file
59
src/main/java/net/fabricmc/loom/util/CacheKey.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 void write(String string) throws IOException {
|
||||
write(string.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
49
src/main/java/net/fabricmc/loom/util/IdentityBiMap.java
Normal file
49
src/main/java/net/fabricmc/loom/util/IdentityBiMap.java
Normal 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);
|
||||
}
|
||||
}
|
||||
100
src/main/java/net/fabricmc/loom/util/JarPackageIndex.java
Normal file
100
src/main/java/net/fabricmc/loom/util/JarPackageIndex.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,10 @@ public interface Platform {
|
||||
boolean isArm();
|
||||
|
||||
boolean isRiscV();
|
||||
|
||||
default boolean isX64() {
|
||||
return is64Bit() && !isArm() && !isRiscV();
|
||||
}
|
||||
}
|
||||
|
||||
Architecture getArchitecture();
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user