From ea43f8681a82ebbd7972e77a6109566f69a65c04 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Sat, 26 Jul 2025 16:30:32 +0100 Subject: [PATCH 01/20] Start on Loom 1.12 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e49795dd..da46a15e 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } group = 'net.fabricmc' -def baseVersion = '1.11' +def baseVersion = '1.12' def ENV = System.getenv() if (ENV.BUILD_NUMBER) { From 9104549347a5e2585428182786203920e63a08d1 Mon Sep 17 00:00:00 2001 From: modmuss Date: Sun, 27 Jul 2025 13:38:15 +0100 Subject: [PATCH 02/20] Misc peformance improvements (#1337) * First pass * Make async cache generic * Suppress with annotation --- .../mods/ModConfigurationRemapper.java | 79 +++++++++++------- .../processors/SpecContextImpl.java | 40 ++++------ .../net/fabricmc/loom/util/AsyncCache.java | 80 +++++++++++++++++++ .../loom/test/unit/AsyncCacheTest.groovy | 45 +++++++++++ 4 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/util/AsyncCache.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/AsyncCacheTest.groovy diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java index 4994dc88..9e537c84 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java @@ -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,25 +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> dependenciesBySourceConfig = new HashMap<>(); - final Map metaCache = new HashMap<>(); + AsyncCache 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 artifactRefs = resolveArtifacts(project, sourceConfig); + Map metadataMap = getMetadata(artifactRefs, metaCache); final List modDependencies = new ArrayList<>(); - for (ArtifactRef artifact : resolveArtifacts(project, sourceConfig)) { - final ArtifactMetadata artifactMetadata; - - artifactMetadata = metaCache.computeIfAbsent(artifact, a -> { - try { - return ArtifactMetadata.create(a, LoomGradlePlugin.LOOM_VERSION); - } 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) { @@ -235,6 +232,24 @@ public class ModConfigurationRemapper { }); } + private static Map getMetadata(List artifacts, AsyncCache cache) { + var futures = new HashMap>(); + + for (ArtifactRef artifact : artifacts) { + CompletableFuture 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. @@ -260,10 +275,10 @@ public class ModConfigurationRemapper { final List artifacts = new ArrayList<>(); final Set resolvedArtifacts = configuration.getResolvedConfiguration().getResolvedArtifacts(); - downloadAllSources(project, resolvedArtifacts); + Map 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)); } @@ -289,9 +304,9 @@ public class ModConfigurationRemapper { return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); } - private static void downloadAllSources(Project project, Set resolvedArtifacts) { + private static Map downloadAllSources(Project project, Set resolvedArtifacts) { if (isCIBuild()) { - return; + return Map.of(); } final DependencyHandler dependencies = project.getDependencies(); @@ -307,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 resolvedSources = query.execute().getResolvedComponents(); + Map 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(); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java index 31db00f1..24a94cc8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java @@ -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; @@ -53,6 +52,7 @@ import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; import net.fabricmc.loom.util.fmj.FabricModJsonHelpers; +import net.fabricmc.loom.util.AsyncCache; import net.fabricmc.loom.util.gradle.GradleUtils; /** @@ -65,7 +65,7 @@ public record SpecContextImpl( List localMods, List compileRuntimeMods) implements SpecContext { public static SpecContextImpl create(Project project) { - final Map> fmjCache = new HashMap<>(); + AsyncCache> fmjCache = new AsyncCache>(); 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 getDependentMods(Project project, Map> fmjCache) { + private static List getDependentMods(Project project, AsyncCache> fmjCache) { final LoomGradleExtension extension = LoomGradleExtension.get(project); - var mods = new ArrayList(); + var futures = new ArrayList>>(); for (RemapConfigurationSettings entry : extension.getRemapConfigurations()) { final Set artifacts = entry.getSourceConfiguration().get().resolve(); for (File artifact : artifacts) { - final List 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 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 getCompileRuntimeMods(Project project, Map> fmjCache) { + private static List getCompileRuntimeMods(Project project, AsyncCache> fmjCache) { var mods = new ArrayList<>(getCompileRuntimeModsFromRemapConfigs(project, fmjCache)); for (Project dependentProject : getCompileRuntimeProjectDependencies(project).toList()) { - List projectMods = fmjCache.computeIfAbsent(dependentProject.getPath(), $ -> { + List 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 getCompileRuntimeModsFromRemapConfigs(Project project, Map> fmjCache) { + private static List getCompileRuntimeModsFromRemapConfigs(Project project, AsyncCache> 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 getMods(Project project, Map> fmjCache, Stream stream) { + private static Stream getMods(Project project, AsyncCache> fmjCache, Stream stream) { return stream.flatMap(resolveArtifacts(project, true)) .map(modFromZip(fmjCache)) .filter(Objects::nonNull); } - private static Set getModIds(Project project, Map> fmjCache, Stream stream) { + private static Set getModIds(Project project, AsyncCache> fmjCache, Stream stream) { return getMods(project, fmjCache, stream) .map(FabricModJson::getId) .collect(Collectors.toSet()); } - private static Function modFromZip(Map> fmjCache) { + private static Function modFromZip(AsyncCache> fmjCache) { return zipPath -> { - final List list = fmjCache.computeIfAbsent(zipPath.toAbsolutePath().toString(), $ -> { + final List 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(); }; } diff --git a/src/main/java/net/fabricmc/loom/util/AsyncCache.java b/src/main/java/net/fabricmc/loom/util/AsyncCache.java new file mode 100644 index 00000000..28bb3e15 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/AsyncCache.java @@ -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 { + private static final Executor EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + private final Map> cache = new ConcurrentHashMap<>(); + + public CompletableFuture get(Object cacheKey, Supplier supplier) { + return cache.computeIfAbsent(cacheKey, $ -> CompletableFuture.supplyAsync(supplier, EXECUTOR)); + } + + public T getBlocking(Object cacheKey, Supplier supplier) { + return join(get(cacheKey, supplier)); + } + + public static List joinList(Collection>> futures) { + return join(futures.stream() + .collect(CompletableFutureCollector.allOf())) + .stream() + .flatMap(List::stream) + .toList(); + } + + public static Map joinMap(Map> 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 join(CompletableFuture future) { + try { + return future.join(); + } catch (CompletionException e) { + sneakyThrow(e.getCause() != null ? e.getCause() : e); + throw new IllegalStateException(); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/AsyncCacheTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncCacheTest.groovy new file mode 100644 index 00000000..a9215901 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncCacheTest.groovy @@ -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" + } +} From cc24a13679d14e80224df170a54808b0df6444c3 Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sun, 27 Jul 2025 13:39:03 +0100 Subject: [PATCH 03/20] Add ability to add and remove annotations via mappings (#1331) * Add ability to add and remove annotations via mappings * Address review comments --- .../layered/spec/FileMappingsSpecBuilder.java | 7 + .../mappings/LayeredMappingsFactory.java | 15 + .../mappings/LayeredMappingsProcessor.java | 23 ++ .../mappings/MappingConfiguration.java | 22 ++ .../annotations/AnnotationNodeSerializer.java | 187 ++++++++++ .../extras/annotations/AnnotationsData.java | 78 ++++ .../extras/annotations/AnnotationsLayer.java | 38 ++ .../annotations/ClassAnnotationData.java | 114 ++++++ .../annotations/GenericAnnotationData.java | 88 +++++ .../annotations/MethodAnnotationData.java | 97 +++++ .../SkipEmptyTypeAdapterFactory.java | 71 ++++ .../extras/annotations/TypeAnnotationKey.java | 28 ++ .../TypeAnnotationNodeSerializer.java | 59 +++ .../mappings/file/FileMappingsLayer.java | 29 +- .../mappings/file/FileMappingsSpec.java | 4 +- .../file/FileMappingsSpecBuilderImpl.java | 9 +- .../minecraft/AnnotationsApplyVisitor.java | 349 ++++++++++++++++++ .../AbstractMappedMinecraftProvider.java | 8 + .../AnnotationsLayerTest.groovy | 156 ++++++++ .../LayeredMappingSpecBuilderTest.groovy | 2 +- .../processor/AnnotationsApplyTest.groovy | 283 ++++++++++++++ 21 files changed, 1662 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationNodeSerializer.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsLayer.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/SkipEmptyTypeAdapterFactory.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy diff --git a/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileMappingsSpecBuilder.java b/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileMappingsSpecBuilder.java index 8028881a..90b6abac 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileMappingsSpecBuilder.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileMappingsSpecBuilder.java @@ -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. * diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java index 15a59738..de3d43f0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java @@ -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 layers, Path mappingsFile) throws IOException { + AnnotationsData annotationsData = processor.getAnnotationsData(layers); + + if (annotationsData == null) { + return; + } + + byte[] data = AnnotationsData.GSON.toJson(annotationsData.toJson()).getBytes(StandardCharsets.UTF_8); + + ZipUtils.add(mappingsFile, AnnotationsLayer.ANNOTATIONS_PATH, data); + } + private void writeSignatureFixes(LayeredMappingsProcessor processor, List layers, Path mappingsFile) throws IOException { Map signatureFixes = processor.getSignatureFixes(layers); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java index e35ff242..54afa31a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java @@ -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,27 @@ public class LayeredMappingsProcessor { return mappingTree; } + @Nullable + public AnnotationsData getAnnotationsData(List layers) throws IOException { + AnnotationsData result = null; + + for (MappingLayer layer : layers) { + if (layer instanceof AnnotationsLayer annotationsLayer) { + AnnotationsData annotationsData = annotationsLayer.getAnnotationsData(); + + if (annotationsData != null) { + if (result == null) { + result = annotationsData; + } else { + result = result.merge(annotationsData); + } + } + } + } + + return result; + } + @Nullable public Map getSignatureFixes(List layers) { Map signatureFixes = new HashMap<>(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java index 90a7945e..f1b654c7 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java @@ -48,6 +48,8 @@ import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradlePlugin; 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; @@ -75,6 +77,8 @@ public class MappingConfiguration { public final Path tinyMappingsJar; private final Path unpickDefinitions; + @Nullable + private AnnotationsData annotationsData; @Nullable private UnpickMetadata unpickMetadata; private Map signatureFixes; @@ -216,10 +220,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.read(reader); + } + } + private void extractUnpickDefinitions(FileSystem jar) throws IOException { Path unpickPath = jar.getPath(UnpickMetadata.UNPICK_DEFINITIONS_PATH); Path unpickMetadataPath = jar.getPath(UnpickMetadata.UNPICK_METADATA_PATH); @@ -295,6 +312,11 @@ public class MappingConfiguration { return unpickMetadata != null; } + @Nullable + public AnnotationsData getAnnotationsData() { + return annotationsData; + } + public UnpickMetadata getUnpickMetadata() { return Objects.requireNonNull(unpickMetadata, "Unpick metadata is not available"); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationNodeSerializer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationNodeSerializer.java new file mode 100644 index 00000000..360f548a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationNodeSerializer.java @@ -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, JsonDeserializer { + @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 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; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java new file mode 100644 index 00000000..27b516d9 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java @@ -0,0 +1,78 @@ +/* + * 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.Reader; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.TypeAnnotationNode; + +public record AnnotationsData(Map classes) { + 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(); + + public static AnnotationsData read(Reader reader) { + JsonObject json = GSON.fromJson(reader, JsonObject.class); + + if (!json.has("version")) { + throw new JsonSyntaxException("Missing annotations version"); + } + + int version = json.getAsJsonPrimitive("version").getAsInt(); + + if (version != 1) { + throw new JsonSyntaxException("Invalid annotations version " + version + ". Try updating loom"); + } + + return GSON.fromJson(json, AnnotationsData.class); + } + + public JsonObject toJson() { + JsonObject json = GSON.toJsonTree(this).getAsJsonObject(); + JsonObject result = new JsonObject(); + result.addProperty("version", 1); + result.asMap().putAll(json.asMap()); + return result; + } + + public AnnotationsData merge(AnnotationsData other) { + Map newClassData = new LinkedHashMap<>(classes); + other.classes.forEach((key, value) -> newClassData.merge(key, value, ClassAnnotationData::merge)); + return new AnnotationsData(newClassData); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsLayer.java new file mode 100644 index 00000000..263d3ec9 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsLayer.java @@ -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; +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java new file mode 100644 index 00000000..dc0d2651 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java @@ -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 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; + +public record ClassAnnotationData( + @SerializedName("remove") + Set annotationsToRemove, + @SerializedName("add") + List annotationsToAdd, + @SerializedName("type_remove") + Set typeAnnotationsToRemove, + @SerializedName("type_add") + List typeAnnotationsToAdd, + Map fields, + Map 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 newAnnotationsToRemove = new LinkedHashSet<>(annotationsToRemove); + newAnnotationsToRemove.addAll(other.annotationsToRemove); + List newAnnotationsToAdd = new ArrayList<>(annotationsToAdd); + newAnnotationsToAdd.addAll(other.annotationsToAdd); + Set newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove); + newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove); + List newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd); + Map newFields = new LinkedHashMap<>(fields); + other.fields.forEach((key, value) -> newFields.merge(key, value, GenericAnnotationData::merge)); + Map newMethods = new LinkedHashMap<>(methods); + other.methods.forEach((key, value) -> newMethods.merge(key, value, MethodAnnotationData::merge)); + return new ClassAnnotationData(newAnnotationsToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd, newFields, newMethods); + } + + 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); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java new file mode 100644 index 00000000..4c858811 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.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.providers.mappings.extras.annotations; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.google.gson.annotations.SerializedName; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.TypeAnnotationNode; + +public record GenericAnnotationData( + @SerializedName("remove") + Set annotationsToRemove, + @SerializedName("add") + List annotationsToAdd, + @SerializedName("type_remove") + Set typeAnnotationsToRemove, + @SerializedName("type_add") + List 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 newAnnotationToRemove = new LinkedHashSet<>(annotationsToRemove); + newAnnotationToRemove.addAll(other.annotationsToRemove); + List newAnnotationsToAdd = new ArrayList<>(annotationsToAdd); + newAnnotationsToAdd.addAll(other.annotationsToAdd); + Set newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove); + newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove); + List newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd); + newTypeAnnotationsToAdd.addAll(other.typeAnnotationsToAdd); + return new GenericAnnotationData(newAnnotationToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd); + } + + 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; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java new file mode 100644 index 00000000..45bde212 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java @@ -0,0 +1,97 @@ +/* + * 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 com.google.gson.annotations.SerializedName; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.TypeAnnotationNode; + +public record MethodAnnotationData( + @SerializedName("remove") + Set annotationsToRemove, + @SerializedName("add") + List annotationsToAdd, + @SerializedName("type_remove") + Set typeAnnotationsToRemove, + @SerializedName("type_add") + List typeAnnotationsToAdd, + Map 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 newAnnotationsToRemove = new LinkedHashSet<>(annotationsToRemove); + newAnnotationsToRemove.addAll(other.annotationsToRemove); + List newAnnotationsToAdd = new ArrayList<>(annotationsToAdd); + newAnnotationsToAdd.addAll(other.annotationsToAdd); + Set newTypeAnnotationsToRemove = new LinkedHashSet<>(typeAnnotationsToRemove); + newTypeAnnotationsToRemove.addAll(other.typeAnnotationsToRemove); + List newTypeAnnotationsToAdd = new ArrayList<>(typeAnnotationsToAdd); + newTypeAnnotationsToAdd.addAll(other.typeAnnotationsToAdd); + Map newParameters = new LinkedHashMap<>(parameters); + other.parameters.forEach((key, value) -> newParameters.merge(key, value, GenericAnnotationData::merge)); + return new MethodAnnotationData(newAnnotationsToRemove, newAnnotationsToAdd, newTypeAnnotationsToRemove, newTypeAnnotationsToAdd, newParameters); + } + + 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; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/SkipEmptyTypeAdapterFactory.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/SkipEmptyTypeAdapterFactory.java new file mode 100644 index 00000000..a503acf1 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/SkipEmptyTypeAdapterFactory.java @@ -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 TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + boolean isMap = Map.class.isAssignableFrom(rawType); + + if (!isMap && !Collection.class.isAssignableFrom(rawType)) { + return null; + } + + TypeAdapter 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(); + } + }; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java new file mode 100644 index 00000000..e2b4307f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java @@ -0,0 +1,28 @@ +/* + * 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; + +public record TypeAnnotationKey(int typeRef, String typePath, String name) { +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java new file mode 100644 index 00000000..92890268 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java @@ -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, JsonDeserializer { + @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; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsLayer.java index df8144f7..4b2dcd0f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsLayer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsLayer.java @@ -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); + } + } + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpec.java index cdf3a266..5d296d91 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpec.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpec.java @@ -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 { @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); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpecBuilderImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpecBuilderImpl.java index 441d5120..ad6c2396 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpecBuilderImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/file/FileMappingsSpecBuilderImpl.java @@ -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); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java new file mode 100644 index 00000000..b22f2870 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java @@ -0,0 +1,349 @@ +/* + * 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.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) { + return new AnnotationsApplyClassVisitor(next, cls.getName(), annotationsData); + } + + public static class AnnotationsApplyClassVisitor extends ClassVisitor { + private final ClassAnnotationData classData; + private boolean hasAddedAnnotations; + + public AnnotationsApplyClassVisitor(ClassVisitor cv, String className, AnnotationsData annotationsData) { + super(Constants.ASM_VERSION, cv); + this.classData = annotationsData.classes().get(className); + hasAddedAnnotations = false; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (classData != null) { + 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 != null && 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 != null && 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) { + boolean hasAddedAnnotations = false; + + @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 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); + } + } + + 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); + } + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index bf3e0791..819cb195 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -46,6 +46,8 @@ import net.fabricmc.loom.configuration.ConfigContext; 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.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; @@ -247,6 +249,12 @@ public abstract class AbstractMappedMinecraftProvider= 16; TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceFactory(), fromM, toM, fixRecords, (builder) -> { + AnnotationsData annotationsData = mappingConfiguration.getAnnotationsData(); + + if (annotationsData != null) { + builder.extraPostApplyVisitor(new AnnotationsApplyVisitor(annotationsData)); + } + builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures)); configureRemapper(remappedJars, builder); }); diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy new file mode 100644 index 00000000..fb5b1fdf --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy @@ -0,0 +1,156 @@ +/* + * 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 == "pkg/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] == ["pkg/MyEnum", "VALUE"] as String[] + annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7] instanceof AnnotationNode && annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7].desc == "pkg/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": "pkg/Annotation4" + }, + { + "desc": "pkg/Annotation5", + "values": { + "foo": { + "type": "int", + "value": 42 + }, + "bar": { + "type": "class", + "value": "Ljava/lang/String;" + }, + "baz": { + "type": "enum_constant", + "owner": "pkg/MyEnum", + "name": "VALUE" + }, + "ann": { + "type": "annotation", + "desc": "pkg/Annotation6" + }, + "arr": { + "type": "array", + "value": [ + { + "type": "int", + "value": 1 + }, + { + "type": "int", + "value": 2 + } + ] + } + } + } + ], + "type_add": [ + { + "desc": "pkg/Annotation7", + "type_ref": 22, + "type_path": "[" + } + ], + "fields": { + "bar:Lbaz;": { + "remove": [ + "java/lang/Deprecated" + ] + } + }, + "methods": { + "bar()V": { + "remove": [ + "java/lang/Deprecated" + ] + } + } + }, + "pkg/Bar": { + "add": [ + { + "desc": "pkg/Annotation1" + } + ] + } + } +} +""" +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingSpecBuilderTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingSpecBuilderTest.groovy index 47e37df5..37c62b90 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingSpecBuilderTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingSpecBuilderTest.groovy @@ -115,7 +115,7 @@ class LayeredMappingSpecBuilderTest extends Specification { } 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 diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy new file mode 100644 index 00000000..9e212001 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy @@ -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, 'net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1', annotationData) + def annotatedNode2 = new ClassNode() + def classVisitor2 = new AnnotationsApplyVisitor.AnnotationsApplyClassVisitor(annotatedNode2, 'net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2', annotationData) + + 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 +''' +} From 97bca29290653a3dd7a595dd5f875029ef7d00d7 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 29 Jul 2025 13:03:17 +0100 Subject: [PATCH 04/20] Gradle 9.1 deprecation fixes + update test and build libs (#1338) * Update test and build libs * Gradle deprecation fixes * RC4 --- gradle/libs.versions.toml | 8 ++--- gradle/test.libs.versions.toml | 12 +++---- .../loom/task/GenVsCodeProjectTask.java | 24 +++++++------- .../task/service/MigrateMappingsService.java | 4 +-- ...ClassMetadataRemappingAnnotationVisitor.kt | 16 ++++----- .../kotlin/remapping/KotlinClassRemapper.kt | 33 ++++++++++--------- .../KotlinMetadataRemappingClassVisitor.kt | 9 ++--- ...KotlinMetadataTinyRemapperExtensionImpl.kt | 4 +-- .../projects/kotlin/build.gradle.kts | 16 ++++----- .../projects/localFileDependency/build.gradle | 2 +- 10 files changed, 64 insertions(+), 64 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20756f1e..5709a588 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,10 +15,10 @@ loom-native = "0.2.0" unpick = "3.0.0-beta.5" # 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" [libraries] # Loom compile libraries diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 43205a57..e0ee4ec5 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -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.0.0-rc-4" +gradle-nightly = "9.1.0-20250729001554+0000" fabric-loader = "0.16.14" [libraries] diff --git a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java index 398bba75..30cf19c8 100644 --- a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java @@ -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() ); } } diff --git a/src/main/java/net/fabricmc/loom/task/service/MigrateMappingsService.java b/src/main/java/net/fabricmc/loom/task/service/MigrateMappingsService.java index 5de12fa6..152f364f 100644 --- a/src/main/java/net/fabricmc/loom/task/service/MigrateMappingsService.java +++ b/src/main/java/net/fabricmc/loom/task/service/MigrateMappingsService.java @@ -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; @@ -174,7 +173,8 @@ public class MigrateMappingsService extends Service { options.release.set(8) } - withType> { - kotlinOptions { - jvmTarget = "1.8" + withType { + 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 { diff --git a/src/test/resources/projects/localFileDependency/build.gradle b/src/test/resources/projects/localFileDependency/build.gradle index 3aa822f3..400d8800 100644 --- a/src/test/resources/projects/localFileDependency/build.gradle +++ b/src/test/resources/projects/localFileDependency/build.gradle @@ -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. From 1829e336950e67ba9318c5ffb38593bdec03869e Mon Sep 17 00:00:00 2001 From: modmuss Date: Wed, 30 Jul 2025 21:29:05 +0100 Subject: [PATCH 05/20] Update Tiny Remapper to 0.11.2 (#1342) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5709a588..82485b6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ gson = "2.10.1" guava = "33.0.0-jre" stitch = "0.6.2" -tiny-remapper = "0.11.1" +tiny-remapper = "0.11.2" access-widener = "2.1.0" mapping-io = "0.7.1" lorenz-tiny = "4.0.2" From 64590eb9158c6f59dee100c566723a2d45ee4887 Mon Sep 17 00:00:00 2001 From: modmuss Date: Thu, 31 Jul 2025 18:25:21 +0100 Subject: [PATCH 06/20] Disable the legacy Mixin Annotation Processor by default. (#1343) --- .../net/fabricmc/loom/extension/MixinExtensionApiImpl.java | 2 +- .../fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy | 4 ++-- .../net/fabricmc/loom/test/integration/FabricAPITest.groovy | 4 ++-- .../fabricmc/loom/test/integration/SimpleProjectTest.groovy | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java index 386aab8a..741ef986 100644 --- a/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java @@ -49,7 +49,7 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI { public MixinExtensionApiImpl(Project project) { this.project = Objects.requireNonNull(project); this.useMixinAp = project.getObjects().property(Boolean.class) - .convention(true); + .convention(false); this.refmapTargetNamespace = project.getObjects().property(String.class) .convention(MappingsNamespace.INTERMEDIARY.toString()); diff --git a/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy b/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy index d40b9ee5..1b6c164e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/benchmark/FabricAPIBenchmark.groovy @@ -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() } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy index 86b5364a..b45e65e6 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy @@ -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() } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/SimpleProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/SimpleProjectTest.groovy index 78052ab7..5246ba61 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/SimpleProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/SimpleProjectTest.groovy @@ -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() From 2617ed1f494b35afc3d15778f9ff65eb13c88a7d Mon Sep 17 00:00:00 2001 From: modmuss Date: Wed, 13 Aug 2025 08:53:00 +0100 Subject: [PATCH 07/20] "Fix" another project isolation issue. (#1353) * "Fix" another project isolation issue. Closes #1352 * Fix --- .../loom/configuration/processors/SpecContextImpl.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java index 24a94cc8..057758a7 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java @@ -48,11 +48,11 @@ 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; import net.fabricmc.loom.util.fmj.FabricModJsonHelpers; -import net.fabricmc.loom.util.AsyncCache; import net.fabricmc.loom.util.gradle.GradleUtils; /** @@ -198,6 +198,14 @@ public record SpecContextImpl( // Returns a list of Loom Projects found in both the runtime and compile classpath private static Stream 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 runtimeProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); final List compileProjects = getLoomProjectDependencies(project, project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)).toList(); From 4a34c4bde5f7d00bb3c8cf64acc95a54cc876dd6 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 2 Sep 2025 09:27:55 +0100 Subject: [PATCH 08/20] fabric.mod.json generation task (#1345) * fabric.mod.json DSL. * A start on generating FMJs * More work * The rest * Task works * Javadoc and cleanup * Fixes --- build.gradle | 1 + .../loom/api/fmj/FabricModJsonV1Spec.java | 785 ++++++++++++++++++ .../loom/task/FabricModJsonV1Task.java | 115 +++ .../util/fmj/gen/FabricModJsonGenerator.java | 29 + .../fmj/gen/FabricModJsonV1Generator.java | 200 +++++ .../loom/util/fmj/gen/GeneratorUtils.java | 143 ++++ .../test/integration/FabricModJsonTask.groovy | 74 ++ .../fmj/FabricModJsonV1GeneratorTest.groovy | 682 +++++++++++++++ 8 files changed, 2029 insertions(+) create mode 100644 src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java create mode 100644 src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java create mode 100644 src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonGenerator.java create mode 100644 src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonV1Generator.java create mode 100644 src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/integration/FabricModJsonTask.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonV1GeneratorTest.groovy diff --git a/build.gradle b/build.gradle index da46a15e..7d030bac 100644 --- a/build.gradle +++ b/build.gradle @@ -161,6 +161,7 @@ dependencies { testImplementation testLibs.bcprov testImplementation testLibs.bcutil testImplementation testLibs.bcpkix + testImplementation testLibs.fabric.loader compileOnly runtimeLibs.jetbrains.annotations testCompileOnly runtimeLibs.jetbrains.annotations diff --git a/src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java b/src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java new file mode 100644 index 00000000..801c9b4e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fmj/FabricModJsonV1Spec.java @@ -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. + * + *

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 getModId(); + + /** + * The version of the mod. + * @return A {@link Property} containing a {@link String} representing the mod version + */ + @Input + public abstract Property getVersion(); + + /** + * The display name of the mod. + * @return A {@link Property} containing a {@link String} representing the mod name + */ + @Input + @Optional + public abstract Property getName(); + + /** + * The description of the mod. + * @return A {@link Property} containing a {@link String} representing the mod description + */ + @Input + @Optional + public abstract Property 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 getProvides(); + + /** + * The environment the mod runs in. + * + *

One of `client`, `server`, or `*`. + * + * @return A {@link Property} containing a {@link String} representing the mod environment + */ + @Input + @Optional + public abstract Property 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 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 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 action) { + create(Entrypoint.class, getEntrypoints(), e -> { + e.getEntrypoint().set(entrypoint); + action.execute(e); + }); + } + + /** + * A list of additional JARs to load with the mod. + * + *

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 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 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 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 action) { + create(Mixin.class, getMixins(), action); + } + + /** + * The path to the access widener file for the mod. + * + *

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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 action) { + create(Person.class, getContributors(), action); + } + + /** + * A map of contact information for the mod. + * + *

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 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 getIcons(); + + /** + * Add a new icon with the given path. + * + *

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 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 action) { + create(Icon.class, getIcons(), action); + } + + /** + * A map of language adapters for the mod. + * + *

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 getLanguageAdapters(); + + /** + * A map of custom data for the mod. + * + *

This can be used by other tools to store additional information about the mod. + * + *

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 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 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 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 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 getValue(); + + /** + * The environment the Mixin configuration applies to. + * + *

One of `client`, `server`, or `*`. + * + * @return A {@link Property} containing a {@link String} representing the Mixin configuration environment + */ + @Input + @Optional + public abstract Property 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 getModId(); + + /** + * A list of version requirements for the dependency. + * + *

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 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 getName(); + + /** + * A map of contact information for the person. + * + *

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 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 getPath(); + + /** + * The size of the icon in pixels (e.g. 16, 32, 64). + * + *

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 getSize(); + } + + // Internal stuff: + + @Inject + @ApiStatus.Internal + protected abstract ObjectFactory getObjectFactory(); + + private void create(Class type, ListProperty list, Action action) { + T item = getObjectFactory().newInstance(type); + action.execute(item); + list.add(item); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java b/src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java new file mode 100644 index 00000000..97bee978 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/FabricModJsonV1Task.java @@ -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 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 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 getSpec(); + + RegularFileProperty getOutputFile(); + } + + public abstract static class FabricModJsonV1WorkAction implements WorkAction { + @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); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonGenerator.java b/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonGenerator.java new file mode 100644 index 00000000..18a9f908 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonGenerator.java @@ -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 { + String generate(Spec spec); +} diff --git a/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonV1Generator.java b/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonV1Generator.java new file mode 100644 index 00000000..848b197d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/fmj/gen/FabricModJsonV1Generator.java @@ -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 { + 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 entrypoints) { + Map> 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 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 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 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 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 customData) { + JsonObject json = new JsonObject(); + customData.forEach((name, o) -> json.add(name, LoomGradlePlugin.GSON.toJsonTree(o))); + return json; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java b/src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java new file mode 100644 index 00000000..3febd3e3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/fmj/gen/GeneratorUtils.java @@ -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 property) { + add(json, key, property, JsonPrimitive::new); + } + + public static void addRequired(JsonObject json, String key, Property property) { + addRequired(json, key, property, JsonPrimitive::new); + } + + public static void addStringOrArray(JsonObject json, String key, ListProperty property) { + if (property.get().isEmpty()) { + return; + } + + add(json, key, property, GeneratorUtils::stringOrArray); + } + + public static void addSingleOrArray(JsonObject json, String key, ListProperty property, Function converter) { + if (property.get().isEmpty()) { + return; + } + + add(json, key, property, entries -> singleOrArray(entries, converter)); + } + + public static void addArray(JsonObject json, String key, ListProperty property, Function converter) { + if (property.get().isEmpty()) { + return; + } + + add(json, key, property, entries -> array(entries, converter)); + } + + public static > void add(JsonObject json, String key, P property, Function converter) { + if (!property.isPresent()) { + return; + } + + json.add(key, converter.apply(property.get())); + } + + public static > void addRequired(JsonObject json, String key, P property, Function converter) { + property.get(); // Ensure it's present + add(json, key, property, converter); + } + + public static void add(JsonObject json, String key, ListProperty property, Function, JsonElement> converter) { + if (property.get().isEmpty()) { + return; + } + + json.add(key, converter.apply(property.get())); + } + + public static void add(JsonObject json, String key, MapProperty property, Function, JsonElement> converter) { + if (property.get().isEmpty()) { + return; + } + + json.add(key, converter.apply(property.get())); + } + + public static void add(JsonObject json, String key, MapProperty property) { + if (property.get().isEmpty()) { + return; + } + + add(json, key, property, GeneratorUtils::map); + } + + public static JsonElement stringOrArray(List strings) { + return singleOrArray(strings, JsonPrimitive::new); + } + + public static JsonElement singleOrArray(List entries, Function converter) { + if (entries.size() == 1) { + return converter.apply(entries.getFirst()); + } + + return array(entries, converter); + } + + public static JsonElement array(List entries, Function converter) { + JsonArray array = new JsonArray(); + + for (V entry : entries) { + array.add(converter.apply(entry)); + } + + return array; + } + + public static JsonObject map(Map map) { + JsonObject obj = new JsonObject(); + + for (Map.Entry entry : map.entrySet()) { + obj.addProperty(entry.getKey(), entry.getValue()); + } + + return obj; + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/FabricModJsonTask.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/FabricModJsonTask.groovy new file mode 100644 index 00000000..33fe07bd --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/FabricModJsonTask.groovy @@ -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 + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonV1GeneratorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonV1GeneratorTest.groovy new file mode 100644 index 00000000..410b4e5a --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonV1GeneratorTest.groovy @@ -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() + } +} From 0b5a242762e2adea6d4eb2fcaff7c3950531e826 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 2 Sep 2025 10:12:40 +0100 Subject: [PATCH 09/20] Bump gradle test versions (#1359) --- gradle/test.libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index e0ee4ec5..77f61566 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -7,8 +7,8 @@ java-debug = "0.53.1" mixin = "0.15.3+mixin.0.8.7" bouncycastle = "1.81" -gradle-latest = "9.0.0-rc-4" -gradle-nightly = "9.1.0-20250729001554+0000" +gradle-latest = "9.0.0" +gradle-nightly = "9.2.0-20250902075334+0000" fabric-loader = "0.16.14" [libraries] From d6ff760ca5adf3a92c3597d532002cd011c30603 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Wed, 3 Sep 2025 10:26:19 +0200 Subject: [PATCH 10/20] remove use of Task.getProject at exec time in ValidateAccessWidenerTask (#1357) --- .../java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java index bc2f2d73..c47f8b0b 100644 --- a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java +++ b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java @@ -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); From 8d9d4df65f253363c611df749dbe10f5f0ddf316 Mon Sep 17 00:00:00 2001 From: Juuz <6596629+Juuxel@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:49:22 +0300 Subject: [PATCH 11/20] Fix split official namespaces existing on versions where they don't make sense (#1361) * Don't use clientOfficial and serverOfficial namespaces on versions with only one jar Fixes #1360. Renames an experimental API in IntermediateMappingsProvider: getIsLegacyMinecraft -> getUseSplitOfficialNamespaces * Add test for 0.30 with deobf mappings + no intermediate mappings * Change split official ns check to Beta 1.0..<1.3 range check * Fix javadoc * Clarify comment in MinecraftVersionMeta --- .../IntermediateMappingsProvider.java | 6 +-- .../mappings/IntermediateMappingsService.java | 8 ++-- .../NoOpIntermediateMappingsProvider.java | 4 +- .../mappings/tiny/MappingsMerger.java | 8 ++-- .../minecraft/MinecraftProvider.java | 12 +++++- .../minecraft/MinecraftVersionMeta.java | 34 +++++++++++++++- .../minecraft/SingleJarMinecraftProvider.java | 7 ++-- .../extension/LoomGradleExtensionApiImpl.java | 9 ++--- .../extension/LoomGradleExtensionImpl.java | 6 +-- .../net/fabricmc/loom/util/Constants.java | 3 +- .../test/integration/LegacyProjectTest.groovy | 39 ++++++++++++++++++- .../loom/test/unit/MappingsMergerTest.groovy | 6 +-- src/test/resources/mappings/0.30-minimal.tiny | 2 + 13 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 src/test/resources/mappings/0.30-minimal.tiny diff --git a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java index d4987b08..9593b914 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java @@ -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> 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 getIsLegacyMinecraft(); + public abstract Property getUseSplitOfficialNamespaces(); /** * Generate or download a tinyv2 mapping file with intermediary and named namespaces. diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java index b9edaffe..16299dae 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java @@ -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 @@ -103,9 +103,9 @@ public final class IntermediateMappingsService extends Service=1.3 + final String expectedSrcNs = minecraftProvider.isLegacySplitOfficialNamespaceVersion() + ? MappingsNamespace.INTERMEDIARY.toString() // >=beta 1.0 and <1.3 + : MappingsNamespace.OFFICIAL.toString(); // >=1.3 or { options.getIntermediaryTiny().set(intermediaryTiny.toFile()); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java index f1c6b997..4c65eeb8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java @@ -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 diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java index 5827a437..68c4ddc2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java @@ -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 @@ -54,8 +54,8 @@ public final class MappingsMerger { Stopwatch stopwatch = Stopwatch.createStarted(); LOGGER.info(":merging mappings"); - if (minecraftProvider.isLegacyVersion()) { - legacyMergeAndSaveMappings(from, out, intermediateMappingsService); + if (minecraftProvider.isLegacySplitOfficialNamespaceVersion()) { + legacyMergedMergeAndSaveMappings(from, out, intermediateMappingsService); } else { mergeAndSaveMappings(from, out, intermediateMappingsService); } @@ -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); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index 30c19c52..af4d4ecd 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -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 @@ -249,7 +249,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(); } @Nullable diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java index 5325cc38..2fbda51f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java @@ -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 @@ -31,6 +31,7 @@ import java.util.Objects; import org.jetbrains.annotations.Nullable; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Platform; @SuppressWarnings("unused") @@ -64,6 +65,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). + * + *

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); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index ee682109..fa7caadc 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -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 sealed class SingleJarMinecraftProvider extends MinecraftProvide } 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; } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index 7099a65f..e547ac61 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -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 @@ -71,7 +71,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.fmj.FabricModJson; @@ -160,11 +159,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; diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 8ec88fb0..40c59eb2 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -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 @@ -295,8 +295,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 diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index c0abfce9..6f6e3236 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -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 @@ -36,6 +36,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() { } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy index 1cb8e760..c2a1680e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy @@ -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 @@ -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 + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/MappingsMergerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/MappingsMergerTest.groovy index 8bf8db99..d37a97b0 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/MappingsMergerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/MappingsMergerTest.groovy @@ -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) diff --git a/src/test/resources/mappings/0.30-minimal.tiny b/src/test/resources/mappings/0.30-minimal.tiny new file mode 100644 index 00000000..ba4779c7 --- /dev/null +++ b/src/test/resources/mappings/0.30-minimal.tiny @@ -0,0 +1,2 @@ +tiny 2 0 official intermediary named +c com/mojang/minecraft/l com/mojang/minecraft/l com/mojang/minecraft/Minecraft From 2e24e358ea932024de675441113e18ed81c50e17 Mon Sep 17 00:00:00 2001 From: modmuss Date: Wed, 3 Sep 2025 16:46:55 +0100 Subject: [PATCH 12/20] Merge differing method/field access permission flags (#1341) * Report differing method/field access flags when merging Minecraft classes * Merge access * Fix merging private final methods * Include intermediary name in mappings hash * Revert "Include intermediary name in mappings hash" This reverts commit 20ea642cb78d84a153e4a16864f832e14dce3b0c. * Workaround issue --- .../minecraft/MinecraftClassMerger.java | 112 +++++++++++- .../minecraft/MinecraftJarMerger.java | 4 +- .../providers/minecraft/VersionsManifest.java | 2 +- .../test/integration/LegacyProjectTest.groovy | 2 +- .../providers/MinecraftClassMergerTest.groovy | 73 ++++++++ .../providers/MinecraftJarMergerTest.groovy | 161 ++++++++++++++++++ .../test/util/MinecraftJarMergerRunner.groovy | 125 ++++++++++++++ 7 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftClassMergerTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarMergerTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/util/MinecraftJarMergerRunner.groovy diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java index b60cb417..85d35733 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java @@ -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 { private final Map entriesClient, entriesServer; private final List 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 toMap(List entries, Map map) { List 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. + * + *

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(); + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java index 450010f0..f54ee421 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java @@ -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 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; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java index f4870d7c..beefa2df 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java @@ -31,7 +31,7 @@ import org.jetbrains.annotations.Nullable; public record VersionsManifest(List versions, Map latest) { public static class Version { - public String id, url, sha1; + public String type, id, url, sha1; } @Nullable diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy index c2a1680e..1702ea0e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy @@ -126,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 { diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftClassMergerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftClassMergerTest.groovy new file mode 100644 index 00000000..46073acb --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftClassMergerTest.groovy @@ -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 + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarMergerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarMergerTest.groovy new file mode 100644 index 00000000..1ce31040 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftJarMergerTest.groovy @@ -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 + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/MinecraftJarMergerRunner.groovy b/src/test/groovy/net/fabricmc/loom/test/util/MinecraftJarMergerRunner.groovy new file mode 100644 index 00000000..5de4c28b --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/util/MinecraftJarMergerRunner.groovy @@ -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 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 + } +} From 213bbfcd18342360076d0efac3b12bfffae3a43f Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Sat, 20 Sep 2025 13:17:37 +0100 Subject: [PATCH 13/20] Add ability to remap annotations data (#1366) * Add ability to remap annotations data * Fix unpick remap test --- .../mappings/LayeredMappingsFactory.java | 6 +- .../mappings/LayeredMappingsProcessor.java | 11 +- .../mappings/MappingConfiguration.java | 8 +- .../extras/annotations/AnnotationsData.java | 133 ++++++++- .../annotations/ClassAnnotationData.java | 47 ++++ .../annotations/GenericAnnotationData.java | 12 + .../annotations/MethodAnnotationData.java | 17 ++ .../extras/annotations/TypeAnnotationKey.java | 5 + .../AbstractMappedMinecraftProvider.java | 7 +- .../AnnotationsDataRemapTest.groovy | 259 ++++++++++++++++++ .../AnnotationsLayerTest.groovy | 21 +- .../service/mocks/MockTinyRemapper.groovy | 5 +- src/test/resources/unpick/remapped.unpick | 4 +- 13 files changed, 496 insertions(+), 39 deletions(-) create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java index de3d43f0..714028f2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsFactory.java @@ -134,13 +134,13 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) { } private void writeAnnotationData(LayeredMappingsProcessor processor, List layers, Path mappingsFile) throws IOException { - AnnotationsData annotationsData = processor.getAnnotationsData(layers); + List annotationsData = processor.getAnnotationsData(layers); - if (annotationsData == null) { + if (annotationsData.isEmpty()) { return; } - byte[] data = AnnotationsData.GSON.toJson(annotationsData.toJson()).getBytes(StandardCharsets.UTF_8); + byte[] data = AnnotationsData.GSON.toJson(AnnotationsData.listToJson(annotationsData)).getBytes(StandardCharsets.UTF_8); ZipUtils.add(mappingsFile, AnnotationsLayer.ANNOTATIONS_PATH, data); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java index 54afa31a..3aa6736f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsProcessor.java @@ -119,20 +119,15 @@ public class LayeredMappingsProcessor { return mappingTree; } - @Nullable - public AnnotationsData getAnnotationsData(List layers) throws IOException { - AnnotationsData result = null; + public List getAnnotationsData(List layers) throws IOException { + List result = new ArrayList<>(); for (MappingLayer layer : layers) { if (layer instanceof AnnotationsLayer annotationsLayer) { AnnotationsData annotationsData = annotationsLayer.getAnnotationsData(); if (annotationsData != null) { - if (result == null) { - result = annotationsData; - } else { - result = result.merge(annotationsData); - } + result.add(annotationsData); } } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java index f1b654c7..58e4dbb4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java @@ -77,8 +77,7 @@ public class MappingConfiguration { public final Path tinyMappingsJar; private final Path unpickDefinitions; - @Nullable - private AnnotationsData annotationsData; + private List annotationsData = List.of(); @Nullable private UnpickMetadata unpickMetadata; private Map signatureFixes; @@ -233,7 +232,7 @@ public class MappingConfiguration { } try (BufferedReader reader = Files.newBufferedReader(annotationsPath, StandardCharsets.UTF_8)) { - annotationsData = AnnotationsData.read(reader); + annotationsData = AnnotationsData.readList(reader); } } @@ -312,8 +311,7 @@ public class MappingConfiguration { return unpickMetadata != null; } - @Nullable - public AnnotationsData getAnnotationsData() { + public List getAnnotationsData() { return annotationsData; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java index 27b516d9..0e834a11 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java @@ -24,19 +24,35 @@ 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.commons.AnnotationRemapper; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.TypeAnnotationNode; -public record AnnotationsData(Map classes) { +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 classes, String namespace) { public static final Gson GSON = new GsonBuilder() .disableHtmlEscaping() .setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) @@ -45,34 +61,139 @@ public record AnnotationsData(Map classes) { .registerTypeAdapter(AnnotationNode.class, new AnnotationNodeSerializer()) .registerTypeAdapterFactory(new SkipEmptyTypeAdapterFactory()) .create(); + private static final Type LIST_TYPE = new TypeToken>() { }.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 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 != 1) { + if (version != CURRENT_VERSION) { throw new JsonSyntaxException("Invalid annotations version " + version + ". Try updating loom"); } - - return GSON.fromJson(json, AnnotationsData.class); } public JsonObject toJson() { JsonObject json = GSON.toJsonTree(this).getAsJsonObject(); JsonObject result = new JsonObject(); - result.addProperty("version", 1); + result.addProperty("version", CURRENT_VERSION); result.asMap().putAll(json.asMap()); return result; } + public static JsonObject listToJson(List 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 newClassData = new LinkedHashMap<>(classes); other.classes.forEach((key, value) -> newClassData.merge(key, value, ClassAnnotationData::merge)); - return new AnnotationsData(newClassData); + 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(new AnnotationRemapper(node.desc, remapped, remapper.getEnvironment().getRemapper())); + 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(new AnnotationRemapper(node.desc, remapped, remapper.getEnvironment().getRemapper())); + return remapped; + } + + static Map remapMap(Map map, Function, K> keyRemapper, Function, V> valueRemapper) { + Map result = LinkedHashMap.newLinkedHashMap(map.size()); + + for (Map.Entry 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 datas = mappingConfiguration.getAnnotationsData(); + + if (datas.isEmpty()) { + return null; + } + + Map 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 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); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java index dc0d2651..14ed43c5 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/ClassAnnotationData.java @@ -30,6 +30,7 @@ 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; @@ -37,6 +38,9 @@ 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 annotationsToRemove, @@ -90,6 +94,49 @@ public record ClassAnnotationData( 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; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java index 4c858811..94a2e96e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/GenericAnnotationData.java @@ -28,12 +28,15 @@ 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 annotationsToRemove, @@ -74,6 +77,15 @@ public record GenericAnnotationData( 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; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java index 45bde212..fde79765 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/MethodAnnotationData.java @@ -30,12 +30,15 @@ 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 annotationsToRemove, @@ -83,6 +86,20 @@ public record MethodAnnotationData( 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; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java index e2b4307f..a58e5fb5 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationKey.java @@ -24,5 +24,10 @@ 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)); + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index 819cb195..10fca71d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -244,15 +244,14 @@ public abstract class AbstractMappedMinecraftProvider 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) -> { - AnnotationsData annotationsData = mappingConfiguration.getAnnotationsData(); - - if (annotationsData != null) { - builder.extraPostApplyVisitor(new AnnotationsApplyVisitor(annotationsData)); + if (remappedAnnotations != null) { + builder.extraPostApplyVisitor(new AnnotationsApplyVisitor(remappedAnnotations)); } builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures)); diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy new file mode 100644 index 00000000..44b4b491 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy @@ -0,0 +1,259 @@ +/* + * 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 spock.lang.Specification + +import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData +import net.fabricmc.loom.test.unit.service.mocks.MockTinyRemapper +import net.fabricmc.tinyremapper.api.TrClass +import net.fabricmc.tinyremapper.api.TrField +import net.fabricmc.tinyremapper.api.TrMethod + +import static org.mockito.Mockito.* + +class AnnotationsDataRemapTest extends Specification { + def "remap annotations data"() { + given: + def reader = new BufferedReader(new StringReader(ANNOTATIONS)) + def annotationsData = AnnotationsData.read(reader) + + def mockTr = new MockTinyRemapper() + + when(mockTr.remapper.map("pkg/Foo")).thenReturn("mapped/pkg/FooMapped") + when(mockTr.remapper.map("pkg/Bar")).thenReturn("mapped/pkg/BarMapped") + + when(mockTr.remapper.map("pkg/Annotation1")).thenReturn("mapped/pkg/Annotation1Mapped") + when(mockTr.remapper.map("pkg/Annotation2")).thenReturn("mapped/pkg/Annotation2Mapped") + when(mockTr.remapper.map("pkg/Annotation3")).thenReturn("mapped/pkg/Annotation3Mapped") + when(mockTr.remapper.map("pkg/Annotation4")).thenReturn("mapped/pkg/Annotation4Mapped") + when(mockTr.remapper.map("pkg/Annotation5")).thenReturn("mapped/pkg/Annotation5Mapped") + when(mockTr.remapper.map("pkg/Annotation6")).thenReturn("mapped/pkg/Annotation6Mapped") + when(mockTr.remapper.map("pkg/Annotation7")).thenReturn("mapped/pkg/Annotation7Mapped") + when(mockTr.remapper.map("pkg/Annotation8")).thenReturn("mapped/pkg/Annotation8Mapped") + + when(mockTr.remapper.map("pkg/MyEnum")).thenReturn("mapped/pkg/MyEnumMapped") + + when(mockTr.remapper.map("baz")).thenReturn("mapped/baz") + + when(mockTr.remapper.mapFieldName("pkg/Foo", "bar", "Lbaz;")).thenReturn("barRenamed") + when(mockTr.remapper.mapMethodName("pkg/Foo", "bar", "()V")).thenReturn("barMethodRenamed") + + def mockClass = mock(TrClass.class) + when(mockTr.trEnvironment.getClass("pkg/Foo")).thenReturn(mockClass) + + def mockField = mock(TrField.class) + when(mockField.name).thenReturn("bar") + when(mockField.desc).thenReturn("Lbaz;") + when(mockClass.fields).thenReturn([mockField]) + + def mockMethod = mock(TrMethod.class) + when(mockMethod.name).thenReturn("bar") + when(mockMethod.desc).thenReturn("()V") + when(mockClass.methods).thenReturn([mockMethod]) + + when: + def remapped = annotationsData.remap(mockTr.tinyRemapper, "mapped") + + then: + def json = AnnotationsData.GSON.newBuilder() + .setPrettyPrinting() + .create() + .toJson(remapped.toJson()) + .replace(" ", "\t") + + json == REMAPPED_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": [ + "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/baz;": { + "remove": [ + "mapped/pkg/Annotation8Mapped" + ] + } + }, + "methods": { + "barMethodRenamed()V": { + "remove": [ + "mapped/pkg/Annotation8Mapped" + ] + } + } + }, + "mapped/pkg/BarMapped": { + "add": [ + { + "desc": "Lmapped/pkg/Annotation1Mapped;" + } + ] + } + }, + "namespace": "mapped" +} +""" +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy index fb5b1fdf..b9879b77 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsLayerTest.groovy @@ -44,11 +44,11 @@ class AnnotationsLayerTest extends Specification { "pkg/Annotation2", "pkg/Annotation3" ] as Set - annotationsData.classes()["pkg/Foo"].annotationsToAdd()[0].desc == "pkg/Annotation4" + 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] == ["pkg/MyEnum", "VALUE"] as String[] - annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7] instanceof AnnotationNode && annotationsData.classes()["pkg/Foo"].annotationsToAdd()[1].values[7].desc == "pkg/Annotation6" + 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;" @@ -83,10 +83,10 @@ class AnnotationsLayerTest extends Specification { ], "add": [ { - "desc": "pkg/Annotation4" + "desc": "Lpkg/Annotation4;" }, { - "desc": "pkg/Annotation5", + "desc": "Lpkg/Annotation5;", "values": { "foo": { "type": "int", @@ -98,12 +98,12 @@ class AnnotationsLayerTest extends Specification { }, "baz": { "type": "enum_constant", - "owner": "pkg/MyEnum", + "owner": "Lpkg/MyEnum;", "name": "VALUE" }, "ann": { "type": "annotation", - "desc": "pkg/Annotation6" + "desc": "Lpkg/Annotation6;" }, "arr": { "type": "array", @@ -123,7 +123,7 @@ class AnnotationsLayerTest extends Specification { ], "type_add": [ { - "desc": "pkg/Annotation7", + "desc": "Lpkg/Annotation7;", "type_ref": 22, "type_path": "[" } @@ -146,11 +146,12 @@ class AnnotationsLayerTest extends Specification { "pkg/Bar": { "add": [ { - "desc": "pkg/Annotation1" + "desc": "Lpkg/Annotation1;" } ] } - } + }, + "namespace": "someNamespace" } """ } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/service/mocks/MockTinyRemapper.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/service/mocks/MockTinyRemapper.groovy index 9721c806..5fec4a1b 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/service/mocks/MockTinyRemapper.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/service/mocks/MockTinyRemapper.groovy @@ -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) diff --git a/src/test/resources/unpick/remapped.unpick b/src/test/resources/unpick/remapped.unpick index a3dab5e2..a25355da 100644 --- a/src/test/resources/unpick/remapped.unpick +++ b/src/test/resources/unpick/remapped.unpick @@ -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 From a8c0b521287abe08451238c7da3a420b0b240968 Mon Sep 17 00:00:00 2001 From: Juuz <6596629+Juuxel@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:47:14 +0300 Subject: [PATCH 14/20] Add task for launching Enigma against the intermediary jar in dev envs (#1354) * Add task for launching Enigma against the intermediary jar in dev envs The task can be used for writing mod-provided javadoc. * Use multi-jar Enigma to support split source sets * Update to Enigma 3.0.1 * Add docs to ModEnigmaTask * Fix Enigma file extension * ModEnigmaTask: Fail if mapping file is missing --- gradle/runtime.libs.versions.toml | 8 +- .../loom/task/tool/ModEnigmaTask.java | 129 ++++++++++++++++++ .../net/fabricmc/loom/util/LoomProblems.java | 39 ++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java create mode 100644 src/main/java/net/fabricmc/loom/util/LoomProblems.java diff --git a/gradle/runtime.libs.versions.toml b/gradle/runtime.libs.versions.toml index f39fe4b0..e930aad1 100644 --- a/gradle/runtime.libs.versions.toml +++ b/gradle/runtime.libs.versions.toml @@ -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" [libraries] # Decompilers @@ -29,5 +30,6 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "j native-support = { module = "net.fabricmc:fabric-loom-native-support", version.ref = "native-support" } fabric-installer = { module = "net.fabricmc:fabric-installer", version.ref = "fabric-installer" } -# Debug tools -renderdoc = { module = "org.renderdoc:renderdoc", version.ref = "renderdoc" } # Not a maven dependency \ No newline at end of file +# Dev tools +renderdoc = { module = "org.renderdoc:renderdoc", version.ref = "renderdoc" } # Not a maven dependency +enigma-swing = { module = "cuchaz:enigma-swing", version.ref = "enigma" } diff --git a/src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java b/src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java new file mode 100644 index 00000000..3bf4b2d7 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/tool/ModEnigmaTask.java @@ -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. + * + *

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 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()); + }); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/LoomProblems.java b/src/main/java/net/fabricmc/loom/util/LoomProblems.java new file mode 100644 index 00000000..bba29b02 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/LoomProblems.java @@ -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); + } +} From 1f84eaf08769368e8f8c336e133cb029beaa7595 Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Thu, 25 Sep 2025 15:08:29 +0100 Subject: [PATCH 15/20] Fix NPE in AnnotationsApplyVisitor (#1371) * Fix NPE in AnnotationsApplyVisitor * Fix test --- .../minecraft/AnnotationsApplyVisitor.java | 21 +++++++++++-------- .../processor/AnnotationsApplyTest.groovy | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java index b22f2870..96a37748 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java @@ -46,31 +46,34 @@ import net.fabricmc.tinyremapper.api.TrClass; public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implements TinyRemapper.ApplyVisitorProvider { @Override public ClassVisitor insertApplyVisitor(TrClass cls, ClassVisitor next) { - return new AnnotationsApplyClassVisitor(next, cls.getName(), annotationsData); + 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, String className, AnnotationsData annotationsData) { + public AnnotationsApplyClassVisitor(ClassVisitor cv, ClassAnnotationData classData) { super(Constants.ASM_VERSION, cv); - this.classData = annotationsData.classes().get(className); + this.classData = classData; hasAddedAnnotations = false; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - if (classData != null) { - access = classData.modifyAccessFlags(access); - } - + 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 != null && classData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) { + if (classData.typeAnnotationsToRemove().contains(new TypeAnnotationKey(typeRef, typePath.toString(), Type.getType(descriptor).getInternalName()))) { return null; } @@ -79,7 +82,7 @@ public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implement @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (classData != null && classData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) { + if (classData.annotationsToRemove().contains(Type.getType(descriptor).getInternalName())) { return null; } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy index 9e212001..c540c187 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest.groovy @@ -46,9 +46,9 @@ class AnnotationsApplyTest extends Specification { def annotationData = AnnotationsData.read(new StringReader(ANNOTATIONS_DATA)) def annotatedNode1 = new ClassNode() - def classVisitor1 = new AnnotationsApplyVisitor.AnnotationsApplyClassVisitor(annotatedNode1, 'net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass1', annotationData) + 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, 'net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2', annotationData) + def classVisitor2 = new AnnotationsApplyVisitor.AnnotationsApplyClassVisitor(annotatedNode2, annotationData.classes().get('net/fabricmc/loom/test/unit/processor/AnnotationsApplyTest$ExampleClass2')) when: def classReader1 = new ClassReader(getClassBytes(ExampleClass1)) From b5f79ef8f64fc11d13f40ee4877b7202f88c3741 Mon Sep 17 00:00:00 2001 From: modmuss Date: Thu, 25 Sep 2025 19:47:25 +0100 Subject: [PATCH 16/20] Update Gradle test versions (#1370) * Update Gradle test versions * 9.3 nightly's * Don't ask. * Fix another test --- gradle/test.libs.versions.toml | 4 +-- .../configuration/CompileConfiguration.java | 26 ++++++++++++++++--- .../integration/MultiMcVersionTest.groovy | 5 ++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 77f61566..7e88ca44 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -7,8 +7,8 @@ java-debug = "0.53.1" mixin = "0.15.3+mixin.0.8.7" bouncycastle = "1.81" -gradle-latest = "9.0.0" -gradle-nightly = "9.2.0-20250902075334+0000" +gradle-latest = "9.1.0" +gradle-nightly = "9.3.0-20250923005153+0000" fabric-loader = "0.16.14" [libraries] diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index b889a68f..6c050bd0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -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; @@ -80,6 +81,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(); @@ -110,7 +113,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); @@ -151,8 +158,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(); @@ -457,4 +463,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)); + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/MultiMcVersionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/MultiMcVersionTest.groovy index 3c8d531b..14ccba0d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/MultiMcVersionTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/MultiMcVersionTest.groovy @@ -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("Fabric Loom:") == 1 where: - version << STANDARD_TEST_VERSIONS + version << [PRE_RELEASE_GRADLE] } } From cf42acf09f4583b1cf1176cc778657d33fca7815 Mon Sep 17 00:00:00 2001 From: modmuss Date: Sat, 27 Sep 2025 09:06:29 +0100 Subject: [PATCH 17/20] Remove Guava and Commons IO Dependencies (#1372) --- build.gradle | 9 --- gradle/libs.versions.toml | 4 -- .../api/decompilers/DecompilerOptions.java | 5 +- .../loom/build/nesting/JarNester.java | 8 +-- .../nesting/NestableJarGenerationTask.java | 7 +- .../configuration/FileDependencyInfo.java | 46 ++++++++++--- .../IntermediaryMappingsProvider.java | 7 +- .../mappings/IntermediateMappingsService.java | 4 +- .../mappings/TinyMappingsService.java | 4 +- .../mappings/tiny/MappingsMerger.java | 5 +- .../minecraft/MinecraftProvider.java | 10 +-- .../minecraft/MinecraftSourceSets.java | 10 +-- .../minecraft/verify/KnownVersions.java | 5 +- .../loom/task/AbstractRemapJarTask.java | 6 +- .../net/fabricmc/loom/task/LoomTasks.java | 4 +- .../task/launch/GenerateDLIConfigTask.java | 4 +- .../task/service/LorenzMappingService.java | 4 +- .../java/net/fabricmc/loom/util/CacheKey.java | 5 +- .../java/net/fabricmc/loom/util/Check.java | 42 ++++++++++++ .../loom/util/DeletingFileVisitor.java | 4 ++ .../java/net/fabricmc/loom/util/Lazy.java | 66 +++++++++++++++++++ .../loom/util/LibraryLocationLogger.java | 6 +- .../loom/util/gradle/SourceSetReference.java | 5 +- 23 files changed, 198 insertions(+), 72 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/util/Check.java create mode 100644 src/main/java/net/fabricmc/loom/util/Lazy.java diff --git a/build.gradle b/build.gradle index 7d030bac..aaf025ef 100644 --- a/build.gradle +++ b/build.gradle @@ -93,9 +93,7 @@ dependencies { implementation gradleApi() // libraries - implementation libs.commons.io implementation libs.gson - implementation libs.guava implementation libs.bundles.asm // game handling utils @@ -228,13 +226,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") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 825021d7..55a81536 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,7 @@ [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.2" @@ -28,9 +26,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" } diff --git a/src/main/java/net/fabricmc/loom/api/decompilers/DecompilerOptions.java b/src/main/java/net/fabricmc/loom/api/decompilers/DecompilerOptions.java index 931ab005..360036ae 100644 --- a/src/main/java/net/fabricmc/loom/api/decompilers/DecompilerOptions.java +++ b/src/main/java/net/fabricmc/loom/api/decompilers/DecompilerOptions.java @@ -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 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(), diff --git a/src/main/java/net/fabricmc/loom/build/nesting/JarNester.java b/src/main/java/net/fabricmc/loom/build/nesting/JarNester.java index 18f8b7e6..31967f3a 100644 --- a/src/main/java/net/fabricmc/loom/build/nesting/JarNester.java +++ b/src/main/java/net/fabricmc/loom/build/nesting/JarNester.java @@ -32,13 +32,13 @@ 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; import org.gradle.api.UncheckedIOException; import org.slf4j.Logger; +import net.fabricmc.loom.util.Check; import net.fabricmc.loom.util.Pair; import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; @@ -50,7 +50,7 @@ public class JarNester { return; } - Preconditions.checkArgument(FabricModJsonFactory.isModJar(modJar), "Cannot nest jars into none mod jar " + modJar.getName()); + Check.require(FabricModJsonFactory.isModJar(modJar), "Cannot nest jars into none mod jar " + modJar.getName()); // Ensure deterministic ordering of entries in fabric.mod.json Collection sortedJars = jars.stream().sorted(Comparator.comparing(File::getName)).toList(); @@ -73,7 +73,7 @@ public class JarNester { for (File file : sortedJars) { String nestedJarPath = "META-INF/jars/" + file.getName(); - Preconditions.checkArgument(FabricModJsonFactory.isModJar(file), "Cannot nest none mod jar: " + file.getName()); + Check.require(FabricModJsonFactory.isModJar(file), "Cannot nest none mod jar: " + file.getName()); for (JsonElement nestedJar : nestedJars) { JsonObject jsonObject = nestedJar.getAsJsonObject(); @@ -95,7 +95,7 @@ public class JarNester { return json; }))); - 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); } diff --git a/src/main/java/net/fabricmc/loom/build/nesting/NestableJarGenerationTask.java b/src/main/java/net/fabricmc/loom/build/nesting/NestableJarGenerationTask.java index 48da25b4..bd494b81 100644 --- a/src/main/java/net/fabricmc/loom/build/nesting/NestableJarGenerationTask.java +++ b/src/main/java/net/fabricmc/loom/build/nesting/NestableJarGenerationTask.java @@ -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; @@ -38,7 +39,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; 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; @@ -61,6 +61,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.ZipReprocessorUtil; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; @@ -88,7 +89,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); @@ -215,7 +216,7 @@ public abstract class NestableJarGenerationTask extends AbstractLoomTask { private void makeNestableJar(final File input, final File output, final String modJsonFile) { 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); } diff --git a/src/main/java/net/fabricmc/loom/configuration/FileDependencyInfo.java b/src/main/java/net/fabricmc/loom/configuration/FileDependencyInfo.java index 603b4c63..9b11776f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/FileDependencyInfo.java +++ b/src/main/java/net/fabricmc/loom/configuration/FileDependencyInfo.java @@ -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 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); @@ -121,7 +119,7 @@ public class FileDependencyInfo extends DependencyInfo { version = json.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) { @@ -150,4 +148,36 @@ public class FileDependencyInfo extends DependencyInfo { public Set resolve() { return this.resolvedFiles; } + + private static T getOnlyElement(Set 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 ""; + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java index f72914e4..30da8be2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java @@ -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)) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java index 16299dae..bc52ffb9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java @@ -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 getMinecraftVersion(); } - private final Supplier memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree); + private final Supplier memoryMappingTree = Lazy.of(this::createMemoryMappingTree); public IntermediateMappingsService(Options options, ServiceFactory serviceFactory) { super(options, serviceFactory); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/TinyMappingsService.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/TinyMappingsService.java index 6135aa23..daa4eee8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/TinyMappingsService.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/TinyMappingsService.java @@ -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 mappingTree = Suppliers.memoize(() -> { + private final Supplier mappingTree = Lazy.of(() -> { Path mappings = getOptions().getMappings().getSingleFile().toPath(); if (getOptions().getZipEntryPath().isPresent()) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java index 68c4ddc2..0a3d3a96 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.Map; import java.util.regex.Pattern; -import com.google.common.base.Stopwatch; import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +50,7 @@ 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.isLegacySplitOfficialNamespaceVersion()) { @@ -60,7 +59,7 @@ public final class MappingsMerger { mergeAndSaveMappings(from, out, intermediateMappingsService); } - LOGGER.info(":merged mappings in " + stopwatch.stop()); + LOGGER.info(":merged mappings in {}ms", System.currentTimeMillis() - start); } @VisibleForTesting diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index af4d4ecd..14fa8be9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -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; @@ -189,7 +189,7 @@ public abstract class MinecraftProvider { } private 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"); @@ -220,20 +220,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; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftSourceSets.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftSourceSets.java index 78489997..f5e4b5bf 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftSourceSets.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftSourceSets.java @@ -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); } @@ -151,9 +151,9 @@ public abstract sealed class MinecraftSourceSets permits MinecraftSourceSets.Sin @Override public void applyDependencies(BiConsumer consumer, List 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); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java index f585a956..37989180 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/verify/KnownVersions.java @@ -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 client, Map server) { - public static final Supplier INSTANCE = Suppliers.memoize(KnownVersions::load); + public static final Supplier INSTANCE = Lazy.of(KnownVersions::load); private static KnownVersions load() { try (InputStream is = KnownVersions.class.getClassLoader().getResourceAsStream("certs/known_versions.json"); diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java index 7e6493e1..f2846ee2 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRemapJarTask.java @@ -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; @@ -62,6 +61,7 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; 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; @@ -238,7 +238,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 { @@ -259,7 +259,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()); } } diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 8fd96545..7bddb6a9 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -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); diff --git a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java index c37ded2c..004209dc 100644 --- a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java +++ b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java @@ -27,6 +27,7 @@ package net.fabricmc.loom.task.launch; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -35,7 +36,6 @@ import java.util.Map; import java.util.StringJoiner; import java.util.stream.Collectors; -import org.apache.commons.io.FileUtils; import org.gradle.api.Project; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.logging.configuration.ConsoleOutput; @@ -161,7 +161,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) { diff --git a/src/main/java/net/fabricmc/loom/task/service/LorenzMappingService.java b/src/main/java/net/fabricmc/loom/task/service/LorenzMappingService.java index 40cc1b0b..3d48feb1 100644 --- a/src/main/java/net/fabricmc/loom/task/service/LorenzMappingService.java +++ b/src/main/java/net/fabricmc/loom/task/service/LorenzMappingService.java @@ -28,7 +28,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.function.Supplier; -import com.google.common.base.Suppliers; import org.cadixdev.lorenz.MappingSet; import org.gradle.api.Project; import org.gradle.api.provider.Property; @@ -37,6 +36,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; @@ -61,7 +61,7 @@ public final class LorenzMappingService extends Service mappings = Suppliers.memoize(this::readMappings); + private final Supplier mappings = Lazy.of(this::readMappings); public LorenzMappingService(Options options, ServiceFactory serviceFactory) { super(options, serviceFactory); diff --git a/src/main/java/net/fabricmc/loom/util/CacheKey.java b/src/main/java/net/fabricmc/loom/util/CacheKey.java index d6fe8ebf..a48c95ae 100644 --- a/src/main/java/net/fabricmc/loom/util/CacheKey.java +++ b/src/main/java/net/fabricmc/loom/util/CacheKey.java @@ -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 jsonSupplier = Suppliers.memoize(() -> GradleTypeAdapter.GSON.toJson(this)); - private final transient Supplier cacheKeySupplier = Suppliers.memoize(() -> Checksum.of(jsonSupplier.get()).sha1().hex(CHECKSUM_LENGTH)); + private final transient Supplier jsonSupplier = Lazy.of(() -> GradleTypeAdapter.GSON.toJson(this)); + private final transient Supplier cacheKeySupplier = Lazy.of(() -> Checksum.of(jsonSupplier.get()).sha1().hex(CHECKSUM_LENGTH)); public static T create(Project project, Class clazz, Action action) { T instance = project.getObjects().newInstance(clazz); diff --git a/src/main/java/net/fabricmc/loom/util/Check.java b/src/main/java/net/fabricmc/loom/util/Check.java new file mode 100644 index 00000000..6ce9a4e4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/Check.java @@ -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(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/DeletingFileVisitor.java b/src/main/java/net/fabricmc/loom/util/DeletingFileVisitor.java index 0bf97022..b65bcbef 100644 --- a/src/main/java/net/fabricmc/loom/util/DeletingFileVisitor.java +++ b/src/main/java/net/fabricmc/loom/util/DeletingFileVisitor.java @@ -32,6 +32,10 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; public class DeletingFileVisitor extends SimpleFileVisitor { + 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); diff --git a/src/main/java/net/fabricmc/loom/util/Lazy.java b/src/main/java/net/fabricmc/loom/util/Lazy.java new file mode 100644 index 00000000..5c2c0109 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/Lazy.java @@ -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 Supplier of(Supplier supplier) { + return new Impl<>(supplier); + } + + private static final class Impl implements Supplier { + final Supplier supplier; + + volatile boolean initialized = false; + @Nullable + T value = null; + + private Impl(Supplier 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; + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/LibraryLocationLogger.java b/src/main/java/net/fabricmc/loom/util/LibraryLocationLogger.java index 1d328c57..beab3bc9 100644 --- a/src/main/java/net/fabricmc/loom/util/LibraryLocationLogger.java +++ b/src/main/java/net/fabricmc/loom/util/LibraryLocationLogger.java @@ -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); diff --git a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetReference.java b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetReference.java index 6876446e..9e2ab367 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetReference.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetReference.java @@ -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()) ); From 8cbe10cb55d19f70ed4f52282b61c4ec19051baa Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 29 Sep 2025 09:53:35 +0100 Subject: [PATCH 18/20] Add system property to renderdoc tasks. (#1373) * Add system property to renderdoc tasks. * Better comment --- src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java | 1 + src/main/java/net/fabricmc/loom/util/Constants.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java b/src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java index c6e851bb..e02b53f6 100644 --- a/src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java +++ b/src/main/java/net/fabricmc/loom/task/RenderDocRunTask.java @@ -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()) { diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 6f6e3236..24294c0d 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -157,6 +157,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 class Manifest { From 312dcc7ca6e2ec7bd1286f21e04a707339b7bd2b Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Mon, 29 Sep 2025 22:25:18 +0100 Subject: [PATCH 19/20] Fix annotation visitor in AnnotationsData.remap (#1374) * Fix annotation visitor in AnnotationsData.remap * Use real TinyRemapper --- gradle/libs.versions.toml | 2 +- .../extras/annotations/AnnotationsData.java | 5 +- .../AnnotationsDataRemapTest.groovy | 74 +++++++++---------- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55a81536..35d33302 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ asm = "9.8" gson = "2.10.1" stitch = "0.6.2" -tiny-remapper = "0.11.2" +tiny-remapper = "0.12.0" access-widener = "2.1.0" mapping-io = "0.7.1" lorenz-tiny = "4.0.2" diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java index 0e834a11..b11cba57 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/AnnotationsData.java @@ -42,7 +42,6 @@ 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.commons.AnnotationRemapper; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.TypeAnnotationNode; @@ -142,13 +141,13 @@ public record AnnotationsData(Map classes, String n static AnnotationNode remap(AnnotationNode node, TinyRemapper remapper) { AnnotationNode remapped = new AnnotationNode(remapper.getEnvironment().getRemapper().mapDesc(node.desc)); - node.accept(new AnnotationRemapper(node.desc, remapped, remapper.getEnvironment().getRemapper())); + 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(new AnnotationRemapper(node.desc, remapped, remapper.getEnvironment().getRemapper())); + node.accept(remapper.createAnnotationRemapperVisitor(remapped, node.desc)); return remapped; } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy index 44b4b491..57de298f 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest.groovy @@ -24,16 +24,15 @@ 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.loom.test.unit.service.mocks.MockTinyRemapper -import net.fabricmc.tinyremapper.api.TrClass -import net.fabricmc.tinyremapper.api.TrField -import net.fabricmc.tinyremapper.api.TrMethod - -import static org.mockito.Mockito.* +import net.fabricmc.tinyremapper.IMappingProvider +import net.fabricmc.tinyremapper.TinyRemapper class AnnotationsDataRemapTest extends Specification { def "remap annotations data"() { @@ -41,42 +40,33 @@ class AnnotationsDataRemapTest extends Specification { def reader = new BufferedReader(new StringReader(ANNOTATIONS)) def annotationsData = AnnotationsData.read(reader) - def mockTr = new MockTinyRemapper() + 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') - when(mockTr.remapper.map("pkg/Foo")).thenReturn("mapped/pkg/FooMapped") - when(mockTr.remapper.map("pkg/Bar")).thenReturn("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') - when(mockTr.remapper.map("pkg/Annotation1")).thenReturn("mapped/pkg/Annotation1Mapped") - when(mockTr.remapper.map("pkg/Annotation2")).thenReturn("mapped/pkg/Annotation2Mapped") - when(mockTr.remapper.map("pkg/Annotation3")).thenReturn("mapped/pkg/Annotation3Mapped") - when(mockTr.remapper.map("pkg/Annotation4")).thenReturn("mapped/pkg/Annotation4Mapped") - when(mockTr.remapper.map("pkg/Annotation5")).thenReturn("mapped/pkg/Annotation5Mapped") - when(mockTr.remapper.map("pkg/Annotation6")).thenReturn("mapped/pkg/Annotation6Mapped") - when(mockTr.remapper.map("pkg/Annotation7")).thenReturn("mapped/pkg/Annotation7Mapped") - when(mockTr.remapper.map("pkg/Annotation8")).thenReturn("mapped/pkg/Annotation8Mapped") + mappings.acceptClass('pkg/MyEnum', 'mapped/pkg/MyEnumMapped') - when(mockTr.remapper.map("pkg/MyEnum")).thenReturn("mapped/pkg/MyEnumMapped") + mappings.acceptClass('baz', 'mapped/baz') - when(mockTr.remapper.map("baz")).thenReturn("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() - when(mockTr.remapper.mapFieldName("pkg/Foo", "bar", "Lbaz;")).thenReturn("barRenamed") - when(mockTr.remapper.mapMethodName("pkg/Foo", "bar", "()V")).thenReturn("barMethodRenamed") - - def mockClass = mock(TrClass.class) - when(mockTr.trEnvironment.getClass("pkg/Foo")).thenReturn(mockClass) - - def mockField = mock(TrField.class) - when(mockField.name).thenReturn("bar") - when(mockField.desc).thenReturn("Lbaz;") - when(mockClass.fields).thenReturn([mockField]) - - def mockMethod = mock(TrMethod.class) - when(mockMethod.name).thenReturn("bar") - when(mockMethod.desc).thenReturn("()V") - when(mockClass.methods).thenReturn([mockMethod]) + remapper.readClassPath(Path.of(Foo.class.protectionDomain.codeSource.location.toURI())) when: - def remapped = annotationsData.remap(mockTr.tinyRemapper, "mapped") + def remapped = annotationsData.remap(remapper, "mapped") then: def json = AnnotationsData.GSON.newBuilder() @@ -88,12 +78,20 @@ class AnnotationsDataRemapTest extends Specification { json == REMAPPED_ANNOTATIONS.trim() } + @CompileStatic + class Foo { + Foo bar + + void bar() { + } + } + @Language("JSON") private static final String ANNOTATIONS = """ { "version": 1, "classes": { - "pkg/Foo": { + "net/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest${'$'}Foo": { "remove": [ "pkg/Annotation1", "pkg/Annotation2", @@ -147,7 +145,7 @@ class AnnotationsDataRemapTest extends Specification { } ], "fields": { - "bar:Lbaz;": { + "bar:Lnet/fabricmc/loom/test/unit/layeredmappings/AnnotationsDataRemapTest${'$'}Foo;": { "remove": [ "pkg/Annotation8" ] @@ -231,7 +229,7 @@ class AnnotationsDataRemapTest extends Specification { } ], "fields": { - "barRenamed:Lmapped/baz;": { + "barRenamed:Lmapped/pkg/FooMapped;": { "remove": [ "mapped/pkg/Annotation8Mapped" ] From 7484a7fd95b7547ee649ce77b974ae75694c963d Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Tue, 30 Sep 2025 13:38:13 +0100 Subject: [PATCH 20/20] Call visitAnnotableParameterCount (#1376) --- .../minecraft/AnnotationsApplyVisitor.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java index 96a37748..786b6114 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/AnnotationsApplyVisitor.java @@ -28,6 +28,7 @@ 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; @@ -240,8 +241,19 @@ public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implement } 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())) { @@ -271,6 +283,16 @@ public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implement 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(); @@ -306,6 +328,11 @@ public record AnnotationsApplyVisitor(AnnotationsData annotationsData) implement } } + 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);