diff options
Diffstat (limited to 'src/main')
75 files changed, 5386 insertions, 253 deletions
diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index 59ffd817..b3a1168f 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -27,10 +27,14 @@ package net.fabricmc.loom; import java.io.File; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -44,50 +48,85 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.plugins.BasePluginConvention; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.ApiStatus; import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.configuration.LoomDependencyManager; import net.fabricmc.loom.configuration.LoomProjectData; +import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfigSettings; +import net.fabricmc.loom.configuration.launch.LaunchProviderSettings; import net.fabricmc.loom.configuration.processors.JarProcessor; import net.fabricmc.loom.configuration.processors.JarProcessorManager; import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; +import net.fabricmc.loom.configuration.providers.forge.FieldMigratedMappingsProvider; +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.minecraft.MinecraftMappedProvider; import net.fabricmc.loom.configuration.providers.mappings.GradleMappingContext; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingSpec; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingSpecBuilder; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsDependency; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; +import net.fabricmc.loom.util.ModPlatform; +import net.fabricmc.loom.util.function.LazyBool; public class LoomGradleExtension { + private static final String FORGE_PROPERTY = "loom.forge"; + private static final String PLATFORM_PROPERTY = "loom.platform"; + private static final String INCLUDE_PROPERTY = "loom.forge.include"; + public String refmapName; public String loaderLaunchMethod; public boolean remapMod = true; public String customManifest = null; public File accessWidener = null; + public Set<File> accessTransformers = new HashSet<>(); public Function<String, Object> intermediaryUrl = mcVer -> "https://maven.fabricmc.net/net/fabricmc/intermediary/" + mcVer + "/intermediary-" + mcVer + "-v2.jar"; public boolean shareCaches = false; + public List<String> mixinConfigs = new ArrayList<>(); // FORGE: Passed to Minecraft + public boolean useFabricMixin = true; // FORGE: Use Fabric Mixin for better refmap resolutions private final ConfigurableFileCollection unmappedMods; private final ConfigurableFileCollection log4jConfigs; final List<LoomDecompiler> decompilers = new ArrayList<>(); private final List<JarProcessor> jarProcessors = new ArrayList<>(); + private boolean silentMojangMappingsLicense = false; + public Boolean generateSrgTiny = null; // Not to be set in the build.gradle private final Project project; + private List<String> dataGenMods = new ArrayList<>(); private LoomDependencyManager dependencyManager; private JarProcessorManager jarProcessorManager; private JsonObject installerJson; private MappingSet[] srcMappingCache = new MappingSet[2]; private Mercury[] srcMercuryCache = new Mercury[2]; + private ModPlatform platform; + private final LazyBool supportsInclude; private Set<File> mixinMappings = Collections.synchronizedSet(new HashSet<>()); + private final List<String> tasksBeforeRun = Collections.synchronizedList(new ArrayList<>()); + public final List<Supplier<SourceSet>> forgeLocalMods = Collections.synchronizedList(new ArrayList<>(Collections.singletonList(new Supplier<SourceSet>() { + @Override + public SourceSet get() { + return project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().getByName("main"); + } + }))); + @ApiStatus.Experimental + public final List<Consumer<RunConfig>> settingsPostEdit = new ArrayList<>(); @ApiStatus.Internal private final LoomProjectData projectData; private NamedDomainObjectContainer<RunConfigSettings> runConfigs; + private NamedDomainObjectContainer<LaunchProviderSettings> launchConfigs; /** * Loom will generate a new genSources task (with a new name, based off of {@link LoomDecompiler#name()}) @@ -109,19 +148,87 @@ public class LoomGradleExtension { } public MappingSet getOrCreateSrcMappingCache(int id, Supplier<MappingSet> factory) { + if (id < 0 || id >= srcMappingCache.length) return factory.get(); return srcMappingCache[id] != null ? srcMappingCache[id] : (srcMappingCache[id] = factory.get()); } public Mercury getOrCreateSrcMercuryCache(int id, Supplier<Mercury> factory) { + if (id < 0 || id >= srcMercuryCache.length) return factory.get(); return srcMercuryCache[id] != null ? srcMercuryCache[id] : (srcMercuryCache[id] = factory.get()); } + public void localMods(Action<SourceSetConsumer> action) { + if (!isForge()) { + throw new UnsupportedOperationException("Not running with Forge support."); + } + + action.execute(new SourceSetConsumer()); + } + + public boolean isDataGenEnabled() { + return isForge() && !dataGenMods.isEmpty(); + } + + public List<String> getDataGenMods() { + return dataGenMods; + } + + public class SourceSetConsumer { + public void add(Object... sourceSets) { + for (Object sourceSet : sourceSets) { + if (sourceSet instanceof SourceSet) { + forgeLocalMods.add(() -> (SourceSet) sourceSet); + } else { + forgeLocalMods.add(() -> project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().findByName(String.valueOf(forgeLocalMods))); + } + } + } + } + + public void dataGen(Action<DataGenConsumer> action) { + if (!isForge()) { + throw new UnsupportedOperationException("Not running with Forge support."); + } + + action.execute(new DataGenConsumer()); + } + + public class DataGenConsumer { + public void mod(String... modIds) { + dataGenMods.addAll(Arrays.asList(modIds)); + + if (modIds.length > 0 && getRunConfigs().findByName("data") == null) { + getRunConfigs().create("data", RunConfigSettings::data); + } + } + } + + public void addTaskBeforeRun(String task) { + this.tasksBeforeRun.add(task); + } + + public List<String> getTasksBeforeRun() { + return tasksBeforeRun; + } + + public void mixinConfig(String... config) { + mixinConfigs.addAll(Arrays.asList(config)); + } + + public void silentMojangMappingsLicense() { + this.silentMojangMappingsLicense = true; + } + + public boolean isSilentMojangMappingsLicenseEnabled() { + return silentMojangMappingsLicense; + } + public Dependency officialMojangMappings() { return layered(LayeredMappingSpecBuilder::officialMojangMappings); } public Dependency layered(Action<LayeredMappingSpecBuilder> action) { - LayeredMappingSpecBuilder builder = new LayeredMappingSpecBuilder(); + LayeredMappingSpecBuilder builder = new LayeredMappingSpecBuilder(this); action.execute(builder); LayeredMappingSpec builtSpec = builder.build(); return new LayeredMappingsDependency(new GradleMappingContext(project, "layers_" + builtSpec.getVersion().replace("+", "_").replace(".", "_")), builtSpec, builtSpec.getVersion()); @@ -130,8 +237,11 @@ public class LoomGradleExtension { public LoomGradleExtension(Project project) { this.project = project; this.unmappedMods = project.files(); + this.supportsInclude = new LazyBool(() -> Boolean.parseBoolean(Objects.toString(project.findProperty(INCLUDE_PROPERTY)))); this.runConfigs = project.container(RunConfigSettings.class, baseName -> new RunConfigSettings(project, baseName)); + this.launchConfigs = project.container(LaunchProviderSettings.class, + baseName -> new LaunchProviderSettings(project, baseName)); this.log4jConfigs = project.files(getDefaultLog4jConfigFile()); projectData = new LoomProjectData(project); } @@ -171,6 +281,10 @@ public class LoomGradleExtension { this.accessWidener = project.file(file); } + public void accessTransformer(Object file) { + this.accessTransformers.add(project.file(file)); + } + public File getUserCache() { File userCache = new File(project.getGradle().getGradleUserHomeDir(), "caches" + File.separator + "fabric-loom"); @@ -281,6 +395,10 @@ public class LoomGradleExtension { return dependencyManager; } + public PatchProvider getPatchProvider() { + return getDependencyManager().getProvider(PatchProvider.class); + } + public MinecraftProviderImpl getMinecraftProvider() { return getDependencyManager().getProvider(MinecraftProviderImpl.class); } @@ -290,7 +408,27 @@ public class LoomGradleExtension { } public MappingsProviderImpl getMappingsProvider() { - return getDependencyManager().getProvider(MappingsProviderImpl.class); + return getDependencyManager().getProvider(isForge() ? FieldMigratedMappingsProvider.class : MappingsProviderImpl.class); + } + + public McpConfigProvider getMcpConfigProvider() { + return getDependencyManager().getProvider(McpConfigProvider.class); + } + + public SrgProvider getSrgProvider() { + return getDependencyManager().getProvider(SrgProvider.class); + } + + public ForgeUniversalProvider getForgeUniversalProvider() { + return getDependencyManager().getProvider(ForgeUniversalProvider.class); + } + + public ForgeUserdevProvider getForgeUserdevProvider() { + return getDependencyManager().getProvider(ForgeUserdevProvider.class); + } + + public ForgeProvider getForgeProvider() { + return getDependencyManager().getProvider(ForgeProvider.class); } public void setDependencyManager(LoomDependencyManager dependencyManager) { @@ -311,7 +449,14 @@ public class LoomGradleExtension { public String getRefmapName() { if (refmapName == null || refmapName.isEmpty()) { - String defaultRefmapName = project.getConvention().getPlugin(BasePluginConvention.class).getArchivesBaseName() + "-refmap.json"; + String defaultRefmapName; + + if (isRootProject()) { + defaultRefmapName = project.getConvention().getPlugin(BasePluginConvention.class).getArchivesBaseName() + "-refmap.json"; + } else { + defaultRefmapName = project.getConvention().getPlugin(BasePluginConvention.class).getArchivesBaseName() + "-" + project.getPath().replaceFirst(":", "").replace(':', '_') + "-refmap.json"; + } + project.getLogger().info("Could not find refmap definition, will be using default name: " + defaultRefmapName); refmapName = defaultRefmapName; } @@ -349,6 +494,44 @@ public class LoomGradleExtension { return shareCaches; } + @ApiStatus.Experimental + public ModPlatform getPlatform() { + if (platform == null) { + Object platformProperty = project.findProperty(PLATFORM_PROPERTY); + + if (platformProperty != null) { + return platform = ModPlatform.valueOf(Objects.toString(platformProperty).toUpperCase(Locale.ROOT)); + } + + Object forgeProperty = project.findProperty(FORGE_PROPERTY); + + if (forgeProperty != null) { + project.getLogger().warn("Project " + project.getPath() + " is using property " + FORGE_PROPERTY + " to enable forge mode. Please use '" + PLATFORM_PROPERTY + " = forge' instead!"); + return platform = Boolean.parseBoolean(Objects.toString(forgeProperty)) ? ModPlatform.FORGE : ModPlatform.FABRIC; + } + + platform = ModPlatform.FABRIC; + } + + return platform; + } + + public boolean isForge() { + return getPlatform() == ModPlatform.FORGE; + } + + public boolean supportsInclude() { + return !isForge() || supportsInclude.getAsBoolean(); + } + + public boolean shouldGenerateSrgTiny() { + if (generateSrgTiny != null) { + return generateSrgTiny; + } + + return isForge(); + } + // Creates a new file each time its called, this is then held onto later when remapping the output jar // Required as now when using parallel builds the old single file could be written by another sourceset compile task public synchronized File getNextMixinMappings() { @@ -381,10 +564,18 @@ public class LoomGradleExtension { action.execute(runConfigs); } + public void launches(Action<NamedDomainObjectContainer<LaunchProviderSettings>> action) { + action.execute(launchConfigs); + } + public NamedDomainObjectContainer<RunConfigSettings> getRunConfigs() { return runConfigs; } + public NamedDomainObjectContainer<LaunchProviderSettings> getLaunchConfigs() { + return launchConfigs; + } + @ApiStatus.Internal public LoomProjectData getProjectData() { return projectData; diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index 4fa4d951..ecac6a76 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -24,6 +24,10 @@ package net.fabricmc.loom; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; @@ -56,9 +60,17 @@ public class LoomGradlePlugin implements BootstrappedPlugin { } public void apply(Project project) { - project.getLogger().lifecycle("Fabric Loom: " + LoomGradlePlugin.class.getPackage().getImplementationVersion()); + String loomVersion = LoomGradlePlugin.class.getPackage().getImplementationVersion(); + Set<String> loggedVersions = new HashSet<>(Arrays.asList(System.getProperty("loom.printed.logged", "").split(","))); + + if (!loggedVersions.contains(loomVersion)) { + loggedVersions.add(loomVersion); + System.setProperty("loom.printed.logged", String.join(",", loggedVersions)); + project.getLogger().lifecycle("Architectury Loom: " + loomVersion); + project.getLogger().lifecycle("You are using an unstable version of Architectury Loom! Please report any issues found!"); + } - refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies(); + refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies() || "true".equals(System.getProperty("loom.refresh")); if (refreshDeps) { MappingsCache.INSTANCE.invalidate(); diff --git a/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java b/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java index d0665693..055b72b6 100644 --- a/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomRepositoryPlugin.java @@ -75,6 +75,18 @@ public class LoomRepositoryPlugin implements Plugin<PluginAware> { repo.setName("Mojang"); repo.setUrl("https://libraries.minecraft.net/"); }); + repositories.maven(repo -> { + repo.setName("Forge"); + repo.setUrl("https://maven.minecraftforge.net/"); + + repo.content(descriptor -> { + descriptor.excludeGroupByRegex("org\\.eclipse\\.?.*"); + }); + repo.metadataSources(sources -> { + sources.mavenPom(); + sources.ignoreGradleMetadataRedirection(); + }); + }); repositories.mavenCentral(); // MinecraftMappedProvider.java diff --git a/src/main/java/net/fabricmc/loom/build/JarRemapper.java b/src/main/java/net/fabricmc/loom/build/JarRemapper.java index 231040b7..d97bf925 100644 --- a/src/main/java/net/fabricmc/loom/build/JarRemapper.java +++ b/src/main/java/net/fabricmc/loom/build/JarRemapper.java @@ -25,6 +25,7 @@ package net.fabricmc.loom.build; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -34,14 +35,16 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import dev.architectury.tinyremapper.IMappingProvider; +import dev.architectury.tinyremapper.InputTag; +import dev.architectury.tinyremapper.OutputConsumerPath; +import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Action; +import org.gradle.api.Project; import org.objectweb.asm.commons.Remapper; +import net.fabricmc.loom.util.LoggerFilter; import net.fabricmc.stitch.util.Pair; -import net.fabricmc.tinyremapper.IMappingProvider; -import net.fabricmc.tinyremapper.InputTag; -import net.fabricmc.tinyremapper.OutputConsumerPath; -import net.fabricmc.tinyremapper.TinyRemapper; public class JarRemapper { private final List<IMappingProvider> mappingProviders = new ArrayList<>(); @@ -63,8 +66,11 @@ public class JarRemapper { return data; } - public void remap() throws IOException { + public void remap(Project project) throws IOException { + LoggerFilter.replaceSystemOut(); TinyRemapper.Builder remapperBuilder = TinyRemapper.newRemapper(); + remapperBuilder.logger(project.getLogger()::lifecycle); + remapperBuilder.logUnknownInvokeDynamic(false); mappingProviders.forEach(remapperBuilder::withMappings); if (remapOptions != null) { @@ -77,7 +83,7 @@ public class JarRemapper { Path[] remapClasspath = classPath.stream() .filter(path -> - remapData.stream().noneMatch(remapData -> remapData.input.equals(path)) + remapData.stream().noneMatch(remapData -> remapData.input.toString().equals(path.toString())) ) .toArray(Path[]::new); @@ -86,13 +92,28 @@ public class JarRemapper { for (RemapData data : remapData) { InputTag tag = remapper.createInputTag(); data.tag = tag; - remapper.readInputsAsync(tag, data.input); + project.getLogger().info(":remapper input -> " + data.input.getFileName().toString()); + + try { + remapper.readInputsAsync(tag, data.input); + } catch (Exception e) { + throw new RuntimeException("Failed to read remapper input " + data.input.getFileName().toString(), e); + } } List<OutputConsumerPath> outputConsumers = new ArrayList<>(); for (RemapData data : remapData) { - OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(data.output).build(); + OutputConsumerPath outputConsumer; + project.getLogger().info(":remapper output -> " + data.output.getFileName().toString()); + + try { + Files.deleteIfExists(data.output); + outputConsumer = new OutputConsumerPath.Builder(data.output).build(); + } catch (Exception e) { + throw new RuntimeException("Failed to create remapper output " + data.output.getFileName().toString(), e); + } + outputConsumers.add(outputConsumer); outputConsumer.addNonClassFiles(data.input); diff --git a/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java b/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java index 4c60633f..4a4a808b 100644 --- a/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java +++ b/src/main/java/net/fabricmc/loom/build/ModCompileRemapper.java @@ -93,7 +93,7 @@ public class ModCompileRemapper { String name = artifact.getModuleVersion().getId().getName(); String version = replaceIfNullOrEmpty(artifact.getModuleVersion().getId().getVersion(), () -> Checksum.truncatedSha256(artifact.getFile())); - if (!isFabricMod(logger, artifact.getFile(), artifact.getId())) { + if (!shouldRemapMod(logger, artifact.getFile(), artifact.getId(), extension.isForge(), sourceConfig.getName())) { addToRegularCompile(project, regularConfig, artifact); continue; } @@ -120,7 +120,7 @@ public class ModCompileRemapper { // Create a mod dependency for each file in the file collection for (File artifact : files) { - if (!isFabricMod(logger, artifact, artifact.getName())) { + if (!shouldRemapMod(logger, artifact, artifact.getName(), extension.isForge(), sourceConfig.getName())) { dependencies.add(regularConfig.getName(), project.files(artifact)); continue; } @@ -152,6 +152,7 @@ public class ModCompileRemapper { // Add all of the remapped mods onto the config for (ModDependencyInfo info : modDependencies) { + project.getLogger().info(":adding " + info.toString() + " into " + info.targetConfig.getName()); project.getDependencies().add(info.targetConfig.getName(), info.getRemappedNotation()); } }); @@ -161,13 +162,28 @@ public class ModCompileRemapper { /** * Checks if an artifact is a fabric mod, according to the presence of a fabric.mod.json. */ - private static boolean isFabricMod(Logger logger, File artifact, Object id) { + private static boolean shouldRemapMod(Logger logger, File artifact, Object id, boolean forge, String config) { try (ZipFile zipFile = new ZipFile(artifact)) { - if (zipFile.getEntry("fabric.mod.json") != null) { - logger.info("Found Fabric mod in modCompile: {}", id); + if (zipFile.getEntry("architectury.common.marker") != null) { + logger.info("Found architectury common mod in " + config + ": {}", id); return true; } + if (forge) { + if (zipFile.getEntry("META-INF/mods.toml") != null) { + logger.info("Found Forge mod in " + config + ": {}", id); + return true; + } + + logger.lifecycle(":could not find forge mod in " + config + " but forcing: {}", id); + return true; + } else { + if (zipFile.getEntry("fabric.mod.json") != null) { + logger.info("Found Fabric mod in " + config + ": {}", id); + return true; + } + } + return false; } catch (IOException e) { return false; diff --git a/src/main/java/net/fabricmc/loom/build/nesting/EmptyNestedJarProvider.java b/src/main/java/net/fabricmc/loom/build/nesting/EmptyNestedJarProvider.java new file mode 100644 index 00000000..e91496e6 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/build/nesting/EmptyNestedJarProvider.java @@ -0,0 +1,38 @@ +/* + * 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.build.nesting; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; + +public enum EmptyNestedJarProvider implements NestedJarProvider { + INSTANCE; + + @Override + public Collection<File> provide() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 4b74001a..23cbaf6f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -40,7 +40,15 @@ 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.MinecraftProviderImpl; +import net.fabricmc.loom.configuration.providers.forge.FieldMigratedMappingsProvider; +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.MappingsProviderImpl; +import net.fabricmc.loom.task.GenVsCodeProjectTask; import net.fabricmc.loom.util.Constants; public final class CompileConfiguration { @@ -51,13 +59,40 @@ public final class CompileConfiguration { final ConfigurationContainer configurations = project.getConfigurations(); LoomProjectData data = project.getExtensions().getByType(LoomGradleExtension.class).getProjectData(); + project.afterEvaluate(project1 -> { + if (project.getExtensions().getByType(LoomGradleExtension.class).shouldGenerateSrgTiny()) { + data.createLazyConfiguration(Constants.Configurations.SRG).configure(configuration -> configuration.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")); + }); + } + }); + data.createLazyConfiguration(Constants.Configurations.MOD_COMPILE_CLASSPATH).configure(configuration -> configuration.setTransitive(true)); data.createLazyConfiguration(Constants.Configurations.MOD_COMPILE_CLASSPATH_MAPPED).configure(configuration -> configuration.setTransitive(false)); data.createLazyConfiguration(Constants.Configurations.MINECRAFT_NAMED).configure(configuration -> configuration.setTransitive(false)); // The launchers do not recurse dependencies data.createLazyConfiguration(Constants.Configurations.MINECRAFT_DEPENDENCIES).configure(configuration -> configuration.setTransitive(false)); data.createLazyConfiguration(Constants.Configurations.LOADER_DEPENDENCIES).configure(configuration -> configuration.setTransitive(false)); data.createLazyConfiguration(Constants.Configurations.MINECRAFT).configure(configuration -> configuration.setTransitive(false)); - data.createLazyConfiguration(Constants.Configurations.INCLUDE).configure(configuration -> configuration.setTransitive(false)); // Dont get transitive deps + + if (project.getExtensions().getByType(LoomGradleExtension.class).isForge()) { + data.createLazyConfiguration(Constants.Configurations.FORGE).configure(configuration -> configuration.setTransitive(false)); + data.createLazyConfiguration(Constants.Configurations.FORGE_USERDEV).configure(configuration -> configuration.setTransitive(false)); + data.createLazyConfiguration(Constants.Configurations.FORGE_INSTALLER).configure(configuration -> configuration.setTransitive(false)); + data.createLazyConfiguration(Constants.Configurations.FORGE_UNIVERSAL).configure(configuration -> configuration.setTransitive(false)); + data.createLazyConfiguration(Constants.Configurations.FORGE_DEPENDENCIES).configure(configuration -> configuration.setTransitive(false)); + data.createLazyConfiguration(Constants.Configurations.MCP_CONFIG).configure(configuration -> configuration.setTransitive(false)); + + extendsFrom(Constants.Configurations.MINECRAFT_DEPENDENCIES, Constants.Configurations.FORGE_DEPENDENCIES, project); + } + + if (project.getExtensions().getByType(LoomGradleExtension.class).supportsInclude()) { + data.createLazyConfiguration(Constants.Configurations.INCLUDE).configure(configuration -> configuration.setTransitive(false)); // Dont get transitive deps + } + data.createLazyConfiguration(Constants.Configurations.MAPPING_CONSTANTS); extendsFrom(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, Constants.Configurations.MAPPING_CONSTANTS, project); @@ -117,7 +152,23 @@ public final class CompileConfiguration { extension.setDependencyManager(dependencyManager); dependencyManager.addProvider(new MinecraftProviderImpl(project)); - dependencyManager.addProvider(new MappingsProviderImpl(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(extension.isForge() ? new FieldMigratedMappingsProvider(project) : new MappingsProviderImpl(project)); dependencyManager.addProvider(new LaunchProvider(project)); dependencyManager.handleDependencies(project); @@ -127,6 +178,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 980474b9..9f6d6db3 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)); default -> { //File collection, try work out the classifiers diff --git a/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java b/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java index a0538a40..9daaa7c7 100644 --- a/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java +++ b/src/main/java/net/fabricmc/loom/configuration/LoomDependencyManager.java @@ -73,7 +73,7 @@ public class LoomDependencyManager { return provider; } - public <T> T getProvider(Class<T> clazz) { + public <T> T getProvider(Class<? extends T> clazz) { for (DependencyProvider provider : dependencyProviderList) { if (provider.getClass() == clazz) { return (T) provider; @@ -131,16 +131,17 @@ 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 (extension.getInstallerJson() == null && !extension.isForge()) { //If we've not found the installer JSON we've probably skipped remapping Fabric loader, let's go looking project.getLogger().info("Searching through modCompileClasspath for installer JSON"); final Configuration configuration = project.getConfigurations().getByName(Constants.Configurations.MOD_COMPILE_CLASSPATH); @@ -159,10 +160,10 @@ public class LoomDependencyManager { handleInstallerJson(extension.getInstallerJson(), project); } } - } - if (extension.getInstallerJson() == null) { - project.getLogger().warn("fabric-installer.json not found in classpath!"); + if (extension.getInstallerJson() == null) { + project.getLogger().warn("fabric-installer.json not found in classpath!"); + } } ModCompileRemapper.remapDependencies(project, mappingsKey, extension, sourceRemapper); diff --git a/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java b/src/main/java/net/fabricmc/loom/configuration/MavenPublication.java index 35dd1d83..178f9aa7 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.sourceConfiguration()); - // 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.sourceConfiguration()); + 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 f553b72a..27244f35 100644 --- a/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/RemapConfiguration.java @@ -27,12 +27,14 @@ 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.Action; 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; @@ -75,6 +77,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); @@ -88,7 +96,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); } }); @@ -116,7 +124,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); } @@ -140,9 +148,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) { // Do not use lambda here, see: https://github.com/gradle/gradle/pull/17087 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 35fdf04e..0d428bac 100644 --- a/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerJarProcessor.java @@ -38,6 +38,7 @@ import java.util.zip.ZipEntry; import com.google.gson.Gson; import com.google.gson.JsonObject; +import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Project; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -55,9 +56,9 @@ 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; public class AccessWidenerJarProcessor implements JarProcessor { private AccessWidener accessWidener = new AccessWidener(); @@ -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 2bd2f540..cecfba3c 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; @@ -51,11 +60,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) { @@ -74,6 +86,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; } @@ -107,6 +127,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 = ""; @@ -118,6 +139,23 @@ public class RunConfig { runConfig.vmArgs = "-XX:+ShowCodeDetailsInExceptionMessages -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(); @@ -214,6 +252,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; } @@ -232,6 +274,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 ef450769..66956521 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java @@ -262,7 +262,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); } /** @@ -271,7 +271,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 c7088a0a..6653de37 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -35,13 +35,21 @@ 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; +import com.google.common.base.Stopwatch; import com.google.gson.JsonObject; +import dev.architectury.tinyremapper.InputTag; +import dev.architectury.tinyremapper.OutputConsumerPath; +import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Project; import org.objectweb.asm.commons.Remapper; import org.zeroturnaround.zip.ZipUtil; @@ -59,10 +67,11 @@ import net.fabricmc.loom.configuration.processors.dependency.ModDependencyInfo; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; 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.tinyremapper.InputTag; -import net.fabricmc.tinyremapper.OutputConsumerPath; -import net.fabricmc.tinyremapper.TinyRemapper; +import net.fabricmc.loom.util.srg.AtRemapper; +import net.fabricmc.loom.util.srg.CoreModClassRemapper; +import net.fabricmc.mapping.tree.TinyTree; public class ModProcessor { public static void processMods(Project project, List<ModDependencyInfo> processList) throws IOException { @@ -96,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 @@ -128,24 +138,29 @@ 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(); MappingsProviderImpl 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()); + Stopwatch stopwatch = Stopwatch.createStarted(); 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) + .logUnknownInvokeDynamic(false) + .withMappings(TinyRemapperMappingsHelper.create(mappings, fromM, toM, false)) + .renameInvalidLocals(false) + .build(); remapper.readClassPathAsync(mc); remapper.readClassPathAsync(mcDeps); @@ -195,6 +210,7 @@ public class ModProcessor { } remapper.finish(); + project.getLogger().lifecycle(":remapped " + remapList.size() + " mods (TinyRemapper, " + fromM + " -> " + toM + ") in " + stopwatch.stop()); for (ModDependencyInfo info : remapList) { outputConsumerMap.get(info).close(); @@ -204,10 +220,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 c98b7bff..22badc39 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/MinecraftProcessedProvider.java @@ -52,7 +52,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 6fb96a0c..45ce2f81 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/LaunchProvider.java @@ -44,6 +44,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 { @@ -66,6 +67,58 @@ public class LaunchProvider extends DependencyProvider { .argument("client", "--assetsDir") .argument("client", new File(getExtension().getUserCache(), "assets").getAbsolutePath()); + if (getExtension().isForge()) { + launchConfig + // Should match YarnNamingService.PATH_TO_MAPPINGS in forge-runtime + .property("fabric.yarnWithSrg.path", getExtension().getMappingsProvider().tinyMappingsWithSrg.toAbsolutePath().toString()) + + .argument("--fml.mcVersion") + .argument(getExtension().getMinecraftProvider().minecraftVersion()) + .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() @@ -80,6 +133,11 @@ public class LaunchProvider extends DependencyProvider { addDependency(Constants.Dependencies.TERMINAL_CONSOLE_APPENDER + Constants.Dependencies.Versions.TERMINAL_CONSOLE_APPENDER, Constants.Configurations.LOOM_DEVELOPMENT_DEPENDENCIES); addDependency(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS, JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME); + if (getExtension().isForge()) { + addDependency(Constants.Dependencies.FORGE_RUNTIME + Constants.Dependencies.Versions.FORGE_RUNTIME, JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME); + addDependency(Constants.Dependencies.JAVAX_ANNOTATIONS + Constants.Dependencies.Versions.JAVAX_ANNOTATIONS, JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME); + } + postPopulationScheduler.accept(this::writeRemapClassPath); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProviderImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProviderImpl.java index e8b85dbb..0ecb49ff 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProviderImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/MinecraftProviderImpl.java @@ -32,6 +32,7 @@ import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.function.Consumer; +import com.google.common.base.Stopwatch; import com.google.common.io.Files; import org.gradle.api.GradleException; import org.gradle.api.Project; @@ -54,10 +55,11 @@ public class MinecraftProviderImpl extends DependencyProvider implements Minecra 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 = ""; public MinecraftProviderImpl(Project project) { super(project); @@ -67,6 +69,10 @@ public class MinecraftProviderImpl extends DependencyProvider implements Minecra 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(); @@ -118,6 +124,12 @@ public class MinecraftProviderImpl extends DependencyProvider implements Minecra 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; @@ -217,11 +229,14 @@ public class MinecraftProviderImpl extends DependencyProvider implements Minecra private void mergeJars(Logger logger) throws IOException { logger.info(":merging jars"); + Stopwatch stopwatch = Stopwatch.createStarted(); try (JarMerger jarMerger = new JarMerger(minecraftClientJar, minecraftServerJar, minecraftMergedJar)) { jarMerger.enableSyntheticParamsOffset(); jarMerger.merge(); } + + logger.info(":merged jars in " + stopwatch); } public File getMergedJar() { @@ -242,6 +257,14 @@ public class MinecraftProviderImpl extends DependencyProvider implements Minecra 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/FieldMigratedMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java new file mode 100644 index 00000000..f5833007 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/FieldMigratedMappingsProvider.java @@ -0,0 +1,281 @@ +/* + * 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.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import dev.architectury.mappingslayers.api.mutable.MappingsEntry; +import dev.architectury.mappingslayers.api.mutable.MutableClassDef; +import dev.architectury.mappingslayers.api.mutable.MutableFieldDef; +import dev.architectury.mappingslayers.api.mutable.MutableTinyTree; +import dev.architectury.mappingslayers.api.utils.MappingsUtils; +import dev.architectury.refmapremapper.utils.DescriptorRemapper; +import org.gradle.api.Project; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Opcodes; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.ThreadingUtils; +import net.fabricmc.loom.util.srg.SrgMerger; +import net.fabricmc.mapping.tree.ClassDef; +import net.fabricmc.mapping.tree.FieldDef; +import net.fabricmc.mapping.tree.TinyMappingFactory; +import net.fabricmc.mapping.tree.TinyTree; + +public class FieldMigratedMappingsProvider extends MappingsProviderImpl { + private List<Map.Entry<FieldMember, String>> migratedFields = new ArrayList<>(); + public Path migratedFieldsCache; + public Path rawTinyMappings; + public Path rawTinyMappingsWithSrg; + + public FieldMigratedMappingsProvider(Project project) { + super(project); + } + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + LoomGradleExtension extension = getExtension(); + PatchProvider patchProvider = getExtension().getPatchProvider(); + migratedFieldsCache = patchProvider.getProjectCacheFolder().resolve("migrated-fields.json"); + migratedFields.clear(); + + if (LoomGradlePlugin.refreshDeps) { + Files.deleteIfExists(migratedFieldsCache); + } else if (Files.exists(migratedFieldsCache)) { + try (BufferedReader reader = Files.newBufferedReader(migratedFieldsCache)) { + Map<String, String> map = new Gson().fromJson(reader, new TypeToken<Map<String, String>>() { + }.getType()); + migratedFields = new ArrayList<>(); + map.forEach((key, newDescriptor) -> { + String[] split = key.split("#"); + migratedFields.add(new AbstractMap.SimpleEntry<>(new FieldMember(split[0], split[1]), newDescriptor)); + }); + } + } + + super.provide(dependency, postPopulationScheduler); + } + + @Override + public void manipulateMappings(Path mappingsJar) throws IOException { + LoomGradleExtension extension = getExtension(); + Path mappingsFolder = getMappedVersionedDir(removeSuffix).resolve("forge/" + extension.getPatchProvider().forgeVersion); + this.rawTinyMappings = tinyMappings.toPath(); + this.rawTinyMappingsWithSrg = tinyMappingsWithSrg; + String mappingsJarName = mappingsJar.getFileName().toString(); + + if (getExtension().shouldGenerateSrgTiny()) { + if (Files.notExists(rawTinyMappingsWithSrg) || isRefreshDeps()) { + // Merge tiny mappings with srg + SrgMerger.mergeSrg(getExtension().getSrgProvider().getSrg().toPath(), rawTinyMappings, rawTinyMappingsWithSrg, true); + } + } + + try { + Files.createDirectories(mappingsFolder); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + tinyMappings = mappingsFolder.resolve("mappings.tiny").toFile(); + tinyMappingsJar = mappingsFolder.resolve("mappings.jar").toFile(); + tinyMappingsWithSrg = mappingsFolder.resolve("mappings-srg.tiny"); + mixinTinyMappingsWithSrg = mappingsFolder.resolve("mixin-srg.tiny").toFile(); + srgToNamedSrg = mappingsFolder.resolve("srg-to-named.srg").toFile(); + + try { + updateFieldMigration(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void updateFieldMigration() throws IOException { + if (!Files.exists(migratedFieldsCache)) { + generateNewFieldMigration(); + Map<String, String> map = new HashMap<>(); + migratedFields.forEach(entry -> { + map.put(entry.getKey().owner + "#" + entry.getKey().field, entry.getValue()); + }); + Files.writeString(migratedFieldsCache, new Gson().toJson(map)); + Files.deleteIfExists(tinyMappings.toPath()); + } + + if (!Files.exists(tinyMappings.toPath())) { + Table<String, String, String> fieldDescriptorMap = HashBasedTable.create(); + + for (Map.Entry<FieldMember, String> entry : migratedFields) { + fieldDescriptorMap.put(entry.getKey().owner, entry.getKey().field, entry.getValue()); + } + + MutableTinyTree mappings; + + try (BufferedReader reader = Files.newBufferedReader(rawTinyMappings)) { + mappings = MappingsUtils.copyAsMutable(TinyMappingFactory.loadWithDetection(reader)); + + for (MutableClassDef classDef : mappings.getClassesMutable()) { + Map<String, String> row = fieldDescriptorMap.row(classDef.getIntermediary()); + + if (!row.isEmpty()) { + for (MutableFieldDef fieldDef : classDef.getFieldsMutable()) { + String newDescriptor = row.get(fieldDef.getIntermediary()); + + if (newDescriptor != null) { + fieldDef.setDescriptor(MappingsEntry.NS_INTERMEDIARY, newDescriptor); + } + } + } + } + } + + Files.writeString(tinyMappings.toPath(), MappingsUtils.serializeToString(mappings), StandardOpenOption.CREATE); + } + } + + private void generateNewFieldMigration() throws IOException { + Map<FieldMember, String> fieldDescriptorMap = new ConcurrentHashMap<>(); + LoomGradleExtension extension = getExtension(); + ThreadingUtils.TaskCompleter completer = ThreadingUtils.taskCompleter(); + + class Visitor extends ClassVisitor { + private final ThreadLocal<String> lastClass = new ThreadLocal<>(); + + Visitor(int api) { + super(api); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + lastClass.set(name); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + fieldDescriptorMap.put(new FieldMember(lastClass.get(), name), descriptor); + return super.visitField(access, name, descriptor, signature, value); + } + } + + Visitor visitor = new Visitor(Opcodes.ASM9); + + for (MinecraftPatchedProvider.Environment environment : MinecraftPatchedProvider.Environment.values()) { + File patchedSrgJar = environment.patchedSrgJar.apply(extension.getMappingsProvider().patchedProvider); + FileSystemUtil.FileSystemDelegate system = FileSystemUtil.getJarFileSystem(patchedSrgJar, false); + completer.onComplete(value -> system.close()); + + for (Path fsPath : (Iterable<? extends Path>) Files.walk(system.get().getPath("/"))::iterator) { + if (Files.isRegularFile(fsPath) && fsPath.toString().endsWith(".class")) { + completer.add(() -> { + byte[] bytes = Files.readAllBytes(fsPath); + new ClassReader(bytes).accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + }); + } + } + } + + completer.complete(); + Map<FieldMember, String> migratedFields = new HashMap<>(); + + try (BufferedReader reader = Files.newBufferedReader(rawTinyMappingsWithSrg)) { + TinyTree mappings = TinyMappingFactory.loadWithDetection(reader); + Map<String, String> srgToIntermediary = new HashMap<>(); + + for (ClassDef aClass : mappings.getClasses()) { + srgToIntermediary.put(aClass.getName("srg"), aClass.getName("intermediary")); + } + + for (ClassDef classDef : mappings.getClasses()) { + String ownerSrg = classDef.getName("srg"); + String ownerIntermediary = classDef.getName("intermediary"); + + for (FieldDef fieldDef : classDef.getFields()) { + String fieldSrg = fieldDef.getName("srg"); + String descriptorSrg = fieldDef.getDescriptor("srg"); + + FieldMember member = new FieldMember(ownerSrg, fieldSrg); + String newDescriptor = fieldDescriptorMap.get(member); + + if (newDescriptor != null && !newDescriptor.equals(descriptorSrg)) { + String fieldIntermediary = fieldDef.getName("intermediary"); + String descriptorIntermediary = fieldDef.getDescriptor("intermediary"); + String newDescriptorRemapped = DescriptorRemapper.remapDescriptor(newDescriptor, + clazz -> srgToIntermediary.getOrDefault(clazz, clazz)); + migratedFields.put(new FieldMember(ownerIntermediary, fieldIntermediary), newDescriptorRemapped); + getProject().getLogger().info(ownerIntermediary + "#" + fieldIntermediary + ": " + descriptorIntermediary + " -> " + newDescriptorRemapped); + } + } + } + } + + this.migratedFields.clear(); + this.migratedFields.addAll(migratedFields.entrySet()); + } + + public static class FieldMember { + public String owner; + public String field; + + public FieldMember(String owner, String field) { + this.owner = owner; + this.field = field; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldMember that = (FieldMember) o; + return Objects.equals(owner, that.owner) && Objects.equals(field, that.field); + } + + @Override + public int hashCode() { + return Objects.hash(owner, field); + } + } +} 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..80c09be9 --- /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.getResolvedVersion()); + 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..bbe3aabe --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/ForgeUniversalProvider.java @@ -0,0 +1,61 @@ +/* + * 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; + +public class ForgeUniversalProvider extends DependencyProvider { + private File forge; + + 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"); + + if (!forge.exists() || isRefreshDeps()) { + File dep = dependency.resolveFile().orElseThrow(() -> new RuntimeException("Could not resolve Forge")); + FileUtils.copyFile(dep, forge); + } + } + + public File getForge() { + return forge; + } + + @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..261fb481 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/MinecraftPatchedProvider.java @@ -0,0 +1,643 @@ +/* + * 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.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.net.URI; +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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.stream.Stream; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableMap; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.gson.JsonParser; +import de.oceanlabs.mcp.mcinjector.adaptors.ParameterAnnotationFixer; +import dev.architectury.tinyremapper.OutputConsumerPath; +import dev.architectury.tinyremapper.TinyRemapper; +import net.minecraftforge.binarypatcher.ConsoleTool; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.NullOutputStream; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.logging.Logger; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +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.MinecraftProviderImpl; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.DependencyDownloader; +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; + +public class MinecraftPatchedProvider extends DependencyProvider { + private static final String LOOM_PATCH_VERSION_KEY = "Loom-Patch-Version"; + private static final String CURRENT_LOOM_PATCH_VERSION = "3"; + private static final String NAME_MAPPING_SERVICE_PATH = "/inject/META-INF/services/cpw.mods.modlauncher.api.INameMappingService"; + + // Step 1: Remap Minecraft to SRG (global) + private File minecraftClientSrgJar; + private File minecraftServerSrgJar; + // Step 2: Binary Patch (global) + private File minecraftClientPatchedSrgJar; + private File minecraftServerPatchedSrgJar; + // Step 3: Merge (global) + private File minecraftMergedPatchedSrgJar; + // Step 4: Access Transform (global or project) + private File minecraftMergedPatchedSrgAtJar; + // Step 5: Remap Patched AT to Official (global or project) + private File minecraftMergedPatchedJar; + + private File projectAtHash; + private Set<File> projectAts = new HashSet<>(); + private boolean atDirty = false; + private boolean filesDirty = false; + + public MinecraftPatchedProvider(Project project) { + super(project); + } + + public void initFiles() throws IOException { + filesDirty = false; + projectAtHash = new File(getExtension().getProjectPersistentCache(), "at.sha256"); + projectAts = getExtension().accessTransformers; + + if (projectAts.isEmpty()) { + 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.projectAts.add(projectAt); + break; + } + } + } + + if (isRefreshDeps() || !projectAtHash.exists()) { + writeAtHash(); + atDirty = !projectAts.isEmpty(); + } else { + byte[] expected = com.google.common.io.Files.asByteSource(projectAtHash).read(); + byte[] current = getProjectAtsHash(); + boolean mismatched = !Arrays.equals(current, expected); + + if (mismatched) { + writeAtHash(); + } + + atDirty = mismatched; + } + + MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); + PatchProvider patchProvider = getExtension().getPatchProvider(); + String minecraftVersion = minecraftProvider.minecraftVersion(); + String patchId = "forge-" + patchProvider.forgeVersion; + + if (getExtension().useFabricMixin) { + patchId += "-fabric-mixin"; + } + + minecraftProvider.setJarSuffix(patchId); + + File globalCache = getExtension().getUserCache(); + File cache = usesProjectCache() ? getExtension().getProjectPersistentCache() : globalCache; + File globalDir = new File(globalCache, patchId); + File projectDir = new File(cache, patchId); + globalDir.mkdirs(); + projectDir.mkdirs(); + + minecraftClientSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-client-srg.jar"); + minecraftServerSrgJar = new File(globalCache, "minecraft-" + minecraftVersion + "-server-srg.jar"); + minecraftClientPatchedSrgJar = new File(globalDir, "client-srg-patched.jar"); + minecraftServerPatchedSrgJar = new File(globalDir, "server-srg-patched.jar"); + minecraftMergedPatchedSrgJar = new File(globalDir, "merged-srg-patched.jar"); + minecraftMergedPatchedSrgAtJar = new File(projectDir, "merged-srg-at-patched.jar"); + minecraftMergedPatchedJar = new File(projectDir, "merged-patched.jar"); + + if (isRefreshDeps() || Stream.of(getGlobalCaches()).anyMatch(Predicates.not(File::exists)) + || !isPatchedJarUpToDate(minecraftMergedPatchedJar)) { + cleanAllCache(); + } else if (atDirty || Stream.of(getProjectCache()).anyMatch(Predicates.not(File::exists))) { + cleanProjectCache(); + } + } + + private byte[] getProjectAtsHash() throws IOException { + if (projectAts.isEmpty()) return ByteSource.empty().hash(Hashing.sha256()).asBytes(); + List<ByteSource> currentBytes = new ArrayList<>(); + + for (File projectAt : projectAts) { + currentBytes.add(com.google.common.io.Files.asByteSource(projectAt)); + } + + return ByteSource.concat(currentBytes).hash(Hashing.sha256()).asBytes(); + } + + public void cleanAllCache() { + for (File file : getGlobalCaches()) { + file.delete(); + } + + cleanProjectCache(); + } + + private File[] getGlobalCaches() { + return new File[] { + minecraftClientSrgJar, + minecraftServerSrgJar, + minecraftClientPatchedSrgJar, + minecraftServerPatchedSrgJar, + minecraftMergedPatchedSrgJar, + }; + } + + public void cleanProjectCache() { + for (File file : getProjectCache()) { + file.delete(); + } + } + + private File[] getProjectCache() { + return new File[] { + minecraftMergedPatchedSrgAtJar, + minecraftMergedPatchedJar + }; + } + + private boolean dirty; + + @Override + public void provide(DependencyInfo dependency, Consumer<Runnable> postPopulationScheduler) throws Exception { + initFiles(); + + if (atDirty) { + getProject().getLogger().lifecycle(":found dirty access transformers"); + } + + this.dirty = false; + + if (!minecraftClientSrgJar.exists() || !minecraftServerSrgJar.exists()) { + this.dirty = true; + // Remap official jars to MCPConfig remapped srg jars + createSrgJars(getProject().getLogger()); + } + + if (!minecraftClientPatchedSrgJar.exists() || !minecraftServerPatchedSrgJar.exists()) { + this.dirty = true; + patchJars(getProject().getLogger()); + injectForgeClasses(getProject().getLogger()); + } + } + + public void finishProvide() throws Exception { + if (dirty || !minecraftMergedPatchedSrgJar.exists()) { + mergeJars(getProject().getLogger()); + } + + if (atDirty || !minecraftMergedPatchedSrgAtJar.exists()) { + this.dirty = true; + accessTransformForge(getProject().getLogger()); + } + + if (dirty) { + remapPatchedJar(getProject().getLogger()); + } + + this.filesDirty = dirty; + this.dirty = false; + } + + private void writeAtHash() throws IOException { + try (FileOutputStream out = new FileOutputStream(projectAtHash)) { + out.write(getProjectAtsHash()); + } + } + + private void createSrgJars(Logger logger) throws Exception { + McpConfigProvider mcpProvider = getExtension().getMcpConfigProvider(); + + MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); + + String[] mappingsPath = {null}; + + if (!ZipUtil.handle(mcpProvider.getMcp(), "config.json", (in, zipEntry) -> { + mappingsPath[0] = JsonParser.parseReader(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() + "!"); + } + + String atDependency = Constants.Dependencies.SPECIAL_SOURCE + Constants.Dependencies.Versions.SPECIAL_SOURCE + ":shaded"; + // Do getFiles() to resolve it before multithreading it + FileCollection classpath = getProject().files(DependencyDownloader.download(getProject(), atDependency).getFiles()); + + ThreadingUtils.run(() -> { + Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), "client", classpath, minecraftProvider.minecraftClientJar.toPath(), tmpSrg[0]), minecraftClientSrgJar.toPath()); + }, () -> { + Files.copy(SpecialSourceExecutor.produceSrgJar(getProject(), "server", classpath, minecraftProvider.minecraftServerJar.toPath(), tmpSrg[0]), minecraftServerSrgJar.toPath()); + }); + } + + private void fixParameterAnnotation(File jarFile) throws Exception { + getProject().getLogger().info(":fixing parameter annotations for " + jarFile.getAbsolutePath()); + Stopwatch stopwatch = Stopwatch.createStarted(); + + try (FileSystem fs = FileSystems.newFileSystem(new URI("jar:" + jarFile.toURI()), ImmutableMap.of("create", false))) { + ThreadingUtils.TaskCompleter completer = ThreadingUtils.taskCompleter(); + + for (Path file : (Iterable<? extends Path>) Files.walk(fs.getPath("/"))::iterator) { + if (!file.toString().endsWith(".class")) continue; + + completer.add(() -> { + 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); + } + }); + } + + completer.complete(); + } + + getProject().getLogger().info(":fixing parameter annotations for " + jarFile.getAbsolutePath() + " in " + stopwatch); + } + + private void injectForgeClasses(Logger logger) throws IOException { + logger.lifecycle(":injecting forge classes into minecraft"); + ThreadingUtils.run(Environment.values(), environment -> { + copyAll(getExtension().getForgeUniversalProvider().getForge(), environment.patchedSrgJar.apply(this)); + copyUserdevFiles(getExtension().getForgeUserdevProvider().getUserdevJar(), environment.patchedSrgJar.apply(this)); + }); + } + + private boolean isPatchedJarUpToDate(File jar) { + if (!jar.exists()) return false; + + boolean[] upToDate = {false}; + + if (!ZipUtil.handle(jar, "META-INF/MANIFEST.MF", (in, zipEntry) -> { + Manifest manifest = new Manifest(in); + Attributes attributes = manifest.getMainAttributes(); + String value = attributes.getValue(LOOM_PATCH_VERSION_KEY); + + if (Objects.equals(value, CURRENT_LOOM_PATCH_VERSION)) { + upToDate[0] = true; + } else { + getProject().getLogger().lifecycle(":forge patched jars not up to date. current version: " + value); + } + })) { + return false; + } + + return upToDate[0]; + } + + private void accessTransformForge(Logger logger) throws Exception { + var atDependency = Constants.Dependencies.ACCESS_TRANSFORMERS + Constants.Dependencies.Versions.ACCESS_TRANSFORMERS; + var classpath = DependencyDownloader.download(getProject(), atDependency); + + logger.lifecycle(":access transforming minecraft"); + + File input = minecraftMergedPatchedSrgJar; + File target = minecraftMergedPatchedSrgAtJar; + Files.deleteIfExists(target.toPath()); + File at = File.createTempFile("at-conf", ".cfg"); + at.deleteOnExit(); + JarUtil.extractFile(input, "META-INF/accesstransformer.cfg", at); + + List<String> args = new ArrayList<>(); + args.add("--inJar"); + args.add(input.getAbsolutePath()); + args.add("--outJar"); + args.add(target.getAbsolutePath()); + args.add("--atFile"); + args.add(at.getAbsolutePath()); + + if (usesProjectCache()) { + for (File projectAt : projectAts) { + args.add("--atFile"); + args.add(projectAt.getAbsolutePath()); + } + } + + getProject().javaexec(spec -> { + spec.setMain("net.minecraftforge.accesstransformer.TransformerProcessor"); + spec.setArgs(args); + spec.setClasspath(classpath); + + // if running with INFO or DEBUG logging + if (getProject().getGradle().getStartParameter().getLogLevel().compareTo(LogLevel.LIFECYCLE) < 0) { + spec.setStandardOutput(System.out); + } else { + spec.setStandardOutput(NullOutputStream.NULL_OUTPUT_STREAM); + spec.setErrorOutput(NullOutputStream.NULL_OUTPUT_STREAM); + } + }).rethrowFailure().assertNormalExitValue(); + } + + public enum Environment { + CLIENT(provider -> provider.minecraftClientSrgJar, + provider -> provider.minecraftClientPatchedSrgJar + ), + SERVER(provider -> provider.minecraftServerSrgJar, + provider -> provider.minecraftServerPatchedSrgJar + ); + + final Function<MinecraftPatchedProvider, File> srgJar; + final Function<MinecraftPatchedProvider, File> patchedSrgJar; + + Environment(Function<MinecraftPatchedProvider, File> srgJar, + Function<MinecraftPatchedProvider, File> patchedSrgJar) { + this.srgJar = srgJar; + this.patchedSrgJar = patchedSrgJar; + } + + public String side() { + return name().toLowerCase(Locale.ROOT); + } + } + + private void remapPatchedJar(Logger logger) throws Exception { + Path[] libraries = MinecraftMappedProvider.getRemapClasspath(getProject()); + logger.lifecycle(":remapping minecraft (TinyRemapper, srg -> official)"); + TinyTree mappingsWithSrg = getExtension().getMappingsProvider().getMappingsWithSrg(); + + Path input = minecraftMergedPatchedSrgAtJar.toPath(); + Path output = minecraftMergedPatchedJar.toPath(); + + Files.deleteIfExists(output); + + TinyRemapper remapper = TinyRemapper.newRemapper() + .logger(getProject().getLogger()::lifecycle) + .logUnknownInvokeDynamic(false) + .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 { + Stopwatch stopwatch = Stopwatch.createStarted(); + logger.lifecycle(":patching jars"); + + PatchProvider patchProvider = getExtension().getPatchProvider(); + patchJars(minecraftClientSrgJar, minecraftClientPatchedSrgJar, patchProvider.clientPatches); + patchJars(minecraftServerSrgJar, minecraftServerPatchedSrgJar, patchProvider.serverPatches); + + ThreadingUtils.run(Environment.values(), environment -> { + copyMissingClasses(environment.srgJar.apply(this), environment.patchedSrgJar.apply(this)); + fixParameterAnnotation(environment.patchedSrgJar.apply(this)); + }); + + logger.lifecycle(":patched jars in " + stopwatch.stop()); + } + + private void patchJars(File clean, File output, Path patches) throws IOException { + PrintStream previous = System.out; + + try { + System.setOut(new PrintStream(NullOutputStream.NULL_OUTPUT_STREAM)); + } 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. + // This will change if upstream Loom adds the possibility for separate projects/source sets per environment. + Files.copy(minecraftClientPatchedSrgJar.toPath(), minecraftMergedPatchedSrgJar.toPath()); + + logger.lifecycle(":copying resources"); + + // Copy resources + MinecraftProviderImpl minecraftProvider = getExtension().getMinecraftProvider(); + copyNonClassFiles(minecraftProvider.minecraftClientJar, minecraftMergedPatchedSrgJar); + copyNonClassFiles(minecraftProvider.minecraftServerJar, minecraftMergedPatchedSrgJar); + } + + 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 { + Predicate<Path> filter = file -> { + String s = file.toString(); + return !s.endsWith(".class") && !s.equalsIgnoreCase("/META-INF/MANIFEST.MF"); + }; + + walkFileSystems(source, target, filter, 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 { + // Removes the Forge name mapping service definition so that our own is used. + // If there are multiple name mapping services with the same "understanding" pair + // (source -> target namespace pair), modlauncher throws a fit and will crash. + // To use our YarnNamingService instead of MCPNamingService, we have to remove this file. + Predicate<Path> filter = file -> !file.toString().equals(NAME_MAPPING_SERVICE_PATH); + + walkFileSystems(source, target, filter, fs -> Collections.singleton(fs.getPath("inject")), (sourceFs, targetFs, sourcePath, targetPath) -> { + Path parent = targetPath.getParent(); + + if (parent != null) { + Files.createDirectories(parent); + } + + Files.copy(sourcePath, targetPath); + }); + + try (FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(target, false)) { + Path manifestPath = delegate.get().getPath("META-INF/MANIFEST.MF"); + + Preconditions.checkArgument(Files.exists(manifestPath), "META-INF/MANIFEST.MF does not exist in patched srg jar!"); + Manifest manifest = new Manifest(); + + try (InputStream stream = Files.newInputStream(manifestPath)) { + manifest.read(stream); + manifest.getMainAttributes().putValue(LOOM_PATCH_VERSION_KEY, CURRENT_LOOM_PATCH_VERSION); + } + + try (OutputStream stream = Files.newOutputStream(manifestPath)) { + manifest.write(stream); + } + } + } + + public File getMergedJar() { + return minecraftMergedPatchedJar; + } + + public boolean usesProjectCache() { + return !projectAts.isEmpty(); + } + + public boolean isAtDirty() { + return atDirty || filesDirty; + } + + @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..b9924a16 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/forge/PatchProvider.java @@ -0,0 +1,90 @@ +/* + * 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.IOException; +import java.io.UncheckedIOException; +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 Path projectCacheFolder; + + 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; + projectCacheFolder = getExtension().getProjectPersistentCache().toPath().resolve(forgeVersion); + clientPatches = projectCacheFolder.resolve("patches-client.lzma"); + serverPatches = projectCacheFolder.resolve("patches-server.lzma"); + + try { + Files.createDirectories(projectCacheFolder); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public Path getProjectCacheFolder() { + return projectCacheFolder; + } + + @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/LayeredMappingSpecBuilder.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpecBuilder.java index eae20663..8b5a471d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpecBuilder.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpecBuilder.java @@ -29,16 +29,25 @@ import java.util.LinkedList; import java.util.List; import org.gradle.api.Action; +import org.jetbrains.annotations.Nullable; +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.mappings.crane.CraneMappingsSpec; import net.fabricmc.loom.configuration.providers.mappings.intermediary.IntermediaryMappingsSpec; import net.fabricmc.loom.configuration.providers.mappings.mojmap.MojangMappingsSpec; import net.fabricmc.loom.configuration.providers.mappings.parchment.ParchmentMappingsSpecBuilder; public class LayeredMappingSpecBuilder { private final List<MappingsSpec<?>> layers = new LinkedList<>(); + @Nullable + private final LoomGradleExtension extension; + + public LayeredMappingSpecBuilder(LoomGradleExtension extension) { + this.extension = extension; + } public LayeredMappingSpecBuilder officialMojangMappings() { - layers.add(new MojangMappingsSpec()); + layers.add(new MojangMappingsSpec(() -> extension != null && extension.isSilentMojangMappingsLicenseEnabled())); return this; } @@ -54,6 +63,11 @@ public class LayeredMappingSpecBuilder { return this; } + public LayeredMappingSpecBuilder crane(String mavenNotation) { + layers.add(new CraneMappingsSpec(mavenNotation)); + return this; + } + public LayeredMappingSpec build() { List<MappingsSpec<?>> builtLayers = new LinkedList<>(); // Intermediary is always the base layer diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java index 8beaafdb..36837ccf 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java @@ -28,20 +28,28 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; 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.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.function.Consumer; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.net.UrlEscapers; 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; @@ -53,25 +61,39 @@ 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.MinecraftProviderImpl; +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.TinyMetadata; import net.fabricmc.mapping.reader.v2.TinyV2Factory; import net.fabricmc.mapping.tree.TinyTree; +import net.fabricmc.mappingio.adapter.MappingNsCompleter; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; +import net.fabricmc.mappingio.format.Tiny2Reader; +import net.fabricmc.mappingio.format.Tiny2Writer; +import net.fabricmc.mappingio.tree.MemoryMappingTree; 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 MappingsProviderImpl extends DependencyProvider implements MappingsProvider { public MinecraftMappedProvider mappedProvider; + public MinecraftPatchedProvider patchedProvider; public String mappingsName; public String minecraftVersion; public String mappingsVersion; + public String removeSuffix; - private final Path mappingsDir; + protected final Path mappingsDir; + protected Path mappingsVersionedDir; private final Path mappingsStepsDir; private Path intermediaryTiny; private boolean hasRefreshed = false; @@ -79,7 +101,12 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings private Path baseTinyMappings; // The mappings we use in practice public File tinyMappings; + // tinyMappings wrapped in a jar public File tinyMappingsJar; + 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; @@ -90,6 +117,28 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings mappingsStepsDir = mappingsDir.resolve("steps"); } + public Path getMappingsVersionedDir() throws IOException { + if (mappingsVersionedDir == null) { + mappingsVersionedDir = mappingsDir.resolve(getExtension().getMinecraftProvider().minecraftVersion()); + + if (!Files.exists(mappingsVersionedDir)) { + Files.createDirectories(mappingsVersionedDir); + } + } + + return mappingsVersionedDir; + } + + public Path getMappedVersionedDir(String name) throws IOException { + Path dir = getMappingsVersionedDir().resolve(name); + + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } + + return dir; + } + public void clean() throws IOException { FileUtils.deleteDirectory(mappingsDir.toFile()); } @@ -98,6 +147,14 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings 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 { MinecraftProviderImpl minecraftProvider = getDependencyManager().getProvider(MinecraftProviderImpl.class); @@ -112,6 +169,13 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings 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(); @@ -123,11 +187,12 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings } // We can save reading the zip file + header by checking the file name - isV2 = mappingsJar.getName().endsWith("-v2.jar"); + isV2 = mappingsJar.getName().endsWith("-v2.jar") || mappingsJar.getName().endsWith("-mergedv2.jar"); } else { isV2 = doesJarContainV2Mappings(mappingsJar.toPath()); } + this.removeSuffix = StringUtils.removeSuffix(mappingsJar.getName(), ".jar"); this.mappingsVersion = version + (isV2 ? "-v2" : ""); initFiles(); @@ -146,18 +211,36 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings jarClassifier = jarClassifier + depStringSplit[3]; } - 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")); + Path mappedVersionedDir = getMappedVersionedDir(removeSuffix); + tinyMappings = mappedVersionedDir.resolve("mappings.tiny").toFile(); + unpickDefinitionsFile = mappedVersionedDir.resolve("definitions.unpick").toFile(); + tinyMappingsJar = new File(getExtension().getUserCache(), removeSuffix + "-" + jarClassifier + ".jar"); + tinyMappingsWithSrg = mappedVersionedDir.resolve("mappings-srg.tiny"); + mixinTinyMappingsWithSrg = mappedVersionedDir.resolve("mappings-mixin-srg.tiny").toFile(); + srgToNamedSrg = mappedVersionedDir.resolve("mappings-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()); } } + if (getExtension().isForge()) { + patchedProvider = new MinecraftPatchedProvider(getProject()); + patchedProvider.provide(dependency, postPopulationScheduler); + } + + manipulateMappings(mappingsJar.toPath()); + + if (getExtension().shouldGenerateSrgTiny()) { + if (Files.notExists(tinyMappingsWithSrg) || isRefreshDeps()) { + // Merge tiny mappings with srg + SrgMerger.mergeSrg(getExtension().getSrgProvider().getSrg().toPath(), tinyMappings.toPath(), tinyMappingsWithSrg, true); + } + } + if (!tinyMappingsJar.exists() || isRefreshDeps()) { ZipUtil.pack(new ZipEntrySource[] {new FileSource("mappings/mappings.tiny", tinyMappings)}, tinyMappingsJar); } @@ -173,6 +256,23 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings populateUnpickClasspath(); } + 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(); @@ -185,7 +285,11 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings extension.setJarProcessorManager(processorManager); processorManager.setupProcessors(); - if (processorManager.active()) { + if (extension.isForge()) { + patchedProvider.finishProvide(); + } + + if (processorManager.active() || (extension.isForge() && patchedProvider.usesProjectCache())) { mappedProvider = new MinecraftProcessedProvider(getProject(), processorManager); getProject().getLogger().lifecycle("Using project based jar storage"); } else { @@ -196,15 +300,28 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings mappedProvider.provide(dependency, postPopulationScheduler); } - private void storeMappings(Project project, MinecraftProviderImpl minecraftProvider, Path yarnJar) throws IOException { + public void manipulateMappings(Path mappingsJar) throws IOException { } + + private void storeMappings(Project project, MinecraftProviderImpl 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()); } - if (baseMappingsAreV2()) { + if (baseMappingsAreMergedV2()) { + // Architectury Loom Patch + // If a merged tiny v2 mappings file is provided + // Skip merging, should save a lot of time + Files.copy(baseTinyMappings, tinyMappings.toPath(), StandardCopyOption.REPLACE_EXISTING); + } else if (baseMappingsAreV2()) { // These are unmerged v2 mappings mergeAndSaveMappings(project, yarnJar); } else { @@ -218,16 +335,53 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings } } + 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); return true; - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException | NoSuchFileException e) { // TODO: just check the mappings version when Parser supports V1 in readMetadata() return false; } } + private boolean baseMappingsAreMergedV2() throws IOException { + try (BufferedReader reader = Files.newBufferedReader(baseTinyMappings)) { + TinyMetadata metadata = TinyV2Factory.readMetadata(reader); + return metadata.getNamespaces().containsAll(Arrays.asList("named", "intermediary", "official")); + } catch (IllegalArgumentException | NoSuchFileException e) { + return false; + } + } + private boolean doesJarContainV2Mappings(Path path) throws IOException { try (FileSystem fs = FileSystems.newFileSystem(path, (ClassLoader) null)) { try (BufferedReader reader = Files.newBufferedReader(fs.getPath("mappings", "mappings.tiny"))) { @@ -293,34 +447,30 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings extractMappings(unmergedYarnJarFs, unmergedYarn); } - Path invertedIntermediary = Paths.get(mappingsStepsDir.toString(), "inverted-intermediary.tiny"); - reorderMappings(getIntermediaryTiny(), invertedIntermediary, "intermediary", "official"); - Path unorderedMergedMappings = Paths.get(mappingsStepsDir.toString(), "unordered-merged.tiny"); - project.getLogger().info(":merging"); - mergeMappings(invertedIntermediary, unmergedYarn, unorderedMergedMappings); - reorderMappings(unorderedMergedMappings, tinyMappings.toPath(), "official", "intermediary", "named"); - } + Stopwatch stopwatch = Stopwatch.createStarted(); + project.getLogger().info(":merging mappings"); + MemoryMappingTree tree = new MemoryMappingTree(); + MappingNsCompleter nsCompleter = new MappingNsCompleter(tree, Collections.singletonMap(MappingNamespace.NAMED.stringValue(), MappingNamespace.INTERMEDIARY.stringValue()), true); - private void reorderMappings(Path oldMappings, Path newMappings, String... newOrder) { - Command command = new CommandReorderTinyV2(); - String[] args = new String[2 + newOrder.length]; - args[0] = oldMappings.toAbsolutePath().toString(); - args[1] = newMappings.toAbsolutePath().toString(); - System.arraycopy(newOrder, 0, args, 2, newOrder.length); - runCommand(command, args); - } + try (BufferedReader reader = Files.newBufferedReader(getIntermediaryTiny(), StandardCharsets.UTF_8)) { + Tiny2Reader.read(reader, nsCompleter); + } - private void mergeMappings(Path intermediaryMappings, Path yarnMappings, Path newMergedMappings) { - try { - Command command = new CommandMergeTinyV2(); - runCommand(command, intermediaryMappings.toAbsolutePath().toString(), - 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); + MemoryMappingTree tempTree = new MemoryMappingTree(); + + MappingSourceNsSwitch sourceNsSwitch = new MappingSourceNsSwitch(tempTree, MappingNamespace.OFFICIAL.stringValue()); + tree.accept(sourceNsSwitch); + tree = tempTree; + + try (BufferedReader reader = Files.newBufferedReader(unmergedYarn, StandardCharsets.UTF_8)) { + Tiny2Reader.read(reader, tree); + } + + try (Tiny2Writer writer = new Tiny2Writer(Files.newBufferedWriter(tinyMappings.toPath(), StandardCharsets.UTF_8), false)) { + tree.accept(writer); } + + project.getLogger().info(":merged mappings in " + stopwatch.stop()); } private void suggestFieldNames(MinecraftProviderImpl minecraftProvider, Path oldMappings, Path newMappings) { @@ -338,8 +488,12 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings } } - private void initFiles() { - baseTinyMappings = mappingsDir.resolve(mappingsName + "-tiny-" + minecraftVersion + "-" + mappingsVersion + "-base"); + private void initFiles() throws IOException { + baseTinyMappings = getMappedVersionedDir(removeSuffix).resolve("mappings-base.tiny"); + + if (Files.exists(mappingsStepsDir)) { + Files.walkFileTree(mappingsStepsDir, new DeletingFileVisitor()); + } } public void cleanFiles() { @@ -381,13 +535,18 @@ public class MappingsProviderImpl extends DependencyProvider implements Mappings intermediaryTiny = mappingsDir.resolve(String.format("intermediary-%s-v2.tiny", minecraftVersion)); - if (!Files.exists(intermediaryTiny) || (isRefreshDeps() && !hasRefreshed)) { + if (isRefreshDeps() && !hasRefreshed) { + Files.deleteIfExists(intermediaryTiny); + } + + if (!Files.exists(intermediaryTiny)) { hasRefreshed = true; + intermediaryTiny = getMappingsVersionedDir().resolve("intermediary-v2.tiny"); // Download and extract intermediary String encodedMinecraftVersion = UrlEscapers.urlFragmentEscaper().escape(minecraftVersion); String intermediaryArtifactUrl = getExtension().getIntermediaryUrl().apply(encodedMinecraftVersion); - Path intermediaryJar = mappingsDir.resolve("v2-intermediary-" + minecraftVersion + ".jar"); + Path intermediaryJar = getMappingsVersionedDir().resolve("intermediary-v2.jar"); DownloadUtil.downloadIfChanged(new URL(intermediaryArtifactUrl), intermediaryJar.toFile(), getProject().getLogger()); extractIntermediary(intermediaryJar, intermediaryTiny); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingLayer.java new file mode 100644 index 00000000..65fa8bd4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingLayer.java @@ -0,0 +1,92 @@ +package net.fabricmc.loom.configuration.providers.mappings.crane; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; + +import dev.architectury.mappingslayers.api.mutable.MutableClassDef; +import dev.architectury.mappingslayers.api.mutable.MutableFieldDef; +import dev.architectury.mappingslayers.api.mutable.MutableMethodDef; +import dev.architectury.mappingslayers.api.mutable.MutableParameterDef; +import dev.architectury.mappingslayers.api.mutable.MutableTinyTree; +import dev.architectury.mappingslayers.api.utils.MappingsUtils; +import org.apache.commons.io.IOUtils; + +import net.fabricmc.loom.configuration.providers.mappings.MappingLayer; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingVisitor; + +public record CraneMappingLayer(File craneJar) implements MappingLayer { + private static final String TINY_FILE_NAME = "crane.tiny"; + + @Override + public void visit(MappingVisitor visitor) throws IOException { + try (FileSystemUtil.FileSystemDelegate fs = FileSystemUtil.getJarFileSystem(craneJar().toPath(), false)) { + try (BufferedReader reader = Files.newBufferedReader(fs.get().getPath(TINY_FILE_NAME), StandardCharsets.UTF_8)) { + // can't use this, it requires 2 namespaces + // Tiny2Reader.read(reader, mappingVisitor); + MutableTinyTree tree = MappingsUtils.deserializeFromString(IOUtils.toString(reader)); + + do { + if (visitor.visitHeader()) { + visitor.visitNamespaces(tree.getMetadata().getNamespaces().get(0), Collections.emptyList()); + } + + if (visitor.visitContent()) { + for (MutableClassDef classDef : tree.getClassesMutable()) { + if (visitor.visitClass(classDef.getName(0))) { + if (!visitor.visitElementContent(MappedElementKind.CLASS)) { + return; + } + + for (MutableFieldDef fieldDef : classDef.getFieldsMutable()) { + if (visitor.visitField(fieldDef.getName(0), fieldDef.getDescriptor(0))) { + if (!visitor.visitElementContent(MappedElementKind.FIELD)) { + return; + } + + if (fieldDef.getComment() != null) { + visitor.visitComment(MappedElementKind.FIELD, fieldDef.getComment()); + } + } + } + + for (MutableMethodDef methodDef : classDef.getMethodsMutable()) { + if (visitor.visitMethod(methodDef.getName(0), methodDef.getDescriptor(0))) { + if (!visitor.visitElementContent(MappedElementKind.METHOD)) { + return; + } + + for (MutableParameterDef parameterDef : methodDef.getParametersMutable()) { + if (visitor.visitMethodArg(parameterDef.getLocalVariableIndex(), parameterDef.getLocalVariableIndex(), parameterDef.getName(0))) { + if (!visitor.visitElementContent(MappedElementKind.METHOD_ARG)) { + return; + } + + if (parameterDef.getComment() != null) { + visitor.visitComment(MappedElementKind.METHOD_ARG, parameterDef.getComment()); + } + } + } + + if (methodDef.getComment() != null) { + visitor.visitComment(MappedElementKind.METHOD, methodDef.getComment()); + } + } + } + + if (classDef.getComment() != null) { + visitor.visitComment(MappedElementKind.FIELD, classDef.getComment()); + } + } + } + } + } while (!visitor.visitEnd()); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingsSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingsSpec.java new file mode 100644 index 00000000..179bcc92 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/crane/CraneMappingsSpec.java @@ -0,0 +1,35 @@ +/* + * 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.mappings.crane; + +import net.fabricmc.loom.configuration.providers.mappings.MappingContext; +import net.fabricmc.loom.configuration.providers.mappings.MappingsSpec; + +public record CraneMappingsSpec(String mavenNotation) implements MappingsSpec<CraneMappingLayer> { + @Override + public CraneMappingLayer createLayer(MappingContext context) { + return new CraneMappingLayer(context.mavenFile(mavenNotation())); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java index 03d0c010..251ff986 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java @@ -47,7 +47,8 @@ import net.fabricmc.mappingio.format.ProGuardReader; public record MojangMappingLayer(MinecraftVersionMeta.Download clientDownload, MinecraftVersionMeta.Download serverDownload, File workingDir, - Logger logger) implements MappingLayer { + Logger logger, + MojangMappingsSpec.SilenceLicenseOption silenceLicense) implements MappingLayer { @Override public void visit(MappingVisitor mappingVisitor) throws IOException { var clientMappings = new File(workingDir(), "client.txt"); @@ -55,7 +56,9 @@ public record MojangMappingLayer(MinecraftVersionMeta.Download clientDownload, download(clientMappings, serverMappings); - printMappingsLicense(clientMappings.toPath()); + if (!silenceLicense.isSilent()) { + printMappingsLicense(clientMappings.toPath()); + } // Make official the source namespace MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingVisitor, MappingNamespace.OFFICIAL.stringValue()); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java index 61bec479..67f22460 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java @@ -28,11 +28,47 @@ import net.fabricmc.loom.configuration.providers.mappings.MappingContext; import net.fabricmc.loom.configuration.providers.mappings.MappingsSpec; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; -public record MojangMappingsSpec() implements MappingsSpec<MojangMappingLayer> { +public record MojangMappingsSpec(SilenceLicenseOption silenceLicense) implements MappingsSpec<MojangMappingLayer> { // Keys in dependency manifest private static final String MANIFEST_CLIENT_MAPPINGS = "client_mappings"; private static final String MANIFEST_SERVER_MAPPINGS = "server_mappings"; + public MojangMappingsSpec(SilenceLicenseSupplier supplier) { + this(new SilenceLicenseOption(supplier)); + } + + public MojangMappingsSpec() { + this(() -> false); + } + + @FunctionalInterface + public interface SilenceLicenseSupplier { + boolean isSilent(); + } + + public record SilenceLicenseOption(SilenceLicenseSupplier supplier) { + public boolean isSilent() { + return supplier.isSilent(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SilenceLicenseOption that)) return false; + return isSilent() == that.isSilent(); + } + + @Override + public int hashCode() { + return Boolean.hashCode(isSilent()); + } + + @Override + public String toString() { + return isSilent() + ""; + } + } + @Override public MojangMappingLayer createLayer(MappingContext context) { MinecraftVersionMeta versionInfo = context.minecraftProvider().getVersionInfo(); @@ -45,7 +81,8 @@ public record MojangMappingsSpec() implements MappingsSpec<MojangMappingLayer> { versionInfo.download(MANIFEST_CLIENT_MAPPINGS), versionInfo.download(MANIFEST_SERVER_MAPPINGS), context.workingDirectory("mojang"), - context.getLogger() + context.getLogger(), + silenceLicense() ); } } 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 84902141..9fc53fd2 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 @@ -28,30 +28,50 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableMap; +import dev.architectury.tinyremapper.IMappingProvider; +import dev.architectury.tinyremapper.NonClassCopyMode; +import dev.architectury.tinyremapper.OutputConsumerPath; +import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Project; +import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.configuration.DependencyProvider; import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; +import net.fabricmc.loom.configuration.providers.minecraft.tr.OutputRemappingHandler; +import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.OperatingSystem; +import net.fabricmc.loom.util.ThreadingUtils; import net.fabricmc.loom.util.TinyRemapperMappingsHelper; -import net.fabricmc.tinyremapper.OutputConsumerPath; -import net.fabricmc.tinyremapper.TinyRemapper; +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; public class MinecraftMappedProvider extends DependencyProvider { - private static final Map<String, String> JSR_TO_JETBRAINS = new ImmutableMap.Builder<String, String>() + public static final Map<String, String> JSR_TO_JETBRAINS = new ImmutableMap.Builder<String, String>() .put("javax/annotation/Nullable", "org/jetbrains/annotations/Nullable") .put("javax/annotation/Nonnull", "org/jetbrains/annotations/NotNull") .put("javax/annotation/concurrent/Immutable", "org/jetbrains/annotations/Unmodifiable") .build(); + private File inputJar; private File minecraftMappedJar; private File minecraftIntermediaryJar; + private File minecraftSrgJar; private MinecraftProviderImpl 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); } @@ -96,50 +128,130 @@ public class MinecraftMappedProvider extends DependencyProvider { } addDependencies(dependency, postPopulationScheduler); + + getProject().afterEvaluate(project -> { + if (getExtension().isForge() && !OperatingSystem.isCIBuild()) { + try { + ForgeSourcesRemapper.addBaseForgeSources(project); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); } - private void mapMinecraftJar() throws IOException { + private void mapMinecraftJar() throws Exception { String fromM = "official"; MappingsProviderImpl 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(); + + Path tmpAssets = Files.createTempFile("tmpAssets", null); + Files.deleteIfExists(tmpAssets); + tmpAssets.toFile().deleteOnExit(); + + List<byte[]> inputByteList = new ArrayList<>(); + + try (FileSystemUtil.FileSystemDelegate inputFs = FileSystemUtil.getJarFileSystem(input, false)) { + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + for (Path path : (Iterable<? extends Path>) Files.walk(inputFs.get().getPath("/"))::iterator) { + if (Files.isRegularFile(path)) { + if (path.getFileName().toString().endsWith(".class")) { + taskCompleter.add(() -> { + byte[] bytes = Files.readAllBytes(path); + + synchronized (inputByteList) { + inputByteList.add(bytes); + } + }); + } + } + } - for (String toM : Arrays.asList("named", "intermediary")) { - Path output = "named".equals(toM) ? outputMapped : outputIntermediary; + taskCompleter.complete(); + } + + try (OutputConsumerPath tmpAssetsPath = new OutputConsumerPath.Builder(tmpAssets).assumeArchive(true).build()) { + if (getExtension().isForge()) { + tmpAssetsPath.addNonClassFiles(input, NonClassCopyMode.FIX_META_INF, null); + } else { + tmpAssetsPath.addNonClassFiles(input); + } + } + byte[][] inputBytes = inputByteList.toArray(new byte[0][0]); + + for (String toM : getExtension().isForge() ? Arrays.asList("intermediary", "srg", "named") : Arrays.asList("intermediary", "named")) { + Path output = "named".equals(toM) ? outputMapped : "srg".equals(toM) ? outputSrg : outputIntermediary; + Stopwatch stopwatch = Stopwatch.createStarted(); getProject().getLogger().lifecycle(":remapping minecraft (TinyRemapper, " + fromM + " -> " + toM + ")"); - Files.deleteIfExists(output); + remapper.readInputs(inputBytes); + remapper.replaceMappings(getMappings(input, fromM, toM)); + OutputRemappingHandler.remap(remapper, tmpAssets, output); + + getProject().getLogger().lifecycle(":remapped minecraft (TinyRemapper, " + fromM + " -> " + toM + ") in " + stopwatch); + remapper.removeInput(); - TinyRemapper remapper = getTinyRemapper(fromM, toM); + if (getExtension().isForge() && !"srg".equals(toM)) { + getProject().getLogger().info(":running forge finalising tasks"); - try (OutputConsumerPath outputConsumer = new OutputConsumerPath.Builder(output).build()) { - outputConsumer.addNonClassFiles(input); - remapper.readClassPath(getRemapClasspath()); - remapper.readInputs(input); - remapper.apply(outputConsumer); - } catch (Exception e) { - throw new RuntimeException("Failed to remap JAR " + input + " with mappings from " + mappingsProvider.tinyMappings, e); - } finally { - remapper.finish(); + 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(); + .logUnknownInvokeDynamic(false) + .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); } @@ -151,7 +263,9 @@ public class MinecraftMappedProvider extends DependencyProvider { public void initFiles(MinecraftProviderImpl minecraftProvider, MappingsProviderImpl 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) { @@ -159,13 +273,17 @@ public class MinecraftMappedProvider extends DependencyProvider { } protected String getJarVersionString(String type) { - return String.format("%s-%s-%s-%s", minecraftProvider.minecraftVersion(), type, getExtension().getMappingsProvider().mappingsName, getExtension().getMappingsProvider().mappingsVersion); + return String.format("%s-%s-%s-%s%s", minecraftProvider.minecraftVersion(), 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 d097de9b..edc3aea8 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.MinecraftProviderImpl; 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(MinecraftProviderImpl minecraftProvider, Project project) throws IOException { @@ -78,8 +79,8 @@ public class MinecraftAssetsProvider { HashedDownloadUtil.downloadIfInvalid(new URL(assetIndex.url()), assetsInfo, assetIndex.sha1(), 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.objects(); - for (Map.Entry<String, AssetObject> entry : parent.entrySet()) { - AssetObject object = entry.getValue(); - String sha1 = object.hash(); - 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.hash(); + 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); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/MercuryUtils.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/MercuryUtils.java new file mode 100644 index 00000000..e6b2c5e6 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/MercuryUtils.java @@ -0,0 +1,42 @@ +/* + * 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.minecraft.tr; + +import org.cadixdev.mercury.Mercury; + +public class MercuryUtils { + public static Mercury copyMercury(Mercury mercury) { + Mercury copy = new Mercury(); + copy.getClassPath().addAll(mercury.getClassPath()); + copy.getContext().putAll(mercury.getContext()); + copy.getProcessors().addAll(mercury.getProcessors()); + copy.setEncoding(mercury.getEncoding()); + copy.setFlexibleAnonymousClassMemberLookups(mercury.isFlexibleAnonymousClassMemberLookups()); + copy.setGracefulClasspathChecks(mercury.isGracefulClasspathChecks()); + copy.setGracefulJavadocClasspathChecks(mercury.isGracefulJavadocClasspathChecks()); + copy.setSourceCompatibility(mercury.getSourceCompatibility()); + return copy; + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/OutputRemappingHandler.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/OutputRemappingHandler.java new file mode 100644 index 00000000..ad7abe39 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/tr/OutputRemappingHandler.java @@ -0,0 +1,82 @@ +/* + * 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.minecraft.tr; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.function.BiConsumer; + +import dev.architectury.tinyremapper.InputTag; +import dev.architectury.tinyremapper.TinyRemapper; + +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.FileSystemUtil.FileSystemDelegate; +import net.fabricmc.loom.util.ThreadingUtils; + +public class OutputRemappingHandler { + public static void remap(TinyRemapper remapper, Path assets, Path output) throws IOException { + remap(remapper, assets, output, null); + } + + public static void remap(TinyRemapper remapper, Path assets, Path output, BiConsumer<String, byte[]> then) throws IOException { + remap(remapper, assets, output, then, (InputTag[]) null); + } + + public static void remap(TinyRemapper remapper, Path assets, Path output, BiConsumer<String, byte[]> then, InputTag... inputTags) throws IOException { + Files.copy(assets, output, StandardCopyOption.REPLACE_EXISTING); + + try (FileSystemDelegate system = FileSystemUtil.getJarFileSystem(output, true)) { + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + remapper.apply((path, bytes) -> { + if (path.startsWith("/")) path = path.substring(1); + + try { + Path fsPath = system.get().getPath(path + ".class"); + + if (fsPath.getParent() != null) { + Files.createDirectories(fsPath.getParent()); + } + + taskCompleter.add(() -> { + Files.write(fsPath, bytes, StandardOpenOption.CREATE); + }); + + if (then != null) { + then.accept(path, bytes); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, inputTags); + + taskCompleter.complete(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java b/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java new file mode 100644 index 00000000..653ac32d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/sources/ForgeSourcesRemapper.java @@ -0,0 +1,236 @@ +/* + * 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.sources; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.mercury.Mercury; +import org.cadixdev.mercury.remapper.MercuryRemapper; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.zeroturnaround.zip.ZipUtil; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.build.ModCompileRemapper; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMappedProvider; +import net.fabricmc.loom.task.GenerateSourcesTask; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.DeletingFileVisitor; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.SourceRemapper; +import net.fabricmc.loom.util.ThreadingUtils; +import net.fabricmc.lorenztiny.TinyMappingsReader; + +public class ForgeSourcesRemapper { + public static void addBaseForgeSources(Project project) throws IOException { + Path sourcesJar = GenerateSourcesTask.getMappedJarFileWithSuffix(project, "-sources.jar").toPath(); + + if (!Files.exists(sourcesJar)) { + addForgeSources(project, sourcesJar); + } + } + + public static void addForgeSources(Project project, Path sourcesJar) throws IOException { + try (FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(sourcesJar, true)) { + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + provideForgeSources(project, (path, bytes) -> { + Path fsPath = delegate.get().getPath(path); + + if (fsPath.getParent() != null) { + try { + Files.createDirectories(fsPath.getParent()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + taskCompleter.add(() -> { + Files.write(fsPath, bytes, StandardOpenOption.CREATE); + }); + }); + + taskCompleter.complete(); + } + } + + public static void provideForgeSources(Project project, BiConsumer<String, byte[]> consumer) throws IOException { + List<Path> forgeInstallerSources = new ArrayList<>(); + + for (ResolvedArtifact artifact : project.getConfigurations().getByName(Constants.Configurations.FORGE_INSTALLER).getResolvedConfiguration().getResolvedArtifacts()) { + File forgeInstallerSource = ModCompileRemapper.findSources(project.getDependencies(), artifact); + + if (forgeInstallerSource != null) { + forgeInstallerSources.add(forgeInstallerSource.toPath()); + } + } + + project.getLogger().lifecycle(":found {} forge source jars", forgeInstallerSources.size()); + Map<String, byte[]> forgeSources = extractSources(forgeInstallerSources); + project.getLogger().lifecycle(":extracted {} forge source classes", forgeSources.size()); + remapSources(project, forgeSources); + forgeSources.forEach(consumer); + } + + private static void remapSources(Project project, Map<String, byte[]> sources) throws IOException { + File tmpInput = File.createTempFile("tmpInputForgeSources", null); + tmpInput.delete(); + tmpInput.deleteOnExit(); + File tmpOutput = File.createTempFile("tmpOutputForgeSources", null); + tmpOutput.delete(); + tmpOutput.deleteOnExit(); + + try (FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(tmpInput, true)) { + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + for (Map.Entry<String, byte[]> entry : sources.entrySet()) { + Path path = delegate.get().getPath(entry.getKey()); + + if (path.getParent() != null) { + Files.createDirectories(path.getParent()); + } + + taskCompleter.add(() -> { + Files.write(path, entry.getValue(), StandardOpenOption.CREATE); + }); + } + + taskCompleter.complete(); + } + + remapForgeSourcesInner(project, tmpInput.toPath(), tmpOutput.toPath()); + tmpInput.delete(); + int[] failedToRemap = {0}; + + try (FileSystemUtil.FileSystemDelegate delegate = FileSystemUtil.getJarFileSystem(tmpOutput, false)) { + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + for (Map.Entry<String, byte[]> entry : new HashSet<>(sources.entrySet())) { + taskCompleter.add(() -> { + Path path = delegate.get().getPath(entry.getKey()); + + if (Files.exists(path)) { + sources.put(entry.getKey(), Files.readAllBytes(path)); + } else { + sources.remove(entry.getKey()); + project.getLogger().error("forge source failed to remap " + entry.getKey()); + failedToRemap[0]++; + } + }); + } + + taskCompleter.complete(); + } + + tmpOutput.delete(); + + if (failedToRemap[0] > 0) { + project.getLogger().error("{} forge sources failed to remap", failedToRemap[0]); + } + } + + private static void remapForgeSourcesInner(Project project, Path tmpInput, Path tmpOutput) throws IOException { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + Mercury mercury = SourceRemapper.createMercuryWithClassPath(project, false); + + MappingSet mappings = new TinyMappingsReader(extension.getMappingsProvider().getMappingsWithSrg(), "srg", "named").read(); + + for (Map.Entry<String, String> entry : MinecraftMappedProvider.JSR_TO_JETBRAINS.entrySet()) { + mappings.getOrCreateClassMapping(entry.getKey()).setDeobfuscatedName(entry.getValue()); + } + + Set<File> files = project.getConfigurations() + .detachedConfiguration(project.getDependencies().create(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS)) + .resolve(); + + for (File file : files) { + mercury.getClassPath().add(file.toPath()); + } + + // Distinct and add the srg jar at the top, so it gets prioritized + mercury.getClassPath().add(0, extension.getMinecraftMappedProvider().getSrgJar().toPath()); + List<Path> newClassPath = mercury.getClassPath().stream() + .distinct() + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + mercury.getClassPath().clear(); + mercury.getClassPath().addAll(newClassPath); + + mercury.getProcessors().add(MercuryRemapper.create(mappings)); + boolean isSrcTmp = false; + + if (!Files.isDirectory(tmpInput)) { + Path tmpInput1 = tmpInput; + // create tmp directory + isSrcTmp = true; + tmpInput = Files.createTempDirectory("fabric-loom-src"); + ZipUtil.unpack(tmpInput1.toFile(), tmpInput.toFile()); + } + + try (FileSystemUtil.FileSystemDelegate outputFs = FileSystemUtil.getJarFileSystem(tmpOutput, true)) { + Path outputFsRoot = outputFs.get().getPath("/"); + mercury.rewrite(tmpInput, outputFsRoot); + } catch (Exception e) { + project.getLogger().warn("Could not remap " + tmpInput + " fully!", e); + } + + if (isSrcTmp) { + Files.walkFileTree(tmpInput, new DeletingFileVisitor()); + } + } + + private static Map<String, byte[]> extractSources(List<Path> forgeInstallerSources) throws IOException { + Map<String, byte[]> sources = new ConcurrentHashMap<>(); + ThreadingUtils.TaskCompleter taskCompleter = ThreadingUtils.taskCompleter(); + + for (Path path : forgeInstallerSources) { + FileSystemUtil.FileSystemDelegate system = FileSystemUtil.getJarFileSystem(path, false); + taskCompleter.onComplete(stopwatch -> system.close()); + + for (Path filePath : (Iterable<? extends Path>) Files.walk(system.get().getPath("/"))::iterator) { + if (Files.isRegularFile(filePath) && filePath.getFileName().toString().endsWith(".java")) { + taskCompleter.add(() -> sources.put(filePath.toString(), Files.readAllBytes(filePath))); + } + } + } + + taskCompleter.complete(); + return sources; + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java index f059ed36..d0b5c56d 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java +++ b/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java @@ -45,6 +45,7 @@ import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.LoomDecompiler; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ConsumingOutputStream; import net.fabricmc.loom.util.OperatingSystem; @@ -101,7 +102,7 @@ public abstract class AbstractFernFlowerDecompiler implements LoomDecompiler { progressGroup.started(); ExecResult result = ForkingJavaExec.javaexec( - project, + project.getRootProject().getPlugins().hasPlugin(Constants.PLUGIN_ID) ? project.getRootProject() : project, spec -> { spec.getMainClass().set(fernFlowerExecutor().getName()); spec.jvmArgs("-Xms200m", "-Xmx3G"); diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java index bbcd80de..836726af 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java +++ b/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java @@ -61,6 +61,7 @@ public class ForkingJavaExec { ConfigurationContainer configurations = project.getBuildscript().getConfigurations(); DependencyHandler handler = project.getDependencies(); return configurations.getByName("classpath") + .plus(project.getRootProject().getBuildscript().getConfigurations().getByName("classpath")) .plus(configurations.detachedConfiguration(handler.localGroovy())); } diff --git a/src/main/java/net/fabricmc/loom/task/AbstractLoomTask.java b/src/main/java/net/fabricmc/loom/task/AbstractLoomTask.java index 557a096c..430416f8 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractLoomTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractLoomTask.java @@ -28,10 +28,11 @@ import org.gradle.api.DefaultTask; import org.gradle.api.tasks.Internal; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.util.Constants; public abstract class AbstractLoomTask extends DefaultTask { public AbstractLoomTask() { - setGroup("fabric"); + setGroup(Constants.TASK_CATEGORY); } @Internal diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java index 89b5a37a..cad89ae8 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java @@ -35,13 +35,14 @@ import org.gradle.api.Project; import org.gradle.api.tasks.JavaExec; import net.fabricmc.loom.configuration.ide.RunConfig; +import net.fabricmc.loom.util.Constants; public abstract class AbstractRunTask extends JavaExec { private final RunConfig config; public AbstractRunTask(Function<Project, RunConfig> configProvider) { super(); - setGroup("fabric"); + setGroup(Constants.TASK_CATEGORY); this.config = configProvider.apply(getProject()); setClasspath(config.sourceSet.getRuntimeClasspath()); @@ -79,6 +80,7 @@ public abstract class AbstractRunTask extends JavaExec { args(argsSplit); setWorkingDir(new File(getProject().getRootDir(), config.runDir)); + environment(config.envVariables); super.exec(); } diff --git a/src/main/java/net/fabricmc/loom/task/GenEclipseRunsTask.java b/src/main/java/net/fabricmc/loom/task/GenEclipseRunsTask.java index acd2bc49..fbc2ef13 100644 --- a/src/main/java/net/fabricmc/loom/task/GenEclipseRunsTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenEclipseRunsTask.java @@ -41,6 +41,7 @@ public class GenEclipseRunsTask extends AbstractLoomTask { public void genRuns() throws IOException { EclipseModel eclipseModel = getProject().getExtensions().getByType(EclipseModel.class); LoomGradleExtension extension = getExtension(); + File dataRunConfigs = new File(getProject().getRootDir(), eclipseModel.getProject().getName() + "_data.launch"); for (RunConfigSettings settings : extension.getRunConfigs()) { if (!settings.isIdeConfigGenerated()) { diff --git a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java index 287e1c0a..48d46b06 100644 --- a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java @@ -28,12 +28,20 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.apache.commons.io.FileUtils; +import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.Project; import org.gradle.api.tasks.TaskAction; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfigSettings; @@ -45,8 +53,12 @@ import net.fabricmc.loom.configuration.ide.RunConfigSettings; public class GenVsCodeProjectTask extends AbstractLoomTask { @TaskAction public void genRuns() { - Project project = getProject(); - File projectDir = project.file(".vscode"); + clean(getProject()); + generate(getProject()); + } + + public static void clean(Project project) { + File projectDir = project.getRootProject().file(".vscode"); if (!projectDir.exists()) { projectDir.mkdir(); @@ -57,10 +69,34 @@ public class GenVsCodeProjectTask extends AbstractLoomTask { if (launchJson.exists()) { launchJson.delete(); } + } + + public static void generate(Project project) { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + File projectDir = project.getRootProject().file(".vscode"); + + if (!projectDir.exists()) { + projectDir.mkdir(); + } + + File launchJson = new File(projectDir, "launch.json"); + File tasksJson = new File(projectDir, "tasks.json"); + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); - VsCodeLaunch launch = new VsCodeLaunch(); + VsCodeLaunch launch; - for (RunConfigSettings settings : getExtension().getRunConfigs()) { + if (launchJson.exists()) { + try { + launch = gson.fromJson(FileUtils.readFileToString(launchJson, StandardCharsets.UTF_8), VsCodeLaunch.class); + } catch (IOException e) { + throw new RuntimeException("Failed to read launch.json", e); + } + } else { + launch = new VsCodeLaunch(); + } + + for (RunConfigSettings settings : extension.getRunConfigs()) { if (!settings.isIdeConfigGenerated()) { continue; } @@ -76,6 +112,39 @@ public class GenVsCodeProjectTask extends AbstractLoomTask { } catch (IOException e) { throw new RuntimeException("Failed to write launch.json", e); } + + VsCodeTasks tasks; + + if (tasksJson.exists()) { + try { + tasks = gson.fromJson(FileUtils.readFileToString(tasksJson, StandardCharsets.UTF_8), VsCodeTasks.class); + } catch (IOException e) { + throw new RuntimeException("Failed to read launch.json", e); + } + } else { + tasks = new VsCodeTasks(); + } + + for (VsCodeConfiguration configuration : launch.configurations) { + if (configuration.preLaunchTask != null && configuration.tasksBeforeRun != null) { + String prefix = Os.isFamily(Os.FAMILY_WINDOWS) ? "gradlew.bat" : "./gradlew"; + tasks.add(new VsCodeTask(configuration.preLaunchTask, prefix + " " + configuration.tasksBeforeRun.stream() + .map(s -> { + int i = s.indexOf('/'); + return i == -1 ? s : s.substring(i + 1); + }).collect(Collectors.joining(" ")), "shell", new String[0])); + } + } + + if (!tasks.tasks.isEmpty()) { + String jsonTasks = gson.toJson(tasks); + + try { + FileUtils.writeStringToFile(tasksJson, jsonTasks, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Failed to write tasks.json", e); + } + } } private static class VsCodeLaunch { @@ -83,7 +152,25 @@ public class GenVsCodeProjectTask extends AbstractLoomTask { public List<VsCodeConfiguration> configurations = new ArrayList<>(); public void add(RunConfig runConfig) { - configurations.add(new VsCodeConfiguration(runConfig)); + if (configurations.stream().noneMatch(config -> Objects.equals(config.name, runConfig.configName))) { + VsCodeConfiguration configuration = new VsCodeConfiguration(runConfig); + configurations.add(configuration); + + if (!configuration.tasksBeforeRun.isEmpty()) { + configuration.preLaunchTask = "generated_" + runConfig.configName; + } + } + } + } + + private static class VsCodeTasks { + public String version = "2.0.0"; + public List<VsCodeTask> tasks = new ArrayList<>(); + + public void add(VsCodeTask vsCodeTask) { + if (tasks.stream().noneMatch(task -> Objects.equals(task.label, vsCodeTask.label))) { + tasks.add(vsCodeTask); + } } } @@ -98,6 +185,10 @@ public class GenVsCodeProjectTask extends AbstractLoomTask { public String mainClass; public String vmArgs; public String args; + public Map<String, String> env = new LinkedHashMap<>(); + public transient List<String> tasksBeforeRun = new ArrayList<>(); + public String preLaunchTask = null; + public String projectName = null; VsCodeConfiguration(RunConfig runConfig) { this.name = runConfig.configName; @@ -105,6 +196,24 @@ public class GenVsCodeProjectTask extends AbstractLoomTask { this.vmArgs = runConfig.vmArgs; this.args = runConfig.programArgs; this.cwd = "${workspaceFolder}/" + runConfig.runDir; + this.projectName = runConfig.vscodeProjectName; + this.env.putAll(runConfig.envVariables); + this.tasksBeforeRun.addAll(runConfig.vscodeBeforeRun); + } + } + + private static class VsCodeTask { + public String label; + public String command; + public String type; + public String[] args; + public String group = "build"; + + VsCodeTask(String label, String command, String type, String[] args) { + this.label = label; + this.command = command; + this.type = type; + this.args = args; } } } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 531ab816..e66822b0 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import org.gradle.api.Project; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.TaskAction; @@ -42,6 +43,7 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; +import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; import net.fabricmc.loom.decompilers.LineNumberRemapper; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.gradle.ProgressLogger; @@ -56,7 +58,6 @@ public class GenerateSourcesTask extends AbstractLoomTask { public GenerateSourcesTask(LoomDecompiler decompiler) { this.decompiler = decompiler; - setGroup("fabric"); getOutputs().upToDateWhen((o) -> false); } @@ -82,6 +83,10 @@ public class GenerateSourcesTask extends AbstractLoomTask { Files.copy(linemappedJarDestination, runtimeJar, StandardCopyOption.REPLACE_EXISTING); Files.delete(linemappedJarDestination); } + + if (getExtension().isForge()) { + ForgeSourcesRemapper.addForgeSources(getProject(), sourcesDestination); + } } private void remapLineNumbers(Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException { @@ -101,7 +106,11 @@ public class GenerateSourcesTask extends AbstractLoomTask { } private File getMappedJarFileWithSuffix(String suffix) { - LoomGradleExtension extension = getProject().getExtensions().getByType(LoomGradleExtension.class); + return getMappedJarFileWithSuffix(getProject(), suffix); + } + + public static File getMappedJarFileWithSuffix(Project project, String suffix) { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); MappingsProviderImpl mappingsProvider = extension.getMappingsProvider(); File mappedJar = mappingsProvider.mappedProvider.getMappedJar(); String path = mappedJar.getAbsolutePath(); diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 8adbe04c..4c7c46a6 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -35,6 +35,7 @@ import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler; +import net.fabricmc.loom.util.Constants; public final class LoomTasks { private LoomTasks() { @@ -50,7 +51,7 @@ public final class LoomTasks { tasks.register("remapJar", RemapJarTask.class, t -> { t.setDescription("Remaps the built project jar to intermediary mappings."); - t.setGroup("fabric"); + t.setGroup(Constants.TASK_CATEGORY); }); tasks.register("downloadAssets", DownloadAssetsTask.class, t -> t.setDescription("Downloads required assets for Fabric.")); @@ -58,6 +59,7 @@ public final class LoomTasks { registerIDETasks(tasks); registerRunTasks(tasks, project); + registerLaunchSettings(project); registerDecompileTasks(tasks, project); } @@ -97,7 +99,6 @@ public final class LoomTasks { tasks.register(taskName, RunGameTask.class, config).configure(t -> { t.setDescription("Starts the '" + config.getConfigName() + "' run configuration"); - t.setGroup("fabric"); if (config.getEnvironment().equals("client")) { t.dependsOn("downloadAssets"); @@ -109,6 +110,17 @@ public final class LoomTasks { extension.getRunConfigs().create("server", RunConfigSettings::server); } + private static void registerLaunchSettings(Project project) { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + Preconditions.checkArgument(extension.getLaunchConfigs().size() == 0, "Launch configurations must not be registered before loom"); + extension.getLaunchConfigs().create("client"); + extension.getLaunchConfigs().create("server"); + + if (extension.isForge()) { + extension.getLaunchConfigs().create("data"); + } + } + private static void registerDecompileTasks(TaskContainer tasks, Project project) { LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index ad46a8a2..c82dec79 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -27,15 +27,36 @@ package net.fabricmc.loom.task; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.Reader; +import java.net.URI; +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.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import dev.architectury.refmapremapper.RefmapRemapper; +import dev.architectury.refmapremapper.remapper.MappingsRemapper; +import dev.architectury.refmapremapper.remapper.ReferenceRemapper; +import dev.architectury.refmapremapper.remapper.Remapper; +import dev.architectury.refmapremapper.remapper.SimpleReferenceRemapper; +import dev.architectury.tinyremapper.IMappingProvider; +import dev.architectury.tinyremapper.TinyRemapper; +import dev.architectury.tinyremapper.TinyUtils; import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -47,24 +68,29 @@ import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.TaskAction; import org.gradle.jvm.tasks.Jar; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import org.zeroturnaround.zip.ZipUtil; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.build.JarRemapper; import net.fabricmc.loom.build.MixinRefmapHelper; -import net.fabricmc.loom.build.nesting.NestedJarPathProvider; +import net.fabricmc.loom.build.nesting.EmptyNestedJarProvider; import net.fabricmc.loom.build.nesting.JarNester; import net.fabricmc.loom.build.nesting.MergedNestedJarProvider; import net.fabricmc.loom.build.nesting.NestedDependencyProvider; +import net.fabricmc.loom.build.nesting.NestedJarPathProvider; import net.fabricmc.loom.build.nesting.NestedJarProvider; import net.fabricmc.loom.configuration.accesswidener.AccessWidenerJarProcessor; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.SourceRemapper; import net.fabricmc.loom.util.TinyRemapperMappingsHelper; import net.fabricmc.loom.util.ZipReprocessorUtil; +import net.fabricmc.mapping.tree.ClassDef; +import net.fabricmc.mapping.tree.FieldDef; +import net.fabricmc.mapping.tree.MethodDef; +import net.fabricmc.mapping.tree.TinyTree; import net.fabricmc.stitch.util.Pair; -import net.fabricmc.tinyremapper.TinyRemapper; -import net.fabricmc.tinyremapper.TinyUtils; public class RemapJarTask extends Jar { private final RegularFileProperty input; @@ -72,6 +98,8 @@ public class RemapJarTask extends Jar { private final Property<Boolean> addDefaultNestedDependencies; private final Property<Boolean> remapAccessWidener; private final List<Action<TinyRemapper.Builder>> remapOptions = new ArrayList<>(); + private final Property<String> fromM; + private final Property<String> toM; public JarRemapper jarRemapper; private FileCollection classpath; private final Set<Object> nestedPaths = new LinkedHashSet<>(); @@ -82,6 +110,10 @@ public class RemapJarTask extends Jar { addNestedDependencies = getProject().getObjects().property(Boolean.class); addDefaultNestedDependencies = getProject().getObjects().property(Boolean.class); remapAccessWidener = getProject().getObjects().property(Boolean.class); + fromM = getProject().getObjects().property(String.class); + toM = getProject().getObjects().property(String.class); + fromM.set("named"); + toM.set(SourceRemapper.intermediary(getProject())); // false by default, I have no idea why I have to do it for this property and not the other one remapAccessWidener.set(false); addDefaultNestedDependencies.set(true); @@ -99,10 +131,76 @@ public class RemapJarTask extends Jar { scheduleRemap(singleRemap || getProject().getExtensions().getByType(LoomGradleExtension.class).isRootProject()); if (singleRemap) { - jarRemapper.remap(); + jarRemapper.remap(getProject()); } } + private ReferenceRemapper createReferenceRemapper(LoomGradleExtension extension, String from, String to) throws IOException { + TinyTree mappings = extension.shouldGenerateSrgTiny() ? extension.getMappingsProvider().getMappingsWithSrg() : extension.getMappingsProvider().getMappings(); + + return new SimpleReferenceRemapper(new SimpleReferenceRemapper.Remapper() { + @Override + @Nullable + public String mapClass(String value) { + return mappings.getClasses().stream() + .filter(classDef -> Objects.equals(classDef.getName(from), value)) + .findFirst() + .map(classDef -> classDef.getName(to)) + .orElse(null); + } + + @Override + @Nullable + public String mapMethod(@Nullable String className, String methodName, String methodDescriptor) { + if (className != null) { + Optional<ClassDef> classDef = mappings.getClasses().stream() + .filter(c -> Objects.equals(c.getName(from), className)) + .findFirst(); + + if (classDef.isPresent()) { + for (MethodDef methodDef : classDef.get().getMethods()) { + if (Objects.equals(methodDef.getName(from), methodName) && Objects.equals(methodDef.getDescriptor(from), methodDescriptor)) { + return methodDef.getName(to); + } + } + } + } + + return mappings.getClasses().stream() + .flatMap(classDef -> classDef.getMethods().stream()) + .filter(methodDef -> Objects.equals(methodDef.getName(from), methodName) && Objects.equals(methodDef.getDescriptor(from), methodDescriptor)) + .findFirst() + .map(methodDef -> methodDef.getName(to)) + .orElse(null); + } + + @Override + @Nullable + public String mapField(@Nullable String className, String fieldName, String fieldDescriptor) { + if (className != null) { + Optional<ClassDef> classDef = mappings.getClasses().stream() + .filter(c -> Objects.equals(c.getName(from), className)) + .findFirst(); + + if (classDef.isPresent()) { + for (FieldDef fieldDef : classDef.get().getFields()) { + if (Objects.equals(fieldDef.getName(from), fieldName) && Objects.equals(fieldDef.getDescriptor(from), fieldDescriptor)) { + return fieldDef.getName(to); + } + } + } + } + + return mappings.getClasses().stream() + .flatMap(classDef -> classDef.getFields().stream()) + .filter(fieldDef -> Objects.equals(fieldDef.getName(from), fieldName) && Objects.equals(fieldDef.getDescriptor(from), fieldDescriptor)) + .findFirst() + .map(fieldDef -> fieldDef.getName(to)) + .orElse(null); + } + }); + } + public void scheduleRemap(boolean isMainRemapTask) throws Throwable { Project project = getProject(); LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); @@ -115,24 +213,27 @@ public class RemapJarTask extends Jar { MappingsProviderImpl mappingsProvider = extension.getMappingsProvider(); - String fromM = "named"; - String toM = "intermediary"; + String fromM = this.fromM.get(); + String toM = this.toM.get(); if (isMainRemapTask) { jarRemapper.addToClasspath(getRemapClasspath()); - jarRemapper.addMappings(TinyRemapperMappingsHelper.create(mappingsProvider.getMappings(), fromM, toM, false)); + jarRemapper.addMappings(TinyRemapperMappingsHelper.create(extension.shouldGenerateSrgTiny() ? mappingsProvider.getMappingsWithSrg() : mappingsProvider.getMappings(), fromM, toM, false)); } for (File mixinMapFile : extension.getAllMixinMappings()) { if (mixinMapFile.exists()) { - jarRemapper.addMappings(TinyUtils.createTinyMappingProvider(mixinMapFile.toPath(), fromM, toM)); + IMappingProvider provider = TinyUtils.createTinyMappingProvider(mixinMapFile.toPath(), fromM, "intermediary"); + jarRemapper.addMappings(!toM.equals("intermediary") ? remapToSrg(extension, provider, "intermediary", toM) : provider); } } // Add remap options to the jar remapper jarRemapper.addOptions(this.remapOptions); + project.getLogger().info(":scheduling remap " + input.getFileName() + " from " + fromM + " to " + toM); + NestedJarProvider nestedJarProvider = getNestedJarProvider(); nestedJarProvider.prepare(getProject()); @@ -165,6 +266,14 @@ public class RemapJarTask extends Jar { project.getLogger().debug("Transformed mixin reference maps in output JAR!"); } + if (!toM.equals("intermediary")) { + try { + remapRefmap(extension, output, "intermediary", toM); + } catch (IOException e) { + throw new RuntimeException("Failed to remap refmap jar", e); + } + } + if (getAddNestedDependencies().getOrElse(false)) { JarNester.nestJars(nestedJarProvider.provide(), output.toFile(), project.getLogger()); } @@ -184,7 +293,45 @@ public class RemapJarTask extends Jar { }); } + private void remapRefmap(LoomGradleExtension extension, Path output, String from, String to) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + output.toUri()), ImmutableMap.of("create", false))) { + Path refmapPath = fs.getPath(extension.getRefmapName()); + + if (Files.exists(refmapPath)) { + try (Reader refmapReader = Files.newBufferedReader(refmapPath, StandardCharsets.UTF_8)) { + Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + JsonObject refmapElement = gson.fromJson(refmapReader, JsonObject.class); + refmapElement = RefmapRemapper.remap(new Remapper() { + ReferenceRemapper remapper = createReferenceRemapper(extension, from, to); + + @Override + @Nullable + public MappingsRemapper remapMappings() { + return className -> remapper; + } + + @Override + @Nullable + public Map.Entry<String, @Nullable MappingsRemapper> remapMappingsData(String data) { + if (Objects.equals(data, "named:intermediary")) { + return new AbstractMap.SimpleEntry<>(Objects.equals(to, "srg") ? "searge" : data, remapMappings()); + } + + return null; + } + }, refmapElement); + Files.delete(refmapPath); + Files.write(refmapPath, gson.toJson(refmapElement).getBytes(StandardCharsets.UTF_8)); + } + } + } + } + private NestedJarProvider getNestedJarProvider() { + if (!getProject().getExtensions().getByType(LoomGradleExtension.class).supportsInclude()) { + return EmptyNestedJarProvider.INSTANCE; + } + Configuration includeConfiguration = getProject().getConfigurations().getByName(Constants.Configurations.INCLUDE); if (!addDefaultNestedDependencies.getOrElse(true)) { @@ -203,6 +350,57 @@ public class RemapJarTask extends Jar { ); } + private IMappingProvider remapToSrg(LoomGradleExtension extension, IMappingProvider parent, String from, String to) throws IOException { + TinyTree mappings = extension.shouldGenerateSrgTiny() ? extension.getMappingsProvider().getMappingsWithSrg() : extension.getMappingsProvider().getMappings(); + + return sink -> { + parent.load(new IMappingProvider.MappingAcceptor() { + @Override + public void acceptClass(String srcName, String dstName) { + String srgName = mappings.getClasses() + .stream() + .filter(it -> Objects.equals(it.getName(from), dstName)) + .findFirst() + .map(it -> it.getName(to)) + .orElse(dstName); + sink.acceptClass(srcName, srgName); + } + + @Override + public void acceptMethod(IMappingProvider.Member method, String dstName) { + String srgName = mappings.getClasses() + .stream() + .flatMap(it -> it.getMethods().stream()) + .filter(it -> Objects.equals(it.getName(from), dstName)) + .findFirst() + .map(it -> it.getName(to)) + .orElse(dstName); + sink.acceptMethod(method, srgName); + } + + @Override + public void acceptField(IMappingProvider.Member field, String dstName) { + String srgName = mappings.getClasses() + .stream() + .flatMap(it -> it.getFields().stream()) + .filter(it -> Objects.equals(it.getName(from), dstName)) + .findFirst() + .map(it -> it.getName(to)) + .orElse(dstName); + sink.acceptField(field, srgName); + } + + @Override + public void acceptMethodArg(IMappingProvider.Member method, int lvIndex, String dstName) { + } + + @Override + public void acceptMethodVar(IMappingProvider.Member method, int lvIndex, int startOpIdx, int asmIndex, String dstName) { + } + }); + }; + } + private Path[] getRemapClasspath() { FileCollection files = this.classpath; @@ -257,4 +455,14 @@ public class RemapJarTask extends Jar { return this; } + + @Input + public Property<String> getFromM() { + return fromM; + } + + @Input + public Property<String> getToM() { + return toM; + } } diff --git a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java index dcdfbbbd..0d7506e7 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapSourcesJarTask.java @@ -37,18 +37,25 @@ import net.fabricmc.loom.util.SourceRemapper; public class RemapSourcesJarTask extends AbstractLoomTask { private Object input; private Object output; - private String direction = "intermediary"; + private String from = "named"; + private String direction; private SourceRemapper sourceRemapper = null; private boolean preserveFileTimestamps = true; private boolean reproducibleFileOrder = false; public RemapSourcesJarTask() { + this.direction = SourceRemapper.intermediary(getProject()); } @TaskAction public void remap() throws Exception { if (sourceRemapper == null) { - SourceRemapper.remapSources(getProject(), getInput(), getOutput(), direction.equals("named"), reproducibleFileOrder, preserveFileTimestamps); + if (from.equals(direction)) { + SourceRemapper.remapSources(getProject(), getInput(), getOutput(), + direction.equals("named") ? SourceRemapper.intermediary(getProject()) : "named", direction, reproducibleFileOrder, preserveFileTimestamps); + } else { + SourceRemapper.remapSources(getProject(), getInput(), getOutput(), from, direction, reproducibleFileOrder, preserveFileTimestamps); + } } else { sourceRemapper.scheduleRemapSources(getInput(), getOutput(), reproducibleFileOrder, preserveFileTimestamps); } @@ -75,6 +82,11 @@ public class RemapSourcesJarTask extends AbstractLoomTask { } @Input + public String getSourceNamespace() { + return from; + } + + @Input public String getTargetNamespace() { return direction; } @@ -87,6 +99,10 @@ public class RemapSourcesJarTask extends AbstractLoomTask { this.output = output; } + public void setSourceNamespace(String value) { + this.from = value; + } + public void setTargetNamespace(String value) { this.direction = value; } diff --git a/src/main/java/net/fabricmc/loom/task/RunDataTask.java b/src/main/java/net/fabricmc/loom/task/RunDataTask.java new file mode 100644 index 00000000..2c9798dd --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/RunDataTask.java @@ -0,0 +1,38 @@ +/* + * 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.task; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.ide.RunConfig; + +@Deprecated // Replaced by RunGameTask +public class RunDataTask extends AbstractRunTask { + public RunDataTask() { + super(project -> { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + return RunConfig.runConfig(project, extension.getRunConfigs().getByName("data")); + }); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index e3607d4c..ba9cd2e6 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -27,6 +27,7 @@ package net.fabricmc.loom.util; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -44,8 +45,9 @@ public class Checksum { try { HashCode hash = Files.asByteSource(file).hash(Hashing.sha1()); - log.debug("Checksum check: '" + hash.toString() + "' == '" + checksum + "'?"); - return hash.toString().equals(checksum); + String hashString = hash.toString(); + log.debug("Checksum check: '" + hashString + "' == '" + checksum + "'?"); + return hashString.equals(checksum); } catch (IOException e) { e.printStackTrace(); } @@ -70,4 +72,9 @@ public class Checksum { throw new UncheckedIOException("Failed to get file hash of " + file, e); } } + + public static byte[] sha256(String string) { + HashCode hash = Hashing.sha256().hashString(string, StandardCharsets.UTF_8); + return hash.asBytes(); + } } diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 95c7acad..5d2806dd 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -33,11 +33,13 @@ import org.objectweb.asm.Opcodes; import net.fabricmc.loom.configuration.RemappedConfigurationEntry; public class Constants { + public static final String PLUGIN_ID = "dev.architectury.loom"; public static final String LIBRARIES_BASE = "https://libraries.minecraft.net/"; public static final String RESOURCES_BASE = "https://resources.download.minecraft.net/"; public static final String VERSION_MANIFESTS = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; public static final String SYSTEM_ARCH = System.getProperty("os.arch").equals("64") ? "64" : "32"; + public static final String TASK_CATEGORY = "loom"; public static final int ASM_VERSION = Opcodes.ASM9; @@ -66,6 +68,13 @@ public class Constants { public static final String MAPPINGS_FINAL = "mappingsFinal"; public static final String LOADER_DEPENDENCIES = "loaderLibraries"; public static final String LOOM_DEVELOPMENT_DEPENDENCIES = "loomDevelopmentDependencies"; + public static final String SRG = "srg"; + public static final String MCP_CONFIG = "mcp"; + public static final String FORGE = "forge"; + public static final String FORGE_USERDEV = "forgeUserdev"; + public static final String FORGE_INSTALLER = "forgeInstaller"; + public static final String FORGE_UNIVERSAL = "forgeUniversal"; + public static final String FORGE_DEPENDENCIES = "forgeDependencies"; public static final String MAPPING_CONSTANTS = "mappingsConstants"; public static final String UNPICK_CLASSPATH = "unpick"; @@ -81,6 +90,10 @@ public class Constants { public static final String DEV_LAUNCH_INJECTOR = "net.fabricmc:dev-launch-injector:"; public static final String TERMINAL_CONSOLE_APPENDER = "net.minecrell:terminalconsoleappender:"; public static final String JETBRAINS_ANNOTATIONS = "org.jetbrains:annotations:"; + public static final String JAVAX_ANNOTATIONS = "com.google.code.findbugs:jsr305:"; // I hate that I have to add these. + public static final String FORGE_RUNTIME = "dev.architectury:architectury-loom-forge-runtime:"; + public static final String ACCESS_TRANSFORMERS = "net.minecraftforge:accesstransformers:"; + public static final String SPECIAL_SOURCE = "net.md-5:SpecialSource:"; private Dependencies() { } @@ -93,6 +106,10 @@ public class Constants { public static final String DEV_LAUNCH_INJECTOR = "0.2.1+build.8"; public static final String TERMINAL_CONSOLE_APPENDER = "1.2.0"; public static final String JETBRAINS_ANNOTATIONS = "19.0.0"; + public static final String JAVAX_ANNOTATIONS = "3.0.2"; + public static final String FORGE_RUNTIME = "$LOOM_VERSION"; // replaced with current version at build time + public static final String ACCESS_TRANSFORMERS = "2.2.0"; + public static final String SPECIAL_SOURCE = "1.8.3"; private Versions() { } @@ -124,4 +141,11 @@ public class Constants { private Knot() { } } + + public static final class ForgeUserDev { + public static final String LAUNCH_TESTING = "net.minecraftforge.userdev.LaunchTesting"; + + private ForgeUserDev() { + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java b/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java new file mode 100644 index 00000000..7915073d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java @@ -0,0 +1,24 @@ +package net.fabricmc.loom.util; + +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; + +/** + * Simplified dependency downloading. + * + * @author Juuz + */ +public final class DependencyDownloader { + /** + * Resolves a dependency as well as its transitive dependencies into a {@link FileCollection}. + * + * @param project the project needing these files + * @param dependencyNotation the dependency notation + * @return the resolved files + */ + public static FileCollection download(Project project, String dependencyNotation) { + var dependency = project.getDependencies().create(dependencyNotation); + var config = project.getConfigurations().detachedConfiguration(dependency); + return config.fileCollection(dep -> true); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java b/src/main/java/net/fabricmc/loom/util/DownloadUtil.java index 90131b4c..1da65ae5 100644 --- a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java +++ b/src/main/java/net/fabricmc/loom/util/DownloadUtil.java @@ -46,8 +46,8 @@ public class DownloadUtil { * @param logger The logger to print everything to, typically from {@link Project#getLogger()} * @throws IOException If an exception occurs during the process */ - public static void downloadIfChanged(URL from, File to, Logger logger) throws IOException { - downloadIfChanged(from, to, logger, false); + public static boolean downloadIfChanged(URL from, File to, Logger logger) throws IOException { + return downloadIfChanged(from, to, logger, false); } /** @@ -59,7 +59,7 @@ public class DownloadUtil { * @param quiet Whether to only print warnings (when <code>true</code>) or everything * @throws IOException If an exception occurs during the process */ - public static void downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException { + public static boolean downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException { HttpURLConnection connection = (HttpURLConnection) from.openConnection(); if (LoomGradlePlugin.refreshDeps) { @@ -100,7 +100,7 @@ public class DownloadUtil { logger.info("'{}' Not Modified, skipping.", to); } - return; //What we've got is already fine + return false; //What we've got is already fine } long contentLength = connection.getContentLengthLong(); @@ -132,6 +132,8 @@ public class DownloadUtil { saveETag(to, eTag, logger); } + + return true; } /** @@ -220,5 +222,7 @@ public class DownloadUtil { if (etagFile.exists()) { etagFile.delete(); } + + HashedDownloadUtil.delete(file); } } diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java new file mode 100644 index 00000000..89c1024b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.loom.util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +public final class FileSystemUtil { + public static class FileSystemDelegate implements AutoCloseable { + private final FileSystem fileSystem; + private final boolean owner; + + public FileSystemDelegate(FileSystem fileSystem, boolean owner) { + this.fileSystem = fileSystem; + this.owner = owner; + } + + public FileSystem get() { + return fileSystem; + } + + @Override + public void close() throws IOException { + if (owner) { + fileSystem.close(); + } + } + } + + private FileSystemUtil() { + } + + private static final Map<String, String> jfsArgsCreate = new HashMap<>(); + private static final Map<String, String> jfsArgsEmpty = new HashMap<>(); + + static { + jfsArgsCreate.put("create", "true"); + } + + public static FileSystemDelegate getJarFileSystem(File file, boolean create) throws IOException { + return getJarFileSystem(file.toURI(), create); + } + + public static FileSystemDelegate getJarFileSystem(Path path, boolean create) throws IOException { + return getJarFileSystem(path.toUri(), create); + } + + public static FileSystemDelegate getJarFileSystem(URI uri, boolean create) throws IOException { + URI jarUri; + + try { + jarUri = new URI("jar:" + uri.getScheme(), uri.getHost(), uri.getPath(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + try { + return new FileSystemDelegate(FileSystems.newFileSystem(jarUri, create ? jfsArgsCreate : jfsArgsEmpty), true); + } catch (FileSystemAlreadyExistsException e) { + return new FileSystemDelegate(FileSystems.getFileSystem(jarUri), false); + } catch (IOException e) { + throw new IOException("Could not create JAR file system for " + uri + " (create: " + create + ")", e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java b/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java index b8628c29..712043da 100644 --- a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java +++ b/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java @@ -41,20 +41,48 @@ import org.gradle.api.logging.Logger; import net.fabricmc.loom.LoomGradlePlugin; public class HashedDownloadUtil { + public static boolean requiresDownload(File to, String expectedHash, Logger logger) { + if (LoomGradlePlugin.refreshDeps) { + return true; + } + + if (to.exists()) { + String sha1 = getSha1(to, logger); + + // The hash in the sha1 file matches + return !expectedHash.equals(sha1); + } + + return true; + } + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet) throws IOException { - downloadIfInvalid(from, to, expectedHash, logger, quiet, () -> { }); + downloadIfInvalid(from, to, expectedHash, logger, quiet, true); + } + + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict) throws IOException { + downloadIfInvalid(from, to, expectedHash, logger, quiet, strict, () -> { }); } - public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, Runnable startDownload) throws IOException { + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict, Runnable startDownload) throws IOException { if (LoomGradlePlugin.refreshDeps) { delete(to); } - String sha1 = getSha1(to, logger); - - if (expectedHash.equals(sha1)) { - // The hash in the sha1 file matches - return; + if (to.exists()) { + if (strict) { + if (Checksum.equals(to, expectedHash)) { + // The hash matches the target file + return; + } + } else { + String sha1 = getSha1(to, logger); + + if (expectedHash.equals(sha1)) { + // The hash in the sha1 file matches + return; + } + } } startDownload.run(); diff --git a/src/main/java/net/fabricmc/loom/util/JarUtil.java b/src/main/java/net/fabricmc/loom/util/JarUtil.java new file mode 100644 index 00000000..98c1a585 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/JarUtil.java @@ -0,0 +1,45 @@ +/* + * 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.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Working with jars. + * + * @author Juuz + */ +public final class JarUtil { + public static void extractFile(File jar, String filePath, File target) throws IOException { + try (FileSystemUtil.FileSystemDelegate fs = FileSystemUtil.getJarFileSystem(jar, false)) { + Path targetPath = target.toPath(); + Files.deleteIfExists(targetPath); + Files.copy(fs.get().getPath(filePath), targetPath); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/LoggerFilter.java b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java new file mode 100644 index 00000000..b7803c34 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java @@ -0,0 +1,49 @@ +/* + * 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.util; + +import java.io.PrintStream; + +import org.jetbrains.annotations.NotNull; + +public class LoggerFilter { + public static void replaceSystemOut() { + try { + PrintStream previous = System.out; + System.setOut(new PrintStream(previous) { + @Override + public PrintStream printf(@NotNull String format, Object... args) { + if (format.equals("unknown invokedynamic bsm: %s/%s%s (tag=%d iif=%b)%n")) { + return this; + } + + return super.printf(format, args); + } + }); + } catch (SecurityException ignored) { + // Failed to replace logger filter, just ignore + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/ModPlatform.java b/src/main/java/net/fabricmc/loom/util/ModPlatform.java new file mode 100644 index 00000000..11b2c4ef --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ModPlatform.java @@ -0,0 +1,30 @@ +/* + * 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.util; + +public enum ModPlatform { + FABRIC, + FORGE, +} diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index 232b018a..b1283fc9 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Set; import java.util.function.Consumer; +import com.google.common.base.Stopwatch; import org.cadixdev.lorenz.MappingSet; import org.cadixdev.mercury.Mercury; import org.cadixdev.mercury.remapper.MercuryRemapper; @@ -42,34 +43,44 @@ import org.zeroturnaround.zip.ZipUtil; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.RemappedConfigurationEntry; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; -import net.fabricmc.loom.util.gradle.ProgressLogger; +import net.fabricmc.loom.configuration.providers.minecraft.tr.MercuryUtils; import net.fabricmc.lorenztiny.TinyMappingsReader; import net.fabricmc.mapping.tree.TinyTree; import net.fabricmc.stitch.util.StitchUtil; public class SourceRemapper { private final Project project; - private final boolean toNamed; - private final List<Consumer<ProgressLogger>> remapTasks = new ArrayList<>(); + private String from; + private String to; + private final List<Consumer<Mercury>> remapTasks = new ArrayList<>(); private Mercury mercury; - public SourceRemapper(Project project, boolean toNamed) { + public SourceRemapper(Project project, boolean named) { + this(project, named ? intermediary(project) : "named", !named ? intermediary(project) : "named"); + } + + public static String intermediary(Project project) { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + return extension.isForge() ? "srg" : "intermediary"; + } + + public SourceRemapper(Project project, String from, String to) { this.project = project; - this.toNamed = toNamed; + this.from = from; + this.to = to; } - public static void remapSources(Project project, File input, File output, boolean named, boolean reproducibleFileOrder, boolean preserveFileTimestamps) { - SourceRemapper sourceRemapper = new SourceRemapper(project, named); + public static void remapSources(Project project, File input, File output, String from, String to, boolean reproducibleFileOrder, boolean preserveFileTimestamps) { + SourceRemapper sourceRemapper = new SourceRemapper(project, from, to); sourceRemapper.scheduleRemapSources(input, output, reproducibleFileOrder, preserveFileTimestamps); sourceRemapper.remapAll(); } public void scheduleRemapSources(File source, File destination, boolean reproducibleFileOrder, boolean preserveFileTimestamps) { - remapTasks.add((logger) -> { + remapTasks.add((mercury) -> { try { - logger.progress("remapping sources - " + source.getName()); - remapSourcesInner(source, destination); + remapSourcesInner(mercury, source, destination); ZipReprocessorUtil.reprocessZip(destination, reproducibleFileOrder, preserveFileTimestamps); // Set the remapped sources creation date to match the sources if we're likely succeeded in making it @@ -87,22 +98,21 @@ public class SourceRemapper { return; } - project.getLogger().lifecycle(":remapping sources"); - - ProgressLogger progressLogger = ProgressLogger.getProgressFactory(project, SourceRemapper.class.getName()); - progressLogger.start("Remapping dependency sources", "sources"); + Stopwatch stopwatch = Stopwatch.createStarted(); + project.getLogger().lifecycle(":remapping " + remapTasks.size() + " sources"); - remapTasks.forEach(consumer -> consumer.accept(progressLogger)); + Mercury mercury = getMercuryInstance(); + ThreadingUtils.run(remapTasks, consumer -> consumer.accept(MercuryUtils.copyMercury(mercury))); - progressLogger.completed(); + project.getLogger().lifecycle(":remapped " + remapTasks.size() + " sources in " + stopwatch.stop()); // TODO: FIXME - WORKAROUND https://github.com/FabricMC/fabric-loom/issues/45 System.gc(); } - private void remapSourcesInner(File source, File destination) throws Exception { - project.getLogger().info(":remapping source jar"); - Mercury mercury = getMercuryInstance(); + private void remapSourcesInner(Mercury mercury, File source, File destination) throws Exception { + Stopwatch stopwatch = Stopwatch.createStarted(); + project.getLogger().info(":remapping source jar " + source.getName() + " from " + from + " to " + to); if (source.equals(destination)) { if (source.isDirectory()) { @@ -152,6 +162,8 @@ public class SourceRemapper { if (isSrcTmp) { Files.walkFileTree(srcPath, new DeletingFileVisitor()); } + + project.getLogger().info(":remapped source jar " + source.getName() + " from " + from + " to " + to + " in " + stopwatch.stop()); } private Mercury getMercuryInstance() { @@ -162,18 +174,27 @@ public class SourceRemapper { LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); MappingsProviderImpl mappingsProvider = extension.getMappingsProvider(); - MappingSet mappings = extension.getOrCreateSrcMappingCache(toNamed ? 1 : 0, () -> { + String intermediary = extension.isForge() ? "srg" : "intermediary"; + int id = -1; + + if (from.equals(intermediary) && to.equals("named")) { + id = 1; + } else if (to.equals(intermediary) && from.equals("named")) { + id = 0; + } + + MappingSet mappings = extension.getOrCreateSrcMappingCache(id, () -> { try { - TinyTree m = mappingsProvider.getMappings(); - project.getLogger().info(":loading " + (toNamed ? "intermediary -> named" : "named -> intermediary") + " source mappings"); - return new TinyMappingsReader(m, toNamed ? "intermediary" : "named", toNamed ? "named" : "intermediary").read(); + TinyTree m = extension.shouldGenerateSrgTiny() ? mappingsProvider.getMappingsWithSrg() : mappingsProvider.getMappings(); + project.getLogger().info(":loading " + from + " -> " + to + " source mappings"); + return new TinyMappingsReader(m, from, to).read(); } catch (Exception e) { throw new RuntimeException(e); } }); - Mercury mercury = extension.getOrCreateSrcMercuryCache(toNamed ? 1 : 0, () -> { - Mercury m = createMercuryWithClassPath(project, toNamed); + Mercury mercury = extension.getOrCreateSrcMercuryCache(id, () -> { + Mercury m = createMercuryWithClassPath(project, to.equals("named")); for (File file : extension.getUnmappedModCollection()) { Path path = file.toPath(); @@ -186,6 +207,10 @@ public class SourceRemapper { m.getClassPath().add(extension.getMinecraftMappedProvider().getMappedJar().toPath()); m.getClassPath().add(extension.getMinecraftMappedProvider().getIntermediaryJar().toPath()); + if (extension.isForge()) { + m.getClassPath().add(extension.getMinecraftMappedProvider().getSrgJar().toPath()); + } + Set<File> files = project.getConfigurations() .detachedConfiguration(project.getDependencies().create(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS)) .resolve(); diff --git a/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java b/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java new file mode 100644 index 00000000..0645f319 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java @@ -0,0 +1,192 @@ +/* + * 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.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.base.Stopwatch; + +public class ThreadingUtils { + public static <T> void run(T[] values, UnsafeConsumer<T> action) { + run(Arrays.stream(values) + .<UnsafeRunnable>map(t -> () -> action.accept(t)) + .collect(Collectors.toList())); + } + + public static <T> void run(Collection<T> values, UnsafeConsumer<T> action) { + run(values.stream() + .<UnsafeRunnable>map(t -> () -> action.accept(t)) + .collect(Collectors.toList())); + } + + public static void run(UnsafeRunnable... jobs) { + run(Arrays.asList(jobs)); + } + + public static void run(Collection<UnsafeRunnable> jobs) { + try { + ExecutorService service = Executors.newFixedThreadPool(Math.min(jobs.size(), Runtime.getRuntime().availableProcessors() / 2)); + List<Future<?>> futures = new LinkedList<>(); + + for (UnsafeRunnable runnable : jobs) { + futures.add(service.submit(() -> { + try { + runnable.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + })); + } + + for (Future<?> future : futures) { + future.get(); + } + + service.shutdownNow(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public static <T, R> List<R> get(Collection<T> values, Function<T, R> action) { + return get(values.stream() + .<UnsafeCallable<R>>map(t -> () -> action.apply(t)) + .collect(Collectors.toList())); + } + + @SafeVarargs + public static <T> List<T> get(UnsafeCallable<T>... jobs) { + return get(Arrays.asList(jobs)); + } + + public static <T> List<T> get(Collection<UnsafeCallable<T>> jobs) { + try { + ExecutorService service = Executors.newFixedThreadPool(Math.min(jobs.size(), Runtime.getRuntime().availableProcessors() / 2)); + List<Future<T>> futures = new LinkedList<>(); + List<T> result = new ArrayList<>(); + + for (UnsafeCallable<T> runnable : jobs) { + futures.add(service.submit(() -> { + try { + return runnable.call(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + })); + } + + for (Future<T> future : futures) { + result.add(future.get()); + } + + service.shutdownNow(); + return result; + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public interface UnsafeRunnable { + void run() throws Throwable; + } + + public interface UnsafeCallable<T> { + T call() throws Throwable; + } + + public interface UnsafeConsumer<T> { + void accept(T value) throws Throwable; + } + + public static TaskCompleter taskCompleter() { + return new TaskCompleter(); + } + + public static class TaskCompleter implements Function<Throwable, Void> { + Stopwatch stopwatch = Stopwatch.createUnstarted(); + List<CompletableFuture<?>> tasks = new ArrayList<>(); + ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + List<UnsafeConsumer<Stopwatch>> completionListener = new ArrayList<>(); + + public TaskCompleter add(UnsafeRunnable job) { + if (!stopwatch.isRunning()) { + stopwatch.start(); + } + + tasks.add(CompletableFuture.runAsync(() -> { + try { + job.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + }, service).exceptionally(this)); + + return this; + } + + public TaskCompleter onComplete(UnsafeConsumer<Stopwatch> consumer) { + completionListener.add(consumer); + return this; + } + + public void complete() { + try { + CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0])).exceptionally(this).get(); + service.shutdownNow(); + + if (stopwatch.isRunning()) { + stopwatch.stop(); + } + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + try { + for (UnsafeConsumer<Stopwatch> consumer : completionListener) { + consumer.accept(stopwatch); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + @Override + public Void apply(Throwable throwable) { + throwable.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperMappingsHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperMappingsHelper.java index 7470c0fa..f5b9b911 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperMappingsHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperMappingsHelper.java @@ -24,13 +24,14 @@ package net.fabricmc.loom.util; +import dev.architectury.tinyremapper.IMappingProvider; + import net.fabricmc.mapping.tree.ClassDef; import net.fabricmc.mapping.tree.FieldDef; import net.fabricmc.mapping.tree.LocalVariableDef; import net.fabricmc.mapping.tree.MethodDef; import net.fabricmc.mapping.tree.ParameterDef; import net.fabricmc.mapping.tree.TinyTree; -import net.fabricmc.tinyremapper.IMappingProvider; public class TinyRemapperMappingsHelper { private TinyRemapperMappingsHelper() { } diff --git a/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java new file mode 100644 index 00000000..37d648d8 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java @@ -0,0 +1,75 @@ +/* + * 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.util.function; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Stream-like utilities for working with collections. + * + * @author Juuz + */ +public final class CollectionUtil { + /** + * Finds the first element matching the predicate. + * + * @param collection the collection to be searched + * @param filter the predicate to be matched + * @param <E> the element type + * @return the first matching element, or empty if none match + */ + public static <E> Optional<E> find(Iterable<? extends E> collection, Predicate<? super E> filter) { + for (E e : collection) { + if (filter.test(e)) { + return Optional.of(e); + } + } + + return Optional.empty(); + } + + /** + * Transforms the collection with a function. + * + * @param collection the source collection + * @param transform the transformation function + * @param <A> the source type + * @param <B> the target type + * @return a mutable list with the transformed entries + */ + public static <A, B> List<B> map(Iterable<? extends A> collection, Function<? super A, ? extends B> transform) { + ArrayList<B> result = new ArrayList<>(); + + for (A a : collection) { + result.add(transform.apply(a)); + } + + return result; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java b/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java new file mode 100644 index 00000000..f17364b2 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java @@ -0,0 +1,39 @@ +/* + * 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.util.function; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Path; + +/** + * Consumes two file systems and corresponding path objects. + * + * @author Juuz + */ +@FunctionalInterface +public interface FsPathConsumer { + void accept(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java b/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java new file mode 100644 index 00000000..0974d95f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java @@ -0,0 +1,38 @@ +/* + * 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.util.function; + +import java.io.IOException; + +/** + * Like Consumer, but can throw IOException. + * + * @param <A> the result type + * @author Juuz + */ +@FunctionalInterface +public interface IoConsumer<A> { + void accept(A a) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/function/LazyBool.java b/src/main/java/net/fabricmc/loom/util/function/LazyBool.java new file mode 100644 index 00000000..ff53aace --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/LazyBool.java @@ -0,0 +1,52 @@ +/* + * 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.util.function; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +/** + * A lazily computed boolean value. + * + * @author Juuz + */ +public final class LazyBool implements BooleanSupplier { + private BooleanSupplier supplier; + private Boolean value; + + public LazyBool(BooleanSupplier supplier) { + this.supplier = Objects.requireNonNull(supplier, "supplier"); + } + + @Override + public boolean getAsBoolean() { + if (value == null) { + value = supplier.getAsBoolean(); + supplier = null; // Release the supplier + } + + return value; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java new file mode 100644 index 00000000..699e0daf --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java @@ -0,0 +1,139 @@ +/* + * 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.util.srg; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +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.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import com.google.common.collect.ImmutableMap; +import org.gradle.api.logging.Logger; + +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mapping.tree.TinyTree; + +/** + * Remaps AT classes from SRG to Yarn. + * + * @author Juuz + */ +public final class AtRemapper { + public static void remap(Logger logger, Path jar, TinyTree mappings) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), ImmutableMap.of("create", false))) { + Path atPath = fs.getPath("META-INF/accesstransformer.cfg"); + + if (Files.exists(atPath)) { + String atContent = Files.readString(atPath); + + String[] lines = atContent.split("\n"); + List<String> output = new ArrayList<>(lines.length); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + + if (line.startsWith("#") || line.isBlank()) { + output.add(i, line); + continue; + } + + String[] parts = line.split("\\s+"); + + if (parts.length < 2) { + logger.warn("Invalid AT Line: " + line); + output.add(i, line); + continue; + } + + String name = parts[1].replace('.', '/'); + parts[1] = CollectionUtil.find( + mappings.getClasses(), + def -> def.getName("srg").equals(name) + ).map(def -> def.getName("named")).orElse(name).replace('/', '.'); + + if (parts.length >= 3) { + if (parts[2].contains("(")) { + parts[2] = parts[2].substring(0, parts[2].indexOf('(')) + remapDescriptor(parts[2].substring(parts[2].indexOf('(')), s -> { + return CollectionUtil.find( + mappings.getClasses(), + def -> def.getName("srg").equals(s) + ).map(def -> def.getName("named")).orElse(s); + }); + } + } + + output.add(i, String.join(" ", parts)); + } + + Files.write(atPath, String.join("\n", output).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } + } + } + + private static String remapDescriptor(String original, UnaryOperator<String> classMappings) { + try { + StringReader reader = new StringReader(original); + StringBuilder result = new StringBuilder(); + boolean insideClassName = false; + StringBuilder className = new StringBuilder(); + + while (true) { + int c = reader.read(); + + if (c == -1) { + break; + } + + if ((char) c == ';') { + insideClassName = false; + result.append(classMappings.apply(className.toString())); + } + + if (insideClassName) { + className.append((char) c); + } else { + result.append((char) c); + } + + if (!insideClassName && (char) c == 'L') { + insideClassName = true; + className.setLength(0); + } + } + + return result.toString(); + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java new file mode 100644 index 00000000..1ad1a87b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java @@ -0,0 +1,115 @@ +/* + * 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.util.srg; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +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.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.logging.Logger; + +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mapping.tree.TinyTree; + +/** + * Remaps coremod class names from SRG to Yarn. + * + * @author Juuz + */ +public final class CoreModClassRemapper { + private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^(.*')((?:com\\.mojang\\.|net\\.minecraft\\.)[A-Za-z0-9.-_$]+)('.*)$"); + + public static void remapJar(Path jar, TinyTree mappings, Logger logger) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), ImmutableMap.of("create", false))) { + Path coremodsJsonPath = fs.getPath("META-INF", "coremods.json"); + + if (Files.notExists(coremodsJsonPath)) { + logger.info(":no coremods in " + jar.getFileName()); + return; + } + + JsonObject coremodsJson; + + try (Reader reader = Files.newBufferedReader(coremodsJsonPath)) { + coremodsJson = new Gson().fromJson(reader, JsonObject.class); + } + + for (Map.Entry<String, JsonElement> nameFileEntry : coremodsJson.entrySet()) { + String file = nameFileEntry.getValue().getAsString(); + Path js = fs.getPath(file); + + if (Files.exists(js)) { + logger.info(":remapping coremod '" + file + "'"); + remap(js, mappings); + } else { + logger.warn("Coremod '" + file + "' listed in coremods.json but not found"); + } + } + } + } + + public static void remap(Path js, TinyTree mappings) throws IOException { + List<String> lines = Files.readAllLines(js); + List<String> output = new ArrayList<>(lines); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + Matcher matcher = CLASS_NAME_PATTERN.matcher(line); + + if (matcher.matches()) { + String className = matcher.group(2).replace('.', '/'); + String remapped = CollectionUtil.find(mappings.getClasses(), def -> def.getName("srg").equals(className)) + .map(def -> def.getName("named")) + .orElse(className); + + if (!className.equals(remapped)) { + output.set(i, matcher.group(1) + remapped.replace('/', '.') + matcher.group(3)); + } + } + } + + if (!lines.equals(output)) { + try (Writer writer = Files.newBufferedWriter(js, StandardCharsets.UTF_8, StandardOpenOption.WRITE)) { + writer.write(String.join("\n", output)); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java new file mode 100644 index 00000000..021410b6 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java @@ -0,0 +1,85 @@ +/* + * 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.util.srg; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import dev.architectury.tinyremapper.IMappingProvider; + +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.FileSystemUtil.FileSystemDelegate; +import net.fabricmc.mapping.tree.ClassDef; +import net.fabricmc.mapping.tree.TinyTree; + +public class InnerClassRemapper { + public static IMappingProvider of(Path fromJar, TinyTree mappingsWithSrg, String from, String to) throws IOException { + return sink -> { + remapInnerClass(fromJar, mappingsWithSrg, from, to, sink::acceptClass); + }; + } + + private static void remapInnerClass(Path fromJar, TinyTree mappingsWithSrg, String from, String to, BiConsumer<String, String> action) { + try (FileSystemDelegate system = FileSystemUtil.getJarFileSystem(fromJar, false)) { + Map<String, String> availableClasses = mappingsWithSrg.getClasses().stream() + .collect(Collectors.groupingBy(classDef -> classDef.getName(from), + Collectors.<ClassDef, String>reducing( + null, + classDef -> classDef.getName(to), + (first, last) -> last + )) + ); + Iterator<Path> iterator = Files.walk(system.get().getPath("/")).iterator(); + + while (iterator.hasNext()) { + Path path = iterator.next(); + String name = path.toString(); + if (name.startsWith("/")) name = name.substring(1); + + if (!Files.isDirectory(path) && name.contains("$") && name.endsWith(".class")) { + String className = name.substring(0, name.length() - 6); + + if (!availableClasses.containsKey(className)) { + String parentName = className.substring(0, className.indexOf('$')); + String childName = className.substring(className.indexOf('$') + 1); + String remappedParentName = availableClasses.getOrDefault(parentName, parentName); + String remappedName = remappedParentName + "$" + childName; + + if (!className.equals(remappedName)) { + action.accept(className, remappedName); + } + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java b/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java new file mode 100644 index 00000000..e0b4ed5b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java @@ -0,0 +1,364 @@ +/* + * 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.util.srg; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +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.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvValidationException; +import dev.architectury.mappingslayers.api.mutable.MutableClassDef; +import dev.architectury.mappingslayers.api.mutable.MutableFieldDef; +import dev.architectury.mappingslayers.api.mutable.MutableMethodDef; +import dev.architectury.mappingslayers.api.mutable.MutableTinyTree; +import dev.architectury.mappingslayers.api.utils.MappingsUtils; +import org.apache.commons.io.IOUtils; +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgReader; +import org.cadixdev.lorenz.model.ClassMapping; +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.jetbrains.annotations.Nullable; + +import net.fabricmc.stitch.commands.tinyv2.TinyClass; +import net.fabricmc.stitch.commands.tinyv2.TinyField; +import net.fabricmc.stitch.commands.tinyv2.TinyFile; +import net.fabricmc.stitch.commands.tinyv2.TinyMethod; +import net.fabricmc.stitch.commands.tinyv2.TinyMethodParameter; +import net.fabricmc.stitch.commands.tinyv2.TinyV2Reader; + +public class MCPReader { + private final Path intermediaryTinyPath; + private final Path srgTsrgPath; + + public MCPReader(Path intermediaryTinyPath, Path srgTsrgPath) { + this.intermediaryTinyPath = intermediaryTinyPath; + this.srgTsrgPath = srgTsrgPath; + } + + public TinyFile read(Path mcpJar) throws IOException { + Map<MemberToken, String> srgTokens = readSrg(); + TinyFile intermediaryTiny = TinyV2Reader.read(intermediaryTinyPath); + Map<String, String> intermediaryToMCPMap = createIntermediaryToMCPMap(intermediaryTiny, srgTokens); + Map<String, String[]> intermediaryToDocsMap = new HashMap<>(); + Map<String, Map<Integer, String>> intermediaryToParamsMap = new HashMap<>(); + + try { + injectMcp(mcpJar, intermediaryToMCPMap, intermediaryToDocsMap, intermediaryToParamsMap); + } catch (CsvValidationException e) { + throw new RuntimeException(e); + } + + mergeTokensIntoIntermediary(intermediaryTiny, intermediaryToMCPMap, intermediaryToDocsMap, intermediaryToParamsMap); + return intermediaryTiny; + } + + private Map<String, String> createIntermediaryToMCPMap(TinyFile tiny, Map<MemberToken, String> officialToMCP) { + Map<String, String> map = new HashMap<>(); + + for (TinyClass tinyClass : tiny.getClassEntries()) { + String classObf = tinyClass.getMapping().get(0); + String classIntermediary = tinyClass.getMapping().get(1); + MemberToken classTokenObf = MemberToken.ofClass(classObf); + + if (officialToMCP.containsKey(classTokenObf)) { + map.put(classIntermediary, officialToMCP.get(classTokenObf)); + } + + for (TinyField tinyField : tinyClass.getFields()) { + String fieldObf = tinyField.getMapping().get(0); + String fieldIntermediary = tinyField.getMapping().get(1); + MemberToken fieldTokenObf = MemberToken.ofField(classTokenObf, fieldObf); + + if (officialToMCP.containsKey(fieldTokenObf)) { + map.put(fieldIntermediary, officialToMCP.get(fieldTokenObf)); + } + } + + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + String methodObf = tinyMethod.getMapping().get(0); + String methodIntermediary = tinyMethod.getMapping().get(1); + MemberToken methodTokenObf = MemberToken.ofMethod(classTokenObf, methodObf, tinyMethod.getMethodDescriptorInFirstNamespace()); + + if (officialToMCP.containsKey(methodTokenObf)) { + map.put(methodIntermediary, officialToMCP.get(methodTokenObf)); + } + } + } + + return map; + } + + private void mergeTokensIntoIntermediary(TinyFile tiny, Map<String, String> intermediaryToMCPMap, Map<String, String[]> intermediaryToDocsMap, Map<String, Map<Integer, String>> intermediaryToParamsMap) { + stripTinyWithParametersAndLocal(tiny); + + // We will be adding the "named" namespace with MCP + tiny.getHeader().getNamespaces().add("named"); + + for (TinyClass tinyClass : tiny.getClassEntries()) { + String classIntermediary = tinyClass.getMapping().get(1); + tinyClass.getMapping().add(intermediaryToMCPMap.getOrDefault(classIntermediary, classIntermediary)); + + for (TinyField tinyField : tinyClass.getFields()) { + String fieldIntermediary = tinyField.getMapping().get(1); + String[] docs = intermediaryToDocsMap.get(fieldIntermediary); + tinyField.getMapping().add(intermediaryToMCPMap.getOrDefault(fieldIntermediary, fieldIntermediary)); + + if (docs != null) { + tinyField.getComments().clear(); + tinyField.getComments().addAll(Arrays.asList(docs)); + } + } + + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + String methodIntermediary = tinyMethod.getMapping().get(1); + String[] docs = intermediaryToDocsMap.get(methodIntermediary); + tinyMethod.getMapping().add(intermediaryToMCPMap.getOrDefault(methodIntermediary, methodIntermediary)); + + if (docs != null) { + tinyMethod.getComments().clear(); + tinyMethod.getComments().addAll(Arrays.asList(docs)); + } + + Map<Integer, String> params = intermediaryToParamsMap.get(methodIntermediary); + + if (params != null) { + for (Map.Entry<Integer, String> entry : params.entrySet()) { + int lvIndex = entry.getKey(); + String paramName = entry.getValue(); + + ArrayList<String> mappings = new ArrayList<>(); + mappings.add(""); + mappings.add(""); + mappings.add(paramName); + tinyMethod.getParameters().add(new TinyMethodParameter(lvIndex, mappings, new ArrayList<>())); + } + } + } + } + } + + private void stripTinyWithParametersAndLocal(TinyFile tiny) { + for (TinyClass tinyClass : tiny.getClassEntries()) { + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + tinyMethod.getParameters().clear(); + tinyMethod.getLocalVariables().clear(); + } + } + } + + private Map<MemberToken, String> readSrg() throws IOException { + Map<MemberToken, String> tokens = new HashMap<>(); + + try (BufferedReader reader = Files.newBufferedReader(srgTsrgPath, StandardCharsets.UTF_8)) { + String content = IOUtils.toString(reader); + + if (content.startsWith("tsrg2")) { + readTsrg2(tokens, content); + } else { + MappingSet mappingSet = new TSrgReader(new StringReader(content)).read(); + + for (TopLevelClassMapping classMapping : mappingSet.getTopLevelClassMappings()) { + appendClass(tokens, classMapping); + } + } + } + + return tokens; + } + + private void readTsrg2(Map<MemberToken, String> tokens, String content) { + MutableTinyTree tree = MappingsUtils.deserializeFromTsrg2(content); + int obfIndex = tree.getMetadata().index("obf"); + int srgIndex = tree.getMetadata().index("srg"); + + for (MutableClassDef classDef : tree.getClassesMutable()) { + MemberToken ofClass = MemberToken.ofClass(classDef.getName(obfIndex)); + tokens.put(ofClass, classDef.getName(srgIndex)); + + for (MutableFieldDef fieldDef : classDef.getFieldsMutable()) { + tokens.put(MemberToken.ofField(ofClass, fieldDef.getName(obfIndex)), fieldDef.getName(srgIndex)); + } + + for (MutableMethodDef methodDef : classDef.getMethodsMutable()) { + tokens.put(MemberToken.ofMethod(ofClass, methodDef.getName(obfIndex), methodDef.getDescriptor(obfIndex)), + methodDef.getName(srgIndex)); + } + } + } + + private void injectMcp(Path mcpJar, Map<String, String> intermediaryToSrgMap, Map<String, String[]> intermediaryToDocsMap, Map<String, Map<Integer, String>> intermediaryToParamsMap) + throws IOException, CsvValidationException { + Map<String, List<String>> srgToIntermediary = inverseMap(intermediaryToSrgMap); + Map<String, List<String>> simpleSrgToIntermediary = new HashMap<>(); + Pattern methodPattern = Pattern.compile("(func_\\d*)_.*"); + + for (Map.Entry<String, List<String>> entry : srgToIntermediary.entrySet()) { + Matcher matcher = methodPattern.matcher(entry.getKey()); + + if (matcher.matches()) { + simpleSrgToIntermediary.put(matcher.group(1), entry.getValue()); + } + } + + try (FileSystem fs = FileSystems.newFileSystem(mcpJar, (ClassLoader) null)) { + Path fields = fs.getPath("fields.csv"); + Path methods = fs.getPath("methods.csv"); + Path params = fs.getPath("params.csv"); + Pattern paramsPattern = Pattern.compile("p_[^\\d]*(\\d+)_(\\d)+_?"); + + try (CSVReader reader = new CSVReader(Files.newBufferedReader(fields, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + List<String> intermediaryField = srgToIntermediary.get(line[0]); + String[] docs = line[3].split("\n"); + + if (intermediaryField != null) { + for (String s : intermediaryField) { + intermediaryToSrgMap.put(s, line[1]); + + if (!line[3].trim().isEmpty() && docs.length > 0) { + intermediaryToDocsMap.put(s, docs); + } + } + } + } + } + + try (CSVReader reader = new CSVReader(Files.newBufferedReader(methods, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + List<String> intermediaryMethod = srgToIntermediary.get(line[0]); + String[] docs = line[3].split("\n"); + + if (intermediaryMethod != null) { + for (String s : intermediaryMethod) { + intermediaryToSrgMap.put(s, line[1]); + + if (!line[3].trim().isEmpty() && docs.length > 0) { + intermediaryToDocsMap.put(s, docs); + } + } + } + } + } + + if (Files.exists(params)) { + try (CSVReader reader = new CSVReader(Files.newBufferedReader(params, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + Matcher param = paramsPattern.matcher(line[0]); + + if (param.matches()) { + String named = line[1]; + String srgMethodStartWith = "func_" + param.group(1); + int lvIndex = Integer.parseInt(param.group(2)); + List<String> intermediaryMethod = simpleSrgToIntermediary.get(srgMethodStartWith); + + if (intermediaryMethod != null) { + for (String s : intermediaryMethod) { + intermediaryToParamsMap.computeIfAbsent(s, s1 -> new HashMap<>()).put(lvIndex, named); + } + } + } + } + } + } + } + } + + private Map<String, List<String>> inverseMap(Map<String, String> intermediaryToMCPMap) { + Map<String, List<String>> map = new HashMap<>(); + + for (Map.Entry<String, String> token : intermediaryToMCPMap.entrySet()) { + map.computeIfAbsent(token.getValue(), s -> new ArrayList<>()).add(token.getKey()); + } + + return map; + } + + private void appendClass(Map<MemberToken, String> tokens, ClassMapping<?, ?> classMapping) { + MemberToken ofClass = MemberToken.ofClass(classMapping.getFullObfuscatedName()); + tokens.put(ofClass, classMapping.getFullDeobfuscatedName()); + + for (FieldMapping fieldMapping : classMapping.getFieldMappings()) { + tokens.put(MemberToken.ofField(ofClass, fieldMapping.getObfuscatedName()), fieldMapping.getDeobfuscatedName()); + } + + for (MethodMapping methodMapping : classMapping.getMethodMappings()) { + tokens.put(MemberToken.ofMethod(ofClass, methodMapping.getObfuscatedName(), methodMapping.getObfuscatedDescriptor()), methodMapping.getDeobfuscatedName()); + } + + for (InnerClassMapping mapping : classMapping.getInnerClassMappings()) { + appendClass(tokens, mapping); + } + } + + private record MemberToken( + TokenType type, + @Nullable MCPReader.MemberToken owner, + String name, + @Nullable String descriptor + ) { + static MemberToken ofClass(String name) { + return new MemberToken(TokenType.CLASS, null, name, null); + } + + static MemberToken ofField(MemberToken owner, String name) { + return new MemberToken(TokenType.FIELD, owner, name, null); + } + + static MemberToken ofMethod(MemberToken owner, String name, String descriptor) { + return new MemberToken(TokenType.METHOD, owner, name, descriptor); + } + } + + private enum TokenType { + CLASS, + METHOD, + FIELD + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/MappingException.java b/src/main/java/net/fabricmc/loom/util/srg/MappingException.java new file mode 100644 index 00000000..ada083ce --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/MappingException.java @@ -0,0 +1,36 @@ +/* + * 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.util.srg; + +/** + * An exception that occurs when processing obfuscation mappings. + * + * @author Juuz + */ +public class MappingException extends RuntimeException { + public MappingException(String message) { + super(message); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java b/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java new file mode 100644 index 00000000..bfdf9332 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java @@ -0,0 +1,107 @@ +/* + * 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.util.srg; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +import org.apache.commons.io.IOUtils; +import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.zeroturnaround.zip.ZipUtil; + +import net.fabricmc.loom.LoomGradleExtension; + +public class SpecialSourceExecutor { + public static Path produceSrgJar(Project project, String side, FileCollection specialSourceCp, Path officialJar, Path srgPath) + throws Exception { + Set<String> filter = Files.readAllLines(srgPath, StandardCharsets.UTF_8).stream() + .filter(s -> !s.startsWith("\t")) + .map(s -> s.split(" ")[0] + ".class") + .collect(Collectors.toSet()); + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + Path stripped = extension.getProjectBuildCache().toPath().resolve(officialJar.getFileName().toString().substring(0, officialJar.getFileName().toString().length() - 4) + "-filtered.jar"); + Files.deleteIfExists(stripped); + + try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(stripped))) { + ZipUtil.iterate(officialJar.toFile(), (in, zipEntry) -> { + if (filter.contains(zipEntry.getName())) { + output.putNextEntry((ZipEntry) zipEntry.clone()); + IOUtils.write(IOUtils.toByteArray(in), output); + output.closeEntry(); + } + }); + } + + Path output = extension.getProjectBuildCache().toPath().resolve(officialJar.getFileName().toString().substring(0, officialJar.getFileName().toString().length() - 4) + "-srg-output.jar"); + Files.deleteIfExists(output); + + String[] args = new String[] { + "--in-jar", + stripped.toAbsolutePath().toString(), + "--out-jar", + output.toAbsolutePath().toString(), + "--srg-in", + srgPath.toAbsolutePath().toString() + }; + + project.getLogger().lifecycle(":remapping minecraft (SpecialSource, " + side + ", official -> srg)"); + + Path workingDir = tmpDir(); + + project.javaexec(spec -> { + spec.setArgs(Arrays.asList(args)); + spec.setClasspath(specialSourceCp); + spec.workingDir(workingDir.toFile()); + spec.setMain("net.md_5.specialsource.SpecialSource"); + spec.setStandardOutput(System.out); + spec.setErrorOutput(System.out); + }).rethrowFailure().assertNormalExitValue(); + + Files.deleteIfExists(stripped); + + Path tmp = tmpFile(); + Files.deleteIfExists(tmp); + Files.copy(output, tmp); + + Files.deleteIfExists(output); + return tmp; + } + + private static Path tmpFile() throws IOException { + return Files.createTempFile(null, null); + } + + private static Path tmpDir() throws IOException { + return Files.createTempDirectory(null); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java new file mode 100644 index 00000000..cfaa34f7 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java @@ -0,0 +1,222 @@ +/* + * 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.util.srg; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import dev.architectury.mappingslayers.api.mutable.MutableClassDef; +import dev.architectury.mappingslayers.api.mutable.MutableFieldDef; +import dev.architectury.mappingslayers.api.mutable.MutableMethodDef; +import dev.architectury.mappingslayers.api.mutable.MutableTinyTree; +import dev.architectury.mappingslayers.api.utils.MappingsUtils; +import org.apache.commons.io.IOUtils; +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgReader; +import org.cadixdev.lorenz.model.ClassMapping; +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.jetbrains.annotations.Nullable; + +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mapping.tree.ClassDef; +import net.fabricmc.mapping.tree.FieldDef; +import net.fabricmc.mapping.tree.MethodDef; +import net.fabricmc.mapping.tree.TinyMappingFactory; +import net.fabricmc.mapping.tree.TinyTree; +import net.fabricmc.stitch.commands.tinyv2.TinyClass; +import net.fabricmc.stitch.commands.tinyv2.TinyField; +import net.fabricmc.stitch.commands.tinyv2.TinyFile; +import net.fabricmc.stitch.commands.tinyv2.TinyHeader; +import net.fabricmc.stitch.commands.tinyv2.TinyMethod; +import net.fabricmc.stitch.commands.tinyv2.TinyV2Writer; + +/** + * Utilities for merging SRG mappings. + * + * @author Juuz + */ +public final class SrgMerger { + /** + * Merges SRG mappings with a tiny mappings tree through the obf names. + * + * @param srg the SRG file in .tsrg format + * @param tiny the tiny file + * @param out the output file, will be in tiny v2 + * @param lenient whether to ignore missing tiny mapping + * @throws IOException if an IO error occurs while reading or writing the mappings + * @throws MappingException if the input tiny tree's default namespace is not 'official' + * or if an element mentioned in the SRG file does not have tiny mappings + */ + public static void mergeSrg(Path srg, Path tiny, Path out, boolean lenient) throws IOException, MappingException { + MappingSet arr = readSrg(srg); + TinyTree foss; + + try (BufferedReader reader = Files.newBufferedReader(tiny)) { + foss = TinyMappingFactory.loadWithDetection(reader); + } + + List<String> namespaces = new ArrayList<>(foss.getMetadata().getNamespaces()); + namespaces.add(1, "srg"); + + if (!"official".equals(namespaces.get(0))) { + throw new MappingException("Mapping file " + tiny + " does not have the 'official' namespace as the default!"); + } + + TinyHeader header = new TinyHeader(namespaces, 2, 0, Collections.emptyMap()); + + List<TinyClass> classes = new ArrayList<>(); + + for (TopLevelClassMapping klass : arr.getTopLevelClassMappings()) { + classToTiny(foss, namespaces, klass, classes::add, lenient); + } + + TinyFile file = new TinyFile(header, classes); + TinyV2Writer.write(file, out); + } + + private static MappingSet readSrg(Path srg) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(srg)) { + String content = IOUtils.toString(reader); + + if (content.startsWith("tsrg2")) { + return readTsrg2(content); + } else { + try (TSrgReader srgReader = new TSrgReader(new StringReader(content))) { + return srgReader.read(); + } + } + } + } + + private static MappingSet readTsrg2(String content) { + MappingSet set = MappingSet.create(); + MutableTinyTree tree = MappingsUtils.deserializeFromTsrg2(content); + int obfIndex = tree.getMetadata().index("obf"); + int srgIndex = tree.getMetadata().index("srg"); + + for (MutableClassDef classDef : tree.getClassesMutable()) { + ClassMapping<?, ?> classMapping = set.getOrCreateClassMapping(classDef.getName(obfIndex)); + classMapping.setDeobfuscatedName(classDef.getName(srgIndex)); + + for (MutableFieldDef fieldDef : classDef.getFieldsMutable()) { + FieldMapping fieldMapping = classMapping.getOrCreateFieldMapping(fieldDef.getName(obfIndex)); + fieldMapping.setDeobfuscatedName(fieldDef.getName(srgIndex)); + } + + for (MutableMethodDef methodDef : classDef.getMethodsMutable()) { + MethodMapping methodMapping = classMapping.getOrCreateMethodMapping(methodDef.getName(obfIndex), methodDef.getDescriptor(obfIndex)); + methodMapping.setDeobfuscatedName(methodDef.getName(srgIndex)); + } + } + + return set; + } + + private static void classToTiny(TinyTree foss, List<String> namespaces, ClassMapping<?, ?> klass, Consumer<TinyClass> classConsumer, boolean lenient) { + String obf = klass.getFullObfuscatedName(); + String srg = klass.getFullDeobfuscatedName(); + ClassDef classDef = foss.getDefaultNamespaceClassMap().get(obf); + + if (classDef == null) { + if (lenient) { + return; + } else { + throw new MappingException("Missing class: " + obf + " (srg: " + srg + ")"); + } + } + + List<String> classNames = CollectionUtil.map( + namespaces, + namespace -> "srg".equals(namespace) ? srg : classDef.getName(namespace) + ); + + List<TinyMethod> methods = new ArrayList<>(); + List<TinyField> fields = new ArrayList<>(); + + for (MethodMapping method : klass.getMethodMappings()) { + MethodDef def = CollectionUtil.find( + classDef.getMethods(), + m -> m.getName("official").equals(method.getObfuscatedName()) && m.getDescriptor("official").equals(method.getObfuscatedDescriptor()) + ).orElse(nullOrThrow(lenient, () -> new MappingException("Missing method: " + method.getFullObfuscatedName() + " (srg: " + method.getFullDeobfuscatedName() + ")"))); + + if (def == null) continue; + + List<String> methodNames = CollectionUtil.map( + namespaces, + namespace -> "srg".equals(namespace) ? method.getDeobfuscatedName() : def.getName(namespace) + ); + + methods.add(new TinyMethod( + def.getDescriptor("official"), methodNames, + /* parameters */ Collections.emptyList(), + /* locals */ Collections.emptyList(), + /* comments */ Collections.emptyList() + )); + } + + for (FieldMapping field : klass.getFieldMappings()) { + FieldDef def = CollectionUtil.find( + classDef.getFields(), + f -> f.getName("official").equals(field.getObfuscatedName()) + ).orElse(nullOrThrow(lenient, () -> new MappingException("Missing field: " + field.getFullObfuscatedName() + " (srg: " + field.getFullDeobfuscatedName() + ")"))); + + if (def == null) continue; + + List<String> fieldNames = CollectionUtil.map( + namespaces, + namespace -> "srg".equals(namespace) ? field.getDeobfuscatedName() : def.getName(namespace) + ); + + fields.add(new TinyField(def.getDescriptor("official"), fieldNames, Collections.emptyList())); + } + + TinyClass tinyClass = new TinyClass(classNames, methods, fields, Collections.emptyList()); + classConsumer.accept(tinyClass); + + for (InnerClassMapping innerKlass : klass.getInnerClassMappings()) { + classToTiny(foss, namespaces, innerKlass, classConsumer, lenient); + } + } + + @Nullable + private static <T, X extends Exception> T nullOrThrow(boolean lenient, Supplier<X> exception) throws X { + if (lenient) { + return null; + } else { + throw exception.get(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java new file mode 100644 index 00000000..cdace98a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java @@ -0,0 +1,47 @@ +/* + * 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.util.srg; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.cadixdev.lorenz.io.srg.SrgWriter; +import org.gradle.api.logging.Logger; + +import net.fabricmc.lorenztiny.TinyMappingsReader; +import net.fabricmc.mapping.tree.TinyTree; + +public class SrgNamedWriter { + public static void writeTo(Logger logger, Path srgFile, TinyTree mappings, String from, String to) throws IOException { + Files.deleteIfExists(srgFile); + + try (SrgWriter writer = new SrgWriter(Files.newBufferedWriter(srgFile))) { + try (TinyMappingsReader reader = new TinyMappingsReader(mappings, from, to)) { + writer.write(reader.read()); + } + } + } +} diff --git a/src/main/resources/idea_run_config_template.xml b/src/main/resources/idea_run_config_template.xml index 0891bc84..6b71bd71 100644 --- a/src/main/resources/idea_run_config_template.xml +++ b/src/main/resources/idea_run_config_template.xml @@ -1,5 +1,6 @@ <component name="ProjectRunConfigurationManager"> <configuration default="false" name="%NAME%" type="Application" factoryName="Application"> + %ENVS% <option name="MAIN_CLASS_NAME" value="%MAIN_CLASS%" /> <module name="%IDEA_MODULE%" /> <option name="PROGRAM_PARAMETERS" value="%PROGRAM_ARGS%" /> diff --git a/src/main/resources/log4j2.fabric.xml b/src/main/resources/log4j2.fabric.xml index fcfd27b5..1feeddd6 100644 --- a/src/main/resources/log4j2.fabric.xml +++ b/src/main/resources/log4j2.fabric.xml @@ -52,6 +52,7 @@ </Appenders> <Loggers> <Logger level="${sys:fabric.log.level:-info}" name="net.minecraft"/> + <Logger level="warn" name="cpw.mods.modlauncher.ClassTransformer"/> <Root level="all"> <AppenderRef ref="DebugFile" level="${sys:fabric.log.debug.level:-debug}"/> <AppenderRef ref="SysOut" level="${sys:fabric.log.level:-info}"/> |