/* * This file is part of fabric-loom, licensed under the MIT License (MIT). * * Copyright (c) 2016-2021 FabricMC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package net.fabricmc.loom.task; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; import javax.inject.Inject; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.TaskAction; import org.gradle.workers.WorkAction; import org.gradle.workers.WorkParameters; import org.gradle.workers.WorkQueue; import org.gradle.workers.WorkerExecutor; import org.gradle.workers.internal.WorkerDaemonClientsManager; import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.configuration.accesswidener.AccessWidenerFile; import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerMappingsProcessor; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.decompilers.LineNumberRemapper; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.FileSystemUtil; import net.fabricmc.loom.util.IOStringConsumer; import net.fabricmc.loom.util.OperatingSystem; import net.fabricmc.loom.util.gradle.ThreadedProgressLoggerConsumer; import net.fabricmc.loom.util.gradle.ThreadedSimpleProgressLogger; import net.fabricmc.loom.util.gradle.WorkerDaemonClientsManagerHelper; import net.fabricmc.loom.util.ipc.IPCClient; import net.fabricmc.loom.util.ipc.IPCServer; public abstract class GenerateSourcesTask extends AbstractLoomTask { public final LoomDecompiler decompiler; @InputFile public abstract RegularFileProperty getInputJar(); /** * Max memory for forked JVM in megabytes. */ @Input public abstract Property getMaxMemory(); @Input public abstract MapProperty getOptions(); @Inject public abstract WorkerExecutor getWorkerExecutor(); @Inject public abstract WorkerDaemonClientsManager getWorkerDaemonClientsManager(); @Inject public GenerateSourcesTask(LoomDecompiler decompiler) { this.decompiler = decompiler; Objects.requireNonNull(getDecompilerConstructor(this.decompiler.getClass().getCanonicalName()), "%s must have a no args constructor".formatted(this.decompiler.getClass().getCanonicalName())); getOutputs().upToDateWhen((o) -> false); getMaxMemory().convention(4096L).finalizeValueOnRead(); getOptions().finalizeValueOnRead(); } @TaskAction public void run() throws IOException { if (!OperatingSystem.is64Bit()) { throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements."); } if (!OperatingSystem.isUnixDomainSocketsSupported()) { getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system."); doWork(null); return; } // Set up the IPC path to get the log output back from the forked JVM final Path ipcPath = Files.createTempFile("loom", "ipc"); Files.deleteIfExists(ipcPath); try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompiler.name(), "Decompiling minecraft sources"); IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) { doWork(ipcPath); } catch (InterruptedException e) { throw new RuntimeException("Failed to shutdown log receiver", e); } finally { Files.deleteIfExists(ipcPath); } } private void doWork(@Nullable Path ipcPath) { final String jvmMarkerValue = UUID.randomUUID().toString(); final WorkQueue workQueue = createWorkQueue(jvmMarkerValue); workQueue.submit(DecompileAction.class, params -> { params.getDecompilerClass().set(decompiler.getClass().getCanonicalName()); params.getOptions().set(getOptions()); params.getInputJar().set(getInputJar()); params.getRuntimeJar().set(getExtension().getMappingsProvider().mappedProvider.getMappedJar()); params.getSourcesDestinationJar().set(getMappedJarFileWithSuffix("-sources.jar")); params.getLinemap().set(getMappedJarFileWithSuffix("-sources.lmap")); params.getLinemapJar().set(getMappedJarFileWithSuffix("-linemapped.jar")); params.getMappings().set(getMappings().toFile()); if (ipcPath != null) { params.getIPCPath().set(ipcPath.toFile()); } params.getClassPath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES)); }); try { workQueue.await(); } finally { if (useProcessIsolation()) { boolean stopped = WorkerDaemonClientsManagerHelper.stopIdleJVM(getWorkerDaemonClientsManager(), jvmMarkerValue); if (!stopped) { throw new RuntimeException("Failed to stop decompile worker JVM"); } } } } private WorkQueue createWorkQueue(String jvmMarkerValue) { if (!useProcessIsolation()) { return getWorkerExecutor().noIsolation(); } return getWorkerExecutor().processIsolation(spec -> { spec.forkOptions(forkOptions -> { forkOptions.setMaxHeapSize("%dm".formatted(getMaxMemory().get())); forkOptions.systemProperty(WorkerDaemonClientsManagerHelper.MARKER_PROP, jvmMarkerValue); }); }); } private boolean useProcessIsolation() { // Useful if you want to debug the decompiler, make sure you run gradle with enough memory. return !Boolean.getBoolean("fabric.loom.genSources.debug"); } public interface DecompileParams extends WorkParameters { Property getDecompilerClass(); MapProperty getOptions(); RegularFileProperty getInputJar(); RegularFileProperty getRuntimeJar(); RegularFileProperty getSourcesDestinationJar(); RegularFileProperty getLinemap(); RegularFileProperty getLinemapJar(); RegularFileProperty getMappings(); RegularFileProperty getIPCPath(); ConfigurableFileCollection getClassPath(); } public abstract static class DecompileAction implements WorkAction { @Override public void execute() { if (!getParameters().getIPCPath().isPresent() || !OperatingSystem.isUnixDomainSocketsSupported()) { // Does not support unix domain sockets, print to sout. doDecompile(System.out::println); return; } final Path ipcPath = getParameters().getIPCPath().get().getAsFile().toPath(); try (IPCClient ipcClient = new IPCClient(ipcPath)) { doDecompile(new ThreadedSimpleProgressLogger(ipcClient)); } catch (Exception e) { throw new RuntimeException("Failed to decompile", e); } } private void doDecompile(IOStringConsumer logger) { final Path inputJar = getParameters().getInputJar().get().getAsFile().toPath(); final Path sourcesDestinationJar = getParameters().getSourcesDestinationJar().get().getAsFile().toPath(); final Path linemap = getParameters().getLinemap().get().getAsFile().toPath(); final Path linemapJar = getParameters().getLinemapJar().get().getAsFile().toPath(); final Path runtimeJar = getParameters().getRuntimeJar().get().getAsFile().toPath(); final LoomDecompiler decompiler; try { decompiler = getDecompilerConstructor(getParameters().getDecompilerClass().get()).newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("Failed to create decompiler", e); } DecompilationMetadata metadata = new DecompilationMetadata( Runtime.getRuntime().availableProcessors(), getParameters().getMappings().get().getAsFile().toPath(), getLibraries(), logger, getParameters().getOptions().get() ); decompiler.decompile( inputJar, sourcesDestinationJar, linemap, metadata ); // Close the decompile loggers try { metadata.logger().accept(ThreadedProgressLoggerConsumer.CLOSE_LOGGERS); } catch (IOException e) { throw new UncheckedIOException("Failed to close loggers", e); } if (Files.exists(linemap)) { try { // Line map the actually jar used to run the game, not the one used to decompile remapLineNumbers(metadata.logger(), runtimeJar, linemap, linemapJar); Files.copy(linemapJar, runtimeJar, StandardCopyOption.REPLACE_EXISTING); Files.delete(linemapJar); } catch (IOException e) { throw new UncheckedIOException("Failed to remap line numbers", e); } } } private void remapLineNumbers(IOStringConsumer logger, Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException { LineNumberRemapper remapper = new LineNumberRemapper(); remapper.readMappings(linemap.toFile()); try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(oldCompiledJar.toFile(), true); FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) { remapper.process(logger, inFs.get().getPath("/"), outFs.get().getPath("/")); } } private Collection getLibraries() { return getParameters().getClassPath().getFiles().stream().map(File::toPath).collect(Collectors.toSet()); } } private File getMappedJarFileWithSuffix(String suffix) { LoomGradleExtension extension = LoomGradleExtension.get(getProject()); MappingsProviderImpl mappingsProvider = extension.getMappingsProvider(); File mappedJar = mappingsProvider.mappedProvider.getMappedJar(); String path = mappedJar.getAbsolutePath(); if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) { throw new RuntimeException("Invalid mapped JAR path: " + path); } return new File(path.substring(0, path.length() - 4) + suffix); } private Path getMappings() { Path baseMappings = getExtension().getMappingsProvider().tinyMappings; if (getExtension().getEnableTransitiveAccessWideners().get()) { List accessWideners = getExtension().getTransitiveAccessWideners(); if (accessWideners.isEmpty()) { return baseMappings; } Path outputMappings; try { outputMappings = Files.createTempFile("loom-transitive-mappings", ".tiny"); } catch (IOException e) { throw new RuntimeException("Failed to create temp file", e); } TransitiveAccessWidenerMappingsProcessor.process(baseMappings, outputMappings, accessWideners, getProject().getLogger()); return outputMappings; } return baseMappings; } private static Constructor getDecompilerConstructor(String clazz) { try { //noinspection unchecked return (Constructor) Class.forName(clazz).getConstructor(); } catch (NoSuchMethodException | ClassNotFoundException e) { return null; } } }