diff --git a/.github/workflows/publish-exp.yml b/.github/workflows/publish-exp.yml new file mode 100644 index 00000000..05e14c9a --- /dev/null +++ b/.github/workflows/publish-exp.yml @@ -0,0 +1,30 @@ +name: Publish +on: + push: + branches: + - 'exp/**' + +jobs: + build: + runs-on: ubuntu-22.04 + container: + image: eclipse-temurin:17-jdk + options: --user root + steps: + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v2 + + # Generate the build number based on tags to allow per branch build numbers, not something github provides by default. + - name: Generate build number + id: buildnumber + uses: onyxmueller/build-tag-number@v1 + with: + token: ${{ secrets.github_token }} + prefix: "build/exp/${{ github.ref }}" + + - run: ./gradlew build publish -x test --stacktrace + env: + MAVEN_URL: ${{ secrets.MAVEN_URL }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + EXPERIMENTAL: true \ No newline at end of file diff --git a/bootstrap/src/main/resources/META-INF/trick-gradle-into-thinking-loom-is-signed.SF b/bootstrap/src/main/resources/META-INF/trick-gradle-into-thinking-loom-is-signed.SF deleted file mode 100644 index 9a1b230b..00000000 --- a/bootstrap/src/main/resources/META-INF/trick-gradle-into-thinking-loom-is-signed.SF +++ /dev/null @@ -1,4 +0,0 @@ -Trick gradle into thinking that loom is signed to skip over transforming all classes in the jar. -This is required to get the bootstrap to well bootstrap on older gradle versions that dont support java 16. - -See https://github.com/gradle/gradle/blob/master/subprojects/core/src/main/java/org/gradle/internal/classpath/InstrumentingClasspathFileTransformer.java#L129 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 845b200a..b020c3c0 100644 --- a/build.gradle +++ b/build.gradle @@ -316,6 +316,16 @@ test { } } +// Workaround https://github.com/gradle/gradle/issues/25898 +tasks.withType(Test).configureEach { + jvmArgs = [ + '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.lang.invoke=ALL-UNNAMED', + '--add-opens=java.base/java.net=ALL-UNNAMED' + ] +} + import org.gradle.api.internal.artifacts.configurations.ConfigurationRoles import org.gradle.launcher.cli.KotlinDslVersion @@ -326,8 +336,7 @@ import org.w3c.dom.Node publishing { publications { - if (isSnapshot) return - + if (!isSnapshot && !ENV.EXPERIMENTAL) { // Also publish a snapshot so people can use the latest version if they wish snapshot(MavenPublication) { publication -> groupId project.group @@ -337,24 +346,25 @@ publishing { from components.java } - // Manually crate the plugin marker for snapshot versions - snapshotPlugin(MavenPublication) { - groupId 'dev.architectury.loom' - artifactId 'dev.architectury.loom.gradle.plugin' - version baseVersion + '-SNAPSHOT' + // Manually crate the plugin marker for snapshot versions + snapshotPlugin(MavenPublication) { + groupId 'dev.architectury.loom' + artifactId 'dev.architectury.loom.gradle.plugin' + version baseVersion + '-SNAPSHOT' - pom.withXml { - // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin - Element root = asElement() - Document document = root.getOwnerDocument() - Node dependencies = root.appendChild(document.createElement('dependencies')) - Node dependency = dependencies.appendChild(document.createElement('dependency')) - Node groupId = dependency.appendChild(document.createElement('groupId')) - groupId.setTextContent(project.group) - Node artifactId = dependency.appendChild(document.createElement('artifactId')) - artifactId.setTextContent(project.archivesBaseName) - Node version = dependency.appendChild(document.createElement('version')) - version.setTextContent(baseVersion + '-SNAPSHOT') + pom.withXml({ + // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin + Element root = asElement() + Document document = root.getOwnerDocument() + Node dependencies = root.appendChild(document.createElement('dependencies')) + Node dependency = dependencies.appendChild(document.createElement('dependency')) + Node groupId = dependency.appendChild(document.createElement('groupId')) + groupId.setTextContent(project.group) + Node artifactId = dependency.appendChild(document.createElement('artifactId')) + artifactId.setTextContent(project.archivesBaseName) + Node version = dependency.appendChild(document.createElement('version')) + version.setTextContent(baseVersion + '-SNAPSHOT') + }) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06c1187f..53e8573e 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.10.1" +tiny-remapper = "0.10.3" access-widener = "2.1.0" mapping-io = "0.6.1" lorenz-tiny = "4.0.2" diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 8cc5457c..5de78a1f 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -6,7 +6,7 @@ mockito = "5.11.0" java-debug = "0.52.0" mixin = "0.12.5+mixin.0.8.5" -gradle-nightly = "8.9-20240426001649+0000" +gradle-nightly = "8.9-20240505002558+0000" fabric-loader = "0.15.10" fabric-installer = "1.0.1" diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index 40be7917..060241b5 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -44,6 +44,7 @@ import net.fabricmc.loom.configuration.LoomConfigurations; import net.fabricmc.loom.configuration.MavenPublication; import net.fabricmc.loom.configuration.ide.IdeConfiguration; import net.fabricmc.loom.configuration.ide.idea.IdeaConfiguration; +import net.fabricmc.loom.configuration.sandbox.SandboxConfiguration; import net.fabricmc.loom.decompilers.DecompilerConfiguration; import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.extension.LoomGradleExtensionImpl; @@ -67,7 +68,8 @@ public class LoomGradlePlugin implements BootstrappedPlugin { LoomTasks.class, DecompilerConfiguration.class, IdeaConfiguration.class, - IdeConfiguration.class + IdeConfiguration.class, + SandboxConfiguration.class ); @Override diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 6a3bfdb0..14571ec4 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -44,6 +44,7 @@ import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.ApiStatus; import net.fabricmc.loom.api.decompilers.DecompilerOptions; +import net.fabricmc.loom.api.manifest.VersionsManifestsAPI; import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; import net.fabricmc.loom.api.mappings.layered.spec.LayeredMappingSpecBuilder; import net.fabricmc.loom.api.processor.MinecraftJarProcessor; @@ -53,6 +54,7 @@ import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.configuration.processors.JarProcessor; import net.fabricmc.loom.configuration.providers.mappings.NoOpIntermediateMappingsProvider; +import net.fabricmc.loom.configuration.providers.minecraft.ManifestLocations; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.task.GenerateSourcesTask; import net.fabricmc.loom.util.DeprecationHelper; @@ -135,7 +137,23 @@ public interface LoomGradleExtensionAPI { InterfaceInjectionExtensionAPI getInterfaceInjection(); - Property getCustomMinecraftManifest(); + @ApiStatus.Experimental + default void versionsManifests(Action action) { + action.execute(getVersionsManifests()); + } + + @ApiStatus.Experimental + ManifestLocations getVersionsManifests(); + + /** + * @deprecated use {@linkplain #getCustomMinecraftMetadata} instead + */ + @Deprecated + default Property getCustomMinecraftManifest() { + return getCustomMinecraftMetadata(); + } + + Property getCustomMinecraftMetadata(); SetProperty getKnownIndyBsms(); diff --git a/src/main/java/net/fabricmc/loom/api/manifest/VersionsManifestsAPI.java b/src/main/java/net/fabricmc/loom/api/manifest/VersionsManifestsAPI.java new file mode 100644 index 00000000..e4fbc063 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/manifest/VersionsManifestsAPI.java @@ -0,0 +1,46 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.manifest; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface VersionsManifestsAPI { + /** + * Adds a URL to a versions manifest json with the default priority of {@code 0}. + * @param url the String-representation of the URL to the manifest json + */ + default void add(String url) { + add(url, 0); + } + + /** + * Adds a URL to a versions manifest json with the given priority. + * @param url the String-representation of the URL to the manifest json + * @param priority the priority with which this URL gets sorted against other entries + * entries are sorted by priority, from lowest to highest + */ + void add(String url, int priority); +} diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java index 2ba73382..c32779c5 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java @@ -115,18 +115,6 @@ public class RunConfig { return e; } - private static void populate(Project project, LoomGradleExtension extension, RunConfig runConfig, String environment, boolean appendProjectPath) { - if (appendProjectPath && !extension.isRootProject()) { - runConfig.configName += " (" + project.getPath() + ")"; - } - - runConfig.eclipseProjectName = project.getExtensions().getByType(EclipseModel.class).getProject().getName(); - - runConfig.mainClass = "net.fabricmc.devlaunchinjector.Main"; - runConfig.vmArgs.add("-Dfabric.dli.config=" + encodeEscaped(extension.getFiles().getDevLauncherConfig().getAbsolutePath())); - runConfig.vmArgs.add("-Dfabric.dli.env=" + environment.toLowerCase()); - } - // Turns camelCase/PascalCase into Capital Case // caseConversionExample -> Case Conversion Example private static String capitalizeCamelCaseName(String name) { @@ -184,7 +172,15 @@ public class RunConfig { boolean appendProjectPath = settings.getAppendProjectPathToConfigName().get(); RunConfig runConfig = new RunConfig(); runConfig.configName = configName; - populate(project, extension, runConfig, environment, appendProjectPath); + + if (appendProjectPath && !extension.isRootProject()) { + runConfig.configName += " (" + project.getPath() + ")"; + } + + runConfig.mainClass = settings.devLaunchMainClass().get(); + runConfig.vmArgs.add("-Dfabric.dli.config=" + encodeEscaped(extension.getFiles().getDevLauncherConfig().getAbsolutePath())); + runConfig.vmArgs.add("-Dfabric.dli.env=" + environment.toLowerCase()); + runConfig.eclipseProjectName = project.getExtensions().getByType(EclipseModel.class).getProject().getName(); runConfig.ideaModuleName = IdeaUtils.getIdeaModuleName(new SourceSetReference(sourceSet, project)); runConfig.runDirIdeaUrl = "file://$PROJECT_DIR$/" + runDir; runConfig.runDir = runDir; diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java index c89e40af..fb12b246 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java @@ -104,6 +104,14 @@ public class RunConfigSettings implements Named { */ private final Property mainClass; + /** + * The true entrypoint, this is usually dev launch injector. + * This should not be changed unless you know what you are doing. + */ + @ApiStatus.Internal + @ApiStatus.Experimental + private final Property devLaunchMainClass; + /** * The source set getter, which obtains the source set from the given project. */ @@ -148,6 +156,7 @@ public class RunConfigSettings implements Named { Objects.requireNonNull(defaultMainClass, "Run config " + name + " must specify default main class"); return RunConfig.getMainClass(environment, extension, defaultMainClass); })); + this.devLaunchMainClass = project.getObjects().property(String.class).convention("net.fabricmc.devlaunchinjector.Main"); this.mods = project.getObjects().domainObjectContainer(ModSettings.class); setSource(p -> { @@ -450,6 +459,12 @@ public class RunConfigSettings implements Named { this.ideConfigGenerated = ideConfigGenerated; } + @ApiStatus.Internal + @ApiStatus.Experimental + public Property devLaunchMainClass() { + return devLaunchMainClass; + } + /** * {@return a container of mod settings for this run configuration} * 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 24058594..d34c4666 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 @@ -31,7 +31,6 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -40,6 +39,7 @@ import org.gradle.api.artifacts.Dependency; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.api.mappings.layered.MappingContext; import net.fabricmc.loom.api.mappings.layered.MappingLayer; @@ -83,6 +83,7 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) { } public Path resolve(Project project) throws IOException { + final LoomGradleExtension extension = LoomGradleExtension.get(project); final MappingContext mappingContext = new GradleMappingContext(project, spec.getVersion().replace("+", "_").replace(".", "_")); final Path mappingsDir = mappingContext.minecraftProvider().dir("layered").toPath(); final Path mappingsZip = mappingsDir.resolve(String.format("%s.%s-%s.jar", GROUP, MODULE, spec.getVersion())); @@ -91,7 +92,8 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) { return mappingsZip; } - var processor = new LayeredMappingsProcessor(spec); + boolean noIntermediateMappings = extension.getIntermediateMappingsProvider() instanceof NoOpIntermediateMappingsProvider; + var processor = new LayeredMappingsProcessor(spec, noIntermediateMappings); List layers = processor.resolveLayers(mappingContext); Files.deleteIfExists(mappingsZip); @@ -117,7 +119,7 @@ public record LayeredMappingsFactory(LayeredMappingSpec spec) { try (Writer writer = new StringWriter()) { var tiny2Writer = new Tiny2FileWriter(writer, false); - MappingDstNsReorder nsReorder = new MappingDstNsReorder(tiny2Writer, Collections.singletonList(MappingsNamespace.NAMED.toString())); + MappingDstNsReorder nsReorder = new MappingDstNsReorder(tiny2Writer, List.of(MappingsNamespace.NAMED.toString(), MappingsNamespace.OFFICIAL.toString())); MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(nsReorder, MappingsNamespace.INTERMEDIARY.toString(), true); AddConstructorMappingVisitor addConstructor = new AddConstructorMappingVisitor(nsSwitch); mappings.accept(addConstructor); 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 6f4a9f63..e35ff242 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 @@ -40,14 +40,17 @@ 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.signatures.SignatureFixesLayer; import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer; +import net.fabricmc.mappingio.adapter.MappingNsCompleter; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.tree.MemoryMappingTree; public class LayeredMappingsProcessor { private final LayeredMappingSpec layeredMappingSpec; + private final boolean noIntermediateMappings; - public LayeredMappingsProcessor(LayeredMappingSpec spec) { + public LayeredMappingsProcessor(LayeredMappingSpec spec, boolean noIntermediateMappings) { this.layeredMappingSpec = spec; + this.noIntermediateMappings = noIntermediateMappings; } public List resolveLayers(MappingContext context) { @@ -104,6 +107,13 @@ public class LayeredMappingsProcessor { } } + if (noIntermediateMappings) { + // HACK: Populate intermediary with named when there are no intermediary mappings being used. + MemoryMappingTree completedTree = new MemoryMappingTree(); + mappingTree.accept(new MappingNsCompleter(completedTree, Map.of("intermediary", "named"))); + return completedTree; + } + return mappingTree; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestLocations.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestLocations.java new file mode 100644 index 00000000..f127e7d3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestLocations.java @@ -0,0 +1,93 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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 java.nio.file.Path; +import java.util.Iterator; +import java.util.PriorityQueue; +import java.util.Queue; + +import net.fabricmc.loom.api.manifest.VersionsManifestsAPI; +import net.fabricmc.loom.configuration.providers.minecraft.ManifestLocations.ManifestLocation; + +public class ManifestLocations implements VersionsManifestsAPI, Iterable { + private static final String FILE_EXTENSION = ".json"; + private final Queue locations = new PriorityQueue<>(); + private final String baseFileName; + + public ManifestLocations(String baseFileName) { + this.baseFileName = baseFileName; + } + + public void addBuiltIn(int priority, String url, String fileName) { + locations.add(new ManifestLocation(priority, url, fileName)); + } + + @Override + public void add(String url, int priority) { + locations.add(new ManifestLocation(priority, url)); + } + + @Override + public Iterator iterator() { + return locations.iterator(); + } + + public class ManifestLocation implements Comparable { + private final int priority; + private final String url; + private final String builtInFileName; + + private ManifestLocation(int priority, String url) { + this(priority, url, null); + } + + private ManifestLocation(int priority, String url, String builtInFileName) { + this.priority = priority; + this.url = url; + this.builtInFileName = builtInFileName; + } + + public boolean isBuiltIn() { + return builtInFileName != null; + } + + public String url() { + return url; + } + + public Path cacheFile(Path dir) { + String fileName = (builtInFileName != null) + ? builtInFileName + FILE_EXTENSION + : baseFileName + "-" + Integer.toHexString(url.hashCode()) + FILE_EXTENSION; + return dir.resolve(fileName); + } + + @Override + public int compareTo(ManifestLocation o) { + return Integer.compare(priority, o.priority); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java index fd2eea8e..afcf3d27 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java @@ -27,25 +27,27 @@ package net.fabricmc.loom.configuration.providers.minecraft; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.function.Function; import org.gradle.api.Project; +import org.gradle.api.provider.Property; import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.DependencyInfo; +import net.fabricmc.loom.configuration.providers.minecraft.ManifestLocations.ManifestLocation; import net.fabricmc.loom.util.Constants; -import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.download.DownloadBuilder; public final class MinecraftMetadataProvider { private final Options options; private final Function download; - private ManifestVersion.Versions versionEntry; + private ManifestEntryLocation versionEntry; private MinecraftVersionMeta versionMeta; private MinecraftMetadataProvider(Options options, Function download) { @@ -55,13 +57,11 @@ public final class MinecraftMetadataProvider { public static MinecraftMetadataProvider create(ConfigContext configContext) { final String minecraftVersion = resolveMinecraftVersion(configContext.project()); - final Path workingDir = MinecraftProvider.minecraftWorkingDirectory(configContext.project(), minecraftVersion).toPath(); return new MinecraftMetadataProvider( MinecraftMetadataProvider.Options.create( minecraftVersion, - configContext.project(), - workingDir.resolve("minecraft-info.json") + configContext.project() ), configContext.extension()::download ); @@ -92,28 +92,29 @@ public final class MinecraftMetadataProvider { return versionMeta; } - private ManifestVersion.Versions getVersionEntry() throws IOException { + private ManifestEntryLocation getVersionEntry() throws IOException { // Custom URL always takes priority if (options.customManifestUrl() != null) { - ManifestVersion.Versions customVersion = new ManifestVersion.Versions(); + VersionsManifest.Version customVersion = new VersionsManifest.Version(); customVersion.id = options.minecraftVersion(); customVersion.url = options.customManifestUrl(); - return customVersion; + return new ManifestEntryLocation(null, customVersion); } - final List suppliers = List.of( - // First try finding the version with caching - () -> getVersions(false), - // Then try finding the experimental version with caching - () -> getExperimentalVersions(false), - // Then force download Mojang's metadata to find the version - () -> getVersions(true), - // Finally try a force downloaded experimental metadata. - () -> getExperimentalVersions(true) - ); + final List suppliers = new ArrayList<>(); - for (ManifestVersionSupplier supplier : suppliers) { - final ManifestVersion.Versions version = supplier.get().getVersion(options.minecraftVersion()); + // First try finding the version with caching + for (ManifestLocation location : options.versionsManifests()) { + suppliers.add(() -> getManifestEntry(location, false)); + } + + // Then force download the manifest to find the version + for (ManifestLocation location : options.versionsManifests()) { + suppliers.add(() -> getManifestEntry(location, true)); + } + + for (ManifestEntrySupplier supplier : suppliers) { + final ManifestEntryLocation version = supplier.get(); if (version != null) { return version; @@ -123,16 +124,8 @@ public final class MinecraftMetadataProvider { throw new RuntimeException("Failed to find minecraft version: " + options.minecraftVersion()); } - private ManifestVersion getVersions(boolean forceDownload) throws IOException { - return getVersions(options.versionManifestUrl(), options.versionManifestPath(), forceDownload); - } - - private ManifestVersion getExperimentalVersions(boolean forceDownload) throws IOException { - return getVersions(options.experimentalVersionManifestUrl(), options.experimentalVersionManifestPath(), forceDownload); - } - - private ManifestVersion getVersions(String url, Path cacheFile, boolean forceDownload) throws IOException { - DownloadBuilder builder = download.apply(url); + private ManifestEntryLocation getManifestEntry(ManifestLocation location, boolean forceDownload) throws IOException { + DownloadBuilder builder = download.apply(location.url()); if (forceDownload) { builder = builder.forceDownload(); @@ -140,48 +133,77 @@ public final class MinecraftMetadataProvider { builder = builder.defaultCache(); } + final Path cacheFile = location.cacheFile(options.userCache()); final String versionManifest = builder.downloadString(cacheFile); - return LoomGradlePlugin.GSON.fromJson(versionManifest, ManifestVersion.class); + final VersionsManifest manifest = LoomGradlePlugin.GSON.fromJson(versionManifest, VersionsManifest.class); + final VersionsManifest.Version version = manifest.getVersion(options.minecraftVersion()); + + if (version != null) { + return new ManifestEntryLocation(location, version); + } + + return null; } private MinecraftVersionMeta readVersionMeta() throws IOException { - final DownloadBuilder builder = download.apply(versionEntry.url); + final DownloadBuilder builder = download.apply(versionEntry.entry.url); - if (versionEntry.sha1 != null) { - builder.sha1(versionEntry.sha1); + if (versionEntry.entry.sha1 != null) { + builder.sha1(versionEntry.entry.sha1); } else { builder.defaultCache(); } - final String json = builder.downloadString(options.minecraftMetadataPath()); + final String fileName = getVersionMetaFileName(); + final Path cacheFile = options.workingDir().resolve(fileName); + final String json = builder.downloadString(cacheFile); return LoomGradlePlugin.GSON.fromJson(json, MinecraftVersionMeta.class); } + private String getVersionMetaFileName() { + String base = "minecraft-info"; + + // custom version metadata + if (versionEntry.manifest == null) { + return base + Integer.toHexString(versionEntry.entry.url.hashCode()) + ".json"; + } + + // custom versions manifest + if (!versionEntry.manifest.isBuiltIn()) { + return base + Integer.toHexString(versionEntry.manifest.url().hashCode()) + ".json"; + } + + return base + ".json"; + } + public record Options(String minecraftVersion, - String versionManifestUrl, - String experimentalVersionManifestUrl, + ManifestLocations versionsManifests, @Nullable String customManifestUrl, - Path versionManifestPath, - Path experimentalVersionManifestPath, - Path minecraftMetadataPath) { - public static Options create(String minecraftVersion, Project project, Path minecraftMetadataPath) { + Path userCache, + Path workingDir) { + public static Options create(String minecraftVersion, Project project) { final LoomGradleExtension extension = LoomGradleExtension.get(project); final Path userCache = extension.getFiles().getUserCache().toPath(); + final Path workingDir = MinecraftProvider.minecraftWorkingDirectory(project, minecraftVersion).toPath(); + + final ManifestLocations manifestLocations = extension.getVersionsManifests(); + final Property customMetaUrl = extension.getCustomMinecraftMetadata(); return new Options( minecraftVersion, - MirrorUtil.getVersionManifests(project), - MirrorUtil.getExperimentalVersions(project), - extension.getCustomMinecraftManifest().getOrNull(), - userCache.resolve("version_manifest.json"), - userCache.resolve("experimental_version_manifest.json"), - minecraftMetadataPath + manifestLocations, + customMetaUrl.getOrNull(), + userCache, + workingDir ); } } @FunctionalInterface - private interface ManifestVersionSupplier { - ManifestVersion get() throws IOException; + private interface ManifestEntrySupplier { + ManifestEntryLocation get() throws IOException; + } + + private record ManifestEntryLocation(ManifestLocation manifest, VersionsManifest.Version entry) { } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestVersion.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java similarity index 91% rename from src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestVersion.java rename to src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java index 391683da..f4870d7c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/ManifestVersion.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/VersionsManifest.java @@ -29,13 +29,13 @@ import java.util.Map; import org.jetbrains.annotations.Nullable; -public record ManifestVersion(List versions, Map latest) { - public static class Versions { +public record VersionsManifest(List versions, Map latest) { + public static class Version { public String id, url, sha1; } @Nullable - public Versions getVersion(String id) { + public Version getVersion(String id) { return versions.stream() .filter(versions -> versions.id.equalsIgnoreCase(id)) .findFirst() diff --git a/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxConfiguration.java new file mode 100644 index 00000000..a07ee658 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxConfiguration.java @@ -0,0 +1,98 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.sandbox; + +import java.nio.file.Path; +import java.util.Objects; + +import javax.inject.Inject; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ExternalModuleDependency; +import org.gradle.api.artifacts.dsl.DependencyFactory; +import org.gradle.api.plugins.JavaPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.ide.RunConfigSettings; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.Platform; +import net.fabricmc.loom.util.gradle.GradleUtils; + +/** + * Allows the user to specify a sandbox maven artifact as a gradle property. + * The sandbox jar is read to figure out if it's supported on the current platform. + * If it is, its added to the runtime classpath and a new client run config is created + */ +public abstract class SandboxConfiguration implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(SandboxConfiguration.class); + + @Inject + protected abstract Project getProject(); + + @Inject + public abstract DependencyFactory getDependencyFactory(); + + @Override + public void run() { + if (getProject().findProperty(Constants.Properties.SANDBOX) == null) { + LOGGER.debug("No fabric sandbox property set"); + return; + } + + GradleUtils.afterSuccessfulEvaluation(getProject(), this::evaluate); + } + + private void evaluate() { + final String sandboxNotation = (String) Objects.requireNonNull(getProject().findProperty(Constants.Properties.SANDBOX)); + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final ExternalModuleDependency dependency = getDependencyFactory().create(sandboxNotation); + final Configuration configuration = getProject().getConfigurations().detachedConfiguration(dependency); + final Path sandboxJar = configuration.getSingleFile().toPath(); + final SandboxMetadata metadata = SandboxMetadata.readFromJar(sandboxJar); + + if (!metadata.supportsPlatform(Platform.CURRENT)) { + LOGGER.info("Sandbox does not support the current platform"); + return; + } + + getProject().getDependencies().add(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME, dependency); + + extension.getRuns().create("clientSandbox", settings -> { + RunConfigSettings clientRun = extension.getRuns().getByName("client"); + + settings.inherit(clientRun); + + settings.name("Client Sandbox"); + + // The sandbox also acts as DLI + // Set the sandbox as the true main class + settings.devLaunchMainClass().set(metadata.mainClass()); + settings.property("fabric.sandbox.realMain", clientRun.getMainClass().get()); + }); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxMetadata.java b/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxMetadata.java new file mode 100644 index 00000000..2c97e8f2 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/sandbox/SandboxMetadata.java @@ -0,0 +1,170 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.sandbox; + +import static net.fabricmc.loom.util.fmj.FabricModJsonUtils.ParseException; +import static net.fabricmc.loom.util.fmj.FabricModJsonUtils.getJsonObject; +import static net.fabricmc.loom.util.fmj.FabricModJsonUtils.readInt; +import static net.fabricmc.loom.util.fmj.FabricModJsonUtils.readString; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import net.fabricmc.loom.util.Platform; +import net.fabricmc.loom.util.ZipUtils; + +public sealed interface SandboxMetadata permits SandboxMetadata.V1 { + String SANDBOX_METADATA_FILENAME = "fabric-sandbox.json"; + + static SandboxMetadata readFromJar(Path path) { + try { + JsonObject jsonObject = ZipUtils.unpackGson(path, SANDBOX_METADATA_FILENAME, JsonObject.class); + int version = readInt(jsonObject, "version"); + return switch (version) { + case 1 -> SandboxMetadata.V1.parseV1(jsonObject); + default -> throw new UnsupportedOperationException("Unsupported sandbox metadata version: " + version); + }; + } catch (IOException e) { + throw new UncheckedIOException("Failed to read: " + SANDBOX_METADATA_FILENAME, e); + } + } + + /** + * @return The main class of the sandbox. + */ + String mainClass(); + + /** + * @param platform The platform to check. + * @return True if the sandbox supports the platform, false otherwise. + */ + boolean supportsPlatform(Platform platform); + + record V1(String mainClass, Map> supportedPlatforms) implements SandboxMetadata { + static V1 parseV1(JsonObject jsonObject) { + String mainClass = readString(jsonObject, "mainClass"); + JsonObject platforms = getJsonObject(jsonObject, "platforms"); + + Map> supportedPlatforms = new HashMap<>(); + + for (Map.Entry entry : platforms.entrySet()) { + if (!entry.getValue().isJsonArray()) { + throw new ParseException("Unexpected json array type for key (%s)", entry.getKey()); + } + + List architectures = new ArrayList<>(); + + for (JsonElement element : entry.getValue().getAsJsonArray()) { + if (!(element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())) { + throw new ParseException("Unexpected json primitive type for key (%s)", entry.getKey()); + } + + architectures.add(parseArchitecture(element.getAsString())); + } + + supportedPlatforms.put(parseOperatingSystem(entry.getKey()), Collections.unmodifiableList(architectures)); + } + + return new V1(mainClass, Collections.unmodifiableMap(supportedPlatforms)); + } + + @Override + public boolean supportsPlatform(Platform platform) { + for (Map.Entry> entry : supportedPlatforms.entrySet()) { + if (!entry.getKey().compatibleWith(platform)) { + continue; + } + + for (Architecture architecture : entry.getValue()) { + if (architecture.compatibleWith(platform)) { + return true; + } + } + } + + return false; + } + } + + enum OperatingSystem { + WINDOWS, + MAC_OS, + LINUX; + + public boolean compatibleWith(Platform platform) { + final Platform.OperatingSystem operatingSystem = platform.getOperatingSystem(); + + return switch (this) { + case WINDOWS -> operatingSystem.isWindows(); + case MAC_OS -> operatingSystem.isMacOS(); + case LINUX -> operatingSystem.isLinux(); + }; + } + } + + enum Architecture { + X86_64, + ARM64; + + public boolean compatibleWith(Platform platform) { + final Platform.Architecture architecture = platform.getArchitecture(); + + if (!architecture.is64Bit()) { + return false; + } + + return switch (this) { + case X86_64 -> !architecture.isArm(); + case ARM64 -> architecture.isArm(); + }; + } + } + + private static OperatingSystem parseOperatingSystem(String os) { + return switch (os) { + case "windows" -> OperatingSystem.WINDOWS; + case "macos" -> OperatingSystem.MAC_OS; + case "linux" -> OperatingSystem.LINUX; + default -> throw new ParseException("Unsupported sandbox operating system: %s", os); + }; + } + + private static Architecture parseArchitecture(String arch) { + return switch (arch) { + case "x86_64" -> Architecture.X86_64; + case "arm64" -> Architecture.ARM64; + default -> throw new ParseException("Unsupported sandbox architecture: %s", arch); + }; + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java index e89d035e..003726b6 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java @@ -49,11 +49,13 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b boolean hasSomeExisting = false; Path incompleteJar = Files.createTempFile("loom-cache-incomplete", ".jar"); - Path existingJar = Files.createTempFile("loom-cache-existing", ".jar"); + Path existingClassesJar = Files.createTempFile("loom-cache-existingClasses", ".jar"); + Path existingSourcesJar = Files.createTempFile("loom-cache-existingSources", ".jar"); // We must delete the empty files, so they can be created as a zip Files.delete(incompleteJar); - Files.delete(existingJar); + Files.delete(existingClassesJar); + Files.delete(existingSourcesJar); // Sources name -> hash Map outputNameMap = new HashMap<>(); @@ -64,12 +66,14 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b try (FileSystemUtil.Delegate inputFs = FileSystemUtil.getJarFileSystem(inputJar, false); FileSystemUtil.Delegate incompleteFs = FileSystemUtil.getJarFileSystem(incompleteJar, true); - FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(existingJar, true)) { + FileSystemUtil.Delegate existingSourcesFs = FileSystemUtil.getJarFileSystem(existingSourcesJar, true); + FileSystemUtil.Delegate existingClassesFs = FileSystemUtil.getJarFileSystem(existingClassesJar, true)) { final List inputClasses = JarWalker.findClasses(inputFs); + final Map rawEntryHashes = getEntryHashes(inputClasses, inputFs.getRoot()); for (ClassEntry entry : inputClasses) { String outputFileName = entry.sourcesFileName(); - String fullHash = baseHash + "/" + entry.hash(inputFs.getRoot()); + String fullHash = baseHash + "/" + entry.hashSuperHierarchy(rawEntryHashes); final CachedData entryData = fileStore.getEntry(fullHash); @@ -82,10 +86,12 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b LOGGER.debug("Cached entry ({}) not found, going to process {}", fullHash, outputFileName); misses++; } else { - final Path outputPath = existingFs.getPath(outputFileName); - Files.createDirectories(outputPath.getParent()); + final Path outputPath = existingSourcesFs.getPath(outputFileName); + createParentDirectories(outputPath); Files.writeString(outputPath, entryData.sources()); + entry.copyTo(inputFs.getRoot(), existingClassesFs.getRoot()); + if (entryData.lineNumbers() != null) { lineNumbersMap.put(entryData.className(), entryData.lineNumbers()); } else { @@ -110,7 +116,8 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b if (isIncomplete && !hasSomeExisting) { // The cache contained nothing of use, fully process the input jar Files.delete(incompleteJar); - Files.delete(existingJar); + Files.delete(existingClassesJar); + Files.delete(existingSourcesJar); LOGGER.info("No cached entries found, going to process the whole jar"); return new FullWorkJob(inputJar, outputJar, outputNameMap) @@ -118,17 +125,33 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b } else if (isIncomplete) { // The cache did not contain everything so we have some work to do LOGGER.info("Some cached entries found, using partial work job"); - return new PartialWorkJob(incompleteJar, existingJar, outputJar, outputNameMap) + return new PartialWorkJob(incompleteJar, existingSourcesJar, existingClassesJar, outputJar, outputNameMap) .asRequest(stats, lineNumbers); } else { // The cached contained everything we need, so the existing jar is the output LOGGER.info("All cached entries found, using completed work job"); Files.delete(incompleteJar); - return new CompletedWorkJob(existingJar) + Files.delete(existingClassesJar); + return new CompletedWorkJob(existingSourcesJar) .asRequest(stats, lineNumbers); } } + private static Map getEntryHashes(List entries, Path root) throws IOException { + final Map rawEntryHashes = new HashMap<>(); + + for (ClassEntry entry : entries) { + String hash = entry.hash(root); + rawEntryHashes.put(entry.name(), hash); + + for (String s : entry.innerClasses()) { + rawEntryHashes.put(s, hash); + } + } + + return Collections.unmodifiableMap(rawEntryHashes); + } + public void completeJob(Path output, WorkJob workJob, ClassLineNumbers lineNumbers) throws IOException { if (workJob instanceof CompletedWorkJob completedWorkJob) { // Fully complete, nothing new to cache @@ -189,7 +212,7 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b if (workJob instanceof PartialWorkJob partialWorkJob) { // Copy all the existing items to the output jar try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(partialWorkJob.output(), false); - FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(partialWorkJob.existing(), false); + FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(partialWorkJob.existingSources(), false); Stream walk = Files.walk(existingFs.getRoot())) { Iterator iterator = walk.iterator(); @@ -203,12 +226,13 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b final Path outputPath = outputFs.getRoot().resolve(existingPath.toString()); LOGGER.debug("Copying existing entry to output: {}", existingPath); - Files.createDirectories(outputPath.getParent()); + createParentDirectories(outputPath); Files.copy(existingPath, outputPath); } } - Files.delete(partialWorkJob.existing()); + Files.delete(partialWorkJob.existingSources()); + Files.delete(partialWorkJob.existingClasses()); Files.move(partialWorkJob.output(), output); } else if (workJob instanceof FullWorkJob fullWorkJob) { // Nothing to merge, just use the output jar @@ -259,11 +283,12 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b * Some work needs to be done. * * @param incomplete A path to jar file containing all the classes to be processed - * @param existing A path pointing to a jar containing existing classes that have previously been processed + * @param existingSources A path pointing to a jar containing existing sources that have previously been processed + * @param existingClasses A path pointing to a jar containing existing classes that have previously been processed * @param output A path to a temporary jar where work output should be written to * @param outputNameMap A map of sources name to hash */ - public record PartialWorkJob(Path incomplete, Path existing, Path output, Map outputNameMap) implements WorkToDoJob { + public record PartialWorkJob(Path incomplete, Path existingSources, Path existingClasses, Path output, Map outputNameMap) implements WorkToDoJob { } /** @@ -275,4 +300,14 @@ public record CachedJarProcessor(CachedFileStore fileStore, String b */ public record FullWorkJob(Path incomplete, Path output, Map outputNameMap) implements WorkToDoJob { } + + private static void createParentDirectories(Path path) throws IOException { + final Path parent = path.getParent(); + + if (parent == null) { + return; + } + + Files.createDirectories(parent); + } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java index f01db22f..666ae2c2 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java @@ -28,11 +28,23 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.StringJoiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.fabricmc.loom.util.Checksum; -public record ClassEntry(String parentClass, List innerClasses) { +/** + * @param name The class name + * @param innerClasses A list of inner class names + * @param superClasses A list of parent classes (super and interface) from the class and all inner classes + */ +public record ClassEntry(String name, List innerClasses, List superClasses) { + private static final Logger LOGGER = LoggerFactory.getLogger(ClassEntry.class); + /** * Copy the class and its inner classes to the target root. * @param sourceRoot The root of the source jar @@ -41,9 +53,9 @@ public record ClassEntry(String parentClass, List innerClasses) { * @throws IOException If an error occurs while copying the files */ public void copyTo(Path sourceRoot, Path targetRoot) throws IOException { - Path targetPath = targetRoot.resolve(parentClass); + Path targetPath = targetRoot.resolve(name); Files.createDirectories(targetPath.getParent()); - Files.copy(sourceRoot.resolve(parentClass), targetPath); + Files.copy(sourceRoot.resolve(name), targetPath); for (String innerClass : innerClasses) { Files.copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass)); @@ -60,7 +72,7 @@ public record ClassEntry(String parentClass, List innerClasses) { public String hash(Path root) throws IOException { StringJoiner joiner = new StringJoiner(","); - joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(parentClass)))); + joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(name)))); for (String innerClass : innerClasses) { joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(innerClass)))); @@ -69,7 +81,34 @@ public record ClassEntry(String parentClass, List innerClasses) { return Checksum.sha256Hex(joiner.toString().getBytes()); } + /** + * Return a hash of the class and its super classes. + */ + public String hashSuperHierarchy(Map hashes) throws IOException { + final String selfHash = Objects.requireNonNull(hashes.get(name), "Hash for own class not found"); + + if (superClasses.isEmpty()) { + return selfHash; + } + + StringJoiner joiner = new StringJoiner(","); + joiner.add(selfHash); + + for (String superClass : superClasses) { + final String superHash = hashes.get(superClass + ".class"); + + if (superHash != null) { + joiner.add(superHash); + } else if (!superClass.startsWith("java/")) { + // This will happen if the super class is not part of the input jar + LOGGER.debug("Hash for super class {} of {} not found", superClass, name); + } + } + + return Checksum.sha256Hex(joiner.toString().getBytes()); + } + public String sourcesFileName() { - return parentClass.replace(".class", ".java"); + return name.replace(".class", ".java"); } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java index ab2f9924..4c2c35ca 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java @@ -25,6 +25,10 @@ package net.fabricmc.loom.decompilers.cache; import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -33,11 +37,22 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Stream; +import org.gradle.api.JavaVersion; +import org.objectweb.asm.ClassReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.fabricmc.loom.util.CompletableFutureCollector; import net.fabricmc.loom.util.FileSystemUtil; public final class JarWalker { @@ -88,7 +103,8 @@ public final class JarWalker { Collections.sort(outerClasses); - List classEntries = new ArrayList<>(); + final Executor executor = getExecutor(); + List> classEntries = new ArrayList<>(); for (String outerClass : outerClasses) { List innerClasList = innerClasses.get(outerClass); @@ -99,10 +115,71 @@ public final class JarWalker { Collections.sort(innerClasList); } - ClassEntry classEntry = new ClassEntry(outerClass, Collections.unmodifiableList(innerClasList)); - classEntries.add(classEntry); + classEntries.add(getClassEntry(outerClass, innerClasList, fs, executor)); } - return Collections.unmodifiableList(classEntries); + try { + return classEntries.stream() + .collect(CompletableFutureCollector.allOf()) + .get(10, TimeUnit.MINUTES); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new RuntimeException("Failed to get class entries", e); + } + } + + private static CompletableFuture getClassEntry(String outerClass, List innerClasses, FileSystemUtil.Delegate fs, Executor executor) { + List>> parentClassesFutures = new ArrayList<>(); + + // Get the super classes of the outer class and any inner classes + parentClassesFutures.add(CompletableFuture.supplyAsync(() -> getSuperClasses(outerClass, fs), executor)); + + for (String innerClass : innerClasses) { + parentClassesFutures.add(CompletableFuture.supplyAsync(() -> getSuperClasses(innerClass, fs), executor)); + } + + return parentClassesFutures.stream() + .collect(CompletableFutureCollector.allOf()) + .thenApply(lists -> lists.stream() + .flatMap(List::stream) + .filter(JarWalker::isNotReservedClass) + .distinct() + .toList()) + .thenApply(parentClasses -> new ClassEntry(outerClass, innerClasses, parentClasses)); + } + + private static List getSuperClasses(String classFile, FileSystemUtil.Delegate fs) { + try (InputStream is = Files.newInputStream(fs.getPath(classFile))) { + final ClassReader reader = new ClassReader(is); + + List parentClasses = new ArrayList<>(); + String superName = reader.getSuperName(); + + if (superName != null) { + parentClasses.add(superName); + } + + Collections.addAll(parentClasses, reader.getInterfaces()); + return Collections.unmodifiableList(parentClasses); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read class file: " + classFile, e); + } + } + + private static Executor getExecutor() { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + try { + Method m = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); + return (ExecutorService) m.invoke(null); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to create virtual thread executor", e); + } + } + + return ForkJoinPool.commonPool(); + } + + // Slight optimization, if we skip over Object + private static boolean isNotReservedClass(String name) { + return !"java/lang/Object".equals(name); } } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index c7b5b4e5..46b03dbd 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -76,10 +76,12 @@ import net.fabricmc.loom.configuration.processors.JarProcessor; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingSpec; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingSpecBuilderImpl; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; +import net.fabricmc.loom.configuration.providers.minecraft.ManifestLocations; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.task.GenerateSourcesTask; import net.fabricmc.loom.util.DeprecationHelper; +import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.ModPlatform; import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; @@ -97,7 +99,8 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final ListProperty jarProcessors; protected final ConfigurableFileCollection log4jConfigs; protected final RegularFileProperty accessWidener; - protected final Property customManifest; + protected final ManifestLocations versionsManifests; + protected final Property customMetadata; protected final SetProperty knownIndyBsms; protected final Property transitiveAccessWideners; protected final Property modProvidedJavadoc; @@ -137,7 +140,10 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA .empty(); this.log4jConfigs = project.files(directories.getDefaultLog4jConfigFile()); this.accessWidener = project.getObjects().fileProperty(); - this.customManifest = project.getObjects().property(String.class); + this.versionsManifests = new ManifestLocations("versions_manifest"); + this.versionsManifests.addBuiltIn(-2, MirrorUtil.getVersionManifests(project), "versions_manifest"); + this.versionsManifests.addBuiltIn(-1, MirrorUtil.getExperimentalVersions(project), "experimental_versions_manifest"); + this.customMetadata = project.getObjects().property(String.class); this.knownIndyBsms = project.getObjects().setProperty(String.class).convention(Set.of( "java/lang/invoke/StringConcatFactory", "java/lang/runtime/ObjectMethods", @@ -301,8 +307,13 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA } @Override - public Property getCustomMinecraftManifest() { - return customManifest; + public ManifestLocations getVersionsManifests() { + return versionsManifests; + } + + @Override + public Property getCustomMinecraftMetadata() { + return customMetadata; } @Override diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 2de2d67a..9a1f79b1 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -160,6 +160,11 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { @ApiStatus.Experimental public abstract Property getUseCache(); + @Input + @Option(option = "reset-cache", description = "When set the cache will be reset") + @ApiStatus.Experimental + public abstract Property getResetCache(); + // Internal outputs @ApiStatus.Internal @Internal @@ -192,6 +197,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); getUseCache().convention(true); + getResetCache().convention(extension.refreshDeps()); } @TaskAction @@ -218,6 +224,11 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { try (var timer = new Timer("Decompiled sources with cache")) { final Path cacheFile = getDecompileCacheFile().getAsFile().get().toPath(); + if (getResetCache().get()) { + LOGGER.warn("Resetting decompile cache"); + Files.deleteIfExists(cacheFile); + } + // TODO ensure we have a lock on this file to prevent multiple tasks from running at the same time // TODO handle being unable to read the cache file Files.createDirectories(cacheFile.getParent()); @@ -254,16 +265,16 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { if (job instanceof CachedJarProcessor.WorkToDoJob workToDoJob) { Path inputJar = workToDoJob.incomplete(); - @Nullable Path existing = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existing() : null; + @Nullable Path existingClasses = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existingClasses() : null; if (getUnpickDefinitions().isPresent()) { try (var timer = new Timer("Unpick")) { - inputJar = unpickJar(inputJar, existing); + inputJar = unpickJar(inputJar, existingClasses); } } try (var timer = new Timer("Decompile")) { - outputLineNumbers = runDecompileJob(inputJar, workToDoJob.output(), existing); + outputLineNumbers = runDecompileJob(inputJar, workToDoJob.output(), existingClasses); removeForgeInnerClassSources(workToDoJob.output()); outputLineNumbers = filterForgeLineNumbers(outputLineNumbers); } @@ -283,6 +294,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { cachedJarProcessor.completeJob(sourcesJar, job, outputLineNumbers); } + LOGGER.info("Decompiled sources written to {}", sourcesJar); + // This is the minecraft jar used at runtime. final Path classesJar = minecraftJar.getPath(); @@ -334,6 +347,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { throw new RuntimeException("Failed to decompile sources"); } + LOGGER.info("Decompiled sources written to {}", sourcesJar); + if (lineNumbers == null) { LOGGER.info("No line numbers to remap, skipping remapping"); return; @@ -514,9 +529,9 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { ); } - private Path unpickJar(Path inputJar, @Nullable Path existingJar) { + private Path unpickJar(Path inputJar, @Nullable Path existingClasses) { final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath(); - final List args = getUnpickArgs(inputJar, outputJar, existingJar); + final List args = getUnpickArgs(inputJar, outputJar, existingClasses); ExecResult result = getExecOperations().javaexec(spec -> { spec.getMainClass().set("daomephsta.unpick.cli.Main"); @@ -530,7 +545,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return outputJar; } - private List getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingJar) { + private List getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingClasses) { var fileArgs = new ArrayList(); fileArgs.add(inputJar.toFile()); @@ -547,8 +562,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { fileArgs.add(file); } - if (existingJar != null) { - fileArgs.add(existingJar.toFile()); + if (existingClasses != null) { + fileArgs.add(existingClasses.toFile()); } return fileArgs.stream() @@ -583,15 +598,15 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { LOGGER.info("Wrote linemap to {}", lineMap); } - private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingJar) { + private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingClasses) { final String jvmMarkerValue = UUID.randomUUID().toString(); final WorkQueue workQueue = createWorkQueue(jvmMarkerValue); ConfigurableFileCollection classpath = getProject().files(); classpath.from(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES)); - if (existingJar != null) { - classpath.from(existingJar); + if (existingClasses != null) { + classpath.from(existingClasses); } workQueue.submit(DecompileAction.class, params -> { @@ -619,7 +634,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { boolean stopped = WorkerDaemonClientsManagerHelper.stopIdleJVM(getWorkerDaemonClientsManager(), jvmMarkerValue); if (!stopped && ipcServer.hasReceivedMessage()) { - throw new RuntimeException("Failed to stop decompile worker JVM"); + LOGGER.info("Failed to stop decompile worker JVM, it may have already been stopped?"); } } } diff --git a/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java b/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java index 9bbccff3..5e41a8af 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java +++ b/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java @@ -206,9 +206,9 @@ public abstract class RemapTaskConfiguration implements Runnable { getArtifacts().add(JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME, remapSourcesTask.map(AbstractArchiveTask::getArchiveFile), artifact -> { artifact.setClassifier("sources"); }); - } else { + } else if (canRemap) { // Sources jar may not have been created with withSourcesJar - getProject().getLogger().warn("Not publishing sources jar as it was not found. Use java.withSourcesJar() to fix."); + getProject().getLogger().warn("Not publishing sources jar as it was not created by the java plugin. Use java.withSourcesJar() to fix."); } }); } diff --git a/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java b/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java index 3cd2c0e8..7a42a50e 100644 --- a/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java +++ b/src/main/java/net/fabricmc/loom/task/service/JarManifestService.java @@ -103,7 +103,8 @@ public abstract class JarManifestService implements BuildService getMixinVersion(Project project) { return project.getConfigurations().named(Constants.Configurations.LOADER_DEPENDENCIES).map(configuration -> { diff --git a/src/main/java/net/fabricmc/loom/util/CompletableFutureCollector.java b/src/main/java/net/fabricmc/loom/util/CompletableFutureCollector.java new file mode 100644 index 00000000..77ccb20f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/CompletableFutureCollector.java @@ -0,0 +1,77 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +public final class CompletableFutureCollector> implements Collector, CompletableFuture>> { + private CompletableFutureCollector() { + } + + public static > Collector, CompletableFuture>> allOf() { + return new CompletableFutureCollector<>(); + } + + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public BiConsumer, T> accumulator() { + return List::add; + } + + @Override + public BinaryOperator> combiner() { + return (left, right) -> { + left.addAll(right); + return left; + }; + } + + @Override + public Function, CompletableFuture>> finisher() { + return ls -> CompletableFuture.allOf(ls.toArray(CompletableFuture[]::new)) + .thenApply(v -> ls + .stream() + .map(CompletableFuture::join) + .toList()); + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index a26c457d..4305f725 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -24,6 +24,7 @@ package net.fabricmc.loom.util; +import org.jetbrains.annotations.ApiStatus; import org.objectweb.asm.Opcodes; public class Constants { @@ -159,6 +160,8 @@ public class Constants { public static final String DISABLE_REMAPPED_VARIANTS = "fabric.loom.disableRemappedVariants"; public static final String DISABLE_PROJECT_DEPENDENT_MODS = "fabric.loom.disableProjectDependentMods"; public static final String LIBRARY_PROCESSORS = "fabric.loom.libraryProcessors"; + @ApiStatus.Experimental + public static final String SANDBOX = "fabric.loom.experimental.sandbox"; public static final String ALLOW_MISMATCHED_PLATFORM_VERSION = "loom.allowMismatchedPlatformVersion"; } diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java index 5559551b..d1c972ab 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java @@ -50,6 +50,16 @@ public final class FabricModJsonUtils { return element.getAsInt(); } + public static JsonObject getJsonObject(JsonObject jsonObject, String key) { + final JsonElement element = getElement(jsonObject, key); + + if (!element.isJsonObject()) { + throw new ParseException("Unexpected json object type for key (%s)", key); + } + + return element.getAsJsonObject(); + } + // Ensure that the schemaVersion json entry, is first in the json file // This exercises an optimisation here: https://github.com/FabricMC/fabric-loader/blob/d69cb72d26497e3f387cf46f9b24340b402a4644/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java#L62 public static JsonObject optimizeFmj(JsonObject json) { @@ -90,8 +100,8 @@ public final class FabricModJsonUtils { } } - static class ParseException extends RuntimeException { - ParseException(String message, Object... args) { + public static class ParseException extends RuntimeException { + public ParseException(String message, Object... args) { super(String.format(Locale.ROOT, message, args)); } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy index 8b900f30..a6af3e2d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy @@ -54,8 +54,10 @@ class ConfigurationCacheTest extends Specification implements GradleProjectTestT result2.task(":${task}").outcome != FAILED where: - task | _ - "help" | _ - "configureClientLaunch" | _ + task | _ + "help" | _ + "configureClientLaunch" | _ + "jar" | _ + "check" | _ } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy index 2e5f32dc..086778ad 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy @@ -50,6 +50,26 @@ class MojangMappingsProjectTest extends Specification implements GradleProjectTe version << STANDARD_TEST_VERSIONS } + @Unroll + def "build no intermediary (gradle #version)"() { + setup: + def gradle = gradleProject(project: "mojangMappings", version: version) + gradle.buildGradle << ''' + loom { + noIntermediateMappings() + } + ''' + + when: + def result = gradle.run(task: "build") + + then: + result.task(":build").outcome == SUCCESS + + where: + version << STANDARD_TEST_VERSIONS + } + @Unroll def "mojang mappings without synthetic field names (gradle #version)"() { setup: diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/SandboxTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/SandboxTest.groovy new file mode 100644 index 00000000..b7b4ea9f --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/SandboxTest.groovy @@ -0,0 +1,112 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.integration + +import java.nio.file.Path + +import org.intellij.lang.annotations.Language +import spock.lang.Specification +import spock.lang.Unroll + +import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper +import net.fabricmc.loom.test.unit.sandbox.SandboxEntrypoint +import net.fabricmc.loom.test.util.GradleProjectTestTrait +import net.fabricmc.loom.test.util.ZipTestUtils +import net.fabricmc.loom.util.Constants +import net.fabricmc.loom.util.ZipUtils + +import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS +import static org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class SandboxTest extends Specification implements GradleProjectTestTrait { + @Unroll + def "sandbox (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + repositories { + mavenLocal() + } + + dependencies { + minecraft "com.mojang:minecraft:1.20.4" + mappings "net.fabricmc:yarn:1.20.4+build.3:v2" + modImplementation "net.fabricmc:fabric-loader:0.15.10" + } + ''' + new File(gradle.getProjectDir(), "gradle.properties").text = "${Constants.Properties.SANDBOX}=net.fabricmc.loom.test:sandbox:1.0.0" + + def mavenHelper = new LocalMavenHelper("net.fabricmc.loom.test", "sandbox", "1.0.0", null, gradle.getMavenLocalDir().toPath()) + mavenHelper.copyToMaven(createDummySandboxJar(), null) + mavenHelper.savePom() + + when: + def result = gradle.run(task: "runClientSandbox") + + then: + result.task(":runClientSandbox").outcome == SUCCESS + result.output.contains("Running real main: net.fabricmc.loader.impl.launch.knot.KnotClient") + // Ensure that we weren't launched via DLI + !result.output.contains("at net.fabricmc.devlaunchinjector.Main") + + where: + version << STANDARD_TEST_VERSIONS + } + + static Path createDummySandboxJar() { + def zip = ZipTestUtils.createZip(["fabric-sandbox.json": METADATA_JSON], ".jar") + ZipUtils.add(zip, "net/fabricmc/loom/test/unit/sandbox/SandboxEntrypoint.class", getClassBytes(SandboxEntrypoint.class)) + return zip + } + + static byte[] getClassBytes(Class clazz) { + return clazz.classLoader.getResourceAsStream(clazz.name.replace('.', '/') + ".class").withCloseable { + it.bytes + } + } + + // Ensure that all platforms that the test may run on are listed here + @Language("json") + private static String METADATA_JSON = """ + { + "version": 1, + "mainClass": "net.fabricmc.loom.test.unit.sandbox.SandboxEntrypoint", + "platforms": { + "windows": [ + "arm64", + "x86_64" + ], + "macos": [ + "arm64", + "x86_64" + ], + "linux": [ + "arm64", + "x86_64" + ] + } + } + """ +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy deleted file mode 100644 index 7f0d0c57..00000000 --- a/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2024 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.decompilers.cache.JarWalker -import net.fabricmc.loom.test.util.ZipTestUtils -import net.fabricmc.loom.util.FileSystemUtil - -class JarWalkerTest extends Specification { - def "find classes in jar"() { - given: - def jar = ZipTestUtils.createZip([ - "net/fabricmc/Test.class": "", - "net/fabricmc/other/Test.class": "", - "net/fabricmc/other/Test\$Inner.class": "", - "net/fabricmc/other/Test\$1.class": "", - ]) - when: - def entries = JarWalker.findClasses(jar) - then: - entries.size() == 2 - - entries[0].parentClass() == "net/fabricmc/Test.class" - entries[0].sourcesFileName() == "net/fabricmc/Test.java" - entries[0].innerClasses().size() == 0 - - entries[1].parentClass() == "net/fabricmc/other/Test.class" - entries[1].sourcesFileName() == "net/fabricmc/other/Test.java" - entries[1].innerClasses().size() == 2 - entries[1].innerClasses()[0] == "net/fabricmc/other/Test\$1.class" - entries[1].innerClasses()[1] == "net/fabricmc/other/Test\$Inner.class" - } - - def "Hash Classes"() { - given: - def jar = ZipTestUtils.createZip(zipEntries) - when: - def entries = JarWalker.findClasses(jar) - def hash = FileSystemUtil.getJarFileSystem(jar).withCloseable { fs -> - return entries[0].hash(fs.root) - } - then: - entries.size() == 1 - hash == expectedHash - where: - expectedHash | zipEntries - "2339de144d8a4a1198adf8142b6d3421ec0baacea13c9ade42a93071b6d62e43" | [ - "net/fabricmc/Test.class": "abc123", - ] - "1053cfadf4e371ec89ff5b58d9b3bdb80373f3179e804b2e241171223709f4d1" | [ - "net/fabricmc/other/Test.class": "Hello", - "net/fabricmc/other/Test\$Inner.class": "World", - "net/fabricmc/other/Test\$Inner\$2.class": "123", - "net/fabricmc/other/Test\$1.class": "test", - ] - "f30b705f3a921b60103a4ee9951aff59b6db87cc289ba24563743d753acff433" | [ - "net/fabricmc/other/Test.class": "Hello", - "net/fabricmc/other/Test\$Inner.class": "World", - "net/fabricmc/other/Test\$Inner\$2.class": "abc123", - "net/fabricmc/other/Test\$1.class": "test", - ] - } -} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/SandboxMetadataTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/SandboxMetadataTest.groovy new file mode 100644 index 00000000..2752e6ca --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/SandboxMetadataTest.groovy @@ -0,0 +1,74 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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 java.nio.file.Path + +import org.intellij.lang.annotations.Language +import spock.lang.Specification + +import net.fabricmc.loom.configuration.sandbox.SandboxMetadata +import net.fabricmc.loom.test.util.PlatformTestUtils +import net.fabricmc.loom.test.util.ZipTestUtils + +class SandboxMetadataTest extends Specification { + def "test sandbox metadata"() { + given: + def sandboxJar = createSandboxJar(""" + { + "version": 1, + "mainClass": "net.fabricmc.loom.test.Main", + "platforms": { + "windows": [ + "arm64", + "x86_64" + ], + "macos": [ + "arm64" + ] + } + } + """) + + when: + def metadata = SandboxMetadata.readFromJar(sandboxJar) + + then: + metadata.mainClass() == "net.fabricmc.loom.test.Main" + + metadata.supportsPlatform(PlatformTestUtils.WINDOWS_X64) + metadata.supportsPlatform(PlatformTestUtils.WINDOWS_ARM64) + + !metadata.supportsPlatform(PlatformTestUtils.LINUX_X64) + !metadata.supportsPlatform(PlatformTestUtils.LINUX_ARM64) + + !metadata.supportsPlatform(PlatformTestUtils.MAC_OS_X64) + metadata.supportsPlatform(PlatformTestUtils.MAC_OS_ARM64) + } + + private static Path createSandboxJar(@Language("json") String json) { + return ZipTestUtils.createZip(["fabric-sandbox.json": json]) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy index 6b157603..9e2e8856 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy @@ -25,33 +25,42 @@ package net.fabricmc.loom.test.unit.cache import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes import spock.lang.Specification +import spock.lang.TempDir import net.fabricmc.loom.decompilers.ClassLineNumbers import net.fabricmc.loom.decompilers.cache.CachedData import net.fabricmc.loom.decompilers.cache.CachedFileStore +import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl import net.fabricmc.loom.decompilers.cache.CachedJarProcessor import net.fabricmc.loom.test.util.ZipTestUtils import net.fabricmc.loom.util.ZipUtils class CachedJarProcessorTest extends Specification { - static Map jarEntries = [ - "net/fabricmc/Example.class": "", - "net/fabricmc/other/Test.class": "", - "net/fabricmc/other/Test\$Inner.class": "", - "net/fabricmc/other/Test\$1.class": "", + static Map jarEntries = [ + "net/fabricmc/Example.class": newClass("net/fabricmc/Example"), + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test"), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner"), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), ] - static String ExampleHash = "abc123/cd372fb85148700fa88095e3492d3f9f5beb43e555e5ff26d95f5a6adc36f8e6" - static String TestHash = "abc123/ecd40b16ec50b636a390cb8da716a22606965f14e526e3051144dd567f336bc5" + static String ExampleHash = "abc123/db5c3a2d04e0c6ea03aef0d217517aa0233f9b8198753d3c96574fe5825a13c4" + static String TestHash = "abc123/06f9f4c7dbca9baa037fbea007298ee15277d97de594bbf6e4a1ee346c079e65" static CachedData ExampleCachedData = new CachedData("net/fabricmc/Example", "Example sources", lineNumber("net/fabricmc/Example")) static CachedData TestCachedData = new CachedData("net/fabricmc/other/Test", "Test sources", lineNumber("net/fabricmc/other/Test")) + @TempDir + Path testPath + def "prepare full work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -71,7 +80,7 @@ class CachedJarProcessorTest extends Specification { def "prepare partial work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -85,7 +94,8 @@ class CachedJarProcessorTest extends Specification { lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers() workJob.outputNameMap().size() == 1 - ZipUtils.unpackNullable(workJob.existing(), "net/fabricmc/Example.java") == "Example sources".bytes + ZipUtils.unpackNullable(workJob.existingSources(), "net/fabricmc/Example.java") == "Example sources".bytes + ZipUtils.unpackNullable(workJob.existingClasses(), "net/fabricmc/Example.class") == newClass("net/fabricmc/Example") // Provide one cached entry // And then one call not finding the entry in the cache @@ -97,7 +107,7 @@ class CachedJarProcessorTest extends Specification { def "prepare completed work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -125,7 +135,7 @@ class CachedJarProcessorTest extends Specification { def "complete full work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -152,7 +162,7 @@ class CachedJarProcessorTest extends Specification { ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes - // Expect two calls looking for the existing entry in the cache + // Expect two calls looking for the existingSources entry in the cache 1 * cache.getEntry(ExampleHash) >> null 1 * cache.getEntry(TestHash) >> null @@ -165,7 +175,7 @@ class CachedJarProcessorTest extends Specification { def "complete partial work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -203,7 +213,7 @@ class CachedJarProcessorTest extends Specification { def "complete completed work job"() { given: - def jar = ZipTestUtils.createZip(jarEntries) + def jar = ZipTestUtils.createZipFromBytes(jarEntries) def cache = Mock(CachedFileStore) def processor = new CachedJarProcessor(cache, "abc123") @@ -231,6 +241,55 @@ class CachedJarProcessorTest extends Specification { 0 * _ // Strict mock } + def "hierarchy change invalidates cache"() { + given: + def jar1 = ZipTestUtils.createZipFromBytes( + [ + "net/fabricmc/Example.class": newClass("net/fabricmc/Example"), + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test", ), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner", ["net/fabricmc/Example"] as String[]), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), + ] + ) + // The second jar changes Example, so we would expect Test to be invalidated, thus causing a full decompile in this case + def jar2 = ZipTestUtils.createZipFromBytes( + [ + "net/fabricmc/Example.class": newClass("net/fabricmc/Example", ["java/lang/Runnable"] as String[]), + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test", ), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner", ["net/fabricmc/Example"] as String[]), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), + ] + ) + + def cache = new CachedFileStoreImpl<>(testPath.resolve("cache"), CachedData.SERIALIZER, new CachedFileStoreImpl.CacheRules(50_000, Duration.ofDays(90))) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar1) + def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob + def outputSourcesJar = workJob.output() + + // Do the work, such as decompiling. + ZipUtils.add(outputSourcesJar, "net/fabricmc/Example.java", "Example sources") + ZipUtils.add(outputSourcesJar, "net/fabricmc/other/Test.java", "Test sources") + + // Complete the job + def outputJar = Files.createTempFile("loom-test-output", ".jar") + Files.delete(outputJar) + + ClassLineNumbers lineNumbers = lineNumbers([ + "net/fabricmc/Example", + "net/fabricmc/other/Test" + ]) + processor.completeJob(outputJar, workJob, lineNumbers) + + workRequest = processor.prepareJob(jar2) + def newWorkJob = workRequest.job() + + then: + newWorkJob instanceof CachedJarProcessor.FullWorkJob + } + private static ClassLineNumbers lineNumbers(List names) { return new ClassLineNumbers(names.collectEntries { [it, lineNumber(it)] }) } @@ -238,4 +297,10 @@ class CachedJarProcessorTest extends Specification { private static ClassLineNumbers.Entry lineNumber(String name) { return new ClassLineNumbers.Entry(name, 0, 0, [:]) } + + private static byte[] newClass(String name, String[] interfaces = null, String superName = "java/lang/Object") { + def writer = new ClassWriter(0) + writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, name, null, superName, interfaces) + return writer.toByteArray() + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy new file mode 100644 index 00000000..3490738c --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy @@ -0,0 +1,154 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.cache + +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import spock.lang.Specification + +import net.fabricmc.loom.decompilers.cache.JarWalker +import net.fabricmc.loom.test.util.ZipTestUtils +import net.fabricmc.loom.util.FileSystemUtil + +class JarWalkerTest extends Specification { + def "find classes in jar"() { + given: + def jar = ZipTestUtils.createZipFromBytes([ + "net/fabricmc/Test.class": newClass("net/fabricmc/Test"), + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test"), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner"), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), + ]) + when: + def entries = JarWalker.findClasses(jar) + then: + entries.size() == 2 + + entries[0].name() == "net/fabricmc/Test.class" + entries[0].sourcesFileName() == "net/fabricmc/Test.java" + entries[0].innerClasses().size() == 0 + + entries[1].name() == "net/fabricmc/other/Test.class" + entries[1].sourcesFileName() == "net/fabricmc/other/Test.java" + entries[1].innerClasses().size() == 2 + entries[1].innerClasses()[0] == "net/fabricmc/other/Test\$1.class" + entries[1].innerClasses()[1] == "net/fabricmc/other/Test\$Inner.class" + } + + def "Hash Classes"() { + given: + def jar = ZipTestUtils.createZipFromBytes(zipEntries) + when: + def entries = JarWalker.findClasses(jar) + def hash = FileSystemUtil.getJarFileSystem(jar).withCloseable { fs -> + return entries[0].hash(fs.root) + } + then: + entries.size() == 1 + hash == expectedHash + where: + expectedHash | zipEntries + "b055df8d9503b60050f6d0db387c84c47fedb4d9ed82c4f8174b4e465a9c479b" | [ + "net/fabricmc/Test.class": newClass("net/fabricmc/Test"), + ] + "3ba069bc20db1ee1b4bb69450dba3fd57a91059bd85e788d5af712aee3191792" | [ + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test"), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner"), + "net/fabricmc/other/Test\$Inner\$2.class": newClass("net/fabricmc/other/Test\$Inner\$2"), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), + ] + "3ba069bc20db1ee1b4bb69450dba3fd57a91059bd85e788d5af712aee3191792" | [ + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test"), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner"), + "net/fabricmc/other/Test\$Inner\$2.class": newClass("net/fabricmc/other/Test\$Inner\$2"), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1"), + ] + } + + def "simple class"() { + given: + def jarEntries = [ + "net/fabricmc/Example.class": newClass("net/fabricmc/Example") + ] + def jar = ZipTestUtils.createZipFromBytes(jarEntries) + + when: + def classes = JarWalker.findClasses(jar) + + then: + classes.size() == 1 + classes[0].name() == "net/fabricmc/Example.class" + classes[0].innerClasses() == [] + classes[0].superClasses() == [] + } + + def "class with interfaces"() { + given: + def jarEntries = [ + "net/fabricmc/Example.class": newClass("net/fabricmc/Example", ["java/lang/Runnable"] as String[]) + ] + def jar = ZipTestUtils.createZipFromBytes(jarEntries) + + when: + def classes = JarWalker.findClasses(jar) + + then: + classes.size() == 1 + classes[0].name() == "net/fabricmc/Example.class" + classes[0].innerClasses() == [] + classes[0].superClasses() == ["java/lang/Runnable"] + } + + def "inner classes"() { + given: + def jarEntries = [ + "net/fabricmc/other/Test.class": newClass("net/fabricmc/other/Test"), + "net/fabricmc/other/Test\$Inner.class": newClass("net/fabricmc/other/Test\$Inner", null, "net/fabricmc/other/Super"), + "net/fabricmc/other/Test\$1.class": newClass("net/fabricmc/other/Test\$1", ["java/lang/Runnable"] as String[]), + ] + def jar = ZipTestUtils.createZipFromBytes(jarEntries) + + when: + def classes = JarWalker.findClasses(jar) + + then: + classes.size() == 1 + classes[0].name() == "net/fabricmc/other/Test.class" + classes[0].innerClasses() == [ + "net/fabricmc/other/Test\$1.class", + "net/fabricmc/other/Test\$Inner.class" + ] + classes[0].superClasses() == [ + "java/lang/Runnable", + "net/fabricmc/other/Super" + ] + } + + private static byte[] newClass(String name, String[] interfaces = null, String superName = "java/lang/Object") { + def writer = new ClassWriter(0) + writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, name, null, superName, interfaces) + return writer.toByteArray() + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy index ff2dd138..83c43dca 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy @@ -28,6 +28,7 @@ import java.nio.file.Path import java.util.function.Supplier import java.util.zip.ZipFile +import groovy.transform.EqualsAndHashCode import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.MinimalExternalModuleDependency import org.gradle.api.logging.Logger @@ -41,11 +42,13 @@ import net.fabricmc.loom.configuration.providers.mappings.IntermediateMappingsSe import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingSpec import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsProcessor 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.utils.AddConstructorMappingVisitor import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider import net.fabricmc.loom.test.unit.LoomMocks import net.fabricmc.loom.util.download.Download import net.fabricmc.loom.util.download.DownloadBuilder +import net.fabricmc.mappingio.MappingReader import net.fabricmc.mappingio.adapter.MappingDstNsReorder import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch import net.fabricmc.mappingio.format.tiny.Tiny2FileWriter @@ -89,17 +92,21 @@ abstract class LayeredMappingsSpecification extends Specification implements Lay } MemoryMappingTree getLayeredMappings(MappingsSpec... specs) { - LayeredMappingSpec spec = new LayeredMappingSpec(specs.toList()) - LayeredMappingsProcessor processor = new LayeredMappingsProcessor(spec) + LayeredMappingsProcessor processor = createLayeredMappingsProcessor(specs) return processor.getMappings(processor.resolveLayers(mappingContext)) } UnpickLayer.UnpickData getUnpickData(MappingsSpec... specs) { - LayeredMappingSpec spec = new LayeredMappingSpec(specs.toList()) - LayeredMappingsProcessor processor = new LayeredMappingsProcessor(spec) + LayeredMappingsProcessor processor = createLayeredMappingsProcessor(specs) return processor.getUnpickData(processor.resolveLayers(mappingContext)) } + private static LayeredMappingsProcessor createLayeredMappingsProcessor(MappingsSpec... specs) { + boolean usingNoIntermediateSpec = specs.any { it instanceof NoIntermediateMappingsSpec } + LayeredMappingSpec spec = new LayeredMappingSpec(specs.toList()) + return new LayeredMappingsProcessor(spec, usingNoIntermediateSpec) + } + String getTiny(MemoryMappingTree mappingTree) { def sw = new StringWriter() mappingTree.accept(new Tiny2FileWriter(sw, false)) @@ -170,4 +177,20 @@ abstract class LayeredMappingsSpecification extends Specification implements Lay return false } } + + @EqualsAndHashCode + static class NoIntermediateMappingsSpec implements MappingsSpec { + static String NO_OP_MAPPINGS = "tiny\t2\t0\tofficial\tintermediary" + + @Override + IntermediaryMappingLayer createLayer(MappingContext context) { + return new IntermediaryMappingLayer(NoIntermediateMappingsSpec.&createNoOpMappings) + } + + private static MemoryMappingTree createNoOpMappings() { + def tree = new MemoryMappingTree() + MappingReader.read(new StringReader(NO_OP_MAPPINGS), tree) + return tree + } + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/MojangMappingLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/MojangMappingLayerTest.groovy index 08297037..a14d84fb 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/MojangMappingLayerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/MojangMappingLayerTest.groovy @@ -68,6 +68,27 @@ class MojangMappingLayerTest extends LayeredMappingsSpecification { !tiny.contains('this$0') } + def "Read mojang mappings with no intermediary" () { + setup: + intermediaryUrl = INTERMEDIARY_1_17_URL + mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17 + when: + def mappings = getLayeredMappings( + new NoIntermediateMappingsSpec(), + buildMojangMappingsSpec(true) + ) + def tiny = getTiny(mappings) + def intermediaryId = mappings.getNamespaceId("intermediary") + def officialId = mappings.getNamespaceId("official") + then: + mappings.srcNamespace == "named" + mappings.dstNamespaces == ["intermediary", "official"] + mappings.classes.size() == 6113 + mappings.getClass("com/mojang/blaze3d/Blaze3D").getDstName(intermediaryId) == "com/mojang/blaze3d/Blaze3D" + mappings.getClass("com/mojang/blaze3d/Blaze3D").getDstName(officialId) == "doe" + mappings.getClass("com/mojang/blaze3d/Blaze3D").getSrcName() == "com/mojang/blaze3d/Blaze3D" + } + static def buildMojangMappingsSpec(boolean nameSyntheticFields) { def builder = MojangMappingsSpecBuilderImpl.builder() builder.setNameSyntheticMembers(nameSyntheticFields) diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftMetadataProviderTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftMetadataProviderTest.groovy index 815d34b1..4566a881 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftMetadataProviderTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/providers/MinecraftMetadataProviderTest.groovy @@ -29,6 +29,7 @@ import java.nio.file.Path import org.intellij.lang.annotations.Language +import net.fabricmc.loom.configuration.providers.minecraft.ManifestLocations import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider import net.fabricmc.loom.test.LoomTestConstants import net.fabricmc.loom.test.unit.download.DownloadTest @@ -152,14 +153,16 @@ class MinecraftMetadataProviderTest extends DownloadTest { } private MinecraftMetadataProvider.Options options(String version, String customUrl) { + ManifestLocations manifests = new ManifestLocations("versions_manifest") + manifests.addBuiltIn(0, "$PATH/versionManifest", "versions_manifest") + manifests.addBuiltIn(1, "$PATH/experimentalVersionManifest", "experimental_versions_manifest") + return new MinecraftMetadataProvider.Options( version, - "$PATH/versionManifest", - "$PATH/experimentalVersionManifest", + manifests, customUrl, - testDir.resolve("version_manifest.json"), - testDir.resolve("experimental_version_manifest.json"), - testDir.resolve("${version}.json") + testDir, + testDir ) } diff --git a/src/test/groovy/net/fabricmc/loom/test/util/MinecraftTestUtils.groovy b/src/test/groovy/net/fabricmc/loom/test/util/MinecraftTestUtils.groovy index 8fb35383..42084f0d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/MinecraftTestUtils.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/MinecraftTestUtils.groovy @@ -29,8 +29,8 @@ import java.time.Duration import com.google.gson.Gson import com.google.gson.GsonBuilder -import net.fabricmc.loom.configuration.providers.minecraft.ManifestVersion 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.util.Constants import net.fabricmc.loom.util.download.Download @@ -41,7 +41,7 @@ class MinecraftTestUtils { static MinecraftVersionMeta getVersionMeta(String id) { def versionManifest = download(Constants.VERSION_MANIFESTS, "version_manifest.json") - def manifest = GSON.fromJson(versionManifest, ManifestVersion.class) + def manifest = GSON.fromJson(versionManifest, VersionsManifest.class) def version = manifest.versions().find { it.id == id } def metaJson = download(version.url, "${id}.json") diff --git a/src/test/groovy/net/fabricmc/loom/test/util/ZipTestUtils.groovy b/src/test/groovy/net/fabricmc/loom/test/util/ZipTestUtils.groovy index 75add96d..be637b4c 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/ZipTestUtils.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/ZipTestUtils.groovy @@ -33,17 +33,17 @@ import java.util.jar.Manifest import net.fabricmc.loom.util.FileSystemUtil class ZipTestUtils { - static Path createZip(Map entries) { + static Path createZip(Map entries, String suffix = ".zip") { return createZipFromBytes(entries.collectEntries { k, v -> [ k, v.getBytes(StandardCharsets.UTF_8) ] - }) + }, suffix) } - static Path createZipFromBytes(Map entries) { - def file = Files.createTempFile("loom-test", ".zip") + static Path createZipFromBytes(Map entries, String suffix = ".zip") { + def file = Files.createTempFile("loom-test", suffix) Files.delete(file) FileSystemUtil.getJarFileSystem(file, true).withCloseable { zip -> diff --git a/src/test/java/net/fabricmc/loom/test/unit/sandbox/SandboxEntrypoint.java b/src/test/java/net/fabricmc/loom/test/unit/sandbox/SandboxEntrypoint.java new file mode 100644 index 00000000..c4b061c8 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/sandbox/SandboxEntrypoint.java @@ -0,0 +1,40 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024 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.sandbox; + +public class SandboxEntrypoint { + public static void main(String[] args) { + String realMain = System.getProperty("fabric.sandbox.realMain"); + + if (realMain == null) { + throw new IllegalStateException("Unable to find real main"); + } + + // Print the curren stacktrace, we can use this to ensure that we haven't been launched via DLI + Thread.dumpStack(); + + System.out.println("Running real main: " + realMain); + } +} diff --git a/src/test/resources/projects/mojangMappings/src/main/java/net/fabricmc/example/ExampleMod.java b/src/test/resources/projects/mojangMappings/src/main/java/net/fabricmc/example/ExampleMod.java index e5ed082e..5ebd1157 100644 --- a/src/test/resources/projects/mojangMappings/src/main/java/net/fabricmc/example/ExampleMod.java +++ b/src/test/resources/projects/mojangMappings/src/main/java/net/fabricmc/example/ExampleMod.java @@ -1,6 +1,7 @@ package net.fabricmc.example; import net.fabricmc.api.ModInitializer; +import com.mojang.blaze3d.Blaze3D; public class ExampleMod implements ModInitializer { @Override