mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-03-28 04:07:01 -05:00
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:
12
build.gradle
12
build.gradle
@@ -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")
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
785
src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java
Normal file
785
src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
115
src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java
Normal file
115
src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
129
src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java
Normal file
129
src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
80
src/main/java/net/fabricmc/loom/util/AsyncCache.java
Normal file
80
src/main/java/net/fabricmc/loom/util/AsyncCache.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
42
src/main/java/net/fabricmc/loom/util/Check.java
Normal file
42
src/main/java/net/fabricmc/loom/util/Check.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
66
src/main/java/net/fabricmc/loom/util/Lazy.java
Normal file
66
src/main/java/net/fabricmc/loom/util/Lazy.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
39
src/main/java/net/fabricmc/loom/util/LoomProblems.java
Normal file
39
src/main/java/net/fabricmc/loom/util/LoomProblems.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
143
src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java
Normal file
143
src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
"""
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
"""
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
'''
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
2
src/test/resources/mappings/0.30-minimal.tiny
Normal file
2
src/test/resources/mappings/0.30-minimal.tiny
Normal file
@@ -0,0 +1,2 @@
|
||||
tiny 2 0 official intermediary named
|
||||
c com/mojang/minecraft/l com/mojang/minecraft/l com/mojang/minecraft/Minecraft
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user