Enable the usage of JSR annotations (#1420)

* Add option to disable remapping JSR annotations

* Move JSR annotation remapping to a JAR processor

* Organize imports

* Remap JetBrains annotations back to JSR when configured

* Fix indentation

* Rename useJsrAnnotations

Rename it to remapJsrAnnotationsToJetBrains to make clear what Loom does

* Update JSR annotation remapper exception message

* Add integration test

* Document remapJsrAnnotationsToJetBrains

* Fix javadoc format

* Checkstyle fix

---------

Co-authored-by: modmuss50 <modmuss50@gmail.com>
This commit is contained in:
opekope2
2025-11-18 20:28:05 +00:00
committed by GitHub
parent dedbf8fa8f
commit 1a890a6db3
5 changed files with 186 additions and 0 deletions

View File

@@ -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.
*
* <p>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.
*
* <p>This has no effect on Minecraft versions that solely use JSpecify annotations.
*
* <p>Default: true
*
* @return the property controlling the remapping of JSR annotations
*/
Property<Boolean> getRemapJsrAnnotationsToJetBrains();
Property<Boolean> getRuntimeOnlyLog4j();
Property<Boolean> getSplitModDependencies();

View File

@@ -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) {

View File

@@ -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<JsrAnnotationRemapperProcessor.Spec> {
private static final Map<String, String> 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<String, String> annotationMapping) implements MinecraftJarProcessor.Spec {
public IMappingProvider getMappings() {
return out -> annotationMapping.forEach(out::acceptClass);
}
}
}

View File

@@ -96,6 +96,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
protected final Property<String> intermediary;
protected final Property<IntermediateMappingsProvider> intermediateMappingsProvider;
private final Property<String> productionNamespace;
private final Property<Boolean> remapJsrAnnotationsToJetBrains;
private final Property<Boolean> runtimeOnlyLog4j;
private final Property<Boolean> splitModDependencies;
private final Property<MinecraftJarConfiguration<?, ?, ?>> 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<Boolean> getRemapJsrAnnotationsToJetBrains() {
return remapJsrAnnotationsToJetBrains;
}
@Override
public Property<Boolean> getRuntimeOnlyLog4j() {
return runtimeOnlyLog4j;

View File

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