diff --git a/build.gradle b/build.gradle index e34a591e..0360efd5 100644 --- a/build.gradle +++ b/build.gradle @@ -143,12 +143,13 @@ dependencies { implementation libs.fabric.tiny.remapper implementation libs.fabric.access.widener implementation libs.fabric.mapping.io - implementation (libs.fabric.lorenz.tiny) { transitive = false } implementation "dev.architectury:refmap-remapper:1.0.5" + implementation libs.fabric.loom.nativelib + // decompilers fernflowerCompileOnly runtimeLibs.fernflower fernflowerCompileOnly libs.fabric.mapping.io diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9730a077..4d22412b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ mapping-io = "0.5.1" lorenz-tiny = "4.0.2" mercury = "0.1.4.17" kotlinx-metadata = "0.9.0" +loom-native = "0.1.0" # Plugins spotless = "6.25.0" @@ -48,6 +49,7 @@ fabric-access-widener = { module = "net.fabricmc:access-widener", version.ref = fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" } fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" } fabric-mercury = { module = "dev.architectury:mercury", version.ref = "mercury" } +fabric-loom-nativelib = { module = "net.fabricmc:fabric-loom-native", version.ref = "loom-native" } # Misc kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index eeb0f48e..9df6097f 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -108,7 +108,7 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { return switch (mappingsNamespace) { case NAMED -> getNamedMinecraftProvider().getMinecraftJarPaths(); case INTERMEDIARY -> getIntermediaryMinecraftProvider().getMinecraftJarPaths(); - case OFFICIAL -> getMinecraftProvider().getMinecraftJars(); + case OFFICIAL, CLIENT_OFFICIAL, SERVER_OFFICIAL -> getMinecraftProvider().getMinecraftJars(); case SRG -> { ModPlatform.assertPlatform(this, ModPlatform.FORGE, () -> "SRG jars are only available on Forge."); yield getSrgMinecraftProvider().getMinecraftJarPaths(); diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index e0ff2f36..6a3bfdb0 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -33,6 +33,7 @@ import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectList; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; @@ -206,7 +207,8 @@ public interface LoomGradleExtensionAPI { */ Property getIntermediaryUrl(); - Property getMinecraftJarConfiguration(); + @ApiStatus.Experimental + Property> getMinecraftJarConfiguration(); default void serverOnlyMinecraftJar() { getMinecraftJarConfiguration().set(MinecraftJarConfiguration.SERVER_ONLY); @@ -235,6 +237,11 @@ public interface LoomGradleExtensionAPI { */ Provider getMinecraftVersion(); + /** + * @return A lazily evaluated {@link FileCollection} containing the named minecraft jars. + */ + FileCollection getNamedMinecraftJars(); + // =================== // Architectury Loom // =================== diff --git a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java index 594a74b8..d4987b08 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java @@ -44,6 +44,14 @@ public abstract class IntermediateMappingsProvider implements Named { public abstract Property> getDownloader(); + /** + * Set to true if the minecraft version is pre 1.3. + * When true the expected src namespace is intermediary, and the expected dst namespaces are clientOfficial and/or serverOfficial + * When false the expected src namespace is named and the expected dst namespace is intermediary + */ + @ApiStatus.Experimental + public abstract Property getIsLegacyMinecraft(); + /** * Generate or download a tinyv2 mapping file with intermediary and named namespaces. * @throws IOException diff --git a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java index 2d8316c7..d4d27f1a 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java @@ -37,6 +37,18 @@ public enum MappingsNamespace { */ OFFICIAL, + /** + * Official names for the Minecraft client jar, usually obfuscated. + * This namespace is used for versions <1.3, where the client and server jars are obfuscated differently. + */ + CLIENT_OFFICIAL, + + /** + * Official names for the Minecraft server jar, usually obfuscated. + * This namespace is used for versions <1.3, where the client and server jars are obfuscated differently. + */ + SERVER_OFFICIAL, + /** * Intermediary mappings have been generated to provide a stable set of names across minecraft versions. * @@ -76,6 +88,8 @@ public enum MappingsNamespace { public static @Nullable MappingsNamespace of(String namespace) { return switch (namespace) { case "official" -> OFFICIAL; + case "clientOfficial" -> CLIENT_OFFICIAL; + case "serverOfficial" -> SERVER_OFFICIAL; case "intermediary" -> INTERMEDIARY; case "srg" -> SRG; case "mojang" -> MOJANG; @@ -86,6 +100,10 @@ public enum MappingsNamespace { @Override public String toString() { - return name().toLowerCase(Locale.ROOT); + return switch (this) { + case CLIENT_OFFICIAL -> "clientOfficial"; + case SERVER_OFFICIAL -> "serverOfficial"; + default -> name().toLowerCase(Locale.ROOT); + }; } } diff --git a/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java b/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java index 41431b65..58451554 100644 --- a/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java +++ b/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java @@ -36,6 +36,8 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; @@ -55,6 +57,8 @@ import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskDependency; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; @@ -65,6 +69,9 @@ import net.fabricmc.loom.util.fmj.FabricModJsonFactory; public final class IncludedJarFactory { private final Project project; + private static final Logger LOGGER = LoggerFactory.getLogger(IncludedJarFactory.class); + private static final String SEMVER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; + private static final Pattern SEMVER_PATTERN = Pattern.compile(SEMVER_REGEX); public IncludedJarFactory(Project project) { this.project = project; @@ -217,7 +224,8 @@ public final class IncludedJarFactory { jsonObject.addProperty("schemaVersion", 1); jsonObject.addProperty("id", modId); - jsonObject.addProperty("version", metadata.version()); + String version = getVersion(metadata); + jsonObject.addProperty("version", version); jsonObject.addProperty("name", metadata.name()); JsonObject custom = new JsonObject(); @@ -236,6 +244,35 @@ public final class IncludedJarFactory { return "_" + classifier; } } + + @Override + public String toString() { + return group + ":" + name + ":" + version + classifier(); + } + } + + private static String getVersion(Metadata metadata) { + String version = metadata.version(); + + if (validSemVer(version)) { + return version; + } + + if (version.endsWith(".Final") || version.endsWith(".final")) { + String trimmedVersion = version.substring(0, version.length() - 6); + + if (validSemVer(trimmedVersion)) { + return trimmedVersion; + } + } + + LOGGER.warn("({}) is not valid semver for dependency {}", version, metadata); + return version; + } + + private static boolean validSemVer(String version) { + Matcher matcher = SEMVER_PATTERN.matcher(version); + return matcher.find(); } public record NestedFile(Metadata metadata, File file) implements Serializable { } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index b9c39721..c855b71d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -32,8 +32,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -42,7 +40,6 @@ import javax.inject.Inject; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.file.FileCollection; -import org.gradle.api.logging.LogLevel; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.plugins.JavaPlugin; @@ -78,6 +75,7 @@ import net.fabricmc.loom.configuration.providers.forge.minecraft.ForgeMinecraftP import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; @@ -88,7 +86,9 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftPr import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.util.Checksum; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ExceptionUtil; +import net.fabricmc.loom.util.ProcessUtil; import net.fabricmc.loom.util.gradle.GradleUtils; import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.service.ScopedSharedServiceManager; @@ -127,6 +127,7 @@ public abstract class CompileConfiguration implements Runnable { try { setupMinecraft(configContext); } catch (Exception e) { + ExceptionUtil.printFileLocks(e, getProject()); throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e); } @@ -196,15 +197,21 @@ public abstract class CompileConfiguration implements Runnable { private synchronized void setupMinecraft(ConfigContext configContext) throws Exception { final Project project = configContext.project(); final LoomGradleExtension extension = configContext.extension(); - final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get(); - // Provide the vanilla mc jars -- TODO share across getProject()s. - final MinecraftProvider minecraftProvider = jarConfiguration.getMinecraftProviderFunction().apply(configContext); + final MinecraftMetadataProvider metadataProvider = MinecraftMetadataProvider.create(configContext); + + var jarConfiguration = extension.getMinecraftJarConfiguration().get(); + + if (jarConfiguration == MinecraftJarConfiguration.MERGED && !metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) { + jarConfiguration = MinecraftJarConfiguration.LEGACY_MERGED; + } + + // Provide the vanilla mc jars + final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(metadataProvider, configContext); if (extension.isForgeLike() && !(minecraftProvider instanceof ForgeMinecraftProvider)) { throw new UnsupportedOperationException("Using Forge with split jars is not supported!"); } - extension.setMinecraftProvider(minecraftProvider); minecraftProvider.provide(); @@ -236,15 +243,15 @@ public abstract class CompileConfiguration implements Runnable { } // Provide the remapped mc jars - final IntermediaryMinecraftProvider intermediaryMinecraftProvider = jarConfiguration.getIntermediaryMinecraftProviderBiFunction().apply(project, minecraftProvider); - NamedMinecraftProvider namedMinecraftProvider = jarConfiguration.getNamedMinecraftProviderBiFunction().apply(project, minecraftProvider); + final IntermediaryMinecraftProvider intermediaryMinecraftProvider = jarConfiguration.createIntermediaryMinecraftProvider(project); + NamedMinecraftProvider namedMinecraftProvider = jarConfiguration.createNamedMinecraftProvider(project); registerGameProcessors(configContext); MinecraftJarProcessorManager minecraftJarProcessorManager = MinecraftJarProcessorManager.create(getProject()); if (minecraftJarProcessorManager != null) { // Wrap the named MC provider for one that will provide the processed jars - namedMinecraftProvider = jarConfiguration.getProcessedNamedMinecraftProviderBiFunction().apply(namedMinecraftProvider, minecraftJarProcessorManager); + namedMinecraftProvider = jarConfiguration.createProcessedNamedMinecraftProvider(namedMinecraftProvider, minecraftJarProcessorManager); } final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(true, extension.refreshDeps(), configContext); @@ -327,8 +334,7 @@ public abstract class CompileConfiguration implements Runnable { final LoomGradleExtension extension = configContext.extension(); extension.getMinecraftJarConfiguration().get() - .getDecompileConfigurationBiFunction() - .apply(configContext.project(), extension.getNamedMinecraftProvider()) + .createDecompileConfiguration(getProject()) .afterEvaluation(); } @@ -403,7 +409,8 @@ public abstract class CompileConfiguration implements Runnable { Files.deleteIfExists(lockFile.file); abrupt = true; } else { - logger.lifecycle(printWithParents(handle.get())); + ProcessUtil processUtil = ProcessUtil.create(getProject()); + logger.lifecycle(processUtil.printWithParents(handle.get())); logger.lifecycle("Waiting for lock to be released..."); long sleptMs = 0; @@ -441,69 +448,6 @@ public abstract class CompileConfiguration implements Runnable { return abrupt ? LockResult.ACQUIRED_PREVIOUS_OWNER_MISSING : LockResult.ACQUIRED_CLEAN; } - private String printWithParents(ProcessHandle processHandle) { - var output = new StringBuilder(); - - List chain = getParentChain(null, processHandle); - - for (int i = 0; i < chain.size(); i++) { - ProcessHandle handle = chain.get(i); - - output.append("\t".repeat(i)); - - if (i != 0) { - output.append("└─ "); - } - - output.append(getInfoString(handle)); - - if (i < chain.size() - 1) { - output.append('\n'); - } - } - - return output.toString(); - } - - private String getInfoString(ProcessHandle handle) { - return "(%s) pid %s '%s%s'%s".formatted( - handle.info().user().orElse("unknown user"), - handle.pid(), - handle.info().command().orElse("unknown command"), - handle.info().arguments().map(arr -> { - if (getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.INFO - && getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.DEBUG) { - return " (run with --info or --debug to show arguments, may reveal sensitive info)"; - } - - String join = String.join(" ", arr); - - if (join.isBlank()) { - return ""; - } - - return " " + join; - }).orElse(" (unknown arguments)"), - handle.info().startInstant().map(instant -> " started at " + instant).orElse("") - ); - } - - private List getParentChain(List collectTo, ProcessHandle processHandle) { - if (collectTo == null) { - collectTo = new ArrayList<>(); - } - - Optional parent = processHandle.parent(); - - if (parent.isPresent()) { - getParentChain(collectTo, parent.get()); - } - - collectTo.add(processHandle); - - return collectTo; - } - private void releaseLock() { final Path lock = getLockFile().file; diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java index 4fde07fd..67d97342 100644 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java @@ -181,8 +181,8 @@ public abstract class FabricApiExtension { if (settings.getCreateRunConfiguration().get()) { extension.getRunConfigs().create("datagen", run -> { - run.setConfigName("Data Generation"); run.inherit(extension.getRunConfigs().getByName("server")); + run.setConfigName("Data Generation"); run.property("fabric-api.datagen"); run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java index 2b9b9b95..6ba46400 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java @@ -61,7 +61,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration { task.getInputJarName().set(minecraftJar.getName()); - task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", minecraftJar.getPath())); + task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", minecraftJar.getPath())); task.dependsOn(project.getTasks().named("validateAccessWidener")); task.setDescription("Decompile minecraft using %s.".formatted(decompilerName)); diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java index 0c8df985..99c67973 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java @@ -55,7 +55,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration commonDecompileTask = createDecompileTasks("Common", task -> { task.getInputJarName().set(commonJar.getName()); - task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", commonJar.getPath())); + task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", commonJar.getPath())); if (mappingConfiguration.hasUnpickDefinitions()) { File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-common-unpicked.jar"); @@ -65,7 +65,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration clientOnlyDecompileTask = createDecompileTasks("ClientOnly", task -> { task.getInputJarName().set(clientOnlyJar.getName()); - task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath())); + task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath())); if (mappingConfiguration.hasUnpickDefinitions()) { File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-clientonly-unpicked.jar"); 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 86e3ddfb..2ba73382 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java @@ -44,6 +44,7 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; @@ -59,6 +60,7 @@ import net.fabricmc.loom.configuration.InstallerData; import net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryContext; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.gradle.SourceSetReference; @@ -138,6 +140,12 @@ public class RunConfig { public static RunConfig runConfig(Project project, RunConfigSettings settings) { settings.evaluateNow(); LoomGradleExtension extension = LoomGradleExtension.get(project); + LibraryContext context = new LibraryContext(extension.getMinecraftProvider().getVersionInfo(), JavaVersion.current()); + + if (settings.getEnvironment().equals("client") && context.usesLWJGL3()) { + settings.startFirstThread(); + } + String name = settings.getName(); String configName = settings.getConfigName(); 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 0e4b1801..c89e40af 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java @@ -357,7 +357,6 @@ public class RunConfigSettings implements Named { * Configure run config with the default client options. */ public void client() { - startFirstThread(); environment("client"); defaultMainClass(Constants.Knot.KNOT_CLIENT); diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java b/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java index 98244e3a..d799b45d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java @@ -116,8 +116,7 @@ record DownloadSourcesHook(Project project, Task task) { private String getGenSourcesTaskName(MinecraftJar.Type jarType) { LoomGradleExtension extension = LoomGradleExtension.get(project); return extension.getMinecraftJarConfiguration().get() - .getDecompileConfigurationBiFunction() - .apply(project, extension.getNamedMinecraftProvider()) + .createDecompileConfiguration(project) .getTaskName(jarType); } diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java index 5da1061f..f502c216 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java @@ -55,12 +55,14 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.task.AbstractLoomTask; +import net.fabricmc.loom.util.Constants; public abstract class IdeaSyncTask extends AbstractLoomTask { @Inject public IdeaSyncTask() { // Always re-run this task. getOutputs().upToDateWhen(element -> false); + setGroup(Constants.TaskGroup.IDE); } @TaskAction diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java index 75af308c..a0568de9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java @@ -50,10 +50,12 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; public final class IntermediateMappingsService implements SharedService { private final Path intermediaryTiny; + private final String expectedSrcNs; private final Supplier memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree); - private IntermediateMappingsService(Path intermediaryTiny) { + private IntermediateMappingsService(Path intermediaryTiny, String expectedSrcNs) { this.intermediaryTiny = intermediaryTiny; + this.expectedSrcNs = expectedSrcNs; } public static synchronized IntermediateMappingsService getInstance(SharedServiceManager sharedServiceManager, Project project, MinecraftProvider minecraftProvider) { @@ -84,7 +86,13 @@ public final class IntermediateMappingsService implements SharedService { throw new UncheckedIOException("Failed to provide intermediate mappings", e); } - return new IntermediateMappingsService(intermediaryTiny); + // When merging legacy versions there will be multiple named namespaces, so use intermediary as the common src ns + // Newer versions will use intermediary as the src ns + final String expectedSrcNs = minecraftProvider.isLegacyVersion() + ? MappingsNamespace.INTERMEDIARY.toString() // <1.3 + : MappingsNamespace.OFFICIAL.toString(); // >=1.3 + + return new IntermediateMappingsService(intermediaryTiny, expectedSrcNs); } private MemoryMappingTree createMemoryMappingTree() { @@ -100,6 +108,10 @@ public final class IntermediateMappingsService implements SharedService { throw new UncheckedIOException("Failed to read intermediary mappings", e); } + if (!expectedSrcNs.equals(tree.getSrcNamespace())) { + throw new RuntimeException("Invalid intermediate mappings: expected source namespace '" + expectedSrcNs + "' but found '" + tree.getSrcNamespace() + "\'"); + } + return tree; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java index e10db1c5..a081405d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java @@ -328,7 +328,7 @@ public class MappingConfiguration { // These are unmerged v2 mappings IntermediateMappingsService intermediateMappingsService = IntermediateMappingsService.getInstance(serviceManager, project, minecraftProvider); - MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, intermediateMappingsService); + MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, minecraftProvider, intermediateMappingsService); } else { if (LoomGradleExtension.get(project).isForgeLike()) { // (2022-09-11) This is due to ordering issues. diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java index 28ddfc63..d198baf9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java @@ -37,11 +37,12 @@ import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; * A bit of a hack, creates an empty intermediary mapping file to be used for mc versions without any intermediate mappings. */ public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappingsProvider { - private static final String HEADER = "tiny\t2\t0\tofficial\tintermediary"; + private static final String HEADER_OFFICIAL_MERGED = "tiny\t2\t0\tofficial\tintermediary"; + private static final String HEADER_OFFICIAL_LEGACY_MERGED = "tiny\t2\t0\tintermediary\tclientOfficial\tserverOfficial\t"; @Override public void provide(Path tinyMappings) throws IOException { - Files.writeString(tinyMappings, HEADER, StandardCharsets.UTF_8); + Files.writeString(tinyMappings, getIsLegacyMinecraft().get() ? HEADER_OFFICIAL_LEGACY_MERGED : HEADER_OFFICIAL_MERGED, StandardCharsets.UTF_8); } @Override diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java index 4328311b..b4ea05c8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java @@ -47,7 +47,7 @@ public record SignatureFixesLayerImpl(Path mappingsFile) implements MappingLayer public Map getSignatureFixes() { try { //noinspection unchecked - return ZipUtils.unpackJackson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class); + return ZipUtils.unpackJson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class); } catch (IOException e) { throw new RuntimeException("Failed to extract signature fixes", e); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java index 0e2f68c6..16af9d5c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java @@ -47,6 +47,6 @@ public record ParchmentMappingLayer(Path parchmentFile, boolean removePrefix) im } private ParchmentTreeV1 getParchmentData() throws IOException { - return ZipUtils.unpackJackson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class); + return ZipUtils.unpackJson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java index db845663..e2f6ba67 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.providers.mappings.IntermediateMappingsService; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.mappingio.adapter.MappingNsCompleter; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.format.tiny.Tiny2FileReader; @@ -49,10 +50,20 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; public final class MappingsMerger { private static final Logger LOGGER = LoggerFactory.getLogger(MappingsMerger.class); - public static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { + public static void mergeAndSaveMappings(Path from, Path out, MinecraftProvider minecraftProvider, IntermediateMappingsService intermediateMappingsService) throws IOException { Stopwatch stopwatch = Stopwatch.createStarted(); LOGGER.info(":merging mappings"); + if (minecraftProvider.isLegacyVersion()) { + legacyMergeAndSaveMappings(from, out, intermediateMappingsService); + } else { + mergeAndSaveMappings(from, out, intermediateMappingsService); + } + + LOGGER.info(":merged mappings in " + stopwatch.stop()); + } + + private static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { MemoryMappingTree intermediaryTree = new MemoryMappingTree(); intermediateMappingsService.getMemoryMappingTree().accept(new MappingSourceNsSwitch(intermediaryTree, MappingsNamespace.INTERMEDIARY.toString())); @@ -70,8 +81,27 @@ public final class MappingsMerger { try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) { officialTree.accept(writer); } + } - LOGGER.info(":merged mappings in " + stopwatch.stop()); + private static void legacyMergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { + MemoryMappingTree intermediaryTree = new MemoryMappingTree(); + intermediateMappingsService.getMemoryMappingTree().accept(intermediaryTree); + + try (BufferedReader reader = Files.newBufferedReader(from, StandardCharsets.UTF_8)) { + Tiny2FileReader.read(reader, intermediaryTree); + } + + MemoryMappingTree officialTree = new MemoryMappingTree(); + MappingNsCompleter nsCompleter = new MappingNsCompleter(officialTree, Map.of(MappingsNamespace.CLIENT_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString(), MappingsNamespace.SERVER_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString())); + intermediaryTree.accept(nsCompleter); + + // versions this old strip inner class attributes + // from the obfuscated jars anyway + //inheritMappedNamesOfEnclosingClasses(officialTree); + + try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) { + officialTree.accept(writer); + } } /** diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java new file mode 100644 index 00000000..28284f1d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java @@ -0,0 +1,83 @@ +/* + * 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.List; + +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.ConfigContext; + +/** + * Minecraft versions prior to 1.3 obfuscate the server and client jars differently. + * The obfuscated jars must be provided separately, and can be merged after remapping. + */ +public final class LegacyMergedMinecraftProvider extends MinecraftProvider { + private final SingleJarMinecraftProvider.Server serverMinecraftProvider; + private final SingleJarMinecraftProvider.Client clientMinecraftProvider; + + public LegacyMergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); + serverMinecraftProvider = SingleJarMinecraftProvider.server(metadataProvider, configContext); + clientMinecraftProvider = SingleJarMinecraftProvider.client(metadataProvider, configContext); + + if (!isLegacyVersion()) { + throw new RuntimeException("something has gone wrong - legacy-merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " allows merging the obfuscated jars - the merged jar configuration should have been selected!"); + } + } + + public SingleJarMinecraftProvider.Server getServerMinecraftProvider() { + return serverMinecraftProvider; + } + + public SingleJarMinecraftProvider.Client getClientMinecraftProvider() { + return clientMinecraftProvider; + } + + @Override + public void provide() throws Exception { + if (!serverMinecraftProvider.provideServer() || !clientMinecraftProvider.provideClient()) { + throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!"); + } + + serverMinecraftProvider.provide(); + clientMinecraftProvider.provide(); + } + + @Override + public List getMinecraftJars() { + return List.of( + serverMinecraftProvider.getMinecraftEnvOnlyJar(), + clientMinecraftProvider.getMinecraftEnvOnlyJar() + ); + } + + @Override + @Deprecated + public MappingsNamespace getOfficialNamespace() { + // Legacy merged providers do not have a single namespace as they delegate to the single jar providers + throw new UnsupportedOperationException("Cannot query the official namespace for legacy-merged minecraft providers"); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java index d3ad1607..bb67fe6e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java @@ -31,13 +31,22 @@ import java.nio.file.Path; import java.util.List; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; public class MergedMinecraftProvider extends MinecraftProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(MergedMinecraftProvider.class); private Path minecraftMergedJar; - public MergedMinecraftProvider(ConfigContext configContext) { - super(configContext); + public MergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); + + if (isLegacyVersion()) { + throw new RuntimeException("something has gone wrong - merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " does not allow merging the obfuscated jars - the legacy-merged jar configuration should have been selected!"); + } } @Override @@ -51,12 +60,17 @@ public class MergedMinecraftProvider extends MinecraftProvider { return List.of(minecraftMergedJar); } + @Override + public MappingsNamespace getOfficialNamespace() { + return MappingsNamespace.OFFICIAL; + } + @Override public void provide() throws Exception { super.provide(); - if (!getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { - throw new UnsupportedOperationException("Minecraft versions 1.2.5 and older cannot be merged. Please use `loom { server/clientOnlyMinecraftJar() }`"); + if (!provideServer() || !provideClient()) { + throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!"); } if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) { @@ -74,18 +88,24 @@ public class MergedMinecraftProvider extends MinecraftProvider { } protected void mergeJars() throws IOException { - getLogger().info(":merging jars"); - - File jarToMerge = getMinecraftServerJar(); + File minecraftClientJar = getMinecraftClientJar(); + File minecraftServerJar = getMinecraftServerJar(); if (getServerBundleMetadata() != null) { extractBundledServerJar(); - jarToMerge = getMinecraftExtractedServerJar(); + minecraftServerJar = getMinecraftExtractedServerJar(); } - Objects.requireNonNull(jarToMerge, "Cannot merge null input jar?"); + mergeJars(minecraftClientJar, minecraftServerJar, minecraftMergedJar.toFile()); + } - try (var jarMerger = new MinecraftJarMerger(getMinecraftClientJar(), jarToMerge, minecraftMergedJar.toFile())) { + public static void mergeJars(File clientJar, File serverJar, File mergedJar) throws IOException { + LOGGER.info(":merging jars"); + + Objects.requireNonNull(clientJar, "Cannot merge null client jar?"); + Objects.requireNonNull(serverJar, "Cannot merge null server jar?"); + + try (var jarMerger = new MinecraftJarMerger(clientJar, serverJar, mergedJar)) { jarMerger.enableSyntheticParamsOffset(); jarMerger.merge(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java index 7d38da2e..b60cb417 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java @@ -257,6 +257,8 @@ public class MinecraftClassMerger { int j = 0; while (i < first.size() || j < second.size()) { + int saved = i + j; + while (i < first.size() && j < second.size() && first.get(i).equals(second.get(j))) { out.add(first.get(i)); @@ -273,6 +275,20 @@ public class MinecraftClassMerger { out.add(second.get(j)); j++; } + + // if the order is scrambled, it's not possible to merge + // the lists while preserving the order from both sides + if (i + j == saved) { + for (; i < first.size(); i++) { + out.add(first.get(i)); + } + + for (; j < second.size(); j++) { + if (!first.contains(second.get(j))) { + out.add(second.get(j)); + } + } + } } return out; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java index d2999c75..2d10f377 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java @@ -25,11 +25,10 @@ package net.fabricmc.loom.configuration.providers.minecraft; import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; import org.gradle.api.Project; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.decompile.DecompileConfiguration; import net.fabricmc.loom.configuration.decompile.SingleJarDecompileConfiguration; @@ -43,96 +42,110 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraft import net.fabricmc.loom.configuration.providers.minecraft.mapped.ProcessedNamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider; -public enum MinecraftJarConfiguration { - MERGED( - ForgeMinecraftProvider::createMerged, - IntermediaryMinecraftProvider.MergedImpl::new, - NamedMinecraftProvider.MergedImpl::new, - SrgMinecraftProvider.MergedImpl::new, - MojangMappedMinecraftProvider.MergedImpl::new, - ProcessedNamedMinecraftProvider.MergedImpl::new, - SingleJarDecompileConfiguration::new, - List.of("client", "server") - ), - SERVER_ONLY( - ForgeMinecraftProvider::createServerOnly, - IntermediaryMinecraftProvider.SingleJarImpl::server, - NamedMinecraftProvider.SingleJarImpl::server, - SrgMinecraftProvider.SingleJarImpl::server, - MojangMappedMinecraftProvider.SingleJarImpl::server, - ProcessedNamedMinecraftProvider.SingleJarImpl::server, - SingleJarDecompileConfiguration::new, - List.of("server") - ), - CLIENT_ONLY( - ForgeMinecraftProvider::createClientOnly, - IntermediaryMinecraftProvider.SingleJarImpl::client, - NamedMinecraftProvider.SingleJarImpl::client, - SrgMinecraftProvider.SingleJarImpl::client, - MojangMappedMinecraftProvider.SingleJarImpl::client, - ProcessedNamedMinecraftProvider.SingleJarImpl::client, - SingleJarDecompileConfiguration::new, - List.of("client") - ), - SPLIT( - SplitMinecraftProvider::new, - IntermediaryMinecraftProvider.SplitImpl::new, - NamedMinecraftProvider.SplitImpl::new, - SrgMinecraftProvider.SplitImpl::new, - MojangMappedMinecraftProvider.SplitImpl::new, - ProcessedNamedMinecraftProvider.SplitImpl::new, - SplitDecompileConfiguration::new, - List.of("client", "server") - ); +public record MinecraftJarConfiguration< + M extends MinecraftProvider, + N extends NamedMinecraftProvider, + Q extends MappedMinecraftProvider>( + MinecraftProviderFactory minecraftProviderFactory, + IntermediaryMinecraftProviderFactory intermediaryMinecraftProviderFactory, + NamedMinecraftProviderFactory namedMinecraftProviderFactory, + ProcessedNamedMinecraftProviderFactory processedNamedMinecraftProviderFactory, + DecompileConfigurationFactory decompileConfigurationFactory, + List supportedEnvironments) { + public static final MinecraftJarConfiguration< + MergedMinecraftProvider, + NamedMinecraftProvider.MergedImpl, + MappedMinecraftProvider> MERGED = new MinecraftJarConfiguration<>( + ForgeMinecraftProvider::createMerged, + IntermediaryMinecraftProvider.MergedImpl::new, + NamedMinecraftProvider.MergedImpl::new, + SrgMinecraftProvider.MergedImpl::new, + MojangMappedMinecraftProvider.MergedImpl::new, + ProcessedNamedMinecraftProvider.MergedImpl::new, + SingleJarDecompileConfiguration::new, + List.of("client", "server") + ); + public static final MinecraftJarConfiguration< + LegacyMergedMinecraftProvider, + NamedMinecraftProvider.LegacyMergedImpl, + MappedMinecraftProvider> LEGACY_MERGED = new MinecraftJarConfiguration<>( + LegacyMergedMinecraftProvider::new, + IntermediaryMinecraftProvider.LegacyMergedImpl::new, + NamedMinecraftProvider.LegacyMergedImpl::new, + ProcessedNamedMinecraftProvider.LegacyMergedImpl::new, + SingleJarDecompileConfiguration::new, + List.of("client", "server") + ); + public static final MinecraftJarConfiguration< + SingleJarMinecraftProvider, + NamedMinecraftProvider.SingleJarImpl, + MappedMinecraftProvider> SERVER_ONLY = new MinecraftJarConfiguration<>( + ForgeMinecraftProvider::createServerOnly, + IntermediaryMinecraftProvider.SingleJarImpl::server, + NamedMinecraftProvider.SingleJarImpl::server, + SrgMinecraftProvider.SingleJarImpl::server, + MojangMappedMinecraftProvider.SingleJarImpl::server, + ProcessedNamedMinecraftProvider.SingleJarImpl::server, + SingleJarDecompileConfiguration::new, + List.of("server") + ); + public static final MinecraftJarConfiguration< + SingleJarMinecraftProvider, + NamedMinecraftProvider.SingleJarImpl, + MappedMinecraftProvider> CLIENT_ONLY = new MinecraftJarConfiguration<>( + ForgeMinecraftProvider::createClientOnly, + IntermediaryMinecraftProvider.SingleJarImpl::client, + NamedMinecraftProvider.SingleJarImpl::client, + SrgMinecraftProvider.SingleJarImpl::client, + MojangMappedMinecraftProvider.SingleJarImpl::client, + ProcessedNamedMinecraftProvider.SingleJarImpl::client, + SingleJarDecompileConfiguration::new, + List.of("client") + ); + public static final MinecraftJarConfiguration< + SplitMinecraftProvider, + NamedMinecraftProvider.SplitImpl, + MappedMinecraftProvider.Split> SPLIT = new MinecraftJarConfiguration<>( + SplitMinecraftProvider::new, + IntermediaryMinecraftProvider.SplitImpl::new, + NamedMinecraftProvider.SplitImpl::new, + SrgMinecraftProvider.SplitImpl::new, + MojangMappedMinecraftProvider.SplitImpl::new, + ProcessedNamedMinecraftProvider.SplitImpl::new, + SplitDecompileConfiguration::new, + List.of("client", "server") + ); - private final Function minecraftProviderFunction; - private final BiFunction> intermediaryMinecraftProviderBiFunction; - private final BiFunction> namedMinecraftProviderBiFunction; - private final BiFunction> srgMinecraftProviderBiFunction; - private final BiFunction> mojangMappedMinecraftProviderBiFunction; - private final BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider> processedNamedMinecraftProviderBiFunction; - private final BiFunction> decompileConfigurationBiFunction; - private final List supportedEnvironments; - - @SuppressWarnings("unchecked") // Just a bit of a generic mess :) - , Q extends MappedMinecraftProvider> MinecraftJarConfiguration( - Function minecraftProviderFunction, - BiFunction> intermediaryMinecraftProviderBiFunction, - BiFunction namedMinecraftProviderBiFunction, - BiFunction> srgMinecraftProviderBiFunction, - BiFunction> mojangMappedMinecraftProviderBiFunction, - BiFunction> processedNamedMinecraftProviderBiFunction, - BiFunction> decompileConfigurationBiFunction, - List supportedEnvironments - ) { - this.minecraftProviderFunction = (Function) minecraftProviderFunction; - this.intermediaryMinecraftProviderBiFunction = (BiFunction>) (Object) intermediaryMinecraftProviderBiFunction; - this.namedMinecraftProviderBiFunction = (BiFunction>) namedMinecraftProviderBiFunction; - this.srgMinecraftProviderBiFunction = (BiFunction>) (Object) srgMinecraftProviderBiFunction; - this.mojangMappedMinecraftProviderBiFunction = (BiFunction>) (Object) mojangMappedMinecraftProviderBiFunction; - this.processedNamedMinecraftProviderBiFunction = (BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider>) (Object) processedNamedMinecraftProviderBiFunction; - this.decompileConfigurationBiFunction = (BiFunction>) decompileConfigurationBiFunction; - this.supportedEnvironments = supportedEnvironments; + public MinecraftProvider createMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext context) { + return minecraftProviderFactory.create(metadataProvider, context); } - public Function getMinecraftProviderFunction() { - return minecraftProviderFunction; + public IntermediaryMinecraftProvider createIntermediaryMinecraftProvider(Project project) { + return intermediaryMinecraftProviderFactory.create(project, getMinecraftProvider(project)); } - public BiFunction> getIntermediaryMinecraftProviderBiFunction() { - return intermediaryMinecraftProviderBiFunction; + public NamedMinecraftProvider createNamedMinecraftProvider(Project project) { + return namedMinecraftProviderFactory.create(project, getMinecraftProvider(project)); } - public BiFunction> getNamedMinecraftProviderBiFunction() { - return namedMinecraftProviderBiFunction; + public ProcessedNamedMinecraftProvider createProcessedNamedMinecraftProvider(NamedMinecraftProvider namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) { + return processedNamedMinecraftProviderFactory.create((N) namedMinecraftProvider, jarProcessorManager); } - public BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider> getProcessedNamedMinecraftProviderBiFunction() { - return processedNamedMinecraftProviderBiFunction; + public DecompileConfiguration createDecompileConfiguration(Project project) { + return decompileConfigurationFactory.create(project, getMappedMinecraftProvider(project)); } - public BiFunction> getDecompileConfigurationBiFunction() { - return decompileConfigurationBiFunction; + private M getMinecraftProvider(Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + //noinspection unchecked + return (M) extension.getMinecraftProvider(); + } + + private Q getMappedMinecraftProvider(Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + //noinspection unchecked + return (Q) extension.getNamedMinecraftProvider(); } public BiFunction> getSrgMinecraftProviderBiFunction() { @@ -146,4 +159,33 @@ public enum MinecraftJarConfiguration { public List getSupportedEnvironments() { return supportedEnvironments; } + + // Factory interfaces: + private interface MinecraftProviderFactory { + M create(MinecraftMetadataProvider metadataProvider, ConfigContext configContext); + } + + private interface IntermediaryMinecraftProviderFactory { + IntermediaryMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface NamedMinecraftProviderFactory { + NamedMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface SrgMinecraftProviderFactory { + SrgMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface MojangMappedMinecraftProviderFactory { + MojangMappedMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface ProcessedNamedMinecraftProviderFactory> { + ProcessedNamedMinecraftProvider create(N namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager); + } + + private interface DecompileConfigurationFactory { + DecompileConfiguration create(Project project, M minecraftProvider); + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java index 5b0f9d9c..450010f0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java @@ -82,6 +82,8 @@ public class MinecraftJarMerger implements AutoCloseable { } } + Files.createDirectories(output.toPath().getParent()); + this.inputClient = (inputClientFs = FileSystemUtil.getJarFileSystem(inputClient, false)).get().getPath("/"); this.inputServer = (inputServerFs = FileSystemUtil.getJarFileSystem(inputServer, false)).get().getPath("/"); this.outputFs = FileSystemUtil.getJarFileSystem(output, true); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java index 62fa2bdd..896a2bd5 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java @@ -82,8 +82,8 @@ public class MinecraftLibraryProvider { final LoomGradleExtension extension = LoomGradleExtension.get(project); final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get(); - final boolean provideClient = jarConfiguration.getSupportedEnvironments().contains("client"); - final boolean provideServer = jarConfiguration.getSupportedEnvironments().contains("server"); + final boolean provideClient = jarConfiguration.supportedEnvironments().contains("client"); + final boolean provideServer = jarConfiguration.supportedEnvironments().contains("server"); assert provideClient || provideServer; if (provideClient) { 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 840207f6..fd2eea8e 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 @@ -35,6 +35,9 @@ 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.util.Constants; import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.download.DownloadBuilder; @@ -45,11 +48,34 @@ public final class MinecraftMetadataProvider { private ManifestVersion.Versions versionEntry; private MinecraftVersionMeta versionMeta; - public MinecraftMetadataProvider(Options options, Function download) { + private MinecraftMetadataProvider(Options options, Function download) { this.options = options; this.download = download; } + 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.extension()::download + ); + } + + private static String resolveMinecraftVersion(Project project) { + final DependencyInfo dependency = DependencyInfo.create(project, Constants.Configurations.MINECRAFT); + return dependency.getDependency().getVersion(); + } + + public String getMinecraftVersion() { + return options.minecraftVersion(); + } + public MinecraftVersionMeta getVersionMeta() { try { if (versionEntry == null) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index d7b53451..6276afa4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -31,13 +31,15 @@ import java.util.List; import java.util.Objects; import com.google.common.base.Preconditions; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; -import org.gradle.api.logging.Logger; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; -import net.fabricmc.loom.configuration.DependencyInfo; import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.download.DownloadExecutor; @@ -45,10 +47,10 @@ import net.fabricmc.loom.util.download.GradleDownloadProgressListener; import net.fabricmc.loom.util.gradle.ProgressGroup; public abstract class MinecraftProvider { - private String minecraftVersion; - private MinecraftMetadataProvider metadataProvider; + private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftProvider.class); + + private final MinecraftMetadataProvider metadataProvider; - private File workingDir; private File minecraftClientJar; // Note this will be the boostrap jar starting with 21w39a private File minecraftServerJar; @@ -58,10 +60,11 @@ public abstract class MinecraftProvider { private BundleMetadata serverBundleMetadata; private String jarPrefix = ""; - private final Project project; + private final ConfigContext configContext; - public MinecraftProvider(ConfigContext configContext) { - this.project = configContext.project(); + public MinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + this.metadataProvider = metadataProvider; + this.configContext = configContext; } protected boolean provideClient() { @@ -73,23 +76,22 @@ public abstract class MinecraftProvider { } public void provide() throws Exception { - final DependencyInfo dependency = DependencyInfo.create(getProject(), Constants.Configurations.MINECRAFT); - minecraftVersion = dependency.getDependency().getVersion(); - if (getExtension().shouldGenerateSrgTiny() && !getExtension().isForgeLike()) { getProject().getDependencies().add(Constants.Configurations.SRG, "de.oceanlabs.mcp:mcp_config:" + minecraftVersion); } initFiles(); - metadataProvider = new MinecraftMetadataProvider( - MinecraftMetadataProvider.Options.create( - minecraftVersion, - getProject(), - file("minecraft-info.json").toPath() - ), - getExtension()::download - ); + final MinecraftVersionMeta.JavaVersion javaVersion = getVersionInfo().javaVersion(); + + if (javaVersion != null) { + final int requiredMajorJavaVersion = getVersionInfo().javaVersion().majorVersion(); + final JavaVersion requiredJavaVersion = JavaVersion.toVersion(requiredMajorJavaVersion); + + if (!JavaVersion.current().isCompatibleWith(requiredJavaVersion)) { + throw new IllegalStateException("Minecraft " + minecraftVersion() + " requires Java " + requiredJavaVersion + " but Gradle is using " + JavaVersion.current()); + } + } downloadJars(); @@ -97,14 +99,11 @@ public abstract class MinecraftProvider { serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath()); } - final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, project); + final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, configContext.project()); libraryProvider.provide(); } protected void initFiles() { - workingDir = new File(getExtension().getFiles().getUserCache(), minecraftVersion); - workingDir.mkdirs(); - if (provideClient()) { minecraftClientJar = file("minecraft-client.jar"); } @@ -140,17 +139,17 @@ public abstract class MinecraftProvider { Preconditions.checkArgument(provideServer(), "Not configured to provide server jar"); Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar"); - getLogger().info(":Extracting server jar from bootstrap"); + LOGGER.info(":Extracting server jar from bootstrap"); if (getServerBundleMetadata().versions().size() != 1) { throw new UnsupportedOperationException("Expected only 1 version in META-INF/versions.list, but got %d".formatted(getServerBundleMetadata().versions().size())); } - getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), project); + getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), configContext.project()); } public File workingDir() { - return workingDir; + return minecraftWorkingDirectory(configContext.project(), minecraftVersion()); } public File dir(String path) { @@ -186,13 +185,20 @@ public abstract class MinecraftProvider { } public String minecraftVersion() { - return minecraftVersion; + return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getMinecraftVersion(); } public MinecraftVersionMeta getVersionInfo() { return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getVersionMeta(); } + /** + * @return true if the minecraft version is older than 1.3. + */ + public boolean isLegacyVersion() { + return !getVersionInfo().isVersionOrNewer(Constants.RELEASE_TIME_1_3); + } + public String getJarPrefix() { return jarPrefix; } @@ -206,21 +212,26 @@ public abstract class MinecraftProvider { return serverBundleMetadata; } - protected Logger getLogger() { - return getProject().getLogger(); - } - public abstract List getMinecraftJars(); + public abstract MappingsNamespace getOfficialNamespace(); + protected Project getProject() { - return project; + return configContext.project(); } protected LoomGradleExtension getExtension() { - return LoomGradleExtension.get(getProject()); + return configContext.extension(); } public boolean refreshDeps() { return getExtension().refreshDeps(); } + + public static File minecraftWorkingDirectory(Project project, String version) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + File workingDir = new File(extension.getFiles().getUserCache(), version); + workingDir.mkdirs(); + return workingDir; + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java index 7a311fbb..5325cc38 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java @@ -47,7 +47,8 @@ public record MinecraftVersionMeta( int minimumLauncherVersion, String releaseTime, String time, - String type + String type, + @Nullable JavaVersion javaVersion ) { private static Map OS_NAMES = Map.of( Platform.OperatingSystem.WINDOWS, "windows", @@ -168,4 +169,7 @@ public record MinecraftVersionMeta( return new File(baseDirectory, path()); } } + + public record JavaVersion(String component, int majorVersion) { + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 94411225..aba9b57a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -28,35 +28,45 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.util.Constants; import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; -public class SingleJarMinecraftProvider extends MinecraftProvider { - private final Environment environment; - +public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvider permits SingleJarMinecraftProvider.Server, SingleJarMinecraftProvider.Client { + private final MappingsNamespace officialNamespace; private Path minecraftEnvOnlyJar; - protected SingleJarMinecraftProvider(ConfigContext configContext, Environment environment) { - super(configContext); - this.environment = environment; + protected SingleJarMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { + super(metadataProvider, configContext); + this.officialNamespace = officialNamespace; } - public static SingleJarMinecraftProvider server(ConfigContext configContext) { - return new SingleJarMinecraftProvider(configContext, new Server()); + public static SingleJarMinecraftProvider.Server server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + return new SingleJarMinecraftProvider.Server(metadataProvider, configContext, getOfficialNamespace(metadataProvider, true)); } - public static SingleJarMinecraftProvider client(ConfigContext configContext) { - return new SingleJarMinecraftProvider(configContext, new Client()); + public static SingleJarMinecraftProvider.Client client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + return new SingleJarMinecraftProvider.Client(metadataProvider, configContext, getOfficialNamespace(metadataProvider, false)); + } + + private static MappingsNamespace getOfficialNamespace(MinecraftMetadataProvider metadataProvider, boolean server) { + // Versions before 1.3 don't have a common namespace, so use side specific namespaces. + if (!metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) { + return server ? MappingsNamespace.SERVER_OFFICIAL : MappingsNamespace.CLIENT_OFFICIAL; + } + + return MappingsNamespace.OFFICIAL; } @Override protected void initFiles() { super.initFiles(); - minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(environment.type())); + minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(type())); } @Override @@ -69,7 +79,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { super.provide(); // Server only JARs are supported on any version, client only JARs are pretty much useless after 1.3. - if (provideClient() && getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { + if (provideClient() && !isLegacyVersion()) { getProject().getLogger().warn("Using `clientOnlyMinecraftJar()` is not recommended for Minecraft versions 1.3 or newer."); } @@ -83,7 +93,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { return; } - final Path inputJar = environment.getInputJar(this); + final Path inputJar = getInputJar(this); TinyRemapper remapper = null; @@ -100,7 +110,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { } } catch (Exception e) { Files.deleteIfExists(minecraftEnvOnlyJar); - throw new RuntimeException("Failed to process %s only jar".formatted(environment.type()), e); + throw new RuntimeException("Failed to process %s only jar".formatted(type()), e); } finally { if (remapper != null) { remapper.finish(); @@ -108,27 +118,24 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { } } - @Override - protected boolean provideClient() { - return environment instanceof Client; - } - - @Override - protected boolean provideServer() { - return environment instanceof Server; - } - public Path getMinecraftEnvOnlyJar() { return minecraftEnvOnlyJar; } - protected interface Environment { - SingleJarEnvType type(); - - Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; + @Override + public MappingsNamespace getOfficialNamespace() { + return officialNamespace; } - public static final class Server implements Environment { + abstract SingleJarEnvType type(); + + abstract Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; + + public static final class Server extends SingleJarMinecraftProvider { + private Server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { + super(metadataProvider, configContext, officialNamespace); + } + @Override public SingleJarEnvType type() { return SingleJarEnvType.SERVER; @@ -145,9 +152,23 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { provider.extractBundledServerJar(); return provider.getMinecraftExtractedServerJar().toPath(); } + + @Override + protected boolean provideServer() { + return true; + } + + @Override + protected boolean provideClient() { + return false; + } } - public static final class Client implements Environment { + public static final class Client extends SingleJarMinecraftProvider { + private Client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { + super(metadataProvider, configContext, officialNamespace); + } + @Override public SingleJarEnvType type() { return SingleJarEnvType.CLIENT; @@ -157,5 +178,15 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception { return provider.getMinecraftClientJar().toPath(); } + + @Override + protected boolean provideServer() { + return false; + } + + @Override + protected boolean provideClient() { + return true; + } } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java index f970aa16..d2190142 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; @@ -35,8 +36,8 @@ public final class SplitMinecraftProvider extends MinecraftProvider { private Path minecraftClientOnlyJar; private Path minecraftCommonJar; - public SplitMinecraftProvider(ConfigContext configContext) { - super(configContext); + public SplitMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } @Override @@ -52,6 +53,11 @@ public final class SplitMinecraftProvider extends MinecraftProvider { return List.of(minecraftClientOnlyJar, minecraftCommonJar); } + @Override + public MappingsNamespace getOfficialNamespace() { + return MappingsNamespace.OFFICIAL; + } + @Override public void provide() throws Exception { super.provide(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index 1e171f56..d860fb3c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -51,6 +51,7 @@ import net.fabricmc.loom.configuration.providers.mappings.TinyMappingsService; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.configuration.providers.minecraft.SignatureFixerApplyVisitor; import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.util.SidedClassVisitor; @@ -205,7 +206,10 @@ public abstract class AbstractMappedMinecraftProvider classNames = extension.isForgeLike() ? InnerClassRemapper.readClassNames(remappedJars.inputJar()) : Set.of(); final Map remappedSignatures = SignatureFixerApplyVisitor.getRemappedSignatures(getTargetNamespace() == MappingsNamespace.INTERMEDIARY, mappingConfiguration, getProject(), configContext.serviceManager(), toM); - TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, true, (builder) -> { + final MinecraftVersionMeta.JavaVersion javaVersion = minecraftProvider.getVersionInfo().javaVersion(); + final boolean fixRecords = javaVersion != null && javaVersion.majorVersion() >= 16; + + TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, fixRecords, (builder) -> { builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures)); configureRemapper(remappedJars, builder); }, classNames); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java index aefed08b..70a1424e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java @@ -29,14 +29,16 @@ import java.util.List; import org.gradle.api.Project; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; import net.fabricmc.tinyremapper.TinyRemapper; -public abstract sealed class IntermediaryMinecraftProvider extends AbstractMappedMinecraftProvider permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl { +public abstract sealed class IntermediaryMinecraftProvider extends AbstractMappedMinecraftProvider permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.LegacyMergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl { public IntermediaryMinecraftProvider(Project project, M minecraftProvider) { super(project, minecraftProvider); } @@ -59,11 +61,49 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace()) ); } } + public static final class LegacyMergedImpl extends IntermediaryMinecraftProvider implements Merged { + private final SingleJarImpl server; + private final SingleJarImpl client; + + public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) { + super(project, minecraftProvider); + server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER); + client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT); + } + + @Override + public List provide(ProvideContext context) throws Exception { + // Map the client and server jars separately + server.provide(context); + client.provide(context); + + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); + + return List.of(getMergedJar()); + } + + @Override + public List getRemappedJars() { + // The delegate providers will handle the remapping + throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars"); + } + + @Override + public List getDependencyTypes() { + return List.of(MinecraftJar.Type.MERGED); + } + } + public static final class SplitImpl extends IntermediaryMinecraftProvider implements Split { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { super(project, minecraftProvider); @@ -72,8 +112,8 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), - new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) + new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()), + new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar()) ); } @@ -102,7 +142,7 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace()) ); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java index 1bfdbe72..f2753609 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java @@ -29,9 +29,11 @@ import java.util.List; import org.gradle.api.Project; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; @@ -60,7 +62,7 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace()) ); } @@ -70,6 +72,55 @@ public abstract class NamedMinecraftProvider extend } } + public static final class LegacyMergedImpl extends NamedMinecraftProvider implements Merged { + private final SingleJarImpl server; + private final SingleJarImpl client; + + public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) { + super(project, minecraftProvider); + server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER); + client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT); + } + + @Override + public List provide(ProvideContext context) throws Exception { + final ProvideContext childContext = context.withApplyDependencies(false); + + // Map the client and server jars separately + server.provide(childContext); + client.provide(childContext); + + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); + + getMavenHelper(MinecraftJar.Type.MERGED).savePom(); + + if (context.applyDependencies()) { + MinecraftSourceSets.get(getProject()).applyDependencies( + (configuration, type) -> getProject().getDependencies().add(configuration, getDependencyNotation(type)), + getDependencyTypes() + ); + } + + return List.of(getMergedJar()); + } + + @Override + public List getRemappedJars() { + // The delegate providers will handle the remapping + throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars"); + } + + @Override + public List getDependencyTypes() { + return List.of(MinecraftJar.Type.MERGED); + } + } + public static final class SplitImpl extends NamedMinecraftProvider implements Split { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { super(project, minecraftProvider); @@ -78,8 +129,8 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), - new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) + new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()), + new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar()) ); } @@ -113,7 +164,7 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace()) ); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java index f5fcdca9..daacc701 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java @@ -37,6 +37,7 @@ import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper; import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ProcessorContextImpl; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; @@ -178,6 +179,17 @@ public abstract class ProcessedNamedMinecraftProvider implements Merged { + public LegacyMergedImpl(NamedMinecraftProvider.LegacyMergedImpl parentMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) { + super(parentMinecraftProvider, jarProcessorManager); + } + + @Override + public MinecraftJar getMergedJar() { + return getProcessedJar(getParentMinecraftProvider().getMergedJar()); + } + } + public static final class SplitImpl extends ProcessedNamedMinecraftProvider implements Split { public SplitImpl(NamedMinecraftProvider.SplitImpl parentMinecraftProvide, MinecraftJarProcessorManager jarProcessorManager) { super(parentMinecraftProvide, jarProcessorManager); diff --git a/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java b/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java new file mode 100644 index 00000000..2c872406 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java @@ -0,0 +1,158 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2019-2021 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.decompilers; + +import static java.text.MessageFormat.format; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public record ClassLineNumbers(Map lineMap) { + public ClassLineNumbers { + Objects.requireNonNull(lineMap, "lineMap"); + + if (lineMap.isEmpty()) { + throw new IllegalArgumentException("lineMap is empty"); + } + } + + public static ClassLineNumbers readMappings(Path lineMappingsPath) { + try (BufferedReader reader = Files.newBufferedReader(lineMappingsPath)) { + return readMappings(reader); + } catch (IOException e) { + throw new UncheckedIOException("Exception reading LineMappings file.", e); + } + } + + public static ClassLineNumbers readMappings(BufferedReader reader) { + var lineMap = new HashMap(); + + String line = null; + int lineNumber = 0; + + record CurrentClass(String className, int maxLine, int maxLineDest) { + void putEntry(Map entries, Map mappings) { + var entry = new ClassLineNumbers.Entry(className(), maxLine(), maxLineDest(), Collections.unmodifiableMap(mappings)); + + final ClassLineNumbers.Entry previous = entries.put(className(), entry); + + if (previous != null) { + throw new IllegalStateException("Duplicate class line mappings for " + className()); + } + } + } + + CurrentClass currentClass = null; + Map currentMappings = new HashMap<>(); + + try { + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) { + continue; + } + + final String[] segments = line.trim().split("\t"); + + if (line.charAt(0) != '\t') { + if (currentClass != null) { + currentClass.putEntry(lineMap, currentMappings); + currentMappings = new HashMap<>(); + } + + currentClass = new CurrentClass(segments[0], Integer.parseInt(segments[1]), Integer.parseInt(segments[2])); + } else { + Objects.requireNonNull(currentClass, "No class line mappings found for line " + lineNumber); + currentMappings.put(Integer.parseInt(segments[0]), Integer.parseInt(segments[1])); + } + + lineNumber++; + } + } catch (Exception e) { + throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", lineNumber, line), e); + } + + assert currentClass != null; + currentClass.putEntry(lineMap, currentMappings); + + return new ClassLineNumbers(Collections.unmodifiableMap(lineMap)); + } + + public void write(Writer writer) throws IOException { + for (Map.Entry entry : lineMap.entrySet()) { + entry.getValue().write(writer); + } + } + + /** + * Merge two ClassLineNumbers together, throwing an exception if there are any duplicate class line mappings. + */ + @Nullable + public static ClassLineNumbers merge(@Nullable ClassLineNumbers a, @Nullable ClassLineNumbers b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } + + var lineMap = new HashMap<>(a.lineMap()); + + for (Map.Entry entry : b.lineMap().entrySet()) { + lineMap.merge(entry.getKey(), entry.getValue(), (v1, v2) -> { + throw new IllegalStateException("Duplicate class line mappings for " + entry.getKey()); + }); + } + + return new ClassLineNumbers(Collections.unmodifiableMap(lineMap)); + } + + public record Entry(String className, int maxLine, int maxLineDest, Map lineMap) { + public void write(Writer writer) throws IOException { + writer.write(className); + writer.write('\t'); + writer.write(Integer.toString(maxLine)); + writer.write('\t'); + writer.write(Integer.toString(maxLineDest)); + writer.write('\n'); + + for (Map.Entry lineEntry : lineMap.entrySet()) { + writer.write('\t'); + writer.write(Integer.toString(lineEntry.getKey())); + writer.write('\t'); + writer.write(Integer.toString(lineEntry.getValue())); + writer.write('\n'); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java index 307a340e..9fefae44 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java +++ b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java @@ -24,93 +24,48 @@ package net.fabricmc.loom.decompilers; -import static java.text.MessageFormat.format; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; -import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.fabricmc.loom.util.AsyncZipProcessor; import net.fabricmc.loom.util.Constants; -import net.fabricmc.loom.util.IOStringConsumer; -/** - * Created by covers1624 on 18/02/19. - */ -public class LineNumberRemapper { - private final Map lineMap = new HashMap<>(); +public record LineNumberRemapper(ClassLineNumbers lineNumbers) { + private static final Logger LOGGER = LoggerFactory.getLogger(LineNumberRemapper.class); - public void readMappings(File lineMappings) { - try (BufferedReader reader = new BufferedReader(new FileReader(lineMappings))) { - RClass clazz = null; - String line = null; - int i = 0; + public void process(Path input, Path output) throws IOException { + AsyncZipProcessor.processEntries(input, output, new AsyncZipProcessor() { + private final Set createdParents = new HashSet<>(); - try { - while ((line = reader.readLine()) != null) { - if (line.isEmpty()) { - continue; - } - - String[] segs = line.trim().split("\t"); - - if (line.charAt(0) != '\t') { - clazz = lineMap.computeIfAbsent(segs[0], RClass::new); - clazz.maxLine = Integer.parseInt(segs[1]); - clazz.maxLineDest = Integer.parseInt(segs[2]); - } else { - clazz.lineMap.put(Integer.parseInt(segs[0]), Integer.parseInt(segs[1])); - } - - i++; - } - } catch (Exception e) { - throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", i, line), e); - } - } catch (IOException e) { - throw new RuntimeException("Exception reading LineMappings file.", e); - } - } - - public void process(IOStringConsumer logger, Path input, Path output) throws IOException { - Files.walkFileTree(input, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String rel = input.relativize(file).toString(); - Path dst = output.resolve(rel); + public void processEntryAsync(Path file, Path dst) throws IOException { Path parent = dst.getParent(); - if (parent != null) { - Files.createDirectories(parent); + synchronized (createdParents) { + if (parent != null && createdParents.add(parent)) { + Files.createDirectories(parent); + } } - String fName = file.getFileName().toString(); + String fileName = file.getFileName().toString(); - if (fName.endsWith(".class")) { - if (Files.exists(dst)) { - Files.delete(dst); - } + if (fileName.endsWith(".class")) { + String idx = fileName.substring(0, fileName.length() - 6); - String idx = rel.substring(0, rel.length() - 6); - - if (logger != null) { - logger.accept("Remapping " + idx); - } + LOGGER.debug("Remapping line numbers for class: " + idx); int dollarPos = idx.indexOf('$'); //This makes the assumption that only Java classes are to be remapped. @@ -118,30 +73,29 @@ public class LineNumberRemapper { idx = idx.substring(0, dollarPos); } - if (lineMap.containsKey(idx)) { + if (lineNumbers.lineMap().containsKey(idx)) { try (InputStream is = Files.newInputStream(file)) { ClassReader reader = new ClassReader(is); ClassWriter writer = new ClassWriter(0); - reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineMap.get(idx)), 0); + reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineNumbers.lineMap().get(idx)), 0); Files.write(dst, writer.toByteArray()); - return FileVisitResult.CONTINUE; + return; } } } Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING); - return FileVisitResult.CONTINUE; } }); } private static class LineNumberVisitor extends ClassVisitor { - private final RClass rClass; + private final ClassLineNumbers.Entry lineNumbers; - LineNumberVisitor(int api, ClassVisitor classVisitor, RClass rClass) { + LineNumberVisitor(int api, ClassVisitor classVisitor, ClassLineNumbers.Entry lineNumbers) { super(api, classVisitor); - this.rClass = rClass; + this.lineNumbers = lineNumbers; } @Override @@ -153,30 +107,19 @@ public class LineNumberRemapper { if (tLine <= 0) { super.visitLineNumber(line, start); - } else if (tLine >= rClass.maxLine) { - super.visitLineNumber(rClass.maxLineDest, start); + } else if (tLine >= lineNumbers.maxLine()) { + super.visitLineNumber(lineNumbers.maxLineDest(), start); } else { Integer matchedLine = null; - while (tLine <= rClass.maxLine && ((matchedLine = rClass.lineMap.get(tLine)) == null)) { + while (tLine <= lineNumbers.maxLine() && ((matchedLine = lineNumbers.lineMap().get(tLine)) == null)) { tLine++; } - super.visitLineNumber(matchedLine != null ? matchedLine : rClass.maxLineDest, start); + super.visitLineNumber(matchedLine != null ? matchedLine : lineNumbers.maxLineDest(), start); } } }; } } - - private static class RClass { - private final String name; - private int maxLine; - private int maxLineDest; - private final Map lineMap = new HashMap<>(); - - private RClass(String name) { - this.name = name; - } - } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java new file mode 100644 index 00000000..3674fe78 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java @@ -0,0 +1,209 @@ +/* + * 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.decompilers.cache; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.decompilers.ClassLineNumbers; + +// Serialised data for a class entry in the cache +// Uses the RIFF format, allows for appending the line numbers to the end of the file +// Stores the source code and line numbers for the class +public record CachedData(String className, String sources, @Nullable ClassLineNumbers.Entry lineNumbers) { + public static final CachedFileStore.EntrySerializer SERIALIZER = new EntrySerializer(); + + private static final String HEADER_ID = "LOOM"; + private static final String NAME_ID = "NAME"; + private static final String SOURCES_ID = "SRC "; + private static final String LINE_NUMBERS_ID = "LNUM"; + + private static final Logger LOGGER = LoggerFactory.getLogger(CachedData.class); + + public CachedData { + Objects.requireNonNull(className, "className"); + Objects.requireNonNull(sources, "sources"); + + if (lineNumbers != null) { + if (!className.equals(lineNumbers.className())) { + throw new IllegalArgumentException("Class name does not match line numbers class name"); + } + } + } + + public void write(FileChannel fileChannel) { + try (var c = new RiffChunk(HEADER_ID, fileChannel)) { + writeClassname(fileChannel); + writeSource(fileChannel); + + if (lineNumbers != null) { + writeLineNumbers(fileChannel); + } + } catch (IOException e) { + throw new RuntimeException("Failed to write cached data", e); + } + } + + private void writeClassname(FileChannel fileChannel) throws IOException { + try (var c = new RiffChunk(NAME_ID, fileChannel)) { + fileChannel.write(ByteBuffer.wrap(className.getBytes(StandardCharsets.UTF_8))); + } + } + + private void writeSource(FileChannel fileChannel) throws IOException { + try (var c = new RiffChunk(SOURCES_ID, fileChannel)) { + fileChannel.write(ByteBuffer.wrap(sources.getBytes(StandardCharsets.UTF_8))); + } + } + + private void writeLineNumbers(FileChannel fileChannel) throws IOException { + Objects.requireNonNull(lineNumbers); + + try (var c = new RiffChunk(LINE_NUMBERS_ID, fileChannel); + StringWriter stringWriter = new StringWriter()) { + lineNumbers.write(stringWriter); + fileChannel.write(ByteBuffer.wrap(stringWriter.toString().getBytes(StandardCharsets.UTF_8))); + } + } + + public static CachedData read(InputStream inputStream) throws IOException { + // Read and validate the RIFF header + final String header = readHeader(inputStream); + + if (!header.equals(HEADER_ID)) { + throw new IOException("Invalid RIFF header: " + header + ", expected " + HEADER_ID); + } + + // Read the data length + int length = readInt(inputStream); + + String className = null; + String sources = null; + ClassLineNumbers.Entry lineNumbers = null; + + while (inputStream.available() > 0) { + String chunkHeader = readHeader(inputStream); + int chunkLength = readInt(inputStream); + byte[] chunkData = readBytes(inputStream, chunkLength); + + switch (chunkHeader) { + case NAME_ID -> { + if (className != null) { + throw new IOException("Duplicate name chunk"); + } + + className = new String(chunkData, StandardCharsets.UTF_8); + } + case SOURCES_ID -> { + if (sources != null) { + throw new IOException("Duplicate sources chunk"); + } + + sources = new String(chunkData, StandardCharsets.UTF_8); + } + case LINE_NUMBERS_ID -> { + if (lineNumbers != null) { + throw new IOException("Duplicate line numbers chunk"); + } + + try (var br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(chunkData), StandardCharsets.UTF_8))) { + ClassLineNumbers classLineNumbers = ClassLineNumbers.readMappings(br); + + if (classLineNumbers.lineMap().size() != 1) { + throw new IOException("Expected exactly one class line numbers entry got " + classLineNumbers.lineMap().size() + " entries"); + } + + lineNumbers = classLineNumbers.lineMap().values().iterator().next(); + } + } + default -> { + // Skip unknown chunk + LOGGER.warn("Skipping unknown chunk: {} of size {}", chunkHeader, chunkLength); + inputStream.skip(chunkLength); + } + } + } + + if (sources == null) { + throw new IOException("Missing sources"); + } + + return new CachedData(className, sources, lineNumbers); + } + + private static String readHeader(InputStream inputStream) throws IOException { + byte[] header = readBytes(inputStream, 4); + return new String(header, StandardCharsets.US_ASCII); + } + + private static int readInt(InputStream inputStream) throws IOException { + byte[] bytes = readBytes(inputStream, 4); + return ByteBuffer.wrap(bytes).getInt(); + } + + private static byte[] readBytes(InputStream inputStream, int length) throws IOException { + byte[] bytes = new byte[length]; + + int read = inputStream.read(bytes); + + if (read != length) { + throw new IOException("Failed to read bytes expected " + length + " bytes but got " + read + " bytes"); + } + + return bytes; + } + + static class EntrySerializer implements CachedFileStore.EntrySerializer { + @Override + public CachedData read(Path path) throws IOException { + try (var inputStream = new BufferedInputStream(Files.newInputStream(path))) { + return CachedData.read(inputStream); + } + } + + @Override + public void write(CachedData entry, Path path) throws IOException { + try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + entry.write(fileChannel); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java new file mode 100644 index 00000000..ad73ec4c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java @@ -0,0 +1,42 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.file.Path; + +import org.jetbrains.annotations.Nullable; + +public interface CachedFileStore { + @Nullable T getEntry(String key) throws IOException; + + void putEntry(String key, T entry) throws IOException; + + interface EntrySerializer { + T read(Path path) throws IOException; + + void write(T entry, Path path) throws IOException; + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java new file mode 100644 index 00000000..545c6b7e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java @@ -0,0 +1,141 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +public record CachedFileStoreImpl(Path root, EntrySerializer entrySerializer, CacheRules cacheRules) implements CachedFileStore { + public CachedFileStoreImpl { + Objects.requireNonNull(root, "root"); + } + + @Override + public @Nullable T getEntry(String key) throws IOException { + Path path = resolve(key); + + if (Files.notExists(path)) { + return null; + } + + // Update last modified, so recently used files stay in the cache + Files.setLastModifiedTime(path, FileTime.from(Instant.now())); + return entrySerializer.read(path); + } + + @Override + public void putEntry(String key, T data) throws IOException { + Path path = resolve(key); + Files.createDirectories(path.getParent()); + entrySerializer.write(data, path); + } + + private Path resolve(String key) { + return root.resolve(key); + } + + public void prune() throws IOException { + // Sorted oldest -> newest + List entries = new ArrayList<>(); + + // Iterate over all the files in the cache, and store them into the sorted list. + try (Stream walk = Files.walk(root)) { + Iterator iterator = walk.iterator(); + + while (iterator.hasNext()) { + final Path entry = iterator.next(); + + if (!Files.isRegularFile(entry)) { + continue; + } + + insertSorted(entries, new PathEntry(entry)); + } + } + + // Delete the oldest files to get under the max file limit + if (entries.size() > cacheRules.maxFiles) { + for (int i = 0; i < cacheRules.maxFiles; i++) { + PathEntry toRemove = entries.remove(0); + Files.delete(toRemove.path); + } + } + + final Instant maxAge = Instant.now().minus(cacheRules().maxAge()); + Iterator iterator = entries.iterator(); + + while (iterator.hasNext()) { + final PathEntry entry = iterator.next(); + + if (entry.lastModified().toInstant().isAfter(maxAge)) { + // File is not longer than the max age + // As this is a sorted list we don't need to keep checking + break; + } + + // Remove all files over the max age + iterator.remove(); + Files.delete(entry.path); + } + } + + private void insertSorted(List list, PathEntry entry) { + int index = Collections.binarySearch(list, entry, Comparator.comparing(PathEntry::lastModified)); + + if (index < 0) { + index = -index - 1; + } + + list.add(index, entry); + } + + /** + * The rules for the cache. + * + * @param maxFiles The maximum number of files in the cache + * @param maxAge The maximum age of a file in the cache + */ + public record CacheRules(long maxFiles, Duration maxAge) { + } + + record PathEntry(Path path, FileTime lastModified) { + PathEntry(Path path) throws IOException { + this(path, Files.getLastModifiedTime(path)); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java new file mode 100644 index 00000000..82952210 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java @@ -0,0 +1,268 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.decompilers.ClassLineNumbers; +import net.fabricmc.loom.util.FileSystemUtil; + +public record CachedJarProcessor(CachedFileStore fileStore, String baseHash) { + private static final Logger LOGGER = LoggerFactory.getLogger(CachedJarProcessor.class); + + public WorkRequest prepareJob(Path inputJar) throws IOException { + boolean isIncomplete = false; + boolean hasSomeExisting = false; + + Path incompleteJar = Files.createTempFile("loom-cache-incomplete", ".jar"); + Path existingJar = Files.createTempFile("loom-cache-existing", ".jar"); + + // We must delete the empty files, so they can be created as a zip + Files.delete(incompleteJar); + Files.delete(existingJar); + + // Sources name -> hash + Map outputNameMap = new HashMap<>(); + Map lineNumbersMap = new HashMap<>(); + + int hits = 0; + int misses = 0; + + try (FileSystemUtil.Delegate inputFs = FileSystemUtil.getJarFileSystem(inputJar, false); + FileSystemUtil.Delegate incompleteFs = FileSystemUtil.getJarFileSystem(incompleteJar, true); + FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(existingJar, true)) { + final List inputClasses = JarWalker.findClasses(inputFs); + + for (ClassEntry entry : inputClasses) { + String outputFileName = entry.sourcesFileName(); + String fullHash = baseHash + "/" + entry.hash(inputFs.getRoot()); + + final CachedData entryData = fileStore.getEntry(fullHash); + + if (entryData == null) { + // Cached entry was not found, so copy the input to the incomplete jar to be processed + entry.copyTo(inputFs.getRoot(), incompleteFs.getRoot()); + isIncomplete = true; + outputNameMap.put(outputFileName, fullHash); + + LOGGER.debug("Cached entry ({}) not found, going to process {}", fullHash, outputFileName); + misses++; + } else { + final Path outputPath = existingFs.getPath(outputFileName); + Files.createDirectories(outputPath.getParent()); + Files.writeString(outputPath, entryData.sources()); + lineNumbersMap.put(entryData.className(), entryData.lineNumbers()); + hasSomeExisting = true; + + LOGGER.debug("Cached entry ({}) found: {}", fullHash, outputFileName); + hits++; + } + } + } + + // A jar file that will be created by the work action, containing the newly processed items. + Path outputJar = Files.createTempFile("loom-cache-output", ".jar"); + Files.delete(outputJar); + + final ClassLineNumbers lineNumbers = lineNumbersMap.isEmpty() ? null : new ClassLineNumbers(Collections.unmodifiableMap(lineNumbersMap)); + final var stats = new CacheStats(hits, misses); + + if (isIncomplete && !hasSomeExisting) { + // The cache contained nothing of use, fully process the input jar + Files.delete(incompleteJar); + Files.delete(existingJar); + + LOGGER.info("No cached entries found, going to process the whole jar"); + return new FullWorkJob(inputJar, outputJar, outputNameMap) + .asRequest(stats, lineNumbers); + } 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) + .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) + .asRequest(stats, lineNumbers); + } + } + + public void completeJob(Path output, WorkJob workJob, ClassLineNumbers lineNumbers) throws IOException { + if (workJob instanceof CompletedWorkJob completedWorkJob) { + // Fully complete, nothing new to cache + Files.move(completedWorkJob.completed(), output); + return; + } + + // Work has been done, we need to cache the newly processed items + if (workJob instanceof WorkToDoJob workToDoJob) { + // Sources name -> hash + Map outputNameMap = workToDoJob.outputNameMap(); + + try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(workToDoJob.output(), false); + Stream walk = Files.walk(outputFs.getRoot())) { + Iterator iterator = walk.iterator(); + + while (iterator.hasNext()) { + final Path fsPath = iterator.next(); + + if (fsPath.startsWith("/META-INF/")) { + continue; + } + + if (!Files.isRegularFile(fsPath)) { + continue; + } + + final String hash = outputNameMap.get(fsPath.toString().substring(outputFs.getRoot().toString().length())); + + if (hash == null) { + throw new IllegalStateException("Unexpected output: " + fsPath); + } + + // Trim the leading / and the .java extension + final String className = fsPath.toString().substring(1, fsPath.toString().length() - ".java".length()); + final String sources = Files.readString(fsPath); + + ClassLineNumbers.Entry lineMapEntry = null; + + if (lineNumbers != null) { + lineMapEntry = lineNumbers.lineMap().get(className); + } + + final var cachedData = new CachedData(className, sources, lineMapEntry); + fileStore.putEntry(hash, cachedData); + + LOGGER.debug("Saving processed entry ({}) to cache: {}", hash, fsPath); + } + } + } else { + throw new IllegalStateException(); + } + + 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); + Stream walk = Files.walk(existingFs.getRoot())) { + Iterator iterator = walk.iterator(); + + while (iterator.hasNext()) { + Path existingPath = iterator.next(); + + if (!Files.isRegularFile(existingPath)) { + continue; + } + + final Path outputPath = outputFs.getRoot().resolve(existingPath.toString()); + + LOGGER.debug("Copying existing entry to output: {}", existingPath); + Files.createDirectories(outputPath.getParent()); + Files.copy(existingPath, outputPath); + } + } + + Files.delete(partialWorkJob.existing()); + Files.move(partialWorkJob.output(), output); + } else if (workJob instanceof FullWorkJob fullWorkJob) { + // Nothing to merge, just use the output jar + Files.move(fullWorkJob.output, output); + } else { + throw new IllegalStateException(); + } + } + + public record WorkRequest(WorkJob job, CacheStats stats, @Nullable ClassLineNumbers lineNumbers) { + } + + public record CacheStats(int hits, int misses) { + } + + public sealed interface WorkJob permits CompletedWorkJob, WorkToDoJob { + default WorkRequest asRequest(CacheStats stats, @Nullable ClassLineNumbers lineNumbers) { + return new WorkRequest(this, stats, lineNumbers); + } + } + + public sealed interface WorkToDoJob extends WorkJob permits PartialWorkJob, FullWorkJob { + /** + * A path to jar file containing all the classes to be processed. + */ + Path incomplete(); + + /** + * @return A jar file to be written to during processing + */ + Path output(); + + /** + * @return A map of sources name to hash + */ + Map outputNameMap(); + } + + /** + * No work to be done, all restored from cache. + * + * @param completed + */ + public record CompletedWorkJob(Path completed) implements WorkJob { + } + + /** + * 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 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 { + } + + /** + * The full jar must be processed. + * + * @param incomplete A path to jar file containing all the classes to be 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 FullWorkJob(Path incomplete, Path output, Map outputNameMap) implements WorkToDoJob { + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java new file mode 100644 index 00000000..f01db22f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java @@ -0,0 +1,75 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.StringJoiner; + +import net.fabricmc.loom.util.Checksum; + +public record ClassEntry(String parentClass, List innerClasses) { + /** + * Copy the class and its inner classes to the target root. + * @param sourceRoot The root of the source jar + * @param targetRoot The root of the target jar + * + * @throws IOException If an error occurs while copying the files + */ + public void copyTo(Path sourceRoot, Path targetRoot) throws IOException { + Path targetPath = targetRoot.resolve(parentClass); + Files.createDirectories(targetPath.getParent()); + Files.copy(sourceRoot.resolve(parentClass), targetPath); + + for (String innerClass : innerClasses) { + Files.copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass)); + } + } + + /** + * Hash the class and its inner classes using sha256. + * @param root The root of the jar + * @return The hash of the class and its inner classes + * + * @throws IOException If an error occurs while hashing the files + */ + public String hash(Path root) throws IOException { + StringJoiner joiner = new StringJoiner(","); + + joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(parentClass)))); + + for (String innerClass : innerClasses) { + joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(innerClass)))); + } + + return Checksum.sha256Hex(joiner.toString().getBytes()); + } + + public String sourcesFileName() { + return parentClass.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 new file mode 100644 index 00000000..ab2f9924 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java @@ -0,0 +1,108 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.FileSystemUtil; + +public final class JarWalker { + private static final Logger LOGGER = LoggerFactory.getLogger(JarWalker.class); + + private JarWalker() { + } + + public static List findClasses(Path jar) throws IOException { + try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) { + return findClasses(fs); + } + } + + public static List findClasses(FileSystemUtil.Delegate fs) throws IOException { + List outerClasses = new ArrayList<>(); + Map> innerClasses = new HashMap<>(); + + // Iterate over all the classes in the jar, and store them into the sorted list. + try (Stream walk = Files.walk(fs.getRoot())) { + Iterator iterator = walk.iterator(); + + while (iterator.hasNext()) { + final Path entry = iterator.next(); + + if (!Files.isRegularFile(entry)) { + continue; + } + + final String fileName = entry.toString().substring(fs.getRoot().toString().length()); + + if (!fileName.endsWith(".class")) { + continue; + } + + boolean isInnerClass = fileName.contains("$"); + + if (isInnerClass) { + String outerClassName = fileName.substring(0, fileName.indexOf('$')) + ".class"; + innerClasses.computeIfAbsent(outerClassName, k -> new ArrayList<>()).add(fileName); + } else { + outerClasses.add(fileName); + } + } + } + + LOGGER.info("Found {} outer classes and {} inner classes", outerClasses.size(), innerClasses.size()); + + Collections.sort(outerClasses); + + List classEntries = new ArrayList<>(); + + for (String outerClass : outerClasses) { + List innerClasList = innerClasses.get(outerClass); + + if (innerClasList == null) { + innerClasList = Collections.emptyList(); + } else { + Collections.sort(innerClasList); + } + + ClassEntry classEntry = new ClassEntry(outerClass, Collections.unmodifiableList(innerClasList)); + classEntries.add(classEntry); + } + + return Collections.unmodifiableList(classEntries); + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java b/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java new file mode 100644 index 00000000..73c8afae --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java @@ -0,0 +1,69 @@ +/* + * 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.decompilers.cache; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; + +/** + * Write a RIFF chunk to a file channel + * + *

Works by writing the chunk header and then reserving space for the chunk size. + * The chunk size is then written after the chunk data has been written. + */ +public class RiffChunk implements AutoCloseable { + private final long position; + private final FileChannel fileChannel; + + public RiffChunk(String id, FileChannel fileChannel) throws IOException { + if (id.length() != 4) { + throw new IllegalArgumentException("ID must be 4 characters long"); + } + + // Write the chunk header and reserve space for the chunk size + fileChannel.write(ByteBuffer.wrap(id.getBytes(StandardCharsets.US_ASCII))); + this.position = fileChannel.position(); + fileChannel.write(ByteBuffer.allocate(4)); + + // Store the position and file channel for later use + this.fileChannel = fileChannel; + } + + @Override + public void close() throws IOException { + long endPosition = fileChannel.position(); + long chunkSize = endPosition - position - 4; + + if (chunkSize > Integer.MAX_VALUE) { + throw new IOException("Chunk size is too large"); + } + + fileChannel.position(position); + fileChannel.write(ByteBuffer.allocate(Integer.BYTES).putInt((int) (chunkSize)).flip()); + fileChannel.position(endPosition); + } +} diff --git a/src/main/java/net/fabricmc/loom/extension/LoomFiles.java b/src/main/java/net/fabricmc/loom/extension/LoomFiles.java index 8b7a3dda..cd4fab5b 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomFiles.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomFiles.java @@ -50,5 +50,6 @@ public interface LoomFiles { File getRemapClasspathFile(); File getGlobalMinecraftRepo(); File getLocalMinecraftRepo(); + File getDecompileCache(String version); File getForgeDependencyRepo(); } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java index 24141908..d9704772 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java @@ -108,6 +108,11 @@ public abstract class LoomFilesBaseImpl implements LoomFiles { return new File(getRootProjectPersistentCache(), "minecraftMaven"); } + @Override + public File getDecompileCache(String version) { + return new File(getUserCache(), "decompile/" + version + ".zip"); + } + @Override public File getForgeDependencyRepo() { return new File(getUserCache(), "forge/transformed-dependencies-v1"); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index 3bee12f5..85ec3c18 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -44,6 +44,7 @@ import org.gradle.api.Project; import org.gradle.api.UncheckedIOException; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; @@ -63,6 +64,7 @@ import net.fabricmc.loom.api.NeoForgeExtensionAPI; import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.api.decompilers.DecompilerOptions; import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.api.mappings.layered.spec.LayeredMappingSpecBuilder; import net.fabricmc.loom.api.processor.MinecraftJarProcessor; import net.fabricmc.loom.api.remapping.RemapperExtension; @@ -103,7 +105,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final Property intermediateMappingsProvider; private final Property runtimeOnlyLog4j; private final Property splitModDependencies; - private final Property minecraftJarConfiguration; + private final Property> minecraftJarConfiguration; private final Property splitEnvironmentalSourceSet; private final InterfaceInjectionExtensionAPI interfaceInjectionExtension; @@ -165,7 +167,8 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA this.minecraftJarProcessors = (ListProperty>) (Object) project.getObjects().listProperty(MinecraftJarProcessor.class); this.minecraftJarProcessors.finalizeValueOnRead(); - this.minecraftJarConfiguration = project.getObjects().property(MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED); + //noinspection unchecked + this.minecraftJarConfiguration = project.getObjects().property((Class>) (Class) MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED); this.minecraftJarConfiguration.finalizeValueOnRead(); this.accessWidener.finalizeValueOnRead(); @@ -384,7 +387,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA } @Override - public Property getMinecraftJarConfiguration() { + public Property> getMinecraftJarConfiguration() { return minecraftJarConfiguration; } @@ -477,6 +480,13 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA return getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftProvider().minecraftVersion()); } + @Override + public FileCollection getNamedMinecraftJars() { + final ConfigurableFileCollection jars = getProject().getObjects().fileCollection(); + jars.from(getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftJars(MappingsNamespace.NAMED))); + return jars; + } + @Override public void silentMojangMappingsLicense() { this.silentMojangMappingsLicense = true; diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index e25f028b..44ab21d0 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -53,6 +53,7 @@ import net.fabricmc.loom.configuration.providers.forge.ForgeRunsProvider; import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; +import net.fabricmc.loom.configuration.providers.mappings.NoOpIntermediateMappingsProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessorManager; import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider; @@ -186,6 +187,11 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen this.intermediaryMinecraftProvider = intermediaryMinecraftProvider; } + @Override + public void noIntermediateMappings() { + setIntermediateMappingsProvider(NoOpIntermediateMappingsProvider.class, p -> { }); + } + @Override public SrgMinecraftProvider getSrgMinecraftProvider() { return Objects.requireNonNull(srgMinecraftProvider, "Cannot get SrgMinecraftProvider before it has been setup"); @@ -312,6 +318,9 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen provider.getDownloader().set(this::download); provider.getDownloader().disallowChanges(); + + provider.getIsLegacyMinecraft().set(getProject().provider(() -> getMinecraftProvider().isLegacyVersion())); + provider.getIsLegacyMinecraft().disallowChanges(); } @Override diff --git a/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java b/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java deleted file mode 100644 index 1dea4eda..00000000 --- a/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 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.kotlin.remapping; - -import java.util.List; - -import kotlinx.metadata.KmAnnotation; -import kotlinx.metadata.KmProperty; -import kotlinx.metadata.internal.extensions.KmClassExtension; -import kotlinx.metadata.internal.extensions.KmConstructorExtension; -import kotlinx.metadata.internal.extensions.KmFunctionExtension; -import kotlinx.metadata.internal.extensions.KmPackageExtension; -import kotlinx.metadata.internal.extensions.KmPropertyExtension; -import kotlinx.metadata.internal.extensions.KmTypeExtension; -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension; -import kotlinx.metadata.jvm.JvmFieldSignature; -import kotlinx.metadata.jvm.JvmMethodSignature; -import kotlinx.metadata.jvm.internal.JvmClassExtension; -import kotlinx.metadata.jvm.internal.JvmConstructorExtension; -import kotlinx.metadata.jvm.internal.JvmFunctionExtension; -import kotlinx.metadata.jvm.internal.JvmPackageExtension; -import kotlinx.metadata.jvm.internal.JvmPropertyExtension; -import kotlinx.metadata.jvm.internal.JvmTypeExtension; -import kotlinx.metadata.jvm.internal.JvmTypeParameterExtension; -import org.jetbrains.annotations.Nullable; - -/* - * This is a fun meme. All of these kotlin classes are marked as "internal" so Kotlin code cannot compile against them. - * However, luckily for us the Java compiler has no idea about this, so they can compile against it :D - * - * This file contains Java wrappers around Kotlin classes, to used by Kotlin. - */ -public interface JvmExtensionWrapper { - record Class(JvmClassExtension extension) implements JvmExtensionWrapper { - @Nullable - public static Class get(KmClassExtension classExtension) { - if (classExtension instanceof JvmClassExtension jvmClassExtension) { - return new Class(jvmClassExtension); - } - - return null; - } - - public List getLocalDelegatedProperties() { - return extension.getLocalDelegatedProperties(); - } - - @Nullable - public String getModuleName() { - return extension.getModuleName(); - } - - public void setModuleName(@Nullable String name) { - extension.setModuleName(name); - } - - @Nullable - public String getAnonymousObjectOriginName() { - return extension.getAnonymousObjectOriginName(); - } - - public void setAnonymousObjectOriginName(@Nullable String name) { - extension.setAnonymousObjectOriginName(name); - } - - public int getJvmFlags() { - return extension.getJvmFlags(); - } - - public void setJvmFlags(int flags) { - extension.setJvmFlags(flags); - } - } - - record Package(JvmPackageExtension extension) { - @Nullable - public static Package get(KmPackageExtension packageExtension) { - if (packageExtension instanceof JvmPackageExtension jvmPackageExtension) { - return new Package(jvmPackageExtension); - } - - return null; - } - - public List getLocalDelegatedProperties() { - return extension.getLocalDelegatedProperties(); - } - - @Nullable - public String getModuleName() { - return extension.getModuleName(); - } - - public void setModuleName(@Nullable String name) { - extension.setModuleName(name); - } - } - - record Function(JvmFunctionExtension extension) { - @Nullable - public static Function get(KmFunctionExtension functionExtension) { - if (functionExtension instanceof JvmFunctionExtension jvmFunctionExtension) { - return new Function(jvmFunctionExtension); - } - - return null; - } - - @Nullable - public JvmMethodSignature getSignature() { - return extension.getSignature(); - } - - public void setSignature(@Nullable JvmMethodSignature signature) { - extension.setSignature(signature); - } - - @Nullable - public String getLambdaClassOriginName() { - return extension.getLambdaClassOriginName(); - } - - public void setLambdaClassOriginName(@Nullable String name) { - extension.setLambdaClassOriginName(name); - } - } - - record Property(JvmPropertyExtension extension) { - @Nullable - public static Property get(KmPropertyExtension propertyExtension) { - if (propertyExtension instanceof JvmPropertyExtension jvmPropertyExtension) { - return new Property(jvmPropertyExtension); - } - - return null; - } - - public int getJvmFlags() { - return extension.getJvmFlags(); - } - - public void setJvmFlags(int flags) { - extension.setJvmFlags(flags); - } - - @Nullable - public JvmFieldSignature getFieldSignature() { - return extension.getFieldSignature(); - } - - public void setFieldSignature(@Nullable JvmFieldSignature signature) { - extension.setFieldSignature(signature); - } - - @Nullable - public JvmMethodSignature getGetterSignature() { - return extension.getGetterSignature(); - } - - public void setGetterSignature(@Nullable JvmMethodSignature signature) { - extension.setGetterSignature(signature); - } - - @Nullable - public JvmMethodSignature getSetterSignature() { - return extension.getSetterSignature(); - } - - public void setSetterSignature(@Nullable JvmMethodSignature signature) { - extension.setSetterSignature(signature); - } - - @Nullable - public JvmMethodSignature getSyntheticMethodForAnnotations() { - return extension.getSyntheticMethodForAnnotations(); - } - - public void setSyntheticMethodForAnnotations(@Nullable JvmMethodSignature signature) { - extension.setSyntheticMethodForAnnotations(signature); - } - - @Nullable - public JvmMethodSignature getSyntheticMethodForDelegate() { - return extension.getSyntheticMethodForDelegate(); - } - - public void setSyntheticMethodForDelegate(@Nullable JvmMethodSignature signature) { - extension.setSyntheticMethodForDelegate(signature); - } - } - - record Constructor(JvmConstructorExtension extension) { - @Nullable - public static Constructor get(KmConstructorExtension constructorExtension) { - if (constructorExtension instanceof JvmConstructorExtension jvmConstructorExtension) { - return new Constructor(jvmConstructorExtension); - } - - return null; - } - - @Nullable - public JvmMethodSignature getSignature() { - return extension.getSignature(); - } - - public void setSignature(@Nullable JvmMethodSignature signature) { - extension.setSignature(signature); - } - } - - record TypeParameter(JvmTypeParameterExtension extension) { - @Nullable - public static TypeParameter get(KmTypeParameterExtension typeParameterExtension) { - if (typeParameterExtension instanceof JvmTypeParameterExtension jvmTypeParameterExtension) { - return new TypeParameter(jvmTypeParameterExtension); - } - - return null; - } - - public List getAnnotations() { - return extension.getAnnotations(); - } - } - - record Type(JvmTypeExtension extension) { - @Nullable - public static Type get(KmTypeExtension typeExtension) { - if (typeExtension instanceof JvmTypeExtension jvmTypeExtension) { - return new Type(jvmTypeExtension); - } - - return null; - } - - public boolean isRaw() { - return extension.isRaw(); - } - - public void setRaw(boolean raw) { - extension.setRaw(raw); - } - - public List getAnnotations() { - return extension.getAnnotations(); - } - } -} diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java index 1fc25bbf..dca2b9f7 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java @@ -40,6 +40,7 @@ import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.services.ServiceReference; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.JavaExec; @@ -52,7 +53,7 @@ import net.fabricmc.loom.util.gradle.SyncTaskBuildService; public abstract class AbstractRunTask extends JavaExec { private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder(); - private final RunConfig config; + private final Provider config; // We control the classpath, as we use a ArgFile to pass it over the command line: https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection(); @@ -63,13 +64,12 @@ public abstract class AbstractRunTask extends JavaExec { public AbstractRunTask(Function configProvider) { super(); setGroup(Constants.TaskGroup.FABRIC); - this.config = configProvider.apply(getProject()); - setClasspath(config.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter())); - - args(config.programArgs); - getMainClass().set(config.mainClass); + this.config = getProject().provider(() -> configProvider.apply(getProject())); + classpath.from(config.map(runConfig -> runConfig.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter()))); + getArgumentProviders().add(() -> config.get().programArgs); + getMainClass().set(config.map(runConfig -> runConfig.mainClass)); getJvmArguments().addAll(getProject().provider(this::getGameJvmArgs)); } @@ -100,8 +100,8 @@ public abstract class AbstractRunTask extends JavaExec { super.setClasspath(classpath); } - setWorkingDir(new File(getProject().getProjectDir(), config.runDir)); - environment(config.environmentVariables); + setWorkingDir(new File(getProject().getProjectDir(), config.get().runDir)); + environment(config.get().environmentVariables); super.exec(); } @@ -133,7 +133,7 @@ public abstract class AbstractRunTask extends JavaExec { } } - args.addAll(config.vmArgs); + args.addAll(config.get().vmArgs); return args; } @@ -204,11 +204,11 @@ public abstract class AbstractRunTask extends JavaExec { @Override public boolean isSatisfiedBy(File element) { if (excludedLibraryPaths == null) { - excludedLibraryPaths = config.getExcludedLibraryPaths(getProject()); + excludedLibraryPaths = config.get().getExcludedLibraryPaths(getProject()); } if (excludedLibraryPaths.contains(element.getAbsolutePath())) { - getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.configName); + getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.get().configName); return false; } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 4cef8192..6071cad4 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -24,6 +24,7 @@ package net.fabricmc.loom.task; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -36,11 +37,15 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; +import java.util.StringJoiner; import java.util.UUID; import java.util.stream.Collectors; @@ -54,9 +59,11 @@ import org.gradle.api.services.ServiceReference; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; import org.gradle.process.ExecOperations; import org.gradle.process.ExecResult; import org.gradle.work.DisableCachingByDefault; @@ -65,8 +72,12 @@ import org.gradle.workers.WorkParameters; import org.gradle.workers.WorkQueue; import org.gradle.workers.WorkerExecutor; import org.gradle.workers.internal.WorkerDaemonClientsManager; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.DecompilerOptions; import net.fabricmc.loom.api.decompilers.LoomDecompiler; @@ -77,9 +88,14 @@ import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; +import net.fabricmc.loom.decompilers.ClassLineNumbers; import net.fabricmc.loom.decompilers.LineNumberRemapper; +import net.fabricmc.loom.decompilers.cache.CachedData; +import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl; +import net.fabricmc.loom.decompilers.cache.CachedJarProcessor; import net.fabricmc.loom.decompilers.linemap.LineMapClassFilter; import net.fabricmc.loom.decompilers.linemap.LineMapVisitor; +import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.FileSystemUtil; @@ -99,6 +115,8 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; @DisableCachingByDefault public abstract class GenerateSourcesTask extends AbstractLoomTask { + private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSourcesTask.class); + private static final String CACHE_VERSION = "v1"; private final DecompilerOptions decompilerOptions; /** @@ -126,10 +144,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { @Optional public abstract ConfigurableFileCollection getUnpickClasspath(); + @InputFiles + @Optional + @ApiStatus.Internal + public abstract ConfigurableFileCollection getUnpickRuntimeClasspath(); + @OutputFile @Optional public abstract RegularFileProperty getUnpickOutputJar(); + @Input + @Option(option = "use-cache", description = "Use the decompile cache") + @ApiStatus.Experimental + public abstract Property getUseCache(); + + // Internal outputs + @ApiStatus.Internal + @Internal + protected abstract RegularFileProperty getDecompileCacheFile(); + // Injects @Inject public abstract WorkerExecutor getWorkerExecutor(); @@ -151,6 +184,12 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { getOutputs().upToDateWhen((o) -> false); getClasspath().from(decompilerOptions.getClasspath()).finalizeValueOnRead(); dependsOn(decompilerOptions.getClasspath().getBuiltBy()); + + LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + getDecompileCacheFile().set(extension.getFiles().getDecompileCache(CACHE_VERSION)); + getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); + + getUseCache().convention(true); } @TaskAction @@ -161,21 +200,197 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements."); } + if (!getUseCache().get()) { + try (var timer = new Timer("Decompiled sources")) { + runWithoutCache(); + } + + return; + } + + LOGGER.info("Using decompile cache."); + + try (var timer = new Timer("Decompiled sources with cache")) { + final Path cacheFile = getDecompileCacheFile().getAsFile().get().toPath(); + + // 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()); + + try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(cacheFile, true)) { + runWithCache(fs.getRoot()); + } + } + } + + private void runWithCache(Path cacheRoot) throws IOException { final MinecraftJar minecraftJar = rebuildInputJar(); - // Input jar is the jar to decompile, this may be unpicked. + final var cacheRules = new CachedFileStoreImpl.CacheRules(50_000, Duration.ofDays(90)); + final var decompileCache = new CachedFileStoreImpl<>(cacheRoot, CachedData.SERIALIZER, cacheRules); + final String cacheKey = getCacheKey(); + final CachedJarProcessor cachedJarProcessor = new CachedJarProcessor(decompileCache, cacheKey); + final CachedJarProcessor.WorkRequest workRequest; + + LOGGER.info("Decompile cache key: {}", cacheKey); + + try (var timer = new Timer("Prepare job")) { + workRequest = cachedJarProcessor.prepareJob(minecraftJar.getPath()); + } + + final CachedJarProcessor.WorkJob job = workRequest.job(); + final CachedJarProcessor.CacheStats cacheStats = workRequest.stats(); + + getProject().getLogger().lifecycle("Decompile cache stats: {} hits, {} misses", cacheStats.hits(), cacheStats.misses()); + + ClassLineNumbers outputLineNumbers = null; + + if (job instanceof CachedJarProcessor.WorkToDoJob workToDoJob) { + Path inputJar = workToDoJob.incomplete(); + @Nullable Path existing = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existing() : null; + + if (getUnpickDefinitions().isPresent()) { + try (var timer = new Timer("Unpick")) { + inputJar = unpickJar(inputJar, existing); + } + } + + try (var timer = new Timer("Decompile")) { + outputLineNumbers = runDecompileJob(inputJar, workToDoJob.output(), existing); + outputLineNumbers = filterForgeLineNumbers(outputLineNumbers); + } + + if (Files.notExists(workToDoJob.output())) { + throw new RuntimeException("Failed to decompile sources"); + } + } else if (job instanceof CachedJarProcessor.CompletedWorkJob completedWorkJob) { + // Nothing to do :) + } + + // The final output sources jar + final Path sourcesJar = getOutputJar().get().getAsFile().toPath(); + Files.deleteIfExists(sourcesJar); + + try (var timer = new Timer("Complete job")) { + cachedJarProcessor.completeJob(sourcesJar, job, outputLineNumbers); + } + + // This is the minecraft jar used at runtime. + final Path classesJar = minecraftJar.getPath(); + + // Remap the line numbers with the new and existing numbers + final ClassLineNumbers existingLinenumbers = workRequest.lineNumbers(); + final ClassLineNumbers lineNumbers = ClassLineNumbers.merge(existingLinenumbers, outputLineNumbers); + + if (lineNumbers == null) { + LOGGER.info("No line numbers to remap, skipping remapping"); + return; + } + + Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar"); + Files.delete(tempJar); + + try (var timer = new Timer("Remap line numbers")) { + remapLineNumbers(lineNumbers, classesJar, tempJar); + } + + Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING); + + try (var timer = new Timer("Prune cache")) { + decompileCache.prune(); + } + } + + private void runWithoutCache() throws IOException { + final MinecraftJar minecraftJar = rebuildInputJar(); + Path inputJar = minecraftJar.getPath(); - // Runtime jar is the jar used to run the game - final Path runtimeJar = inputJar; + // The final output sources jar + final Path sourcesJar = getOutputJar().get().getAsFile().toPath(); if (getUnpickDefinitions().isPresent()) { - inputJar = unpickJar(inputJar); + try (var timer = new Timer("Unpick")) { + inputJar = unpickJar(inputJar, null); + } } + ClassLineNumbers lineNumbers; + + try (var timer = new Timer("Decompile")) { + lineNumbers = runDecompileJob(inputJar, sourcesJar, null); + lineNumbers = filterForgeLineNumbers(lineNumbers); + } + + if (Files.notExists(sourcesJar)) { + throw new RuntimeException("Failed to decompile sources"); + } + + if (lineNumbers == null) { + LOGGER.info("No line numbers to remap, skipping remapping"); + return; + } + + // This is the minecraft jar used at runtime. + final Path classesJar = minecraftJar.getPath(); + final Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar"); + Files.delete(tempJar); + + try (var timer = new Timer("Remap line numbers")) { + remapLineNumbers(lineNumbers, classesJar, tempJar); + } + + Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING); + } + + private String getCacheKey() { + var sj = new StringJoiner(","); + sj.add(getDecompilerCheckKey()); + sj.add(getUnpickCacheKey()); + + LOGGER.info("Decompile cache data: {}", sj); + + try { + return Checksum.sha256Hex(sj.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private String getDecompilerCheckKey() { + var sj = new StringJoiner(","); + sj.add(decompilerOptions.getDecompilerClassName().get()); + sj.add(fileCollectionHash(decompilerOptions.getClasspath())); + + for (Map.Entry entry : decompilerOptions.getOptions().get().entrySet()) { + sj.add(entry.getKey() + "=" + entry.getValue()); + } + + return sj.toString(); + } + + private String getUnpickCacheKey() { + if (!getUnpickDefinitions().isPresent()) { + return ""; + } + + var sj = new StringJoiner(","); + sj.add(fileHash(getUnpickDefinitions().getAsFile().get())); + sj.add(fileCollectionHash(getUnpickConstantJar())); + sj.add(fileCollectionHash(getUnpickRuntimeClasspath())); + + return sj.toString(); + } + + @Nullable + private ClassLineNumbers runDecompileJob(Path inputJar, Path outputJar, @Nullable Path existingJar) throws IOException { + final Platform platform = Platform.CURRENT; + final Path lineMapFile = File.createTempFile("loom", "linemap").toPath(); + Files.delete(lineMapFile); + if (!platform.supportsUnixDomainSockets()) { getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system."); - doWork(null, inputJar, runtimeJar); - return; + doWork(null, inputJar, outputJar, lineMapFile, existingJar); + return readLineNumbers(lineMapFile); } // Set up the IPC path to get the log output back from the forked JVM @@ -184,7 +399,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompilerOptions.getName(), "Decompiling minecraft sources"); IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) { - doWork(logReceiver, inputJar, runtimeJar); + doWork(logReceiver, inputJar, outputJar, lineMapFile, existingJar); } catch (InterruptedException e) { throw new RuntimeException("Failed to shutdown log receiver", e); } finally { @@ -197,6 +412,28 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { ForgeSourcesRemapper.addForgeSources(getProject(), serviceManager, getOutputJar().get().getAsFile().toPath()); } } + + return readLineNumbers(lineMapFile); + } + + @Nullable + private ClassLineNumbers filterForgeLineNumbers(@Nullable ClassLineNumbers lineNumbers) { + if (lineNumbers == null) { + return null; + } + + if (getParameters().getForge().get()) { + try { + // Remove Forge and NeoForge classes from linemap + // TODO: We should instead not decompile Forge's classes at all + LineMapVisitor.process(linemap, next -> new LineMapClassFilter(next, name -> { + // Skip both Forge and NeoForge classes. + return !name.startsWith("net/minecraftforge/") && !name.startsWith("net/neoforged/"); + })); + } catch (IOException e) { + throw new UncheckedIOException("Failed to process linemap", e); + } + } } // Re-run the named minecraft provider to give us a fresh jar to decompile. @@ -209,7 +446,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(false, true, configContext); minecraftJars = getExtension().getNamedMinecraftProvider().provide(provideContext); } catch (Exception e) { - throw new RuntimeException("Failed to rebuild input jars", e); + ExceptionUtil.printFileLocks(e, getProject()); + throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to rebuild input jars", e); } for (MinecraftJar minecraftJar : minecraftJars) { @@ -224,13 +462,13 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { ); } - private Path unpickJar(Path inputJar) { + private Path unpickJar(Path inputJar, @Nullable Path existingJar) { final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath(); - final List args = getUnpickArgs(inputJar, outputJar); + final List args = getUnpickArgs(inputJar, outputJar, existingJar); ExecResult result = getExecOperations().javaexec(spec -> { spec.getMainClass().set("daomephsta.unpick.cli.Main"); - spec.classpath(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); + spec.classpath(getUnpickRuntimeClasspath()); spec.args(args); spec.systemProperty("java.util.logging.config.file", writeUnpickLogConfig().getAbsolutePath()); }); @@ -240,7 +478,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return outputJar; } - private List getUnpickArgs(Path inputJar, Path outputJar) { + private List getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingJar) { var fileArgs = new ArrayList(); fileArgs.add(inputJar.toFile()); @@ -257,6 +495,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { fileArgs.add(file); } + if (existingJar != null) { + fileArgs.add(existingJar.toFile()); + } + return fileArgs.stream() .map(File::getAbsolutePath) .toList(); @@ -275,25 +517,36 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return unpickLoggingConfigFile; } - private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path runtimeJar) { + private void remapLineNumbers(ClassLineNumbers lineNumbers, Path inputJar, Path outputJar) throws IOException { + Objects.requireNonNull(lineNumbers, "lineNumbers"); + final var remapper = new LineNumberRemapper(lineNumbers); + remapper.process(inputJar, outputJar); + } + + private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingJar) { 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); + } + workQueue.submit(DecompileAction.class, params -> { params.getDecompilerOptions().set(decompilerOptions.toDto()); params.getInputJar().set(inputJar.toFile()); - params.getRuntimeJar().set(runtimeJar.toFile()); - params.getSourcesDestinationJar().set(getOutputJar()); - params.getLinemap().set(getMappedJarFileWithSuffix("-sources.lmap", runtimeJar)); - params.getLinemapJar().set(getMappedJarFileWithSuffix("-linemapped.jar", runtimeJar)); + params.getOutputJar().set(outputJar.toFile()); + params.getLinemapFile().set(linemapFile.toFile()); params.getMappings().set(getMappings().toFile()); if (ipcServer != null) { params.getIPCPath().set(ipcServer.getPath().toFile()); } - params.getClassPath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES)); + params.getClassPath().setFrom(classpath); // Architectury params.getForge().set(getExtension().isForgeLike()); @@ -338,10 +591,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { Property getDecompilerOptions(); RegularFileProperty getInputJar(); - RegularFileProperty getRuntimeJar(); - RegularFileProperty getSourcesDestinationJar(); - RegularFileProperty getLinemap(); - RegularFileProperty getLinemapJar(); + RegularFileProperty getOutputJar(); + RegularFileProperty getLinemapFile(); RegularFileProperty getMappings(); RegularFileProperty getIPCPath(); @@ -372,10 +623,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { private void doDecompile(IOStringConsumer logger) { final Path inputJar = getParameters().getInputJar().get().getAsFile().toPath(); - final Path sourcesDestinationJar = getParameters().getSourcesDestinationJar().get().getAsFile().toPath(); - final Path linemap = getParameters().getLinemap().get().getAsFile().toPath(); - final Path linemapJar = getParameters().getLinemapJar().get().getAsFile().toPath(); - final Path runtimeJar = getParameters().getRuntimeJar().get().getAsFile().toPath(); + final Path linemap = getParameters().getLinemapFile().get().getAsFile().toPath(); + final Path outputJar = getParameters().getOutputJar().get().getAsFile().toPath(); final DecompilerOptions.Dto decompilerOptions = getParameters().getDecompilerOptions().get(); @@ -391,7 +640,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { throw new RuntimeException("Failed to create decompiler", e); } - DecompilationMetadata metadata = new DecompilationMetadata( + final var metadata = new DecompilationMetadata( decompilerOptions.maxThreads(), getParameters().getMappings().get().getAsFile().toPath(), getLibraries(), @@ -401,7 +650,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { decompiler.decompile( inputJar, - sourcesDestinationJar, + outputJar, linemap, metadata ); @@ -412,41 +661,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { } catch (IOException e) { throw new UncheckedIOException("Failed to close loggers", e); } - - if (Files.exists(linemap)) { - if (getParameters().getForge().get()) { - try { - // Remove Forge and NeoForge classes from linemap - // TODO: We should instead not decompile Forge's classes at all - LineMapVisitor.process(linemap, next -> new LineMapClassFilter(next, name -> { - // Skip both Forge and NeoForge classes. - return !name.startsWith("net/minecraftforge/") && !name.startsWith("net/neoforged/"); - })); - } catch (IOException e) { - throw new UncheckedIOException("Failed to process linemap", e); - } - } - - try { - // Line map the actually jar used to run the game, not the one used to decompile - remapLineNumbers(metadata.logger(), runtimeJar, linemap, linemapJar); - - Files.copy(linemapJar, runtimeJar, StandardCopyOption.REPLACE_EXISTING); - Files.delete(linemapJar); - } catch (IOException e) { - throw new UncheckedIOException("Failed to remap line numbers", e); - } - } - } - - static void remapLineNumbers(IOStringConsumer logger, Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException { - LineNumberRemapper remapper = new LineNumberRemapper(); - remapper.readMappings(linemap.toFile()); - - try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(oldCompiledJar.toFile(), true); - FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) { - remapper.process(logger, inFs.get().getPath("/"), outFs.get().getPath("/")); - } } private Collection getLibraries() { @@ -462,16 +676,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return getMappedJarFileWithSuffix(suffix, runtimeJar.get().getAsFile().toPath()); } - public static File getMappedJarFileWithSuffix(String suffix, Path runtimeJar) { - final String path = runtimeJar.toFile().getAbsolutePath(); - - if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) { - throw new RuntimeException("Invalid mapped JAR path: " + path); - } - - return new File(path.substring(0, path.length() - 4) + suffix); - } - private Path getMappings() { Path inputMappings = getExtension().getPlatformMappingFile(); @@ -530,8 +734,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return outputMappings; } - public interface MappingsProcessor { - boolean transform(MemoryMappingTree mappings); + public static File getJarFileWithSuffix(String suffix, Path runtimeJar) { + final String path = runtimeJar.toFile().getAbsolutePath(); + + if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) { + throw new RuntimeException("Invalid mapped JAR path: " + path); + } + + return new File(path.substring(0, path.length() - 4) + suffix); + } + + @Nullable + private static ClassLineNumbers readLineNumbers(Path linemapFile) throws IOException { + if (Files.notExists(linemapFile)) { + return null; + } + + try (BufferedReader reader = Files.newBufferedReader(linemapFile, StandardCharsets.UTF_8)) { + return ClassLineNumbers.readMappings(reader); + } } private static Constructor getDecompilerConstructor(String clazz) { @@ -544,4 +765,43 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { throw new RuntimeException(e); } } + + private static String fileHash(File file) { + try { + return Checksum.sha256Hex(Files.readAllBytes(file.toPath())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String fileCollectionHash(FileCollection files) { + var sj = new StringJoiner(","); + + files.getFiles() + .stream() + .sorted(Comparator.comparing(File::getAbsolutePath)) + .map(GenerateSourcesTask::fileHash) + .forEach(sj::add); + + return sj.toString(); + } + + public interface MappingsProcessor { + boolean transform(MemoryMappingTree mappings); + } + + private final class Timer implements AutoCloseable { + private final String name; + private final long start; + + Timer(String name) { + this.name = name; + this.start = System.currentTimeMillis(); + } + + @Override + public void close() { + getProject().getLogger().info("{} took {}ms", name, System.currentTimeMillis() - start); + } + } } diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index e32cfa5d..794f26ac 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -164,13 +164,18 @@ public abstract class LoomTasks implements Runnable { // Remove the client or server run config when not required. Done by name to not remove any possible custom run configs GradleUtils.afterSuccessfulEvaluation(getProject(), () -> { - String taskName = switch (extension.getMinecraftJarConfiguration().get()) { - case SERVER_ONLY -> "client"; - case CLIENT_ONLY -> "server"; - default -> null; - }; + String taskName; - if (taskName == null) { + boolean serverOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.SERVER_ONLY; + boolean clientOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.CLIENT_ONLY; + + if (serverOnly) { + // Server only, remove the client run config + taskName = "client"; + } else if (clientOnly) { + // Client only, remove the server run config + taskName = "server"; + } else { return; } diff --git a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java b/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java index 9569968a..a774ac25 100644 --- a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java +++ b/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java @@ -100,6 +100,6 @@ public abstract class PrepareJarRemapTask extends AbstractLoomTask { } static void prepare(TinyRemapperService tinyRemapperService, Path inputFile) { - tinyRemapperService.getTinyRemapperForInputs().readInputs(tinyRemapperService.getOrCreateTag(inputFile), inputFile); + tinyRemapperService.getTinyRemapperForInputs().readInputsAsync(tinyRemapperService.getOrCreateTag(inputFile), inputFile); } } diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index a62dcc99..e3d6fbbd 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -86,6 +86,7 @@ import net.fabricmc.loom.util.SidedClassVisitor; import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; +import net.fabricmc.loom.util.fmj.FabricModJsonUtils; import net.fabricmc.loom.util.service.BuildSharedServiceManager; import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; import net.fabricmc.tinyremapper.OutputConsumerPath; @@ -101,6 +102,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { @Input public abstract Property getAddNestedDependencies(); + /** + * Whether to optimize the fabric.mod.json file, by default this is false. + * + *

The schemaVersion entry will be placed first in the json file + */ + @Input + public abstract Property getOptimizeFabricModJson(); + /** * Gets the jar paths to the access wideners that will be converted to ATs for Forge runtime. * If you specify multiple files, they will be merged into one. @@ -145,6 +154,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { final ConfigurationContainer configurations = getProject().getConfigurations(); getClasspath().from(configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); getAddNestedDependencies().convention(true).finalizeValueOnRead(); + getOptimizeFabricModJson().convention(false).finalizeValueOnRead(); getReadMixinConfigsFromManifest().convention(LoomGradleExtension.get(getProject()).isForgeLike()).finalizeValueOnRead(); getInjectAccessWidener().convention(false); @@ -236,6 +246,8 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { if (!getAtAccessWideners().get().isEmpty()) { params.getMappingBuildServiceUuid().set(UnsafeWorkQueueHelper.create(MappingsService.createDefault(getProject(), serviceManagerProvider.get().get(), getSourceNamespace().get(), getTargetNamespace().get()))); } + + params.getOptimizeFmj().set(getOptimizeFabricModJson().get()); }); } @@ -293,6 +305,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { Property getUseMixinExtension(); Property getMultiProjectOptimisation(); + Property getOptimizeFmj(); record RefmapData(List mixinConfigs, String refmapName) implements Serializable { } ListProperty getMixinData(); @@ -348,6 +361,10 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { rewriteJar(); + if (getParameters().getOptimizeFmj().get()) { + optimizeFMJ(); + } + if (tinyRemapperService != null && !getParameters().getMultiProjectOptimisation().get()) { tinyRemapperService.close(); } @@ -482,6 +499,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { } } } + + private void optimizeFMJ() throws IOException { + if (!ZipUtils.contains(outputFile, FabricModJsonFactory.FABRIC_MOD_JSON)) { + return; + } + + ZipUtils.transformJson(JsonObject.class, outputFile, FabricModJsonFactory.FABRIC_MOD_JSON, FabricModJsonUtils::optimizeFmj); + } } @Override diff --git a/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java new file mode 100644 index 00000000..4d3af845 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java @@ -0,0 +1,88 @@ +/* + * 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.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public interface AsyncZipProcessor { + static void processEntries(Path inputZip, Path outputZip, AsyncZipProcessor processor) throws IOException { + try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputZip, false); + FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputZip, true)) { + final Path inRoot = inFs.get().getPath("/"); + final Path outRoot = outFs.get().getPath("/"); + + List> futures = new ArrayList<>(); + final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + Files.walkFileTree(inRoot, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path inputFile, BasicFileAttributes attrs) throws IOException { + final CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + final String rel = inRoot.relativize(inputFile).toString(); + final Path outputFile = outRoot.resolve(rel); + processor.processEntryAsync(inputFile, outputFile); + } catch (IOException e) { + throw new CompletionException(e); + } + + return null; + }, executor); + + futures.add(future); + return FileVisitResult.CONTINUE; + } + }); + + // Wait for all futures to complete + for (CompletableFuture future : futures) { + try { + future.join(); + } catch (CompletionException e) { + if (e.getCause() instanceof IOException ioe) { + throw ioe; + } + + throw new RuntimeException("Failed to process zip", e.getCause()); + } + } + + executor.shutdown(); + } + } + + void processEntryAsync(Path inputEntry, Path outputEntry) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index 97b1d49c..b9192d41 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -68,6 +68,11 @@ public class Checksum { } } + public static String sha256Hex(byte[] input) throws IOException { + HashCode hash = ByteSource.wrap(input).hash(Hashing.sha256()); + return Checksum.toHex(hash.asBytes()); + } + public static String sha1Hex(Path path) throws IOException { HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1()); return toHex(hash.asBytes()); @@ -102,6 +107,7 @@ public class Checksum { public static String projectHash(Project project) { String str = project.getProjectDir().getAbsolutePath() + ":" + project.getPath(); - return toHex(str.getBytes(StandardCharsets.UTF_8)).substring(0, 16); + String hex = sha1Hex(str.getBytes(StandardCharsets.UTF_8)); + return hex.substring(hex.length() - 16); } } diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index ee76d7f9..68f27557 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -37,6 +37,7 @@ public class Constants { public static final String FABRIC_REPOSITORY = "https://maven.fabricmc.net/"; public static final int ASM_VERSION = Opcodes.ASM9; + public static final String RELEASE_TIME_1_3 = "2012-07-25T22:00:00+00:00"; private Constants() { } diff --git a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java index 3683301e..8f942260 100644 --- a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2022 FabricMC + * Copyright (c) 2022-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 @@ -24,8 +24,17 @@ package net.fabricmc.loom.util; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; import java.util.function.BiFunction; +import org.gradle.api.Project; + +import net.fabricmc.loom.nativeplatform.LoomNativePlatform; + public final class ExceptionUtil { /** * Creates a descriptive user-facing wrapper exception for an underlying cause. @@ -44,4 +53,40 @@ public final class ExceptionUtil { String descriptiveMessage = "%s, %s: %s".formatted(message, cause.getClass().getName(), cause.getMessage()); return constructor.apply(descriptiveMessage, cause); } + + public static void printFileLocks(Throwable e, Project project) { + Throwable cause = e; + + while (cause != null) { + if (cause instanceof FileSystemException fse) { + printFileLocks(fse.getFile(), project); + break; + } + + cause = cause.getCause(); + } + } + + private static void printFileLocks(String filename, Project project) { + final Path path = Paths.get(filename); + + if (!Files.exists(path)) { + return; + } + + final List processes = LoomNativePlatform.getProcessesWithLockOn(path); + + if (processes.isEmpty()) { + return; + } + + final ProcessUtil processUtil = ProcessUtil.create(project); + + final String noun = processes.size() == 1 ? "process has" : "processes have"; + project.getLogger().error("The following {} a lock on the file '{}':", noun, path); + + for (ProcessHandle process : processes) { + project.getLogger().error(processUtil.printWithParents(process)); + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java index 0a097c83..07fe08cf 100644 --- a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -43,6 +43,10 @@ public final class FileSystemUtil { return get().getPath(path, more); } + public Path getRoot() { + return get().getPath("/"); + } + public byte[] readAllBytes(String path) throws IOException { Path fsPath = getPath(path); diff --git a/src/main/java/net/fabricmc/loom/util/ProcessUtil.java b/src/main/java/net/fabricmc/loom/util/ProcessUtil.java new file mode 100644 index 00000000..93f8e1e3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ProcessUtil.java @@ -0,0 +1,109 @@ +/* + * 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.List; +import java.util.Optional; +import java.util.StringJoiner; + +import org.gradle.api.Project; +import org.gradle.api.logging.LogLevel; + +import net.fabricmc.loom.nativeplatform.LoomNativePlatform; + +public record ProcessUtil(LogLevel logLevel) { + private static final String EXPLORER_COMMAND = "C:\\Windows\\explorer.exe"; + + public static ProcessUtil create(Project project) { + return new ProcessUtil(project.getGradle().getStartParameter().getLogLevel()); + } + + public String printWithParents(ProcessHandle handle) { + String result = printWithParents(handle, 0).trim(); + + if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) { + return "Run with --info or --debug to show arguments, may reveal sensitive info\n" + result; + } + + return result; + } + + private String printWithParents(ProcessHandle handle, int depth) { + var lines = new ArrayList(); + getWindowTitles(handle).ifPresent(titles -> lines.add("title: " + titles)); + lines.add("pid: " + handle.pid()); + handle.info().command().ifPresent(command -> lines.add("command: " + command)); + getProcessArguments(handle).ifPresent(arguments -> lines.add("arguments: " + arguments)); + handle.info().startInstant().ifPresent(instant -> lines.add("started at: " + instant)); + handle.info().user().ifPresent(user -> lines.add("user: " + user)); + handle.parent().ifPresent(parent -> lines.add("parent:\n" + printWithParents(parent, depth + 1))); + + StringBuilder sj = new StringBuilder(); + + for (String line : lines) { + sj.append("\t".repeat(depth)).append("- ").append(line).append('\n'); + } + + return sj.toString(); + } + + private Optional getProcessArguments(ProcessHandle handle) { + if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) { + return Optional.empty(); + } + + return handle.info().arguments().map(arr -> { + String join = String.join(" ", arr); + + if (join.isBlank()) { + return ""; + } + + return " " + join; + }); + } + + private Optional getWindowTitles(ProcessHandle processHandle) { + if (processHandle.info().command().orElse("").equals(EXPLORER_COMMAND)) { + // Explorer is a single process, so the window titles are not useful + return Optional.empty(); + } + + List titles = LoomNativePlatform.getWindowTitlesForPid(processHandle.pid()); + + if (titles.isEmpty()) { + return Optional.empty(); + } + + final StringJoiner joiner = new StringJoiner(", "); + + for (String title : titles) { + joiner.add("'" + title + "'"); + } + + return Optional.of(joiner.toString()); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java index 0d6891d8..f182b46b 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java @@ -74,7 +74,7 @@ public final class TinyRemapperHelper { MemoryMappingTree mappingTree = extension.getMappingConfiguration().getMappingsService(serviceManager, mappingOption).getMappingTree(); if (fixRecords && !mappingTree.getSrcNamespace().equals(fromM)) { - throw new IllegalStateException("Mappings src namespace must match remap src namespace"); + throw new IllegalStateException("Mappings src namespace must match remap src namespace, expected " + fromM + " but got " + mappingTree.getSrcNamespace()); } int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString()); diff --git a/src/main/java/net/fabricmc/loom/util/ZipUtils.java b/src/main/java/net/fabricmc/loom/util/ZipUtils.java index 25f91df5..6da4520b 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipUtils.java +++ b/src/main/java/net/fabricmc/loom/util/ZipUtils.java @@ -79,13 +79,13 @@ public class ZipUtils { public static void unpackAll(Path zip, Path output) throws IOException { try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(zip, false); - Stream walk = Files.walk(fs.get().getPath("/"))) { + Stream walk = Files.walk(fs.getRoot())) { Iterator iterator = walk.iterator(); while (iterator.hasNext()) { Path fsPath = iterator.next(); if (!Files.isRegularFile(fsPath)) continue; - Path dstPath = output.resolve(fs.get().getPath("/").relativize(fsPath).toString()); + Path dstPath = output.resolve(fs.getRoot().relativize(fsPath).toString()); Path dstPathParent = dstPath.getParent(); if (dstPathParent != null) Files.createDirectories(dstPathParent); Files.copy(fsPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); @@ -121,7 +121,7 @@ public class ZipUtils { } } - public static T unpackJackson(Path zip, String path, Class clazz) throws IOException { + public static T unpackJson(Path zip, String path, Class clazz) throws IOException { final byte[] bytes = unpack(zip, path); return LoomGradlePlugin.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz); } @@ -215,6 +215,14 @@ public class ZipUtils { s -> LoomGradlePlugin.GSON.toJson(s, typeOfT).getBytes(StandardCharsets.UTF_8)); } + public static void transformJson(Class typeOfT, Path zip, String path, UnsafeUnaryOperator transformer) throws IOException { + int transformed = transformJson(typeOfT, zip, Map.of(path, transformer)); + + if (transformed != 1) { + throw new IOException("Failed to transform " + path + " in " + zip); + } + } + public static int transform(Path zip, Collection>> transforms) throws IOException { return transform(zip, transforms.stream()); } diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java index bf101370..f1b414db 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java @@ -36,11 +36,14 @@ import java.nio.file.Path; import java.util.Optional; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import dev.architectury.loom.metadata.ModMetadataFile; import dev.architectury.loom.metadata.ModMetadataFiles; import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.util.FileSystemUtil; @@ -49,7 +52,9 @@ import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.gradle.SourceSetHelper; public final class FabricModJsonFactory { - private static final String FABRIC_MOD_JSON = "fabric.mod.json"; + public static final String FABRIC_MOD_JSON = "fabric.mod.json"; + + private static final Logger LOGGER = LoggerFactory.getLogger(FabricModJsonFactory.class); private FabricModJsonFactory() { } @@ -158,6 +163,11 @@ public final class FabricModJsonFactory { try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { return create(LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class), new FabricModJsonSource.SourceSetSource(sourceSets)); + } catch (JsonSyntaxException e) { + LOGGER.warn("Failed to parse fabric.mod.json: {}", file.getAbsolutePath()); + return null; + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + file.getAbsolutePath(), e); } } 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 78b94032..5559551b 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java @@ -25,13 +25,14 @@ package net.fabricmc.loom.util.fmj; import java.util.Locale; +import java.util.Map; import java.util.function.Predicate; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -final class FabricModJsonUtils { +public final class FabricModJsonUtils { private FabricModJsonUtils() { } @@ -49,6 +50,30 @@ final class FabricModJsonUtils { return element.getAsInt(); } + // 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) { + if (!json.has("schemaVersion")) { + // No schemaVersion, something will explode later?! + return json; + } + + // Create a new json object with the schemaVersion first + var out = new JsonObject(); + out.add("schemaVersion", json.get("schemaVersion")); + + for (Map.Entry entry : json.entrySet()) { + if (entry.getKey().equals("schemaVersion")) { + continue; + } + + // Add all other entries + out.add(entry.getKey(), entry.getValue()); + } + + return out; + } + private static JsonElement getElement(JsonObject jsonObject, String key) { final JsonElement element = jsonObject.get(key); diff --git a/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java b/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java index 7d1825fd..45ad0873 100644 --- a/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java +++ b/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.stream.Stream; import net.fabricmc.loom.LoomGradlePlugin; -import net.fabricmc.loom.kotlin.remapping.JvmExtensionWrapper; import net.fabricmc.loom.kotlin.remapping.KotlinMetadataTinyRemapperExtensionImpl; /** @@ -62,8 +61,7 @@ public class KotlinRemapperClassloader extends URLClassLoader { public static KotlinRemapperClassloader create(KotlinClasspath classpathProvider) { // Include the libraries that are not on the kotlin classpath. final Stream loomUrls = getClassUrls( - KotlinMetadataTinyRemapperExtensionImpl.class, // Loom (Kotlin) - JvmExtensionWrapper.class // Loom (Java) + KotlinMetadataTinyRemapperExtensionImpl.class // Loom (Kotlin) ); final URL[] urls = Stream.concat( diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt index ffb59ccc..d77a0d48 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt @@ -40,18 +40,17 @@ import kotlinx.metadata.KmTypeAlias import kotlinx.metadata.KmTypeParameter import kotlinx.metadata.KmTypeProjection import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.internal.extensions.KmClassExtension -import kotlinx.metadata.internal.extensions.KmConstructorExtension -import kotlinx.metadata.internal.extensions.KmFunctionExtension -import kotlinx.metadata.internal.extensions.KmPackageExtension -import kotlinx.metadata.internal.extensions.KmPropertyExtension -import kotlinx.metadata.internal.extensions.KmTypeAliasExtension -import kotlinx.metadata.internal.extensions.KmTypeExtension -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension -import kotlinx.metadata.internal.extensions.KmValueParameterExtension import kotlinx.metadata.isLocalClassName import kotlinx.metadata.jvm.JvmFieldSignature import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.annotations +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.localDelegatedProperties +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations +import kotlinx.metadata.jvm.syntheticMethodForDelegate import kotlinx.metadata.jvm.toJvmInternalName import org.objectweb.asm.commons.Remapper @@ -68,7 +67,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { clazz.nestedClasses.replaceAll(this::remap) clazz.sealedSubclasses.replaceAll(this::remap) clazz.contextReceiverTypes.replaceAll(this::remap) - clazz.getExtensions().replaceAll(this::remap) + clazz.localDelegatedProperties.replaceAll(this::remap) return clazz } @@ -81,7 +80,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { pkg.functions.replaceAll(this::remap) pkg.properties.replaceAll(this::remap) pkg.typeAliases.replaceAll(this::remap) - pkg.getExtensions().replaceAll(this::remap) + pkg.localDelegatedProperties.replaceAll(this::remap) return pkg } @@ -107,7 +106,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { type.abbreviatedType = type.abbreviatedType?.let { remap(it) } type.outerType = type.outerType?.let { remap(it) } type.flexibleTypeUpperBound = type.flexibleTypeUpperBound?.let { remap(it) } - type.getExtensions().replaceAll(this::remap) + type.annotations.replaceAll(this::remap) return type } @@ -117,7 +116,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { function.contextReceiverTypes.replaceAll(this::remap) function.valueParameters.replaceAll(this::remap) function.returnType = remap(function.returnType) - function.getExtensions().replaceAll(this::remap) + function.signature = function.signature?.let { remap(it) } return function } @@ -127,7 +126,11 @@ class KotlinClassRemapper(private val remapper: Remapper) { property.contextReceiverTypes.replaceAll(this::remap) property.setterParameter = property.setterParameter?.let { remap(it) } property.returnType = remap(property.returnType) - property.getExtensions().replaceAll(this::remap) + property.fieldSignature = property.fieldSignature?.let { remap(it) } + property.getterSignature = property.getterSignature?.let { remap(it) } + property.setterSignature = property.setterSignature?.let { remap(it) } + property.syntheticMethodForAnnotations = property.syntheticMethodForAnnotations?.let { remap(it) } + property.syntheticMethodForDelegate = property.syntheticMethodForDelegate?.let { remap(it) } return property } @@ -136,19 +139,18 @@ class KotlinClassRemapper(private val remapper: Remapper) { typeAlias.underlyingType = remap(typeAlias.underlyingType) typeAlias.expandedType = remap(typeAlias.expandedType) typeAlias.annotations.replaceAll(this::remap) - typeAlias.getExtensions().replaceAll(this::remap) return typeAlias } private fun remap(constructor: KmConstructor): KmConstructor { constructor.valueParameters.replaceAll(this::remap) - constructor.getExtensions().replaceAll(this::remap) + constructor.signature = constructor.signature?.let { remap(it) } return constructor } private fun remap(typeParameter: KmTypeParameter): KmTypeParameter { typeParameter.upperBounds.replaceAll(this::remap) - typeParameter.getExtensions().replaceAll(this::remap) + typeParameter.annotations.replaceAll(this::remap) return typeParameter } @@ -163,7 +165,6 @@ class KotlinClassRemapper(private val remapper: Remapper) { private fun remap(valueParameter: KmValueParameter): KmValueParameter { valueParameter.type = remap(valueParameter.type) valueParameter.varargElementType = valueParameter.varargElementType?.let { remap(it) } - valueParameter.getExtensions().replaceAll(this::remap) return valueParameter } @@ -171,76 +172,6 @@ class KotlinClassRemapper(private val remapper: Remapper) { return KmAnnotation(remap(annotation.className), annotation.arguments) } - private fun remap(classExtension: KmClassExtension): KmClassExtension { - JvmExtensionWrapper.Class.get(classExtension)?.let { - it.localDelegatedProperties.replaceAll(this::remap) - return it.extension - } - - return classExtension - } - - private fun remap(packageExtension: KmPackageExtension): KmPackageExtension { - JvmExtensionWrapper.Package.get(packageExtension)?.let { - it.localDelegatedProperties.replaceAll(this::remap) - return it.extension - } - - return packageExtension - } - - private fun remap(typeExtension: KmTypeExtension): KmTypeExtension { - JvmExtensionWrapper.Type.get(typeExtension)?.let { - it.annotations.replaceAll(this::remap) - return it.extension - } - - return typeExtension - } - - private fun remap(functionExtension: KmFunctionExtension): KmFunctionExtension { - JvmExtensionWrapper.Function.get(functionExtension)?.let { - it.signature = it.signature?.let { sig -> remap(sig) } - return it.extension - } - - return functionExtension - } - - private fun remap(propertyExtension: KmPropertyExtension): KmPropertyExtension { - JvmExtensionWrapper.Property.get(propertyExtension)?.let { - it.fieldSignature = it.fieldSignature?.let { sig -> remap(sig) } - it.getterSignature = it.getterSignature?.let { sig -> remap(sig) } - it.setterSignature = it.setterSignature?.let { sig -> remap(sig) } - it.syntheticMethodForAnnotations = it.syntheticMethodForAnnotations?.let { sig -> remap(sig) } - it.syntheticMethodForDelegate = it.syntheticMethodForDelegate?.let { sig -> remap(sig) } - return it.extension - } - - return propertyExtension - } - - private fun remap(typeAliasExtension: KmTypeAliasExtension): KmTypeAliasExtension { - return typeAliasExtension - } - - private fun remap(typeParameterExtension: KmTypeParameterExtension): KmTypeParameterExtension { - return typeParameterExtension - } - - private fun remap(valueParameterExtension: KmValueParameterExtension): KmValueParameterExtension { - return valueParameterExtension - } - - private fun remap(constructorExtension: KmConstructorExtension): KmConstructorExtension { - JvmExtensionWrapper.Constructor.get(constructorExtension)?.let { - it.signature = it.signature?.let { sig -> remap(sig) } - return it.extension - } - - return constructorExtension - } - private fun remap(signature: JvmMethodSignature): JvmMethodSignature { return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor)) } diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt deleted file mode 100644 index e3061018..00000000 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2023 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. - */ - -@file:Suppress("UNCHECKED_CAST") - -package net.fabricmc.loom.kotlin.remapping - -import kotlinx.metadata.KmClass -import kotlinx.metadata.KmConstructor -import kotlinx.metadata.KmFunction -import kotlinx.metadata.KmPackage -import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmType -import kotlinx.metadata.KmTypeAlias -import kotlinx.metadata.KmTypeParameter -import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.internal.extensions.KmClassExtension -import kotlinx.metadata.internal.extensions.KmConstructorExtension -import kotlinx.metadata.internal.extensions.KmFunctionExtension -import kotlinx.metadata.internal.extensions.KmPackageExtension -import kotlinx.metadata.internal.extensions.KmPropertyExtension -import kotlinx.metadata.internal.extensions.KmTypeAliasExtension -import kotlinx.metadata.internal.extensions.KmTypeExtension -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension -import kotlinx.metadata.internal.extensions.KmValueParameterExtension -import java.lang.reflect.Field -import kotlin.reflect.KClass - -val KM_CLASS_EXTENSIONS = getField(KmClass::class) -val KM_PACKAGE_EXTENSIONS = getField(KmPackage::class) -val KM_TYPE_EXTENSIONS = getField(KmType::class) -val KM_FUNCTION_EXTENSIONS = getField(KmFunction::class) -val KM_PROPERTY_EXTENSIONS = getField(KmProperty::class) -val KM_TYPE_ALIAS_EXTENSIONS = getField(KmTypeAlias::class) -val KM_TYPE_PARAMETER_EXTENSIONS = getField(KmTypeParameter::class) -val KM_VALUE_PARAMETER_EXTENSIONS = getField(KmValueParameter::class) -val KM_CONSTRUCTOR_EXTENSIONS = getField(KmConstructor::class) - -fun KmClass.getExtensions(): MutableList { - return KM_CLASS_EXTENSIONS.get(this) as MutableList -} - -fun KmPackage.getExtensions(): MutableList { - return KM_PACKAGE_EXTENSIONS.get(this) as MutableList -} - -fun KmType.getExtensions(): MutableList { - return KM_TYPE_EXTENSIONS.get(this) as MutableList -} - -fun KmFunction.getExtensions(): MutableList { - return KM_FUNCTION_EXTENSIONS.get(this) as MutableList -} - -fun KmProperty.getExtensions(): MutableList { - return KM_PROPERTY_EXTENSIONS.get(this) as MutableList -} - -fun KmTypeAlias.getExtensions(): MutableList { - return KM_TYPE_ALIAS_EXTENSIONS.get(this) as MutableList -} - -fun KmTypeParameter.getExtensions(): MutableList { - return KM_TYPE_PARAMETER_EXTENSIONS.get(this) as MutableList -} - -fun KmValueParameter.getExtensions(): MutableList { - return KM_VALUE_PARAMETER_EXTENSIONS.get(this) as MutableList -} - -fun KmConstructor.getExtensions(): MutableList { - return KM_CONSTRUCTOR_EXTENSIONS.get(this) as MutableList -} - -private fun getField(clazz: KClass<*>): Field { - val field = clazz.java.getDeclaredField("extensions") - field.isAccessible = true - return field -} diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt index 074fe2c0..545ccd25 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt @@ -57,7 +57,11 @@ class KotlinMetadataRemappingClassVisitor(private val remapper: Remapper, next: var result: AnnotationVisitor? = super.visitAnnotation(descriptor, visible) if (descriptor == ANNOTATION_DESCRIPTOR && result != null) { - result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className) + try { + result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className) + } catch (e: Exception) { + throw RuntimeException("Failed to remap Kotlin metadata annotation in class $className", e) + } } return result diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy index 778b6611..66919c0f 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy @@ -88,12 +88,15 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait modDatagenImplementation fabricApi.module("fabric-data-generation-api-v1", "0.90.0+1.20.2") } + + println("%%" + loom.runs.datagen.configName + "%%") ''' when: def result = gradle.run(task: "runDatagen") then: result.task(":runDatagen").outcome == SUCCESS + result.output.contains("%%Data Generation%%") where: version << STANDARD_TEST_VERSIONS diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy index dd220320..e7eda02b 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy @@ -84,7 +84,7 @@ class DebugLineNumbersTest extends Specification implements GradleProjectTestTra ''' when: // First generate sources - def genSources = gradle.run(task: "genSources") + def genSources = gradle.run(task: "genSources", args: ["--info"]) genSources.task(":genSources").outcome == SUCCESS // Print out the source of the file diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy index 7431aae0..120d9e73 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy @@ -74,4 +74,36 @@ class DecompileTest extends Specification implements GradleProjectTestTrait { where: version << STANDARD_TEST_VERSIONS } + + def "decompile cache"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE, gradleHomeDir: File.createTempDir()) + gradle.buildSrc("decompile") + gradle.buildGradle << ''' + dependencies { + minecraft "com.mojang:minecraft:1.20.4" + mappings "net.fabricmc:yarn:1.20.4+build.3:v2" + } + ''' + + when: + def result = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"]) + + // Add fabric API to the project, this introduces some transitive access wideners + gradle.buildGradle << ''' + dependencies { + modImplementation "net.fabricmc.fabric-api:fabric-api:0.96.4+1.20.4" + } + ''' + + def result2 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"]) + + // And run again, with no changes + def result3 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"]) + + then: + result.task(":genSourcesWithVineflower").outcome == SUCCESS + result2.task(":genSourcesWithVineflower").outcome == SUCCESS + result3.task(":genSourcesWithVineflower").outcome == SUCCESS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy index 907de8f1..1cb8e760 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy @@ -24,6 +24,8 @@ package net.fabricmc.loom.test.integration +import java.nio.file.Path + import spock.lang.Specification import spock.lang.Unroll @@ -118,4 +120,31 @@ class LegacyProjectTest extends Specification implements GradleProjectTestTrait 'b1.8.1' | _ 'a1.2.5' | _ } + + @Unroll + def "Legacy merged"() { + setup: + def mappings = Path.of("src/test/resources/mappings/1.2.5-intermediary.tiny.zip").toAbsolutePath() + def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE) + + gradle.buildGradle << """ + dependencies { + minecraft "com.mojang:minecraft:1.2.5" + mappings loom.layered() { + // No names + } + + modImplementation "net.fabricmc:fabric-loader:0.15.7" + } + """ + gradle.buildSrc("legacyMergedIntermediary") + + when: + def result = gradle.run(task: "build", args: [ + "-Ploom.test.legacyMergedIntermediary.mappingPath=${mappings}" + ]) + + then: + result.task(":build").outcome == SUCCESS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy new file mode 100644 index 00000000..1ddda621 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy @@ -0,0 +1,80 @@ +/* + * 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 spock.lang.Specification +import spock.lang.Unroll + +import net.fabricmc.loom.build.nesting.IncludedJarFactory +import net.fabricmc.loom.test.util.GradleProjectTestTrait + +class SemVerParsingTest extends Specification implements GradleProjectTestTrait { + @Unroll + def "test valid Semantic Versioning strings"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.validSemVer(version) == true + + where: + version | _ + "1.0.0" | _ + "2.5.3" | _ + "3.0.0-beta.2" | _ + "4.2.1-alpha+001" | _ + "5.0.0-rc.1+build.1" | _ + } + + @Unroll + def "test non-Semantic Versioning strings"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.validSemVer(version) == false + + where: + version | _ + "1.0" | _ + "3.0.0.Beta1-120922-126" | _ + "3.0.2.Final" | _ + "4.2.1.4.RELEASE" | _ + } + + @Unroll + def "test '.Final' suffixed SemVer"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.getVersion(metadata) == expectedVersion + + where: + metadata | expectedVersion + new IncludedJarFactory.Metadata("group", "name", "1.0.0.Final", null) | "1.0.0" + new IncludedJarFactory.Metadata("group", "name", "2.5.3.final", null) | "2.5.3" + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy index 90829f74..31609a91 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy @@ -26,6 +26,8 @@ package net.fabricmc.loom.test.integration.buildSrc.decompile import java.nio.file.Path +import com.google.common.io.Files + import net.fabricmc.loom.api.decompilers.DecompilationMetadata import net.fabricmc.loom.api.decompilers.LoomDecompiler @@ -33,5 +35,6 @@ class CustomDecompiler implements LoomDecompiler { @Override void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { println("Running custom decompiler") + Files.touch(sourcesDestination.toFile()) } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy new file mode 100644 index 00000000..137f629f --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy @@ -0,0 +1,63 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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.buildSrc.legacyMergedIntermediary + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.provider.Property + +import net.fabricmc.loom.api.LoomGradleExtensionAPI +import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider +import net.fabricmc.loom.util.ZipUtils + +class TestPlugin implements Plugin { + @Override + void apply(Project project) { + LoomGradleExtensionAPI extension = project.getExtensions().getByName("loom") + extension.setIntermediateMappingsProvider(LegacyIntermediaryProvider.class) { + mappingPath.set(project.property("loom.test.legacyMergedIntermediary.mappingPath")) + } + } + + abstract static class LegacyIntermediaryProvider extends IntermediateMappingsProvider { + final String name = "legacyMerged" + + abstract Property getMappingPath(); + + @Override + void provide(Path tinyMappings) throws IOException { + if (getMinecraftVersion().get() != "1.2.5") { + throw new IllegalStateException("This plugin only supports Minecraft 1.2.5") + } + + byte[] data = ZipUtils.unpack(Paths.get(getMappingPath().get()), "1.2.5-intermediary.tiny") + Files.write(tinyMappings, data) + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy new file mode 100644 index 00000000..684147e3 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy @@ -0,0 +1,78 @@ +/* + * 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.Files +import java.nio.file.Path + +import spock.lang.Specification + +import net.fabricmc.loom.test.util.ZipTestUtils +import net.fabricmc.loom.util.AsyncZipProcessor +import net.fabricmc.loom.util.ZipUtils + +class AsyncZipProcessorTest extends Specification { + def "process async"() { + given: + def inputZip = ZipTestUtils.createZip(createEntries()) + def outputZip = ZipTestUtils.createZip(Collections.emptyMap()) + Files.delete(outputZip) + + when: + // Process the input zip asynchronously, converting all entries to uppercase + AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry -> + def str = Files.readString(inputEntry) + Files.writeString(outputEntry, str.toUpperCase()) + } + + then: + ZipUtils.unpack(outputZip, "file1.txt") == "FILE1".bytes + ZipUtils.unpack(outputZip, "file500.txt") == "FILE500".bytes + ZipUtils.unpack(outputZip, "file800.txt") == "FILE800".bytes + } + + def "re throws"() { + given: + def inputZip = ZipTestUtils.createZip(createEntries()) + def outputZip = ZipTestUtils.createZip(Collections.emptyMap()) + Files.delete(outputZip) + + when: + AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry -> + throw new IOException("Test exception") + } + + then: + thrown(IOException) + } + + Map createEntries(int count = 10000) { + Map entries = [:] + for (int i = 0; i < count; i++) { + entries.put("file" + i + ".txt", "file$i") + } + return entries + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy new file mode 100644 index 00000000..37fd125b --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy @@ -0,0 +1,99 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2023 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.ClassLineNumbers + +class ClassLineNumbersTest extends Specification { + def "read linemap"() { + when: + def reader = new BufferedReader(new StringReader(LINE_MAP)) + def lineNumbers = ClassLineNumbers.readMappings(reader) + def lineMap = lineNumbers.lineMap() + + then: + lineMap.size() == 2 + lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].lineMap().size() == 39 + lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLine() == 203 + lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLineDest() == 187 + + lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].lineMap().size() == 6 + lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLine() == 25 + lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLineDest() == 30 + } + + private static final String LINE_MAP = """ +net/minecraft/server/dedicated/ServerPropertiesHandler\t203\t187 +\t48\t187 +\t91\t92 +\t96\t97 +\t110\t108 +\t112\t109 +\t113\t110 +\t115\t111 +\t116\t112 +\t118\t113 +\t119\t113 +\t120\t113 +\t122\t114 +\t130\t115 +\t147\t129 +\t149\t131 +\t151\t133 +\t154\t136 +\t158\t141 +\t159\t142 +\t163\t144 +\t164\t145 +\t165\t146 +\t166\t147 +\t168\t149 +\t169\t150 +\t170\t151 +\t172\t153 +\t175\t155 +\t176\t156 +\t177\t157 +\t178\t158 +\t181\t160 +\t186\t165 +\t187\t166 +\t192\t171 +\t194\t173 +\t195\t174 +\t197\t176 +\t203\t182 + +net/minecraft/server/dedicated/ServerPropertiesLoader\t25\t30 +\t11\t15 +\t12\t16 +\t16\t20 +\t20\t24 +\t24\t28 +\t25\t30 +""" +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy new file mode 100644 index 00000000..7f0d0c57 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy @@ -0,0 +1,87 @@ +/* + * 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/ProcessUtilTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy new file mode 100644 index 00000000..76a96ea3 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy @@ -0,0 +1,41 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 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 org.gradle.api.logging.LogLevel +import spock.lang.Specification + +import net.fabricmc.loom.util.ProcessUtil + +class ProcessUtilTest extends Specification { + def "print process info"() { + when: + def output = new ProcessUtil(LogLevel.DEBUG).printWithParents(ProcessHandle.current()) + + then: + // Just a simple check to see if the output is not empty + !output.isEmpty() + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy index e967638e..7798a5c0 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy @@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.time.ZoneId +import com.google.gson.JsonObject import spock.lang.Specification import net.fabricmc.loom.util.Checksum @@ -188,4 +189,28 @@ class ZipUtilsTest extends Specification { "Etc/GMT-6" | _ "Etc/GMT+9" | _ } + + def "transform json"() { + given: + def dir = File.createTempDir() + def zip = File.createTempFile("loom-zip-test", ".zip").toPath() + new File(dir, "test.json").text = """ + { + "test": "This is a test of transforming" + } + """ + ZipUtils.pack(dir.toPath(), zip) + + when: + ZipUtils.transformJson(JsonObject.class, zip, "test.json") { json -> + def test = json.get("test").getAsString() + json.addProperty("test", test.toUpperCase()) + json + } + + def transformed = ZipUtils.unpackJson(zip, "test.json", JsonObject.class) + + then: + transformed.get("test").asString == "THIS IS A TEST OF TRANSFORMING" + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy new file mode 100644 index 00000000..d43417ea --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy @@ -0,0 +1,62 @@ +/* + * 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 java.nio.channels.FileChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +import spock.lang.Specification +import spock.lang.TempDir + +import net.fabricmc.loom.decompilers.ClassLineNumbers +import net.fabricmc.loom.decompilers.cache.CachedData + +class CachedDataTest extends Specification { + @TempDir + Path testPath + + // Simple test to check if the CachedData class can be written and read from a file + def "Read + Write CachedData"() { + given: + def lineNumberEntry = new ClassLineNumbers.Entry("net/test/TestClass", 1, 2, [1: 2, 4: 7]) + def cachedData = new CachedData("net/test/TestClass", "Example sources", lineNumberEntry) + def path = testPath.resolve("cachedData.bin") + when: + // Write the cachedData to a file + FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE).withCloseable { + cachedData.write(it) + } + + // And read it back + def readCachedData = Files.newInputStream(path).withCloseable { + return CachedData.read(it) + } + + then: + cachedData == readCachedData + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy new file mode 100644 index 00000000..04292ab4 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy @@ -0,0 +1,132 @@ +/* + * 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 java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.FileTime +import java.time.Duration +import java.time.Instant + +import spock.lang.Specification +import spock.lang.TempDir + +import net.fabricmc.loom.decompilers.cache.CachedFileStore +import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl +import net.fabricmc.loom.util.FileSystemUtil + +class CachedFileStoreTest extends Specification { + @TempDir + Path testPath + + FileSystemUtil.Delegate zipDelegate + Path root + + void setup() { + zipDelegate = FileSystemUtil.getJarFileSystem(testPath.resolve("cache.zip"), true) + root = zipDelegate.get().getPath("/") + } + + void cleanup() { + zipDelegate.close() + } + + def "putEntry"() { + given: + def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7)) + def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules) + when: + store.putEntry("abc", "Hello world".bytes) + then: + Files.exists(root.resolve("abc")) + } + + def "getEntry"() { + given: + def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7)) + def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules) + when: + store.putEntry("abc", "Hello world".bytes) + def entry = store.getEntry("abc") + def unknownEntry = store.getEntry("123") + then: + entry == "Hello world".bytes + unknownEntry == null + } + + def "pruneManyFiles"() { + given: + def cacheRules = new CachedFileStoreImpl.CacheRules(250, Duration.ofDays(7)) + def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules) + when: + + for (i in 0..<500) { + def key = "test_" + i + store.putEntry(key, "Hello world".bytes) + // Higher files are older and should be removed. + Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i))) + } + + store.prune() + + then: + Files.exists(root.resolve("test_0")) + Files.exists(root.resolve("test_100")) + Files.notExists(root.resolve("test_300")) + } + + def "pruneOldFiles"() { + given: + def cacheRules = new CachedFileStoreImpl.CacheRules(1000, Duration.ofSeconds(250)) + def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules) + when: + + for (i in 0..<500) { + def key = "test_" + i + store.putEntry(key, "Hello world".bytes) + // Higher files are older and should be removed. + Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i))) + } + + store.prune() + + then: + Files.exists(root.resolve("test_0")) + Files.exists(root.resolve("test_100")) + Files.notExists(root.resolve("test_300")) + } + + private static CachedFileStore.EntrySerializer BYTE_ARRAY_SERIALIZER = new CachedFileStore.EntrySerializer() { + @Override + byte[] read(Path path) throws IOException { + return Files.readAllBytes(path) + } + + @Override + void write(byte[] entry, Path path) throws IOException { + Files.write(path, entry) + } + } +} 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 new file mode 100644 index 00000000..6b157603 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy @@ -0,0 +1,241 @@ +/* + * 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 java.nio.file.Files + +import spock.lang.Specification + +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.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 String ExampleHash = "abc123/cd372fb85148700fa88095e3492d3f9f5beb43e555e5ff26d95f5a6adc36f8e6" + static String TestHash = "abc123/ecd40b16ec50b636a390cb8da716a22606965f14e526e3051144dd567f336bc5" + + 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")) + + def "prepare full work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob + + then: + workRequest.lineNumbers() == null + workJob.outputNameMap().size() == 2 + + // Expect two calls looking for the existing entry in the cache + 2 * cache.getEntry(_) >> null + + 0 * _ // Strict mock + } + + def "prepare partial work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob + def lineMap = workRequest.lineNumbers().lineMap() + + then: + lineMap.size() == 1 + lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers() + + workJob.outputNameMap().size() == 1 + ZipUtils.unpackNullable(workJob.existing(), "net/fabricmc/Example.java") == "Example sources".bytes + + // Provide one cached entry + // And then one call not finding the entry in the cache + 1 * cache.getEntry(ExampleHash) >> ExampleCachedData + 1 * cache.getEntry(_) >> null + + 0 * _ // Strict mock + } + + def "prepare completed work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob + def lineMap = workRequest.lineNumbers().lineMap() + + then: + lineMap.size() == 2 + lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers() + lineMap.get("net/fabricmc/other/Test") == TestCachedData.lineNumbers() + + workJob.completed() != null + ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/Example.java") == "Example sources".bytes + ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/other/Test.java") == "Test sources".bytes + + // Provide one cached entry + // And then two calls not finding the entry in the cache + 1 * cache.getEntry(ExampleHash) >> ExampleCachedData + 1 * cache.getEntry(TestHash) >> TestCachedData + + 0 * _ // Strict mock + } + + def "complete full work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob + + // Do the work, such as decompiling. + ZipUtils.add(workJob.output(), "net/fabricmc/Example.java", "Example sources") + ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources") + + 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) + + then: + workJob.outputNameMap().size() == 2 + + 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 + 1 * cache.getEntry(ExampleHash) >> null + 1 * cache.getEntry(TestHash) >> null + + // Expect the new work to be put into the cache + 1 * cache.putEntry(ExampleHash, ExampleCachedData) + 1 * cache.putEntry(TestHash, TestCachedData) + + 0 * _ // Strict mock + } + + def "complete partial work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob + + // Do the work + ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources") + + 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) + + then: + workJob.outputNameMap().size() == 1 + + ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes + ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes + + // The cache already contains sources for example, but not for test + 1 * cache.getEntry(ExampleHash) >> ExampleCachedData + 1 * cache.getEntry(TestHash) >> null + + // Expect the new work to be put into the cache + 1 * cache.putEntry(TestHash, TestCachedData) + + 0 * _ // Strict mock + } + + def "complete completed work job"() { + given: + def jar = ZipTestUtils.createZip(jarEntries) + def cache = Mock(CachedFileStore) + def processor = new CachedJarProcessor(cache, "abc123") + + when: + def workRequest = processor.prepareJob(jar) + def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob + + 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) + + then: + ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes + ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes + + // The cache already contains sources for example, but not for test + 1 * cache.getEntry(ExampleHash) >> ExampleCachedData + 1 * cache.getEntry(TestHash) >> TestCachedData + + 0 * _ // Strict mock + } + + private static ClassLineNumbers lineNumbers(List names) { + return new ClassLineNumbers(names.collectEntries { [it, lineNumber(it)] }) + } + + private static ClassLineNumbers.Entry lineNumber(String name) { + return new ClassLineNumbers.Entry(name, 0, 0, [:]) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy new file mode 100644 index 00000000..c0c42284 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy @@ -0,0 +1,128 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.test.unit.fmj + +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import org.intellij.lang.annotations.Language +import spock.lang.Specification + +import net.fabricmc.loom.util.fmj.FabricModJsonUtils + +class FabricModJsonUtilsTest extends Specification { + // Test that the schemaVersion is moved to the first position + def "optimize FMJ"() { + given: + // Matches LoomGradlePlugin + def gson = new GsonBuilder().setPrettyPrinting().create() + def json = gson.fromJson(INPUT_FMJ, JsonObject.class) + when: + def outputJson = FabricModJsonUtils.optimizeFmj(json) + def output = gson.toJson(outputJson) + then: + output == OUTPUT_FMJ + true + } + + // schemaVersion is not first + @Language("json") + static String INPUT_FMJ = """ +{ + "id": "modid", + "version": "1.0.0", + "name": "Example mod", + "description": "This is an example description! Tell everyone what your mod is about!", + "license": "CC0-1.0", + "icon": "assets/modid/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.example.ExampleMod" + ], + "client": [ + "com.example.ExampleModClient" + ] + }, + "schemaVersion": 1, + "mixins": [ + "modid.mixins.json", + { + "config": "modid.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": "\\u003e\\u003d0.15.0", + "minecraft": "~1.20.4", + "java": "\\u003e\\u003d17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} + +""".trim() + + // schemaVersion is first, everything else is unchanged + @Language("json") + static String OUTPUT_FMJ = """ +{ + "schemaVersion": 1, + "id": "modid", + "version": "1.0.0", + "name": "Example mod", + "description": "This is an example description! Tell everyone what your mod is about!", + "license": "CC0-1.0", + "icon": "assets/modid/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.example.ExampleMod" + ], + "client": [ + "com.example.ExampleModClient" + ] + }, + "mixins": [ + "modid.mixins.json", + { + "config": "modid.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": "\\u003e\\u003d0.15.0", + "minecraft": "~1.20.4", + "java": "\\u003e\\u003d17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} + +""".trim() +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy index d99f352f..5afbf436 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy @@ -30,6 +30,7 @@ class IntermediaryMappingLayerTest extends LayeredMappingsSpecification { def "Read intermediary mappings" () { setup: intermediaryUrl = INTERMEDIARY_1_17_URL + mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17 when: def mappings = getSingleMapping(new IntermediaryMappingsSpec()) def tiny = getTiny(mappings) diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy index fffbc67a..c69e665e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy @@ -34,13 +34,13 @@ interface LayeredMappingsTestConstants { client_mappings: new MinecraftVersionMeta.Download(null, "227d16f520848747a59bef6f490ae19dc290a804", 6431705, "https://launcher.mojang.com/v1/objects/227d16f520848747a59bef6f490ae19dc290a804/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "84d80036e14bc5c7894a4fad9dd9f367d3000334", 4948536, "https://launcher.mojang.com/v1/objects/84d80036e14bc5c7894a4fad9dd9f367d3000334/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, "2021-06-08T11:00:40+00:00", null, null, null) public static final Map DOWNLOADS_1_16_5 = [ client_mappings: new MinecraftVersionMeta.Download(null, "e3dfb0001e1079a1af72ee21517330edf52e6192", 5746047, "https://launcher.mojang.com/v1/objects/e3dfb0001e1079a1af72ee21517330edf52e6192/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "81d5c793695d8cde63afddb40dde88e3a88132ac", 4400926, "https://launcher.mojang.com/v1/objects/81d5c793695d8cde63afddb40dde88e3a88132ac/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, "2021-01-14T16:05:32+00:00", null, null, null) public static final String PARCHMENT_NOTATION = "org.parchmentmc.data:parchment-1.16.5:20210608-SNAPSHOT@zip" public static final String PARCHMENT_URL = "https://maven.parchmentmc.net/org/parchmentmc/data/parchment-1.16.5/20210608-SNAPSHOT/parchment-1.16.5-20210608-SNAPSHOT.zip" diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy index 11e2a707..4062cfeb 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy @@ -45,7 +45,7 @@ trait GradleProjectTestTrait { String gradleVersion = options.version as String ?: LoomTestConstants.DEFAULT_GRADLE String warningMode = options.warningMode as String ?: "fail" File projectDir = options.projectDir as File ?: options.sharedFiles ? sharedProjectDir : File.createTempDir() - File gradleHomeDir = gradleHomeDir + File gradleHomeDir = options.gradleHomeDir as File ?: gradleHomeDir setupProject(options, projectDir) diff --git a/src/test/resources/mappings/1.2.5-intermediary.tiny.zip b/src/test/resources/mappings/1.2.5-intermediary.tiny.zip new file mode 100644 index 00000000..0086053b Binary files /dev/null and b/src/test/resources/mappings/1.2.5-intermediary.tiny.zip differ diff --git a/src/test/resources/mappings/ATTRIBUTIONS.md b/src/test/resources/mappings/ATTRIBUTIONS.md new file mode 100644 index 00000000..52005c30 --- /dev/null +++ b/src/test/resources/mappings/ATTRIBUTIONS.md @@ -0,0 +1,2 @@ +The file `1.2.5-intermediary.tiny` was taken from OrnitheMC's "[Calamus](https://github.com/OrnitheMC/calamus/blob/gen2/mappings/1.2.5.tiny)" intermediaries under the CC0 license. +The file was rewritten in Tiny V2 format, but the mappings are otherwise unmodified. diff --git a/src/test/resources/projects/runconfigs/build.gradle b/src/test/resources/projects/runconfigs/build.gradle index 679a3b23..c32435a4 100644 --- a/src/test/resources/projects/runconfigs/build.gradle +++ b/src/test/resources/projects/runconfigs/build.gradle @@ -49,4 +49,8 @@ dependencies { base { archivesName = "fabric-example-mod" +} + +runClient { + // Realise this task to ensure that the runConfig is lazily evaluated } \ No newline at end of file