Merge remote-tracking branch 'upstream/exp/1.12' into exp/1.12

# Conflicts:
#	build.gradle
#	gradle/runtime.libs.versions.toml
#	src/main/java/net/fabricmc/loom/build/nesting/JarNester.java
#	src/main/java/net/fabricmc/loom/build/nesting/NestableJarGenerationTask.java
#	src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java
#	src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java
#	src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java
#	src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java
#	src/main/java/net/fabricmc/loom/task/service/LorenzMappingService.java
#	src/main/java/net/fabricmc/loom/util/Constants.java
This commit is contained in:
Juuz
2025-10-01 01:09:17 +03:00
94 changed files with 5435 additions and 254 deletions

View File

@@ -23,7 +23,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
}
group = "dev.architectury"
def baseVersion = '1.11'
def baseVersion = '1.12'
def ENV = System.getenv()
def runNumber = ENV.GITHUB_RUN_NUMBER ?: "9999"
@@ -100,9 +100,7 @@ dependencies {
implementation gradleApi()
// libraries
implementation libs.commons.io
implementation libs.gson
implementation libs.guava
implementation libs.bundles.asm
// game handling utils
@@ -185,6 +183,7 @@ dependencies {
testImplementation testLibs.bcprov
testImplementation testLibs.bcutil
testImplementation testLibs.bcpkix
testImplementation testLibs.fabric.loader
compileOnly runtimeLibs.jetbrains.annotations
testCompileOnly runtimeLibs.jetbrains.annotations
@@ -258,13 +257,6 @@ checkstyle {
toolVersion = libs.versions.checkstyle.get()
}
// Workaround https://github.com/gradle/gradle/issues/27035
configurations.checkstyle {
resolutionStrategy.capabilitiesResolution.withCapability("com.google.collections:google-collections") {
select("com.google.guava:guava:0")
}
}
codenarc {
toolVersion = libs.versions.codenarc.get()
configFile = file("codenarc.groovy")

View File

@@ -1,12 +1,10 @@
[versions]
kotlin = "2.0.21"
asm = "9.8"
commons-io = "2.15.1"
gson = "2.10.1"
guava = "33.0.0-jre"
stitch = "0.6.2"
tiny-remapper = "0.11.1"
tiny-remapper = "0.12.0"
access-widener = "2.1.0"
mapping-io = "0.7.1"
lorenz-tiny = "4.0.2"
@@ -15,10 +13,10 @@ loom-native = "0.2.0"
unpick = "3.0.0-beta.9"
# Plugins
spotless = "6.25.0"
test-retry = "1.5.6"
checkstyle = "10.17.0"
codenarc = "3.4.0"
spotless = "7.2.1"
test-retry = "1.6.2"
checkstyle = "10.26.1"
codenarc = "3.6.0"
# Architectury libraries
forge-installer-tools = "1.2.0"
@@ -39,9 +37,7 @@ asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
fabric-stitch = { module = "net.fabricmc:stitch", version.ref = "stitch" }
fabric-tiny-remapper = { module = "net.fabricmc:tiny-remapper", version.ref = "tiny-remapper" }

View File

@@ -12,8 +12,9 @@ jetbrains-annotations = "26.0.2"
native-support = "1.0.1"
fabric-installer = "1.0.3"
# Debug tools
# Dev tools
renderdoc = "1.37"
enigma = "3.0.1"
# Forge Runtime depedencies
javax-annotations = "3.0.2"
@@ -39,8 +40,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
# Dev tools
renderdoc = { module = "org.renderdoc:renderdoc", version.ref = "renderdoc" } # Not a maven dependency
enigma-swing = { module = "cuchaz:enigma-swing", version.ref = "enigma" }
# Forge Runtime depedencies
javax-annotations = { module = "com.google.code.findbugs:jsr305", version.ref = "javax-annotations" }

View File

@@ -1,14 +1,14 @@
[versions]
spock = "2.3-groovy-3.0"
junit = "5.12.2"
javalin = "6.6.0"
mockito = "5.17.0"
junit = "5.13.4"
javalin = "6.7.0"
mockito = "5.18.0"
java-debug = "0.53.1"
mixin = "0.15.3+mixin.0.8.7"
bouncycastle = "1.80"
bouncycastle = "1.81"
gradle-latest = "9.0.0-rc-1"
gradle-nightly = "9.1.0-20250620001442+0000"
gradle-latest = "9.1.0"
gradle-nightly = "9.3.0-20250923005153+0000"
fabric-loader = "0.16.14"
[libraries]

View File

@@ -27,12 +27,13 @@ package net.fabricmc.loom.api.decompilers;
import java.io.Serializable;
import java.util.Map;
import com.google.common.base.Preconditions;
import org.gradle.api.Named;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import net.fabricmc.loom.util.Check;
public abstract class DecompilerOptions implements Named {
/**
* Class name for to the {@link LoomDecompiler}.
@@ -75,7 +76,7 @@ public abstract class DecompilerOptions implements Named {
public record Dto(String className, Map<String, String> options, int maxThreads) implements Serializable { }
public Dto toDto() {
Preconditions.checkArgument(getDecompilerClassName().isPresent(), "No decompiler classname specified for decompiler: " + getName());
Check.require(getDecompilerClassName().isPresent(), "No decompiler classname specified for decompiler: " + getName());
return new Dto(
getDecompilerClassName().get(),
getOptions().get(),

View File

@@ -0,0 +1,785 @@
/*
* 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.api.fmj;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.jetbrains.annotations.ApiStatus;
/**
* Represents the Fabric mod JSON v1 specification.
*
* <p>This class defines properties of a Fabric mod JSON file via a type-safe DSL.
*/
public abstract class FabricModJsonV1Spec {
/**
* The ID of the mod.
* @return A {@link Property} containing a {@link String} representing the mod ID
*/
@Input
public abstract Property<String> getModId();
/**
* The version of the mod.
* @return A {@link Property} containing a {@link String} representing the mod version
*/
@Input
public abstract Property<String> getVersion();
/**
* The display name of the mod.
* @return A {@link Property} containing a {@link String} representing the mod name
*/
@Input
@Optional
public abstract Property<String> getName();
/**
* The description of the mod.
* @return A {@link Property} containing a {@link String} representing the mod description
*/
@Input
@Optional
public abstract Property<String> getDescription();
/**
* A list of other mod IDs that this mod uses as aliases.
* @return A {@link ListProperty} containing a list of {@link String} representing the mod aliases
*/
@Input
@Optional
public abstract ListProperty<String> getProvides();
/**
* The environment the mod runs in.
*
* <p>One of `client`, `server`, or `*`.
*
* @return A {@link Property} containing a {@link String} representing the mod environment
*/
@Input
@Optional
public abstract Property<String> getEnvironment();
/**
* Sets the environment to 'client', indicating the mod only runs on the client side.
*/
public void client() {
getEnvironment().set("client");
}
/**
* Sets the environment to 'server', indicating the mod only runs on the server side.
*/
public void server() {
getEnvironment().set("server");
}
/**
* A list of entrypoints for the mod.
* @return A {@link ListProperty} containing a list of {@link Entrypoint} representing the mod entrypoints
*/
@Input
@Optional
public abstract ListProperty<Entrypoint> getEntrypoints();
/**
* Add a new entrypoint with the given name and value.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param value The value of the entrypoint, typically a fully qualified class name
*/
public void entrypoint(String entrypoint, String value) {
entrypoint(entrypoint, value, metadata -> { });
}
/**
* Add a new entrypoint with the given name and value, and configure it with the given action.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param value The value of the entrypoint, typically a fully qualified class name
* @param action An action to configure the entrypoint further
*/
public void entrypoint(String entrypoint, String value, Action<Entrypoint> action) {
entrypoint(entrypoint, metadata -> {
metadata.getValue().set(value);
action.execute(metadata);
});
}
/**
* Add a new entrypoint with the given name, and configure it with the given action.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param action An action to configure the entrypoint
*/
public void entrypoint(String entrypoint, Action<Entrypoint> action) {
create(Entrypoint.class, getEntrypoints(), e -> {
e.getEntrypoint().set(entrypoint);
action.execute(e);
});
}
/**
* A list of additional JARs to load with the mod.
*
* <p>A path relative to the root of the mod JAR.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the additional JARs
*/
@Input
@Optional
public abstract ListProperty<String> getJars();
/**
* A list of Mixin configurations for the mod.
*
* @return A {@link ListProperty} containing a list of {@link Mixin} representing the mod Mixins
*/
@Input
@Optional
public abstract ListProperty<Mixin> getMixins();
/**
* Add a new Mixin configuration with the given value.
*
* @param value The value of the Mixin configuration, typically a path to a JSON file
*/
public void mixin(String value) {
mixin(value, mixin -> { });
}
/**
* Add a new Mixin configuration with the given value, and configure it with the given action.
*
* @param value The value of the Mixin configuration, typically a path to a JSON file
* @param action An action to configure the Mixin further
*/
public void mixin(String value, Action<Mixin> action) {
mixin(mixin -> {
mixin.getValue().set(value);
action.execute(mixin);
});
}
/**
* Add a new Mixin configuration, and configure it with the given action.
*
* @param action An action to configure the Mixin
*/
public void mixin(Action<Mixin> action) {
create(Mixin.class, getMixins(), action);
}
/**
* The path to the access widener file for the mod.
*
* <p>A path relative to the root of the mod JAR.
*
* @return A {@link Property} containing a {@link String} representing the access widener path
*/
@Input
@Optional
public abstract Property<String> getAccessWidener();
/**
* A list of depedencies that this mod depends on (required).
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getDepends();
/**
* Add a required dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the dependency
* @param versionRequirements A collection of version requirement strings
*/
public void depends(String modId, Iterable<String> versionRequirements) {
depends(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a required dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the dependency
* @param versionRequirement A version requirement string
*/
public void depends(String modId, String versionRequirement) {
depends(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a required dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the dependency
* @param action An action to configure the dependency further
*/
public void depends(String modId, Action<Dependency> action) {
depends(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a required dependency, and configure it with the given action.
*
* @param action An action to configure the dependency
*/
public void depends(Action<Dependency> action) {
create(Dependency.class, getDepends(), action);
}
/**
* A list of recommended dependencies.
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod recommended dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getRecommends();
/**
* Add a recommended dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the recommended dependency
* @param versionRequirements A collection of version requirement strings
*/
public void recommends(String modId, Iterable<String> versionRequirements) {
recommends(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a recommended dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the recommended dependency
* @param versionRequirement A version requirement string
*/
public void recommends(String modId, String versionRequirement) {
recommends(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a recommended dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the recommended dependency
* @param action An action to configure the recommended dependency further
*/
public void recommends(String modId, Action<Dependency> action) {
recommends(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a recommended dependency, and configure it with the given action.
*
* @param action An action to configure the recommended dependency
*/
public void recommends(Action<Dependency> action) {
create(Dependency.class, getRecommends(), action);
}
/**
* A list of suggested dependencies.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod suggested dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getSuggests();
/**
* Add a suggested dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the suggested dependency
* @param versionRequirements A collection of version requirement strings
*/
public void suggests(String modId, Iterable<String> versionRequirements) {
suggests(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a suggested dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the suggested dependency
* @param versionRequirement A version requirement string
*/
public void suggests(String modId, String versionRequirement) {
suggests(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a suggested dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the suggested dependency
* @param action An action to configure the suggested dependency further
*/
public void suggests(String modId, Action<Dependency> action) {
suggests(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a suggested dependency, and configure it with the given action.
*
* @param action An action to configure the suggested dependency
*/
public void suggests(Action<Dependency> action) {
create(Dependency.class, getSuggests(), action);
}
/**
* A list of conflicting dependencies.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod conflicting dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getConflicts();
/**
* Add a conflicting dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the conflicting dependency
* @param versionRequirements A collection of version requirement strings
*/
public void conflicts(String modId, Iterable<String> versionRequirements) {
conflicts(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a conflicting dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the conflicting dependency
* @param versionRequirement A version requirement string
*/
public void conflicts(String modId, String versionRequirement) {
conflicts(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a conflicting dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the conflicting dependency
* @param action An action to configure the conflicting dependency further
*/
public void conflicts(String modId, Action<Dependency> action) {
conflicts(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a conflicting dependency, and configure it with the given action.
*
* @param action An action to configure the conflicting dependency
*/
public void conflicts(Action<Dependency> action) {
create(Dependency.class, getConflicts(), action);
}
/**
* A list of dependencies that this mod breaks.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod broken dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getBreaks();
/**
* Add a broken dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the broken dependency
* @param versionRequirements A collection of version requirement strings
*/
public void breaks(String modId, Iterable<String> versionRequirements) {
breaks(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a broken dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the broken dependency
* @param versionRequirement A version requirement string
*/
public void breaks(String modId, String versionRequirement) {
breaks(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a broken dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the broken dependency
* @param action An action to configure the broken dependency further
*/
public void breaks(String modId, Action<Dependency> action) {
breaks(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a broken dependency, and configure it with the given action.
*
* @param action An action to configure the broken dependency
*/
public void breaks(Action<Dependency> action) {
create(Dependency.class, getBreaks(), action);
}
/**
* A list of licenses for the mod.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the mod licenses
*/
@Input
@Optional
public abstract ListProperty<String> getLicenses();
/**
* A list of authors of the mod.
*
* @return A {@link ListProperty} containing a list of {@link Person} representing the mod authors
*/
@Input
@Optional
public abstract ListProperty<Person> getAuthors();
/**
* Add a new author with the given name.
*
* @param name The name of the author
*/
public void author(String name) {
author(name, person -> { });
}
/**
* Add a new author with the given name, and configure it with the given action.
*
* @param name The name of the author
* @param action An action to configure the author further
*/
public void author(String name, Action<Person> action) {
author(person -> {
person.getName().set(name);
action.execute(person);
});
}
/**
* Add a new author, and configure it with the given action.
*
* @param action An action to configure the author
*/
public void author(Action<Person> action) {
create(Person.class, getAuthors(), action);
}
/**
* A list of contributors to the mod.
*
* @return A {@link ListProperty} containing a list of {@link Person} representing the mod contributors
*/
@Input
@Optional
public abstract ListProperty<Person> getContributors();
/**
* Add a new contributor with the given name.
*
* @param name The name of the contributor
*/
public void contributor(String name) {
contributor(name, person -> { });
}
/**
* Add a new contributor with the given name, and configure it with the given action.
*
* @param name The name of the contributor
* @param action An action to configure the contributor further
*/
public void contributor(String name, Action<Person> action) {
contributor(person -> {
person.getName().set(name);
action.execute(person);
});
}
/**
* Add a new contributor, and configure it with the given action.
*
* @param action An action to configure the contributor
*/
public void contributor(Action<Person> action) {
create(Person.class, getContributors(), action);
}
/**
* A map of contact information for the mod.
*
* <p>The key is the platform (e.g. "email", "github", "discord") and the value is the contact detail for that platform.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the mod contact information
*/
@Input
@Optional
public abstract MapProperty<String, String> getContactInformation();
/**
* A list of icons for the mod.
*
* @return A {@link ListProperty} containing a list of {@link Icon} representing the mod icons
*/
@Input
@Optional
public abstract ListProperty<Icon> getIcons();
/**
* Add a new icon with the given path.
*
* <p>Note: Only 1 unsized icon is allowed. If you need to specify multiple icons or sizes, use {@link #icon(int, String)}
*
* @param path The path to the icon file, relative to the root of the mod JAR
*/
public void icon(String path) {
icon(path, icon -> { });
}
/**
* Add a new icon with the given size and path.
*
* @param size The size of the icon in pixels (e.g. 16, 32, 64)
* @param path The path to the icon file, relative to the root of the mod JAR
*/
public void icon(int size, String path) {
icon(path, icon -> icon.getSize().set(size));
}
/**
* Add a new icon with the given path, and configure it with the given action.
*
* @param path The path to the icon file, relative to the root of the mod JAR
* @param action An action to configure the icon further
*/
public void icon(String path, Action<Icon> action) {
icon(icon -> {
icon.getPath().set(path);
action.execute(icon);
});
}
/**
* Add a new icon, and configure it with the given action.
*
* @param action An action to configure the icon
*/
public void icon(Action<Icon> action) {
create(Icon.class, getIcons(), action);
}
/**
* A map of language adapters for the mod.
*
* <p>The key is the adapter name and the value is the fully qualified class name of the adapter.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the mod language adapters
*/
@Input
@Optional
public abstract MapProperty<String, String> getLanguageAdapters();
/**
* A map of custom data for the mod.
*
* <p>This can be used by other tools to store additional information about the mod.
*
* <p>The object is encoded to JSON using Gson, so it can be any type that Gson supports.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link Object} values representing the mod custom data
*/
@Input
@Optional
public abstract MapProperty<String, Object> getCustomData();
public abstract static class Entrypoint {
/**
* The name of the entrypoint, such as "main" or "client".
*
* @return A {@link Property} containing a {@link String} representing the entrypoint name
*/
@Input
public abstract Property<String> getEntrypoint();
/**
* The value of the entrypoint, typically a fully qualified class name.
*
* @return A {@link Property} containing a {@link String} representing the entrypoint value
*/
@Input
public abstract Property<String> getValue();
/**
* The language adapter to use for this entrypoint, if any.
*
* @return A {@link Property} containing a {@link String} representing the entrypoint language adapter
*/
@Input
@Optional
public abstract Property<String> getAdapter();
}
public abstract static class Mixin {
/**
* The value of the Mixin configuration, typically a path to a JSON file.
*
* @return A {@link Property} containing a {@link String} representing the Mixin configuration value
*/
@Input
public abstract Property<String> getValue();
/**
* The environment the Mixin configuration applies to.
*
* <p>One of `client`, `server`, or `*`.
*
* @return A {@link Property} containing a {@link String} representing the Mixin configuration environment
*/
@Input
@Optional
public abstract Property<String> getEnvironment();
}
public abstract static class Dependency {
/**
* The mod ID of the dependency.
*
* @return A {@link Property} containing a {@link String} representing the dependency mod ID
*/
@Input
public abstract Property<String> getModId();
/**
* A list of version requirements for the dependency.
*
* <p>Each version requirement is a string that specifies a version or range of versions.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the dependency version requirements
*/
@Input
@Optional
public abstract ListProperty<String> getVersionRequirements();
}
public abstract static class Person {
/**
* The name of the person.
*
* @return A {@link Property} containing a {@link String} representing the person's name
*/
@Input
public abstract Property<String> getName();
/**
* A map of contact information for the person.
*
* <p>The key is the platform (e.g. "email", "github", "discord") and the value is the contact detail for that platform.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the person's contact information
*/
@Input
@Optional
public abstract MapProperty<String, String> getContactInformation();
}
public abstract static class Icon {
/**
* The path to the icon file, relative to the root of the mod JAR.
*
* @return A {@link Property} containing a {@link String} representing the icon path
*/
@Input
public abstract Property<String> getPath();
/**
* The size of the icon in pixels (e.g. 16, 32, 64).
*
* <p>If not specified, the icon is considered to be "unsized". Only one unsized icon is allowed.
*
* @return A {@link Property} containing an {@link Integer} representing the icon size
*/
@Input
@Optional // Icon is required if there is more than 1 icon specified
public abstract Property<Integer> getSize();
}
// Internal stuff:
@Inject
@ApiStatus.Internal
protected abstract ObjectFactory getObjectFactory();
private <T> void create(Class<T> type, ListProperty<T> list, Action<T> action) {
T item = getObjectFactory().newInstance(type);
action.execute(item);
list.add(item);
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -45,12 +45,12 @@ public abstract class IntermediateMappingsProvider implements Named {
public abstract Property<Function<String, DownloadBuilder>> getDownloader();
/**
* Set to true if the minecraft version is pre 1.3.
* Set to true if the minecraft version is at least Beta 1.0 and pre 1.3.
* When true the expected src namespace is intermediary, and the expected dst namespaces are clientOfficial and/or serverOfficial
* When false the expected src namespace is named and the expected dst namespace is intermediary
*/
@ApiStatus.Experimental
public abstract Property<Boolean> getIsLegacyMinecraft();
public abstract Property<Boolean> getUseSplitOfficialNamespaces();
/**
* Generate or download a tinyv2 mapping file with intermediary and named namespaces.

View File

@@ -71,6 +71,13 @@ public interface FileMappingsSpecBuilder {
*/
FileMappingsSpecBuilder enigmaMappings();
/**
* Marks that the zip file contains annotation data.
*
* @return this builder
*/
FileMappingsSpecBuilder containsAnnotations();
/**
* Marks that the zip file contains unpick data.
*

View File

@@ -32,7 +32,6 @@ import java.util.Comparator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.base.Preconditions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -40,6 +39,7 @@ import org.gradle.api.UncheckedIOException;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import net.fabricmc.loom.util.Check;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.Pair;
@@ -53,7 +53,7 @@ public class JarNester {
return;
}
Preconditions.checkArgument(FabricModJsonFactory.isNestableModJar(modJar, platform), "Cannot nest jars into none mod jar " + modJar.getName());
Check.require(FabricModJsonFactory.isNestableModJar(modJar, platform), "Cannot nest jars into none mod jar " + modJar.getName());
// Ensure deterministic ordering of entries in fabric.mod.json
Collection<File> sortedJars = jars.stream().sorted(Comparator.comparing(File::getName)).toList();
@@ -81,7 +81,7 @@ public class JarNester {
for (File file : sortedJars) {
String nestedJarPath = "META-INF/jars/" + file.getName();
Preconditions.checkArgument(FabricModJsonFactory.isNestableModJar(file, platform), "Cannot nest none mod jar: " + file.getName());
Check.require(FabricModJsonFactory.isNestableModJar(file, platform), "Cannot nest none mod jar: " + file.getName());
for (JsonElement nestedJar : nestedJars) {
JsonObject jsonObject = nestedJar.getAsJsonObject();
@@ -138,7 +138,7 @@ public class JarNester {
return json;
}) : null));
Preconditions.checkState(count > 0, "Failed to transform fabric.mod.json");
Check.require(count > 0, "Failed to transform fabric.mod.json");
} catch (IOException e) {
throw new java.io.UncheckedIOException("Failed to nest jars into " + modJar.getName(), e);
}

View File

@@ -29,6 +29,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -40,7 +41,6 @@ import java.util.regex.Pattern;
import javax.inject.Inject;
import com.google.gson.JsonObject;
import org.apache.commons.io.FileUtils;
import org.gradle.api.artifacts.ArtifactView;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ComponentIdentifier;
@@ -63,6 +63,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.DeletingFileVisitor;
import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.ZipReprocessorUtil;
import net.fabricmc.loom.util.ZipUtils;
@@ -104,7 +105,7 @@ public abstract class NestableJarGenerationTask extends AbstractLoomTask {
try {
File targetDir = getOutputDirectory().get().getAsFile();
FileUtils.deleteDirectory(targetDir);
DeletingFileVisitor.deleteDirectory(targetDir.toPath());
targetDir.mkdirs();
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -239,7 +240,7 @@ public abstract class NestableJarGenerationTask extends AbstractLoomTask {
private void makeNestableJar(final File input, final File output, final @Nullable String modJsonFile, final @Nullable String nestingMetadata) {
try {
FileUtils.copyFile(input, output);
Files.copy(input.toPath(), output.toPath());
} catch (IOException e) {
throw new UncheckedIOException("Failed to copy mod file %s".formatted(input), e);
}

View File

@@ -33,6 +33,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -97,6 +98,8 @@ import net.fabricmc.loom.util.service.ScopedServiceFactory;
import net.fabricmc.loom.util.service.ServiceFactory;
public abstract class CompileConfiguration implements Runnable {
private static final String LOCK_PROPERTY_KEY = "fabric.loom.internal.global.lock";
@Inject
protected abstract Project getProject();
@@ -127,7 +130,11 @@ public abstract class CompileConfiguration implements Runnable {
}
try {
setupMinecraft(configContext);
// Setting up loom across Gradle projects is not thread safe, synchronize it here to ensure that multiple projects cannot use it.
// There is no easy way around this, as we want to use the same global cache for downloaded or generated files.
synchronized (getGlobalLockObject()) {
setupMinecraft(configContext);
}
LoomDependencyManager dependencyManager = new LoomDependencyManager();
extension.setDependencyManager(dependencyManager);
@@ -195,8 +202,7 @@ public abstract class CompileConfiguration implements Runnable {
}
}
// This is not thread safe across getProject()s synchronize it here just to be sure, might be possible to move this further down, but for now this will do.
private synchronized void setupMinecraft(ConfigContext configContext) throws Exception {
private void setupMinecraft(ConfigContext configContext) throws Exception {
final Project project = configContext.project();
final LoomGradleExtension extension = configContext.extension();
@@ -571,4 +577,18 @@ public abstract class CompileConfiguration implements Runnable {
}
});
}
// This is a nasty piece of work, but seems to work quite nicely.
// We need a lock that works across classloaders, a regular synchronized method will not work here.
// We can abuse system properties as a shared object store that we know for sure will be on the same classloader regardless of what Gradle does to loom.
// This allows us to ensure that all instances of loom regardless of classloader get the same object to lock on.
private static Object getGlobalLockObject() {
if (!System.getProperties().contains(LOCK_PROPERTY_KEY)) {
// The .intern resolves a possible race where two difference value objects (remember not the same classloader) are set.
//noinspection StringOperationCanBeSimplified
System.getProperties().setProperty(LOCK_PROPERTY_KEY, LOCK_PROPERTY_KEY.intern());
}
return Objects.requireNonNull(System.getProperty(LOCK_PROPERTY_KEY));
}
}

View File

@@ -35,10 +35,8 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.collect.Iterables;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.commons.io.FilenameUtils;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
@@ -64,12 +62,12 @@ public class FileDependencyInfo extends DependencyInfo {
case 0 -> //Don't think Gradle would ever let you do this
throw new IllegalStateException("Empty dependency?");
case 1 -> //Single file dependency
classifierToFile.put("", Iterables.getOnlyElement(files));
classifierToFile.put("", getOnlyElement(files));
default -> { //File collection, try work out the classifiers
List<File> sortedFiles = files.stream().sorted(Comparator.comparing(File::getName, Comparator.comparingInt(String::length))).collect(Collectors.toList());
//First element in sortedFiles is the one with the shortest name, we presume all the others are different classifier types of this
File shortest = sortedFiles.remove(0);
String shortestName = FilenameUtils.removeExtension(shortest.getName()); //name.jar -> name
File shortest = sortedFiles.removeFirst();
String shortestName = removeExtension(shortest); //name.jar -> name
for (File file : sortedFiles) {
if (!file.getName().startsWith(shortestName)) {
@@ -84,7 +82,7 @@ public class FileDependencyInfo extends DependencyInfo {
for (File file : sortedFiles) {
//Now we just have to work out what classifier type the other files are, this shouldn't even return an empty string
String classifier = FilenameUtils.removeExtension(file.getName()).substring(start);
String classifier = removeExtension(file).substring(start);
//The classifier could well be separated with a dash (thing name.jar and name-sources.jar), we don't want that leading dash
if (classifierToFile.put(classifier.charAt(0) == '-' ? classifier.substring(1) : classifier, file) != null) {
@@ -104,7 +102,7 @@ public class FileDependencyInfo extends DependencyInfo {
byte[] modJson;
try {
if ("jar".equals(FilenameUtils.getExtension(root.getName())) && (modJson = ZipUtils.unpackNullable(root.toPath(), "fabric.mod.json")) != null) {
if ("jar".equals(getExtension(root)) && (modJson = ZipUtils.unpackNullable(root.toPath(), "fabric.mod.json")) != null) {
//It's a Fabric mod, see how much we can extract out
JsonObject json = new Gson().fromJson(new String(modJson, StandardCharsets.UTF_8), JsonObject.class);
@@ -142,7 +140,7 @@ public class FileDependencyInfo extends DependencyInfo {
version = loader.get("version").getAsString();
} else {
//Not a Fabric mod, just have to make something up
name = FilenameUtils.removeExtension(root.getName());
name = removeExtension(root);
version = "1.0";
}
} catch (IOException e) {
@@ -171,4 +169,36 @@ public class FileDependencyInfo extends DependencyInfo {
public Set<File> resolve() {
return this.resolvedFiles;
}
private static <T> T getOnlyElement(Set<T> set) {
if (set.size() != 1) {
throw new IllegalArgumentException("Expected exactly one element but got " + set.size());
}
return set.iterator().next();
}
private static String removeExtension(File file) {
String filename = file.getName();
int lastDot = filename.lastIndexOf('.');
int lastSeparator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
if (lastDot > lastSeparator) {
return filename.substring(0, lastDot);
}
return filename;
}
private static String getExtension(File file) {
String filename = file.getName();
int lastDot = filename.lastIndexOf('.');
int lastSeparator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
if (lastDot > lastSeparator && lastDot != filename.length() - 1) {
return filename.substring(lastDot + 1);
}
return "";
}
}

View File

@@ -35,7 +35,9 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.gradle.api.Project;
@@ -72,6 +74,7 @@ import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.SourceRemapper;
import net.fabricmc.loom.util.AsyncCache;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
import net.fabricmc.loom.util.service.ServiceFactory;
@@ -153,26 +156,19 @@ public class ModConfigurationRemapper {
// the installer data. The installer data has to be added before
// any mods are remapped since remapping needs the dependencies provided by that data.
final Map<Configuration, List<ModDependency>> dependenciesBySourceConfig = new HashMap<>();
final Map<ArtifactRef, ArtifactMetadata> metaCache = new HashMap<>();
AsyncCache<ArtifactMetadata> metaCache = new AsyncCache<>();
configsToRemap.forEach((sourceConfig, remappedConfig) -> {
/*
sourceConfig - The source configuration where the intermediary named artifacts come from. i.e "modApi"
remappedConfig - The target configuration where the remapped artifacts go
*/
final Configuration clientRemappedConfig = clientConfigsToRemap.get(sourceConfig);
List<ArtifactRef> artifactRefs = resolveArtifacts(project, sourceConfig);
Map<ArtifactRef, ArtifactMetadata> metadataMap = getMetadata(artifactRefs, metaCache);
final List<ModDependency> modDependencies = new ArrayList<>();
for (ArtifactRef artifact : resolveArtifacts(project, sourceConfig)) {
final ArtifactMetadata artifactMetadata;
artifactMetadata = metaCache.computeIfAbsent(artifact, a -> {
try {
return ArtifactMetadata.create(project, a, LoomGradlePlugin.LOOM_VERSION, extension.getPlatform().get(),
extension.isForgeLike() && extension.getForgeProvider().usesMojangAtRuntime() ? true : null);
} catch (IOException e) {
throw ExceptionUtil.createDescriptiveWrapper(UncheckedIOException::new, "Failed to read metadata from " + a.path(), e);
}
});
for (ArtifactRef artifact : artifactRefs) {
final ArtifactMetadata artifactMetadata = Objects.requireNonNull(metadataMap.get(artifact), "Failed to find metadata for artifact");
if (artifactMetadata.installerData() != null) {
if (extension.getInstallerData() != null) {
@@ -236,6 +232,24 @@ public class ModConfigurationRemapper {
});
}
private static Map<ArtifactRef, ArtifactMetadata> getMetadata(List<ArtifactRef> artifacts, AsyncCache<ArtifactMetadata> cache) {
var futures = new HashMap<ArtifactRef, CompletableFuture<ArtifactMetadata>>();
for (ArtifactRef artifact : artifacts) {
CompletableFuture<ArtifactMetadata> future = cache.get(artifact, () -> {
try {
return ArtifactMetadata.create(artifact, LoomGradlePlugin.LOOM_VERSION);
} catch (IOException e) {
throw ExceptionUtil.createDescriptiveWrapper(UncheckedIOException::new, "Failed to read metadata from " + artifact.path(), e);
}
});
futures.put(artifact, future);
}
return AsyncCache.joinMap(futures);
}
private static void createConstraints(ArtifactRef artifact, Configuration targetConfig, Configuration sourceConfig, DependencyHandler dependencies) {
if (true) {
// Disabled due to the gradle module metadata causing issues. Try the MavenProject test to reproduce issue.
@@ -261,10 +275,10 @@ public class ModConfigurationRemapper {
final List<ArtifactRef> artifacts = new ArrayList<>();
final Set<ResolvedArtifact> resolvedArtifacts = configuration.getResolvedConfiguration().getResolvedArtifacts();
downloadAllSources(project, resolvedArtifacts);
Map<ResolvedArtifact, Path> sourcesMap = downloadAllSources(project, resolvedArtifacts);
for (ResolvedArtifact artifact : resolvedArtifacts) {
final Path sources = findSources(project, artifact);
@Nullable Path sources = sourcesMap.get(artifact);
artifacts.add(new ArtifactRef.ResolvedArtifactRef(artifact, sources));
}
@@ -290,9 +304,9 @@ public class ModConfigurationRemapper {
return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
}
private static void downloadAllSources(Project project, Set<ResolvedArtifact> resolvedArtifacts) {
private static Map<ResolvedArtifact, Path> downloadAllSources(Project project, Set<ResolvedArtifact> resolvedArtifacts) {
if (isCIBuild()) {
return;
return Map.of();
}
final DependencyHandler dependencies = project.getDependencies();
@@ -308,26 +322,28 @@ public class ModConfigurationRemapper {
.withArtifacts(JvmLibrary.class, SourcesArtifact.class);
// Run a single query for all of the artifacts, this will allow them to be resolved in parallel before they are queried individually
query.execute();
}
Set<ComponentArtifactsResult> resolvedSources = query.execute().getResolvedComponents();
Map<ResolvedArtifact, Path> sources = new HashMap<>();
@Nullable
public static Path findSources(Project project, ResolvedArtifact artifact) {
if (isCIBuild()) {
return null;
for (ResolvedArtifact resolvedArtifact : resolvedArtifacts) {
for (ComponentArtifactsResult sourceArtifact : resolvedSources) {
if (sourceArtifact.getId().equals(resolvedArtifact.getId().getComponentIdentifier())) {
Path sourcesPath = getSourcesPath(sourceArtifact);
if (sourcesPath != null) {
sources.put(resolvedArtifact, sourcesPath);
}
}
}
}
final DependencyHandler dependencies = project.getDependencies();
return sources;
}
@SuppressWarnings("unchecked") ArtifactResolutionQuery query = dependencies.createArtifactResolutionQuery()
.forComponents(artifact.getId().getComponentIdentifier())
.withArtifacts(JvmLibrary.class, SourcesArtifact.class);
for (ComponentArtifactsResult result : query.execute().getResolvedComponents()) {
for (ArtifactResult srcArtifact : result.getArtifacts(SourcesArtifact.class)) {
if (srcArtifact instanceof ResolvedArtifactResult) {
return ((ResolvedArtifactResult) srcArtifact).getFile().toPath();
}
private static Path getSourcesPath(ComponentArtifactsResult sourceArtifact) {
for (ArtifactResult srcArtifact : sourceArtifact.getArtifacts(SourcesArtifact.class)) {
if (srcArtifact instanceof ResolvedArtifactResult) {
return ((ResolvedArtifactResult) srcArtifact).getFile().toPath();
}
}

View File

@@ -29,11 +29,10 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -49,6 +48,7 @@ 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.AsyncCache;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
@@ -65,7 +65,7 @@ public record SpecContextImpl(
List<FabricModJson> localMods,
List<ModHolder> compileRuntimeMods) implements SpecContext {
public static SpecContextImpl create(Project project) {
final Map<String, List<FabricModJson>> fmjCache = new HashMap<>();
AsyncCache<List<FabricModJson>> fmjCache = new AsyncCache<List<FabricModJson>>();
return new SpecContextImpl(
getDependentMods(project, fmjCache),
FabricModJsonHelpers.getModsInProject(project),
@@ -74,23 +74,19 @@ public record SpecContextImpl(
}
// Reruns a list of mods found on both the compile and/or runtime classpaths
private static List<FabricModJson> getDependentMods(Project project, Map<String, List<FabricModJson>> fmjCache) {
private static List<FabricModJson> getDependentMods(Project project, AsyncCache<List<FabricModJson>> fmjCache) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
var mods = new ArrayList<FabricModJson>();
var futures = new ArrayList<CompletableFuture<List<FabricModJson>>>();
for (RemapConfigurationSettings entry : extension.getRemapConfigurations()) {
final Set<File> artifacts = entry.getSourceConfiguration().get().resolve();
for (File artifact : artifacts) {
final List<FabricModJson> fabricModJson = fmjCache.computeIfAbsent(artifact.toPath().toAbsolutePath().toString(), $ -> {
futures.add(fmjCache.get(artifact.toPath().toAbsolutePath().toString(), () -> {
return FabricModJsonFactory.createFromZipOptional(artifact.toPath())
.map(List::of)
.orElseGet(List::of);
});
if (!fabricModJson.isEmpty()) {
mods.add(fabricModJson.get(0));
}
}));
}
}
@@ -98,13 +94,11 @@ public record SpecContextImpl(
if (!extension.isProjectIsolationActive() && !GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS)) {
// Add all the dependent projects
for (Project dependentProject : getDependentProjects(project).toList()) {
mods.addAll(fmjCache.computeIfAbsent(dependentProject.getPath(), $ -> {
return FabricModJsonHelpers.getModsInProject(dependentProject);
}));
futures.add(fmjCache.get(dependentProject.getPath(), () -> FabricModJsonHelpers.getModsInProject(dependentProject)));
}
}
return sorted(mods);
return sorted(AsyncCache.joinList(futures));
}
private static Stream<Project> getDependentProjects(Project project) {
@@ -116,11 +110,11 @@ public record SpecContextImpl(
}
// Returns a list of mods that are on both to compile and runtime classpath
private static List<ModHolder> getCompileRuntimeMods(Project project, Map<String, List<FabricModJson>> fmjCache) {
private static List<ModHolder> getCompileRuntimeMods(Project project, AsyncCache<List<FabricModJson>> fmjCache) {
var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(project, fmjCache));
for (Project dependentProject : getCompileRuntimeProjectDependencies(project).toList()) {
List<FabricModJson> projectMods = fmjCache.computeIfAbsent(dependentProject.getPath(), $ -> {
List<FabricModJson> projectMods = fmjCache.getBlocking(dependentProject.getPath(), () -> {
return FabricModJsonHelpers.getModsInProject(dependentProject);
});
@@ -133,7 +127,7 @@ public record SpecContextImpl(
}
// Returns a list of jar mods that are found on the compile and runtime remapping configurations
private static List<ModHolder> getCompileRuntimeModsFromRemapConfigs(Project project, Map<String, List<FabricModJson>> fmjCache) {
private static List<ModHolder> getCompileRuntimeModsFromRemapConfigs(Project project, AsyncCache<List<FabricModJson>> fmjCache) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
// A set of mod ids from all remap configurations that are considered for dependency transforms.
@@ -168,26 +162,26 @@ public record SpecContextImpl(
.toList();
}
private static Stream<FabricModJson> getMods(Project project, Map<String, List<FabricModJson>> fmjCache, Stream<RemapConfigurationSettings> stream) {
private static Stream<FabricModJson> getMods(Project project, AsyncCache<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) {
private static Set<String> getModIds(Project project, AsyncCache<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) {
private static Function<Path, @Nullable FabricModJson> modFromZip(AsyncCache<List<FabricModJson>> fmjCache) {
return zipPath -> {
final List<FabricModJson> list = fmjCache.computeIfAbsent(zipPath.toAbsolutePath().toString(), $ -> {
final List<FabricModJson> list = fmjCache.getBlocking(zipPath.toAbsolutePath().toString(), () -> {
return FabricModJsonFactory.createFromZipOptional(zipPath)
.map(List::of)
.orElseGet(List::of);
});
return list.isEmpty() ? null : list.get(0);
return list.isEmpty() ? null : list.getFirst();
};
}
@@ -204,6 +198,14 @@ public record SpecContextImpl(
// Returns a list of Loom Projects found in both the runtime and compile classpath
private static Stream<Project> getCompileRuntimeProjectDependencies(Project project) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
// TODO provide a project isolated way of doing this.
if (extension.isProjectIsolationActive()
|| GradleUtils.getBooleanProperty(project, Constants.Properties.DISABLE_PROJECT_DEPENDENT_MODS)) {
return Stream.empty();
}
final Stream<Project> runtimeProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
final List<Project> compileProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)).toList();

View File

@@ -25,13 +25,14 @@
package net.fabricmc.loom.configuration.providers.mappings;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import javax.inject.Inject;
import com.google.common.net.UrlEscapers;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
@@ -70,7 +71,7 @@ public abstract class IntermediaryMappingsProvider extends IntermediateMappingsP
// Download and extract intermediary
final Path intermediaryJarPath = Files.createTempFile(getName(), ".jar");
final String encodedMcVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftVersion().get());
final String encodedMcVersion = URLEncoder.encode(getMinecraftVersion().get(), StandardCharsets.UTF_8);
final String urlRaw = getIntermediaryUrl().get();
if (project != null && urlRaw.equals(LoomGradleExtensionApiImpl.DEFAULT_INTERMEDIARY_URL)) {
@@ -108,7 +109,7 @@ public abstract class IntermediaryMappingsProvider extends IntermediateMappingsP
@Override
public @NotNull String getName() {
final String encodedMcVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftVersion().get());
final String encodedMcVersion = URLEncoder.encode(getMinecraftVersion().get(), StandardCharsets.UTF_8);
final String urlRaw = getIntermediaryUrl().get();
if (!LoomGradleExtensionApiImpl.DEFAULT_INTERMEDIARY_URL.equals(urlRaw)) {

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -33,7 +33,6 @@ import java.nio.file.Path;
import java.util.Collections;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
@@ -48,6 +47,7 @@ import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.Lazy;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
@@ -68,7 +68,7 @@ public final class IntermediateMappingsService extends Service<IntermediateMappi
Property<String> getMinecraftVersion();
}
private final Supplier<MemoryMappingTree> memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree);
private final Supplier<MemoryMappingTree> memoryMappingTree = Lazy.of(this::createMemoryMappingTree);
public IntermediateMappingsService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);
@@ -103,9 +103,9 @@ public final class IntermediateMappingsService extends Service<IntermediateMappi
final IntermediateMappingsProvider intermediateProvider = extension.getIntermediateMappingsProvider();
// When merging legacy versions there will be multiple named namespaces, so use intermediary as the common src ns
// Newer versions will use intermediary as the src ns
final String expectedSrcNs = minecraftProvider.isLegacyVersion()
? MappingsNamespace.INTERMEDIARY.toString() // <1.3
: MappingsNamespace.OFFICIAL.toString(); // >=1.3
final String expectedSrcNs = minecraftProvider.isLegacySplitOfficialNamespaceVersion()
? MappingsNamespace.INTERMEDIARY.toString() // >=beta 1.0 and <1.3
: MappingsNamespace.OFFICIAL.toString(); // >=1.3 or <b1.0
return TYPE.create(project, options -> {
options.getIntermediaryTiny().set(intermediaryTiny.toFile());

View File

@@ -46,6 +46,8 @@ import net.fabricmc.loom.api.mappings.layered.MappingLayer;
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.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsLayer;
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;
@@ -100,6 +102,7 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) {
Files.deleteIfExists(mappingsZip);
writeMapping(processor, layers, mappingsZip);
writeAnnotationData(processor, layers, mappingsZip);
writeSignatureFixes(processor, layers, mappingsZip);
writeUnpickData(processor, layers, mappingsZip);
@@ -130,6 +133,18 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) {
}
}
private void writeAnnotationData(LayeredMappingsProcessor processor, List<MappingLayer> layers, Path mappingsFile) throws IOException {
List<AnnotationsData> annotationsData = processor.getAnnotationsData(layers);
if (annotationsData.isEmpty()) {
return;
}
byte[] data = AnnotationsData.GSON.toJson(AnnotationsData.listToJson(annotationsData)).getBytes(StandardCharsets.UTF_8);
ZipUtils.add(mappingsFile, AnnotationsLayer.ANNOTATIONS_PATH, data);
}
private void writeSignatureFixes(LayeredMappingsProcessor processor, List<MappingLayer> layers, Path mappingsFile) throws IOException {
Map<String, String> signatureFixes = processor.getSignatureFixes(layers);

View File

@@ -38,6 +38,8 @@ import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.api.mappings.layered.MappingLayer;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsLayer;
import net.fabricmc.loom.configuration.providers.mappings.extras.signatures.SignatureFixesLayer;
import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer;
import net.fabricmc.mappingio.adapter.MappingNsCompleter;
@@ -117,6 +119,22 @@ public class LayeredMappingsProcessor {
return mappingTree;
}
public List<AnnotationsData> getAnnotationsData(List<MappingLayer> layers) throws IOException {
List<AnnotationsData> result = new ArrayList<>();
for (MappingLayer layer : layers) {
if (layer instanceof AnnotationsLayer annotationsLayer) {
AnnotationsData annotationsData = annotationsLayer.getAnnotationsData();
if (annotationsData != null) {
result.add(annotationsData);
}
}
}
return result;
}
@Nullable
public Map<String, String> getSignatureFixes(List<MappingLayer> layers) {
Map<String, String> signatureFixes = new HashMap<>();

View File

@@ -62,6 +62,8 @@ import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.configuration.DependencyInfo;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsLayer;
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;
@@ -101,6 +103,7 @@ public class MappingConfiguration {
private final Map<MappingOption, Supplier<Path>> mappingOptions;
private final Path unpickDefinitions;
private List<AnnotationsData> annotationsData = List.of();
@Nullable
private UnpickMetadata unpickMetadata;
private Map<String, String> signatureFixes;
@@ -439,10 +442,23 @@ public class MappingConfiguration {
}
private void extractExtras(FileSystem jar) throws IOException {
extractAnnotationsData(jar);
extractUnpickDefinitions(jar);
extractSignatureFixes(jar);
}
private void extractAnnotationsData(FileSystem jar) throws IOException {
Path annotationsPath = jar.getPath(AnnotationsLayer.ANNOTATIONS_PATH);
if (!Files.exists(annotationsPath)) {
return;
}
try (BufferedReader reader = Files.newBufferedReader(annotationsPath, StandardCharsets.UTF_8)) {
annotationsData = AnnotationsData.readList(reader);
}
}
private void extractUnpickDefinitions(FileSystem jar) throws IOException {
Path unpickPath = jar.getPath(UnpickMetadata.UNPICK_DEFINITIONS_PATH);
Path unpickMetadataPath = jar.getPath(UnpickMetadata.UNPICK_METADATA_PATH);
@@ -525,6 +541,10 @@ public class MappingConfiguration {
return unpickMetadata != null;
}
public List<AnnotationsData> getAnnotationsData() {
return annotationsData;
}
public UnpickMetadata getUnpickMetadata() {
return Objects.requireNonNull(unpickMetadata, "Unpick metadata is not available");
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -42,7 +42,7 @@ public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappi
@Override
public void provide(Path tinyMappings) throws IOException {
Files.writeString(tinyMappings, getIsLegacyMinecraft().get() ? HEADER_OFFICIAL_LEGACY_MERGED : HEADER_OFFICIAL_MERGED, StandardCharsets.UTF_8);
Files.writeString(tinyMappings, getUseSplitOfficialNamespaces().get() ? HEADER_OFFICIAL_LEGACY_MERGED : HEADER_OFFICIAL_MERGED, StandardCharsets.UTF_8);
}
@Override

View File

@@ -29,7 +29,6 @@ import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
@@ -41,6 +40,7 @@ import org.gradle.api.tasks.Optional;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.util.FileSystemUtil;
import net.fabricmc.loom.util.Lazy;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
@@ -80,7 +80,7 @@ public final class TinyMappingsService extends Service<TinyMappingsService.Optio
super(options, serviceFactory);
}
private final Supplier<MemoryMappingTree> mappingTree = Suppliers.memoize(() -> {
private final Supplier<MemoryMappingTree> mappingTree = Lazy.of(() -> {
Path mappings = getOptions().getMappings().getSingleFile().toPath();
if (getOptions().getZipEntryPath().isPresent()) {

View File

@@ -0,0 +1,187 @@
/*
* 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.extras.annotations;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.tree.AnnotationNode;
class AnnotationNodeSerializer implements JsonSerializer<AnnotationNode>, JsonDeserializer<AnnotationNode> {
@Override
public AnnotationNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String desc = jsonObject.getAsJsonPrimitive("desc").getAsString();
AnnotationNode annotation = new AnnotationNode(desc);
JsonObject values = jsonObject.getAsJsonObject("values");
if (values != null) {
for (Map.Entry<String, JsonElement> entry : values.entrySet()) {
deserializeAnnotationValue(annotation, entry.getKey(), entry.getValue(), context);
}
}
return annotation;
}
private static void deserializeAnnotationValue(AnnotationVisitor visitor, @Nullable String name, JsonElement value, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = value.getAsJsonObject();
switch (obj.getAsJsonPrimitive("type").getAsString()) {
case "byte" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsByte());
case "boolean" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsBoolean());
case "char" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsString().charAt(0));
case "short" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsShort());
case "int" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsInt());
case "long" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsLong());
case "float" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsFloat());
case "double" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsDouble());
case "string" -> visitor.visit(name, obj.getAsJsonPrimitive("value").getAsString());
case "class" ->
visitor.visit(name, org.objectweb.asm.Type.getType(obj.getAsJsonPrimitive("value").getAsString()));
case "enum_constant" ->
visitor.visitEnum(name, obj.getAsJsonPrimitive("owner").getAsString(), obj.getAsJsonPrimitive("name").getAsString());
case "annotation" -> {
AnnotationNode annotation = context.deserialize(obj, AnnotationNode.class);
AnnotationVisitor av = visitor.visitAnnotation(name, annotation.desc);
if (av != null) {
annotation.accept(av);
}
}
case "array" -> {
AnnotationVisitor av = visitor.visitArray(name);
if (av != null) {
for (JsonElement element : obj.getAsJsonArray("value")) {
deserializeAnnotationValue(av, null, element, context);
}
av.visitEnd();
}
}
}
}
@Override
public JsonElement serialize(AnnotationNode src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
json.addProperty("desc", src.desc);
if (src.values != null && !src.values.isEmpty()) {
JsonObject values = new JsonObject();
for (int i = 0; i < src.values.size() - 1; i += 2) {
String name = String.valueOf(src.values.get(i));
Object value = src.values.get(i + 1);
values.add(name, serializeAnnotationValue(value, context));
}
json.add("values", values);
}
return json;
}
private static JsonObject serializeAnnotationValue(Object value, JsonSerializationContext context) {
JsonObject json = new JsonObject();
switch (value) {
case Byte b -> {
json.addProperty("type", "byte");
json.addProperty("value", b);
}
case Boolean b -> {
json.addProperty("type", "boolean");
json.addProperty("value", b);
}
case Character c -> {
json.addProperty("type", "char");
json.addProperty("value", c);
}
case Short s -> {
json.addProperty("type", "short");
json.addProperty("value", s);
}
case Integer i -> {
json.addProperty("type", "int");
json.addProperty("value", i);
}
case Long l -> {
json.addProperty("type", "long");
json.addProperty("value", l);
}
case Float f -> {
json.addProperty("type", "float");
json.addProperty("value", f);
}
case Double d -> {
json.addProperty("type", "double");
json.addProperty("value", d);
}
case String str -> {
json.addProperty("type", "string");
json.addProperty("value", str);
}
case org.objectweb.asm.Type type -> {
json.addProperty("type", "class");
json.addProperty("value", type.getDescriptor());
}
case String[] enumConstant -> {
json.addProperty("type", "enum_constant");
json.addProperty("owner", enumConstant[0]);
json.addProperty("name", enumConstant[1]);
}
case AnnotationNode annotation -> {
json.addProperty("type", "annotation");
JsonObject annJson = context.serialize(annotation).getAsJsonObject();
json.asMap().putAll(annJson.asMap());
}
case List<?> list -> {
json.addProperty("type", "array");
JsonArray array = new JsonArray(list.size());
for (Object o : list) {
array.add(serializeAnnotationValue(o, context));
}
json.add("value", array);
}
default -> throw new IllegalArgumentException("Unknown annotation value type: " + value);
}
return json;
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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.extras.annotations;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import org.gradle.api.Project;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.util.TinyRemapperHelper;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.tinyremapper.TinyRemapper;
public record AnnotationsData(Map<String, ClassAnnotationData> classes, String namespace) {
public static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.enableComplexMapKeySerialization()
.registerTypeAdapter(TypeAnnotationNode.class, new TypeAnnotationNodeSerializer())
.registerTypeAdapter(AnnotationNode.class, new AnnotationNodeSerializer())
.registerTypeAdapterFactory(new SkipEmptyTypeAdapterFactory())
.create();
private static final Type LIST_TYPE = new TypeToken<List<AnnotationNode>>() { }.getType();
private static final int CURRENT_VERSION = 1;
public AnnotationsData {
if (namespace == null) {
namespace = MappingsNamespace.NAMED.toString();
}
}
public static AnnotationsData read(Reader reader) {
JsonObject json = GSON.fromJson(reader, JsonObject.class);
checkVersion(json);
return GSON.fromJson(json, AnnotationsData.class);
}
public static List<AnnotationsData> readList(Reader reader) {
JsonObject json = GSON.fromJson(reader, JsonObject.class);
checkVersion(json);
JsonElement values = json.get("values");
if (values == null || values.isJsonNull()) {
return List.of(GSON.fromJson(json, AnnotationsData.class));
}
return GSON.fromJson(values, LIST_TYPE);
}
private static void checkVersion(JsonObject json) {
if (!json.has("version")) {
throw new JsonSyntaxException("Missing annotations version");
}
int version = json.getAsJsonPrimitive("version").getAsInt();
if (version != CURRENT_VERSION) {
throw new JsonSyntaxException("Invalid annotations version " + version + ". Try updating loom");
}
}
public JsonObject toJson() {
JsonObject json = GSON.toJsonTree(this).getAsJsonObject();
JsonObject result = new JsonObject();
result.addProperty("version", CURRENT_VERSION);
result.asMap().putAll(json.asMap());
return result;
}
public static JsonObject listToJson(List<AnnotationsData> annotationsData) {
if (annotationsData.size() == 1) {
return annotationsData.getFirst().toJson();
}
JsonObject result = new JsonObject();
result.addProperty("version", CURRENT_VERSION);
result.add("values", GSON.toJsonTree(annotationsData));
return result;
}
public AnnotationsData merge(AnnotationsData other) {
if (!namespace.equals(other.namespace)) {
throw new IllegalArgumentException("Cannot merge annotations from namespace " + other.namespace + " into annotations from namespace " + this.namespace);
}
Map<String, ClassAnnotationData> newClassData = new LinkedHashMap<>(classes);
other.classes.forEach((key, value) -> newClassData.merge(key, value, ClassAnnotationData::merge));
return new AnnotationsData(newClassData, namespace);
}
public AnnotationsData remap(TinyRemapper remapper, String newNamespace) {
return new AnnotationsData(
remapMap(
classes,
entry -> remapper.getEnvironment().getRemapper().map(entry.getKey()),
entry -> entry.getValue().remap(entry.getKey(), remapper)
),
newNamespace
);
}
static AnnotationNode remap(AnnotationNode node, TinyRemapper remapper) {
AnnotationNode remapped = new AnnotationNode(remapper.getEnvironment().getRemapper().mapDesc(node.desc));
node.accept(remapper.createAnnotationRemapperVisitor(remapped, node.desc));
return remapped;
}
static TypeAnnotationNode remap(TypeAnnotationNode node, TinyRemapper remapper) {
TypeAnnotationNode remapped = new TypeAnnotationNode(node.typeRef, node.typePath, remapper.getEnvironment().getRemapper().mapDesc(node.desc));
node.accept(remapper.createAnnotationRemapperVisitor(remapped, node.desc));
return remapped;
}
static <K, V> Map<K, V> remapMap(Map<K, V> map, Function<Map.Entry<K, V>, K> keyRemapper, Function<Map.Entry<K, V>, V> valueRemapper) {
Map<K, V> result = LinkedHashMap.newLinkedHashMap(map.size());
for (Map.Entry<K, V> entry : map.entrySet()) {
if (result.put(keyRemapper.apply(entry), valueRemapper.apply(entry)) != null) {
throw new IllegalStateException("Remapping annotations resulted in duplicate key: " + keyRemapper.apply(entry));
}
}
return result;
}
@Nullable
public static AnnotationsData getRemappedAnnotations(MappingsNamespace targetNamespace, MappingConfiguration mappingConfiguration, Project project, ServiceFactory serviceFactory, String newNamespace) throws IOException {
List<AnnotationsData> datas = mappingConfiguration.getAnnotationsData();
if (datas.isEmpty()) {
return null;
}
Map<String, TinyRemapper> existingRemappers = new HashMap<>();
AnnotationsData result = datas.getFirst().remap(targetNamespace, project, serviceFactory, newNamespace, existingRemappers);
for (int i = 1; i < datas.size(); i++) {
result = result.merge(datas.get(i).remap(targetNamespace, project, serviceFactory, newNamespace, existingRemappers));
}
return result;
}
private AnnotationsData remap(MappingsNamespace targetNamespace, Project project, ServiceFactory serviceFactory, String newNamespace, Map<String, TinyRemapper> existingRemappers) throws IOException {
if (namespace.equals(targetNamespace.toString())) {
return this;
}
TinyRemapper remapper = existingRemappers.get(namespace);
if (remapper == null) {
remapper = TinyRemapperHelper.getTinyRemapper(project, serviceFactory, namespace, newNamespace);
existingRemappers.put(namespace, remapper);
}
return remap(remapper, newNamespace);
}
}

View File

@@ -0,0 +1,38 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.providers.mappings.extras.annotations;
import java.io.IOException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
@ApiStatus.Experimental
public interface AnnotationsLayer {
String ANNOTATIONS_PATH = "extras/annotations.json";
@Nullable
AnnotationsData getAnnotationsData() throws IOException;
}

View File

@@ -0,0 +1,161 @@
/*
* 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.extras.annotations;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.api.TrRemapper;
public record ClassAnnotationData(
@SerializedName("remove")
Set<String> annotationsToRemove,
@SerializedName("add")
List<AnnotationNode> annotationsToAdd,
@SerializedName("type_remove")
Set<TypeAnnotationKey> typeAnnotationsToRemove,
@SerializedName("type_add")
List<TypeAnnotationNode> typeAnnotationsToAdd,
Map<String, GenericAnnotationData> fields,
Map<String, MethodAnnotationData> methods
) {
public ClassAnnotationData {
if (annotationsToRemove == null) {
annotationsToRemove = new LinkedHashSet<>();
}
if (annotationsToAdd == null) {
annotationsToAdd = new ArrayList<>();
}
if (typeAnnotationsToRemove == null) {
typeAnnotationsToRemove = new LinkedHashSet<>();
}
if (typeAnnotationsToAdd == null) {
typeAnnotationsToAdd = new ArrayList<>();
}
if (fields == null) {
fields = new LinkedHashMap<>();
}
if (methods == null) {
methods = new LinkedHashMap<>();
}
}
ClassAnnotationData merge(ClassAnnotationData other) {
Set<String> newAnnotationsToRemove = new LinkedHashSet<>(annotationsToRemove);
newAnnotationsToRemove.addAll(other.annotationsToRemove);
List<AnnotationNode> newAnnotationsToAdd = new ArrayList<>(annotationsToAdd);
newAnnotationsToAdd.addAll(other.annotationsToAdd);
Set<TypeAnnotationKey> newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove);
newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove);
List<TypeAnnotationNode> newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd);
Map<String, GenericAnnotationData> newFields = new LinkedHashMap<>(fields);
other.fields.forEach((key, value) -> newFields.merge(key, value, GenericAnnotationData::merge));
Map<String, MethodAnnotationData> newMethods = new LinkedHashMap<>(methods);
other.methods.forEach((key, value) -> newMethods.merge(key, value, MethodAnnotationData::merge));
return new ClassAnnotationData(newAnnotationsToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd, newFields, newMethods);
}
ClassAnnotationData remap(String className, TinyRemapper remapper) {
return new ClassAnnotationData(
annotationsToRemove.stream().map(remapper.getEnvironment().getRemapper()::map).collect(Collectors.toCollection(LinkedHashSet::new)),
annotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new)),
typeAnnotationsToRemove.stream().map(key -> key.remap(remapper)).collect(Collectors.toCollection(LinkedHashSet::new)),
typeAnnotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new)),
AnnotationsData.remapMap(
fields,
entry -> remapField(className, entry.getKey(), remapper),
entry -> entry.getValue().remap(remapper)
),
AnnotationsData.remapMap(
methods,
entry -> remapMethod(className, entry.getKey(), remapper),
entry -> entry.getValue().remap(remapper)
)
);
}
private static String remapField(String className, String field, TinyRemapper remapper) {
String[] nameDesc = field.split(":", 2);
if (nameDesc.length != 2) {
return field;
}
TrRemapper trRemapper = remapper.getEnvironment().getRemapper();
return trRemapper.mapFieldName(className, nameDesc[0], nameDesc[1]) + ":" + trRemapper.mapDesc(nameDesc[1]);
}
private static String remapMethod(String className, String method, TinyRemapper remapper) {
int parenIndex = method.indexOf('(');
if (parenIndex == -1) {
return method;
}
String name = method.substring(0, parenIndex);
String desc = method.substring(parenIndex);
TrRemapper trRemapper = remapper.getEnvironment().getRemapper();
return trRemapper.mapMethodName(className, name, desc) + trRemapper.mapMethodDesc(desc);
}
public int modifyAccessFlags(int access) {
if (annotationsToRemove.contains("java/lang/Deprecated")) {
access &= ~Opcodes.ACC_DEPRECATED;
}
if (annotationsToAdd.stream().anyMatch(ann -> "Ljava/lang/Deprecated;".equals(ann.desc))) {
access |= Opcodes.ACC_DEPRECATED;
}
return access;
}
@Nullable
public GenericAnnotationData getFieldData(String fieldName, String fieldDesc) {
return fields.get(fieldName + ":" + fieldDesc);
}
@Nullable
public MethodAnnotationData getMethodData(String methodName, String methodDesc) {
return methods.get(methodName + methodDesc);
}
}

View File

@@ -0,0 +1,100 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.providers.mappings.extras.annotations;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.gson.annotations.SerializedName;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
import net.fabricmc.tinyremapper.TinyRemapper;
public record GenericAnnotationData(
@SerializedName("remove")
Set<String> annotationsToRemove,
@SerializedName("add")
List<AnnotationNode> annotationsToAdd,
@SerializedName("type_remove")
Set<TypeAnnotationKey> typeAnnotationsToRemove,
@SerializedName("type_add")
List<TypeAnnotationNode> typeAnnotationsToAdd
) {
public GenericAnnotationData {
if (annotationsToRemove == null) {
annotationsToRemove = new LinkedHashSet<>();
}
if (annotationsToAdd == null) {
annotationsToAdd = new ArrayList<>();
}
if (typeAnnotationsToRemove == null) {
typeAnnotationsToRemove = new LinkedHashSet<>();
}
if (typeAnnotationsToAdd == null) {
typeAnnotationsToAdd = new ArrayList<>();
}
}
GenericAnnotationData merge(GenericAnnotationData other) {
Set<String> newAnnotationToRemove = new LinkedHashSet<>(annotationsToRemove);
newAnnotationToRemove.addAll(other.annotationsToRemove);
List<AnnotationNode> newAnnotationsToAdd = new ArrayList<>(annotationsToAdd);
newAnnotationsToAdd.addAll(other.annotationsToAdd);
Set<TypeAnnotationKey> newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove);
newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove);
List<TypeAnnotationNode> newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd);
newTypeAnnotationsToAdd.addAll(other.typeAnnotationsToAdd);
return new GenericAnnotationData(newAnnotationToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd);
}
GenericAnnotationData remap(TinyRemapper remapper) {
return new GenericAnnotationData(
annotationsToRemove.stream().map(remapper.getEnvironment().getRemapper()::map).collect(Collectors.toCollection(LinkedHashSet::new)),
annotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new)),
typeAnnotationsToRemove.stream().map(key -> key.remap(remapper)).collect(Collectors.toCollection(LinkedHashSet::new)),
typeAnnotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new))
);
}
public int modifyAccessFlags(int access) {
if (annotationsToRemove.contains("java/lang/Deprecated")) {
access &= ~Opcodes.ACC_DEPRECATED;
}
if (annotationsToAdd.stream().anyMatch(ann -> "Ljava/lang/Deprecated;".equals(ann.desc))) {
access |= Opcodes.ACC_DEPRECATED;
}
return access;
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.extras.annotations;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.gson.annotations.SerializedName;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
import net.fabricmc.tinyremapper.TinyRemapper;
public record MethodAnnotationData(
@SerializedName("remove")
Set<String> annotationsToRemove,
@SerializedName("add")
List<AnnotationNode> annotationsToAdd,
@SerializedName("type_remove")
Set<TypeAnnotationKey> typeAnnotationsToRemove,
@SerializedName("type_add")
List<TypeAnnotationNode> typeAnnotationsToAdd,
Map<Integer, GenericAnnotationData> parameters
) {
public MethodAnnotationData {
if (annotationsToRemove == null) {
annotationsToRemove = new LinkedHashSet<>();
}
if (annotationsToAdd == null) {
annotationsToAdd = new ArrayList<>();
}
if (typeAnnotationsToRemove == null) {
typeAnnotationsToRemove = new LinkedHashSet<>();
}
if (typeAnnotationsToAdd == null) {
typeAnnotationsToAdd = new ArrayList<>();
}
if (parameters == null) {
parameters = new LinkedHashMap<>();
}
}
MethodAnnotationData merge(MethodAnnotationData other) {
Set<String> newAnnotationsToRemove = new LinkedHashSet<>(annotationsToRemove);
newAnnotationsToRemove.addAll(other.annotationsToRemove);
List<AnnotationNode> newAnnotationsToAdd = new ArrayList<>(annotationsToAdd);
newAnnotationsToAdd.addAll(other.annotationsToAdd);
Set<TypeAnnotationKey> newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove);
newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove);
List<TypeAnnotationNode> newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd);
newTypeAnnotationsToAdd.addAll(other.typeAnnotationsToAdd);
Map<Integer, GenericAnnotationData> newParameters = new LinkedHashMap<>(parameters);
other.parameters.forEach((key, value) -> newParameters.merge(key, value, GenericAnnotationData::merge));
return new MethodAnnotationData(newAnnotationsToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd, newParameters);
}
MethodAnnotationData remap(TinyRemapper remapper) {
return new MethodAnnotationData(
annotationsToRemove.stream().map(remapper.getEnvironment().getRemapper()::map).collect(Collectors.toCollection(LinkedHashSet::new)),
annotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new)),
typeAnnotationsToRemove.stream().map(key -> key.remap(remapper)).collect(Collectors.toCollection(LinkedHashSet::new)),
typeAnnotationsToAdd.stream().map(ann -> AnnotationsData.remap(ann, remapper)).collect(Collectors.toCollection(ArrayList::new)),
AnnotationsData.remapMap(
parameters,
Map.Entry::getKey,
entry -> entry.getValue().remap(remapper)
)
);
}
public int modifyAccessFlags(int access) {
if (annotationsToRemove.contains("java/lang/Deprecated")) {
access &= ~Opcodes.ACC_DEPRECATED;
}
if (annotationsToAdd.stream().anyMatch(ann -> "Ljava/lang/Deprecated;".equals(ann.desc))) {
access |= Opcodes.ACC_DEPRECATED;
}
return access;
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.extras.annotations;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
// https://github.com/google/gson/issues/512#issuecomment-1203356412
class SkipEmptyTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<?> rawType = type.getRawType();
boolean isMap = Map.class.isAssignableFrom(rawType);
if (!isMap && !Collection.class.isAssignableFrom(rawType)) {
return null;
}
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null || isEmpty(value)) {
delegate.write(out, null);
} else {
delegate.write(out, value);
}
}
@Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
private boolean isEmpty(T value) {
return isMap ? ((Map<?, ?>) value).isEmpty() : ((Collection<?>) value).isEmpty();
}
};
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.extras.annotations;
import net.fabricmc.tinyremapper.TinyRemapper;
public record TypeAnnotationKey(int typeRef, String typePath, String name) {
TypeAnnotationKey remap(TinyRemapper remapper) {
return new TypeAnnotationKey(typeRef, typePath, remapper.getEnvironment().getRemapper().map(name));
}
}

View File

@@ -0,0 +1,59 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.providers.mappings.extras.annotations;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
class TypeAnnotationNodeSerializer implements JsonSerializer<TypeAnnotationNode>, JsonDeserializer<TypeAnnotationNode> {
@Override
public TypeAnnotationNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
AnnotationNode annotation = context.deserialize(json, AnnotationNode.class);
JsonObject jsonObject = json.getAsJsonObject();
int typeRef = jsonObject.getAsJsonPrimitive("type_ref").getAsInt();
String typePath = jsonObject.getAsJsonPrimitive("type_path").getAsString();
TypeAnnotationNode typeAnnotation = new TypeAnnotationNode(typeRef, TypePath.fromString(typePath), annotation.desc);
annotation.accept(typeAnnotation);
return typeAnnotation;
}
@Override
public JsonElement serialize(TypeAnnotationNode src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = context.serialize(src, AnnotationNode.class).getAsJsonObject();
json.addProperty("type_ref", src.typeRef);
json.addProperty("type_path", src.typePath.toString());
return json;
}
}

View File

@@ -24,6 +24,7 @@
package net.fabricmc.loom.configuration.providers.mappings.file;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -34,6 +35,8 @@ import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.api.mappings.layered.MappingLayer;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsLayer;
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;
@@ -51,8 +54,9 @@ public record FileMappingsLayer(
String fallbackSourceNamespace, String fallbackTargetNamespace,
boolean enigma, // Enigma cannot be automatically detected since it's stored in a directory.
boolean unpick,
boolean annotations,
String mergeNamespace
) implements MappingLayer, UnpickLayer {
) implements MappingLayer, UnpickLayer, AnnotationsLayer {
@Override
public void visit(MappingVisitor mappingVisitor) throws IOException {
// Bare file
@@ -111,4 +115,27 @@ public record FileMappingsLayer(
return UnpickData.read(unpickMetadata, unpickDefinitions);
}
}
@Override
public @Nullable AnnotationsData getAnnotationsData() throws IOException {
if (!annotations) {
return null;
}
if (!ZipUtils.isZip(path)) {
throw new UnsupportedOperationException("Annotations data is only supported for zip file mapping layers.");
}
try (FileSystemUtil.Delegate fileSystem = FileSystemUtil.getJarFileSystem(path)) {
final Path annotations = fileSystem.get().getPath(AnnotationsLayer.ANNOTATIONS_PATH);
if (!Files.exists(annotations)) {
return null;
}
try (BufferedReader reader = Files.newBufferedReader(annotations)) {
return AnnotationsData.read(reader);
}
}
}
}

View File

@@ -31,11 +31,11 @@ import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
public record FileMappingsSpec(
FileSpec fileSpec, String mappingPath,
String fallbackSourceNamespace, String fallbackTargetNamespace,
boolean enigma, boolean unpick,
boolean enigma, boolean unpick, boolean annotations,
String mergeNamespace
) implements MappingsSpec<FileMappingsLayer> {
@Override
public FileMappingsLayer createLayer(MappingContext context) {
return new FileMappingsLayer(fileSpec.get(context), mappingPath, fallbackSourceNamespace, fallbackTargetNamespace, enigma, unpick, mergeNamespace);
return new FileMappingsLayer(fileSpec.get(context), mappingPath, fallbackSourceNamespace, fallbackTargetNamespace, enigma, unpick, annotations, mergeNamespace);
}
}

View File

@@ -42,6 +42,7 @@ public class FileMappingsSpecBuilderImpl implements FileMappingsSpecBuilder {
private String fallbackTargetNamespace = MappingsNamespace.NAMED.toString();
private boolean enigma = false;
private boolean unpick = false;
private boolean annotations = false;
private String mergeNamespace = MappingsNamespace.INTERMEDIARY.toString();
private FileMappingsSpecBuilderImpl(FileSpec fileSpec) {
@@ -71,6 +72,12 @@ public class FileMappingsSpecBuilderImpl implements FileMappingsSpecBuilder {
return this;
}
@Override
public FileMappingsSpecBuilder containsAnnotations() {
annotations = true;
return this;
}
@Override
public FileMappingsSpecBuilderImpl containsUnpick() {
unpick = true;
@@ -96,6 +103,6 @@ public class FileMappingsSpecBuilderImpl implements FileMappingsSpecBuilder {
}
public FileMappingsSpec build() {
return new FileMappingsSpec(fileSpec, mappingPath, fallbackSourceNamespace, fallbackTargetNamespace, enigma, unpick, mergeNamespace);
return new FileMappingsSpec(fileSpec, mappingPath, fallbackSourceNamespace, fallbackTargetNamespace, enigma, unpick, annotations, mergeNamespace);
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -51,16 +51,16 @@ public final class MappingsMerger {
private static final Logger LOGGER = LoggerFactory.getLogger(MappingsMerger.class);
public static void mergeAndSaveMappings(Path from, Path out, MinecraftProvider minecraftProvider, IntermediateMappingsService intermediateMappingsService) throws IOException {
Stopwatch stopwatch = Stopwatch.createStarted();
long start = System.currentTimeMillis();
LOGGER.info(":merging mappings");
if (minecraftProvider.isLegacyVersion()) {
legacyMergeAndSaveMappings(from, out, intermediateMappingsService);
if (minecraftProvider.isLegacySplitOfficialNamespaceVersion()) {
legacyMergedMergeAndSaveMappings(from, out, intermediateMappingsService);
} else {
mergeAndSaveMappings(from, out, intermediateMappingsService);
}
LOGGER.info(":merged mappings in " + stopwatch.stop());
LOGGER.info(":merged mappings in {}ms", System.currentTimeMillis() - start);
}
@VisibleForTesting
@@ -85,7 +85,7 @@ public final class MappingsMerger {
}
@VisibleForTesting
public static void legacyMergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException {
public static void legacyMergedMergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException {
MemoryMappingTree intermediaryTree = new MemoryMappingTree();
intermediateMappingsService.getMemoryMappingTree().accept(intermediaryTree);

View File

@@ -0,0 +1,379 @@
/*
* 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;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.TypeAnnotationNode;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.ClassAnnotationData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.GenericAnnotationData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.MethodAnnotationData;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.TypeAnnotationKey;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.tinyremapper.TinyRemapper;
import net.fabricmc.tinyremapper.api.TrClass;
public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implements TinyRemapper.ApplyVisitorProvider {
@Override
public ClassVisitor insertApplyVisitor(TrClass cls, ClassVisitor next) {
ClassAnnotationData classData = annotationsData.classes().get(cls.getName());
if (classData == null) {
return next;
}
return new AnnotationsApplyClassVisitor(next, classData);
}
public static class AnnotationsApplyClassVisitor extends ClassVisitor {
private final ClassAnnotationData classData;
private boolean hasAddedAnnotations;
public AnnotationsApplyClassVisitor(ClassVisitor cv, ClassAnnotationData classData) {
super(Constants.ASM_VERSION, cv);
this.classData = classData;
hasAddedAnnotations = false;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
access = classData.modifyAccessFlags(access);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) {
if (classData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) {
return null;
}
return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (classData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) {
return null;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public void visitNestMember(String nestMember) {
addClassAnnotations();
super.visitNestMember(nestMember);
}
@Override
public void visitPermittedSubclass(String permittedSubclass) {
addClassAnnotations();
super.visitPermittedSubclass(permittedSubclass);
}
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
addClassAnnotations();
super.visitInnerClass(name, outerName, innerName, access);
}
@Override
public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) {
addClassAnnotations();
RecordComponentVisitor rcv = super.visitRecordComponent(name, descriptor, signature);
if (rcv == null) {
return null;
}
GenericAnnotationData fieldData = classData.getFieldData(name, descriptor);
if (fieldData == null) {
return rcv;
}
return new RecordComponentVisitor(Constants.ASM_VERSION, rcv) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (fieldData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) {
return null;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) {
if (fieldData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) {
return null;
}
return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}
@Override
public void visitEnd() {
for (AnnotationNode annotation : fieldData.annotationsToAdd()) {
AnnotationVisitor av = delegate.visitAnnotation(annotation.desc, false);
if (av != null) {
annotation.accept(av);
}
}
for (TypeAnnotationNode typeAnnotation : fieldData.typeAnnotationsToAdd()) {
AnnotationVisitor av = delegate.visitTypeAnnotation(typeAnnotation.typeRef, typeAnnotation.typePath, typeAnnotation.desc, false);
if (av != null) {
typeAnnotation.accept(av);
}
}
super.visitEnd();
}
};
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
addClassAnnotations();
GenericAnnotationData fieldData = classData.getFieldData(name, descriptor);
if (fieldData == null) {
return super.visitField(access, name, descriptor, signature, value);
}
FieldVisitor fv = super.visitField(fieldData.modifyAccessFlags(access), name, descriptor, signature, value);
if (fv == null) {
return null;
}
return new FieldVisitor(Constants.ASM_VERSION, fv) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (fieldData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) {
return null;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) {
if (fieldData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) {
return null;
}
return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}
@Override
public void visitEnd() {
for (AnnotationNode annotation : fieldData.annotationsToAdd()) {
AnnotationVisitor av = fv.visitAnnotation(annotation.desc, false);
if (av != null) {
annotation.accept(av);
}
}
for (TypeAnnotationNode typeAnnotation : fieldData.typeAnnotationsToAdd()) {
AnnotationVisitor av = fv.visitTypeAnnotation(typeAnnotation.typeRef, typeAnnotation.typePath, typeAnnotation.desc, false);
if (av != null) {
typeAnnotation.accept(av);
}
}
super.visitEnd();
}
};
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
addClassAnnotations();
MethodAnnotationData methodData = classData.getMethodData(name, descriptor);
if (methodData == null) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor mv = super.visitMethod(methodData.modifyAccessFlags(access), name, descriptor, signature, exceptions);
if (mv == null) {
return null;
}
return new MethodVisitor(Constants.ASM_VERSION, mv) {
int syntheticParameterCount = 0;
boolean visitedAnnotableParameterCount = false;
boolean hasAddedAnnotations = false;
@Override
public void visitParameter(String name, int access) {
if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
syntheticParameterCount++;
}
super.visitParameter(name, access);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (methodData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) {
return null;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) {
if (methodData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) {
return null;
}
return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}
@Override
public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) {
GenericAnnotationData parameterData = methodData.parameters().get(parameter);
if (parameterData != null && parameterData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) {
return null;
}
return super.visitParameterAnnotation(parameter, descriptor, visible);
}
@Override
public void visitAnnotableParameterCount(int parameterCount, boolean visible) {
if (!visible && !methodData.parameters().isEmpty()) {
parameterCount = Math.max(parameterCount, Type.getArgumentCount(descriptor) - syntheticParameterCount);
visitedAnnotableParameterCount = true;
}
super.visitAnnotableParameterCount(parameterCount, visible);
}
@Override
public void visitCode() {
addMethodAnnotations();
super.visitCode();
}
@Override
public void visitEnd() {
addMethodAnnotations();
super.visitEnd();
}
void addMethodAnnotations() {
if (hasAddedAnnotations) {
return;
}
hasAddedAnnotations = true;
for (AnnotationNode annotation : methodData.annotationsToAdd()) {
AnnotationVisitor av = mv.visitAnnotation(annotation.desc, false);
if (av != null) {
annotation.accept(av);
}
}
for (TypeAnnotationNode typeAnnotation : methodData.typeAnnotationsToAdd()) {
AnnotationVisitor av = mv.visitTypeAnnotation(typeAnnotation.typeRef, typeAnnotation.typePath, typeAnnotation.desc, false);
if (av != null) {
typeAnnotation.accept(av);
}
}
if (!visitedAnnotableParameterCount && !methodData.parameters().isEmpty()) {
mv.visitAnnotableParameterCount(Type.getArgumentCount(descriptor) - syntheticParameterCount, false);
visitedAnnotableParameterCount = true;
}
methodData.parameters().forEach((paramIndex, paramData) -> {
for (AnnotationNode annotation : paramData.annotationsToAdd()) {
AnnotationVisitor av = mv.visitParameterAnnotation(paramIndex, annotation.desc, false);
if (av != null) {
annotation.accept(av);
}
}
});
}
};
}
@Override
public void visitEnd() {
addClassAnnotations();
super.visitEnd();
}
private void addClassAnnotations() {
if (hasAddedAnnotations) {
return;
}
hasAddedAnnotations = true;
for (AnnotationNode annotation : classData.annotationsToAdd()) {
AnnotationVisitor av = cv.visitAnnotation(annotation.desc, false);
if (av != null) {
annotation.accept(av);
}
}
for (TypeAnnotationNode typeAnnotation : classData.typeAnnotationsToAdd()) {
AnnotationVisitor av = cv.visitTypeAnnotation(typeAnnotation.typeRef, typeAnnotation.typePath, typeAnnotation.desc, false);
if (av != null) {
typeAnnotation.accept(av);
}
}
}
}
}

View File

@@ -29,16 +29,21 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
import org.jetbrains.annotations.VisibleForTesting;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InnerClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.Constants;
@@ -48,6 +53,11 @@ public class MinecraftClassMerger {
private static final String ITF_LIST_DESCRIPTOR = "Lnet/fabricmc/api/EnvironmentInterfaces;";
private static final String SIDED_DESCRIPTOR = "Lnet/fabricmc/api/Environment;";
// The permission flags that are allowed to differ between client and server.
private static final int PERMISSION_BITS = Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE;
private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftClassMerger.class);
private abstract static class Merger<T> {
private final Map<String, T> entriesClient, entriesServer;
private final List<String> entryNames;
@@ -66,6 +76,10 @@ public class MinecraftClassMerger {
public abstract void applySide(T entry, String side);
public T merge(T clientEntry, T serverEntry) {
return clientEntry;
}
private List<String> toMap(List<T> entries, Map<String, T> map) {
List<String> list = new ArrayList<>(entries.size());
@@ -84,7 +98,7 @@ public class MinecraftClassMerger {
T entryServer = entriesServer.get(s);
if (entryClient != null && entryServer != null) {
list.add(entryClient);
list.add(merge(entryClient, entryServer));
} else if (entryClient != null) {
applySide(entryClient, "CLIENT");
list.add(entryClient);
@@ -232,6 +246,21 @@ public class MinecraftClassMerger {
AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false);
visitSideAnnotation(av, side);
}
@Override
public FieldNode merge(FieldNode clientEntry, FieldNode serverEntry) {
if (clientEntry.access == serverEntry.access) {
return super.merge(clientEntry, serverEntry);
}
LOGGER.debug("Field has different access modifiers: {}#{}{}, client: '{}', server: '{}'",
nodeOut.name, clientEntry.name, clientEntry.desc,
formatMethodAccessFlags(clientEntry.access),
formatMethodAccessFlags(serverEntry.access));
clientEntry.access = mergeAccess(clientEntry.access, serverEntry.access);
return clientEntry;
}
}.merge(nodeOut.fields);
new Merger<>(nodeC.methods, nodeS.methods) {
@@ -245,6 +274,26 @@ public class MinecraftClassMerger {
AnnotationVisitor av = entry.visitAnnotation(SIDED_DESCRIPTOR, false);
visitSideAnnotation(av, side);
}
@Override
public MethodNode merge(MethodNode clientEntry, MethodNode serverEntry) {
if (clientEntry.access == serverEntry.access) {
return super.merge(clientEntry, serverEntry);
}
LOGGER.debug("Method has different access modifiers: {}#{}{}, client: '{}', server: '{}'",
nodeOut.name, clientEntry.name, clientEntry.desc,
formatMethodAccessFlags(clientEntry.access),
formatMethodAccessFlags(serverEntry.access));
try {
clientEntry.access = mergeAccess(clientEntry.access, serverEntry.access);
} catch (IllegalStateException e) {
throw new IllegalStateException("Failed to merge method %s#%s%s %s".formatted(nodeOut.name, clientEntry.name, clientEntry.desc, e.getMessage()), e);
}
return clientEntry;
}
}.merge(nodeOut.methods);
nodeOut.accept(writer);
@@ -293,4 +342,65 @@ public class MinecraftClassMerger {
return out;
}
/**
* When merging 2 members with differing access we pick the least permissive access.
* This ensures that the mod is compiled against the "worst case" access level.
* At runtime fabric-loader will make all methods public, meaning it doesn't cause an issue in dev envs.
* If a mod needs to uses one of these members it should use an access widener.
*
* <p>Allow merging private final members as the final modifier is irrelevant for private members.
*/
@VisibleForTesting
public static int mergeAccess(int clientAccess, int serverAccess) {
validateAccessMerge(clientAccess, serverAccess);
if (getAccessRating(clientAccess) > getAccessRating(serverAccess)) {
return serverAccess;
}
return clientAccess;
}
private static void validateAccessMerge(int clientAccess, int serverAccess) {
int clientFlags = clientAccess & ~PERMISSION_BITS;
int serverFlags = serverAccess & ~PERMISSION_BITS;
if (clientFlags != serverFlags) {
// If the access flags are different beyond the permission bits, we cannot merge them.
throw new IllegalStateException("Cannot merge methods with differing non-permission bits: client: %s server: %s"
.formatted(formatMethodAccessFlags(clientAccess), formatMethodAccessFlags(serverAccess)));
}
}
private static int getAccessRating(int access) {
if ((access & Opcodes.ACC_PUBLIC) != 0) {
return 2;
} else if ((access & Opcodes.ACC_PROTECTED) != 0) {
return 1;
} else {
return 0;
}
}
@VisibleForTesting
public static String formatMethodAccessFlags(int access) {
var joiner = new StringJoiner(" ");
if ((access & Opcodes.ACC_PUBLIC) != 0) joiner.add("public");
if ((access & Opcodes.ACC_PRIVATE) != 0) joiner.add("private");
if ((access & Opcodes.ACC_PROTECTED) != 0) joiner.add("protected");
if ((access & Opcodes.ACC_STATIC) != 0) joiner.add("static");
if ((access & Opcodes.ACC_FINAL) != 0) joiner.add("final");
if ((access & Opcodes.ACC_SYNCHRONIZED) != 0) joiner.add("synchronized");
if ((access & Opcodes.ACC_BRIDGE) != 0) joiner.add("bridge");
if ((access & Opcodes.ACC_VARARGS) != 0) joiner.add("varargs");
if ((access & Opcodes.ACC_NATIVE) != 0) joiner.add("native");
if ((access & Opcodes.ACC_ABSTRACT) != 0) joiner.add("abstract");
if ((access & Opcodes.ACC_STRICT) != 0) joiner.add("strictfp");
if ((access & Opcodes.ACC_SYNTHETIC) != 0) joiner.add("synthetic");
if ((access & Opcodes.ACC_MANDATED) != 0) joiner.add("mandated");
return joiner.toString();
}
}

View File

@@ -67,7 +67,7 @@ public class MinecraftJarMerger implements AutoCloseable {
}
}
private static final MinecraftClassMerger CLASS_MERGER = new MinecraftClassMerger();
private final MinecraftClassMerger classMerger = new MinecraftClassMerger();
private final FileSystemUtil.Delegate inputClientFs, inputServerFs, outputFs;
private final Path inputClient, inputServer;
private final Map<String, Entry> entriesClient, entriesServer;
@@ -194,7 +194,7 @@ public class MinecraftJarMerger implements AutoCloseable {
result = entry1;
} else {
if (isClass) {
result = new Entry(entry1.path, entry1.metadata, CLASS_MERGER.merge(entry1.data, entry2.data));
result = new Entry(entry1.path, entry1.metadata, classMerger.merge(entry1.data, entry2.data));
} else {
// FIXME: More heuristics?
result = entry1;

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2018-2021 FabricMC
* Copyright (c) 2018-2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +31,6 @@ 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;
import org.gradle.api.Project;
import org.jetbrains.annotations.Nullable;
@@ -44,6 +43,7 @@ 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.Check;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.DownloadExecutor;
import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
@@ -194,7 +194,7 @@ public abstract class MinecraftProvider {
}
public final void extractBundledServerJar() throws IOException {
Preconditions.checkArgument(provideServer(), "Not configured to provide server jar");
Check.require(provideServer(), "Not configured to provide server jar");
Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar");
LOGGER.info(":Extracting server jar from bootstrap");
@@ -225,20 +225,20 @@ public abstract class MinecraftProvider {
}
public File getMinecraftClientJar() {
Preconditions.checkArgument(provideClient(), "Not configured to provide client jar");
Check.require(provideClient(), "Not configured to provide client jar");
return minecraftClientJar;
}
// May be null on older versions
@Nullable
public File getMinecraftExtractedServerJar() {
Preconditions.checkArgument(provideServer(), "Not configured to provide server jar");
Check.require(provideServer(), "Not configured to provide server jar");
return minecraftExtractedServerJar;
}
// This may be the server bundler jar on newer versions prob not what you want.
public File getMinecraftServerJar() {
Preconditions.checkArgument(provideServer(), "Not configured to provide server jar");
Check.require(provideServer(), "Not configured to provide server jar");
return minecraftServerJar;
}
@@ -254,7 +254,15 @@ public abstract class MinecraftProvider {
* @return true if the minecraft version is older than 1.3.
*/
public boolean isLegacyVersion() {
return !getVersionInfo().isVersionOrNewer(Constants.RELEASE_TIME_1_3);
return getVersionInfo().isLegacyVersion();
}
/**
* Returns true if the minecraft version is between Beta 1.0 (inclusive) and 1.3 (exclusive),
* which splits the {@code official} mapping namespace into env-specific variants.
*/
public boolean isLegacySplitOfficialNamespaceVersion() {
return getVersionInfo().isLegacySplitOfficialNamespaceVersion();
}
public String getJarPrefix() {

View File

@@ -27,7 +27,6 @@ package net.fabricmc.loom.configuration.providers.minecraft;
import java.util.List;
import java.util.function.BiConsumer;
import com.google.common.base.Preconditions;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.plugins.JavaPlugin;
@@ -37,6 +36,7 @@ import org.gradle.jvm.tasks.Jar;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.RemapConfigurations;
import net.fabricmc.loom.task.AbstractRemapJarTask;
import net.fabricmc.loom.util.Check;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
@@ -53,7 +53,7 @@ public abstract sealed class MinecraftSourceSets permits MinecraftSourceSets.Sin
public void evaluateSplit(Project project) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
Preconditions.checkArgument(extension.areEnvironmentSourceSetsSplit());
Check.require(extension.areEnvironmentSourceSetsSplit());
Split.INSTANCE.evaluate(project);
}
@@ -155,9 +155,9 @@ public abstract sealed class MinecraftSourceSets permits MinecraftSourceSets.Sin
@Override
public void applyDependencies(BiConsumer<String, MinecraftJar.Type> consumer, List<MinecraftJar.Type> targets) {
Preconditions.checkArgument(targets.size() == 2);
Preconditions.checkArgument(targets.contains(MinecraftJar.Type.COMMON));
Preconditions.checkArgument(targets.contains(MinecraftJar.Type.CLIENT_ONLY));
Check.require(targets.size() == 2);
Check.require(targets.contains(MinecraftJar.Type.COMMON));
Check.require(targets.contains(MinecraftJar.Type.CLIENT_ONLY));
consumer.accept(MINECRAFT_COMMON_NAMED.runtime(), MinecraftJar.Type.COMMON);
consumer.accept(MINECRAFT_CLIENT_ONLY_NAMED.runtime(), MinecraftJar.Type.CLIENT_ONLY);

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
* Copyright (c) 2021-2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -32,6 +32,7 @@ import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.Platform;
@SuppressWarnings("unused")
@@ -65,6 +66,37 @@ public record MinecraftVersionMeta(
return this.releaseTime().compareTo(releaseTime) >= 0;
}
/**
* Returns true if the version was released before 1.3.
* This means that the client and server can't be merged normally due to different obfuscation
* or one of the environments missing.
*/
public boolean isLegacyVersion() {
return !isVersionOrNewer(Constants.RELEASE_TIME_1_3);
}
public boolean hasClient() {
return downloads().containsKey("client");
}
public boolean hasServer() {
return downloads().containsKey("server");
}
/**
* Returns true if the version was released after Beta 1.0 (inclusive) but before 1.3 (exclusive).
*
* <p>This includes some versions that only have a client jar or a server jar to match behaviour
* across all versions in the range.
*/
public boolean isLegacySplitOfficialNamespaceVersion() {
// TODO: Allow "official" as the obf namespace on single-env versions in this range by checking the mappings
// to see which one they have.
// Likewise, "clientOfficial"/"serverOfficial" could be allowed older single-env releases
// as an alternative to "official".
return isLegacyVersion() && isVersionOrNewer(Constants.RELEASE_TIME_BETA_1_0);
}
public boolean hasNativesToExtract() {
return libraries.stream().anyMatch(Library::hasNatives);
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
* Copyright (c) 2022-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
@@ -31,7 +31,6 @@ import java.util.List;
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.util.Constants;
import net.fabricmc.loom.util.TinyRemapperLoggerAdapter;
import net.fabricmc.tinyremapper.NonClassCopyMode;
import net.fabricmc.tinyremapper.OutputConsumerPath;
@@ -55,8 +54,8 @@ public abstract class SingleJarMinecraftProvider extends MinecraftProvider {
}
private static MappingsNamespace getOfficialNamespace(MinecraftMetadataProvider metadataProvider, boolean server) {
// Versions before 1.3 don't have a common namespace, so use side specific namespaces.
if (!metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) {
// Some versions before 1.3 don't have a common namespace, so use side specific namespaces.
if (metadataProvider.getVersionMeta().isLegacySplitOfficialNamespaceVersion()) {
return server ? MappingsNamespace.SERVER_OFFICIAL : MappingsNamespace.CLIENT_OFFICIAL;
}

View File

@@ -31,7 +31,7 @@ import org.jetbrains.annotations.Nullable;
public record VersionsManifest(List<Version> versions, Map<String, String> latest) {
public static class Version {
public String id, url, sha1;
public String type, id, url, sha1;
}
@Nullable

View File

@@ -53,6 +53,8 @@ import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper;
import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.configuration.providers.mappings.TinyMappingsService;
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
import net.fabricmc.loom.configuration.providers.minecraft.AnnotationsApplyVisitor;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
@@ -257,11 +259,16 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
Files.deleteIfExists(remappedJars.outputJarPath());
final Set<String> classNames = extension.isForgeLike() ? InnerClassRemapper.readClassNames(remappedJars.inputJar()) : Set.of();
final AnnotationsData remappedAnnotations = AnnotationsData.getRemappedAnnotations(getTargetNamespace(), mappingConfiguration, getProject(), configContext.serviceFactory(), toM);
final Map<String, String> remappedSignatures = SignatureFixerApplyVisitor.getRemappedSignatures(getTargetNamespace() == MappingsNamespace.INTERMEDIARY, mappingConfiguration, getProject(), configContext.serviceFactory(), toM);
final MinecraftVersionMeta.JavaVersion javaVersion = minecraftProvider.getVersionInfo().javaVersion();
final boolean fixRecords = javaVersion != null && javaVersion.majorVersion() >= 16;
TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceFactory(), fromM, toM, fixRecords, (builder) -> {
if (remappedAnnotations != null) {
builder.extraPostApplyVisitor(new AnnotationsApplyVisitor(remappedAnnotations));
}
builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures));
if (extension.isNeoForge()) builder.extension(new MixinExtension(inputTag -> true));
configureRemapper(remappedJars, builder);

View File

@@ -33,9 +33,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.util.Lazy;
/**
* The know versions keep track of the versions that are signed using SHA1 or not signature at all.
@@ -44,7 +43,7 @@ import net.fabricmc.loom.LoomGradlePlugin;
public record KnownVersions(
Map<String, String> client,
Map<String, String> server) {
public static final Supplier<KnownVersions> INSTANCE = Suppliers.memoize(KnownVersions::load);
public static final Supplier<KnownVersions> INSTANCE = Lazy.of(KnownVersions::load);
private static KnownVersions load() {
try (InputStream is = KnownVersions.class.getClassLoader().getResourceAsStream("certs/known_versions.json");

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021-2024 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
@@ -81,7 +81,6 @@ import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfigura
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.task.GenerateSourcesTask;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.DeprecationHelper;
import net.fabricmc.loom.util.MirrorUtil;
import net.fabricmc.loom.util.ModPlatform;
@@ -184,11 +183,11 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
// if no configuration is selected by the user, attempt to select one
// based on the mc version and which sides are present for it
if (!metadataProvider.getVersionMeta().downloads().containsKey("server")) {
if (!metadataProvider.getVersionMeta().hasServer()) {
return MinecraftJarConfiguration.CLIENT_ONLY;
} else if (!metadataProvider.getVersionMeta().downloads().containsKey("client")) {
} else if (!metadataProvider.getVersionMeta().hasClient()) {
return MinecraftJarConfiguration.SERVER_ONLY;
} else if (metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) {
} else if (!metadataProvider.getVersionMeta().isLegacyVersion()) {
return MinecraftJarConfiguration.MERGED;
} else {
return MinecraftJarConfiguration.LEGACY_MERGED;

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021-2024 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
@@ -337,8 +337,8 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
provider.getDownloader().set(this::download);
provider.getDownloader().disallowChanges();
provider.getIsLegacyMinecraft().set(getProject().provider(() -> getMinecraftProvider().isLegacyVersion()));
provider.getIsLegacyMinecraft().disallowChanges();
provider.getUseSplitOfficialNamespaces().set(getProject().provider(() -> getMinecraftProvider().isLegacySplitOfficialNamespaceVersion()));
provider.getUseSplitOfficialNamespaces().disallowChanges();
}
@Override

View File

@@ -50,7 +50,7 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI {
public MixinExtensionApiImpl(Project project) {
this.project = Objects.requireNonNull(project);
this.useMixinAp = project.getObjects().property(Boolean.class)
.convention(project.provider(() -> !LoomGradleExtension.get(project).isNeoForge() && (!LoomGradleExtension.get(project).isForge() || !LoomGradleExtension.get(project).getForgeProvider().usesMojangAtRuntime())));
.convention(false);
this.refmapTargetNamespace = project.getObjects().property(String.class)
.convention(project.provider(() -> IntermediaryNamespaces.runtimeIntermediary(project)));

View File

@@ -36,7 +36,6 @@ import java.util.jar.Manifest;
import javax.inject.Inject;
import com.google.common.base.Preconditions;
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
@@ -63,6 +62,7 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.task.service.ClientEntriesService;
import net.fabricmc.loom.task.service.JarManifestService;
import net.fabricmc.loom.util.Check;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.ZipReprocessorUtil;
import net.fabricmc.loom.util.ZipUtils;
@@ -239,7 +239,7 @@ public abstract class AbstractRemapJarTask extends Jar {
return out.toByteArray();
}));
Preconditions.checkState(count > 0, "Did not transform any jar manifest");
Check.require(count > 0, "Did not transform any jar manifest");
}
protected void rewriteJar() throws IOException {
@@ -260,7 +260,7 @@ public abstract class AbstractRemapJarTask extends Jar {
}
private SourceSet getClientSourceSet() {
Preconditions.checkArgument(LoomGradleExtension.get(getProject()).areEnvironmentSourceSetsSplit(), "Cannot get client sourceset as project is not split");
Check.require(LoomGradleExtension.get(getProject()).areEnvironmentSourceSetsSplit(), "Cannot get client sourceset as project is not split");
return SourceSetHelper.getSourceSetByName(getClientOnlySourceSetName().get(), getProject());
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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 java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkAction;
import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkQueue;
import org.gradle.workers.WorkerExecutor;
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec;
import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator;
/**
* A task that generates a {@code fabric.mod.json} file using the configured {@link FabricModJsonV1Spec} specification.
*/
public abstract class FabricModJsonV1Task extends AbstractLoomTask {
/**
* The fabric.mod.json spec.
*/
@Nested
public abstract Property<FabricModJsonV1Spec> getJson();
/**
* The output file to write the generated fabric.mod.json to.
*/
@OutputFile
public abstract RegularFileProperty getOutputFile();
@Inject
protected abstract WorkerExecutor getWorkerExecutor();
@Inject
protected abstract ObjectFactory getObjectFactory();
public FabricModJsonV1Task() {
getJson().set(getObjectFactory().newInstance(FabricModJsonV1Spec.class));
}
/**
* Configure the fabric.mod.json spec.
*
* @param action A {@link Action} that configures the spec.
*/
public void json(Action<FabricModJsonV1Spec> action) {
action.execute(getJson().get());
}
@TaskAction
public void run() {
final WorkQueue workQueue = getWorkerExecutor().noIsolation();
workQueue.submit(FabricModJsonV1WorkAction.class, params -> {
params.getSpec().set(getJson());
params.getOutputFile().set(getOutputFile());
});
}
public interface FabricModJsonV1WorkParameters extends WorkParameters {
Property<FabricModJsonV1Spec> getSpec();
RegularFileProperty getOutputFile();
}
public abstract static class FabricModJsonV1WorkAction implements WorkAction<FabricModJsonV1WorkParameters> {
@Override
public void execute() {
FabricModJsonV1Spec spec = getParameters().getSpec().get();
Path outputPath = getParameters().getOutputFile().get().getAsFile().toPath();
String json = FabricModJsonV1Generator.INSTANCE.generate(spec);
try {
Files.writeString(outputPath, json);
} catch (IOException e) {
throw new UncheckedIOException("Failed to write fabric.mod.json", e);
}
}
}
}

View File

@@ -166,18 +166,18 @@ public abstract class GenVsCodeProjectTask extends AbstractLoomTask {
Path projectPath = project.getProjectDir().toPath();
String relativeRunDir = rootPath.relativize(projectPath).resolve(runConfig.runDir).toString();
return new VsCodeConfiguration(
"java",
runConfig.configName,
"launch",
"${workspaceFolder}/" + relativeRunDir,
"integratedTerminal",
false,
runConfig.mainClass,
RunConfig.joinArguments(runConfig.vmArgs),
RunConfig.joinArguments(runConfig.programArgs),
new HashMap<>(runConfig.environmentVariables),
runConfig.projectName,
rootPath.resolve(relativeRunDir).toAbsolutePath().toString()
"java",
runConfig.configName,
"launch",
"${workspaceFolder}/" + relativeRunDir,
"integratedTerminal",
false,
runConfig.mainClass,
RunConfig.joinArguments(runConfig.vmArgs),
RunConfig.joinArguments(runConfig.programArgs),
new HashMap<>(runConfig.environmentVariables),
runConfig.projectName,
rootPath.resolve(relativeRunDir).toAbsolutePath().toString()
);
}
}

View File

@@ -28,7 +28,6 @@ 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;
@@ -45,6 +44,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
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.Check;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.Platform;
@@ -141,7 +141,7 @@ public abstract class LoomTasks implements Runnable {
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");
Check.require(extension.getRunConfigs().isEmpty(), "Run configurations must not be registered before loom");
extension.getRunConfigs().whenObjectAdded(config -> {
var runTask = getTasks().register(getRunConfigTaskName(config), RunGameTask.class, config);

View File

@@ -73,6 +73,7 @@ public abstract class RenderDocRunTask extends RunGameTask {
exec.args("--working-dir", new File(getProjectDir().get(), getInternalRunDir().get()));
exec.args(getJavaLauncher().get().getExecutablePath());
exec.args(getJvmArgs());
exec.args("-D%s=true".formatted(Constants.Properties.RENDER_DOC));
exec.args(getMainClass().get());
for (CommandLineArgumentProvider provider : getArgumentProviders()) {

View File

@@ -83,7 +83,7 @@ public abstract class ValidateAccessWidenerTask extends DefaultTask {
try (BufferedReader reader = Files.newBufferedReader(getAccessWidener().get().getAsFile().toPath(), StandardCharsets.UTF_8)) {
accessWidenerReader.read(reader, "named");
} catch (AccessWidenerFormatException e) {
getProject().getLogger().error("Failed to validate access-widener file {} on line {}: {}", getAccessWidener().get().getAsFile().getName(), e.getLineNumber(), e.getMessage());
getLogger().error("Failed to validate access-widener file {} on line {}: {}", getAccessWidener().get().getAsFile().getName(), e.getLineNumber(), e.getMessage());
throw e;
} catch (IOException e) {
throw new UncheckedIOException("Failed to read access widener", e);

View File

@@ -28,6 +28,7 @@ import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -42,7 +43,6 @@ import java.util.stream.Collectors;
import dev.architectury.loom.forge.config.ConfigValue;
import dev.architectury.loom.forge.config.ForgeRunTemplate;
import dev.architectury.loom.forge.dependency.ForgeRunsProvider;
import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
@@ -293,7 +293,7 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
launchConfig.property("fabric.log.disableAnsi", "false");
}
FileUtils.writeStringToFile(getDevLauncherConfig().getAsFile().get(), launchConfig.asString(), StandardCharsets.UTF_8);
Files.writeString(getDevLauncherConfig().getAsFile().get().toPath(), launchConfig.asString(), StandardCharsets.UTF_8);
}
private static String getAllLog4JConfigFiles(Project project) {

View File

@@ -28,7 +28,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
import dev.architectury.loom.mappings.MappingOption;
import org.cadixdev.lorenz.MappingSet;
import org.gradle.api.Project;
@@ -38,6 +37,7 @@ import org.gradle.api.tasks.Nested;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.util.Lazy;
import net.fabricmc.loom.util.service.Service;
import net.fabricmc.loom.util.service.ServiceFactory;
import net.fabricmc.loom.util.service.ServiceType;
@@ -65,7 +65,7 @@ public final class LorenzMappingService extends Service<LorenzMappingService.Opt
));
}
private final Supplier<MappingSet> mappings = Suppliers.memoize(this::readMappings);
private final Supplier<MappingSet> mappings = Lazy.of(this::readMappings);
public LorenzMappingService(Options options, ServiceFactory serviceFactory) {
super(options, serviceFactory);

View File

@@ -29,7 +29,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.cadixdev.lorenz.MappingSet;
import org.cadixdev.mercury.Mercury;
@@ -181,7 +180,8 @@ 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(Map.of("group", "net.fabricmc", "name", "yarn", "version", mappings, "classifier", "v2")));
String mavenNotation = "net.fabricmc:yarn:%s:v2".formatted(mappings);
return project.getConfigurations().detachedConfiguration(project.getDependencies().create(mavenNotation));
} catch (IOException e) {
throw new UncheckedIOException("Failed to resolve mappings", e);
}

View File

@@ -0,0 +1,129 @@
/*
* 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.tool;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.problems.ProblemId;
import org.gradle.api.problems.ProblemReporter;
import org.gradle.api.problems.Problems;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.UntrackedTask;
import org.gradle.process.ExecOperations;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.task.AbstractLoomTask;
import net.fabricmc.loom.util.LoomProblems;
import net.fabricmc.loom.util.LoomVersions;
/**
* Add this task to a mod development environment to use Enigma against the game jars.
* This can be used for writing mod-provided javadoc etc.
*
* <p>Usage:
* {@snippet lang=groovy :
* tasks.register('enigma', ModEnigmaTask) {
* // Must be a single Enigma-formatted mapping file:
* mappingFile = file('src/main/resources/my_mod_data.mapping')
* }
* }
*/
@UntrackedTask(because = "Enigma should always launch")
public abstract class ModEnigmaTask extends AbstractLoomTask {
private static final ProblemId MAPPINGS_MISSING_PROBLEM = LoomProblems.problemId("mappings-missing", "Mapping file doesn't exist");
private static final String ENIGMA_MAIN_CLASS = "cuchaz.enigma.gui.Main";
// Must be a ListProperty because the order matters.
@Input
public abstract ListProperty<Path> getMinecraftJars();
/**
* The mapping file path. It must be a single Enigma-formatted file.
*/
@OutputFile
public abstract RegularFileProperty getMappingFile();
/**
* The Enigma classpath. You can add any Enigma plugin files to this file collection.
*/
@Classpath
public abstract ConfigurableFileCollection getToolClasspath();
@ApiStatus.Internal
@Inject
protected abstract ExecOperations getExecOperations();
@ApiStatus.Internal
@Inject
protected abstract Problems getProblems();
public ModEnigmaTask() {
getMinecraftJars().convention(getProject().provider(() -> getExtension().getMinecraftJars(MappingsNamespace.INTERMEDIARY)));
getToolClasspath().from(getEnigmaClasspath(getProject()));
}
private static FileCollection getEnigmaClasspath(Project project) {
final Dependency enigmaDep = project.getDependencies().create(LoomVersions.ENIGMA_SWING.mavenNotation());
return project.getConfigurations().detachedConfiguration(enigmaDep);
}
@TaskAction
public void launch() {
final Path mappingFile = getMappingFile().get().getAsFile().toPath().toAbsolutePath();
if (Files.notExists(mappingFile)) {
final ProblemReporter reporter = getProblems().getReporter();
reporter.throwing(new RuntimeException("Mapping file " + mappingFile + " doesn't exist"), MAPPINGS_MISSING_PROBLEM,
spec -> spec
.fileLocation(mappingFile.toString())
.solution("Create the missing mapping file. Remember to add it to the fabric.mod.json if needed!"));
}
getExecOperations().javaexec(spec -> {
spec.getMainClass().set(ENIGMA_MAIN_CLASS);
spec.setClasspath(getToolClasspath());
spec.jvmArgs("-Xmx2048m");
for (Path path : getMinecraftJars().get()) {
spec.args("-jar", path.toAbsolutePath().toString());
}
spec.args("-mappings", mappingFile.toString());
});
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class AsyncCache<T> {
private static final Executor EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
private final Map<Object, CompletableFuture<T>> cache = new ConcurrentHashMap<>();
public CompletableFuture<T> get(Object cacheKey, Supplier<T> supplier) {
return cache.computeIfAbsent(cacheKey, $ -> CompletableFuture.supplyAsync(supplier, EXECUTOR));
}
public T getBlocking(Object cacheKey, Supplier<T> supplier) {
return join(get(cacheKey, supplier));
}
public static <T> List<T> joinList(Collection<CompletableFuture<List<T>>> futures) {
return join(futures.stream()
.collect(CompletableFutureCollector.allOf()))
.stream()
.flatMap(List::stream)
.toList();
}
public static <K, V> Map<K, V> joinMap(Map<K, CompletableFuture<V>> futures) {
return futures.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> join(entry.getValue())
));
}
// Rethrows the exception from the CompletableFuture, if it exists.
public static <T> T join(CompletableFuture<T> future) {
try {
return future.join();
} catch (CompletionException e) {
sneakyThrow(e.getCause() != null ? e.getCause() : e);
throw new IllegalStateException();
}
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
}

View File

@@ -26,7 +26,6 @@ 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;
@@ -38,8 +37,8 @@ import net.fabricmc.loom.util.gradle.GradleTypeAdapter;
*/
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));
private final transient Supplier<String> jsonSupplier = Lazy.of(() -> GradleTypeAdapter.GSON.toJson(this));
private final transient Supplier<String> cacheKeySupplier = Lazy.of(() -> 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);

View File

@@ -0,0 +1,42 @@
/*
* 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;
public final class Check {
private Check() {
}
public static void require(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
public static void require(boolean expression) {
if (!expression) {
throw new IllegalArgumentException();
}
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2022 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
@@ -39,6 +39,7 @@ public class Constants {
public static final int ASM_VERSION = Opcodes.ASM9;
public static final String RELEASE_TIME_1_3 = "2012-07-25T22:00:00+00:00";
public static final String RELEASE_TIME_BETA_1_0 = "2010-12-19T22:00:00+00:00";
private Constants() {
}
@@ -183,6 +184,10 @@ public class Constants {
* 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";
/**
* Set to true in all {@link net.fabricmc.loom.task.RenderDocRunTask} can be used to determine at runtime if running with loom's renderdoc setup.
*/
public static final String RENDER_DOC = "fabric.loom.renderdoc.enabled";
public static final String ALLOW_MISMATCHED_PLATFORM_VERSION = "loom.allowMismatchedPlatformVersion";
public static final String IGNORE_DEPENDENCY_LOOM_VERSION_VALIDATION = "loom.ignoreDependencyLoomVersionValidation";
}

View File

@@ -32,6 +32,10 @@ import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
public class DeletingFileVisitor extends SimpleFileVisitor<Path> {
public static void deleteDirectory(Path directory) throws IOException {
Files.walkFileTree(directory, new DeletingFileVisitor());
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) throws IOException {
Files.delete(path);

View File

@@ -0,0 +1,66 @@
/*
* 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 org.jetbrains.annotations.Nullable;
// Can be replaced by Lazy Constants (https://openjdk.org/jeps/526) once available.
public final class Lazy {
private Lazy() {
}
public static <T> Supplier<T> of(Supplier<T> supplier) {
return new Impl<>(supplier);
}
private static final class Impl<T> implements Supplier<T> {
final Supplier<T> supplier;
volatile boolean initialized = false;
@Nullable
T value = null;
private Impl(Supplier<T> supplier) {
this.supplier = supplier;
}
@Override
public T get() {
// Classic double-checked locking pattern
if (!initialized) {
synchronized (this) {
if (!initialized) {
value = supplier.get();
initialized = true;
}
}
}
return value;
}
}
}

View File

@@ -26,10 +26,8 @@ package net.fabricmc.loom.util;
import java.util.List;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import kotlin.metadata.jvm.KotlinClassMetadata;
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.tree.ClassNode;
@@ -52,9 +50,7 @@ public final class LibraryLocationLogger {
ClassRemapper.class,
ClassNode.class,
ASMifier.class,
Gson.class,
Preconditions.class,
FileUtils.class
Gson.class
);
private static final Logger LOGGER = LoggerFactory.getLogger(LibraryLocationLogger.class);

View File

@@ -0,0 +1,39 @@
/*
* 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 org.gradle.api.problems.ProblemGroup;
import org.gradle.api.problems.ProblemId;
public final class LoomProblems {
public static final ProblemGroup PROBLEM_GROUP = ProblemGroup.create("loom", "Loom");
private LoomProblems() {
}
public static ProblemId problemId(String name, String displayName) {
return ProblemId.create(name, displayName, PROBLEM_GROUP);
}
}

View File

@@ -0,0 +1,29 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2025 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.fmj.gen;
public interface FabricModJsonGenerator<Spec> {
String generate(Spec spec);
}

View File

@@ -0,0 +1,200 @@
/*
* 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.fmj.gen;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.add;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addArray;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addRequired;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addStringOrArray;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec;
// Opposite of https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java
public final class FabricModJsonV1Generator implements FabricModJsonGenerator<FabricModJsonV1Spec> {
private static final int VERSION = 1;
public static final FabricModJsonV1Generator INSTANCE = new FabricModJsonV1Generator();
private FabricModJsonV1Generator() {
}
public String generate(FabricModJsonV1Spec spec) {
Objects.requireNonNull(spec);
JsonObject fmj = new JsonObject();
fmj.addProperty("schemaVersion", VERSION);
// Required
addRequired(fmj, "id", spec.getModId());
addRequired(fmj, "version", spec.getVersion());
// All other fields are optional
// Match the order as specified in V1ModMetadataParser to make it easier to compare
addArray(fmj, "provides", spec.getProvides(), JsonPrimitive::new);
add(fmj, "environment", spec.getEnvironment());
add(fmj, "entrypoints", spec.getEntrypoints(), this::generateEntrypoints);
addArray(fmj, "jars", spec.getJars(), this::generateJar);
addArray(fmj, "mixins", spec.getMixins(), this::generateMixins);
add(fmj, "accessWidener", spec.getAccessWidener());
add(fmj, "depends", spec.getDepends(), this::generateDependencies);
add(fmj, "recommends", spec.getRecommends(), this::generateDependencies);
add(fmj, "suggests", spec.getSuggests(), this::generateDependencies);
add(fmj, "conflicts", spec.getConflicts(), this::generateDependencies);
add(fmj, "breaks", spec.getBreaks(), this::generateDependencies);
add(fmj, "name", spec.getName());
add(fmj, "description", spec.getDescription());
addArray(fmj, "authors", spec.getAuthors(), this::generatePerson);
addArray(fmj, "contributors", spec.getContributors(), this::generatePerson);
add(fmj, "contact", spec.getContactInformation());
addStringOrArray(fmj, "license", spec.getLicenses());
add(fmj, "icon", spec.getIcons(), this::generateIcon);
add(fmj, "languageAdapters", spec.getLanguageAdapters());
add(fmj, "custom", spec.getCustomData(), this::generateCustomData);
return LoomGradlePlugin.GSON.toJson(fmj);
}
private JsonElement generatePerson(FabricModJsonV1Spec.Person person) {
if (person.getContactInformation().get().isEmpty()) {
return new JsonPrimitive(person.getName().get());
}
JsonObject json = new JsonObject();
addRequired(json, "name", person.getName());
add(json, "contact", person.getContactInformation());
return json;
}
private JsonObject generateEntrypoints(List<FabricModJsonV1Spec.Entrypoint> entrypoints) {
Map<String, List<FabricModJsonV1Spec.Entrypoint>> entrypointsMap = entrypoints.stream()
.collect(Collectors.groupingBy(entrypoint -> entrypoint.getEntrypoint().get()));
JsonObject json = new JsonObject();
entrypointsMap.forEach((entrypoint, entries) -> json.add(entrypoint, generateEntrypoint(entries)));
return json;
}
private JsonArray generateEntrypoint(List<FabricModJsonV1Spec.Entrypoint> entries) {
JsonArray json = new JsonArray();
for (FabricModJsonV1Spec.Entrypoint entry : entries) {
json.add(generateEntrypointEntry(entry));
}
return json;
}
private JsonElement generateEntrypointEntry(FabricModJsonV1Spec.Entrypoint entrypoint) {
if (!entrypoint.getAdapter().isPresent()) {
return new JsonPrimitive(entrypoint.getValue().get());
}
JsonObject json = new JsonObject();
addRequired(json, "value", entrypoint.getValue());
addRequired(json, "adapter", entrypoint.getAdapter());
return json;
}
private JsonObject generateJar(String jar) {
JsonObject json = new JsonObject();
json.addProperty("file", jar);
return json;
}
private JsonElement generateMixins(FabricModJsonV1Spec.Mixin mixin) {
if (!mixin.getEnvironment().isPresent()) {
return new JsonPrimitive(mixin.getValue().get());
}
JsonObject json = new JsonObject();
addRequired(json, "config", mixin.getValue());
addRequired(json, "environment", mixin.getEnvironment());
return json;
}
private JsonObject generateDependencies(List<FabricModJsonV1Spec.Dependency> dependencies) {
JsonObject json = new JsonObject();
for (FabricModJsonV1Spec.Dependency dependency : dependencies) {
json.add(dependency.getModId().get(), generateDependency(dependency));
}
return json;
}
private JsonElement generateDependency(FabricModJsonV1Spec.Dependency dependency) {
List<String> requirements = dependency.getVersionRequirements().get();
if (requirements.isEmpty()) {
throw new IllegalStateException("Dependency " + dependency.getModId().get() + " must have at least one version requirement");
}
if (requirements.size() == 1) {
return new JsonPrimitive(dependency.getModId().get());
}
JsonArray json = new JsonArray();
for (String s : requirements) {
json.add(s);
}
return json;
}
private JsonElement generateIcon(List<FabricModJsonV1Spec.Icon> icons) {
if (icons.size() == 1 && !icons.getFirst().getSize().isPresent()) {
return new JsonPrimitive(icons.getFirst().getPath().get());
}
JsonObject json = new JsonObject();
for (FabricModJsonV1Spec.Icon icon : icons) {
String size = String.valueOf(icon.getSize().get());
json.addProperty(size, icon.getPath().get());
}
return json;
}
private JsonObject generateCustomData(Map<String, Object> customData) {
JsonObject json = new JsonObject();
customData.forEach((name, o) -> json.add(name, LoomGradlePlugin.GSON.toJsonTree(o)));
return json;
}
}

View File

@@ -0,0 +1,143 @@
/*
* 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.fmj.gen;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
public final class GeneratorUtils {
private GeneratorUtils() {
}
public static void add(JsonObject json, String key, Property<String> property) {
add(json, key, property, JsonPrimitive::new);
}
public static void addRequired(JsonObject json, String key, Property<String> property) {
addRequired(json, key, property, JsonPrimitive::new);
}
public static void addStringOrArray(JsonObject json, String key, ListProperty<String> property) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, GeneratorUtils::stringOrArray);
}
public static <V> void addSingleOrArray(JsonObject json, String key, ListProperty<V> property, Function<V, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, entries -> singleOrArray(entries, converter));
}
public static <V> void addArray(JsonObject json, String key, ListProperty<V> property, Function<V, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, entries -> array(entries, converter));
}
public static <V, P extends Property<V>> void add(JsonObject json, String key, P property, Function<V, JsonElement> converter) {
if (!property.isPresent()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static <V, P extends Property<V>> void addRequired(JsonObject json, String key, P property, Function<V, JsonElement> converter) {
property.get(); // Ensure it's present
add(json, key, property, converter);
}
public static <V> void add(JsonObject json, String key, ListProperty<V> property, Function<List<V>, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static <K, V> void add(JsonObject json, String key, MapProperty<K, V> property, Function<Map<K, V>, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static void add(JsonObject json, String key, MapProperty<String, String> property) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, GeneratorUtils::map);
}
public static JsonElement stringOrArray(List<String> strings) {
return singleOrArray(strings, JsonPrimitive::new);
}
public static <V> JsonElement singleOrArray(List<V> entries, Function<V, JsonElement> converter) {
if (entries.size() == 1) {
return converter.apply(entries.getFirst());
}
return array(entries, converter);
}
public static <V> JsonElement array(List<V> entries, Function<V, JsonElement> converter) {
JsonArray array = new JsonArray();
for (V entry : entries) {
array.add(converter.apply(entry));
}
return array;
}
public static JsonObject map(Map<String, String> map) {
JsonObject obj = new JsonObject();
for (Map.Entry<String, String> entry : map.entrySet()) {
obj.addProperty(entry.getKey(), entry.getValue());
}
return obj;
}
}

View File

@@ -24,16 +24,17 @@
package net.fabricmc.loom.util.gradle;
import com.google.common.base.Preconditions;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import net.fabricmc.loom.util.Check;
/**
* A reference to a {@link SourceSet} from a {@link Project}.
*/
public record SourceSetReference(SourceSet sourceSet, Project project) {
public SourceSetReference {
Preconditions.checkArgument(
Check.require(
SourceSetHelper.isSourceSetOfProject(sourceSet, project),
"SourceSet (%s) does not own to (%s) project".formatted(sourceSet.getName(), project.getName())
);

View File

@@ -36,8 +36,7 @@ class KotlinClassMetadataRemappingAnnotationVisitor(
private val remapper: Remapper,
val next: AnnotationVisitor,
val className: String?,
) :
AnnotationNode(Opcodes.ASM9, KotlinMetadataRemappingClassVisitor.ANNOTATION_DESCRIPTOR) {
) : AnnotationNode(Opcodes.ASM9, KotlinMetadataRemappingClassVisitor.ANNOTATION_DESCRIPTOR) {
private val logger = LoggerFactory.getLogger(javaClass)
override fun visit(
@@ -100,12 +99,13 @@ class KotlinClassMetadataRemappingAnnotationVisitor(
var kpackage = metadata.kmPackage
kpackage = KotlinClassRemapper(remapper).remap(kpackage)
val remapped =
KotlinClassMetadata.MultiFileClassPart(
kpackage,
metadata.facadeClassName,
metadata.version,
metadata.flags,
).write()
KotlinClassMetadata
.MultiFileClassPart(
kpackage,
metadata.facadeClassName,
metadata.version,
metadata.flags,
).write()
writeClassHeader(remapped)
validateKotlinClassHeader(remapped, header)
}

View File

@@ -55,7 +55,9 @@ import kotlin.metadata.jvm.syntheticMethodForDelegate
import kotlin.metadata.jvm.toJvmInternalName
@OptIn(ExperimentalContextReceivers::class)
class KotlinClassRemapper(private val remapper: Remapper) {
class KotlinClassRemapper(
private val remapper: Remapper,
) {
fun remap(clazz: KmClass): KmClass {
clazz.name = remap(clazz.name)
clazz.typeParameters.replaceAll(this::remap)
@@ -154,13 +156,16 @@ class KotlinClassRemapper(private val remapper: Remapper) {
return typeParameter
}
private fun remap(typeProjection: KmTypeProjection): KmTypeProjection {
return KmTypeProjection(typeProjection.variance, typeProjection.type?.let { remap(it) })
}
private fun remap(typeProjection: KmTypeProjection): KmTypeProjection =
KmTypeProjection(
typeProjection.variance,
typeProjection.type?.let {
remap(it)
},
)
private fun remap(flexibleTypeUpperBound: KmFlexibleTypeUpperBound): KmFlexibleTypeUpperBound {
return KmFlexibleTypeUpperBound(remap(flexibleTypeUpperBound.type), flexibleTypeUpperBound.typeFlexibilityId)
}
private fun remap(flexibleTypeUpperBound: KmFlexibleTypeUpperBound): KmFlexibleTypeUpperBound =
KmFlexibleTypeUpperBound(remap(flexibleTypeUpperBound.type), flexibleTypeUpperBound.typeFlexibilityId)
private fun remap(valueParameter: KmValueParameter): KmValueParameter {
valueParameter.type = remap(valueParameter.type)
@@ -168,15 +173,11 @@ class KotlinClassRemapper(private val remapper: Remapper) {
return valueParameter
}
private fun remap(annotation: KmAnnotation): KmAnnotation {
return KmAnnotation(remap(annotation.className), annotation.arguments)
}
private fun remap(annotation: KmAnnotation): KmAnnotation = KmAnnotation(remap(annotation.className), annotation.arguments)
private fun remap(signature: JvmMethodSignature): JvmMethodSignature {
return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor))
}
private fun remap(signature: JvmMethodSignature): JvmMethodSignature =
JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor))
private fun remap(signature: JvmFieldSignature): JvmFieldSignature {
return JvmFieldSignature(signature.name, remapper.mapDesc(signature.descriptor))
}
private fun remap(signature: JvmFieldSignature): JvmFieldSignature =
JvmFieldSignature(signature.name, remapper.mapDesc(signature.descriptor))
}

View File

@@ -31,7 +31,10 @@ import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.Remapper
class KotlinMetadataRemappingClassVisitor(private val remapper: Remapper, next: ClassVisitor?) : ClassVisitor(Opcodes.ASM9, next) {
class KotlinMetadataRemappingClassVisitor(
private val remapper: Remapper,
next: ClassVisitor?,
) : ClassVisitor(Opcodes.ASM9, next) {
companion object {
val ANNOTATION_DESCRIPTOR: String = Type.getDescriptor(Metadata::class.java)
}
@@ -68,7 +71,5 @@ class KotlinMetadataRemappingClassVisitor(private val remapper: Remapper, next:
}
@VisibleForTesting
fun getRuntimeKotlinVersion(): String {
return KotlinVersion.CURRENT.toString()
}
fun getRuntimeKotlinVersion(): String = KotlinVersion.CURRENT.toString()
}

View File

@@ -33,9 +33,7 @@ object KotlinMetadataTinyRemapperExtensionImpl : KotlinMetadataTinyRemapperExten
override fun insertApplyVisitor(
cls: TrClass,
next: ClassVisitor?,
): ClassVisitor {
return KotlinMetadataRemappingClassVisitor(cls.environment.remapper, next)
}
): ClassVisitor = KotlinMetadataRemappingClassVisitor(cls.environment.remapper, next)
override fun attach(builder: TinyRemapper.Builder) {
builder.extraPreApplyVisitor(this)

View File

@@ -48,10 +48,10 @@ class FabricAPIBenchmark implements GradleProjectTestTrait {
patch: "fabric_api"
)
if (!gradle.buildGradle.text.contains("loom.mixin.useLegacyMixinAp")) {
if (gradle.buildGradle.text.contains("loom.mixin.useLegacyMixinAp")) {
gradle.buildGradle << """
allprojects {
loom.mixin.useLegacyMixinAp = false
loom.mixin.useLegacyMixinAp = true
}
""".stripIndent()
}

View File

@@ -50,10 +50,10 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait {
)
// Disable the mixin ap if needed. Fabric API is a large enough test project to see if something breaks.
if (disableMixinAp) {
if (!disableMixinAp) {
gradle.buildGradle << """
allprojects {
loom.mixin.useLegacyMixinAp = false
loom.mixin.useLegacyMixinAp = true
}
""".stripIndent()
}

View File

@@ -0,0 +1,74 @@
/*
* 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.test.integration
import spock.lang.Specification
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class FabricModJsonTask extends Specification implements GradleProjectTestTrait {
@Unroll
def "Generate FMJ"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: version)
gradle.buildGradle << '''
dependencies {
minecraft "com.mojang:minecraft:1.21.8"
mappings "net.fabricmc:yarn:1.21.8+build.1:v2"
}
tasks.register("generateModJson", net.fabricmc.loom.task.FabricModJsonV1Task) {
outputFile = file("fabric.mod.json")
json {
modId = "examplemod"
version = "1.0.0"
}
}
'''
when:
// Run the task twice to ensure its up to date
def result = gradle.run(task: "generateModJson")
then:
result.task(":generateModJson").outcome == SUCCESS
new File(gradle.projectDir, "fabric.mod.json").text == """
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0"
}
""".stripIndent().trim()
where:
version << STANDARD_TEST_VERSIONS
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2018-2022 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
@@ -24,6 +24,7 @@
package net.fabricmc.loom.test.integration
import java.nio.file.Files
import java.nio.file.Path
import spock.lang.Specification
@@ -125,7 +126,7 @@ class LegacyProjectTest extends Specification implements GradleProjectTestTrait
def "Legacy merged"() {
setup:
def mappings = Path.of("src/test/resources/mappings/1.2.5-intermediary.tiny.zip").toAbsolutePath()
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE)
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE, gradleHomeDir: File.createTempDir())
gradle.buildGradle << """
dependencies {
@@ -147,4 +148,40 @@ class LegacyProjectTest extends Specification implements GradleProjectTestTrait
then:
result.task(":build").outcome == SUCCESS
}
def "Legacy single jar version with mappings but no intermediates"() {
setup:
def mappings = Path.of('src/test/resources/mappings/0.30-minimal.tiny')
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE)
Files.copy(mappings, gradle.projectDir.toPath().resolve('mappings.tiny'))
gradle.buildGradle << """
loom.noIntermediateMappings()
dependencies {
minecraft "com.mojang:minecraft:c0.30_01c"
mappings loom.layered {
it.mappings file("mappings.tiny")
}
modImplementation "net.fabricmc:fabric-loader:0.15.7"
}
"""
def sourceFile = new File(gradle.projectDir, 'src/main/java/Test.java')
sourceFile.parentFile.mkdirs()
sourceFile.text = """
public final class Test {
public static void foo() {
// Reference a mapped class
System.out.println(com.mojang.minecraft.Minecraft.class);
}
}
"""
when:
def result = gradle.run(task: "build")
then:
result.task(":build").outcome == SUCCESS
}
}

View File

@@ -29,6 +29,7 @@ import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE
import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
@@ -89,7 +90,7 @@ class MultiMcVersionTest extends Specification implements GradleProjectTestTrait
// See: https://github.com/gradle/gradle/issues/30401
// By default parallel configuration of all projects is preferred.
args: [
"-Dorg.gradle.internal.isolated-projects.configure-on-demand.tasks=true"
"-Dorg.gradle.internal.isolated-projects.configure-on-demand=true"
])
then:
@@ -98,6 +99,6 @@ class MultiMcVersionTest extends Specification implements GradleProjectTestTrait
result.output.count("Isolated projects is enabled, Loom support is highly experimental, not all features will be enabled.") == 1
where:
version << STANDARD_TEST_VERSIONS
version << [PRE_RELEASE_GRADLE]
}
}

View File

@@ -87,12 +87,12 @@ class SimpleProjectTest extends Specification implements GradleProjectTestTrait
}
@Unroll
def "remap mixins with tiny-remapper"() {
def "remap mixins with mixin AP"() {
setup:
def gradle = gradleProject(project: "simple", version: PRE_RELEASE_GRADLE)
gradle.buildGradle << """
allprojects {
loom.mixin.useLegacyMixinAp = false
loom.mixin.useLegacyMixinAp = true
}
""".stripIndent()

View File

@@ -0,0 +1,45 @@
/*
* 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.test.unit
import spock.lang.Specification
import net.fabricmc.loom.util.AsyncCache
class AsyncCacheTest extends Specification {
def "rethrows error"() {
given:
def cache = new AsyncCache()
def cacheKey = "testKey"
def supplier = { throw new RuntimeException("Test exception") }
when:
cache.getBlocking(cacheKey, supplier)
then:
def e = thrown(RuntimeException)
e.message == "Test exception"
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
* Copyright (c) 2024-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
@@ -36,8 +36,6 @@ import net.fabricmc.mappingio.MappingReader
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch
import net.fabricmc.mappingio.tree.MemoryMappingTree
import static org.junit.jupiter.api.Assertions.*
class MappingsMergerTest {
@TempDir
Path tempDir
@@ -106,7 +104,7 @@ class MappingsMergerTest {
IntermediateMappingsService intermediateMappingsService = LoomMocks.intermediateMappingsServiceMock(intermediateMappingsServiceOptions)
when:
MappingsMerger.legacyMergeAndSaveMappings(mappingsTiny, mergedMappingsTiny, intermediateMappingsService)
MappingsMerger.legacyMergedMergeAndSaveMappings(mappingsTiny, mergedMappingsTiny, intermediateMappingsService)
def mappings = new MemoryMappingTree()
MappingReader.read(mergedMappingsTiny, mappings)

View File

@@ -0,0 +1,682 @@
/*
* 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.test.unit.fmj
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.intellij.lang.annotations.Language
import spock.lang.Specification
import net.fabricmc.loader.impl.metadata.ModMetadataParser
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec
import net.fabricmc.loom.test.util.GradleTestUtil
import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator
class FabricModJsonV1GeneratorTest extends Specification {
static Project project = GradleTestUtil.mockProject()
static ObjectFactory objectFactory = project.getObjects()
def "minimal"() {
given:
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0"
}
""")
tryParse(json) == 1
}
def "single license"() {
given:
def spec = baseSpec()
spec.licenses.add("MIT")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"license": "MIT"
}
""")
tryParse(json) == 1
}
def "multiple licenses"() {
given:
def spec = baseSpec()
spec.licenses.addAll("MIT", "Apache-2.0")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"license": [
"MIT",
"Apache-2.0"
]
}
""")
tryParse(json) == 1
}
def "named author"() {
given:
def spec = baseSpec()
spec.author("Epic Modder")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"authors": [
"Epic Modder"
]
}
""")
tryParse(json) == 1
}
def "author with contact info"() {
given:
def spec = baseSpec()
spec.author("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"authors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
]
}
""")
tryParse(json) == 1
}
def "named contributor"() {
given:
def spec = baseSpec()
spec.contributor("Epic Modder")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contributors": [
"Epic Modder"
]
}
""")
tryParse(json) == 1
}
def "contributor with contact info"() {
given:
def spec = baseSpec()
spec.contributor("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contributors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
]
}
""")
tryParse(json) == 1
}
def "contact info"() {
given:
def spec = baseSpec()
spec.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
""")
tryParse(json) == 1
}
def "provides"() {
given:
def spec = baseSpec()
spec.provides.set(['oldid', 'veryoldid'])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"provides": [
"oldid",
"veryoldid"
]
}
""")
tryParse(json) == 1
}
def "environment"() {
given:
def spec = baseSpec()
spec.environment.set("client")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"environment": "client"
}
""")
tryParse(json) == 1
}
def "jars"() {
given:
def spec = baseSpec()
spec.jars.set(["libs/some-lib.jar"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"jars": [
{
"file": "libs/some-lib.jar"
}
]
}
""")
tryParse(json) == 1
}
def "entrypoints"() {
given:
def spec = baseSpec()
spec.entrypoint("main", "com.example.Main")
spec.entrypoint("main", "com.example.Blocks")
spec.entrypoint("client", "com.example.KotlinClient::init") {
it.adapter.set("kotlin")
}
spec.entrypoint("client") {
it.value.set("com.example.Client")
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"entrypoints": {
"client": [
{
"value": "com.example.KotlinClient::init",
"adapter": "kotlin"
},
"com.example.Client"
],
"main": [
"com.example.Main",
"com.example.Blocks"
]
}
}
""")
tryParse(json) == 1
}
def "mixins"() {
given:
def spec = baseSpec()
spec.mixin("mymod.mixins.json")
spec.mixin("mymod.client.mixins.json") {
it.environment.set("client")
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"mixins": [
"mymod.mixins.json",
{
"config": "mymod.client.mixins.json",
"environment": "client"
}
]
}
""")
tryParse(json) == 1
}
def "access widener"() {
given:
def spec = baseSpec()
spec.accessWidener.set("mymod.accesswidener")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"accessWidener": "mymod.accesswidener"
}
""")
tryParse(json) == 1
}
def "depends"() {
given:
def spec = baseSpec()
spec.depends("fabricloader", ">=0.14.0")
spec.depends("fabric-api", [">=0.14.0", "<0.15.0"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"depends": {
"fabricloader": "fabricloader",
"fabric-api": [
"\\u003e\\u003d0.14.0",
"\\u003c0.15.0"
]
}
}
""")
tryParse(json) == 1
}
def "single icon"() {
given:
def spec = baseSpec()
spec.icon("icon.png")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"icon": "icon.png"
}
""")
tryParse(json) == 1
}
def "multiple icons"() {
given:
def spec = baseSpec()
spec.icon(64, "icon_64.png")
spec.icon(128, "icon_128.png")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"icon": {
"64": "icon_64.png",
"128": "icon_128.png"
}
}
""")
tryParse(json) == 1
}
def "language adapters"() {
given:
def spec = baseSpec()
spec.languageAdapters.put("kotlin", "net.fabricmc.loader.api.language.KotlinAdapter")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"languageAdapters": {
"kotlin": "net.fabricmc.loader.api.language.KotlinAdapter"
}
}
""")
tryParse(json) == 1
}
def "custom data"() {
given:
def spec = baseSpec()
spec.customData.put("examplemap", ["custom": "data"])
spec.customData.put("examplelist", [1, 2, 3])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"custom": {
"examplemap": {
"custom": "data"
},
"examplelist": [
1,
2,
3
]
}
}
""")
tryParse(json) == 1
}
def "complete"() {
given:
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
spec.name.set("Example Mod")
spec.description.set("This is an example mod.")
spec.licenses.addAll("MIT", "Apache-2.0")
spec.author("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
spec.contributor("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
spec.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
spec.provides.set(['oldid', 'veryoldid'])
spec.environment.set("client")
spec.jars.set(["libs/some-lib.jar"])
spec.entrypoint("main", "com.example.Main")
spec.entrypoint("main", "com.example.Blocks")
spec.entrypoint("client", "com.example.KotlinClient::init") {
it.adapter.set("kotlin")
}
spec.entrypoint("client") {
it.value.set("com.example.Client")
}
spec.mixin("mymod.mixins.json")
spec.mixin("mymod.client.mixins.json") {
it.environment.set("client")
}
spec.accessWidener.set("mymod.accesswidener")
spec.depends("fabricloader", ">=0.14.0")
spec.depends("fabric-api", [">=0.14.0", "<0.15.0"])
spec.recommends("recommended-mod", ">=1.0.0")
spec.suggests("suggested-mod", ">=1.0.0")
spec.conflicts("conflicting-mod", "<1.0.0")
spec.breaks("broken-mod", "<1.0.0")
spec.icon(64, "icon_64.png")
spec.icon(128, "icon_128.png")
spec.languageAdapters.put("kotlin", "net.fabricmc.loader.api.language.KotlinAdapter")
spec.customData.put("examplemap", ["custom": "data"])
spec.customData.put("examplelist", [1, 2, 3])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"provides": [
"oldid",
"veryoldid"
],
"environment": "client",
"entrypoints": {
"client": [
{
"value": "com.example.KotlinClient::init",
"adapter": "kotlin"
},
"com.example.Client"
],
"main": [
"com.example.Main",
"com.example.Blocks"
]
},
"jars": [
{
"file": "libs/some-lib.jar"
}
],
"mixins": [
"mymod.mixins.json",
{
"config": "mymod.client.mixins.json",
"environment": "client"
}
],
"accessWidener": "mymod.accesswidener",
"depends": {
"fabricloader": "fabricloader",
"fabric-api": [
"\\u003e\\u003d0.14.0",
"\\u003c0.15.0"
]
},
"recommends": {
"recommended-mod": "recommended-mod"
},
"suggests": {
"suggested-mod": "suggested-mod"
},
"conflicts": {
"conflicting-mod": "conflicting-mod"
},
"breaks": {
"broken-mod": "broken-mod"
},
"name": "Example Mod",
"description": "This is an example mod.",
"authors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
],
"contributors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
],
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
},
"license": [
"MIT",
"Apache-2.0"
],
"icon": {
"64": "icon_64.png",
"128": "icon_128.png"
},
"languageAdapters": {
"kotlin": "net.fabricmc.loader.api.language.KotlinAdapter"
},
"custom": {
"examplemap": {
"custom": "data"
},
"examplelist": [
1,
2,
3
]
}
}
""")
tryParse(json) == 1
}
// Ensure that Fabric loader can actually parse the generated JSON.
private static int tryParse(String json) {
def meta = new ByteArrayInputStream(json.bytes).withCloseable {
//noinspection GroovyAccessibility
ModMetadataParser.readModMetadata(it, false)
}
return meta.getSchemaVersion()
}
private static FabricModJsonV1Spec baseSpec() {
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
return spec
}
private static String j(@Language("JSON") String json) {
return json.stripIndent().trim()
}
}

View File

@@ -0,0 +1,257 @@
/*
* 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.test.unit.layeredmappings
import java.nio.file.Path
import groovy.transform.CompileStatic
import org.intellij.lang.annotations.Language
import spock.lang.Specification
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData
import net.fabricmc.tinyremapper.IMappingProvider
import net.fabricmc.tinyremapper.TinyRemapper
class AnnotationsDataRemapTest extends Specification {
def "remap annotations data"() {
given:
def reader = new BufferedReader(new StringReader(ANNOTATIONS))
def annotationsData = AnnotationsData.read(reader)
def remapper = TinyRemapper.newRemapper()
.withMappings { mappings ->
mappings.acceptClass('net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest$Foo', 'mapped/pkg/FooMapped')
mappings.acceptClass('pkg/Bar', 'mapped/pkg/BarMapped')
mappings.acceptClass('pkg/Annotation1', 'mapped/pkg/Annotation1Mapped')
mappings.acceptClass('pkg/Annotation2', 'mapped/pkg/Annotation2Mapped')
mappings.acceptClass('pkg/Annotation3', 'mapped/pkg/Annotation3Mapped')
mappings.acceptClass('pkg/Annotation4', 'mapped/pkg/Annotation4Mapped')
mappings.acceptClass('pkg/Annotation5', 'mapped/pkg/Annotation5Mapped')
mappings.acceptClass('pkg/Annotation6', 'mapped/pkg/Annotation6Mapped')
mappings.acceptClass('pkg/Annotation7', 'mapped/pkg/Annotation7Mapped')
mappings.acceptClass('pkg/Annotation8', 'mapped/pkg/Annotation8Mapped')
mappings.acceptClass('pkg/MyEnum', 'mapped/pkg/MyEnumMapped')
mappings.acceptClass('baz', 'mapped/baz')
mappings.acceptField(new IMappingProvider.Member('net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest$Foo', 'bar', 'Lnet/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest$Foo;'), 'barRenamed')
mappings.acceptMethod(new IMappingProvider.Member('net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest$Foo', 'bar', '()V'), 'barMethodRenamed')
}
.build()
remapper.readClassPath(Path.of(Foo.class.protectionDomain.codeSource.location.toURI()))
when:
def remapped = annotationsData.remap(remapper, "mapped")
then:
def json = AnnotationsData.GSON.newBuilder()
.setPrettyPrinting()
.create()
.toJson(remapped.toJson())
.replace(" ", "\t")
json == REMAPPED_ANNOTATIONS.trim()
}
@CompileStatic
class Foo {
Foo bar
void bar() {
}
}
@Language("JSON")
private static final String ANNOTATIONS = """
{
"version": 1,
"classes": {
"net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest${'$'}Foo": {
"remove": [
"pkg/Annotation1",
"pkg/Annotation2",
"pkg/Annotation3"
],
"add": [
{
"desc": "Lpkg/Annotation4;"
},
{
"desc": "Lpkg/Annotation5;",
"values": {
"foo": {
"type": "int",
"value": 42
},
"bar": {
"type": "class",
"value": "Ljava/lang/String;"
},
"baz": {
"type": "enum_constant",
"owner": "Lpkg/MyEnum;",
"name": "VALUE"
},
"ann": {
"type": "annotation",
"desc": "Lpkg/Annotation6;"
},
"arr": {
"type": "array",
"value": [
{
"type": "int",
"value": 1
},
{
"type": "int",
"value": 2
}
]
}
}
}
],
"type_add": [
{
"desc": "Lpkg/Annotation7;",
"type_ref": 22,
"type_path": "["
}
],
"fields": {
"bar:Lnet/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest${'$'}Foo;": {
"remove": [
"pkg/Annotation8"
]
}
},
"methods": {
"bar()V": {
"remove": [
"pkg/Annotation8"
]
}
}
},
"pkg/Bar": {
"add": [
{
"desc": "Lpkg/Annotation1;"
}
]
}
},
"namespace": "someNamespace"
}
"""
@Language("JSON")
private static final String REMAPPED_ANNOTATIONS = """
{
"version": 1,
"classes": {
"mapped/pkg/FooMapped": {
"remove": [
"mapped/pkg/Annotation1Mapped",
"mapped/pkg/Annotation2Mapped",
"mapped/pkg/Annotation3Mapped"
],
"add": [
{
"desc": "Lmapped/pkg/Annotation4Mapped;"
},
{
"desc": "Lmapped/pkg/Annotation5Mapped;",
"values": {
"foo": {
"type": "int",
"value": 42
},
"bar": {
"type": "class",
"value": "Ljava/lang/String;"
},
"baz": {
"type": "enum_constant",
"owner": "Lmapped/pkg/MyEnumMapped;",
"name": "VALUE"
},
"ann": {
"type": "annotation",
"desc": "Lmapped/pkg/Annotation6Mapped;"
},
"arr": {
"type": "array",
"value": [
{
"type": "int",
"value": 1
},
{
"type": "int",
"value": 2
}
]
}
}
}
],
"type_add": [
{
"desc": "Lmapped/pkg/Annotation7Mapped;",
"type_ref": 22,
"type_path": "["
}
],
"fields": {
"barRenamed:Lmapped/pkg/FooMapped;": {
"remove": [
"mapped/pkg/Annotation8Mapped"
]
}
},
"methods": {
"barMethodRenamed()V": {
"remove": [
"mapped/pkg/Annotation8Mapped"
]
}
}
},
"mapped/pkg/BarMapped": {
"add": [
{
"desc": "Lmapped/pkg/Annotation1Mapped;"
}
]
}
},
"namespace": "mapped"
}
"""
}

View File

@@ -0,0 +1,157 @@
/*
* 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.test.unit.layeredmappings
import org.intellij.lang.annotations.Language
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AnnotationNode
import spock.lang.Specification
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData
class AnnotationsLayerTest extends Specification {
def "read annotations"() {
when:
def reader = new BufferedReader(new StringReader(ANNOTATIONS))
def annotationsData = AnnotationsData.read(reader)
then:
annotationsData.classes().size() == 2
annotationsData.classes()["pkg/Foo"].annotationsToRemove() == [
"pkg/Annotation1",
"pkg/Annotation2",
"pkg/Annotation3"
] as Set
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[0].desc == "Lpkg/Annotation4;"
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[1] == 42
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[3] == Type.getType("Ljava/lang/String;")
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[5] == ["Lpkg/MyEnum;", "VALUE"] as String[]
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7] instanceof AnnotationNode && annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7].desc == "Lpkg/Annotation6;"
annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[9] == [1, 2]
annotationsData.classes()["pkg/Foo"].typeAnnotationsToAdd()[0].typePath.toString() == "["
annotationsData.classes()["pkg/Foo"].fields().keySet().first() == "bar:Lbaz;"
annotationsData.classes()["pkg/Foo"].methods().keySet().first() == "bar()V"
annotationsData.classes()["pkg/Foo"].methods().values().first().typeAnnotationsToAdd().isEmpty()
}
def "write annotations"() {
when:
def reader = new BufferedReader(new StringReader(ANNOTATIONS))
def annotationsData = AnnotationsData.read(reader)
def json = AnnotationsData.GSON.newBuilder()
.setPrettyPrinting()
.create()
.toJson(annotationsData.toJson())
.replace(" ", "\t")
then:
json == ANNOTATIONS.trim()
}
@Language("JSON")
private static final String ANNOTATIONS = """
{
"version": 1,
"classes": {
"pkg/Foo": {
"remove": [
"pkg/Annotation1",
"pkg/Annotation2",
"pkg/Annotation3"
],
"add": [
{
"desc": "Lpkg/Annotation4;"
},
{
"desc": "Lpkg/Annotation5;",
"values": {
"foo": {
"type": "int",
"value": 42
},
"bar": {
"type": "class",
"value": "Ljava/lang/String;"
},
"baz": {
"type": "enum_constant",
"owner": "Lpkg/MyEnum;",
"name": "VALUE"
},
"ann": {
"type": "annotation",
"desc": "Lpkg/Annotation6;"
},
"arr": {
"type": "array",
"value": [
{
"type": "int",
"value": 1
},
{
"type": "int",
"value": 2
}
]
}
}
}
],
"type_add": [
{
"desc": "Lpkg/Annotation7;",
"type_ref": 22,
"type_path": "["
}
],
"fields": {
"bar:Lbaz;": {
"remove": [
"java/lang/Deprecated"
]
}
},
"methods": {
"bar()V": {
"remove": [
"java/lang/Deprecated"
]
}
}
},
"pkg/Bar": {
"add": [
{
"desc": "Lpkg/Annotation1;"
}
]
}
},
"namespace": "someNamespace"
}
"""
}

View File

@@ -113,7 +113,7 @@ class LayeredMappingSpecBuilderTest extends LayeredMappingsSpecification {
}
def layers = spec.layers()
then:
spec.version == "layered+hash.1133958200"
spec.version == "layered+hash.771237341"
layers.size() == 2
layers[0].class == IntermediaryMappingsSpec
layers[1].class == FileMappingsSpec

View File

@@ -0,0 +1,283 @@
/*
* 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.test.unit.processor
import groovy.transform.CompileStatic
import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.util.Textifier
import org.objectweb.asm.util.TraceClassVisitor
import spock.lang.Specification
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData
import net.fabricmc.loom.configuration.providers.minecraft.AnnotationsApplyVisitor
class AnnotationsApplyTest extends Specification {
def "apply annotations"() {
given:
def annotationData = AnnotationsData.read(new StringReader(ANNOTATIONS_DATA))
def annotatedNode1 = new ClassNode()
def classVisitor1 = new AnnotationsApplyVisitor.AnnotationsApplyClassVisitor(annotatedNode1, annotationData.classes().get('net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1'))
def annotatedNode2 = new ClassNode()
def classVisitor2 = new AnnotationsApplyVisitor.AnnotationsApplyClassVisitor(annotatedNode2, annotationData.classes().get('net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2'))
when:
def classReader1 = new ClassReader(getClassBytes(ExampleClass1))
classReader1.accept(classVisitor1, ClassReader.SKIP_CODE)
def text1 = textifyImportantPartsOfClass(annotatedNode1)
def field1Text = textify(annotatedNode1.fields.find { it.name == "field1" })
def field2Text = textify(annotatedNode1.fields.find { it.name == "field2" })
def method1Text = textify(annotatedNode1.methods.find { it.name == "method1" })
def method2Text = textify(annotatedNode1.methods.find { it.name == "method2" })
//noinspection GrDeprecatedAPIUsage
def classReader2 = new ClassReader(getClassBytes(ExampleClass2))
classReader2.accept(classVisitor2, ClassReader.SKIP_CODE)
def text2 = textifyImportantPartsOfClass(annotatedNode2)
then:
text1 == EXPECTED_TEXT1
text2 == EXPECTED_TEXT2
field1Text == EXPECTED_FIELD1
field2Text == EXPECTED_FIELD2
method1Text == EXPECTED_METHOD1
method2Text == EXPECTED_METHOD2
}
static byte[] getClassBytes(Class<?> clazz) {
return clazz.classLoader.getResourceAsStream(clazz.name.replace('.', '/') + ".class").withCloseable {
it.bytes
}
}
static String textify(FieldNode field) {
def cv = new TraceClassVisitor(null)
field.accept(cv)
def sw = new StringWriter()
cv.p.print(new PrintWriter(sw))
return sw.toString()
}
static String textify(MethodNode method) {
def cv = new TraceClassVisitor(null)
method.accept(cv)
def sw = new StringWriter()
cv.p.print(new PrintWriter(sw))
return sw.toString()
}
static String textifyImportantPartsOfClass(ClassNode clazz) {
ClassNode strippedClass = new ClassNode()
clazz.accept(strippedClass)
strippedClass.version = 52
strippedClass.fields.clear()
strippedClass.methods.clear()
def stringWriter = new StringWriter()
def printWriter = new PrintWriter(stringWriter)
def textifier = new Textifier()
def traceClassVisitor = new TraceClassVisitor(null, textifier, printWriter)
strippedClass.accept(traceClassVisitor)
return stringWriter.toString()
}
@CompileStatic
@ApiStatus.Internal
class ExampleClass1 {
@Deprecated
String field1
@Nullable
String field2
@Deprecated
void method1(@NotNull String parameter) {
}
void method2() {
}
}
@CompileStatic
@Deprecated
class ExampleClass2 {
}
@Language("JSON")
private static final String ANNOTATIONS_DATA = '''
{
"version": 1,
"classes": {
"net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1": {
"remove": [
"org/jetbrains/annotations/ApiStatus$Internal"
],
"add": [
{
"desc": "Ljava/lang/Deprecated;"
}
],
"fields": {
"field1:Ljava/lang/String;": {
"remove": [
"java/lang/Deprecated"
],
"add": [
{
"desc": "Lorg/jetbrains/annotations/ApiStatus$Internal;"
}
]
},
"field2:Ljava/lang/String;": {
"remove": [
"org/jetbrains/annotations/Nullable"
],
"add": [
{
"desc": "Ljava/lang/Deprecated;"
},
{
"desc": "Lorg/jetbrains/annotations/ApiStatus$Internal;"
}
]
}
},
"methods": {
"method1(Ljava/lang/String;)V": {
"remove": [
"java/lang/Deprecated"
],
"add": [
{
"desc": "Lorg/jetbrains/annotations/ApiStatus$OverrideOnly;"
}
],
"parameters": {
"0": {
"remove": [
"org/jetbrains/annotations/NotNull"
],
"add": [
{
"desc": "Lorg/jetbrains/annotations/UnknownNullability;"
}
]
}
}
},
"method2()V": {
"add": [
{
"desc": "Ljava/lang/Deprecated;"
},
{
"desc": "Lorg/jetbrains/annotations/Contract;",
"values": {
"pure": {
"type": "boolean",
"value": true
}
}
}
]
}
}
},
"net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2": {
"remove": [
"java/lang/Deprecated"
],
"add": [
{
"desc": "Lorg/jetbrains/annotations/ApiStatus$Internal;"
}
]
}
}
}
'''
private static final String EXPECTED_TEXT1 = '''// class version 52.0 (52)
// DEPRECATED
// access flags 0x20021
public class net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1 implements groovy/lang/GroovyObject {
// compiled from: AnnotationsApplyTest.groovy
@Ljava/lang/Deprecated;() // invisible
// access flags 0x1
public INNERCLASS net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1 net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest ExampleClass1
}
'''
private static final String EXPECTED_TEXT2 = '''// class version 52.0 (52)
// access flags 0x21
public class net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2 implements groovy/lang/GroovyObject {
// compiled from: AnnotationsApplyTest.groovy
@Lorg/jetbrains/annotations/ApiStatus$Internal;() // invisible
// access flags 0x1
public INNERCLASS net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2 net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest ExampleClass2
}
'''
private static final String EXPECTED_FIELD1 = '''
// access flags 0x2
private Ljava/lang/String; field1
@Lorg/jetbrains/annotations/ApiStatus$Internal;() // invisible
'''
private static final String EXPECTED_FIELD2 = '''
// DEPRECATED
// access flags 0x20002
private Ljava/lang/String; field2
@Ljava/lang/Deprecated;() // invisible
@Lorg/jetbrains/annotations/ApiStatus$Internal;() // invisible
'''
private static final String EXPECTED_METHOD1 = '''
// access flags 0x1
public method1(Ljava/lang/String;)V
@Lorg/jetbrains/annotations/ApiStatus$OverrideOnly;() // invisible
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/UnknownNullability;() // invisible, parameter 0
'''
private static final String EXPECTED_METHOD2 = '''
// DEPRECATED
// access flags 0x20001
public method2()V
@Ljava/lang/Deprecated;() // invisible
@Lorg/jetbrains/annotations/Contract;(pure=true) // invisible
'''
}

View File

@@ -0,0 +1,73 @@
/*
* 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.test.unit.providers
import spock.lang.Specification
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftClassMerger
import static org.objectweb.asm.Opcodes.ACC_FINAL
import static org.objectweb.asm.Opcodes.ACC_PRIVATE
import static org.objectweb.asm.Opcodes.ACC_PROTECTED
import static org.objectweb.asm.Opcodes.ACC_PUBLIC
import static org.objectweb.asm.Opcodes.ACC_STATIC
class MinecraftClassMergerTest extends Specification {
// Defined here as we cannot use bitwise OR in the where block
private static int ACC_PUBLIC_STATIC = ACC_PUBLIC | ACC_STATIC
private static int ACC_PRIVATE_STATIC = ACC_PRIVATE | ACC_STATIC
private static int ACC_PRIVATE_FINAL = ACC_PRIVATE | ACC_FINAL
def "merge access"() {
when:
def merged = MinecraftClassMerger.mergeAccess(client, server)
then:
MinecraftClassMerger.formatMethodAccessFlags(merged) == MinecraftClassMerger.formatMethodAccessFlags(expected)
where:
client | server | expected
ACC_PUBLIC | ACC_PUBLIC | ACC_PUBLIC
ACC_PRIVATE | ACC_PUBLIC | ACC_PRIVATE
ACC_PUBLIC | ACC_PRIVATE | ACC_PRIVATE
ACC_PROTECTED | ACC_PRIVATE | ACC_PRIVATE
ACC_PROTECTED | ACC_PUBLIC | ACC_PROTECTED
ACC_PUBLIC_STATIC | ACC_PRIVATE_STATIC | ACC_PRIVATE_STATIC
}
def "cannot merge access"() {
when:
MinecraftClassMerger.mergeAccess(client, server)
then:
thrown(IllegalStateException)
where:
client | server
ACC_PRIVATE_STATIC | ACC_PUBLIC
ACC_PRIVATE | ACC_PRIVATE_STATIC
ACC_PRIVATE_FINAL | ACC_PUBLIC
}
}

View File

@@ -0,0 +1,161 @@
/*
* 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.test.unit.providers
import java.nio.file.Path
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import spock.lang.Specification
import spock.lang.TempDir
import net.fabricmc.loom.LoomGradlePlugin
import net.fabricmc.loom.configuration.providers.BundleMetadata
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarMerger
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta
import net.fabricmc.loom.configuration.providers.minecraft.VersionsManifest
import net.fabricmc.loom.test.LoomTestConstants
import net.fabricmc.loom.test.util.GradleTestUtil
import net.fabricmc.loom.util.Constants
import net.fabricmc.loom.util.ZipUtils
import net.fabricmc.loom.util.download.Download
import net.fabricmc.loom.util.download.DownloadExecutor
class MinecraftJarMergerTest extends Specification {
private static final Path dir = LoomTestConstants.TEST_DIR.toPath().resolve("jar-merger")
@TempDir
Path tempDir
def "25w31a"() {
setup:
def jars = prepareJars("25w31a")
def out = tempDir.resolve("25w31a.merged.jar")
when:
def merger = new MinecraftJarMerger(jars.clientJar.toFile(), jars.serverJar.toFile(), out.toFile())
merger.merge()
then:
methodAccess(out, "net/minecraft/server/MinecraftServer", "v", "()Ljk;") == Opcodes.ACC_PROTECTED
}
def "1.13.2"() {
setup:
def jars = prepareJars("1.13.2")
def out = tempDir.resolve("1.13.2.merged.jar")
when:
def merger = new MinecraftJarMerger(jars.clientJar.toFile(), jars.serverJar.toFile(), out.toFile())
merger.merge()
then:
methodAccess(out, "net/minecraft/server/MinecraftServer", "a", "(Z)V") == Opcodes.ACC_PROTECTED
fieldAccess(out, "net/minecraft/server/MinecraftServer", "f", "Ljava/util/Queue;") == (Opcodes.ACC_PROTECTED | Opcodes.ACC_FINAL)
}
static int methodAccess(Path jar, String owner, String name, String desc) {
return getClassNode(jar, owner).methods.find { it.name == name && it.desc == desc }.access
}
static int fieldAccess(Path jar, String owner, String name, String desc) {
return getClassNode(jar, owner).fields.find { it.name == name && it.desc == desc }.access
}
static ClassNode getClassNode(Path jar, String owner) {
byte[] data = ZipUtils.unpack(jar, "${owner}.class")
ClassReader reader = new ClassReader(data)
ClassNode node = new ClassNode(Constants.ASM_VERSION)
reader.accept(node, 0)
return node
}
static Jars prepareJars(String id) {
def jars = downloadJars(id)
def bundleMetadata = BundleMetadata.fromJar(jars.serverJar)
if (bundleMetadata == null) {
return jars
}
def unpackedJar = dir.resolve(id + ".unpacked.jar")
bundleMetadata.versions().get(0)
.unpackEntry(jars.serverJar, unpackedJar, GradleTestUtil.mockProject())
return new Jars(
clientJar: jars.clientJar,
serverJar: unpackedJar
)
}
static Jars downloadJars(String id) {
def manifestJson = Download.create(Constants.VERSION_MANIFESTS)
.downloadString()
def manifest = LoomGradlePlugin.GSON.fromJson(manifestJson, VersionsManifest.class)
def version = manifest.getVersion(id)
new DownloadExecutor(2).withCloseable {
return downloadVersion(version, it)
}
}
static Jars downloadVersion(VersionsManifest.Version version, DownloadExecutor downloadExecutor) {
def manifest = Download.create(version.url)
.sha1(version.sha1)
.downloadString(dir.resolve(version.id + ".json"))
def meta = LoomGradlePlugin.GSON.fromJson(manifest, MinecraftVersionMeta.class)
def client = meta.download("client")
def server = meta.download("server")
if (server == null) {
return null
}
def clientJar = download(client, downloadExecutor)
def serverJar = download(server, downloadExecutor)
return new Jars(
clientJar: clientJar,
serverJar: serverJar
)
}
static Path download(MinecraftVersionMeta.Download download, DownloadExecutor executor) {
Path jarPath = dir.resolve(download.sha1() + ".jar")
Download.create(download.url())
.sha1(download.sha1())
.downloadPathAsync(jarPath, executor)
return jarPath
}
static class Jars {
Path clientJar
Path serverJar
}
}

View File

@@ -24,17 +24,20 @@
package net.fabricmc.loom.test.unit.service.mocks
import org.mockito.Answers
import net.fabricmc.tinyremapper.TinyRemapper
import net.fabricmc.tinyremapper.api.TrEnvironment
import net.fabricmc.tinyremapper.api.TrRemapper
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when
import static org.mockito.Mockito.withSettings
class MockTinyRemapper {
TinyRemapper tinyRemapper = mock(TinyRemapper.class)
TrEnvironment trEnvironment = mock(TrEnvironment.class)
TrRemapper remapper = mock(TrRemapper.class)
TrRemapper remapper = mock(TrRemapper.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS))
MockTinyRemapper() {
when(tinyRemapper.getEnvironment()).thenReturn(trEnvironment)

View File

@@ -0,0 +1,125 @@
/*
* 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.test.util
import java.nio.file.Files
import java.nio.file.Path
import net.fabricmc.loom.LoomGradlePlugin
import net.fabricmc.loom.configuration.providers.BundleMetadata
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarMerger
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta
import net.fabricmc.loom.configuration.providers.minecraft.VersionsManifest
import net.fabricmc.loom.util.Constants
import net.fabricmc.loom.util.download.Download
import net.fabricmc.loom.util.download.DownloadExecutor
class MinecraftJarMergerRunner {
static Path dir = Path.of(".gradle", "test-files", "tomerge")
static void main(String[] args) {
def versionManifest = Download.create(Constants.VERSION_MANIFESTS)
.downloadString()
final VersionsManifest manifest = LoomGradlePlugin.GSON.fromJson(versionManifest, VersionsManifest.class)
List<VersionInfo> versions = []
// Download all the minecraft jars
new DownloadExecutor(10).withCloseable {
for (def version in manifest.versions()) {
if (version.type == "snapshot" && version.id != "25w31a") {
continue
}
if (version.id == "1.2.5") {
// Cannot merge any version before this.
break
}
def info = downloadVersion(version, it)
if (info != null) {
versions.add(info)
}
}
}
for (def info in versions) {
println("Merging version " + info.id)
def mergedJar = dir.resolve(info.id + ".merged.jar")
Files.deleteIfExists(mergedJar)
def serverJar = info.serverJar.toFile()
def bundleMetadata = BundleMetadata.fromJar(info.serverJar)
if (bundleMetadata != null) {
def unpackedJar = dir.resolve(info.id + ".unpacked.jar")
bundleMetadata.versions().get(0)
.unpackEntry(info.serverJar, unpackedJar, GradleTestUtil.mockProject())
serverJar = unpackedJar.toFile()
}
def merger = new MinecraftJarMerger(info.clientJar.toFile(), serverJar, mergedJar.toFile())
merger.merge()
}
}
// Returns null if the version does not have a server jar
static VersionInfo downloadVersion(VersionsManifest.Version version, DownloadExecutor downloadExecutor) {
def manifest = Download.create(version.url)
.sha1(version.sha1)
.downloadString(dir.resolve(version.id + ".json"))
def meta = LoomGradlePlugin.GSON.fromJson(manifest, MinecraftVersionMeta.class)
def client = meta.download("client")
def server = meta.download("server")
if (server == null) {
return null
}
def clientJar = download(client, downloadExecutor)
def serverJar = download(server, downloadExecutor)
return new VersionInfo(
id: version.id,
clientJar: clientJar,
serverJar: serverJar
)
}
static Path download(MinecraftVersionMeta.Download download, DownloadExecutor executor) {
Path jarPath = dir.resolve(download.sha1() + ".jar")
Download.create(download.url())
.sha1(download.sha1())
.downloadPathAsync(jarPath, executor)
return jarPath
}
static class VersionInfo {
String id
Path clientJar
Path serverJar
}
}

View File

@@ -0,0 +1,2 @@
tiny 2 0 official intermediary named
c com/mojang/minecraft/l com/mojang/minecraft/l com/mojang/minecraft/Minecraft

View File

@@ -1,6 +1,6 @@
import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.0.21"
@@ -18,9 +18,9 @@ tasks {
withType<JavaCompile> {
options.release.set(8)
}
withType<KotlinCompile<KotlinJvmOptions>> {
kotlinOptions {
jvmTarget = "1.8"
withType<KotlinCompile> {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
}
@@ -29,10 +29,10 @@ group = "com.example"
version = "0.0.1"
dependencies {
minecraft(group = "com.mojang", name = "minecraft", version = "1.16.5")
mappings(group = "net.fabricmc", name = "yarn", version = "1.16.5+build.5", classifier = "v2")
minecraft("com.mojang:minecraft:1.16.5")
mappings("net.fabricmc:yarn:1.16.5+build.5:v2")
modImplementation("net.fabricmc:fabric-loader:0.16.9")
modImplementation(group = "net.fabricmc", name = "fabric-language-kotlin", version = "1.12.3+kotlin.2.0.21")
modImplementation("net.fabricmc:fabric-language-kotlin:1.12.3+kotlin.2.0.21")
}
publishing {

View File

@@ -29,7 +29,7 @@ dependencies {
// Local files
modImplementation files("test-data-a.jar", "test-data-b.jar") // multiple files in a bare FileCollection
modImplementation fileTree("myFileTree") // an entire file tree
modImplementation name: "test-data-e" // a flatDir dependency
modImplementation ":test-data-e" // a flatDir dependency
// PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
// You may need to force-disable transitiveness on them.

View File

@@ -2,7 +2,7 @@ unpick v3
target_field mapped.bar.Y quux I g
target_field mapped.bar.Z null Lmapped/foo/X; g
target_field mapped.bar.Z foo Lmapped/foo/X; g
target_method mapped.bar.Y bar2 (Lmapped/foo/X;)V
@@ -13,7 +13,7 @@ group float
mapped.bar.Y.quux:int
group float
mapped.bar.Y.*:float
mapped.bar.Y.baz:float
group int
@scope class mapped.bar.Y