diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 68d1c169..2c644561 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -266,6 +266,19 @@ public interface LoomGradleExtensionAPI { boolean areEnvironmentSourceSetsSplit(); + /** + * When enabled, Loom remaps JSR {@code Nullable}, {@code Nonnull}, and {@code Immutable} annotations to their JetBrains counterparts in the Minecraft JAR. + * + *

When disabled, Loom keeps JSR annotations as-is, and remaps any JetBrains {@code Nullable}, {@code NotNull}, and {@code Unmodifiable} annotations to their JSR counterparts in the Minecraft JAR. + * + *

This has no effect on Minecraft versions that solely use JSpecify annotations. + * + *

Default: true + * + * @return the property controlling the remapping of JSR annotations + */ + Property getRemapJsrAnnotationsToJetBrains(); + Property getRuntimeOnlyLog4j(); Property getSplitModDependencies(); diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 901c1337..8add7c42 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -61,6 +61,7 @@ import net.fabricmc.loom.build.mixin.KaptApInvoker; import net.fabricmc.loom.build.mixin.ScalaApInvoker; import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor; import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; +import net.fabricmc.loom.configuration.processors.JsrAnnotationRemapperProcessor; import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ModJavadocProcessor; import net.fabricmc.loom.configuration.processors.speccontext.DebofConfiguration; @@ -241,6 +242,10 @@ public abstract class CompileConfiguration implements Runnable { if (interfaceInjection.isEnabled()) { extension.addMinecraftJarProcessor(InterfaceInjectionProcessor.class, "fabric-loom:interface-inject", interfaceInjection.getEnableDependencyInterfaceInjection().get()); } + + if (!extension.getRemapJsrAnnotationsToJetBrains().get()) { + extension.addMinecraftJarProcessor(JsrAnnotationRemapperProcessor.class, "fabric-loom:jsr-annotations"); + } } private void setupMixinAp(MixinExtension mixin) { diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/JsrAnnotationRemapperProcessor.java b/src/main/java/net/fabricmc/loom/configuration/processors/JsrAnnotationRemapperProcessor.java new file mode 100644 index 00000000..08ba51c2 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/processors/JsrAnnotationRemapperProcessor.java @@ -0,0 +1,88 @@ +/* + * 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.processors; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; + +import javax.inject.Inject; + +import org.jspecify.annotations.Nullable; + +import net.fabricmc.loom.api.processor.MinecraftJarProcessor; +import net.fabricmc.loom.api.processor.ProcessorContext; +import net.fabricmc.loom.api.processor.SpecContext; +import net.fabricmc.loom.util.TinyRemapperLoggerAdapter; +import net.fabricmc.tinyremapper.IMappingProvider; +import net.fabricmc.tinyremapper.OutputConsumerPath; +import net.fabricmc.tinyremapper.TinyRemapper; + +public class JsrAnnotationRemapperProcessor implements MinecraftJarProcessor { + private static final Map JETBRAINS_TO_JSR = Map.of( + "org/jetbrains/annotations/Nullable", "javax/annotation/Nullable", + "org/jetbrains/annotations/NotNull", "javax/annotation/Nonnull", + "org/jetbrains/annotations/Unmodifiable", "javax/annotation/concurrent/Immutable" + ); + + private final String name; + + @Inject + public JsrAnnotationRemapperProcessor(String name) { + this.name = name; + } + + @Override + public @Nullable Spec buildSpec(SpecContext context) { + return new Spec(JETBRAINS_TO_JSR); + } + + @Override + public void processJar(Path jar, Spec spec, ProcessorContext context) throws IOException { + TinyRemapper tinyRemapper = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE) + .withMappings(spec.getMappings()) + .build(); + + try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(jar).build()) { + tinyRemapper.readInputs(jar); + tinyRemapper.apply(outputConsumer); + } catch (Exception e) { + throw new RuntimeException("Failed to remap JAR " + jar + " with mapping " + spec.annotationMapping(), e); + } finally { + tinyRemapper.finish(); + } + } + + @Override + public String getName() { + return name; + } + + public record Spec(Map annotationMapping) implements MinecraftJarProcessor.Spec { + public IMappingProvider getMappings() { + return out -> annotationMapping.forEach(out::acceptClass); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index 48cb1a52..400fff15 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -96,6 +96,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final Property intermediary; protected final Property intermediateMappingsProvider; private final Property productionNamespace; + private final Property remapJsrAnnotationsToJetBrains; private final Property runtimeOnlyLog4j; private final Property splitModDependencies; private final Property> minecraftJarConfiguration; @@ -180,6 +181,9 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA this.accessWidener.finalizeValueOnRead(); this.getGameJarProcessors().finalizeValueOnRead(); + this.remapJsrAnnotationsToJetBrains = project.getObjects().property(Boolean.class).convention(true); + this.remapJsrAnnotationsToJetBrains.finalizeValueOnRead(); + this.runtimeOnlyLog4j = project.getObjects().property(Boolean.class).convention(false); this.runtimeOnlyLog4j.finalizeValueOnRead(); @@ -406,6 +410,11 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA return minecraftJarConfiguration; } + @Override + public Property getRemapJsrAnnotationsToJetBrains() { + return remapJsrAnnotationsToJetBrains; + } + @Override public Property getRuntimeOnlyLog4j() { return runtimeOnlyLog4j; diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/JsrAnnotationsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/JsrAnnotationsTest.groovy new file mode 100644 index 00000000..c382b205 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/JsrAnnotationsTest.groovy @@ -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.test.integration + +import java.nio.charset.StandardCharsets + +import spock.lang.Specification + +import net.fabricmc.loom.test.util.GradleProjectTestTrait +import net.fabricmc.loom.util.ZipUtils + +import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class JsrAnnotationsTest extends Specification implements GradleProjectTestTrait { + static final String MAPPINGS = "21w13a-net.fabricmc.yarn.21w13a.21w13a+build.30-v2" + static final boolean REMAP_JSR_DEFAULT = true + + def "Remap JSR annotations to JetBrains #remapJsrAnnotations"() { + setup: + def gradle = gradleProject(project: "unpick", version: PRE_RELEASE_GRADLE) + gradle.buildGradle << """ + loom { + remapJsrAnnotationsToJetBrains = ${remapJsrAnnotations} + } + """.stripIndent() + when: + def result = gradle.run(tasks: [ + "genSourcesWithVineflower", + "--info" + ]) + def source = getClassSource(gradle, "net/minecraft/util/annotation/MethodsReturnNonnullByDefault.java", remapJsrAnnotations != REMAP_JSR_DEFAULT) + + then: + result.task(":genSourcesWithVineflower").outcome == SUCCESS + source.contains(remapJsrAnnotations ? "@NotNull" : "@Nonnull") + source.contains(remapJsrAnnotations ? "import org.jetbrains.annotations.NotNull;" : "import javax.annotation.Nonnull;") + !source.contains(remapJsrAnnotations ? "@Nonnull" : "@NotNull") + !source.contains(remapJsrAnnotations ? "import javax.annotation.Nonnull;" : "import org.jetbrains.annotations.NotNull;") + + where: + [remapJsrAnnotations] << [[true, false]].combinations() + } + + private static String getClassSource(GradleProject gradle, String classname, boolean local, String mappings = MAPPINGS) { + File sourcesJar = local ? gradle.getGeneratedLocalSources(mappings) : gradle.getGeneratedSources(mappings) + return new String(ZipUtils.unpack(sourcesJar.toPath(), classname), StandardCharsets.UTF_8) + } +}