diff options
Diffstat (limited to 'src/main/java/net/fabricmc/loom/configuration')
25 files changed, 1873 insertions, 141 deletions
diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 45d114e1..a3e7e448 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -30,6 +30,7 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.javadoc.Javadoc; import net.fabricmc.loom.LoomGradleExtension; @@ -39,7 +40,14 @@ import net.fabricmc.loom.build.mixin.ScalaApInvoker; import net.fabricmc.loom.configuration.ide.SetupIntelijRunConfigs; import net.fabricmc.loom.configuration.providers.LaunchProvider; import net.fabricmc.loom.configuration.providers.MinecraftProvider; +import net.fabricmc.loom.configuration.providers.forge.ForgeProvider; +import net.fabricmc.loom.configuration.providers.forge.ForgeUniversalProvider; +import net.fabricmc.loom.configuration.providers.forge.ForgeUserdevProvider; +import net.fabricmc.loom.configuration.providers.forge.McpConfigProvider; +import net.fabricmc.loom.configuration.providers.forge.PatchProvider; +import net.fabricmc.loom.configuration.providers.forge.SrgProvider; import net.fabricmc.loom.configuration.providers.mappings.MappingsProvider; +import net.fabricmc.loom.task.GenVsCodeProjectTask; import net.fabricmc.loom.util.Constants; public final class CompileConfiguration { @@ -61,8 +69,40 @@ public final class CompileConfiguration { Configuration minecraftConfig = project.getConfigurations().maybeCreate(Constants.Configurations.MINECRAFT); minecraftConfig.setTransitive(false); - Configuration includeConfig = project.getConfigurations().maybeCreate(Constants.Configurations.INCLUDE); - includeConfig.setTransitive(false); // Dont get transitive deps + project.afterEvaluate(project1 -> { + if (project.getExtensions().getByType(LoomGradleExtension.class).shouldGenerateSrgTiny()) { + Configuration srg = project.getConfigurations().maybeCreate(Constants.Configurations.SRG); + srg.setTransitive(false); + } + + if (project.getExtensions().getByType(LoomGradleExtension.class).isDataGenEnabled()) { + project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName("main").resources(files -> { + files.srcDir(project.file("src/generated/resources")); + }); + } + }); + + if (project.getExtensions().getByType(LoomGradleExtension.class).isForge()) { + Configuration forgeConfig = project.getConfigurations().maybeCreate(Constants.Configurations.FORGE); + forgeConfig.setTransitive(false); + Configuration forgeUserdevConfig = project.getConfigurations().maybeCreate(Constants.Configurations.FORGE_USERDEV); + forgeUserdevConfig.setTransitive(false); + Configuration forgeInstallerConfig = project.getConfigurations().maybeCreate(Constants.Configurations.FORGE_INSTALLER); + forgeInstallerConfig.setTransitive(false); + Configuration forgeUniversalConfig = project.getConfigurations().maybeCreate(Constants.Configurations.FORGE_UNIVERSAL); + forgeUniversalConfig.setTransitive(false); + Configuration forgeDependencies = project.getConfigurations().maybeCreate(Constants.Configurations.FORGE_DEPENDENCIES); + forgeDependencies.setTransitive(false); + Configuration mcpConfig = project.getConfigurations().maybeCreate(Constants.Configurations.MCP_CONFIG); + mcpConfig.setTransitive(false); + + extendsFrom(Constants.Configurations.MINECRAFT_DEPENDENCIES, Constants.Configurations.FORGE_DEPENDENCIES, project); + } + + if (project.getExtensions().getByType(LoomGradleExtension.class).supportsInclude()) { + Configuration includeConfig = project.getConfigurations().maybeCreate(Constants.Configurations.INCLUDE); + includeConfig.setTransitive(false); // Dont get transitive deps + } project.getConfigurations().maybeCreate(Constants.Configurations.MAPPING_CONSTANTS); extendsFrom(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, Constants.Configurations.MAPPING_CONSTANTS, project); @@ -117,6 +157,22 @@ public final class CompileConfiguration { extension.setDependencyManager(dependencyManager); dependencyManager.addProvider(new MinecraftProvider(project)); + + if (extension.isForge()) { + dependencyManager.addProvider(new ForgeProvider(project)); + dependencyManager.addProvider(new ForgeUserdevProvider(project)); + } + + if (extension.shouldGenerateSrgTiny()) { + dependencyManager.addProvider(new SrgProvider(project)); + } + + if (extension.isForge()) { + dependencyManager.addProvider(new McpConfigProvider(project)); + dependencyManager.addProvider(new PatchProvider(project)); + dependencyManager.addProvider(new ForgeUniversalProvider(project)); + } + dependencyManager.addProvider(new MappingsProvider(project)); dependencyManager.addProvider(new LaunchProvider(project)); @@ -127,6 +183,7 @@ public final class CompileConfiguration { project.getTasks().getByName("cleanEclipse").finalizedBy(project.getTasks().getByName("cleanEclipseRuns")); SetupIntelijRunConfigs.setup(project); + GenVsCodeProjectTask.generate(project); // Enables the default mod remapper if (extension.remapMod) { diff --git a/src/main/java/net/fabricmc/loom/configuration/DependencyProvider.java b/src/main/java/net/fabricmc/loom/configuration/DependencyProvider.java index 63ef8354..2b971427 100644 --- a/src/main/java/net/fabricmc/loom/configuration/DependencyProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/DependencyProvider.java @@ -190,7 +190,7 @@ public abstract class DependencyProvider { this.resolvedFiles = files; switch (files.size()) { case 0: //Don't think Gradle would ever let you do this - throw new IllegalStateException("Empty dependency?"); + throw new IllegalStateException("Empty dependency for " + configuration.getName()); case 1: //Single file dependency classifierToFile.put("", Iterables.getOnlyElement(files)); diff --git a/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java b/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java index 397908c5..f610c5ee 100644 --- a/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java +++ b/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java @@ -130,14 +130,15 @@ public class LoomDependencyManager { try { provider.provide(info, afterTasks::add); } catch (Exception e) { - throw new RuntimeException("Failed to provide " + dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion() + " : " + e.toString(), e); + throw new RuntimeException("Failed to provide " + dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion() + " : " + e.toString() + "\n\tEnsure minecraft is not open and try running with --refresh-dependencies. Use --stacktrace to see the full stacktrace.", e); } } } } SourceRemapper sourceRemapper = new SourceRemapper(project, true); - String mappingsKey = mappingsProvider.getMappingsKey(); + String platformSuffix = extension.isForge() ? "_forge" : ""; + String mappingsKey = mappingsProvider.getMappingsKey() + platformSuffix; if (extension.getInstallerJson() == null) { //If we've not found the installer JSON we've probably skipped remapping Fabric loader, let's go looking @@ -160,7 +161,7 @@ public class LoomDependencyManager { } } - if (extension.getInstallerJson() == null) { + if (extension.getInstallerJson() == null && !extension.isForge()) { project.getLogger().warn("fabric-installer.json not found in classpath!"); } diff --git a/src/main/java/net/fabricmc/loom/configuration/MavenConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/MavenConfiguration.java index 2fb2ae7c..118fd546 100644 --- a/src/main/java/net/fabricmc/loom/configuration/MavenConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/MavenConfiguration.java @@ -25,6 +25,7 @@ package net.fabricmc.loom.configuration; import org.gradle.api.Project; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import net.fabricmc.loom.LoomGradleExtension; @@ -52,6 +53,22 @@ public class MavenConfiguration { repo.setUrl("https://libraries.minecraft.net/"); }); + project.getRepositories().maven(repo -> { + repo.setName("Forge"); + repo.setUrl("https://files.minecraftforge.net/maven/"); + + repo.metadataSources(sources -> { + sources.mavenPom(); + + try { + MavenArtifactRepository.MetadataSources.class.getDeclaredMethod("ignoreGradleMetadataRedirection") + .invoke(sources); + } catch (Throwable ignored) { + // Method not available + } + }); + }); + project.getRepositories().mavenCentral(); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java b/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java index 92acad95..16cc4560 100644 --- a/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java +++ b/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java @@ -34,6 +34,7 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ExcludeRule; import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.logging.Logger; import org.gradle.api.publish.Publication; import org.gradle.api.publish.PublishingExtension; @@ -46,6 +47,14 @@ public final class MavenPublication { public static void configure(Project project) { project.afterEvaluate((p) -> { + // add modsCompile to maven-publish + PublishingExtension mavenPublish = p.getExtensions().findByType(PublishingExtension.class); + + if (mavenPublish == null) { + p.getLogger().info("No maven publications for project [" + p.getName() + "], skipping configuration."); + return; + } + for (RemappedConfigurationEntry entry : Constants.MOD_COMPILE_ENTRIES) { if (!entry.hasMavenScope()) { continue; @@ -53,23 +62,20 @@ public final class MavenPublication { Configuration compileModsConfig = p.getConfigurations().getByName(entry.getSourceConfiguration()); - // add modsCompile to maven-publish - PublishingExtension mavenPublish = p.getExtensions().findByType(PublishingExtension.class); - - if (mavenPublish != null) { - processEntry(entry, compileModsConfig, mavenPublish); - } + p.getLogger().info("Processing maven publication for project [" + p.getName() + "] of " + entry.getSourceConfiguration()); + processEntry(p.getLogger(), entry, compileModsConfig, mavenPublish); } }); } - private static void processEntry(RemappedConfigurationEntry entry, Configuration compileModsConfig, PublishingExtension mavenPublish) { + private static void processEntry(Logger logger, RemappedConfigurationEntry entry, Configuration compileModsConfig, PublishingExtension mavenPublish) { mavenPublish.publications((publications) -> { for (Publication publication : publications) { if (!(publication instanceof org.gradle.api.publish.maven.MavenPublication)) { continue; } + logger.info("Processing maven publication [" + publication.getName() + "]"); ((org.gradle.api.publish.maven.MavenPublication) publication).pom((pom) -> pom.withXml((xml) -> { Node dependencies = GroovyXmlUtil.getOrCreateNode(xml.asNode(), "dependencies"); Set<String> foundArtifacts = new HashSet<>(); @@ -85,9 +91,12 @@ public final class MavenPublication { for (Dependency dependency : compileModsConfig.getAllDependencies()) { if (foundArtifacts.contains(dependency.getGroup() + ":" + dependency.getName())) { + logger.info("Found inserted artifact " + dependency.getGroup() + ":" + dependency.getName()); continue; } + logger.info("Inserting artifact " + dependency.getGroup() + ":" + dependency.getName()); + Node depNode = dependencies.appendNode("dependency"); depNode.appendNode("groupId", dependency.getGroup()); depNode.appendNode("artifactId", dependency.getName()); diff --git a/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java index 7534c27b..7e1a1f3f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java @@ -27,11 +27,13 @@ package net.fabricmc.loom.configuration; import java.io.IOException; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.UnknownTaskException; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.api.tasks.bundling.Jar; import org.jetbrains.annotations.ApiStatus; import net.fabricmc.loom.LoomGradleExtension; @@ -74,6 +76,12 @@ public class RemapConfiguration { remapJarTask.getInput().set(jarTask.getArchivePath()); } + if (extension.isForge()) { + ((Jar) jarTask).manifest(manifest -> { + manifest.attributes(ImmutableMap.of("MixinConfigs", String.join(",", extension.mixinConfigs))); + }); + } + if (isDefaultRemap) { extension.getUnmappedModCollection().from(jarTask); remapJarTask.getAddNestedDependencies().set(true); @@ -87,7 +95,7 @@ public class RemapConfiguration { // TODO this might be wrong? project.getTasks().withType(RemapJarTask.class).forEach(task -> { - if (task.getAddNestedDependencies().getOrElse(false)) { + if (extension.supportsInclude() && task.getAddNestedDependencies().getOrElse(false)) { NestedDependencyProvider.getRequiredTasks(project).forEach(task::dependsOn); } }); @@ -115,7 +123,7 @@ public class RemapConfiguration { rootProject.getTasks().register(remapAllJarsTaskName, AbstractLoomTask.class, task -> { task.doLast(t -> { try { - jarRemapper.remap(); + jarRemapper.remap(project); } catch (IOException e) { throw new RuntimeException("Failed to remap jars", e); } @@ -139,9 +147,10 @@ public class RemapConfiguration { RemapSourcesJarTask remapSourcesJarTask = (RemapSourcesJarTask) project.getTasks().findByName(remapSourcesJarTaskName); Preconditions.checkNotNull(remapSourcesJarTask, "Could not find " + remapSourcesJarTaskName + " in " + project.getName()); - remapSourcesJarTask.setInput(sourcesTask.getArchivePath()); remapSourcesJarTask.setOutput(sourcesTask.getArchivePath()); - remapSourcesJarTask.dependsOn(project.getTasks().getByName(sourcesJarTaskName)); + sourcesTask.setClassifier(sourcesTask.getClassifier() == null ? "dev" : sourcesTask.getClassifier() + "-dev"); + remapSourcesJarTask.setInput(sourcesTask.getArchivePath()); + remapSourcesJarTask.dependsOn(sourcesTask); if (isDefaultRemap) { remapSourcesJarTask.doLast(task -> project.getArtifacts().add("archives", remapSourcesJarTask.getOutput())); diff --git a/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java b/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java index 936e6b7a..42f0e7bf 100644 --- a/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java @@ -55,6 +55,7 @@ import net.fabricmc.accesswidener.AccessWidenerVisitor; import net.fabricmc.accesswidener.AccessWidenerWriter; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.processors.JarProcessor; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.Constants; import net.fabricmc.tinyremapper.TinyRemapper; @@ -94,8 +95,10 @@ public class AccessWidenerJarProcessor implements JarProcessor { throw new UnsupportedOperationException(String.format("Access Widener namespace '%s' is not a valid namespace, it must be one of: '%s'", accessWidener.getNamespace(), String.join(", ", validNamespaces))); } - TinyRemapper tinyRemapper = loomGradleExtension.getMinecraftMappedProvider().getTinyRemapper("official", "named"); - tinyRemapper.readClassPath(loomGradleExtension.getMinecraftMappedProvider().getRemapClasspath()); + TinyRemapper tinyRemapper = loomGradleExtension.getMinecraftMappedProvider().getTinyRemapper(); + tinyRemapper.replaceMappings(loomGradleExtension.getMinecraftMappedProvider().getMappings(null, "official", "named")); + loomGradleExtension.getMinecraftMappedProvider(); + tinyRemapper.readClassPath(MinecraftMappedProvider.getRemapClasspath(project)); AccessWidenerRemapper remapper = new AccessWidenerRemapper(accessWidener, tinyRemapper.getRemapper(), "named"); accessWidener = remapper.remap(); 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 2fc79906..603de2db 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java @@ -24,12 +24,21 @@ package net.fabricmc.loom.configuration.ide; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -52,11 +61,14 @@ public class RunConfig { public String configName; public String eclipseProjectName; public String ideaModuleName; + public String vscodeProjectName; public String mainClass; public String runDirIdeaUrl; public String runDir; public String vmArgs; public String programArgs; + public List<String> vscodeBeforeRun = new ArrayList<>(); + public final Map<String, String> envVariables = new HashMap<>(); public SourceSet sourceSet; public Element genRuns(Element doc) { @@ -75,6 +87,14 @@ public class RunConfig { this.addXml(root, "option", ImmutableMap.of("name", "PROGRAM_PARAMETERS", "value", programArgs)); } + if (!envVariables.isEmpty()) { + Element envs = this.addXml(root, "envs", ImmutableMap.of()); + + for (Map.Entry<String, String> envEntry : envVariables.entrySet()) { + this.addXml(envs, "env", ImmutableMap.of("name", envEntry.getKey(), "value", envEntry.getValue())); + } + } + return root; } @@ -108,6 +128,7 @@ public class RunConfig { private static void populate(Project project, LoomGradleExtension extension, RunConfig runConfig, String environment) { runConfig.configName += extension.isRootProject() ? "" : " (" + project.getPath() + ")"; runConfig.eclipseProjectName = project.getExtensions().getByType(EclipseModel.class).getProject().getName(); + runConfig.vscodeProjectName = extension.isRootProject() ? "" : project.getPath(); runConfig.vmArgs = ""; runConfig.programArgs = ""; @@ -119,6 +140,23 @@ public class RunConfig { runConfig.vmArgs = "-Dfabric.dli.config=" + encodeEscaped(extension.getDevLauncherConfig().getAbsolutePath()) + " -Dfabric.dli.env=" + environment.toLowerCase(); } + if (extension.isForge()) { + List<String> modClasses = new ArrayList<>(); + + for (Supplier<SourceSet> sourceSetSupplier : extension.forgeLocalMods) { + SourceSet sourceSet = sourceSetSupplier.get(); + String sourceSetName = sourceSet.getName() + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 7); + + Stream.concat( + Stream.of(sourceSet.getOutput().getResourcesDir().getAbsolutePath()), + StreamSupport.stream(sourceSet.getOutput().getClassesDirs().spliterator(), false) + .map(File::getAbsolutePath) + ).map(s -> sourceSetName + "%%" + s).collect(Collectors.toCollection(() -> modClasses)); + } + + runConfig.envVariables.put("MOD_CLASSES", String.join(File.pathSeparator, modClasses)); + } + if (extension.getLoaderLaunchMethod().equals("launchwrapper")) { // if installer.json found... JsonObject installerJson = extension.getInstallerJson(); @@ -215,6 +253,10 @@ public class RunConfig { runConfig.programArgs = runConfig.programArgs.trim(); runConfig.vmArgs = runConfig.vmArgs.trim(); + for (Consumer<RunConfig> consumer : extension.settingsPostEdit) { + consumer.accept(runConfig); + } + return runConfig; } @@ -233,6 +275,25 @@ public class RunConfig { dummyConfig = dummyConfig.replace("%PROGRAM_ARGS%", programArgs.replaceAll("\"", """)); dummyConfig = dummyConfig.replace("%VM_ARGS%", vmArgs.replaceAll("\"", """)); + String envs = ""; + + if (!envVariables.isEmpty()) { + StringBuilder builder = new StringBuilder("<envs>"); + + for (Map.Entry<String, String> env : envVariables.entrySet()) { + builder.append("<env name=\""); + builder.append(env.getKey().replaceAll("\"", """)); + builder.append("\" value=\""); + builder.append(env.getValue().replaceAll("\"", """)); + builder.append("\"/>"); + } + + builder.append("</envs>"); + envs = builder.toString(); + } + + dummyConfig = dummyConfig.replace("%ENVS%", envs); + return dummyConfig; } 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 eb17466d..bfb62ae9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java @@ -267,7 +267,7 @@ public final class RunConfigSettings implements Named { public void client() { startFirstThread(); environment("client"); - defaultMainClass(Constants.Knot.KNOT_CLIENT); + defaultMainClass(getExtension().isForge() ? Constants.ForgeUserDev.LAUNCH_TESTING : Constants.Knot.KNOT_CLIENT); } /** @@ -276,7 +276,15 @@ public final class RunConfigSettings implements Named { public void server() { programArg("nogui"); environment("server"); - defaultMainClass(Constants.Knot.KNOT_SERVER); + defaultMainClass(getExtension().isForge() ? Constants.ForgeUserDev.LAUNCH_TESTING : Constants.Knot.KNOT_SERVER); + } + + /** + * Configure run config with the default server options. + */ + public void data() { + environment("data"); + defaultMainClass(getExtension().isForge() ? Constants.ForgeUserDev.LAUNCH_TESTING : Constants.Knot.KNOT_SERVER); } /** diff --git a/src/main/java/net/fabricmc/loom/configuration/launch/LaunchProviderSettings.java b/src/main/java/net/fabricmc/loom/configuration/launch/LaunchProviderSettings.java new file mode 100644 index 00000000..985a9db3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/launch/LaunchProviderSettings.java @@ -0,0 +1,78 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.launch; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.gradle.api.Named; +import org.gradle.api.Project; + +public class LaunchProviderSettings implements Named { + private final String name; + private List<Map.Entry<String, String>> properties = new ArrayList<>(); + private List<String> arguments = new ArrayList<>(); + + public LaunchProviderSettings(Project project, String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public void arg(String argument) { + this.arguments.add(argument); + } + + public void arg(String... arguments) { + this.arguments.addAll(Arrays.asList(arguments)); + } + + public void arg(Collection<String> arguments) { + this.arguments.addAll(arguments); + } + + public void property(String key, String value) { + this.properties.add(new AbstractMap.SimpleEntry<>(key, value)); + } + + public void properties(Map<String, String> arguments) { + this.properties.addAll(arguments.entrySet()); + } + + public List<Map.Entry<String, String>> getProperties() { + return properties; + } + + public List<String> getArguments() { + return arguments; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java index 367a49a6..990c042c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -35,9 +35,13 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.jar.Attributes; import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -60,7 +64,11 @@ import net.fabricmc.loom.configuration.processors.dependency.ModDependencyInfo; import net.fabricmc.loom.configuration.providers.mappings.MappingsProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.LoggerFilter; import net.fabricmc.loom.util.TinyRemapperMappingsHelper; +import net.fabricmc.loom.util.srg.AtRemapper; +import net.fabricmc.loom.util.srg.CoreModClassRemapper; +import net.fabricmc.mapping.tree.TinyTree; import net.fabricmc.tinyremapper.InputTag; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; @@ -97,6 +105,7 @@ public class ModProcessor { } private static void stripNestedJars(File file) { + if (!ZipUtil.containsEntry(file, "fabric.mod.json")) return; // Strip out all contained jar info as we dont want loader to try and load the jars contained in dev. ZipUtil.transformEntries(file, new ZipEntryTransformerEntry[] {(new ZipEntryTransformerEntry("fabric.mod.json", new StringZipEntryTransformer() { @Override @@ -129,24 +138,27 @@ public class ModProcessor { private static void remapJars(Project project, List<ModDependencyInfo> processList) throws IOException { LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); - String fromM = "intermediary"; + String fromM = extension.isForge() ? "srg" : "intermediary"; String toM = "named"; MinecraftMappedProvider mappedProvider = extension.getMinecraftMappedProvider(); MappingsProvider mappingsProvider = extension.getMappingsProvider(); - Path mc = mappedProvider.getIntermediaryJar().toPath(); + Path mc = extension.isForge() ? mappedProvider.getSrgJar().toPath() : mappedProvider.getIntermediaryJar().toPath(); Path[] mcDeps = project.getConfigurations().getByName(Constants.Configurations.LOADER_DEPENDENCIES).getFiles() - .stream().map(File::toPath).toArray(Path[]::new); + .stream().map(File::toPath).toArray(Path[]::new); List<ModDependencyInfo> remapList = processList.stream().filter(ModDependencyInfo::requiresRemapping).collect(Collectors.toList()); project.getLogger().lifecycle(":remapping " + remapList.size() + " mods (TinyRemapper, " + fromM + " -> " + toM + ")"); + TinyTree mappings = extension.isForge() ? mappingsProvider.getMappingsWithSrg() : mappingsProvider.getMappings(); + LoggerFilter.replaceSystemOut(); TinyRemapper remapper = TinyRemapper.newRemapper() - .withMappings(TinyRemapperMappingsHelper.create(mappingsProvider.getMappings(), fromM, toM, false)) - .renameInvalidLocals(false) - .build(); + .logger(project.getLogger()::lifecycle) + .withMappings(TinyRemapperMappingsHelper.create(mappings, fromM, toM, false)) + .renameInvalidLocals(false) + .build(); remapper.readClassPathAsync(mc); remapper.readClassPathAsync(mcDeps); @@ -198,10 +210,56 @@ public class ModProcessor { ZipUtil.replaceEntry(info.getRemappedOutput(), info.getAccessWidener(), accessWidener); } + if (extension.isForge()) { + AtRemapper.remap(project.getLogger(), info.getRemappedOutput().toPath(), mappings); + CoreModClassRemapper.remapJar(info.getRemappedOutput().toPath(), mappings, project.getLogger()); + + if (ZipUtil.containsEntry(info.getRemappedOutput(), "META-INF/MANIFEST.MF")) { + ZipUtil.transformEntry(info.getRemappedOutput(), "META-INF/MANIFEST.MF", (in, zipEntry, out) -> { + Manifest manifest = new Manifest(in); + fixManifest(manifest); + out.putNextEntry(new ZipEntry(zipEntry.getName())); + manifest.write(out); + out.closeEntry(); + }); + } + + List<String> filesToRemove = new ArrayList<>(); + ZipUtil.iterate(info.getRemappedOutput(), (in, zipEntry) -> { + if (zipEntry.getName().toLowerCase(Locale.ROOT).endsWith(".rsa") || zipEntry.getName().toLowerCase(Locale.ROOT).endsWith(".sf")) { + if (zipEntry.getName().startsWith("META-INF")) { + filesToRemove.add(zipEntry.getName()); + } + } + }); + ZipUtil.removeEntries(info.getRemappedOutput(), filesToRemove.toArray(new String[0])); + } + info.finaliseRemapping(); } } + private static void fixManifest(Manifest manifest) { + Attributes mainAttrs = manifest.getMainAttributes(); + + mainAttrs.remove(Attributes.Name.SIGNATURE_VERSION); + + for (Iterator<Attributes> it = manifest.getEntries().values().iterator(); it.hasNext(); ) { + Attributes attrs = it.next(); + + for (Iterator<Object> it2 = attrs.keySet().iterator(); it2.hasNext(); ) { + Attributes.Name attrName = (Attributes.Name) it2.next(); + String name = attrName.toString(); + + if (name.endsWith("-Digest") || name.contains("-Digest-") || name.equals("Magic")) { + it2.remove(); + } + } + + if (attrs.isEmpty()) it.remove(); + } + } + public static JsonObject readInstallerJson(File file, Project project) { try { LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java b/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java index adf85fd2..9673a9a4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java @@ -50,7 +50,9 @@ public class MinecraftProcessedProvider extends MinecraftMappedProvider { @Override protected void addDependencies(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) { - if (jarProcessorManager.isInvalid(projectMappedJar) || isRefreshDeps()) { + boolean isForgeAtDirty = getExtension().isForge() && getExtension().getMappingsProvider().patchedProvider.isAtDirty(); + + if (jarProcessorManager.isInvalid(projectMappedJar) || isRefreshDeps() || isForgeAtDirty) { getProject().getLogger().info(":processing mapped jar"); invalidateJars(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java index f4511160..a602612e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java @@ -45,6 +45,7 @@ import org.gradle.api.plugins.JavaPlugin; import net.fabricmc.loom.configuration.DependencyProvider; import net.fabricmc.loom.configuration.RemappedConfigurationEntry; +import net.fabricmc.loom.configuration.launch.LaunchProviderSettings; import net.fabricmc.loom.util.Constants; public class LaunchProvider extends DependencyProvider { @@ -69,6 +70,57 @@ public class LaunchProvider extends DependencyProvider { .argument("client", "--assetsDir") .argument("client", new File(getExtension().getUserCache(), "assets").getAbsolutePath()); + if (getExtension().isForge()) { + launchConfig + .property("fabric.yarnWithSrg.path", getExtension().getMappingsProvider().tinyMappingsWithSrg.toAbsolutePath().toString()) + + .argument("--fml.mcVersion") + .argument(getExtension().getMinecraftProvider().getMinecraftVersion()) + .argument("--fml.forgeVersion") + .argument(getExtension().getForgeProvider().getVersion().getForgeVersion()) + + .argument("client", "--launchTarget") + .argument("client", "fmluserdevclient") + + .argument("server", "--launchTarget") + .argument("server", "fmluserdevserver") + + .argument("data", "--launchTarget") + .argument("data", "fmluserdevdata") + .argument("data", "--all") + .argument("data", "--mod") + .argument("data", String.join(",", getExtension().getDataGenMods())) + .argument("data", "--output") + .argument("data", getProject().file("src/generated/resources").getAbsolutePath()) + + .property("mixin.env.remapRefMap", "true"); + + if (getExtension().useFabricMixin) { + launchConfig.property("mixin.forgeloom.inject.mappings.srg-named", getExtension().getMappingsProvider().mixinTinyMappingsWithSrg.getAbsolutePath()); + } else { + launchConfig.property("net.minecraftforge.gradle.GradleStart.srg.srg-mcp", getExtension().getMappingsProvider().srgToNamedSrg.getAbsolutePath()); + } + + List<String> mixinConfigs = getExtension().mixinConfigs; + + if (mixinConfigs != null) { + for (String config : mixinConfigs) { + launchConfig.argument("-mixin.config"); + launchConfig.argument(config); + } + } + } + + for (LaunchProviderSettings settings : getExtension().getLaunchConfigs()) { + for (String argument : settings.getArguments()) { + launchConfig.argument(settings.getName(), argument); + } + + for (Map.Entry<String, String> property : settings.getProperties()) { + launchConfig.property(settings.getName(), property.getKey(), property.getValue()); + } + } + //Enable ansi by default for idea and vscode if (new File(getProject().getRootDir(), ".vscode").exists() || new File(getProject().getRootDir(), ".idea").exists() @@ -83,6 +135,10 @@ public class LaunchProvider extends DependencyProvider { addDependency(Constants.Dependencies.TERMINAL_CONSOLE_APPENDER + Constants.Dependencies.Versions.TERMINAL_CONSOLE_APPENDER, Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES); annotationDependency = addDependency(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS, JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME); + if (getExtension().isForge()) { + addDependency(Constants.Dependencies.JAVAX_ANNOTATIONS + Constants.Dependencies.Versions.JAVAX_ANNOTATIONS, "compileOnly"); + } + postPopulationScheduler.accept(this::writeRemapClassPath); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProvider.java index 508e8c3e..226f3f17 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProvider.java @@ -34,6 +34,7 @@ import java.util.function.Consumer; import java.util.zip.ZipError; import com.google.common.io.Files; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -56,10 +57,13 @@ public class MinecraftProvider extends DependencyProvider { private MinecraftLibraryProvider libraryProvider; private File minecraftJson; - private File minecraftClientJar; - private File minecraftServerJar; + public File minecraftClientJar; + public File minecraftServerJar; private File minecraftMergedJar; private File versionManifestJson; + private String jarSuffix = ""; + + Gson gson = new Gson(); public MinecraftProvider(Project project) { super(project); @@ -69,6 +73,10 @@ public class MinecraftProvider extends DependencyProvider { public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { minecraftVersion = dependency.getDependency().getVersion(); + if (getExtension().shouldGenerateSrgTiny() && !getExtension().isForge()) { + addDependency("de.oceanlabs.mcp:mcp_config:" + minecraftVersion, Constants.Configurations.SRG); + } + boolean offline = getProject().getGradle().getStartParameter().isOffline(); initFiles(); @@ -119,6 +127,12 @@ public class MinecraftProvider extends DependencyProvider { versionManifestJson = new File(getExtension().getUserCache(), "version_manifest.json"); } + public void deleteFiles() { + DownloadUtil.delete(minecraftClientJar); + DownloadUtil.delete(minecraftServerJar); + DownloadUtil.delete(minecraftMergedJar); + } + private void downloadMcJson(boolean offline) throws IOException { if (getExtension().isShareCaches() && !getExtension().isRootProject() && versionManifestJson.exists() && !isRefreshDeps()) { return; @@ -241,6 +255,14 @@ public class MinecraftProvider extends DependencyProvider { return libraryProvider; } + public String getJarSuffix() { + return jarSuffix; + } + + public void setJarSuffix(String jarSuffix) { + this.jarSuffix = jarSuffix; + } + @Override public String getTargetConfig() { return Constants.Configurations.MINECRAFT; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeProvider.java new file mode 100644 index 00000000..d1e0d462 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeProvider.java @@ -0,0 +1,87 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.util.function.Consumer; + +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; + +public class ForgeProvider extends DependencyProvider { + private ForgeVersion version = new ForgeVersion(null); + + public ForgeProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + version = new ForgeVersion(dependency.getDependency().getVersion()); + addDependency(dependency.getDepString() + ":userdev", Constants.Configurations.FORGE_USERDEV); + addDependency(dependency.getDepString() + ":installer", Constants.Configurations.FORGE_INSTALLER); + } + + public ForgeVersion getVersion() { + return version; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.FORGE; + } + + public static final class ForgeVersion { + private final String minecraftVersion; + private final String forgeVersion; + + public ForgeVersion(String combined) { + if (combined == null) { + this.minecraftVersion = "NO_VERSION"; + this.forgeVersion = "NO_VERSION"; + return; + } + + int hyphenIndex = combined.indexOf('-'); + + if (hyphenIndex != -1) { + this.minecraftVersion = combined.substring(0, hyphenIndex); + this.forgeVersion = combined.substring(hyphenIndex + 1); + } else { + this.minecraftVersion = "NO_VERSION"; + this.forgeVersion = combined; + } + } + + public String getMinecraftVersion() { + return minecraftVersion; + } + + public String getForgeVersion() { + return forgeVersion; + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUniversalProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUniversalProvider.java new file mode 100644 index 00000000..f0c9ba0c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUniversalProvider.java @@ -0,0 +1,72 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.io.File; +import java.util.function.Consumer; + +import org.apache.commons.io.FileUtils; +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.JarUtil; + +public class ForgeUniversalProvider extends DependencyProvider { + private File forge; + private File forgeManifest; + + public ForgeUniversalProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + forge = new File(getExtension().getProjectPersistentCache(), "forge-" + dependency.getDependency().getVersion() + "-universal.jar"); + forgeManifest = new File(getExtension().getProjectPersistentCache(), "forge-" + dependency.getDependency().getVersion() + "-manifest.mf"); + + if (!forge.exists() || isRefreshDeps()) { + File dep = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge")); + FileUtils.copyFile(dep, forge); + } + + if (!forgeManifest.exists() || isRefreshDeps()) { + JarUtil.extractFile(forge, "META-INF/MANIFEST.MF", forgeManifest); + } + } + + public File getForge() { + return forge; + } + + public File getForgeManifest() { + return forgeManifest; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.FORGE_UNIVERSAL; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java new file mode 100644 index 00000000..de0394ce --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUserdevProvider.java @@ -0,0 +1,105 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.io.File; +import java.io.Reader; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; + +public class ForgeUserdevProvider extends DependencyProvider { + private File userdevJar; + + public ForgeUserdevProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + userdevJar = new File(getExtension().getProjectPersistentCache(), "forge-" + dependency.getDependency().getVersion() + "-userdev.jar"); + + Path configJson = getExtension() + .getProjectPersistentCache() + .toPath() + .resolve("forge-config-" + dependency.getDependency().getVersion() + ".json"); + + if (!userdevJar.exists() || Files.notExists(configJson) || isRefreshDeps()) { + File resolved = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge userdev")); + Files.copy(resolved.toPath(), userdevJar.toPath(), StandardCopyOption.REPLACE_EXISTING); + + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + resolved.toURI()), ImmutableMap.of("create", false))) { + Files.copy(fs.getPath("config.json"), configJson, StandardCopyOption.REPLACE_EXISTING); + } + } + + JsonObject json; + + try (Reader reader = Files.newBufferedReader(configJson)) { + json = new Gson().fromJson(reader, JsonObject.class); + } + + addDependency(json.get("mcp").getAsString(), Constants.Configurations.MCP_CONFIG); + addDependency(json.get("mcp").getAsString(), Constants.Configurations.SRG); + addDependency(json.get("universal").getAsString(), Constants.Configurations.FORGE_UNIVERSAL); + + for (JsonElement lib : json.get("libraries").getAsJsonArray()) { + if (lib.getAsString().startsWith("org.spongepowered:mixin:")) { + if (getExtension().useFabricMixin) { + addDependency("net.fabricmc:sponge-mixin:0.8.2+build.24", Constants.Configurations.FORGE_DEPENDENCIES); + continue; + } + } + + addDependency(lib.getAsString(), Constants.Configurations.FORGE_DEPENDENCIES); + } + + // TODO: Read launch configs from the JSON too + // TODO: Should I copy the patches from here as well? + // That'd require me to run the "MCP environment" fully up to merging. + } + + public File getUserdevJar() { + return userdevJar; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.FORGE_USERDEV; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java new file mode 100644 index 00000000..037f4710 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/McpConfigProvider.java @@ -0,0 +1,72 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Consumer; + +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; + +public class McpConfigProvider extends DependencyProvider { + private File mcp; + + public McpConfigProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + init(dependency.getDependency().getVersion()); + + if (mcp.exists() && !isRefreshDeps()) { + return; // No work for us to do here + } + + Path mcpZip = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve MCPConfig")).toPath(); + + if (!mcp.exists() || isRefreshDeps()) { + Files.copy(mcpZip, mcp.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + + private void init(String version) { + mcp = new File(getExtension().getUserCache(), "mcp-" + version + ".zip"); + } + + public File getMcp() { + return mcp; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.MCP_CONFIG; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java new file mode 100644 index 00000000..cccfc7f8 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java @@ -0,0 +1,585 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Field; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonParser; +import de.oceanlabs.mcp.mcinjector.adaptors.ParameterAnnotationFixer; +import net.minecraftforge.accesstransformer.AccessTransformerEngine; +import net.minecraftforge.accesstransformer.TransformerProcessor; +import net.minecraftforge.accesstransformer.parser.AccessTransformerList; +import net.minecraftforge.binarypatcher.ConsoleTool; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.NullOutputStream; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; +import org.zeroturnaround.zip.ZipUtil; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.configuration.providers.MinecraftProvider; +import net.fabricmc.loom.configuration.providers.mappings.MappingsProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; +import net.fabricmc.loom.util.Checksum; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.JarUtil; +import net.fabricmc.loom.util.ThreadingUtils; +import net.fabricmc.loom.util.TinyRemapperMappingsHelper; +import net.fabricmc.loom.util.function.FsPathConsumer; +import net.fabricmc.loom.util.srg.InnerClassRemapper; +import net.fabricmc.loom.util.srg.SpecialSourceExecutor; +import net.fabricmc.mapping.tree.TinyTree; +import net.fabricmc.tinyremapper.OutputConsumerPath; +import net.fabricmc.tinyremapper.TinyRemapper; + +public class MinecraftPatchedProvider extends DependencyProvider { + private final MappingsProvider mappingsProvider; + // Step 1: Remap Minecraft to SRG + private File minecraftClientSrgJar; + private File minecraftServerSrgJar; + // Step 2: Binary Patch + private File minecraftClientPatchedSrgJar; + private File minecraftServerPatchedSrgJar; + // Step 3: Access Transform + private File minecraftClientPatchedSrgATJar; + private File minecraftServerPatchedSrgATJar; + // Step 4: Remap Patched AT to Official + private File minecraftClientPatchedOfficialJar; + private File minecraftServerPatchedOfficialJar; + // Step 5: Merge + private File minecraftMergedPatchedJar; + private File projectAtHash; + @Nullable + private File projectAt = null; + private boolean atDirty = false; + + public MinecraftPatchedProvider(MappingsProvider mappingsProvider, Project project) { + super(project); + this.mappingsProvider = mappingsProvider; + } + + public void initFiles() throws IOException { + projectAtHash = new File(getExtension().getProjectPersistentCache(), "at.sha256"); + + SourceSet main = getProject().getConvention().findPlugin(JavaPluginConvention.class).getSourceSets().getByName("main"); + + for (File srcDir : main.getResources().getSrcDirs()) { + File projectAt = new File(srcDir, "META-INF/accesstransformer.cfg"); + + if (projectAt.exists()) { + this.projectAt = projectAt; + break; + } + } + + if (isRefreshDeps() || !projectAtHash.exists()) { + writeAtHash(); + atDirty = projectAt != null; + } else { + byte[] expected = com.google.common.io.Files.asByteSource(projectAtHash).read(); + byte[] current = projectAt != null ? Checksum.sha256(projectAt) : Checksum.sha256(""); + boolean mismatched = !Arrays.equals(current, expected); + + if (mismatched) { + writeAtHash(); + } + + atDirty = mismatched; + } + + MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider(); + PatchProvider patchProvider = getExtension().getPatchProvider(); + String minecraftVersion = minecraftProvider.getMinecraftVersion(); + String jarSuffix = "-patched-forge-" + patchProvider.forgeVersion; + + if (getExtension().useFabricMixin) { + jarSuffix += "-fabric-mixin"; + } + + minecraftProvider.setJarSuffix(jarSuffix); + + File globalCache = getExtension().getUserCache(); + File cache = usesProjectCache() ? getExtension().getProjectPersistentCache() : globalCache; + + minecraftClientSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-client-srg.jar"); + minecraftServerSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-server-srg.jar"); + minecraftClientPatchedSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-client-srg" + jarSuffix + ".jar"); + minecraftServerPatchedSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-server-srg" + jarSuffix + ".jar"); + minecraftClientPatchedSrgATJar = new File(cache, "minecraft-" + minecraftVersion + "-client-srg-at" + jarSuffix + ".jar"); + minecraftServerPatchedSrgATJar = new File(cache, "minecraft-" + minecraftVersion + "-server-srg-at" + jarSuffix + ".jar"); + minecraftClientPatchedOfficialJar = new File(cache, "minecraft-" + minecraftVersion + "-client" + jarSuffix + ".jar"); + minecraftServerPatchedOfficialJar = new File(cache, "minecraft-" + minecraftVersion + "-server" + jarSuffix + ".jar"); + minecraftMergedPatchedJar = new File(cache, "minecraft-" + minecraftVersion + "-merged" + jarSuffix + ".jar"); + + if (isRefreshDeps() || Stream.of(getGlobalCaches()).anyMatch(Predicates.not(File::exists))) { + cleanAllCache(); + } else if (atDirty || Stream.of(getProjectCache()).anyMatch(Predicates.not(File::exists))) { + cleanProjectCache(); + } + } + + public void cleanAllCache() { + for (File file : getGlobalCaches()) { + file.delete(); + } + + cleanProjectCache(); + } + + private File[] getGlobalCaches() { + return new File[] { + minecraftClientSrgJar, + minecraftServerSrgJar, + minecraftClientPatchedSrgJar, + minecraftServerPatchedSrgJar + }; + } + + public void cleanProjectCache() { + for (File file : getProjectCache()) { + file.delete(); + } + } + + private File[] getProjectCache() { + return new File[] { + minecraftClientPatchedSrgATJar, + minecraftServerPatchedSrgATJar, + minecraftClientPatchedOfficialJar, + minecraftServerPatchedOfficialJar, + minecraftMergedPatchedJar + }; + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + initFiles(); + + if (atDirty) { + getProject().getLogger().lifecycle(":found dirty access transformers"); + } + + boolean dirty = false; + + if (!minecraftClientSrgJar.exists() || !minecraftServerSrgJar.exists()) { + dirty = true; + // Remap official jars to MCPConfig remapped srg jars + createSrgJars(getProject().getLogger()); + } + + if (!minecraftClientPatchedSrgJar.exists() || !minecraftServerPatchedSrgJar.exists()) { + dirty = true; + patchJars(getProject().getLogger()); + injectForgeClasses(getProject().getLogger()); + } + + if (atDirty || !minecraftClientPatchedSrgATJar.exists() || !minecraftServerPatchedSrgATJar.exists()) { + dirty = true; + accessTransformForge(getProject().getLogger()); + } + + if (dirty) { + remapPatchedJars(getProject().getLogger()); + } + + if (dirty || !minecraftMergedPatchedJar.exists()) { + mergeJars(getProject().getLogger()); + } + } + + private void writeAtHash() throws IOException { + try (FileOutputStream out = new FileOutputStream(projectAtHash)) { + if (projectAt != null) { + out.write(Checksum.sha256(projectAt)); + } else { + out.write(Checksum.sha256("")); + } + } + } + + private void createSrgJars(Logger logger) throws Exception { + McpConfigProvider mcpProvider = getExtension().getMcpConfigProvider(); + + MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider(); + + String[] mappingsPath = {null}; + + if (!ZipUtil.handle(mcpProvider.getMcp(), "config.json", (in, zipEntry) -> { + mappingsPath[0] = new JsonParser().parse(new InputStreamReader(in)).getAsJsonObject().get("data").getAsJsonObject().get("mappings").getAsString(); + })) { + throw new IllegalStateException("Failed to find 'config.json' in " + mcpProvider.getMcp().getAbsolutePath() + "!"); + } + + Path[] tmpSrg = {null}; + + if (!ZipUtil.handle(mcpProvider.getMcp(), mappingsPath[0], (in, zipEntry) -> { + tmpSrg[0] = Files.createTempFile(null, null); + + try (BufferedWriter writer = Files.newBufferedWriter(tmpSrg[0])) { + IOUtils.copy(in, writer, StandardCharsets.UTF_8); + } + })) { + throw new IllegalStateException("Failed to find mappings '" + mappingsPath[0] + "' in " + mcpProvider.getMcp().getAbsolutePath() + "!"); + } + + File specialSourceJar = new File(getExtension().getUserCache(), "SpecialSource-1.8.3-shaded.jar"); + DownloadUtil.downloadIfChanged(new URL("https://repo1.maven.org/maven2/net/md-5/SpecialSource/1.8.3/SpecialSource-1.8.3-shaded.jar"), specialSourceJar, getProject().getLogger(), true); + + ThreadingUtils.run(() -> { + Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), mappingsProvider, "client", specialSourceJar, minecraftProvider.minecraftClientJar.toPath(), tmpSrg[0]), minecraftClientSrgJar.toPath()); + }, () -> { + Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), mappingsProvider, "server", specialSourceJar, minecraftProvider.minecraftServerJar.toPath(), tmpSrg[0]), minecraftServerSrgJar.toPath()); + }); + } + + private void fixParameterAnnotation(File jarFile) throws Exception { + getProject().getLogger().info(":fixing parameter annotations for " + jarFile.toString()); + + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + jarFile.toURI()), ImmutableMap.of("create", false))) { + for (Path rootDir : fs.getRootDirectories()) { + for (Path file : (Iterable<? extends Path>) Files.walk(rootDir)::iterator) { + if (!file.toString().endsWith(".class")) continue; + byte[] bytes = Files.readAllBytes(file); + ClassReader reader = new ClassReader(bytes); + ClassNode node = new ClassNode(); + ClassVisitor visitor = new ParameterAnnotationFixer(node, null); + reader.accept(visitor, 0); + + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + node.accept(writer); + byte[] out = writer.toByteArray(); + + if (!Arrays.equals(bytes, out)) { + Files.delete(file); + Files.write(file, out); + } + } + } + } + } + + private void injectForgeClasses(Logger logger) throws IOException { + logger.lifecycle(":injecting forge classes into minecraft"); + ThreadingUtils.run(Arrays.asList(Environment.values()), environment -> { + copyAll(getExtension().getForgeUniversalProvider().getForge(), environment.patchedSrgJar.apply(this)); + copyUserdevFiles(getExtension().getForgeUserdevProvider().getUserdevJar(), environment.patchedSrgJar.apply(this)); + }); + + logger.lifecycle(":injecting loom classes into minecraft"); + File injection = File.createTempFile("loom-injection", ".jar"); + + try (InputStream in = MinecraftProvider.class.getResourceAsStream("/inject/injection.jar")) { + FileUtils.copyInputStreamToFile(in, injection); + } + + for (Environment environment : Environment.values()) { + String side = environment.side(); + File target = environment.patchedSrgJar.apply(this); + walkFileSystems(injection, target, it -> { + if (it.getFileName().toString().equals("MANIFEST.MF")) { + return false; + } + + return getExtension().useFabricMixin || !it.getFileName().toString().endsWith("cpw.mods.modlauncher.api.ITransformationService"); + }, this::copyReplacing); + } + } + + private void accessTransformForge(Logger logger) throws Exception { + for (Environment environment : Environment.values()) { + String side = environment.side(); + logger.lifecycle(":access transforming minecraft (" + side + ")"); + + File input = environment.patchedSrgJar.apply(this); + File inputCopied = File.createTempFile("at" + side, ".jar"); + FileUtils.copyFile(input, inputCopied); + File target = environment.patchedSrgATJar.apply(this); + target.delete(); + File at = File.createTempFile("at" + side, ".cfg"); + JarUtil.extractFile(inputCopied, "META-INF/accesstransformer.cfg", at); + String[] args = new String[] { + "--inJar", inputCopied.getAbsolutePath(), + "--outJar", target.getAbsolutePath(), + "--atFile", at.getAbsolutePath() + }; + + if (usesProjectCache()) { + args = Arrays.copyOf(args, args.length + 2); + args[args.length - 2] = "--atFile"; + args[args.length - 1] = projectAt.getAbsolutePath(); + } + + resetAccessTransformerEngine(); + TransformerProcessor.main(args); + inputCopied.delete(); + } + } + + private void resetAccessTransformerEngine() throws Exception { + // Thank you Forge, I love you + Field field = AccessTransformerEngine.class.getDeclaredField("masterList"); + field.setAccessible(true); + AccessTransformerList list = (AccessTransformerList) field.get(AccessTransformerEngine.INSTANCE); + field = AccessTransformerList.class.getDeclaredField("accessTransformers"); + field.setAccessible(true); + ((Map<?, ?>) field.get(list)).clear(); + } + + private enum Environment { + CLIENT(provider -> provider.minecraftClientSrgJar, + provider -> provider.minecraftClientPatchedSrgJar, + provider -> provider.minecraftClientPatchedSrgATJar, + provider -> provider.minecraftClientPatchedOfficialJar + ), + SERVER(provider -> provider.minecraftServerSrgJar, + provider -> provider.minecraftServerPatchedSrgJar, + provider -> provider.minecraftServerPatchedSrgATJar, + provider -> provider.minecraftServerPatchedOfficialJar + ); + + final Function<MinecraftPatchedProvider, File> srgJar; + final Function<MinecraftPatchedProvider, File> patchedSrgJar; + final Function<MinecraftPatchedProvider, File> patchedSrgATJar; + final Function<MinecraftPatchedProvider, File> patchedOfficialJar; + + Environment(Function<MinecraftPatchedProvider, File> srgJar, + Function<MinecraftPatchedProvider, File> patchedSrgJar, + Function<MinecraftPatchedProvider, File> patchedSrgATJar, + Function<MinecraftPatchedProvider, File> patchedOfficialJar) { + this.srgJar = srgJar; + this.patchedSrgJar = patchedSrgJar; + this.patchedSrgATJar = patchedSrgATJar; + this.patchedOfficialJar = patchedOfficialJar; + } + + public String side() { + return name().toLowerCase(Locale.ROOT); + } + } + + private void remapPatchedJars(Logger logger) throws Exception { + Path[] libraries = MinecraftMappedProvider.getRemapClasspath(getProject()); + + ThreadingUtils.run(Arrays.asList(Environment.values()), environment -> { + logger.lifecycle(":remapping minecraft (TinyRemapper, " + environment.side() + ", srg -> official)"); + TinyTree mappingsWithSrg = getExtension().getMappingsProvider().getMappingsWithSrg(); + + Path input = environment.patchedSrgATJar.apply(this).toPath(); + Path output = environment.patchedOfficialJar.apply(this).toPath(); + + Files.deleteIfExists(output); + + TinyRemapper remapper = TinyRemapper.newRemapper() + .logger(getProject().getLogger()::lifecycle) + .withMappings(TinyRemapperMappingsHelper.create(mappingsWithSrg, "srg", "official", true)) + .withMappings(InnerClassRemapper.of(input, mappingsWithSrg, "srg", "official")) + .renameInvalidLocals(true) + .rebuildSourceFilenames(true) + .fixPackageAccess(true) + .build(); + + try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(output).build()) { + outputConsumer.addNonClassFiles(input); + + remapper.readClassPath(libraries); + remapper.readInputs(input); + remapper.apply(outputConsumer); + } finally { + remapper.finish(); + } + }); + } + + private void patchJars(Logger logger) throws IOException { + logger.lifecycle(":patching jars"); + + PatchProvider patchProvider = getExtension().getPatchProvider(); + patchJars(minecraftClientSrgJar, minecraftClientPatchedSrgJar, patchProvider.clientPatches); + patchJars(minecraftServerSrgJar, minecraftServerPatchedSrgJar, patchProvider.serverPatches); + + ThreadingUtils.run(Arrays.asList(Environment.values()), environment -> { + copyMissingClasses(environment.srgJar.apply(this), environment.patchedSrgJar.apply(this)); + fixParameterAnnotation(environment.patchedSrgJar.apply(this)); + }); + } + + private void patchJars(File clean, File output, Path patches) throws IOException { + PrintStream previous = System.out; + + try { + System.setOut(new PrintStream(new NullOutputStream())); + } catch (SecurityException ignored) { + // Failed to replace logger filter, just ignore + } + + ConsoleTool.main(new String[] { + "--clean", clean.getAbsolutePath(), + "--output", output.getAbsolutePath(), + "--apply", patches.toAbsolutePath().toString() + }); + + try { + System.setOut(previous); + } catch (SecurityException ignored) { + // Failed to replace logger filter, just ignore + } + } + + private void mergeJars(Logger logger) throws IOException { + // FIXME: Hack here: There are no server-only classes so we can just copy the client JAR. + FileUtils.copyFile(minecraftClientPatchedOfficialJar, minecraftMergedPatchedJar); + + logger.lifecycle(":copying resources"); + + // Copy resources + MinecraftProvider minecraftProvider = getExtension().getMinecraftProvider(); + copyNonClassFiles(minecraftProvider.minecraftClientJar, minecraftMergedPatchedJar); + copyNonClassFiles(minecraftProvider.minecraftServerJar, minecraftMergedPatchedJar); + } + + private void walkFileSystems(File source, File target, Predicate<Path> filter, Function<FileSystem, Iterable<Path>> toWalk, FsPathConsumer action) + throws IOException { + try (FileSystemUtil.FileSystemDelegate sourceFs = FileSystemUtil.getJarFileSystem(source, false); + FileSystemUtil.FileSystemDelegate targetFs = FileSystemUtil.getJarFileSystem(target, false)) { + for (Path sourceDir : toWalk.apply(sourceFs.get())) { + Path dir = sourceDir.toAbsolutePath(); + Files.walk(dir) + .filter(Files::isRegularFile) + .filter(filter) + .forEach(it -> { + boolean root = dir.getParent() == null; + + try { + Path relativeSource = root ? it : dir.relativize(it); + Path targetPath = targetFs.get().getPath(relativeSource.toString()); + action.accept(sourceFs.get(), targetFs.get(), it, targetPath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } + } + + private void walkFileSystems(File source, File target, Predicate<Path> filter, FsPathConsumer action) throws IOException { + walkFileSystems(source, target, filter, FileSystem::getRootDirectories, action); + } + + private void copyAll(File source, File target) throws IOException { + walkFileSystems(source, target, it -> true, this::copyReplacing); + } + + private void copyMissingClasses(File source, File target) throws IOException { + walkFileSystems(source, target, it -> it.toString().endsWith(".class"), (sourceFs, targetFs, sourcePath, targetPath) -> { + if (Files.exists(targetPath)) return; + Path parent = targetPath.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + Files.copy(sourcePath, targetPath); + }); + } + + private void copyNonClassFiles(File source, File target) throws IOException { + walkFileSystems(source, target, it -> !it.toString().endsWith(".class"), this::copyReplacing); + } + + private void copyReplacing(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException { + Path parent = targetPath.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + + private void copyUserdevFiles(File source, File target) throws IOException { + walkFileSystems(source, target, file -> true, fs -> Collections.singleton(fs.getPath("inject")), (sourceFs, targetFs, sourcePath, targetPath) -> { + Path parent = targetPath.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + Files.copy(sourcePath, targetPath); + }); + } + + public File getMergedJar() { + return minecraftMergedPatchedJar; + } + + public boolean usesProjectCache() { + return projectAt != null; + } + + public boolean isAtDirty() { + return atDirty; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.MINECRAFT; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java new file mode 100644 index 00000000..24f1c866 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java @@ -0,0 +1,76 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableMap; +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; + +public class PatchProvider extends DependencyProvider { + public Path clientPatches; + public Path serverPatches; + public String forgeVersion; + + public PatchProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + init(dependency.getDependency().getVersion()); + + if (Files.notExists(clientPatches) || Files.notExists(serverPatches) || isRefreshDeps()) { + getProject().getLogger().info(":extracting forge patches"); + + Path installerJar = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge installer")).toPath(); + + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + installerJar.toUri()), ImmutableMap.of("create", false))) { + Files.copy(fs.getPath("data", "client.lzma"), clientPatches, StandardCopyOption.REPLACE_EXISTING); + Files.copy(fs.getPath("data", "server.lzma"), serverPatches, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private void init(String forgeVersion) { + this.forgeVersion = forgeVersion; + clientPatches = getExtension().getProjectPersistentCache().toPath().resolve("patches-" + forgeVersion + "-client.lzma"); + serverPatches = getExtension().getProjectPersistentCache().toPath().resolve("patches-" + forgeVersion + "-server.lzma"); + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.FORGE_INSTALLER; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java new file mode 100644 index 00000000..1fe73c21 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/SrgProvider.java @@ -0,0 +1,78 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 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.forge; + +import java.io.File; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableMap; +import org.gradle.api.Project; + +import net.fabricmc.loom.configuration.DependencyProvider; +import net.fabricmc.loom.util.Constants; + +public class SrgProvider extends DependencyProvider { + private File srg; + + public SrgProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + init(dependency.getDependency().getVersion()); + + if (srg.exists() && !isRefreshDeps()) { + return; // No work for us to do here + } + + Path srgZip = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve srg")).toPath(); + + if (!srg.exists() || isRefreshDeps()) { + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + srgZip.toUri()), ImmutableMap.of("create", false))) { + Files.copy(fs.getPath("config", "joined.tsrg"), srg.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private void init(String version) { + srg = new File(getExtension().getUserCache(), "srg-" + version + ".tsrg"); + } + + public File getSrg() { + return srg; + } + + @Override + public String getTargetConfig() { + return Constants.Configurations.SRG; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProvider.java index 448082b9..40ae05d0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProvider.java @@ -32,9 +32,12 @@ import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import com.google.common.base.Preconditions; @@ -43,6 +46,7 @@ import com.google.gson.JsonObject; import org.apache.commons.io.FileUtils; import org.apache.tools.ant.util.StringUtils; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; import org.zeroturnaround.zip.FileSource; import org.zeroturnaround.zip.ZipEntrySource; import org.zeroturnaround.zip.ZipUtil; @@ -54,19 +58,27 @@ import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor; import net.fabricmc.loom.configuration.processors.JarProcessorManager; import net.fabricmc.loom.configuration.processors.MinecraftProcessedProvider; import net.fabricmc.loom.configuration.providers.MinecraftProvider; +import net.fabricmc.loom.configuration.providers.forge.MinecraftPatchedProvider; +import net.fabricmc.loom.configuration.providers.forge.SrgProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.DeletingFileVisitor; import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.srg.MCPReader; +import net.fabricmc.loom.util.srg.SrgMerger; +import net.fabricmc.loom.util.srg.SrgNamedWriter; import net.fabricmc.mapping.reader.v2.TinyV2Factory; import net.fabricmc.mapping.tree.TinyTree; import net.fabricmc.stitch.Command; import net.fabricmc.stitch.commands.CommandProposeFieldNames; import net.fabricmc.stitch.commands.tinyv2.CommandMergeTinyV2; import net.fabricmc.stitch.commands.tinyv2.CommandReorderTinyV2; +import net.fabricmc.stitch.commands.tinyv2.TinyFile; +import net.fabricmc.stitch.commands.tinyv2.TinyV2Writer; public class MappingsProvider extends DependencyProvider { public MinecraftMappedProvider mappedProvider; + public MinecraftPatchedProvider patchedProvider; public String mappingsName; public String minecraftVersion; @@ -81,6 +93,11 @@ public class MappingsProvider extends DependencyProvider { // The mappings we use in practice public File tinyMappings; public File tinyMappingsJar; + public File mappingsMixinExport; + public Path tinyMappingsWithSrg; + public File mixinTinyMappingsWithSrg; // FORGE: The mixin mappings have srg names in intermediary. + public File srgToNamedSrg; // FORGE: srg to named in srg file format + private File unpickDefinitionsFile; private boolean hasUnpickDefinitions; private UnpickMetadata unpickMetadata; @@ -99,6 +116,14 @@ public class MappingsProvider extends DependencyProvider { return MappingsCache.INSTANCE.get(tinyMappings.toPath()); } + public TinyTree getMappingsWithSrg() throws IOException { + if (getExtension().shouldGenerateSrgTiny()) { + return MappingsCache.INSTANCE.get(tinyMappingsWithSrg); + } + + throw new UnsupportedOperationException("Not running with Forge support / Tiny srg support."); + } + @Override public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { MinecraftProvider minecraftProvider = getDependencyManager().getProvider(MinecraftProvider.class); @@ -113,6 +138,13 @@ public class MappingsProvider extends DependencyProvider { boolean isV2; + if (isMCP(mappingsJar.toPath())) { + File old = mappingsJar; + mappingsJar = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".zip") + "-" + minecraftVersion + ".jar").toFile(); + FileUtils.copyFile(old, mappingsJar); + mappingsName += "-" + minecraftVersion; + } + // Only do this for official yarn, there isn't really a way we can get the mc version for all mappings if (dependency.getDependency().getGroup() != null && dependency.getDependency().getGroup().equals("net.fabricmc") && dependency.getDependency().getName().equals("yarn") && dependency.getDependency().getVersion() != null) { String yarnVersion = dependency.getDependency().getVersion(); @@ -150,9 +182,12 @@ public class MappingsProvider extends DependencyProvider { tinyMappings = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".jar") + ".tiny").toFile(); unpickDefinitionsFile = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".jar") + ".unpick").toFile(); tinyMappingsJar = new File(getExtension().getUserCache(), mappingsJar.getName().replace(".jar", "-" + jarClassifier + ".jar")); + tinyMappingsWithSrg = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".jar") + "-srg.tiny"); + mixinTinyMappingsWithSrg = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".jar") + "-mixin-srg.tiny").toFile(); + srgToNamedSrg = mappingsDir.resolve(StringUtils.removeSuffix(mappingsJar.getName(), ".jar") + "-srg-named.srg").toFile(); if (!tinyMappings.exists() || isRefreshDeps()) { - storeMappings(getProject(), minecraftProvider, mappingsJar.toPath()); + storeMappings(getProject(), minecraftProvider, mappingsJar.toPath(), postPopulationScheduler); } else { try (FileSystem fileSystem = FileSystems.newFileSystem(mappingsJar.toPath(), (ClassLoader) null)) { extractUnpickDefinitions(fileSystem, unpickDefinitionsFile.toPath()); @@ -174,6 +209,29 @@ public class MappingsProvider extends DependencyProvider { populateUnpickClasspath(); } + if (getExtension().shouldGenerateSrgTiny()) { + if (Files.notExists(tinyMappingsWithSrg) || isRefreshDeps()) { + SrgMerger.mergeSrg(getExtension().getSrgProvider().getSrg().toPath(), tinyMappings.toPath(), tinyMappingsWithSrg, true); + } + } + + if (getExtension().isForge()) { + if (!getExtension().shouldGenerateSrgTiny()) { + throw new IllegalStateException("We have to generate srg tiny in a forge environment!"); + } + + if (!mixinTinyMappingsWithSrg.exists() || isRefreshDeps()) { + List<String> lines = new ArrayList<>(Files.readAllLines(tinyMappingsWithSrg)); + lines.set(0, lines.get(0).replace("intermediary", "yraidemretni").replace("srg", "intermediary")); + Files.deleteIfExists(mixinTinyMappingsWithSrg.toPath()); + Files.write(mixinTinyMappingsWithSrg.toPath(), lines); + } + + if (!srgToNamedSrg.exists() || isRefreshDeps()) { + SrgNamedWriter.writeTo(getProject().getLogger(), srgToNamedSrg.toPath(), getMappingsWithSrg(), "srg", "named"); + } + } + addDependency(tinyMappingsJar, Constants.Configurations.MAPPINGS_FINAL); LoomGradleExtension extension = getExtension(); @@ -186,7 +244,12 @@ public class MappingsProvider extends DependencyProvider { extension.setJarProcessorManager(processorManager); processorManager.setupProcessors(); - if (processorManager.active()) { + if (extension.isForge()) { + patchedProvider = new MinecraftPatchedProvider(this, getProject()); + patchedProvider.provide(dependency, postPopulationScheduler); + } + + if (processorManager.active() || (extension.isForge() && patchedProvider.usesProjectCache())) { mappedProvider = new MinecraftProcessedProvider(getProject(), processorManager); getProject().getLogger().lifecycle("Using project based jar storage"); } else { @@ -197,9 +260,15 @@ public class MappingsProvider extends DependencyProvider { mappedProvider.provide(dependency, postPopulationScheduler); } - private void storeMappings(Project project, MinecraftProvider minecraftProvider, Path yarnJar) throws IOException { + private void storeMappings(Project project, MinecraftProvider minecraftProvider, Path yarnJar, Consumer<Runnable> postPopulationScheduler) + throws Exception { project.getLogger().info(":extracting " + yarnJar.getFileName()); + if (isMCP(yarnJar)) { + readAndMergeMCP(yarnJar, postPopulationScheduler); + return; + } + try (FileSystem fileSystem = FileSystems.newFileSystem(yarnJar, (ClassLoader) null)) { extractMappings(fileSystem, baseTinyMappings); extractUnpickDefinitions(fileSystem, unpickDefinitionsFile.toPath()); @@ -219,6 +288,34 @@ public class MappingsProvider extends DependencyProvider { } } + private void readAndMergeMCP(Path mcpJar, Consumer<Runnable> postPopulationScheduler) throws Exception { + Path intermediaryTinyPath = getIntermediaryTiny(); + SrgProvider provider = getExtension().getSrgProvider(); + + if (provider == null) { + if (!getExtension().shouldGenerateSrgTiny()) { + Configuration srg = getProject().getConfigurations().maybeCreate(Constants.Configurations.SRG); + srg.setTransitive(false); + } + + provider = new SrgProvider(getProject()); + getProject().getDependencies().add(provider.getTargetConfig(), "de.oceanlabs.mcp:mcp_config:" + minecraftVersion); + Configuration configuration = getProject().getConfigurations().getByName(provider.getTargetConfig()); + provider.provide(DependencyInfo.create(getProject(), configuration.getDependencies().iterator().next(), configuration), postPopulationScheduler); + } + + Path srgPath = provider.getSrg().toPath(); + + TinyFile file = new MCPReader(intermediaryTinyPath, srgPath).read(mcpJar); + TinyV2Writer.write(file, tinyMappings.toPath()); + } + + private boolean isMCP(Path path) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(path, (ClassLoader) null)) { + return Files.exists(fs.getPath("fields.csv")) && Files.exists(fs.getPath("methods.csv")); + } + } + private boolean baseMappingsAreV2() throws IOException { try (BufferedReader reader = Files.newBufferedReader(baseTinyMappings)) { TinyV2Factory.readMetadata(reader); @@ -234,7 +331,7 @@ public class MappingsProvider extends DependencyProvider { try (BufferedReader reader = Files.newBufferedReader(fs.getPath("mappings", "mappings.tiny"))) { TinyV2Factory.readMetadata(reader); return true; - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | NoSuchFileException e) { return false; } } @@ -315,20 +412,20 @@ public class MappingsProvider extends DependencyProvider { try { Command command = new CommandMergeTinyV2(); runCommand(command, intermediaryMappings.toAbsolutePath().toString(), - yarnMappings.toAbsolutePath().toString(), - newMergedMappings.toAbsolutePath().toString(), - "intermediary", "official"); + yarnMappings.toAbsolutePath().toString(), + newMergedMappings.toAbsolutePath().toString(), + "intermediary", "official"); } catch (Exception e) { throw new RuntimeException("Could not merge mappings from " + intermediaryMappings.toString() - + " with mappings from " + yarnMappings, e); + + " with mappings from " + yarnMappings, e); } } private void suggestFieldNames(MinecraftProvider minecraftProvider, Path oldMappings, Path newMappings) { Command command = new CommandProposeFieldNames(); runCommand(command, minecraftProvider.getMergedJar().getAbsolutePath(), - oldMappings.toAbsolutePath().toString(), - newMappings.toAbsolutePath().toString()); + oldMappings.toAbsolutePath().toString(), + newMappings.toAbsolutePath().toString()); } private void runCommand(Command command, String... args) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MojangMappingsDependency.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MojangMappingsDependency.java index 62879458..09010c87 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MojangMappingsDependency.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MojangMappingsDependency.java @@ -45,9 +45,19 @@ import org.cadixdev.lorenz.model.FieldMapping; import org.cadixdev.lorenz.model.InnerClassMapping; import org.cadixdev.lorenz.model.MethodMapping; import org.cadixdev.lorenz.model.TopLevelClassMapping; +import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ExternalModuleDependency; +import org.gradle.api.artifacts.ModuleIdentifier; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.MutableVersionConstraint; import org.gradle.api.artifacts.SelfResolvingDependency; +import org.gradle.api.artifacts.VersionConstraint; +import org.gradle.api.internal.artifacts.DefaultModuleIdentifier; +import org.gradle.api.internal.artifacts.ModuleVersionSelectorStrictSpec; +import org.gradle.api.internal.artifacts.dependencies.AbstractModuleDependency; +import org.gradle.api.internal.artifacts.dependencies.DefaultMutableVersionConstraint; import org.gradle.api.tasks.TaskDependency; import org.zeroturnaround.zip.ByteSource; import org.zeroturnaround.zip.ZipEntrySource; @@ -60,7 +70,7 @@ import net.fabricmc.loom.util.HashedDownloadUtil; import net.fabricmc.lorenztiny.TinyMappingsReader; import net.fabricmc.mapping.tree.TinyMappingFactory; -public class MojangMappingsDependency implements SelfResolvingDependency { +public class MojangMappingsDependency extends AbstractModuleDependency implements SelfResolvingDependency, ExternalModuleDependency { public static final String GROUP = "net.minecraft"; public static final String MODULE = "mappings"; // Keys in dependency manifest @@ -70,12 +80,51 @@ public class MojangMappingsDependency implements SelfResolvingDependency { private final Project project; private final LoomGradleExtension extension; + private boolean changing; + private boolean force; + public MojangMappingsDependency(Project project, LoomGradleExtension extension) { + super(null); this.project = project; this.extension = extension; } @Override + public ExternalModuleDependency copy() { + MojangMappingsDependency copiedProjectDependency = new MojangMappingsDependency(project, extension); + this.copyTo(copiedProjectDependency); + return copiedProjectDependency; + } + + @Override + public void version(Action<? super MutableVersionConstraint> action) { + } + + @Override + public boolean isForce() { + return this.force; + } + + @Override + public ExternalModuleDependency setForce(boolean force) { + this.validateMutation(this.force, force); + this.force = force; + return this; + } + + @Override + public boolean isChanging() { + return this.changing; + } + + @Override + public ExternalModuleDependency setChanging(boolean changing) { + this.validateMutation(this.changing, changing); + this.changing = changing; + return this; + } + + @Override public Set<File> resolve() { Path mappingsDir = extension.getMappingsProvider().getMappingsDir(); Path mappingsFile = mappingsDir.resolve(String.format("%s.%s-%s.tiny", GROUP, MODULE, getVersion())); @@ -101,20 +150,22 @@ public class MojangMappingsDependency implements SelfResolvingDependency { } } - try (BufferedReader clientBufferedReader = Files.newBufferedReader(clientMappings, StandardCharsets.UTF_8)) { - project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - project.getLogger().warn("Using of the official minecraft mappings is at your own risk!"); - project.getLogger().warn("Please make sure to read and understand the following license:"); - project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - String line; + if (!extension.isSilentMojangMappingsLicenseEnabled()) { + try (BufferedReader clientBufferedReader = Files.newBufferedReader(clientMappings, StandardCharsets.UTF_8)) { + project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + project.getLogger().warn("Using of the official minecraft mappings is at your own risk!"); + project.getLogger().warn("Please make sure to read and understand the following license:"); + project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + String line; - while ((line = clientBufferedReader.readLine()).startsWith("#")) { - project.getLogger().warn(line); - } + while ((line = clientBufferedReader.readLine()).startsWith("#")) { + project.getLogger().warn(line); + } - project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); - } catch (IOException e) { - throw new RuntimeException("Failed to read client mappings", e); + project.getLogger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + } catch (IOException e) { + throw new RuntimeException("Failed to read client mappings", e); + } } return Collections.singleton(mappingsFile.toFile()); @@ -160,7 +211,7 @@ public class MojangMappingsDependency implements SelfResolvingDependency { ClassMapping<?, ?> mojangClassMapping = intermediaryToMojang.getOrCreateClassMapping(inputMappings.getFullObfuscatedName()) .setDeobfuscatedName(namedClass.getFullDeobfuscatedName()); - for (FieldMapping fieldMapping : inputMappings .getFieldMappings()) { + for (FieldMapping fieldMapping : inputMappings.getFieldMappings()) { namedClass.getFieldMapping(fieldMapping.getDeobfuscatedName()) .ifPresent(namedField -> { mojangClassMapping.getOrCreateFieldMapping(fieldMapping.getSignature()) @@ -168,7 +219,7 @@ public class MojangMappingsDependency implements SelfResolvingDependency { }); } - for (MethodMapping methodMapping : inputMappings .getMethodMappings()) { + for (MethodMapping methodMapping : inputMappings.getMethodMappings()) { namedClass.getMethodMapping(methodMapping.getDeobfuscatedSignature()) .ifPresent(namedMethod -> { mojangClassMapping.getOrCreateMethodMapping(methodMapping.getSignature()) @@ -203,10 +254,26 @@ public class MojangMappingsDependency implements SelfResolvingDependency { @Override public String getVersion() { + if (extension.getDependencyManager() == null) return "1.0.0"; return extension.getMinecraftProvider().getMinecraftVersion(); } @Override + public VersionConstraint getVersionConstraint() { + return new DefaultMutableVersionConstraint(getVersion()); + } + + @Override + public boolean matchesStrictly(ModuleVersionIdentifier identifier) { + return (new ModuleVersionSelectorStrictSpec(this)).isSatisfiedBy(identifier); + } + + @Override + public ModuleIdentifier getModule() { + return DefaultModuleIdentifier.newId(GROUP, MODULE); + } + + @Override public boolean contentEquals(Dependency dependency) { if (dependency instanceof MojangMappingsDependency) { return ((MojangMappingsDependency) dependency).extension.getMinecraftProvider().getMinecraftVersion().equals(getVersion()); @@ -216,11 +283,6 @@ public class MojangMappingsDependency implements SelfResolvingDependency { } @Override - public Dependency copy() { - return new MojangMappingsDependency(project, extension); - } - - @Override public String getReason() { return null; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java index 0032a177..88fd4338 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMappedProvider.java @@ -25,21 +25,39 @@ package net.fabricmc.loom.configuration.providers.minecraft; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import com.google.common.collect.ImmutableMap; import org.gradle.api.Project; +import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.configuration.DependencyProvider; import net.fabricmc.loom.configuration.providers.MinecraftProvider; import net.fabricmc.loom.configuration.providers.mappings.MappingsProvider; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.DownloadUtil; import net.fabricmc.loom.util.TinyRemapperMappingsHelper; +import net.fabricmc.loom.util.srg.AtRemapper; +import net.fabricmc.loom.util.srg.CoreModClassRemapper; +import net.fabricmc.loom.util.srg.InnerClassRemapper; +import net.fabricmc.mapping.tree.TinyTree; +import net.fabricmc.tinyremapper.IMappingProvider; +import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; @@ -50,8 +68,10 @@ public class MinecraftMappedProvider extends DependencyProvider { .put("javax/annotation/concurrent/Immutable", "org/jetbrains/annotations/Unmodifiable") .build(); + private File inputJar; private File minecraftMappedJar; private File minecraftIntermediaryJar; + private File minecraftSrgJar; private MinecraftProvider minecraftProvider; @@ -65,11 +85,13 @@ public class MinecraftMappedProvider extends DependencyProvider { throw new RuntimeException("mappings file not found"); } - if (!getExtension().getMinecraftProvider().getMergedJar().exists()) { + if (!inputJar.exists()) { throw new RuntimeException("input merged jar not found"); } - if (!minecraftMappedJar.exists() || !getIntermediaryJar().exists() || isRefreshDeps()) { + boolean isForgeAtDirty = getExtension().isForge() && getExtension().getMappingsProvider().patchedProvider.isAtDirty(); + + if (!minecraftMappedJar.exists() || !getIntermediaryJar().exists() || (getExtension().isForge() && !getSrgJar().exists()) || isRefreshDeps() || isForgeAtDirty) { if (minecraftMappedJar.exists()) { minecraftMappedJar.delete(); } @@ -80,12 +102,22 @@ public class MinecraftMappedProvider extends DependencyProvider { minecraftIntermediaryJar.delete(); } + if (getExtension().isForge() && minecraftSrgJar.exists()) { + minecraftSrgJar.delete(); + } + try { mapMinecraftJar(); } catch (Throwable t) { // Cleanup some some things that may be in a bad state now - minecraftMappedJar.delete(); - minecraftIntermediaryJar.delete(); + DownloadUtil.delete(minecraftMappedJar); + DownloadUtil.delete(minecraftIntermediaryJar); + getExtension().getMinecraftProvider().deleteFiles(); + + if (getExtension().isForge()) { + minecraftSrgJar.delete(); + } + getExtension().getMappingsProvider().cleanFiles(); throw new RuntimeException("Failed to remap minecraft", t); } @@ -98,48 +130,120 @@ public class MinecraftMappedProvider extends DependencyProvider { addDependencies(dependency, postPopulationScheduler); } - private void mapMinecraftJar() throws IOException { + private void mapMinecraftJar() throws Exception { String fromM = "official"; MappingsProvider mappingsProvider = getExtension().getMappingsProvider(); - Path input = minecraftProvider.getMergedJar().toPath(); + Path input = inputJar.toPath(); Path outputMapped = minecraftMappedJar.toPath(); Path outputIntermediary = minecraftIntermediaryJar.toPath(); + Path outputSrg = minecraftSrgJar == null ? null : minecraftSrgJar.toPath(); + + Path[] libraries = getRemapClasspath(getProject()); + TinyRemapper remapper = getTinyRemapper(); + remapper.readClassPath(libraries); + remapper.prepareClasses(); - for (String toM : Arrays.asList("named", "intermediary")) { - Path output = "named".equals(toM) ? outputMapped : outputIntermediary; + for (String toM : getExtension().isForge() ? Arrays.asList("named", "intermediary", "srg") : Arrays.asList("named", "intermediary")) { + Path output = "named".equals(toM) ? outputMapped : "srg".equals(toM) ? outputSrg : outputIntermediary; getProject().getLogger().lifecycle(":remapping minecraft (TinyRemapper, " + fromM + " -> " + toM + ")"); Files.deleteIfExists(output); - TinyRemapper remapper = getTinyRemapper(fromM, toM); - try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(output).build()) { - outputConsumer.addNonClassFiles(input); - remapper.readClassPath(getRemapClasspath()); + if (getExtension().isForge()) { + outputConsumer.addNonClassFiles(input, NonClassCopyMode.FIX_META_INF, remapper); + } else { + outputConsumer.addNonClassFiles(input); + } + + remapper.replaceMappings(getMappings(input, fromM, toM)); remapper.readInputs(input); remapper.apply(outputConsumer); } catch (Exception e) { + Files.deleteIfExists(output); throw new RuntimeException("Failed to remap JAR " + input + " with mappings from " + mappingsProvider.tinyMappings, e); } finally { - remapper.finish(); + remapper.removeInput(); + } + + if (getExtension().isForge() && !"srg".equals(toM)) { + getProject().getLogger().info(":running forge finalising tasks"); + + // TODO: Relocate this to its own class + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + output.toUri()), ImmutableMap.of("create", false))) { + Path manifestPath = fs.getPath("META-INF", "MANIFEST.MF"); + Manifest minecraftManifest; + Manifest forgeManifest; + + try (InputStream in = Files.newInputStream(manifestPath)) { + minecraftManifest = new Manifest(in); + } + + try (InputStream in = new FileInputStream(getExtension().getForgeUniversalProvider().getForgeManifest())) { + forgeManifest = new Manifest(in); + } + + for (Map.Entry<String, Attributes> forgeEntry : forgeManifest.getEntries().entrySet()) { + if (forgeEntry.getKey().endsWith("/")) { + minecraftManifest.getEntries().put(forgeEntry.getKey(), forgeEntry.getValue()); + } + } + + Files.delete(manifestPath); + + try (OutputStream out = Files.newOutputStream(manifestPath)) { + minecraftManifest.write(out); + } + } + + TinyTree yarnWithSrg = getExtension().getMappingsProvider().getMappingsWithSrg(); + AtRemapper.remap(getProject().getLogger(), output, yarnWithSrg); + CoreModClassRemapper.remapJar(output, yarnWithSrg, getProject().getLogger()); } } + + remapper.finish(); } - public TinyRemapper getTinyRemapper(String fromM, String toM) throws IOException { - return TinyRemapper.newRemapper() - .withMappings(TinyRemapperMappingsHelper.create(getExtension().getMappingsProvider().getMappings(), fromM, toM, true)) - .withMappings(out -> JSR_TO_JETBRAINS.forEach(out::acceptClass)) + public TinyRemapper getTinyRemapper() throws IOException { + TinyRemapper.Builder builder = TinyRemapper.newRemapper() .renameInvalidLocals(true) - .rebuildSourceFilenames(true) - .build(); + .ignoreConflicts(getExtension().isForge()) + .cacheMappings(true) + .threads(Runtime.getRuntime().availableProcessors()) + .logger(getProject().getLogger()::lifecycle) + .rebuildSourceFilenames(true); + + if (getExtension().isForge()) { + /* FORGE: Required for classes like aej$OptionalNamedTag (1.16.4) which are added by Forge patches. + * They won't get remapped to their proper packages, so IllegalAccessErrors will happen without ._. + */ + builder.fixPackageAccess(true); + } + + return builder.build(); } - public Path[] getRemapClasspath() { - return getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES).getFiles() + public Set<IMappingProvider> getMappings(@Nullable Path fromJar, String fromM, String toM) throws IOException { + Set<IMappingProvider> providers = new HashSet<>(); + providers.add(TinyRemapperMappingsHelper.create(getExtension().isForge() ? getExtension().getMappingsProvider().getMappingsWithSrg() : getExtension().getMappingsProvider().getMappings(), fromM, toM, true)); + + if (getExtension().isForge()) { + if (fromJar != null) { + providers.add(InnerClassRemapper.of(fromJar, getExtension().getMappingsProvider().getMappingsWithSrg(), fromM, toM)); + } + } else { + providers.add(out -> JSR_TO_JETBRAINS.forEach(out::acceptClass)); + } + + return providers; + } + + public static Path[] getRemapClasspath(Project project) { + return project.getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES).getFiles() .stream().map(File::toPath).toArray(Path[]::new); } @@ -153,7 +257,9 @@ public class MinecraftMappedProvider extends DependencyProvider { public void initFiles(MinecraftProvider minecraftProvider, MappingsProvider mappingsProvider) { this.minecraftProvider = minecraftProvider; minecraftIntermediaryJar = new File(getExtension().getUserCache(), "minecraft-" + getJarVersionString("intermediary") + ".jar"); + minecraftSrgJar = !getExtension().isForge() ? null : new File(getExtension().getUserCache(), "minecraft-" + getJarVersionString("srg") + ".jar"); minecraftMappedJar = new File(getJarDirectory(getExtension().getUserCache(), "mapped"), "minecraft-" + getJarVersionString("mapped") + ".jar"); + inputJar = getExtension().isForge() ? mappingsProvider.patchedProvider.getMergedJar() : minecraftProvider.getMergedJar(); } protected File getJarDirectory(File parentDirectory, String type) { @@ -161,13 +267,17 @@ public class MinecraftMappedProvider extends DependencyProvider { } protected String getJarVersionString(String type) { - return String.format("%s-%s-%s-%s", minecraftProvider.getMinecraftVersion(), type, getExtension().getMappingsProvider().mappingsName, getExtension().getMappingsProvider().mappingsVersion); + return String.format("%s-%s-%s-%s%s", minecraftProvider.getMinecraftVersion(), type, getExtension().getMappingsProvider().mappingsName, getExtension().getMappingsProvider().mappingsVersion, minecraftProvider.getJarSuffix()); } public File getIntermediaryJar() { return minecraftIntermediaryJar; } + public File getSrgJar() { + return minecraftSrgJar; + } + public File getMappedJar() { return minecraftMappedJar; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java index 931e8672..44a9a3e8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java @@ -28,14 +28,16 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.URL; -import java.util.Deque; import java.util.Map; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import com.google.common.base.Stopwatch; +import me.tongfei.progressbar.DelegatingProgressBarConsumer; +import me.tongfei.progressbar.ProgressBar; +import me.tongfei.progressbar.ProgressBarBuilder; +import me.tongfei.progressbar.ProgressBarStyle; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -45,7 +47,6 @@ import net.fabricmc.loom.configuration.providers.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.HashedDownloadUtil; -import net.fabricmc.loom.util.gradle.ProgressLogger; public class MinecraftAssetsProvider { public static void provide(MinecraftProvider minecraftProvider, Project project) throws IOException { @@ -78,8 +79,8 @@ public class MinecraftAssetsProvider { HashedDownloadUtil.downloadIfInvalid(new URL(assetIndex.getUrl()), assetsInfo, assetIndex.getSha1(), project.getLogger(), false); } - Deque<ProgressLogger> loggers = new ConcurrentLinkedDeque<>(); - ExecutorService executor = Executors.newFixedThreadPool(Math.min(10, Math.max(Runtime.getRuntime().availableProcessors() / 2, 1))); + ExecutorService executor = Executors.newFixedThreadPool(Math.min(16, Math.max(Runtime.getRuntime().availableProcessors() * 2, 1))); + int toDownload = 0; AssetIndex index; @@ -91,72 +92,78 @@ public class MinecraftAssetsProvider { Map<String, AssetObject> parent = index.getFileMap(); - for (Map.Entry<String, AssetObject> entry : parent.entrySet()) { - AssetObject object = entry.getValue(); - String sha1 = object.getHash(); - String filename = "objects" + File.separator + sha1.substring(0, 2) + File.separator + sha1; - File file = new File(assets, filename); - - if (offline) { - if (file.exists()) { - project.getLogger().warn("Outdated asset " + entry.getKey()); - } else { - throw new GradleException("Asset " + entry.getKey() + " not found at " + file.getAbsolutePath()); - } - } else { - executor.execute(() -> { - final String[] assetName = {entry.getKey()}; - int end = assetName[0].lastIndexOf("/") + 1; + ProgressBar[] progressBar = {null}; - if (end > 0) { - assetName[0] = assetName[0].substring(end); + try { + for (Map.Entry<String, AssetObject> entry : parent.entrySet()) { + AssetObject object = entry.getValue(); + String sha1 = object.getHash(); + String filename = "objects" + File.separator + sha1.substring(0, 2) + File.separator + sha1; + File file = new File(assets, filename); + + if (offline) { + if (file.exists()) { + project.getLogger().warn("Outdated asset " + entry.getKey()); + } else { + throw new GradleException("Asset " + entry.getKey() + " not found at " + file.getAbsolutePath()); + } + } else if (HashedDownloadUtil.requiresDownload(file, sha1, project.getLogger())) { + toDownload++; + + synchronized (progressBar) { + if (progressBar[0] == null) { + progressBar[0] = new ProgressBarBuilder() + .setConsumer(new DelegatingProgressBarConsumer(project.getLogger()::lifecycle)) + .setInitialMax(toDownload) + .setUpdateIntervalMillis(2000) + .setTaskName(":downloading assets") + .setStyle(ProgressBarStyle.ASCII) + .showSpeed() + .build(); + } + + progressBar[0].maxHint(toDownload); } - project.getLogger().debug("validating asset " + assetName[0]); - - final ProgressLogger[] progressLogger = new ProgressLogger[1]; + executor.execute(() -> { + String assetName = entry.getKey(); + int end = assetName.lastIndexOf("/") + 1; - try { - HashedDownloadUtil.downloadIfInvalid(new URL(Constants.RESOURCES_BASE + sha1.substring(0, 2) + "/" + sha1), file, sha1, project.getLogger(), true, () -> { - ProgressLogger logger = loggers.pollFirst(); + if (end > 0) { + assetName = assetName.substring(end); + } - if (logger == null) { - //Create a new logger if we need one - progressLogger[0] = ProgressLogger.getProgressFactory(project, MinecraftAssetsProvider.class.getName()); - progressLogger[0].start("Downloading assets...", "assets"); - } else { - // use a free logger if we can - progressLogger[0] = logger; - } + project.getLogger().debug(":downloading asset " + assetName); - project.getLogger().debug("downloading asset " + assetName[0]); - progressLogger[0].progress(String.format("%-30.30s", assetName[0]) + " - " + sha1); - }); - } catch (IOException e) { - throw new RuntimeException("Failed to download: " + assetName[0], e); - } + try { + HashedDownloadUtil.downloadIfInvalid(new URL(Constants.RESOURCES_BASE + sha1.substring(0, 2) + "/" + sha1), file, sha1, project.getLogger(), true, false); + } catch (IOException e) { + throw new RuntimeException("Failed to download: " + assetName, e); + } - if (progressLogger[0] != null) { - //Give this logger back if we used it - loggers.add(progressLogger[0]); - } - }); + synchronized (progressBar) { + progressBar[0].step(); + } + }); + } } - } - project.getLogger().info("Took " + stopwatch.stop() + " to iterate " + parent.size() + " asset index."); + project.getLogger().info("Took " + stopwatch.stop() + " to iterate " + parent.size() + " asset index."); - //Wait for the assets to all download - executor.shutdown(); + //Wait for the assets to all download + executor.shutdown(); - try { - if (executor.awaitTermination(2, TimeUnit.HOURS)) { - executor.shutdownNow(); + try { + if (executor.awaitTermination(2, TimeUnit.HOURS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } finally { + if (progressBar[0] != null) { + progressBar[0].close(); } - } catch (InterruptedException e) { - throw new RuntimeException(e); } - - loggers.forEach(ProgressLogger::completed); } } |