Merge remote-tracking branch 'FabricMC/exp/1.6' into exp/1.6

# Conflicts:
#	gradle/libs.versions.toml
#	src/main/java/net/fabricmc/loom/LoomGradleExtension.java
#	src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java
#	src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java
#	src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java
#	src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java
#	src/main/java/net/fabricmc/loom/extension/LoomFiles.java
#	src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java
#	src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java
#	src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java
#	src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java
#	src/main/java/net/fabricmc/loom/task/RemapJarTask.java
#	src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java
This commit is contained in:
shedaniel
2024-03-23 01:06:56 +09:00
91 changed files with 3636 additions and 924 deletions

View File

@@ -143,12 +143,13 @@ dependencies {
implementation libs.fabric.tiny.remapper implementation libs.fabric.tiny.remapper
implementation libs.fabric.access.widener implementation libs.fabric.access.widener
implementation libs.fabric.mapping.io implementation libs.fabric.mapping.io
implementation (libs.fabric.lorenz.tiny) { implementation (libs.fabric.lorenz.tiny) {
transitive = false transitive = false
} }
implementation "dev.architectury:refmap-remapper:1.0.5" implementation "dev.architectury:refmap-remapper:1.0.5"
implementation libs.fabric.loom.nativelib
// decompilers // decompilers
fernflowerCompileOnly runtimeLibs.fernflower fernflowerCompileOnly runtimeLibs.fernflower
fernflowerCompileOnly libs.fabric.mapping.io fernflowerCompileOnly libs.fabric.mapping.io

View File

@@ -12,6 +12,7 @@ mapping-io = "0.5.1"
lorenz-tiny = "4.0.2" lorenz-tiny = "4.0.2"
mercury = "0.1.4.17" mercury = "0.1.4.17"
kotlinx-metadata = "0.9.0" kotlinx-metadata = "0.9.0"
loom-native = "0.1.0"
# Plugins # Plugins
spotless = "6.25.0" spotless = "6.25.0"
@@ -48,6 +49,7 @@ fabric-access-widener = { module = "net.fabricmc:access-widener", version.ref =
fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" } fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" }
fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" } fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" }
fabric-mercury = { module = "dev.architectury:mercury", version.ref = "mercury" } fabric-mercury = { module = "dev.architectury:mercury", version.ref = "mercury" }
fabric-loom-nativelib = { module = "net.fabricmc:fabric-loom-native", version.ref = "loom-native" }
# Misc # Misc
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

View File

@@ -108,7 +108,7 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
return switch (mappingsNamespace) { return switch (mappingsNamespace) {
case NAMED -> getNamedMinecraftProvider().getMinecraftJarPaths(); case NAMED -> getNamedMinecraftProvider().getMinecraftJarPaths();
case INTERMEDIARY -> getIntermediaryMinecraftProvider().getMinecraftJarPaths(); case INTERMEDIARY -> getIntermediaryMinecraftProvider().getMinecraftJarPaths();
case OFFICIAL -> getMinecraftProvider().getMinecraftJars(); case OFFICIAL, CLIENT_OFFICIAL, SERVER_OFFICIAL -> getMinecraftProvider().getMinecraftJars();
case SRG -> { case SRG -> {
ModPlatform.assertPlatform(this, ModPlatform.FORGE, () -> "SRG jars are only available on Forge."); ModPlatform.assertPlatform(this, ModPlatform.FORGE, () -> "SRG jars are only available on Forge.");
yield getSrgMinecraftProvider().getMinecraftJarPaths(); yield getSrgMinecraftProvider().getMinecraftJarPaths();

View File

@@ -33,6 +33,7 @@ import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.NamedDomainObjectList; import org.gradle.api.NamedDomainObjectList;
import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty; import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
@@ -206,7 +207,8 @@ public interface LoomGradleExtensionAPI {
*/ */
Property<String> getIntermediaryUrl(); Property<String> getIntermediaryUrl();
Property<MinecraftJarConfiguration> getMinecraftJarConfiguration(); @ApiStatus.Experimental
Property<MinecraftJarConfiguration<?, ?, ?>> getMinecraftJarConfiguration();
default void serverOnlyMinecraftJar() { default void serverOnlyMinecraftJar() {
getMinecraftJarConfiguration().set(MinecraftJarConfiguration.SERVER_ONLY); getMinecraftJarConfiguration().set(MinecraftJarConfiguration.SERVER_ONLY);
@@ -235,6 +237,11 @@ public interface LoomGradleExtensionAPI {
*/ */
Provider<String> getMinecraftVersion(); Provider<String> getMinecraftVersion();
/**
* @return A lazily evaluated {@link FileCollection} containing the named minecraft jars.
*/
FileCollection getNamedMinecraftJars();
// =================== // ===================
// Architectury Loom // Architectury Loom
// =================== // ===================

View File

@@ -44,6 +44,14 @@ public abstract class IntermediateMappingsProvider implements Named {
public abstract Property<Function<String, DownloadBuilder>> getDownloader(); public abstract Property<Function<String, DownloadBuilder>> getDownloader();
/**
* Set to true if the minecraft version is pre 1.3.
* When true the expected src namespace is intermediary, and the expected dst namespaces are clientOfficial and/or serverOfficial
* When false the expected src namespace is named and the expected dst namespace is intermediary
*/
@ApiStatus.Experimental
public abstract Property<Boolean> getIsLegacyMinecraft();
/** /**
* Generate or download a tinyv2 mapping file with intermediary and named namespaces. * Generate or download a tinyv2 mapping file with intermediary and named namespaces.
* @throws IOException * @throws IOException

View File

@@ -37,6 +37,18 @@ public enum MappingsNamespace {
*/ */
OFFICIAL, OFFICIAL,
/**
* Official names for the Minecraft client jar, usually obfuscated.
* This namespace is used for versions <1.3, where the client and server jars are obfuscated differently.
*/
CLIENT_OFFICIAL,
/**
* Official names for the Minecraft server jar, usually obfuscated.
* This namespace is used for versions <1.3, where the client and server jars are obfuscated differently.
*/
SERVER_OFFICIAL,
/** /**
* Intermediary mappings have been generated to provide a stable set of names across minecraft versions. * Intermediary mappings have been generated to provide a stable set of names across minecraft versions.
* *
@@ -76,6 +88,8 @@ public enum MappingsNamespace {
public static @Nullable MappingsNamespace of(String namespace) { public static @Nullable MappingsNamespace of(String namespace) {
return switch (namespace) { return switch (namespace) {
case "official" -> OFFICIAL; case "official" -> OFFICIAL;
case "clientOfficial" -> CLIENT_OFFICIAL;
case "serverOfficial" -> SERVER_OFFICIAL;
case "intermediary" -> INTERMEDIARY; case "intermediary" -> INTERMEDIARY;
case "srg" -> SRG; case "srg" -> SRG;
case "mojang" -> MOJANG; case "mojang" -> MOJANG;
@@ -86,6 +100,10 @@ public enum MappingsNamespace {
@Override @Override
public String toString() { public String toString() {
return name().toLowerCase(Locale.ROOT); return switch (this) {
case CLIENT_OFFICIAL -> "clientOfficial";
case SERVER_OFFICIAL -> "serverOfficial";
default -> name().toLowerCase(Locale.ROOT);
};
} }
} }

View File

@@ -36,6 +36,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
@@ -55,6 +57,8 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskDependency; import org.gradle.api.tasks.TaskDependency;
import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.LoomGradlePlugin;
@@ -65,6 +69,9 @@ import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
public final class IncludedJarFactory { public final class IncludedJarFactory {
private final Project project; private final Project project;
private static final Logger LOGGER = LoggerFactory.getLogger(IncludedJarFactory.class);
private static final String SEMVER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$";
private static final Pattern SEMVER_PATTERN = Pattern.compile(SEMVER_REGEX);
public IncludedJarFactory(Project project) { public IncludedJarFactory(Project project) {
this.project = project; this.project = project;
@@ -217,7 +224,8 @@ public final class IncludedJarFactory {
jsonObject.addProperty("schemaVersion", 1); jsonObject.addProperty("schemaVersion", 1);
jsonObject.addProperty("id", modId); jsonObject.addProperty("id", modId);
jsonObject.addProperty("version", metadata.version()); String version = getVersion(metadata);
jsonObject.addProperty("version", version);
jsonObject.addProperty("name", metadata.name()); jsonObject.addProperty("name", metadata.name());
JsonObject custom = new JsonObject(); JsonObject custom = new JsonObject();
@@ -236,6 +244,35 @@ public final class IncludedJarFactory {
return "_" + classifier; return "_" + classifier;
} }
} }
@Override
public String toString() {
return group + ":" + name + ":" + version + classifier();
}
}
private static String getVersion(Metadata metadata) {
String version = metadata.version();
if (validSemVer(version)) {
return version;
}
if (version.endsWith(".Final") || version.endsWith(".final")) {
String trimmedVersion = version.substring(0, version.length() - 6);
if (validSemVer(trimmedVersion)) {
return trimmedVersion;
}
}
LOGGER.warn("({}) is not valid semver for dependency {}", version, metadata);
return version;
}
private static boolean validSemVer(String version) {
Matcher matcher = SEMVER_PATTERN.matcher(version);
return matcher.find();
} }
public record NestedFile(Metadata metadata, File file) implements Serializable { } public record NestedFile(Metadata metadata, File file) implements Serializable { }

View File

@@ -32,8 +32,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -42,7 +40,6 @@ import javax.inject.Inject;
import org.gradle.api.GradleException; import org.gradle.api.GradleException;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging; import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPlugin;
@@ -78,6 +75,7 @@ import net.fabricmc.loom.configuration.providers.forge.minecraft.ForgeMinecraftP
import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider;
@@ -88,7 +86,9 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftPr
import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper;
import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.extension.MixinExtension;
import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.ProcessUtil;
import net.fabricmc.loom.util.gradle.GradleUtils; import net.fabricmc.loom.util.gradle.GradleUtils;
import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.gradle.SourceSetHelper;
import net.fabricmc.loom.util.service.ScopedSharedServiceManager; import net.fabricmc.loom.util.service.ScopedSharedServiceManager;
@@ -127,6 +127,7 @@ public abstract class CompileConfiguration implements Runnable {
try { try {
setupMinecraft(configContext); setupMinecraft(configContext);
} catch (Exception e) { } catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e); throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e);
} }
@@ -196,15 +197,21 @@ public abstract class CompileConfiguration implements Runnable {
private synchronized void setupMinecraft(ConfigContext configContext) throws Exception { private synchronized void setupMinecraft(ConfigContext configContext) throws Exception {
final Project project = configContext.project(); final Project project = configContext.project();
final LoomGradleExtension extension = configContext.extension(); final LoomGradleExtension extension = configContext.extension();
final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get();
// Provide the vanilla mc jars -- TODO share across getProject()s. final MinecraftMetadataProvider metadataProvider = MinecraftMetadataProvider.create(configContext);
final MinecraftProvider minecraftProvider = jarConfiguration.getMinecraftProviderFunction().apply(configContext);
var jarConfiguration = extension.getMinecraftJarConfiguration().get();
if (jarConfiguration == MinecraftJarConfiguration.MERGED && !metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) {
jarConfiguration = MinecraftJarConfiguration.LEGACY_MERGED;
}
// Provide the vanilla mc jars
final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(metadataProvider, configContext);
if (extension.isForgeLike() && !(minecraftProvider instanceof ForgeMinecraftProvider)) { if (extension.isForgeLike() && !(minecraftProvider instanceof ForgeMinecraftProvider)) {
throw new UnsupportedOperationException("Using Forge with split jars is not supported!"); throw new UnsupportedOperationException("Using Forge with split jars is not supported!");
} }
extension.setMinecraftProvider(minecraftProvider); extension.setMinecraftProvider(minecraftProvider);
minecraftProvider.provide(); minecraftProvider.provide();
@@ -236,15 +243,15 @@ public abstract class CompileConfiguration implements Runnable {
} }
// Provide the remapped mc jars // Provide the remapped mc jars
final IntermediaryMinecraftProvider<?> intermediaryMinecraftProvider = jarConfiguration.getIntermediaryMinecraftProviderBiFunction().apply(project, minecraftProvider); final IntermediaryMinecraftProvider<?> intermediaryMinecraftProvider = jarConfiguration.createIntermediaryMinecraftProvider(project);
NamedMinecraftProvider<?> namedMinecraftProvider = jarConfiguration.getNamedMinecraftProviderBiFunction().apply(project, minecraftProvider); NamedMinecraftProvider<?> namedMinecraftProvider = jarConfiguration.createNamedMinecraftProvider(project);
registerGameProcessors(configContext); registerGameProcessors(configContext);
MinecraftJarProcessorManager minecraftJarProcessorManager = MinecraftJarProcessorManager.create(getProject()); MinecraftJarProcessorManager minecraftJarProcessorManager = MinecraftJarProcessorManager.create(getProject());
if (minecraftJarProcessorManager != null) { if (minecraftJarProcessorManager != null) {
// Wrap the named MC provider for one that will provide the processed jars // Wrap the named MC provider for one that will provide the processed jars
namedMinecraftProvider = jarConfiguration.getProcessedNamedMinecraftProviderBiFunction().apply(namedMinecraftProvider, minecraftJarProcessorManager); namedMinecraftProvider = jarConfiguration.createProcessedNamedMinecraftProvider(namedMinecraftProvider, minecraftJarProcessorManager);
} }
final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(true, extension.refreshDeps(), configContext); final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(true, extension.refreshDeps(), configContext);
@@ -327,8 +334,7 @@ public abstract class CompileConfiguration implements Runnable {
final LoomGradleExtension extension = configContext.extension(); final LoomGradleExtension extension = configContext.extension();
extension.getMinecraftJarConfiguration().get() extension.getMinecraftJarConfiguration().get()
.getDecompileConfigurationBiFunction() .createDecompileConfiguration(getProject())
.apply(configContext.project(), extension.getNamedMinecraftProvider())
.afterEvaluation(); .afterEvaluation();
} }
@@ -403,7 +409,8 @@ public abstract class CompileConfiguration implements Runnable {
Files.deleteIfExists(lockFile.file); Files.deleteIfExists(lockFile.file);
abrupt = true; abrupt = true;
} else { } else {
logger.lifecycle(printWithParents(handle.get())); ProcessUtil processUtil = ProcessUtil.create(getProject());
logger.lifecycle(processUtil.printWithParents(handle.get()));
logger.lifecycle("Waiting for lock to be released..."); logger.lifecycle("Waiting for lock to be released...");
long sleptMs = 0; long sleptMs = 0;
@@ -441,69 +448,6 @@ public abstract class CompileConfiguration implements Runnable {
return abrupt ? LockResult.ACQUIRED_PREVIOUS_OWNER_MISSING : LockResult.ACQUIRED_CLEAN; return abrupt ? LockResult.ACQUIRED_PREVIOUS_OWNER_MISSING : LockResult.ACQUIRED_CLEAN;
} }
private String printWithParents(ProcessHandle processHandle) {
var output = new StringBuilder();
List<ProcessHandle> chain = getParentChain(null, processHandle);
for (int i = 0; i < chain.size(); i++) {
ProcessHandle handle = chain.get(i);
output.append("\t".repeat(i));
if (i != 0) {
output.append("└─ ");
}
output.append(getInfoString(handle));
if (i < chain.size() - 1) {
output.append('\n');
}
}
return output.toString();
}
private String getInfoString(ProcessHandle handle) {
return "(%s) pid %s '%s%s'%s".formatted(
handle.info().user().orElse("unknown user"),
handle.pid(),
handle.info().command().orElse("unknown command"),
handle.info().arguments().map(arr -> {
if (getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.INFO
&& getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.DEBUG) {
return " (run with --info or --debug to show arguments, may reveal sensitive info)";
}
String join = String.join(" ", arr);
if (join.isBlank()) {
return "";
}
return " " + join;
}).orElse(" (unknown arguments)"),
handle.info().startInstant().map(instant -> " started at " + instant).orElse("")
);
}
private List<ProcessHandle> getParentChain(List<ProcessHandle> collectTo, ProcessHandle processHandle) {
if (collectTo == null) {
collectTo = new ArrayList<>();
}
Optional<ProcessHandle> parent = processHandle.parent();
if (parent.isPresent()) {
getParentChain(collectTo, parent.get());
}
collectTo.add(processHandle);
return collectTo;
}
private void releaseLock() { private void releaseLock() {
final Path lock = getLockFile().file; final Path lock = getLockFile().file;

View File

@@ -181,8 +181,8 @@ public abstract class FabricApiExtension {
if (settings.getCreateRunConfiguration().get()) { if (settings.getCreateRunConfiguration().get()) {
extension.getRunConfigs().create("datagen", run -> { extension.getRunConfigs().create("datagen", run -> {
run.setConfigName("Data Generation");
run.inherit(extension.getRunConfigs().getByName("server")); run.inherit(extension.getRunConfigs().getByName("server"));
run.setConfigName("Data Generation");
run.property("fabric-api.datagen"); run.property("fabric-api.datagen");
run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath());

View File

@@ -61,7 +61,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration<Mapp
// Decompiler will be passed to the constructor of GenerateSourcesTask // Decompiler will be passed to the constructor of GenerateSourcesTask
project.getTasks().register(taskName, GenerateSourcesTask.class, options).configure(task -> { project.getTasks().register(taskName, GenerateSourcesTask.class, options).configure(task -> {
task.getInputJarName().set(minecraftJar.getName()); task.getInputJarName().set(minecraftJar.getName());
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", minecraftJar.getPath())); task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", minecraftJar.getPath()));
task.dependsOn(project.getTasks().named("validateAccessWidener")); task.dependsOn(project.getTasks().named("validateAccessWidener"));
task.setDescription("Decompile minecraft using %s.".formatted(decompilerName)); task.setDescription("Decompile minecraft using %s.".formatted(decompilerName));

View File

@@ -55,7 +55,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
final TaskProvider<Task> commonDecompileTask = createDecompileTasks("Common", task -> { final TaskProvider<Task> commonDecompileTask = createDecompileTasks("Common", task -> {
task.getInputJarName().set(commonJar.getName()); task.getInputJarName().set(commonJar.getName());
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", commonJar.getPath())); task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", commonJar.getPath()));
if (mappingConfiguration.hasUnpickDefinitions()) { if (mappingConfiguration.hasUnpickDefinitions()) {
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-common-unpicked.jar"); File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-common-unpicked.jar");
@@ -65,7 +65,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
final TaskProvider<Task> clientOnlyDecompileTask = createDecompileTasks("ClientOnly", task -> { final TaskProvider<Task> clientOnlyDecompileTask = createDecompileTasks("ClientOnly", task -> {
task.getInputJarName().set(clientOnlyJar.getName()); task.getInputJarName().set(clientOnlyJar.getName());
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath())); task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath()));
if (mappingConfiguration.hasUnpickDefinitions()) { if (mappingConfiguration.hasUnpickDefinitions()) {
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-clientonly-unpicked.jar"); File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-clientonly-unpicked.jar");

View File

@@ -44,6 +44,7 @@ import java.util.stream.Collectors;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedArtifact;
@@ -59,6 +60,7 @@ import net.fabricmc.loom.configuration.InstallerData;
import net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask; import net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask;
import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils;
import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.configuration.providers.BundleMetadata;
import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryContext;
import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.gradle.SourceSetReference; import net.fabricmc.loom.util.gradle.SourceSetReference;
@@ -138,6 +140,12 @@ public class RunConfig {
public static RunConfig runConfig(Project project, RunConfigSettings settings) { public static RunConfig runConfig(Project project, RunConfigSettings settings) {
settings.evaluateNow(); settings.evaluateNow();
LoomGradleExtension extension = LoomGradleExtension.get(project); LoomGradleExtension extension = LoomGradleExtension.get(project);
LibraryContext context = new LibraryContext(extension.getMinecraftProvider().getVersionInfo(), JavaVersion.current());
if (settings.getEnvironment().equals("client") && context.usesLWJGL3()) {
settings.startFirstThread();
}
String name = settings.getName(); String name = settings.getName();
String configName = settings.getConfigName(); String configName = settings.getConfigName();

View File

@@ -357,7 +357,6 @@ public class RunConfigSettings implements Named {
* Configure run config with the default client options. * Configure run config with the default client options.
*/ */
public void client() { public void client() {
startFirstThread();
environment("client"); environment("client");
defaultMainClass(Constants.Knot.KNOT_CLIENT); defaultMainClass(Constants.Knot.KNOT_CLIENT);

View File

@@ -116,8 +116,7 @@ record DownloadSourcesHook(Project project, Task task) {
private String getGenSourcesTaskName(MinecraftJar.Type jarType) { private String getGenSourcesTaskName(MinecraftJar.Type jarType) {
LoomGradleExtension extension = LoomGradleExtension.get(project); LoomGradleExtension extension = LoomGradleExtension.get(project);
return extension.getMinecraftJarConfiguration().get() return extension.getMinecraftJarConfiguration().get()
.getDecompileConfigurationBiFunction() .createDecompileConfiguration(project)
.apply(project, extension.getNamedMinecraftProvider())
.getTaskName(jarType); .getTaskName(jarType);
} }

View File

@@ -55,12 +55,14 @@ import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfig;
import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.configuration.ide.RunConfigSettings;
import net.fabricmc.loom.task.AbstractLoomTask; import net.fabricmc.loom.task.AbstractLoomTask;
import net.fabricmc.loom.util.Constants;
public abstract class IdeaSyncTask extends AbstractLoomTask { public abstract class IdeaSyncTask extends AbstractLoomTask {
@Inject @Inject
public IdeaSyncTask() { public IdeaSyncTask() {
// Always re-run this task. // Always re-run this task.
getOutputs().upToDateWhen(element -> false); getOutputs().upToDateWhen(element -> false);
setGroup(Constants.TaskGroup.IDE);
} }
@TaskAction @TaskAction

View File

@@ -50,10 +50,12 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree;
public final class IntermediateMappingsService implements SharedService { public final class IntermediateMappingsService implements SharedService {
private final Path intermediaryTiny; private final Path intermediaryTiny;
private final String expectedSrcNs;
private final Supplier<MemoryMappingTree> memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree); private final Supplier<MemoryMappingTree> memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree);
private IntermediateMappingsService(Path intermediaryTiny) { private IntermediateMappingsService(Path intermediaryTiny, String expectedSrcNs) {
this.intermediaryTiny = intermediaryTiny; this.intermediaryTiny = intermediaryTiny;
this.expectedSrcNs = expectedSrcNs;
} }
public static synchronized IntermediateMappingsService getInstance(SharedServiceManager sharedServiceManager, Project project, MinecraftProvider minecraftProvider) { public static synchronized IntermediateMappingsService getInstance(SharedServiceManager sharedServiceManager, Project project, MinecraftProvider minecraftProvider) {
@@ -84,7 +86,13 @@ public final class IntermediateMappingsService implements SharedService {
throw new UncheckedIOException("Failed to provide intermediate mappings", e); throw new UncheckedIOException("Failed to provide intermediate mappings", e);
} }
return new IntermediateMappingsService(intermediaryTiny); // When merging legacy versions there will be multiple named namespaces, so use intermediary as the common src ns
// Newer versions will use intermediary as the src ns
final String expectedSrcNs = minecraftProvider.isLegacyVersion()
? MappingsNamespace.INTERMEDIARY.toString() // <1.3
: MappingsNamespace.OFFICIAL.toString(); // >=1.3
return new IntermediateMappingsService(intermediaryTiny, expectedSrcNs);
} }
private MemoryMappingTree createMemoryMappingTree() { private MemoryMappingTree createMemoryMappingTree() {
@@ -100,6 +108,10 @@ public final class IntermediateMappingsService implements SharedService {
throw new UncheckedIOException("Failed to read intermediary mappings", e); throw new UncheckedIOException("Failed to read intermediary mappings", e);
} }
if (!expectedSrcNs.equals(tree.getSrcNamespace())) {
throw new RuntimeException("Invalid intermediate mappings: expected source namespace '" + expectedSrcNs + "' but found '" + tree.getSrcNamespace() + "\'");
}
return tree; return tree;
} }

View File

@@ -328,7 +328,7 @@ public class MappingConfiguration {
// These are unmerged v2 mappings // These are unmerged v2 mappings
IntermediateMappingsService intermediateMappingsService = IntermediateMappingsService.getInstance(serviceManager, project, minecraftProvider); IntermediateMappingsService intermediateMappingsService = IntermediateMappingsService.getInstance(serviceManager, project, minecraftProvider);
MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, intermediateMappingsService); MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, minecraftProvider, intermediateMappingsService);
} else { } else {
if (LoomGradleExtension.get(project).isForgeLike()) { if (LoomGradleExtension.get(project).isForgeLike()) {
// (2022-09-11) This is due to ordering issues. // (2022-09-11) This is due to ordering issues.

View File

@@ -37,11 +37,12 @@ import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider;
* A bit of a hack, creates an empty intermediary mapping file to be used for mc versions without any intermediate mappings. * A bit of a hack, creates an empty intermediary mapping file to be used for mc versions without any intermediate mappings.
*/ */
public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappingsProvider { public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappingsProvider {
private static final String HEADER = "tiny\t2\t0\tofficial\tintermediary"; private static final String HEADER_OFFICIAL_MERGED = "tiny\t2\t0\tofficial\tintermediary";
private static final String HEADER_OFFICIAL_LEGACY_MERGED = "tiny\t2\t0\tintermediary\tclientOfficial\tserverOfficial\t";
@Override @Override
public void provide(Path tinyMappings) throws IOException { public void provide(Path tinyMappings) throws IOException {
Files.writeString(tinyMappings, HEADER, StandardCharsets.UTF_8); Files.writeString(tinyMappings, getIsLegacyMinecraft().get() ? HEADER_OFFICIAL_LEGACY_MERGED : HEADER_OFFICIAL_MERGED, StandardCharsets.UTF_8);
} }
@Override @Override

View File

@@ -47,7 +47,7 @@ public record SignatureFixesLayerImpl(Path mappingsFile) implements MappingLayer
public Map<String, String> getSignatureFixes() { public Map<String, String> getSignatureFixes() {
try { try {
//noinspection unchecked //noinspection unchecked
return ZipUtils.unpackJackson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class); return ZipUtils.unpackJson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to extract signature fixes", e); throw new RuntimeException("Failed to extract signature fixes", e);
} }

View File

@@ -47,6 +47,6 @@ public record ParchmentMappingLayer(Path parchmentFile, boolean removePrefix) im
} }
private ParchmentTreeV1 getParchmentData() throws IOException { private ParchmentTreeV1 getParchmentData() throws IOException {
return ZipUtils.unpackJackson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class); return ZipUtils.unpackJson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class);
} }
} }

View File

@@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.IntermediateMappingsService; import net.fabricmc.loom.configuration.providers.mappings.IntermediateMappingsService;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.mappingio.adapter.MappingNsCompleter; import net.fabricmc.mappingio.adapter.MappingNsCompleter;
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
import net.fabricmc.mappingio.format.tiny.Tiny2FileReader; import net.fabricmc.mappingio.format.tiny.Tiny2FileReader;
@@ -49,10 +50,20 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree;
public final class MappingsMerger { public final class MappingsMerger {
private static final Logger LOGGER = LoggerFactory.getLogger(MappingsMerger.class); private static final Logger LOGGER = LoggerFactory.getLogger(MappingsMerger.class);
public static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { public static void mergeAndSaveMappings(Path from, Path out, MinecraftProvider minecraftProvider, IntermediateMappingsService intermediateMappingsService) throws IOException {
Stopwatch stopwatch = Stopwatch.createStarted(); Stopwatch stopwatch = Stopwatch.createStarted();
LOGGER.info(":merging mappings"); LOGGER.info(":merging mappings");
if (minecraftProvider.isLegacyVersion()) {
legacyMergeAndSaveMappings(from, out, intermediateMappingsService);
} else {
mergeAndSaveMappings(from, out, intermediateMappingsService);
}
LOGGER.info(":merged mappings in " + stopwatch.stop());
}
private static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException {
MemoryMappingTree intermediaryTree = new MemoryMappingTree(); MemoryMappingTree intermediaryTree = new MemoryMappingTree();
intermediateMappingsService.getMemoryMappingTree().accept(new MappingSourceNsSwitch(intermediaryTree, MappingsNamespace.INTERMEDIARY.toString())); intermediateMappingsService.getMemoryMappingTree().accept(new MappingSourceNsSwitch(intermediaryTree, MappingsNamespace.INTERMEDIARY.toString()));
@@ -70,8 +81,27 @@ public final class MappingsMerger {
try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) { try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) {
officialTree.accept(writer); officialTree.accept(writer);
} }
}
LOGGER.info(":merged mappings in " + stopwatch.stop()); private static void legacyMergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException {
MemoryMappingTree intermediaryTree = new MemoryMappingTree();
intermediateMappingsService.getMemoryMappingTree().accept(intermediaryTree);
try (BufferedReader reader = Files.newBufferedReader(from, StandardCharsets.UTF_8)) {
Tiny2FileReader.read(reader, intermediaryTree);
}
MemoryMappingTree officialTree = new MemoryMappingTree();
MappingNsCompleter nsCompleter = new MappingNsCompleter(officialTree, Map.of(MappingsNamespace.CLIENT_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString(), MappingsNamespace.SERVER_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString()));
intermediaryTree.accept(nsCompleter);
// versions this old strip inner class attributes
// from the obfuscated jars anyway
//inheritMappedNamesOfEnclosingClasses(officialTree);
try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) {
officialTree.accept(writer);
}
} }
/** /**

View File

@@ -0,0 +1,83 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.configuration.providers.minecraft;
import java.nio.file.Path;
import java.util.List;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext;
/**
* Minecraft versions prior to 1.3 obfuscate the server and client jars differently.
* The obfuscated jars must be provided separately, and can be merged after remapping.
*/
public final class LegacyMergedMinecraftProvider extends MinecraftProvider {
private final SingleJarMinecraftProvider.Server serverMinecraftProvider;
private final SingleJarMinecraftProvider.Client clientMinecraftProvider;
public LegacyMergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
super(metadataProvider, configContext);
serverMinecraftProvider = SingleJarMinecraftProvider.server(metadataProvider, configContext);
clientMinecraftProvider = SingleJarMinecraftProvider.client(metadataProvider, configContext);
if (!isLegacyVersion()) {
throw new RuntimeException("something has gone wrong - legacy-merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " allows merging the obfuscated jars - the merged jar configuration should have been selected!");
}
}
public SingleJarMinecraftProvider.Server getServerMinecraftProvider() {
return serverMinecraftProvider;
}
public SingleJarMinecraftProvider.Client getClientMinecraftProvider() {
return clientMinecraftProvider;
}
@Override
public void provide() throws Exception {
if (!serverMinecraftProvider.provideServer() || !clientMinecraftProvider.provideClient()) {
throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!");
}
serverMinecraftProvider.provide();
clientMinecraftProvider.provide();
}
@Override
public List<Path> getMinecraftJars() {
return List.of(
serverMinecraftProvider.getMinecraftEnvOnlyJar(),
clientMinecraftProvider.getMinecraftEnvOnlyJar()
);
}
@Override
@Deprecated
public MappingsNamespace getOfficialNamespace() {
// Legacy merged providers do not have a single namespace as they delegate to the single jar providers
throw new UnsupportedOperationException("Cannot query the official namespace for legacy-merged minecraft providers");
}
}

View File

@@ -31,13 +31,22 @@ import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.ConfigContext;
public class MergedMinecraftProvider extends MinecraftProvider { public class MergedMinecraftProvider extends MinecraftProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(MergedMinecraftProvider.class);
private Path minecraftMergedJar; private Path minecraftMergedJar;
public MergedMinecraftProvider(ConfigContext configContext) { public MergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
super(configContext); super(metadataProvider, configContext);
if (isLegacyVersion()) {
throw new RuntimeException("something has gone wrong - merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " does not allow merging the obfuscated jars - the legacy-merged jar configuration should have been selected!");
}
} }
@Override @Override
@@ -51,12 +60,17 @@ public class MergedMinecraftProvider extends MinecraftProvider {
return List.of(minecraftMergedJar); return List.of(minecraftMergedJar);
} }
@Override
public MappingsNamespace getOfficialNamespace() {
return MappingsNamespace.OFFICIAL;
}
@Override @Override
public void provide() throws Exception { public void provide() throws Exception {
super.provide(); super.provide();
if (!getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { if (!provideServer() || !provideClient()) {
throw new UnsupportedOperationException("Minecraft versions 1.2.5 and older cannot be merged. Please use `loom { server/clientOnlyMinecraftJar() }`"); throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!");
} }
if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) { if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) {
@@ -74,18 +88,24 @@ public class MergedMinecraftProvider extends MinecraftProvider {
} }
protected void mergeJars() throws IOException { protected void mergeJars() throws IOException {
getLogger().info(":merging jars"); File minecraftClientJar = getMinecraftClientJar();
File minecraftServerJar = getMinecraftServerJar();
File jarToMerge = getMinecraftServerJar();
if (getServerBundleMetadata() != null) { if (getServerBundleMetadata() != null) {
extractBundledServerJar(); extractBundledServerJar();
jarToMerge = getMinecraftExtractedServerJar(); minecraftServerJar = getMinecraftExtractedServerJar();
} }
Objects.requireNonNull(jarToMerge, "Cannot merge null input jar?"); mergeJars(minecraftClientJar, minecraftServerJar, minecraftMergedJar.toFile());
}
try (var jarMerger = new MinecraftJarMerger(getMinecraftClientJar(), jarToMerge, minecraftMergedJar.toFile())) { public static void mergeJars(File clientJar, File serverJar, File mergedJar) throws IOException {
LOGGER.info(":merging jars");
Objects.requireNonNull(clientJar, "Cannot merge null client jar?");
Objects.requireNonNull(serverJar, "Cannot merge null server jar?");
try (var jarMerger = new MinecraftJarMerger(clientJar, serverJar, mergedJar)) {
jarMerger.enableSyntheticParamsOffset(); jarMerger.enableSyntheticParamsOffset();
jarMerger.merge(); jarMerger.merge();
} }

View File

@@ -257,6 +257,8 @@ public class MinecraftClassMerger {
int j = 0; int j = 0;
while (i < first.size() || j < second.size()) { while (i < first.size() || j < second.size()) {
int saved = i + j;
while (i < first.size() && j < second.size() while (i < first.size() && j < second.size()
&& first.get(i).equals(second.get(j))) { && first.get(i).equals(second.get(j))) {
out.add(first.get(i)); out.add(first.get(i));
@@ -273,6 +275,20 @@ public class MinecraftClassMerger {
out.add(second.get(j)); out.add(second.get(j));
j++; j++;
} }
// if the order is scrambled, it's not possible to merge
// the lists while preserving the order from both sides
if (i + j == saved) {
for (; i < first.size(); i++) {
out.add(first.get(i));
}
for (; j < second.size(); j++) {
if (!first.contains(second.get(j))) {
out.add(second.get(j));
}
}
}
} }
return out; return out;

View File

@@ -25,11 +25,10 @@
package net.fabricmc.loom.configuration.providers.minecraft; package net.fabricmc.loom.configuration.providers.minecraft;
import java.util.List; import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.gradle.api.Project; import org.gradle.api.Project;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.decompile.DecompileConfiguration; import net.fabricmc.loom.configuration.decompile.DecompileConfiguration;
import net.fabricmc.loom.configuration.decompile.SingleJarDecompileConfiguration; import net.fabricmc.loom.configuration.decompile.SingleJarDecompileConfiguration;
@@ -43,96 +42,110 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraft
import net.fabricmc.loom.configuration.providers.minecraft.mapped.ProcessedNamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.ProcessedNamedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider;
public enum MinecraftJarConfiguration { public record MinecraftJarConfiguration<
MERGED( M extends MinecraftProvider,
ForgeMinecraftProvider::createMerged, N extends NamedMinecraftProvider<M>,
IntermediaryMinecraftProvider.MergedImpl::new, Q extends MappedMinecraftProvider>(
NamedMinecraftProvider.MergedImpl::new, MinecraftProviderFactory<M> minecraftProviderFactory,
SrgMinecraftProvider.MergedImpl::new, IntermediaryMinecraftProviderFactory<M> intermediaryMinecraftProviderFactory,
MojangMappedMinecraftProvider.MergedImpl::new, NamedMinecraftProviderFactory<M> namedMinecraftProviderFactory,
ProcessedNamedMinecraftProvider.MergedImpl::new, ProcessedNamedMinecraftProviderFactory<M, N> processedNamedMinecraftProviderFactory,
SingleJarDecompileConfiguration::new, DecompileConfigurationFactory<Q> decompileConfigurationFactory,
List.of("client", "server") List<String> supportedEnvironments) {
), public static final MinecraftJarConfiguration<
SERVER_ONLY( MergedMinecraftProvider,
ForgeMinecraftProvider::createServerOnly, NamedMinecraftProvider.MergedImpl,
IntermediaryMinecraftProvider.SingleJarImpl::server, MappedMinecraftProvider> MERGED = new MinecraftJarConfiguration<>(
NamedMinecraftProvider.SingleJarImpl::server, ForgeMinecraftProvider::createMerged,
SrgMinecraftProvider.SingleJarImpl::server, IntermediaryMinecraftProvider.MergedImpl::new,
MojangMappedMinecraftProvider.SingleJarImpl::server, NamedMinecraftProvider.MergedImpl::new,
ProcessedNamedMinecraftProvider.SingleJarImpl::server, SrgMinecraftProvider.MergedImpl::new,
SingleJarDecompileConfiguration::new, MojangMappedMinecraftProvider.MergedImpl::new,
List.of("server") ProcessedNamedMinecraftProvider.MergedImpl::new,
), SingleJarDecompileConfiguration::new,
CLIENT_ONLY( List.of("client", "server")
ForgeMinecraftProvider::createClientOnly, );
IntermediaryMinecraftProvider.SingleJarImpl::client, public static final MinecraftJarConfiguration<
NamedMinecraftProvider.SingleJarImpl::client, LegacyMergedMinecraftProvider,
SrgMinecraftProvider.SingleJarImpl::client, NamedMinecraftProvider.LegacyMergedImpl,
MojangMappedMinecraftProvider.SingleJarImpl::client, MappedMinecraftProvider> LEGACY_MERGED = new MinecraftJarConfiguration<>(
ProcessedNamedMinecraftProvider.SingleJarImpl::client, LegacyMergedMinecraftProvider::new,
SingleJarDecompileConfiguration::new, IntermediaryMinecraftProvider.LegacyMergedImpl::new,
List.of("client") NamedMinecraftProvider.LegacyMergedImpl::new,
), ProcessedNamedMinecraftProvider.LegacyMergedImpl::new,
SPLIT( SingleJarDecompileConfiguration::new,
SplitMinecraftProvider::new, List.of("client", "server")
IntermediaryMinecraftProvider.SplitImpl::new, );
NamedMinecraftProvider.SplitImpl::new, public static final MinecraftJarConfiguration<
SrgMinecraftProvider.SplitImpl::new, SingleJarMinecraftProvider,
MojangMappedMinecraftProvider.SplitImpl::new, NamedMinecraftProvider.SingleJarImpl,
ProcessedNamedMinecraftProvider.SplitImpl::new, MappedMinecraftProvider> SERVER_ONLY = new MinecraftJarConfiguration<>(
SplitDecompileConfiguration::new, ForgeMinecraftProvider::createServerOnly,
List.of("client", "server") IntermediaryMinecraftProvider.SingleJarImpl::server,
); NamedMinecraftProvider.SingleJarImpl::server,
SrgMinecraftProvider.SingleJarImpl::server,
MojangMappedMinecraftProvider.SingleJarImpl::server,
ProcessedNamedMinecraftProvider.SingleJarImpl::server,
SingleJarDecompileConfiguration::new,
List.of("server")
);
public static final MinecraftJarConfiguration<
SingleJarMinecraftProvider,
NamedMinecraftProvider.SingleJarImpl,
MappedMinecraftProvider> CLIENT_ONLY = new MinecraftJarConfiguration<>(
ForgeMinecraftProvider::createClientOnly,
IntermediaryMinecraftProvider.SingleJarImpl::client,
NamedMinecraftProvider.SingleJarImpl::client,
SrgMinecraftProvider.SingleJarImpl::client,
MojangMappedMinecraftProvider.SingleJarImpl::client,
ProcessedNamedMinecraftProvider.SingleJarImpl::client,
SingleJarDecompileConfiguration::new,
List.of("client")
);
public static final MinecraftJarConfiguration<
SplitMinecraftProvider,
NamedMinecraftProvider.SplitImpl,
MappedMinecraftProvider.Split> SPLIT = new MinecraftJarConfiguration<>(
SplitMinecraftProvider::new,
IntermediaryMinecraftProvider.SplitImpl::new,
NamedMinecraftProvider.SplitImpl::new,
SrgMinecraftProvider.SplitImpl::new,
MojangMappedMinecraftProvider.SplitImpl::new,
ProcessedNamedMinecraftProvider.SplitImpl::new,
SplitDecompileConfiguration::new,
List.of("client", "server")
);
private final Function<ConfigContext, MinecraftProvider> minecraftProviderFunction; public MinecraftProvider createMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext context) {
private final BiFunction<Project, MinecraftProvider, IntermediaryMinecraftProvider<?>> intermediaryMinecraftProviderBiFunction; return minecraftProviderFactory.create(metadataProvider, context);
private final BiFunction<Project, MinecraftProvider, NamedMinecraftProvider<?>> namedMinecraftProviderBiFunction;
private final BiFunction<Project, MinecraftProvider, SrgMinecraftProvider<?>> srgMinecraftProviderBiFunction;
private final BiFunction<Project, MinecraftProvider, MojangMappedMinecraftProvider<?>> mojangMappedMinecraftProviderBiFunction;
private final BiFunction<NamedMinecraftProvider<?>, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider<?, ?>> processedNamedMinecraftProviderBiFunction;
private final BiFunction<Project, MappedMinecraftProvider, DecompileConfiguration<?>> decompileConfigurationBiFunction;
private final List<String> supportedEnvironments;
@SuppressWarnings("unchecked") // Just a bit of a generic mess :)
<M extends MinecraftProvider, P extends NamedMinecraftProvider<M>, Q extends MappedMinecraftProvider> MinecraftJarConfiguration(
Function<ConfigContext, M> minecraftProviderFunction,
BiFunction<Project, M, IntermediaryMinecraftProvider<M>> intermediaryMinecraftProviderBiFunction,
BiFunction<Project, M, P> namedMinecraftProviderBiFunction,
BiFunction<Project, M, SrgMinecraftProvider<M>> srgMinecraftProviderBiFunction,
BiFunction<Project, M, MojangMappedMinecraftProvider<M>> mojangMappedMinecraftProviderBiFunction,
BiFunction<P, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider<M, P>> processedNamedMinecraftProviderBiFunction,
BiFunction<Project, Q, DecompileConfiguration<?>> decompileConfigurationBiFunction,
List<String> supportedEnvironments
) {
this.minecraftProviderFunction = (Function<ConfigContext, MinecraftProvider>) minecraftProviderFunction;
this.intermediaryMinecraftProviderBiFunction = (BiFunction<Project, MinecraftProvider, IntermediaryMinecraftProvider<?>>) (Object) intermediaryMinecraftProviderBiFunction;
this.namedMinecraftProviderBiFunction = (BiFunction<Project, MinecraftProvider, NamedMinecraftProvider<?>>) namedMinecraftProviderBiFunction;
this.srgMinecraftProviderBiFunction = (BiFunction<Project, MinecraftProvider, SrgMinecraftProvider<?>>) (Object) srgMinecraftProviderBiFunction;
this.mojangMappedMinecraftProviderBiFunction = (BiFunction<Project, MinecraftProvider, MojangMappedMinecraftProvider<?>>) (Object) mojangMappedMinecraftProviderBiFunction;
this.processedNamedMinecraftProviderBiFunction = (BiFunction<NamedMinecraftProvider<?>, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider<?, ?>>) (Object) processedNamedMinecraftProviderBiFunction;
this.decompileConfigurationBiFunction = (BiFunction<Project, MappedMinecraftProvider, DecompileConfiguration<?>>) decompileConfigurationBiFunction;
this.supportedEnvironments = supportedEnvironments;
} }
public Function<ConfigContext, MinecraftProvider> getMinecraftProviderFunction() { public IntermediaryMinecraftProvider<M> createIntermediaryMinecraftProvider(Project project) {
return minecraftProviderFunction; return intermediaryMinecraftProviderFactory.create(project, getMinecraftProvider(project));
} }
public BiFunction<Project, MinecraftProvider, IntermediaryMinecraftProvider<?>> getIntermediaryMinecraftProviderBiFunction() { public NamedMinecraftProvider<M> createNamedMinecraftProvider(Project project) {
return intermediaryMinecraftProviderBiFunction; return namedMinecraftProviderFactory.create(project, getMinecraftProvider(project));
} }
public BiFunction<Project, MinecraftProvider, NamedMinecraftProvider<?>> getNamedMinecraftProviderBiFunction() { public ProcessedNamedMinecraftProvider<M, N> createProcessedNamedMinecraftProvider(NamedMinecraftProvider<?> namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) {
return namedMinecraftProviderBiFunction; return processedNamedMinecraftProviderFactory.create((N) namedMinecraftProvider, jarProcessorManager);
} }
public BiFunction<NamedMinecraftProvider<?>, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider<?, ?>> getProcessedNamedMinecraftProviderBiFunction() { public DecompileConfiguration<Q> createDecompileConfiguration(Project project) {
return processedNamedMinecraftProviderBiFunction; return decompileConfigurationFactory.create(project, getMappedMinecraftProvider(project));
} }
public BiFunction<Project, MappedMinecraftProvider, DecompileConfiguration<?>> getDecompileConfigurationBiFunction() { private M getMinecraftProvider(Project project) {
return decompileConfigurationBiFunction; LoomGradleExtension extension = LoomGradleExtension.get(project);
//noinspection unchecked
return (M) extension.getMinecraftProvider();
}
private Q getMappedMinecraftProvider(Project project) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
//noinspection unchecked
return (Q) extension.getNamedMinecraftProvider();
} }
public BiFunction<Project, MinecraftProvider, SrgMinecraftProvider<?>> getSrgMinecraftProviderBiFunction() { public BiFunction<Project, MinecraftProvider, SrgMinecraftProvider<?>> getSrgMinecraftProviderBiFunction() {
@@ -146,4 +159,33 @@ public enum MinecraftJarConfiguration {
public List<String> getSupportedEnvironments() { public List<String> getSupportedEnvironments() {
return supportedEnvironments; return supportedEnvironments;
} }
// Factory interfaces:
private interface MinecraftProviderFactory<M extends MinecraftProvider> {
M create(MinecraftMetadataProvider metadataProvider, ConfigContext configContext);
}
private interface IntermediaryMinecraftProviderFactory<M extends MinecraftProvider> {
IntermediaryMinecraftProvider<M> create(Project project, M minecraftProvider);
}
private interface NamedMinecraftProviderFactory<M extends MinecraftProvider> {
NamedMinecraftProvider<M> create(Project project, M minecraftProvider);
}
private interface SrgMinecraftProviderFactory<M extends MinecraftProvider> {
SrgMinecraftProvider<M> create(Project project, M minecraftProvider);
}
private interface MojangMappedMinecraftProviderFactory<M extends MinecraftProvider> {
MojangMappedMinecraftProvider<M> create(Project project, M minecraftProvider);
}
private interface ProcessedNamedMinecraftProviderFactory<M extends MinecraftProvider, N extends NamedMinecraftProvider<M>> {
ProcessedNamedMinecraftProvider<M, N> create(N namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager);
}
private interface DecompileConfigurationFactory<M extends MappedMinecraftProvider> {
DecompileConfiguration<M> create(Project project, M minecraftProvider);
}
} }

View File

@@ -82,6 +82,8 @@ public class MinecraftJarMerger implements AutoCloseable {
} }
} }
Files.createDirectories(output.toPath().getParent());
this.inputClient = (inputClientFs = FileSystemUtil.getJarFileSystem(inputClient, false)).get().getPath("/"); this.inputClient = (inputClientFs = FileSystemUtil.getJarFileSystem(inputClient, false)).get().getPath("/");
this.inputServer = (inputServerFs = FileSystemUtil.getJarFileSystem(inputServer, false)).get().getPath("/"); this.inputServer = (inputServerFs = FileSystemUtil.getJarFileSystem(inputServer, false)).get().getPath("/");
this.outputFs = FileSystemUtil.getJarFileSystem(output, true); this.outputFs = FileSystemUtil.getJarFileSystem(output, true);

View File

@@ -82,8 +82,8 @@ public class MinecraftLibraryProvider {
final LoomGradleExtension extension = LoomGradleExtension.get(project); final LoomGradleExtension extension = LoomGradleExtension.get(project);
final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get(); final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get();
final boolean provideClient = jarConfiguration.getSupportedEnvironments().contains("client"); final boolean provideClient = jarConfiguration.supportedEnvironments().contains("client");
final boolean provideServer = jarConfiguration.getSupportedEnvironments().contains("server"); final boolean provideServer = jarConfiguration.supportedEnvironments().contains("server");
assert provideClient || provideServer; assert provideClient || provideServer;
if (provideClient) { if (provideClient) {

View File

@@ -35,6 +35,9 @@ import org.jetbrains.annotations.Nullable;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.DependencyInfo;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.MirrorUtil;
import net.fabricmc.loom.util.download.DownloadBuilder; import net.fabricmc.loom.util.download.DownloadBuilder;
@@ -45,11 +48,34 @@ public final class MinecraftMetadataProvider {
private ManifestVersion.Versions versionEntry; private ManifestVersion.Versions versionEntry;
private MinecraftVersionMeta versionMeta; private MinecraftVersionMeta versionMeta;
public MinecraftMetadataProvider(Options options, Function<String, DownloadBuilder> download) { private MinecraftMetadataProvider(Options options, Function<String, DownloadBuilder> download) {
this.options = options; this.options = options;
this.download = download; this.download = download;
} }
public static MinecraftMetadataProvider create(ConfigContext configContext) {
final String minecraftVersion = resolveMinecraftVersion(configContext.project());
final Path workingDir = MinecraftProvider.minecraftWorkingDirectory(configContext.project(), minecraftVersion).toPath();
return new MinecraftMetadataProvider(
MinecraftMetadataProvider.Options.create(
minecraftVersion,
configContext.project(),
workingDir.resolve("minecraft-info.json")
),
configContext.extension()::download
);
}
private static String resolveMinecraftVersion(Project project) {
final DependencyInfo dependency = DependencyInfo.create(project, Constants.Configurations.MINECRAFT);
return dependency.getDependency().getVersion();
}
public String getMinecraftVersion() {
return options.minecraftVersion();
}
public MinecraftVersionMeta getVersionMeta() { public MinecraftVersionMeta getVersionMeta() {
try { try {
if (versionEntry == null) { if (versionEntry == null) {

View File

@@ -31,13 +31,15 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.DependencyInfo;
import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.configuration.providers.BundleMetadata;
import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.DownloadExecutor; import net.fabricmc.loom.util.download.DownloadExecutor;
@@ -45,10 +47,10 @@ import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
import net.fabricmc.loom.util.gradle.ProgressGroup; import net.fabricmc.loom.util.gradle.ProgressGroup;
public abstract class MinecraftProvider { public abstract class MinecraftProvider {
private String minecraftVersion; private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftProvider.class);
private MinecraftMetadataProvider metadataProvider;
private final MinecraftMetadataProvider metadataProvider;
private File workingDir;
private File minecraftClientJar; private File minecraftClientJar;
// Note this will be the boostrap jar starting with 21w39a // Note this will be the boostrap jar starting with 21w39a
private File minecraftServerJar; private File minecraftServerJar;
@@ -58,10 +60,11 @@ public abstract class MinecraftProvider {
private BundleMetadata serverBundleMetadata; private BundleMetadata serverBundleMetadata;
private String jarPrefix = ""; private String jarPrefix = "";
private final Project project; private final ConfigContext configContext;
public MinecraftProvider(ConfigContext configContext) { public MinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
this.project = configContext.project(); this.metadataProvider = metadataProvider;
this.configContext = configContext;
} }
protected boolean provideClient() { protected boolean provideClient() {
@@ -73,23 +76,22 @@ public abstract class MinecraftProvider {
} }
public void provide() throws Exception { public void provide() throws Exception {
final DependencyInfo dependency = DependencyInfo.create(getProject(), Constants.Configurations.MINECRAFT);
minecraftVersion = dependency.getDependency().getVersion();
if (getExtension().shouldGenerateSrgTiny() && !getExtension().isForgeLike()) { if (getExtension().shouldGenerateSrgTiny() && !getExtension().isForgeLike()) {
getProject().getDependencies().add(Constants.Configurations.SRG, "de.oceanlabs.mcp:mcp_config:" + minecraftVersion); getProject().getDependencies().add(Constants.Configurations.SRG, "de.oceanlabs.mcp:mcp_config:" + minecraftVersion);
} }
initFiles(); initFiles();
metadataProvider = new MinecraftMetadataProvider( final MinecraftVersionMeta.JavaVersion javaVersion = getVersionInfo().javaVersion();
MinecraftMetadataProvider.Options.create(
minecraftVersion, if (javaVersion != null) {
getProject(), final int requiredMajorJavaVersion = getVersionInfo().javaVersion().majorVersion();
file("minecraft-info.json").toPath() final JavaVersion requiredJavaVersion = JavaVersion.toVersion(requiredMajorJavaVersion);
),
getExtension()::download if (!JavaVersion.current().isCompatibleWith(requiredJavaVersion)) {
); throw new IllegalStateException("Minecraft " + minecraftVersion() + " requires Java " + requiredJavaVersion + " but Gradle is using " + JavaVersion.current());
}
}
downloadJars(); downloadJars();
@@ -97,14 +99,11 @@ public abstract class MinecraftProvider {
serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath()); serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath());
} }
final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, project); final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, configContext.project());
libraryProvider.provide(); libraryProvider.provide();
} }
protected void initFiles() { protected void initFiles() {
workingDir = new File(getExtension().getFiles().getUserCache(), minecraftVersion);
workingDir.mkdirs();
if (provideClient()) { if (provideClient()) {
minecraftClientJar = file("minecraft-client.jar"); minecraftClientJar = file("minecraft-client.jar");
} }
@@ -140,17 +139,17 @@ public abstract class MinecraftProvider {
Preconditions.checkArgument(provideServer(), "Not configured to provide server jar"); Preconditions.checkArgument(provideServer(), "Not configured to provide server jar");
Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar"); Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar");
getLogger().info(":Extracting server jar from bootstrap"); LOGGER.info(":Extracting server jar from bootstrap");
if (getServerBundleMetadata().versions().size() != 1) { if (getServerBundleMetadata().versions().size() != 1) {
throw new UnsupportedOperationException("Expected only 1 version in META-INF/versions.list, but got %d".formatted(getServerBundleMetadata().versions().size())); throw new UnsupportedOperationException("Expected only 1 version in META-INF/versions.list, but got %d".formatted(getServerBundleMetadata().versions().size()));
} }
getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), project); getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), configContext.project());
} }
public File workingDir() { public File workingDir() {
return workingDir; return minecraftWorkingDirectory(configContext.project(), minecraftVersion());
} }
public File dir(String path) { public File dir(String path) {
@@ -186,13 +185,20 @@ public abstract class MinecraftProvider {
} }
public String minecraftVersion() { public String minecraftVersion() {
return minecraftVersion; return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getMinecraftVersion();
} }
public MinecraftVersionMeta getVersionInfo() { public MinecraftVersionMeta getVersionInfo() {
return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getVersionMeta(); return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getVersionMeta();
} }
/**
* @return true if the minecraft version is older than 1.3.
*/
public boolean isLegacyVersion() {
return !getVersionInfo().isVersionOrNewer(Constants.RELEASE_TIME_1_3);
}
public String getJarPrefix() { public String getJarPrefix() {
return jarPrefix; return jarPrefix;
} }
@@ -206,21 +212,26 @@ public abstract class MinecraftProvider {
return serverBundleMetadata; return serverBundleMetadata;
} }
protected Logger getLogger() {
return getProject().getLogger();
}
public abstract List<Path> getMinecraftJars(); public abstract List<Path> getMinecraftJars();
public abstract MappingsNamespace getOfficialNamespace();
protected Project getProject() { protected Project getProject() {
return project; return configContext.project();
} }
protected LoomGradleExtension getExtension() { protected LoomGradleExtension getExtension() {
return LoomGradleExtension.get(getProject()); return configContext.extension();
} }
public boolean refreshDeps() { public boolean refreshDeps() {
return getExtension().refreshDeps(); return getExtension().refreshDeps();
} }
public static File minecraftWorkingDirectory(Project project, String version) {
LoomGradleExtension extension = LoomGradleExtension.get(project);
File workingDir = new File(extension.getFiles().getUserCache(), version);
workingDir.mkdirs();
return workingDir;
}
} }

View File

@@ -47,7 +47,8 @@ public record MinecraftVersionMeta(
int minimumLauncherVersion, int minimumLauncherVersion,
String releaseTime, String releaseTime,
String time, String time,
String type String type,
@Nullable JavaVersion javaVersion
) { ) {
private static Map<Platform.OperatingSystem, String> OS_NAMES = Map.of( private static Map<Platform.OperatingSystem, String> OS_NAMES = Map.of(
Platform.OperatingSystem.WINDOWS, "windows", Platform.OperatingSystem.WINDOWS, "windows",
@@ -168,4 +169,7 @@ public record MinecraftVersionMeta(
return new File(baseDirectory, path()); return new File(baseDirectory, path());
} }
} }
public record JavaVersion(String component, int majorVersion) {
}
} }

View File

@@ -28,35 +28,45 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.configuration.providers.BundleMetadata;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.NonClassCopyMode;
import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.OutputConsumerPath;
import net.fabricmc.tinyremapper.TinyRemapper; import net.fabricmc.tinyremapper.TinyRemapper;
public class SingleJarMinecraftProvider extends MinecraftProvider { public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvider permits SingleJarMinecraftProvider.Server, SingleJarMinecraftProvider.Client {
private final Environment environment; private final MappingsNamespace officialNamespace;
private Path minecraftEnvOnlyJar; private Path minecraftEnvOnlyJar;
protected SingleJarMinecraftProvider(ConfigContext configContext, Environment environment) { protected SingleJarMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) {
super(configContext); super(metadataProvider, configContext);
this.environment = environment; this.officialNamespace = officialNamespace;
} }
public static SingleJarMinecraftProvider server(ConfigContext configContext) { public static SingleJarMinecraftProvider.Server server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
return new SingleJarMinecraftProvider(configContext, new Server()); return new SingleJarMinecraftProvider.Server(metadataProvider, configContext, getOfficialNamespace(metadataProvider, true));
} }
public static SingleJarMinecraftProvider client(ConfigContext configContext) { public static SingleJarMinecraftProvider.Client client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
return new SingleJarMinecraftProvider(configContext, new Client()); return new SingleJarMinecraftProvider.Client(metadataProvider, configContext, getOfficialNamespace(metadataProvider, false));
}
private static MappingsNamespace getOfficialNamespace(MinecraftMetadataProvider metadataProvider, boolean server) {
// Versions before 1.3 don't have a common namespace, so use side specific namespaces.
if (!metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) {
return server ? MappingsNamespace.SERVER_OFFICIAL : MappingsNamespace.CLIENT_OFFICIAL;
}
return MappingsNamespace.OFFICIAL;
} }
@Override @Override
protected void initFiles() { protected void initFiles() {
super.initFiles(); super.initFiles();
minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(environment.type())); minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(type()));
} }
@Override @Override
@@ -69,7 +79,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
super.provide(); super.provide();
// Server only JARs are supported on any version, client only JARs are pretty much useless after 1.3. // Server only JARs are supported on any version, client only JARs are pretty much useless after 1.3.
if (provideClient() && getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { if (provideClient() && !isLegacyVersion()) {
getProject().getLogger().warn("Using `clientOnlyMinecraftJar()` is not recommended for Minecraft versions 1.3 or newer."); getProject().getLogger().warn("Using `clientOnlyMinecraftJar()` is not recommended for Minecraft versions 1.3 or newer.");
} }
@@ -83,7 +93,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
return; return;
} }
final Path inputJar = environment.getInputJar(this); final Path inputJar = getInputJar(this);
TinyRemapper remapper = null; TinyRemapper remapper = null;
@@ -100,7 +110,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
} }
} catch (Exception e) { } catch (Exception e) {
Files.deleteIfExists(minecraftEnvOnlyJar); Files.deleteIfExists(minecraftEnvOnlyJar);
throw new RuntimeException("Failed to process %s only jar".formatted(environment.type()), e); throw new RuntimeException("Failed to process %s only jar".formatted(type()), e);
} finally { } finally {
if (remapper != null) { if (remapper != null) {
remapper.finish(); remapper.finish();
@@ -108,27 +118,24 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
} }
} }
@Override
protected boolean provideClient() {
return environment instanceof Client;
}
@Override
protected boolean provideServer() {
return environment instanceof Server;
}
public Path getMinecraftEnvOnlyJar() { public Path getMinecraftEnvOnlyJar() {
return minecraftEnvOnlyJar; return minecraftEnvOnlyJar;
} }
protected interface Environment { @Override
SingleJarEnvType type(); public MappingsNamespace getOfficialNamespace() {
return officialNamespace;
Path getInputJar(SingleJarMinecraftProvider provider) throws Exception;
} }
public static final class Server implements Environment { abstract SingleJarEnvType type();
abstract Path getInputJar(SingleJarMinecraftProvider provider) throws Exception;
public static final class Server extends SingleJarMinecraftProvider {
private Server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) {
super(metadataProvider, configContext, officialNamespace);
}
@Override @Override
public SingleJarEnvType type() { public SingleJarEnvType type() {
return SingleJarEnvType.SERVER; return SingleJarEnvType.SERVER;
@@ -145,9 +152,23 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
provider.extractBundledServerJar(); provider.extractBundledServerJar();
return provider.getMinecraftExtractedServerJar().toPath(); return provider.getMinecraftExtractedServerJar().toPath();
} }
@Override
protected boolean provideServer() {
return true;
}
@Override
protected boolean provideClient() {
return false;
}
} }
public static final class Client implements Environment { public static final class Client extends SingleJarMinecraftProvider {
private Client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) {
super(metadataProvider, configContext, officialNamespace);
}
@Override @Override
public SingleJarEnvType type() { public SingleJarEnvType type() {
return SingleJarEnvType.CLIENT; return SingleJarEnvType.CLIENT;
@@ -157,5 +178,15 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception { public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception {
return provider.getMinecraftClientJar().toPath(); return provider.getMinecraftClientJar().toPath();
} }
@Override
protected boolean provideServer() {
return false;
}
@Override
protected boolean provideClient() {
return true;
}
} }
} }

View File

@@ -28,6 +28,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.configuration.providers.BundleMetadata;
@@ -35,8 +36,8 @@ public final class SplitMinecraftProvider extends MinecraftProvider {
private Path minecraftClientOnlyJar; private Path minecraftClientOnlyJar;
private Path minecraftCommonJar; private Path minecraftCommonJar;
public SplitMinecraftProvider(ConfigContext configContext) { public SplitMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) {
super(configContext); super(metadataProvider, configContext);
} }
@Override @Override
@@ -52,6 +53,11 @@ public final class SplitMinecraftProvider extends MinecraftProvider {
return List.of(minecraftClientOnlyJar, minecraftCommonJar); return List.of(minecraftClientOnlyJar, minecraftCommonJar);
} }
@Override
public MappingsNamespace getOfficialNamespace() {
return MappingsNamespace.OFFICIAL;
}
@Override @Override
public void provide() throws Exception { public void provide() throws Exception {
super.provide(); super.provide();

View File

@@ -51,6 +51,7 @@ import net.fabricmc.loom.configuration.providers.mappings.TinyMappingsService;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.configuration.providers.minecraft.SignatureFixerApplyVisitor; import net.fabricmc.loom.configuration.providers.minecraft.SignatureFixerApplyVisitor;
import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.extension.LoomFiles;
import net.fabricmc.loom.util.SidedClassVisitor; import net.fabricmc.loom.util.SidedClassVisitor;
@@ -205,7 +206,10 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
final Set<String> classNames = extension.isForgeLike() ? InnerClassRemapper.readClassNames(remappedJars.inputJar()) : Set.of(); final Set<String> classNames = extension.isForgeLike() ? InnerClassRemapper.readClassNames(remappedJars.inputJar()) : Set.of();
final Map<String, String> remappedSignatures = SignatureFixerApplyVisitor.getRemappedSignatures(getTargetNamespace() == MappingsNamespace.INTERMEDIARY, mappingConfiguration, getProject(), configContext.serviceManager(), toM); final Map<String, String> remappedSignatures = SignatureFixerApplyVisitor.getRemappedSignatures(getTargetNamespace() == MappingsNamespace.INTERMEDIARY, mappingConfiguration, getProject(), configContext.serviceManager(), toM);
TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, true, (builder) -> { final MinecraftVersionMeta.JavaVersion javaVersion = minecraftProvider.getVersionInfo().javaVersion();
final boolean fixRecords = javaVersion != null && javaVersion.majorVersion() >= 16;
TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, fixRecords, (builder) -> {
builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures)); builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures));
configureRemapper(remappedJars, builder); configureRemapper(remappedJars, builder);
}, classNames); }, classNames);

View File

@@ -29,14 +29,16 @@ import java.util.List;
import org.gradle.api.Project; import org.gradle.api.Project;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType;
import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider;
import net.fabricmc.tinyremapper.TinyRemapper; import net.fabricmc.tinyremapper.TinyRemapper;
public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftProvider> extends AbstractMappedMinecraftProvider<M> permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl { public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftProvider> extends AbstractMappedMinecraftProvider<M> permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.LegacyMergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl {
public IntermediaryMinecraftProvider(Project project, M minecraftProvider) { public IntermediaryMinecraftProvider(Project project, M minecraftProvider) {
super(project, minecraftProvider); super(project, minecraftProvider);
} }
@@ -59,11 +61,49 @@ public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftPr
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace())
); );
} }
} }
public static final class LegacyMergedImpl extends IntermediaryMinecraftProvider<LegacyMergedMinecraftProvider> implements Merged {
private final SingleJarImpl server;
private final SingleJarImpl client;
public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) {
super(project, minecraftProvider);
server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER);
client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT);
}
@Override
public List<MinecraftJar> provide(ProvideContext context) throws Exception {
// Map the client and server jars separately
server.provide(context);
client.provide(context);
// then merge them
MergedMinecraftProvider.mergeJars(
client.getEnvOnlyJar().toFile(),
server.getEnvOnlyJar().toFile(),
getMergedJar().toFile()
);
return List.of(getMergedJar());
}
@Override
public List<RemappedJars> getRemappedJars() {
// The delegate providers will handle the remapping
throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars");
}
@Override
public List<MinecraftJar.Type> getDependencyTypes() {
return List.of(MinecraftJar.Type.MERGED);
}
}
public static final class SplitImpl extends IntermediaryMinecraftProvider<SplitMinecraftProvider> implements Split { public static final class SplitImpl extends IntermediaryMinecraftProvider<SplitMinecraftProvider> implements Split {
public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) {
super(project, minecraftProvider); super(project, minecraftProvider);
@@ -72,8 +112,8 @@ public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftPr
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()),
new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar())
); );
} }
@@ -102,7 +142,7 @@ public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftPr
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace())
); );
} }

View File

@@ -29,9 +29,11 @@ import java.util.List;
import org.gradle.api.Project; import org.gradle.api.Project;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets;
import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType;
import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider;
@@ -60,7 +62,7 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace())
); );
} }
@@ -70,6 +72,55 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
} }
} }
public static final class LegacyMergedImpl extends NamedMinecraftProvider<LegacyMergedMinecraftProvider> implements Merged {
private final SingleJarImpl server;
private final SingleJarImpl client;
public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) {
super(project, minecraftProvider);
server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER);
client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT);
}
@Override
public List<MinecraftJar> provide(ProvideContext context) throws Exception {
final ProvideContext childContext = context.withApplyDependencies(false);
// Map the client and server jars separately
server.provide(childContext);
client.provide(childContext);
// then merge them
MergedMinecraftProvider.mergeJars(
client.getEnvOnlyJar().toFile(),
server.getEnvOnlyJar().toFile(),
getMergedJar().toFile()
);
getMavenHelper(MinecraftJar.Type.MERGED).savePom();
if (context.applyDependencies()) {
MinecraftSourceSets.get(getProject()).applyDependencies(
(configuration, type) -> getProject().getDependencies().add(configuration, getDependencyNotation(type)),
getDependencyTypes()
);
}
return List.of(getMergedJar());
}
@Override
public List<RemappedJars> getRemappedJars() {
// The delegate providers will handle the remapping
throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars");
}
@Override
public List<MinecraftJar.Type> getDependencyTypes() {
return List.of(MinecraftJar.Type.MERGED);
}
}
public static final class SplitImpl extends NamedMinecraftProvider<SplitMinecraftProvider> implements Split { public static final class SplitImpl extends NamedMinecraftProvider<SplitMinecraftProvider> implements Split {
public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) {
super(project, minecraftProvider); super(project, minecraftProvider);
@@ -78,8 +129,8 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()),
new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar())
); );
} }
@@ -113,7 +164,7 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
@Override @Override
public List<RemappedJars> getRemappedJars() { public List<RemappedJars> getRemappedJars() {
return List.of( return List.of(
new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace())
); );
} }

View File

@@ -37,6 +37,7 @@ import net.fabricmc.loom.configuration.ConfigContext;
import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper; import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper;
import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager;
import net.fabricmc.loom.configuration.processors.ProcessorContextImpl; import net.fabricmc.loom.configuration.processors.ProcessorContextImpl;
import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
@@ -178,6 +179,17 @@ public abstract class ProcessedNamedMinecraftProvider<M extends MinecraftProvide
} }
} }
public static final class LegacyMergedImpl extends ProcessedNamedMinecraftProvider<LegacyMergedMinecraftProvider, NamedMinecraftProvider.LegacyMergedImpl> implements Merged {
public LegacyMergedImpl(NamedMinecraftProvider.LegacyMergedImpl parentMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) {
super(parentMinecraftProvider, jarProcessorManager);
}
@Override
public MinecraftJar getMergedJar() {
return getProcessedJar(getParentMinecraftProvider().getMergedJar());
}
}
public static final class SplitImpl extends ProcessedNamedMinecraftProvider<SplitMinecraftProvider, NamedMinecraftProvider.SplitImpl> implements Split { public static final class SplitImpl extends ProcessedNamedMinecraftProvider<SplitMinecraftProvider, NamedMinecraftProvider.SplitImpl> implements Split {
public SplitImpl(NamedMinecraftProvider.SplitImpl parentMinecraftProvide, MinecraftJarProcessorManager jarProcessorManager) { public SplitImpl(NamedMinecraftProvider.SplitImpl parentMinecraftProvide, MinecraftJarProcessorManager jarProcessorManager) {
super(parentMinecraftProvide, jarProcessorManager); super(parentMinecraftProvide, jarProcessorManager);

View File

@@ -0,0 +1,158 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers;
import static java.text.MessageFormat.format;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
public record ClassLineNumbers(Map<String, ClassLineNumbers.Entry> lineMap) {
public ClassLineNumbers {
Objects.requireNonNull(lineMap, "lineMap");
if (lineMap.isEmpty()) {
throw new IllegalArgumentException("lineMap is empty");
}
}
public static ClassLineNumbers readMappings(Path lineMappingsPath) {
try (BufferedReader reader = Files.newBufferedReader(lineMappingsPath)) {
return readMappings(reader);
} catch (IOException e) {
throw new UncheckedIOException("Exception reading LineMappings file.", e);
}
}
public static ClassLineNumbers readMappings(BufferedReader reader) {
var lineMap = new HashMap<String, ClassLineNumbers.Entry>();
String line = null;
int lineNumber = 0;
record CurrentClass(String className, int maxLine, int maxLineDest) {
void putEntry(Map<String, ClassLineNumbers.Entry> entries, Map<Integer, Integer> mappings) {
var entry = new ClassLineNumbers.Entry(className(), maxLine(), maxLineDest(), Collections.unmodifiableMap(mappings));
final ClassLineNumbers.Entry previous = entries.put(className(), entry);
if (previous != null) {
throw new IllegalStateException("Duplicate class line mappings for " + className());
}
}
}
CurrentClass currentClass = null;
Map<Integer, Integer> currentMappings = new HashMap<>();
try {
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
final String[] segments = line.trim().split("\t");
if (line.charAt(0) != '\t') {
if (currentClass != null) {
currentClass.putEntry(lineMap, currentMappings);
currentMappings = new HashMap<>();
}
currentClass = new CurrentClass(segments[0], Integer.parseInt(segments[1]), Integer.parseInt(segments[2]));
} else {
Objects.requireNonNull(currentClass, "No class line mappings found for line " + lineNumber);
currentMappings.put(Integer.parseInt(segments[0]), Integer.parseInt(segments[1]));
}
lineNumber++;
}
} catch (Exception e) {
throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", lineNumber, line), e);
}
assert currentClass != null;
currentClass.putEntry(lineMap, currentMappings);
return new ClassLineNumbers(Collections.unmodifiableMap(lineMap));
}
public void write(Writer writer) throws IOException {
for (Map.Entry<String, ClassLineNumbers.Entry> entry : lineMap.entrySet()) {
entry.getValue().write(writer);
}
}
/**
* Merge two ClassLineNumbers together, throwing an exception if there are any duplicate class line mappings.
*/
@Nullable
public static ClassLineNumbers merge(@Nullable ClassLineNumbers a, @Nullable ClassLineNumbers b) {
if (a == null) {
return b;
} else if (b == null) {
return a;
}
var lineMap = new HashMap<>(a.lineMap());
for (Map.Entry<String, Entry> entry : b.lineMap().entrySet()) {
lineMap.merge(entry.getKey(), entry.getValue(), (v1, v2) -> {
throw new IllegalStateException("Duplicate class line mappings for " + entry.getKey());
});
}
return new ClassLineNumbers(Collections.unmodifiableMap(lineMap));
}
public record Entry(String className, int maxLine, int maxLineDest, Map<Integer, Integer> lineMap) {
public void write(Writer writer) throws IOException {
writer.write(className);
writer.write('\t');
writer.write(Integer.toString(maxLine));
writer.write('\t');
writer.write(Integer.toString(maxLineDest));
writer.write('\n');
for (Map.Entry<Integer, Integer> lineEntry : lineMap.entrySet()) {
writer.write('\t');
writer.write(Integer.toString(lineEntry.getKey()));
writer.write('\t');
writer.write(Integer.toString(lineEntry.getValue()));
writer.write('\n');
}
}
}
}

View File

@@ -24,93 +24,48 @@
package net.fabricmc.loom.decompilers; package net.fabricmc.loom.decompilers;
import static java.text.MessageFormat.format;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes; import java.util.HashSet;
import java.util.HashMap; import java.util.Set;
import java.util.Map;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label; import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.MethodVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.AsyncZipProcessor;
import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.IOStringConsumer;
/** public record LineNumberRemapper(ClassLineNumbers lineNumbers) {
* Created by covers1624 on 18/02/19. private static final Logger LOGGER = LoggerFactory.getLogger(LineNumberRemapper.class);
*/
public class LineNumberRemapper {
private final Map<String, RClass> lineMap = new HashMap<>();
public void readMappings(File lineMappings) { public void process(Path input, Path output) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(lineMappings))) { AsyncZipProcessor.processEntries(input, output, new AsyncZipProcessor() {
RClass clazz = null; private final Set<Path> createdParents = new HashSet<>();
String line = null;
int i = 0;
try {
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
String[] segs = line.trim().split("\t");
if (line.charAt(0) != '\t') {
clazz = lineMap.computeIfAbsent(segs[0], RClass::new);
clazz.maxLine = Integer.parseInt(segs[1]);
clazz.maxLineDest = Integer.parseInt(segs[2]);
} else {
clazz.lineMap.put(Integer.parseInt(segs[0]), Integer.parseInt(segs[1]));
}
i++;
}
} catch (Exception e) {
throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", i, line), e);
}
} catch (IOException e) {
throw new RuntimeException("Exception reading LineMappings file.", e);
}
}
public void process(IOStringConsumer logger, Path input, Path output) throws IOException {
Files.walkFileTree(input, new SimpleFileVisitor<>() {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { public void processEntryAsync(Path file, Path dst) throws IOException {
String rel = input.relativize(file).toString();
Path dst = output.resolve(rel);
Path parent = dst.getParent(); Path parent = dst.getParent();
if (parent != null) { synchronized (createdParents) {
Files.createDirectories(parent); if (parent != null && createdParents.add(parent)) {
Files.createDirectories(parent);
}
} }
String fName = file.getFileName().toString(); String fileName = file.getFileName().toString();
if (fName.endsWith(".class")) { if (fileName.endsWith(".class")) {
if (Files.exists(dst)) { String idx = fileName.substring(0, fileName.length() - 6);
Files.delete(dst);
}
String idx = rel.substring(0, rel.length() - 6); LOGGER.debug("Remapping line numbers for class: " + idx);
if (logger != null) {
logger.accept("Remapping " + idx);
}
int dollarPos = idx.indexOf('$'); //This makes the assumption that only Java classes are to be remapped. int dollarPos = idx.indexOf('$'); //This makes the assumption that only Java classes are to be remapped.
@@ -118,30 +73,29 @@ public class LineNumberRemapper {
idx = idx.substring(0, dollarPos); idx = idx.substring(0, dollarPos);
} }
if (lineMap.containsKey(idx)) { if (lineNumbers.lineMap().containsKey(idx)) {
try (InputStream is = Files.newInputStream(file)) { try (InputStream is = Files.newInputStream(file)) {
ClassReader reader = new ClassReader(is); ClassReader reader = new ClassReader(is);
ClassWriter writer = new ClassWriter(0); ClassWriter writer = new ClassWriter(0);
reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineMap.get(idx)), 0); reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineNumbers.lineMap().get(idx)), 0);
Files.write(dst, writer.toByteArray()); Files.write(dst, writer.toByteArray());
return FileVisitResult.CONTINUE; return;
} }
} }
} }
Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING); Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
} }
}); });
} }
private static class LineNumberVisitor extends ClassVisitor { private static class LineNumberVisitor extends ClassVisitor {
private final RClass rClass; private final ClassLineNumbers.Entry lineNumbers;
LineNumberVisitor(int api, ClassVisitor classVisitor, RClass rClass) { LineNumberVisitor(int api, ClassVisitor classVisitor, ClassLineNumbers.Entry lineNumbers) {
super(api, classVisitor); super(api, classVisitor);
this.rClass = rClass; this.lineNumbers = lineNumbers;
} }
@Override @Override
@@ -153,30 +107,19 @@ public class LineNumberRemapper {
if (tLine <= 0) { if (tLine <= 0) {
super.visitLineNumber(line, start); super.visitLineNumber(line, start);
} else if (tLine >= rClass.maxLine) { } else if (tLine >= lineNumbers.maxLine()) {
super.visitLineNumber(rClass.maxLineDest, start); super.visitLineNumber(lineNumbers.maxLineDest(), start);
} else { } else {
Integer matchedLine = null; Integer matchedLine = null;
while (tLine <= rClass.maxLine && ((matchedLine = rClass.lineMap.get(tLine)) == null)) { while (tLine <= lineNumbers.maxLine() && ((matchedLine = lineNumbers.lineMap().get(tLine)) == null)) {
tLine++; tLine++;
} }
super.visitLineNumber(matchedLine != null ? matchedLine : rClass.maxLineDest, start); super.visitLineNumber(matchedLine != null ? matchedLine : lineNumbers.maxLineDest(), start);
} }
} }
}; };
} }
} }
private static class RClass {
private final String name;
private int maxLine;
private int maxLineDest;
private final Map<Integer, Integer> lineMap = new HashMap<>();
private RClass(String name) {
this.name = name;
}
}
} }

View File

@@ -0,0 +1,209 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.decompilers.ClassLineNumbers;
// Serialised data for a class entry in the cache
// Uses the RIFF format, allows for appending the line numbers to the end of the file
// Stores the source code and line numbers for the class
public record CachedData(String className, String sources, @Nullable ClassLineNumbers.Entry lineNumbers) {
public static final CachedFileStore.EntrySerializer<CachedData> SERIALIZER = new EntrySerializer();
private static final String HEADER_ID = "LOOM";
private static final String NAME_ID = "NAME";
private static final String SOURCES_ID = "SRC ";
private static final String LINE_NUMBERS_ID = "LNUM";
private static final Logger LOGGER = LoggerFactory.getLogger(CachedData.class);
public CachedData {
Objects.requireNonNull(className, "className");
Objects.requireNonNull(sources, "sources");
if (lineNumbers != null) {
if (!className.equals(lineNumbers.className())) {
throw new IllegalArgumentException("Class name does not match line numbers class name");
}
}
}
public void write(FileChannel fileChannel) {
try (var c = new RiffChunk(HEADER_ID, fileChannel)) {
writeClassname(fileChannel);
writeSource(fileChannel);
if (lineNumbers != null) {
writeLineNumbers(fileChannel);
}
} catch (IOException e) {
throw new RuntimeException("Failed to write cached data", e);
}
}
private void writeClassname(FileChannel fileChannel) throws IOException {
try (var c = new RiffChunk(NAME_ID, fileChannel)) {
fileChannel.write(ByteBuffer.wrap(className.getBytes(StandardCharsets.UTF_8)));
}
}
private void writeSource(FileChannel fileChannel) throws IOException {
try (var c = new RiffChunk(SOURCES_ID, fileChannel)) {
fileChannel.write(ByteBuffer.wrap(sources.getBytes(StandardCharsets.UTF_8)));
}
}
private void writeLineNumbers(FileChannel fileChannel) throws IOException {
Objects.requireNonNull(lineNumbers);
try (var c = new RiffChunk(LINE_NUMBERS_ID, fileChannel);
StringWriter stringWriter = new StringWriter()) {
lineNumbers.write(stringWriter);
fileChannel.write(ByteBuffer.wrap(stringWriter.toString().getBytes(StandardCharsets.UTF_8)));
}
}
public static CachedData read(InputStream inputStream) throws IOException {
// Read and validate the RIFF header
final String header = readHeader(inputStream);
if (!header.equals(HEADER_ID)) {
throw new IOException("Invalid RIFF header: " + header + ", expected " + HEADER_ID);
}
// Read the data length
int length = readInt(inputStream);
String className = null;
String sources = null;
ClassLineNumbers.Entry lineNumbers = null;
while (inputStream.available() > 0) {
String chunkHeader = readHeader(inputStream);
int chunkLength = readInt(inputStream);
byte[] chunkData = readBytes(inputStream, chunkLength);
switch (chunkHeader) {
case NAME_ID -> {
if (className != null) {
throw new IOException("Duplicate name chunk");
}
className = new String(chunkData, StandardCharsets.UTF_8);
}
case SOURCES_ID -> {
if (sources != null) {
throw new IOException("Duplicate sources chunk");
}
sources = new String(chunkData, StandardCharsets.UTF_8);
}
case LINE_NUMBERS_ID -> {
if (lineNumbers != null) {
throw new IOException("Duplicate line numbers chunk");
}
try (var br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(chunkData), StandardCharsets.UTF_8))) {
ClassLineNumbers classLineNumbers = ClassLineNumbers.readMappings(br);
if (classLineNumbers.lineMap().size() != 1) {
throw new IOException("Expected exactly one class line numbers entry got " + classLineNumbers.lineMap().size() + " entries");
}
lineNumbers = classLineNumbers.lineMap().values().iterator().next();
}
}
default -> {
// Skip unknown chunk
LOGGER.warn("Skipping unknown chunk: {} of size {}", chunkHeader, chunkLength);
inputStream.skip(chunkLength);
}
}
}
if (sources == null) {
throw new IOException("Missing sources");
}
return new CachedData(className, sources, lineNumbers);
}
private static String readHeader(InputStream inputStream) throws IOException {
byte[] header = readBytes(inputStream, 4);
return new String(header, StandardCharsets.US_ASCII);
}
private static int readInt(InputStream inputStream) throws IOException {
byte[] bytes = readBytes(inputStream, 4);
return ByteBuffer.wrap(bytes).getInt();
}
private static byte[] readBytes(InputStream inputStream, int length) throws IOException {
byte[] bytes = new byte[length];
int read = inputStream.read(bytes);
if (read != length) {
throw new IOException("Failed to read bytes expected " + length + " bytes but got " + read + " bytes");
}
return bytes;
}
static class EntrySerializer implements CachedFileStore.EntrySerializer<CachedData> {
@Override
public CachedData read(Path path) throws IOException {
try (var inputStream = new BufferedInputStream(Files.newInputStream(path))) {
return CachedData.read(inputStream);
}
}
@Override
public void write(CachedData entry, Path path) throws IOException {
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
entry.write(fileChannel);
}
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.file.Path;
import org.jetbrains.annotations.Nullable;
public interface CachedFileStore<T> {
@Nullable T getEntry(String key) throws IOException;
void putEntry(String key, T entry) throws IOException;
interface EntrySerializer<T> {
T read(Path path) throws IOException;
void write(T entry, Path path) throws IOException;
}
}

View File

@@ -0,0 +1,141 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
public record CachedFileStoreImpl<T>(Path root, EntrySerializer<T> entrySerializer, CacheRules cacheRules) implements CachedFileStore<T> {
public CachedFileStoreImpl {
Objects.requireNonNull(root, "root");
}
@Override
public @Nullable T getEntry(String key) throws IOException {
Path path = resolve(key);
if (Files.notExists(path)) {
return null;
}
// Update last modified, so recently used files stay in the cache
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
return entrySerializer.read(path);
}
@Override
public void putEntry(String key, T data) throws IOException {
Path path = resolve(key);
Files.createDirectories(path.getParent());
entrySerializer.write(data, path);
}
private Path resolve(String key) {
return root.resolve(key);
}
public void prune() throws IOException {
// Sorted oldest -> newest
List<PathEntry> entries = new ArrayList<>();
// Iterate over all the files in the cache, and store them into the sorted list.
try (Stream<Path> walk = Files.walk(root)) {
Iterator<Path> iterator = walk.iterator();
while (iterator.hasNext()) {
final Path entry = iterator.next();
if (!Files.isRegularFile(entry)) {
continue;
}
insertSorted(entries, new PathEntry(entry));
}
}
// Delete the oldest files to get under the max file limit
if (entries.size() > cacheRules.maxFiles) {
for (int i = 0; i < cacheRules.maxFiles; i++) {
PathEntry toRemove = entries.remove(0);
Files.delete(toRemove.path);
}
}
final Instant maxAge = Instant.now().minus(cacheRules().maxAge());
Iterator<PathEntry> iterator = entries.iterator();
while (iterator.hasNext()) {
final PathEntry entry = iterator.next();
if (entry.lastModified().toInstant().isAfter(maxAge)) {
// File is not longer than the max age
// As this is a sorted list we don't need to keep checking
break;
}
// Remove all files over the max age
iterator.remove();
Files.delete(entry.path);
}
}
private void insertSorted(List<PathEntry> list, PathEntry entry) {
int index = Collections.binarySearch(list, entry, Comparator.comparing(PathEntry::lastModified));
if (index < 0) {
index = -index - 1;
}
list.add(index, entry);
}
/**
* The rules for the cache.
*
* @param maxFiles The maximum number of files in the cache
* @param maxAge The maximum age of a file in the cache
*/
public record CacheRules(long maxFiles, Duration maxAge) {
}
record PathEntry(Path path, FileTime lastModified) {
PathEntry(Path path) throws IOException {
this(path, Files.getLastModifiedTime(path));
}
}
}

View File

@@ -0,0 +1,268 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.decompilers.ClassLineNumbers;
import net.fabricmc.loom.util.FileSystemUtil;
public record CachedJarProcessor(CachedFileStore<CachedData> fileStore, String baseHash) {
private static final Logger LOGGER = LoggerFactory.getLogger(CachedJarProcessor.class);
public WorkRequest prepareJob(Path inputJar) throws IOException {
boolean isIncomplete = false;
boolean hasSomeExisting = false;
Path incompleteJar = Files.createTempFile("loom-cache-incomplete", ".jar");
Path existingJar = Files.createTempFile("loom-cache-existing", ".jar");
// We must delete the empty files, so they can be created as a zip
Files.delete(incompleteJar);
Files.delete(existingJar);
// Sources name -> hash
Map<String, String> outputNameMap = new HashMap<>();
Map<String, ClassLineNumbers.Entry> lineNumbersMap = new HashMap<>();
int hits = 0;
int misses = 0;
try (FileSystemUtil.Delegate inputFs = FileSystemUtil.getJarFileSystem(inputJar, false);
FileSystemUtil.Delegate incompleteFs = FileSystemUtil.getJarFileSystem(incompleteJar, true);
FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(existingJar, true)) {
final List<ClassEntry> inputClasses = JarWalker.findClasses(inputFs);
for (ClassEntry entry : inputClasses) {
String outputFileName = entry.sourcesFileName();
String fullHash = baseHash + "/" + entry.hash(inputFs.getRoot());
final CachedData entryData = fileStore.getEntry(fullHash);
if (entryData == null) {
// Cached entry was not found, so copy the input to the incomplete jar to be processed
entry.copyTo(inputFs.getRoot(), incompleteFs.getRoot());
isIncomplete = true;
outputNameMap.put(outputFileName, fullHash);
LOGGER.debug("Cached entry ({}) not found, going to process {}", fullHash, outputFileName);
misses++;
} else {
final Path outputPath = existingFs.getPath(outputFileName);
Files.createDirectories(outputPath.getParent());
Files.writeString(outputPath, entryData.sources());
lineNumbersMap.put(entryData.className(), entryData.lineNumbers());
hasSomeExisting = true;
LOGGER.debug("Cached entry ({}) found: {}", fullHash, outputFileName);
hits++;
}
}
}
// A jar file that will be created by the work action, containing the newly processed items.
Path outputJar = Files.createTempFile("loom-cache-output", ".jar");
Files.delete(outputJar);
final ClassLineNumbers lineNumbers = lineNumbersMap.isEmpty() ? null : new ClassLineNumbers(Collections.unmodifiableMap(lineNumbersMap));
final var stats = new CacheStats(hits, misses);
if (isIncomplete && !hasSomeExisting) {
// The cache contained nothing of use, fully process the input jar
Files.delete(incompleteJar);
Files.delete(existingJar);
LOGGER.info("No cached entries found, going to process the whole jar");
return new FullWorkJob(inputJar, outputJar, outputNameMap)
.asRequest(stats, lineNumbers);
} else if (isIncomplete) {
// The cache did not contain everything so we have some work to do
LOGGER.info("Some cached entries found, using partial work job");
return new PartialWorkJob(incompleteJar, existingJar, outputJar, outputNameMap)
.asRequest(stats, lineNumbers);
} else {
// The cached contained everything we need, so the existing jar is the output
LOGGER.info("All cached entries found, using completed work job");
Files.delete(incompleteJar);
return new CompletedWorkJob(existingJar)
.asRequest(stats, lineNumbers);
}
}
public void completeJob(Path output, WorkJob workJob, ClassLineNumbers lineNumbers) throws IOException {
if (workJob instanceof CompletedWorkJob completedWorkJob) {
// Fully complete, nothing new to cache
Files.move(completedWorkJob.completed(), output);
return;
}
// Work has been done, we need to cache the newly processed items
if (workJob instanceof WorkToDoJob workToDoJob) {
// Sources name -> hash
Map<String, String> outputNameMap = workToDoJob.outputNameMap();
try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(workToDoJob.output(), false);
Stream<Path> walk = Files.walk(outputFs.getRoot())) {
Iterator<Path> iterator = walk.iterator();
while (iterator.hasNext()) {
final Path fsPath = iterator.next();
if (fsPath.startsWith("/META-INF/")) {
continue;
}
if (!Files.isRegularFile(fsPath)) {
continue;
}
final String hash = outputNameMap.get(fsPath.toString().substring(outputFs.getRoot().toString().length()));
if (hash == null) {
throw new IllegalStateException("Unexpected output: " + fsPath);
}
// Trim the leading / and the .java extension
final String className = fsPath.toString().substring(1, fsPath.toString().length() - ".java".length());
final String sources = Files.readString(fsPath);
ClassLineNumbers.Entry lineMapEntry = null;
if (lineNumbers != null) {
lineMapEntry = lineNumbers.lineMap().get(className);
}
final var cachedData = new CachedData(className, sources, lineMapEntry);
fileStore.putEntry(hash, cachedData);
LOGGER.debug("Saving processed entry ({}) to cache: {}", hash, fsPath);
}
}
} else {
throw new IllegalStateException();
}
if (workJob instanceof PartialWorkJob partialWorkJob) {
// Copy all the existing items to the output jar
try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(partialWorkJob.output(), false);
FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(partialWorkJob.existing(), false);
Stream<Path> walk = Files.walk(existingFs.getRoot())) {
Iterator<Path> iterator = walk.iterator();
while (iterator.hasNext()) {
Path existingPath = iterator.next();
if (!Files.isRegularFile(existingPath)) {
continue;
}
final Path outputPath = outputFs.getRoot().resolve(existingPath.toString());
LOGGER.debug("Copying existing entry to output: {}", existingPath);
Files.createDirectories(outputPath.getParent());
Files.copy(existingPath, outputPath);
}
}
Files.delete(partialWorkJob.existing());
Files.move(partialWorkJob.output(), output);
} else if (workJob instanceof FullWorkJob fullWorkJob) {
// Nothing to merge, just use the output jar
Files.move(fullWorkJob.output, output);
} else {
throw new IllegalStateException();
}
}
public record WorkRequest(WorkJob job, CacheStats stats, @Nullable ClassLineNumbers lineNumbers) {
}
public record CacheStats(int hits, int misses) {
}
public sealed interface WorkJob permits CompletedWorkJob, WorkToDoJob {
default WorkRequest asRequest(CacheStats stats, @Nullable ClassLineNumbers lineNumbers) {
return new WorkRequest(this, stats, lineNumbers);
}
}
public sealed interface WorkToDoJob extends WorkJob permits PartialWorkJob, FullWorkJob {
/**
* A path to jar file containing all the classes to be processed.
*/
Path incomplete();
/**
* @return A jar file to be written to during processing
*/
Path output();
/**
* @return A map of sources name to hash
*/
Map<String, String> outputNameMap();
}
/**
* No work to be done, all restored from cache.
*
* @param completed
*/
public record CompletedWorkJob(Path completed) implements WorkJob {
}
/**
* Some work needs to be done.
*
* @param incomplete A path to jar file containing all the classes to be processed
* @param existing A path pointing to a jar containing existing classes that have previously been processed
* @param output A path to a temporary jar where work output should be written to
* @param outputNameMap A map of sources name to hash
*/
public record PartialWorkJob(Path incomplete, Path existing, Path output, Map<String, String> outputNameMap) implements WorkToDoJob {
}
/**
* The full jar must be processed.
*
* @param incomplete A path to jar file containing all the classes to be processed
* @param output A path to a temporary jar where work output should be written to
* @param outputNameMap A map of sources name to hash
*/
public record FullWorkJob(Path incomplete, Path output, Map<String, String> outputNameMap) implements WorkToDoJob {
}
}

View File

@@ -0,0 +1,75 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.StringJoiner;
import net.fabricmc.loom.util.Checksum;
public record ClassEntry(String parentClass, List<String> innerClasses) {
/**
* Copy the class and its inner classes to the target root.
* @param sourceRoot The root of the source jar
* @param targetRoot The root of the target jar
*
* @throws IOException If an error occurs while copying the files
*/
public void copyTo(Path sourceRoot, Path targetRoot) throws IOException {
Path targetPath = targetRoot.resolve(parentClass);
Files.createDirectories(targetPath.getParent());
Files.copy(sourceRoot.resolve(parentClass), targetPath);
for (String innerClass : innerClasses) {
Files.copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass));
}
}
/**
* Hash the class and its inner classes using sha256.
* @param root The root of the jar
* @return The hash of the class and its inner classes
*
* @throws IOException If an error occurs while hashing the files
*/
public String hash(Path root) throws IOException {
StringJoiner joiner = new StringJoiner(",");
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(parentClass))));
for (String innerClass : innerClasses) {
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(innerClass))));
}
return Checksum.sha256Hex(joiner.toString().getBytes());
}
public String sourcesFileName() {
return parentClass.replace(".class", ".java");
}
}

View File

@@ -0,0 +1,108 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.util.FileSystemUtil;
public final class JarWalker {
private static final Logger LOGGER = LoggerFactory.getLogger(JarWalker.class);
private JarWalker() {
}
public static List<ClassEntry> findClasses(Path jar) throws IOException {
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) {
return findClasses(fs);
}
}
public static List<ClassEntry> findClasses(FileSystemUtil.Delegate fs) throws IOException {
List<String> outerClasses = new ArrayList<>();
Map<String, List<String>> innerClasses = new HashMap<>();
// Iterate over all the classes in the jar, and store them into the sorted list.
try (Stream<Path> walk = Files.walk(fs.getRoot())) {
Iterator<Path> iterator = walk.iterator();
while (iterator.hasNext()) {
final Path entry = iterator.next();
if (!Files.isRegularFile(entry)) {
continue;
}
final String fileName = entry.toString().substring(fs.getRoot().toString().length());
if (!fileName.endsWith(".class")) {
continue;
}
boolean isInnerClass = fileName.contains("$");
if (isInnerClass) {
String outerClassName = fileName.substring(0, fileName.indexOf('$')) + ".class";
innerClasses.computeIfAbsent(outerClassName, k -> new ArrayList<>()).add(fileName);
} else {
outerClasses.add(fileName);
}
}
}
LOGGER.info("Found {} outer classes and {} inner classes", outerClasses.size(), innerClasses.size());
Collections.sort(outerClasses);
List<ClassEntry> classEntries = new ArrayList<>();
for (String outerClass : outerClasses) {
List<String> innerClasList = innerClasses.get(outerClass);
if (innerClasList == null) {
innerClasList = Collections.emptyList();
} else {
Collections.sort(innerClasList);
}
ClassEntry classEntry = new ClassEntry(outerClass, Collections.unmodifiableList(innerClasList));
classEntries.add(classEntry);
}
return Collections.unmodifiableList(classEntries);
}
}

View File

@@ -0,0 +1,69 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.cache;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
/**
* Write a RIFF chunk to a file channel
*
* <p>Works by writing the chunk header and then reserving space for the chunk size.
* The chunk size is then written after the chunk data has been written.
*/
public class RiffChunk implements AutoCloseable {
private final long position;
private final FileChannel fileChannel;
public RiffChunk(String id, FileChannel fileChannel) throws IOException {
if (id.length() != 4) {
throw new IllegalArgumentException("ID must be 4 characters long");
}
// Write the chunk header and reserve space for the chunk size
fileChannel.write(ByteBuffer.wrap(id.getBytes(StandardCharsets.US_ASCII)));
this.position = fileChannel.position();
fileChannel.write(ByteBuffer.allocate(4));
// Store the position and file channel for later use
this.fileChannel = fileChannel;
}
@Override
public void close() throws IOException {
long endPosition = fileChannel.position();
long chunkSize = endPosition - position - 4;
if (chunkSize > Integer.MAX_VALUE) {
throw new IOException("Chunk size is too large");
}
fileChannel.position(position);
fileChannel.write(ByteBuffer.allocate(Integer.BYTES).putInt((int) (chunkSize)).flip());
fileChannel.position(endPosition);
}
}

View File

@@ -50,5 +50,6 @@ public interface LoomFiles {
File getRemapClasspathFile(); File getRemapClasspathFile();
File getGlobalMinecraftRepo(); File getGlobalMinecraftRepo();
File getLocalMinecraftRepo(); File getLocalMinecraftRepo();
File getDecompileCache(String version);
File getForgeDependencyRepo(); File getForgeDependencyRepo();
} }

View File

@@ -108,6 +108,11 @@ public abstract class LoomFilesBaseImpl implements LoomFiles {
return new File(getRootProjectPersistentCache(), "minecraftMaven"); return new File(getRootProjectPersistentCache(), "minecraftMaven");
} }
@Override
public File getDecompileCache(String version) {
return new File(getUserCache(), "decompile/" + version + ".zip");
}
@Override @Override
public File getForgeDependencyRepo() { public File getForgeDependencyRepo() {
return new File(getUserCache(), "forge/transformed-dependencies-v1"); return new File(getUserCache(), "forge/transformed-dependencies-v1");

View File

@@ -44,6 +44,7 @@ import org.gradle.api.Project;
import org.gradle.api.UncheckedIOException; import org.gradle.api.UncheckedIOException;
import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty; import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory; import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.ListProperty;
@@ -63,6 +64,7 @@ import net.fabricmc.loom.api.NeoForgeExtensionAPI;
import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.api.decompilers.DecompilerOptions; import net.fabricmc.loom.api.decompilers.DecompilerOptions;
import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.api.mappings.layered.spec.LayeredMappingSpecBuilder; import net.fabricmc.loom.api.mappings.layered.spec.LayeredMappingSpecBuilder;
import net.fabricmc.loom.api.processor.MinecraftJarProcessor; import net.fabricmc.loom.api.processor.MinecraftJarProcessor;
import net.fabricmc.loom.api.remapping.RemapperExtension; import net.fabricmc.loom.api.remapping.RemapperExtension;
@@ -103,7 +105,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
protected final Property<IntermediateMappingsProvider> intermediateMappingsProvider; protected final Property<IntermediateMappingsProvider> intermediateMappingsProvider;
private final Property<Boolean> runtimeOnlyLog4j; private final Property<Boolean> runtimeOnlyLog4j;
private final Property<Boolean> splitModDependencies; private final Property<Boolean> splitModDependencies;
private final Property<MinecraftJarConfiguration> minecraftJarConfiguration; private final Property<MinecraftJarConfiguration<?, ?, ?>> minecraftJarConfiguration;
private final Property<Boolean> splitEnvironmentalSourceSet; private final Property<Boolean> splitEnvironmentalSourceSet;
private final InterfaceInjectionExtensionAPI interfaceInjectionExtension; private final InterfaceInjectionExtensionAPI interfaceInjectionExtension;
@@ -165,7 +167,8 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
this.minecraftJarProcessors = (ListProperty<MinecraftJarProcessor<?>>) (Object) project.getObjects().listProperty(MinecraftJarProcessor.class); this.minecraftJarProcessors = (ListProperty<MinecraftJarProcessor<?>>) (Object) project.getObjects().listProperty(MinecraftJarProcessor.class);
this.minecraftJarProcessors.finalizeValueOnRead(); this.minecraftJarProcessors.finalizeValueOnRead();
this.minecraftJarConfiguration = project.getObjects().property(MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED); //noinspection unchecked
this.minecraftJarConfiguration = project.getObjects().property((Class<MinecraftJarConfiguration<?, ?, ?>>) (Class<?>) MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED);
this.minecraftJarConfiguration.finalizeValueOnRead(); this.minecraftJarConfiguration.finalizeValueOnRead();
this.accessWidener.finalizeValueOnRead(); this.accessWidener.finalizeValueOnRead();
@@ -384,7 +387,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
} }
@Override @Override
public Property<MinecraftJarConfiguration> getMinecraftJarConfiguration() { public Property<MinecraftJarConfiguration<?, ?, ?>> getMinecraftJarConfiguration() {
return minecraftJarConfiguration; return minecraftJarConfiguration;
} }
@@ -477,6 +480,13 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
return getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftProvider().minecraftVersion()); return getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftProvider().minecraftVersion());
} }
@Override
public FileCollection getNamedMinecraftJars() {
final ConfigurableFileCollection jars = getProject().getObjects().fileCollection();
jars.from(getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftJars(MappingsNamespace.NAMED)));
return jars;
}
@Override @Override
public void silentMojangMappingsLicense() { public void silentMojangMappingsLicense() {
this.silentMojangMappingsLicense = true; this.silentMojangMappingsLicense = true;

View File

@@ -53,6 +53,7 @@ import net.fabricmc.loom.configuration.providers.forge.ForgeRunsProvider;
import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider; import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider;
import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory;
import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration;
import net.fabricmc.loom.configuration.providers.mappings.NoOpIntermediateMappingsProvider;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessorManager; import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessorManager;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider;
@@ -186,6 +187,11 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
this.intermediaryMinecraftProvider = intermediaryMinecraftProvider; this.intermediaryMinecraftProvider = intermediaryMinecraftProvider;
} }
@Override
public void noIntermediateMappings() {
setIntermediateMappingsProvider(NoOpIntermediateMappingsProvider.class, p -> { });
}
@Override @Override
public SrgMinecraftProvider<?> getSrgMinecraftProvider() { public SrgMinecraftProvider<?> getSrgMinecraftProvider() {
return Objects.requireNonNull(srgMinecraftProvider, "Cannot get SrgMinecraftProvider before it has been setup"); return Objects.requireNonNull(srgMinecraftProvider, "Cannot get SrgMinecraftProvider before it has been setup");
@@ -312,6 +318,9 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
provider.getDownloader().set(this::download); provider.getDownloader().set(this::download);
provider.getDownloader().disallowChanges(); provider.getDownloader().disallowChanges();
provider.getIsLegacyMinecraft().set(getProject().provider(() -> getMinecraftProvider().isLegacyVersion()));
provider.getIsLegacyMinecraft().disallowChanges();
} }
@Override @Override

View File

@@ -1,271 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2023 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.kotlin.remapping;
import java.util.List;
import kotlinx.metadata.KmAnnotation;
import kotlinx.metadata.KmProperty;
import kotlinx.metadata.internal.extensions.KmClassExtension;
import kotlinx.metadata.internal.extensions.KmConstructorExtension;
import kotlinx.metadata.internal.extensions.KmFunctionExtension;
import kotlinx.metadata.internal.extensions.KmPackageExtension;
import kotlinx.metadata.internal.extensions.KmPropertyExtension;
import kotlinx.metadata.internal.extensions.KmTypeExtension;
import kotlinx.metadata.internal.extensions.KmTypeParameterExtension;
import kotlinx.metadata.jvm.JvmFieldSignature;
import kotlinx.metadata.jvm.JvmMethodSignature;
import kotlinx.metadata.jvm.internal.JvmClassExtension;
import kotlinx.metadata.jvm.internal.JvmConstructorExtension;
import kotlinx.metadata.jvm.internal.JvmFunctionExtension;
import kotlinx.metadata.jvm.internal.JvmPackageExtension;
import kotlinx.metadata.jvm.internal.JvmPropertyExtension;
import kotlinx.metadata.jvm.internal.JvmTypeExtension;
import kotlinx.metadata.jvm.internal.JvmTypeParameterExtension;
import org.jetbrains.annotations.Nullable;
/*
* This is a fun meme. All of these kotlin classes are marked as "internal" so Kotlin code cannot compile against them.
* However, luckily for us the Java compiler has no idea about this, so they can compile against it :D
*
* This file contains Java wrappers around Kotlin classes, to used by Kotlin.
*/
public interface JvmExtensionWrapper {
record Class(JvmClassExtension extension) implements JvmExtensionWrapper {
@Nullable
public static Class get(KmClassExtension classExtension) {
if (classExtension instanceof JvmClassExtension jvmClassExtension) {
return new Class(jvmClassExtension);
}
return null;
}
public List<KmProperty> getLocalDelegatedProperties() {
return extension.getLocalDelegatedProperties();
}
@Nullable
public String getModuleName() {
return extension.getModuleName();
}
public void setModuleName(@Nullable String name) {
extension.setModuleName(name);
}
@Nullable
public String getAnonymousObjectOriginName() {
return extension.getAnonymousObjectOriginName();
}
public void setAnonymousObjectOriginName(@Nullable String name) {
extension.setAnonymousObjectOriginName(name);
}
public int getJvmFlags() {
return extension.getJvmFlags();
}
public void setJvmFlags(int flags) {
extension.setJvmFlags(flags);
}
}
record Package(JvmPackageExtension extension) {
@Nullable
public static Package get(KmPackageExtension packageExtension) {
if (packageExtension instanceof JvmPackageExtension jvmPackageExtension) {
return new Package(jvmPackageExtension);
}
return null;
}
public List<KmProperty> getLocalDelegatedProperties() {
return extension.getLocalDelegatedProperties();
}
@Nullable
public String getModuleName() {
return extension.getModuleName();
}
public void setModuleName(@Nullable String name) {
extension.setModuleName(name);
}
}
record Function(JvmFunctionExtension extension) {
@Nullable
public static Function get(KmFunctionExtension functionExtension) {
if (functionExtension instanceof JvmFunctionExtension jvmFunctionExtension) {
return new Function(jvmFunctionExtension);
}
return null;
}
@Nullable
public JvmMethodSignature getSignature() {
return extension.getSignature();
}
public void setSignature(@Nullable JvmMethodSignature signature) {
extension.setSignature(signature);
}
@Nullable
public String getLambdaClassOriginName() {
return extension.getLambdaClassOriginName();
}
public void setLambdaClassOriginName(@Nullable String name) {
extension.setLambdaClassOriginName(name);
}
}
record Property(JvmPropertyExtension extension) {
@Nullable
public static Property get(KmPropertyExtension propertyExtension) {
if (propertyExtension instanceof JvmPropertyExtension jvmPropertyExtension) {
return new Property(jvmPropertyExtension);
}
return null;
}
public int getJvmFlags() {
return extension.getJvmFlags();
}
public void setJvmFlags(int flags) {
extension.setJvmFlags(flags);
}
@Nullable
public JvmFieldSignature getFieldSignature() {
return extension.getFieldSignature();
}
public void setFieldSignature(@Nullable JvmFieldSignature signature) {
extension.setFieldSignature(signature);
}
@Nullable
public JvmMethodSignature getGetterSignature() {
return extension.getGetterSignature();
}
public void setGetterSignature(@Nullable JvmMethodSignature signature) {
extension.setGetterSignature(signature);
}
@Nullable
public JvmMethodSignature getSetterSignature() {
return extension.getSetterSignature();
}
public void setSetterSignature(@Nullable JvmMethodSignature signature) {
extension.setSetterSignature(signature);
}
@Nullable
public JvmMethodSignature getSyntheticMethodForAnnotations() {
return extension.getSyntheticMethodForAnnotations();
}
public void setSyntheticMethodForAnnotations(@Nullable JvmMethodSignature signature) {
extension.setSyntheticMethodForAnnotations(signature);
}
@Nullable
public JvmMethodSignature getSyntheticMethodForDelegate() {
return extension.getSyntheticMethodForDelegate();
}
public void setSyntheticMethodForDelegate(@Nullable JvmMethodSignature signature) {
extension.setSyntheticMethodForDelegate(signature);
}
}
record Constructor(JvmConstructorExtension extension) {
@Nullable
public static Constructor get(KmConstructorExtension constructorExtension) {
if (constructorExtension instanceof JvmConstructorExtension jvmConstructorExtension) {
return new Constructor(jvmConstructorExtension);
}
return null;
}
@Nullable
public JvmMethodSignature getSignature() {
return extension.getSignature();
}
public void setSignature(@Nullable JvmMethodSignature signature) {
extension.setSignature(signature);
}
}
record TypeParameter(JvmTypeParameterExtension extension) {
@Nullable
public static TypeParameter get(KmTypeParameterExtension typeParameterExtension) {
if (typeParameterExtension instanceof JvmTypeParameterExtension jvmTypeParameterExtension) {
return new TypeParameter(jvmTypeParameterExtension);
}
return null;
}
public List<KmAnnotation> getAnnotations() {
return extension.getAnnotations();
}
}
record Type(JvmTypeExtension extension) {
@Nullable
public static Type get(KmTypeExtension typeExtension) {
if (typeExtension instanceof JvmTypeExtension jvmTypeExtension) {
return new Type(jvmTypeExtension);
}
return null;
}
public boolean isRaw() {
return extension.isRaw();
}
public void setRaw(boolean raw) {
extension.setRaw(raw);
}
public List<KmAnnotation> getAnnotations() {
return extension.getAnnotations();
}
}
}

View File

@@ -40,6 +40,7 @@ import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.services.ServiceReference; import org.gradle.api.services.ServiceReference;
import org.gradle.api.specs.Spec; import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.JavaExec;
@@ -52,7 +53,7 @@ import net.fabricmc.loom.util.gradle.SyncTaskBuildService;
public abstract class AbstractRunTask extends JavaExec { public abstract class AbstractRunTask extends JavaExec {
private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder(); private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
private final RunConfig config; private final Provider<RunConfig> config;
// We control the classpath, as we use a ArgFile to pass it over the command line: https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile // We control the classpath, as we use a ArgFile to pass it over the command line: https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile
private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection(); private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection();
@@ -63,13 +64,12 @@ public abstract class AbstractRunTask extends JavaExec {
public AbstractRunTask(Function<Project, RunConfig> configProvider) { public AbstractRunTask(Function<Project, RunConfig> configProvider) {
super(); super();
setGroup(Constants.TaskGroup.FABRIC); setGroup(Constants.TaskGroup.FABRIC);
this.config = configProvider.apply(getProject());
setClasspath(config.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter())); this.config = getProject().provider(() -> configProvider.apply(getProject()));
args(config.programArgs);
getMainClass().set(config.mainClass);
classpath.from(config.map(runConfig -> runConfig.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter())));
getArgumentProviders().add(() -> config.get().programArgs);
getMainClass().set(config.map(runConfig -> runConfig.mainClass));
getJvmArguments().addAll(getProject().provider(this::getGameJvmArgs)); getJvmArguments().addAll(getProject().provider(this::getGameJvmArgs));
} }
@@ -100,8 +100,8 @@ public abstract class AbstractRunTask extends JavaExec {
super.setClasspath(classpath); super.setClasspath(classpath);
} }
setWorkingDir(new File(getProject().getProjectDir(), config.runDir)); setWorkingDir(new File(getProject().getProjectDir(), config.get().runDir));
environment(config.environmentVariables); environment(config.get().environmentVariables);
super.exec(); super.exec();
} }
@@ -133,7 +133,7 @@ public abstract class AbstractRunTask extends JavaExec {
} }
} }
args.addAll(config.vmArgs); args.addAll(config.get().vmArgs);
return args; return args;
} }
@@ -204,11 +204,11 @@ public abstract class AbstractRunTask extends JavaExec {
@Override @Override
public boolean isSatisfiedBy(File element) { public boolean isSatisfiedBy(File element) {
if (excludedLibraryPaths == null) { if (excludedLibraryPaths == null) {
excludedLibraryPaths = config.getExcludedLibraryPaths(getProject()); excludedLibraryPaths = config.get().getExcludedLibraryPaths(getProject());
} }
if (excludedLibraryPaths.contains(element.getAbsolutePath())) { if (excludedLibraryPaths.contains(element.getAbsolutePath())) {
getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.configName); getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.get().configName);
return false; return false;
} }

View File

@@ -24,6 +24,7 @@
package net.fabricmc.loom.task; package net.fabricmc.loom.task;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -36,11 +37,15 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.StringJoiner;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -54,9 +59,11 @@ import org.gradle.api.services.ServiceReference;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option;
import org.gradle.process.ExecOperations; import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult; import org.gradle.process.ExecResult;
import org.gradle.work.DisableCachingByDefault; import org.gradle.work.DisableCachingByDefault;
@@ -65,8 +72,12 @@ import org.gradle.workers.WorkParameters;
import org.gradle.workers.WorkQueue; import org.gradle.workers.WorkQueue;
import org.gradle.workers.WorkerExecutor; import org.gradle.workers.WorkerExecutor;
import org.gradle.workers.internal.WorkerDaemonClientsManager; import org.gradle.workers.internal.WorkerDaemonClientsManager;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.decompilers.DecompilationMetadata; import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
import net.fabricmc.loom.api.decompilers.DecompilerOptions; import net.fabricmc.loom.api.decompilers.DecompilerOptions;
import net.fabricmc.loom.api.decompilers.LoomDecompiler; import net.fabricmc.loom.api.decompilers.LoomDecompiler;
@@ -77,9 +88,14 @@ import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider;
import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper; import net.fabricmc.loom.configuration.sources.ForgeSourcesRemapper;
import net.fabricmc.loom.decompilers.ClassLineNumbers;
import net.fabricmc.loom.decompilers.LineNumberRemapper; import net.fabricmc.loom.decompilers.LineNumberRemapper;
import net.fabricmc.loom.decompilers.cache.CachedData;
import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl;
import net.fabricmc.loom.decompilers.cache.CachedJarProcessor;
import net.fabricmc.loom.decompilers.linemap.LineMapClassFilter; import net.fabricmc.loom.decompilers.linemap.LineMapClassFilter;
import net.fabricmc.loom.decompilers.linemap.LineMapVisitor; import net.fabricmc.loom.decompilers.linemap.LineMapVisitor;
import net.fabricmc.loom.util.Checksum;
import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.ExceptionUtil;
import net.fabricmc.loom.util.FileSystemUtil; import net.fabricmc.loom.util.FileSystemUtil;
@@ -99,6 +115,8 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree;
@DisableCachingByDefault @DisableCachingByDefault
public abstract class GenerateSourcesTask extends AbstractLoomTask { public abstract class GenerateSourcesTask extends AbstractLoomTask {
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSourcesTask.class);
private static final String CACHE_VERSION = "v1";
private final DecompilerOptions decompilerOptions; private final DecompilerOptions decompilerOptions;
/** /**
@@ -126,10 +144,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
@Optional @Optional
public abstract ConfigurableFileCollection getUnpickClasspath(); public abstract ConfigurableFileCollection getUnpickClasspath();
@InputFiles
@Optional
@ApiStatus.Internal
public abstract ConfigurableFileCollection getUnpickRuntimeClasspath();
@OutputFile @OutputFile
@Optional @Optional
public abstract RegularFileProperty getUnpickOutputJar(); public abstract RegularFileProperty getUnpickOutputJar();
@Input
@Option(option = "use-cache", description = "Use the decompile cache")
@ApiStatus.Experimental
public abstract Property<Boolean> getUseCache();
// Internal outputs
@ApiStatus.Internal
@Internal
protected abstract RegularFileProperty getDecompileCacheFile();
// Injects // Injects
@Inject @Inject
public abstract WorkerExecutor getWorkerExecutor(); public abstract WorkerExecutor getWorkerExecutor();
@@ -151,6 +184,12 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
getOutputs().upToDateWhen((o) -> false); getOutputs().upToDateWhen((o) -> false);
getClasspath().from(decompilerOptions.getClasspath()).finalizeValueOnRead(); getClasspath().from(decompilerOptions.getClasspath()).finalizeValueOnRead();
dependsOn(decompilerOptions.getClasspath().getBuiltBy()); dependsOn(decompilerOptions.getClasspath().getBuiltBy());
LoomGradleExtension extension = LoomGradleExtension.get(getProject());
getDecompileCacheFile().set(extension.getFiles().getDecompileCache(CACHE_VERSION));
getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH));
getUseCache().convention(true);
} }
@TaskAction @TaskAction
@@ -161,21 +200,197 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements."); throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements.");
} }
if (!getUseCache().get()) {
try (var timer = new Timer("Decompiled sources")) {
runWithoutCache();
}
return;
}
LOGGER.info("Using decompile cache.");
try (var timer = new Timer("Decompiled sources with cache")) {
final Path cacheFile = getDecompileCacheFile().getAsFile().get().toPath();
// TODO ensure we have a lock on this file to prevent multiple tasks from running at the same time
// TODO handle being unable to read the cache file
Files.createDirectories(cacheFile.getParent());
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(cacheFile, true)) {
runWithCache(fs.getRoot());
}
}
}
private void runWithCache(Path cacheRoot) throws IOException {
final MinecraftJar minecraftJar = rebuildInputJar(); final MinecraftJar minecraftJar = rebuildInputJar();
// Input jar is the jar to decompile, this may be unpicked. final var cacheRules = new CachedFileStoreImpl.CacheRules(50_000, Duration.ofDays(90));
final var decompileCache = new CachedFileStoreImpl<>(cacheRoot, CachedData.SERIALIZER, cacheRules);
final String cacheKey = getCacheKey();
final CachedJarProcessor cachedJarProcessor = new CachedJarProcessor(decompileCache, cacheKey);
final CachedJarProcessor.WorkRequest workRequest;
LOGGER.info("Decompile cache key: {}", cacheKey);
try (var timer = new Timer("Prepare job")) {
workRequest = cachedJarProcessor.prepareJob(minecraftJar.getPath());
}
final CachedJarProcessor.WorkJob job = workRequest.job();
final CachedJarProcessor.CacheStats cacheStats = workRequest.stats();
getProject().getLogger().lifecycle("Decompile cache stats: {} hits, {} misses", cacheStats.hits(), cacheStats.misses());
ClassLineNumbers outputLineNumbers = null;
if (job instanceof CachedJarProcessor.WorkToDoJob workToDoJob) {
Path inputJar = workToDoJob.incomplete();
@Nullable Path existing = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existing() : null;
if (getUnpickDefinitions().isPresent()) {
try (var timer = new Timer("Unpick")) {
inputJar = unpickJar(inputJar, existing);
}
}
try (var timer = new Timer("Decompile")) {
outputLineNumbers = runDecompileJob(inputJar, workToDoJob.output(), existing);
outputLineNumbers = filterForgeLineNumbers(outputLineNumbers);
}
if (Files.notExists(workToDoJob.output())) {
throw new RuntimeException("Failed to decompile sources");
}
} else if (job instanceof CachedJarProcessor.CompletedWorkJob completedWorkJob) {
// Nothing to do :)
}
// The final output sources jar
final Path sourcesJar = getOutputJar().get().getAsFile().toPath();
Files.deleteIfExists(sourcesJar);
try (var timer = new Timer("Complete job")) {
cachedJarProcessor.completeJob(sourcesJar, job, outputLineNumbers);
}
// This is the minecraft jar used at runtime.
final Path classesJar = minecraftJar.getPath();
// Remap the line numbers with the new and existing numbers
final ClassLineNumbers existingLinenumbers = workRequest.lineNumbers();
final ClassLineNumbers lineNumbers = ClassLineNumbers.merge(existingLinenumbers, outputLineNumbers);
if (lineNumbers == null) {
LOGGER.info("No line numbers to remap, skipping remapping");
return;
}
Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar");
Files.delete(tempJar);
try (var timer = new Timer("Remap line numbers")) {
remapLineNumbers(lineNumbers, classesJar, tempJar);
}
Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING);
try (var timer = new Timer("Prune cache")) {
decompileCache.prune();
}
}
private void runWithoutCache() throws IOException {
final MinecraftJar minecraftJar = rebuildInputJar();
Path inputJar = minecraftJar.getPath(); Path inputJar = minecraftJar.getPath();
// Runtime jar is the jar used to run the game // The final output sources jar
final Path runtimeJar = inputJar; final Path sourcesJar = getOutputJar().get().getAsFile().toPath();
if (getUnpickDefinitions().isPresent()) { if (getUnpickDefinitions().isPresent()) {
inputJar = unpickJar(inputJar); try (var timer = new Timer("Unpick")) {
inputJar = unpickJar(inputJar, null);
}
} }
ClassLineNumbers lineNumbers;
try (var timer = new Timer("Decompile")) {
lineNumbers = runDecompileJob(inputJar, sourcesJar, null);
lineNumbers = filterForgeLineNumbers(lineNumbers);
}
if (Files.notExists(sourcesJar)) {
throw new RuntimeException("Failed to decompile sources");
}
if (lineNumbers == null) {
LOGGER.info("No line numbers to remap, skipping remapping");
return;
}
// This is the minecraft jar used at runtime.
final Path classesJar = minecraftJar.getPath();
final Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar");
Files.delete(tempJar);
try (var timer = new Timer("Remap line numbers")) {
remapLineNumbers(lineNumbers, classesJar, tempJar);
}
Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING);
}
private String getCacheKey() {
var sj = new StringJoiner(",");
sj.add(getDecompilerCheckKey());
sj.add(getUnpickCacheKey());
LOGGER.info("Decompile cache data: {}", sj);
try {
return Checksum.sha256Hex(sj.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private String getDecompilerCheckKey() {
var sj = new StringJoiner(",");
sj.add(decompilerOptions.getDecompilerClassName().get());
sj.add(fileCollectionHash(decompilerOptions.getClasspath()));
for (Map.Entry<String, String> entry : decompilerOptions.getOptions().get().entrySet()) {
sj.add(entry.getKey() + "=" + entry.getValue());
}
return sj.toString();
}
private String getUnpickCacheKey() {
if (!getUnpickDefinitions().isPresent()) {
return "";
}
var sj = new StringJoiner(",");
sj.add(fileHash(getUnpickDefinitions().getAsFile().get()));
sj.add(fileCollectionHash(getUnpickConstantJar()));
sj.add(fileCollectionHash(getUnpickRuntimeClasspath()));
return sj.toString();
}
@Nullable
private ClassLineNumbers runDecompileJob(Path inputJar, Path outputJar, @Nullable Path existingJar) throws IOException {
final Platform platform = Platform.CURRENT;
final Path lineMapFile = File.createTempFile("loom", "linemap").toPath();
Files.delete(lineMapFile);
if (!platform.supportsUnixDomainSockets()) { if (!platform.supportsUnixDomainSockets()) {
getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system."); getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system.");
doWork(null, inputJar, runtimeJar); doWork(null, inputJar, outputJar, lineMapFile, existingJar);
return; return readLineNumbers(lineMapFile);
} }
// Set up the IPC path to get the log output back from the forked JVM // Set up the IPC path to get the log output back from the forked JVM
@@ -184,7 +399,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompilerOptions.getName(), "Decompiling minecraft sources"); try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompilerOptions.getName(), "Decompiling minecraft sources");
IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) { IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) {
doWork(logReceiver, inputJar, runtimeJar); doWork(logReceiver, inputJar, outputJar, lineMapFile, existingJar);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException("Failed to shutdown log receiver", e); throw new RuntimeException("Failed to shutdown log receiver", e);
} finally { } finally {
@@ -197,6 +412,28 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
ForgeSourcesRemapper.addForgeSources(getProject(), serviceManager, getOutputJar().get().getAsFile().toPath()); ForgeSourcesRemapper.addForgeSources(getProject(), serviceManager, getOutputJar().get().getAsFile().toPath());
} }
} }
return readLineNumbers(lineMapFile);
}
@Nullable
private ClassLineNumbers filterForgeLineNumbers(@Nullable ClassLineNumbers lineNumbers) {
if (lineNumbers == null) {
return null;
}
if (getParameters().getForge().get()) {
try {
// Remove Forge and NeoForge classes from linemap
// TODO: We should instead not decompile Forge's classes at all
LineMapVisitor.process(linemap, next -> new LineMapClassFilter(next, name -> {
// Skip both Forge and NeoForge classes.
return !name.startsWith("net/minecraftforge/") && !name.startsWith("net/neoforged/");
}));
} catch (IOException e) {
throw new UncheckedIOException("Failed to process linemap", e);
}
}
} }
// Re-run the named minecraft provider to give us a fresh jar to decompile. // Re-run the named minecraft provider to give us a fresh jar to decompile.
@@ -209,7 +446,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(false, true, configContext); final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(false, true, configContext);
minecraftJars = getExtension().getNamedMinecraftProvider().provide(provideContext); minecraftJars = getExtension().getNamedMinecraftProvider().provide(provideContext);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to rebuild input jars", e); ExceptionUtil.printFileLocks(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to rebuild input jars", e);
} }
for (MinecraftJar minecraftJar : minecraftJars) { for (MinecraftJar minecraftJar : minecraftJars) {
@@ -224,13 +462,13 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
); );
} }
private Path unpickJar(Path inputJar) { private Path unpickJar(Path inputJar, @Nullable Path existingJar) {
final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath(); final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath();
final List<String> args = getUnpickArgs(inputJar, outputJar); final List<String> args = getUnpickArgs(inputJar, outputJar, existingJar);
ExecResult result = getExecOperations().javaexec(spec -> { ExecResult result = getExecOperations().javaexec(spec -> {
spec.getMainClass().set("daomephsta.unpick.cli.Main"); spec.getMainClass().set("daomephsta.unpick.cli.Main");
spec.classpath(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); spec.classpath(getUnpickRuntimeClasspath());
spec.args(args); spec.args(args);
spec.systemProperty("java.util.logging.config.file", writeUnpickLogConfig().getAbsolutePath()); spec.systemProperty("java.util.logging.config.file", writeUnpickLogConfig().getAbsolutePath());
}); });
@@ -240,7 +478,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return outputJar; return outputJar;
} }
private List<String> getUnpickArgs(Path inputJar, Path outputJar) { private List<String> getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingJar) {
var fileArgs = new ArrayList<File>(); var fileArgs = new ArrayList<File>();
fileArgs.add(inputJar.toFile()); fileArgs.add(inputJar.toFile());
@@ -257,6 +495,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
fileArgs.add(file); fileArgs.add(file);
} }
if (existingJar != null) {
fileArgs.add(existingJar.toFile());
}
return fileArgs.stream() return fileArgs.stream()
.map(File::getAbsolutePath) .map(File::getAbsolutePath)
.toList(); .toList();
@@ -275,25 +517,36 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return unpickLoggingConfigFile; return unpickLoggingConfigFile;
} }
private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path runtimeJar) { private void remapLineNumbers(ClassLineNumbers lineNumbers, Path inputJar, Path outputJar) throws IOException {
Objects.requireNonNull(lineNumbers, "lineNumbers");
final var remapper = new LineNumberRemapper(lineNumbers);
remapper.process(inputJar, outputJar);
}
private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingJar) {
final String jvmMarkerValue = UUID.randomUUID().toString(); final String jvmMarkerValue = UUID.randomUUID().toString();
final WorkQueue workQueue = createWorkQueue(jvmMarkerValue); final WorkQueue workQueue = createWorkQueue(jvmMarkerValue);
ConfigurableFileCollection classpath = getProject().files();
classpath.from(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
if (existingJar != null) {
classpath.from(existingJar);
}
workQueue.submit(DecompileAction.class, params -> { workQueue.submit(DecompileAction.class, params -> {
params.getDecompilerOptions().set(decompilerOptions.toDto()); params.getDecompilerOptions().set(decompilerOptions.toDto());
params.getInputJar().set(inputJar.toFile()); params.getInputJar().set(inputJar.toFile());
params.getRuntimeJar().set(runtimeJar.toFile()); params.getOutputJar().set(outputJar.toFile());
params.getSourcesDestinationJar().set(getOutputJar()); params.getLinemapFile().set(linemapFile.toFile());
params.getLinemap().set(getMappedJarFileWithSuffix("-sources.lmap", runtimeJar));
params.getLinemapJar().set(getMappedJarFileWithSuffix("-linemapped.jar", runtimeJar));
params.getMappings().set(getMappings().toFile()); params.getMappings().set(getMappings().toFile());
if (ipcServer != null) { if (ipcServer != null) {
params.getIPCPath().set(ipcServer.getPath().toFile()); params.getIPCPath().set(ipcServer.getPath().toFile());
} }
params.getClassPath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES)); params.getClassPath().setFrom(classpath);
// Architectury // Architectury
params.getForge().set(getExtension().isForgeLike()); params.getForge().set(getExtension().isForgeLike());
@@ -338,10 +591,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
Property<DecompilerOptions.Dto> getDecompilerOptions(); Property<DecompilerOptions.Dto> getDecompilerOptions();
RegularFileProperty getInputJar(); RegularFileProperty getInputJar();
RegularFileProperty getRuntimeJar(); RegularFileProperty getOutputJar();
RegularFileProperty getSourcesDestinationJar(); RegularFileProperty getLinemapFile();
RegularFileProperty getLinemap();
RegularFileProperty getLinemapJar();
RegularFileProperty getMappings(); RegularFileProperty getMappings();
RegularFileProperty getIPCPath(); RegularFileProperty getIPCPath();
@@ -372,10 +623,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
private void doDecompile(IOStringConsumer logger) { private void doDecompile(IOStringConsumer logger) {
final Path inputJar = getParameters().getInputJar().get().getAsFile().toPath(); final Path inputJar = getParameters().getInputJar().get().getAsFile().toPath();
final Path sourcesDestinationJar = getParameters().getSourcesDestinationJar().get().getAsFile().toPath(); final Path linemap = getParameters().getLinemapFile().get().getAsFile().toPath();
final Path linemap = getParameters().getLinemap().get().getAsFile().toPath(); final Path outputJar = getParameters().getOutputJar().get().getAsFile().toPath();
final Path linemapJar = getParameters().getLinemapJar().get().getAsFile().toPath();
final Path runtimeJar = getParameters().getRuntimeJar().get().getAsFile().toPath();
final DecompilerOptions.Dto decompilerOptions = getParameters().getDecompilerOptions().get(); final DecompilerOptions.Dto decompilerOptions = getParameters().getDecompilerOptions().get();
@@ -391,7 +640,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
throw new RuntimeException("Failed to create decompiler", e); throw new RuntimeException("Failed to create decompiler", e);
} }
DecompilationMetadata metadata = new DecompilationMetadata( final var metadata = new DecompilationMetadata(
decompilerOptions.maxThreads(), decompilerOptions.maxThreads(),
getParameters().getMappings().get().getAsFile().toPath(), getParameters().getMappings().get().getAsFile().toPath(),
getLibraries(), getLibraries(),
@@ -401,7 +650,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
decompiler.decompile( decompiler.decompile(
inputJar, inputJar,
sourcesDestinationJar, outputJar,
linemap, linemap,
metadata metadata
); );
@@ -412,41 +661,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException("Failed to close loggers", e); throw new UncheckedIOException("Failed to close loggers", e);
} }
if (Files.exists(linemap)) {
if (getParameters().getForge().get()) {
try {
// Remove Forge and NeoForge classes from linemap
// TODO: We should instead not decompile Forge's classes at all
LineMapVisitor.process(linemap, next -> new LineMapClassFilter(next, name -> {
// Skip both Forge and NeoForge classes.
return !name.startsWith("net/minecraftforge/") && !name.startsWith("net/neoforged/");
}));
} catch (IOException e) {
throw new UncheckedIOException("Failed to process linemap", e);
}
}
try {
// Line map the actually jar used to run the game, not the one used to decompile
remapLineNumbers(metadata.logger(), runtimeJar, linemap, linemapJar);
Files.copy(linemapJar, runtimeJar, StandardCopyOption.REPLACE_EXISTING);
Files.delete(linemapJar);
} catch (IOException e) {
throw new UncheckedIOException("Failed to remap line numbers", e);
}
}
}
static void remapLineNumbers(IOStringConsumer logger, Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException {
LineNumberRemapper remapper = new LineNumberRemapper();
remapper.readMappings(linemap.toFile());
try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(oldCompiledJar.toFile(), true);
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) {
remapper.process(logger, inFs.get().getPath("/"), outFs.get().getPath("/"));
}
} }
private Collection<Path> getLibraries() { private Collection<Path> getLibraries() {
@@ -462,16 +676,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return getMappedJarFileWithSuffix(suffix, runtimeJar.get().getAsFile().toPath()); return getMappedJarFileWithSuffix(suffix, runtimeJar.get().getAsFile().toPath());
} }
public static File getMappedJarFileWithSuffix(String suffix, Path runtimeJar) {
final String path = runtimeJar.toFile().getAbsolutePath();
if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) {
throw new RuntimeException("Invalid mapped JAR path: " + path);
}
return new File(path.substring(0, path.length() - 4) + suffix);
}
private Path getMappings() { private Path getMappings() {
Path inputMappings = getExtension().getPlatformMappingFile(); Path inputMappings = getExtension().getPlatformMappingFile();
@@ -530,8 +734,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
return outputMappings; return outputMappings;
} }
public interface MappingsProcessor { public static File getJarFileWithSuffix(String suffix, Path runtimeJar) {
boolean transform(MemoryMappingTree mappings); final String path = runtimeJar.toFile().getAbsolutePath();
if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) {
throw new RuntimeException("Invalid mapped JAR path: " + path);
}
return new File(path.substring(0, path.length() - 4) + suffix);
}
@Nullable
private static ClassLineNumbers readLineNumbers(Path linemapFile) throws IOException {
if (Files.notExists(linemapFile)) {
return null;
}
try (BufferedReader reader = Files.newBufferedReader(linemapFile, StandardCharsets.UTF_8)) {
return ClassLineNumbers.readMappings(reader);
}
} }
private static Constructor<LoomDecompiler> getDecompilerConstructor(String clazz) { private static Constructor<LoomDecompiler> getDecompilerConstructor(String clazz) {
@@ -544,4 +765,43 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static String fileHash(File file) {
try {
return Checksum.sha256Hex(Files.readAllBytes(file.toPath()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static String fileCollectionHash(FileCollection files) {
var sj = new StringJoiner(",");
files.getFiles()
.stream()
.sorted(Comparator.comparing(File::getAbsolutePath))
.map(GenerateSourcesTask::fileHash)
.forEach(sj::add);
return sj.toString();
}
public interface MappingsProcessor {
boolean transform(MemoryMappingTree mappings);
}
private final class Timer implements AutoCloseable {
private final String name;
private final long start;
Timer(String name) {
this.name = name;
this.start = System.currentTimeMillis();
}
@Override
public void close() {
getProject().getLogger().info("{} took {}ms", name, System.currentTimeMillis() - start);
}
}
} }

View File

@@ -164,13 +164,18 @@ public abstract class LoomTasks implements Runnable {
// Remove the client or server run config when not required. Done by name to not remove any possible custom run configs // Remove the client or server run config when not required. Done by name to not remove any possible custom run configs
GradleUtils.afterSuccessfulEvaluation(getProject(), () -> { GradleUtils.afterSuccessfulEvaluation(getProject(), () -> {
String taskName = switch (extension.getMinecraftJarConfiguration().get()) { String taskName;
case SERVER_ONLY -> "client";
case CLIENT_ONLY -> "server";
default -> null;
};
if (taskName == null) { boolean serverOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.SERVER_ONLY;
boolean clientOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.CLIENT_ONLY;
if (serverOnly) {
// Server only, remove the client run config
taskName = "client";
} else if (clientOnly) {
// Client only, remove the server run config
taskName = "server";
} else {
return; return;
} }

View File

@@ -100,6 +100,6 @@ public abstract class PrepareJarRemapTask extends AbstractLoomTask {
} }
static void prepare(TinyRemapperService tinyRemapperService, Path inputFile) { static void prepare(TinyRemapperService tinyRemapperService, Path inputFile) {
tinyRemapperService.getTinyRemapperForInputs().readInputs(tinyRemapperService.getOrCreateTag(inputFile), inputFile); tinyRemapperService.getTinyRemapperForInputs().readInputsAsync(tinyRemapperService.getOrCreateTag(inputFile), inputFile);
} }
} }

View File

@@ -86,6 +86,7 @@ import net.fabricmc.loom.util.SidedClassVisitor;
import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.ZipUtils;
import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJson;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory; import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
import net.fabricmc.loom.util.fmj.FabricModJsonUtils;
import net.fabricmc.loom.util.service.BuildSharedServiceManager; import net.fabricmc.loom.util.service.BuildSharedServiceManager;
import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper;
import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.OutputConsumerPath;
@@ -101,6 +102,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
@Input @Input
public abstract Property<Boolean> getAddNestedDependencies(); public abstract Property<Boolean> getAddNestedDependencies();
/**
* Whether to optimize the fabric.mod.json file, by default this is false.
*
* <p>The schemaVersion entry will be placed first in the json file
*/
@Input
public abstract Property<Boolean> getOptimizeFabricModJson();
/** /**
* Gets the jar paths to the access wideners that will be converted to ATs for Forge runtime. * Gets the jar paths to the access wideners that will be converted to ATs for Forge runtime.
* If you specify multiple files, they will be merged into one. * If you specify multiple files, they will be merged into one.
@@ -145,6 +154,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
final ConfigurationContainer configurations = getProject().getConfigurations(); final ConfigurationContainer configurations = getProject().getConfigurations();
getClasspath().from(configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); getClasspath().from(configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME));
getAddNestedDependencies().convention(true).finalizeValueOnRead(); getAddNestedDependencies().convention(true).finalizeValueOnRead();
getOptimizeFabricModJson().convention(false).finalizeValueOnRead();
getReadMixinConfigsFromManifest().convention(LoomGradleExtension.get(getProject()).isForgeLike()).finalizeValueOnRead(); getReadMixinConfigsFromManifest().convention(LoomGradleExtension.get(getProject()).isForgeLike()).finalizeValueOnRead();
getInjectAccessWidener().convention(false); getInjectAccessWidener().convention(false);
@@ -236,6 +246,8 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
if (!getAtAccessWideners().get().isEmpty()) { if (!getAtAccessWideners().get().isEmpty()) {
params.getMappingBuildServiceUuid().set(UnsafeWorkQueueHelper.create(MappingsService.createDefault(getProject(), serviceManagerProvider.get().get(), getSourceNamespace().get(), getTargetNamespace().get()))); params.getMappingBuildServiceUuid().set(UnsafeWorkQueueHelper.create(MappingsService.createDefault(getProject(), serviceManagerProvider.get().get(), getSourceNamespace().get(), getTargetNamespace().get())));
} }
params.getOptimizeFmj().set(getOptimizeFabricModJson().get());
}); });
} }
@@ -293,6 +305,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
Property<Boolean> getUseMixinExtension(); Property<Boolean> getUseMixinExtension();
Property<Boolean> getMultiProjectOptimisation(); Property<Boolean> getMultiProjectOptimisation();
Property<Boolean> getOptimizeFmj();
record RefmapData(List<String> mixinConfigs, String refmapName) implements Serializable { } record RefmapData(List<String> mixinConfigs, String refmapName) implements Serializable { }
ListProperty<RefmapData> getMixinData(); ListProperty<RefmapData> getMixinData();
@@ -348,6 +361,10 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
rewriteJar(); rewriteJar();
if (getParameters().getOptimizeFmj().get()) {
optimizeFMJ();
}
if (tinyRemapperService != null && !getParameters().getMultiProjectOptimisation().get()) { if (tinyRemapperService != null && !getParameters().getMultiProjectOptimisation().get()) {
tinyRemapperService.close(); tinyRemapperService.close();
} }
@@ -482,6 +499,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
} }
} }
} }
private void optimizeFMJ() throws IOException {
if (!ZipUtils.contains(outputFile, FabricModJsonFactory.FABRIC_MOD_JSON)) {
return;
}
ZipUtils.transformJson(JsonObject.class, outputFile, FabricModJsonFactory.FABRIC_MOD_JSON, FabricModJsonUtils::optimizeFmj);
}
} }
@Override @Override

View File

@@ -0,0 +1,88 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public interface AsyncZipProcessor {
static void processEntries(Path inputZip, Path outputZip, AsyncZipProcessor processor) throws IOException {
try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputZip, false);
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputZip, true)) {
final Path inRoot = inFs.get().getPath("/");
final Path outRoot = outFs.get().getPath("/");
List<CompletableFuture<Void>> futures = new ArrayList<>();
final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Files.walkFileTree(inRoot, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path inputFile, BasicFileAttributes attrs) throws IOException {
final CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
try {
final String rel = inRoot.relativize(inputFile).toString();
final Path outputFile = outRoot.resolve(rel);
processor.processEntryAsync(inputFile, outputFile);
} catch (IOException e) {
throw new CompletionException(e);
}
return null;
}, executor);
futures.add(future);
return FileVisitResult.CONTINUE;
}
});
// Wait for all futures to complete
for (CompletableFuture<Void> future : futures) {
try {
future.join();
} catch (CompletionException e) {
if (e.getCause() instanceof IOException ioe) {
throw ioe;
}
throw new RuntimeException("Failed to process zip", e.getCause());
}
}
executor.shutdown();
}
}
void processEntryAsync(Path inputEntry, Path outputEntry) throws IOException;
}

View File

@@ -68,6 +68,11 @@ public class Checksum {
} }
} }
public static String sha256Hex(byte[] input) throws IOException {
HashCode hash = ByteSource.wrap(input).hash(Hashing.sha256());
return Checksum.toHex(hash.asBytes());
}
public static String sha1Hex(Path path) throws IOException { public static String sha1Hex(Path path) throws IOException {
HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1()); HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1());
return toHex(hash.asBytes()); return toHex(hash.asBytes());
@@ -102,6 +107,7 @@ public class Checksum {
public static String projectHash(Project project) { public static String projectHash(Project project) {
String str = project.getProjectDir().getAbsolutePath() + ":" + project.getPath(); String str = project.getProjectDir().getAbsolutePath() + ":" + project.getPath();
return toHex(str.getBytes(StandardCharsets.UTF_8)).substring(0, 16); String hex = sha1Hex(str.getBytes(StandardCharsets.UTF_8));
return hex.substring(hex.length() - 16);
} }
} }

View File

@@ -37,6 +37,7 @@ public class Constants {
public static final String FABRIC_REPOSITORY = "https://maven.fabricmc.net/"; public static final String FABRIC_REPOSITORY = "https://maven.fabricmc.net/";
public static final int ASM_VERSION = Opcodes.ASM9; public static final int ASM_VERSION = Opcodes.ASM9;
public static final String RELEASE_TIME_1_3 = "2012-07-25T22:00:00+00:00";
private Constants() { private Constants() {
} }

View File

@@ -1,7 +1,7 @@
/* /*
* This file is part of fabric-loom, licensed under the MIT License (MIT). * This file is part of fabric-loom, licensed under the MIT License (MIT).
* *
* Copyright (c) 2022 FabricMC * Copyright (c) 2022-2024 FabricMC
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +24,17 @@
package net.fabricmc.loom.util; package net.fabricmc.loom.util;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.gradle.api.Project;
import net.fabricmc.loom.nativeplatform.LoomNativePlatform;
public final class ExceptionUtil { public final class ExceptionUtil {
/** /**
* Creates a descriptive user-facing wrapper exception for an underlying cause. * Creates a descriptive user-facing wrapper exception for an underlying cause.
@@ -44,4 +53,40 @@ public final class ExceptionUtil {
String descriptiveMessage = "%s, %s: %s".formatted(message, cause.getClass().getName(), cause.getMessage()); String descriptiveMessage = "%s, %s: %s".formatted(message, cause.getClass().getName(), cause.getMessage());
return constructor.apply(descriptiveMessage, cause); return constructor.apply(descriptiveMessage, cause);
} }
public static void printFileLocks(Throwable e, Project project) {
Throwable cause = e;
while (cause != null) {
if (cause instanceof FileSystemException fse) {
printFileLocks(fse.getFile(), project);
break;
}
cause = cause.getCause();
}
}
private static void printFileLocks(String filename, Project project) {
final Path path = Paths.get(filename);
if (!Files.exists(path)) {
return;
}
final List<ProcessHandle> processes = LoomNativePlatform.getProcessesWithLockOn(path);
if (processes.isEmpty()) {
return;
}
final ProcessUtil processUtil = ProcessUtil.create(project);
final String noun = processes.size() == 1 ? "process has" : "processes have";
project.getLogger().error("The following {} a lock on the file '{}':", noun, path);
for (ProcessHandle process : processes) {
project.getLogger().error(processUtil.printWithParents(process));
}
}
} }

View File

@@ -43,6 +43,10 @@ public final class FileSystemUtil {
return get().getPath(path, more); return get().getPath(path, more);
} }
public Path getRoot() {
return get().getPath("/");
}
public byte[] readAllBytes(String path) throws IOException { public byte[] readAllBytes(String path) throws IOException {
Path fsPath = getPath(path); Path fsPath = getPath(path);

View File

@@ -0,0 +1,109 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
import org.gradle.api.Project;
import org.gradle.api.logging.LogLevel;
import net.fabricmc.loom.nativeplatform.LoomNativePlatform;
public record ProcessUtil(LogLevel logLevel) {
private static final String EXPLORER_COMMAND = "C:\\Windows\\explorer.exe";
public static ProcessUtil create(Project project) {
return new ProcessUtil(project.getGradle().getStartParameter().getLogLevel());
}
public String printWithParents(ProcessHandle handle) {
String result = printWithParents(handle, 0).trim();
if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) {
return "Run with --info or --debug to show arguments, may reveal sensitive info\n" + result;
}
return result;
}
private String printWithParents(ProcessHandle handle, int depth) {
var lines = new ArrayList<String>();
getWindowTitles(handle).ifPresent(titles -> lines.add("title: " + titles));
lines.add("pid: " + handle.pid());
handle.info().command().ifPresent(command -> lines.add("command: " + command));
getProcessArguments(handle).ifPresent(arguments -> lines.add("arguments: " + arguments));
handle.info().startInstant().ifPresent(instant -> lines.add("started at: " + instant));
handle.info().user().ifPresent(user -> lines.add("user: " + user));
handle.parent().ifPresent(parent -> lines.add("parent:\n" + printWithParents(parent, depth + 1)));
StringBuilder sj = new StringBuilder();
for (String line : lines) {
sj.append("\t".repeat(depth)).append("- ").append(line).append('\n');
}
return sj.toString();
}
private Optional<String> getProcessArguments(ProcessHandle handle) {
if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) {
return Optional.empty();
}
return handle.info().arguments().map(arr -> {
String join = String.join(" ", arr);
if (join.isBlank()) {
return "";
}
return " " + join;
});
}
private Optional<String> getWindowTitles(ProcessHandle processHandle) {
if (processHandle.info().command().orElse("").equals(EXPLORER_COMMAND)) {
// Explorer is a single process, so the window titles are not useful
return Optional.empty();
}
List<String> titles = LoomNativePlatform.getWindowTitlesForPid(processHandle.pid());
if (titles.isEmpty()) {
return Optional.empty();
}
final StringJoiner joiner = new StringJoiner(", ");
for (String title : titles) {
joiner.add("'" + title + "'");
}
return Optional.of(joiner.toString());
}
}

View File

@@ -74,7 +74,7 @@ public final class TinyRemapperHelper {
MemoryMappingTree mappingTree = extension.getMappingConfiguration().getMappingsService(serviceManager, mappingOption).getMappingTree(); MemoryMappingTree mappingTree = extension.getMappingConfiguration().getMappingsService(serviceManager, mappingOption).getMappingTree();
if (fixRecords && !mappingTree.getSrcNamespace().equals(fromM)) { if (fixRecords && !mappingTree.getSrcNamespace().equals(fromM)) {
throw new IllegalStateException("Mappings src namespace must match remap src namespace"); throw new IllegalStateException("Mappings src namespace must match remap src namespace, expected " + fromM + " but got " + mappingTree.getSrcNamespace());
} }
int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString()); int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString());

View File

@@ -79,13 +79,13 @@ public class ZipUtils {
public static void unpackAll(Path zip, Path output) throws IOException { public static void unpackAll(Path zip, Path output) throws IOException {
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(zip, false); try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(zip, false);
Stream<Path> walk = Files.walk(fs.get().getPath("/"))) { Stream<Path> walk = Files.walk(fs.getRoot())) {
Iterator<Path> iterator = walk.iterator(); Iterator<Path> iterator = walk.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
Path fsPath = iterator.next(); Path fsPath = iterator.next();
if (!Files.isRegularFile(fsPath)) continue; if (!Files.isRegularFile(fsPath)) continue;
Path dstPath = output.resolve(fs.get().getPath("/").relativize(fsPath).toString()); Path dstPath = output.resolve(fs.getRoot().relativize(fsPath).toString());
Path dstPathParent = dstPath.getParent(); Path dstPathParent = dstPath.getParent();
if (dstPathParent != null) Files.createDirectories(dstPathParent); if (dstPathParent != null) Files.createDirectories(dstPathParent);
Files.copy(fsPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); Files.copy(fsPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
@@ -121,7 +121,7 @@ public class ZipUtils {
} }
} }
public static <T> T unpackJackson(Path zip, String path, Class<T> clazz) throws IOException { public static <T> T unpackJson(Path zip, String path, Class<T> clazz) throws IOException {
final byte[] bytes = unpack(zip, path); final byte[] bytes = unpack(zip, path);
return LoomGradlePlugin.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz); return LoomGradlePlugin.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz);
} }
@@ -215,6 +215,14 @@ public class ZipUtils {
s -> LoomGradlePlugin.GSON.toJson(s, typeOfT).getBytes(StandardCharsets.UTF_8)); s -> LoomGradlePlugin.GSON.toJson(s, typeOfT).getBytes(StandardCharsets.UTF_8));
} }
public static <T> void transformJson(Class<T> typeOfT, Path zip, String path, UnsafeUnaryOperator<T> transformer) throws IOException {
int transformed = transformJson(typeOfT, zip, Map.of(path, transformer));
if (transformed != 1) {
throw new IOException("Failed to transform " + path + " in " + zip);
}
}
public static int transform(Path zip, Collection<Pair<String, UnsafeUnaryOperator<byte[]>>> transforms) throws IOException { public static int transform(Path zip, Collection<Pair<String, UnsafeUnaryOperator<byte[]>>> transforms) throws IOException {
return transform(zip, transforms.stream()); return transform(zip, transforms.stream());
} }

View File

@@ -36,11 +36,14 @@ import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import dev.architectury.loom.metadata.ModMetadataFile; import dev.architectury.loom.metadata.ModMetadataFile;
import dev.architectury.loom.metadata.ModMetadataFiles; import dev.architectury.loom.metadata.ModMetadataFiles;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting; import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.util.FileSystemUtil; import net.fabricmc.loom.util.FileSystemUtil;
@@ -49,7 +52,9 @@ import net.fabricmc.loom.util.ZipUtils;
import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.gradle.SourceSetHelper;
public final class FabricModJsonFactory { public final class FabricModJsonFactory {
private static final String FABRIC_MOD_JSON = "fabric.mod.json"; public static final String FABRIC_MOD_JSON = "fabric.mod.json";
private static final Logger LOGGER = LoggerFactory.getLogger(FabricModJsonFactory.class);
private FabricModJsonFactory() { private FabricModJsonFactory() {
} }
@@ -158,6 +163,11 @@ public final class FabricModJsonFactory {
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
return create(LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class), new FabricModJsonSource.SourceSetSource(sourceSets)); return create(LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class), new FabricModJsonSource.SourceSetSource(sourceSets));
} catch (JsonSyntaxException e) {
LOGGER.warn("Failed to parse fabric.mod.json: {}", file.getAbsolutePath());
return null;
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + file.getAbsolutePath(), e);
} }
} }

View File

@@ -25,13 +25,14 @@
package net.fabricmc.loom.util.fmj; package net.fabricmc.loom.util.fmj;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
final class FabricModJsonUtils { public final class FabricModJsonUtils {
private FabricModJsonUtils() { private FabricModJsonUtils() {
} }
@@ -49,6 +50,30 @@ final class FabricModJsonUtils {
return element.getAsInt(); return element.getAsInt();
} }
// Ensure that the schemaVersion json entry, is first in the json file
// This exercises an optimisation here: https://github.com/FabricMC/fabric-loader/blob/d69cb72d26497e3f387cf46f9b24340b402a4644/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java#L62
public static JsonObject optimizeFmj(JsonObject json) {
if (!json.has("schemaVersion")) {
// No schemaVersion, something will explode later?!
return json;
}
// Create a new json object with the schemaVersion first
var out = new JsonObject();
out.add("schemaVersion", json.get("schemaVersion"));
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
if (entry.getKey().equals("schemaVersion")) {
continue;
}
// Add all other entries
out.add(entry.getKey(), entry.getValue());
}
return out;
}
private static JsonElement getElement(JsonObject jsonObject, String key) { private static JsonElement getElement(JsonObject jsonObject, String key) {
final JsonElement element = jsonObject.get(key); final JsonElement element = jsonObject.get(key);

View File

@@ -31,7 +31,6 @@ import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.kotlin.remapping.JvmExtensionWrapper;
import net.fabricmc.loom.kotlin.remapping.KotlinMetadataTinyRemapperExtensionImpl; import net.fabricmc.loom.kotlin.remapping.KotlinMetadataTinyRemapperExtensionImpl;
/** /**
@@ -62,8 +61,7 @@ public class KotlinRemapperClassloader extends URLClassLoader {
public static KotlinRemapperClassloader create(KotlinClasspath classpathProvider) { public static KotlinRemapperClassloader create(KotlinClasspath classpathProvider) {
// Include the libraries that are not on the kotlin classpath. // Include the libraries that are not on the kotlin classpath.
final Stream<URL> loomUrls = getClassUrls( final Stream<URL> loomUrls = getClassUrls(
KotlinMetadataTinyRemapperExtensionImpl.class, // Loom (Kotlin) KotlinMetadataTinyRemapperExtensionImpl.class // Loom (Kotlin)
JvmExtensionWrapper.class // Loom (Java)
); );
final URL[] urls = Stream.concat( final URL[] urls = Stream.concat(

View File

@@ -40,18 +40,17 @@ import kotlinx.metadata.KmTypeAlias
import kotlinx.metadata.KmTypeParameter import kotlinx.metadata.KmTypeParameter
import kotlinx.metadata.KmTypeProjection import kotlinx.metadata.KmTypeProjection
import kotlinx.metadata.KmValueParameter import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.internal.extensions.KmClassExtension
import kotlinx.metadata.internal.extensions.KmConstructorExtension
import kotlinx.metadata.internal.extensions.KmFunctionExtension
import kotlinx.metadata.internal.extensions.KmPackageExtension
import kotlinx.metadata.internal.extensions.KmPropertyExtension
import kotlinx.metadata.internal.extensions.KmTypeAliasExtension
import kotlinx.metadata.internal.extensions.KmTypeExtension
import kotlinx.metadata.internal.extensions.KmTypeParameterExtension
import kotlinx.metadata.internal.extensions.KmValueParameterExtension
import kotlinx.metadata.isLocalClassName import kotlinx.metadata.isLocalClassName
import kotlinx.metadata.jvm.JvmFieldSignature import kotlinx.metadata.jvm.JvmFieldSignature
import kotlinx.metadata.jvm.JvmMethodSignature import kotlinx.metadata.jvm.JvmMethodSignature
import kotlinx.metadata.jvm.annotations
import kotlinx.metadata.jvm.fieldSignature
import kotlinx.metadata.jvm.getterSignature
import kotlinx.metadata.jvm.localDelegatedProperties
import kotlinx.metadata.jvm.setterSignature
import kotlinx.metadata.jvm.signature
import kotlinx.metadata.jvm.syntheticMethodForAnnotations
import kotlinx.metadata.jvm.syntheticMethodForDelegate
import kotlinx.metadata.jvm.toJvmInternalName import kotlinx.metadata.jvm.toJvmInternalName
import org.objectweb.asm.commons.Remapper import org.objectweb.asm.commons.Remapper
@@ -68,7 +67,7 @@ class KotlinClassRemapper(private val remapper: Remapper) {
clazz.nestedClasses.replaceAll(this::remap) clazz.nestedClasses.replaceAll(this::remap)
clazz.sealedSubclasses.replaceAll(this::remap) clazz.sealedSubclasses.replaceAll(this::remap)
clazz.contextReceiverTypes.replaceAll(this::remap) clazz.contextReceiverTypes.replaceAll(this::remap)
clazz.getExtensions().replaceAll(this::remap) clazz.localDelegatedProperties.replaceAll(this::remap)
return clazz return clazz
} }
@@ -81,7 +80,7 @@ class KotlinClassRemapper(private val remapper: Remapper) {
pkg.functions.replaceAll(this::remap) pkg.functions.replaceAll(this::remap)
pkg.properties.replaceAll(this::remap) pkg.properties.replaceAll(this::remap)
pkg.typeAliases.replaceAll(this::remap) pkg.typeAliases.replaceAll(this::remap)
pkg.getExtensions().replaceAll(this::remap) pkg.localDelegatedProperties.replaceAll(this::remap)
return pkg return pkg
} }
@@ -107,7 +106,7 @@ class KotlinClassRemapper(private val remapper: Remapper) {
type.abbreviatedType = type.abbreviatedType?.let { remap(it) } type.abbreviatedType = type.abbreviatedType?.let { remap(it) }
type.outerType = type.outerType?.let { remap(it) } type.outerType = type.outerType?.let { remap(it) }
type.flexibleTypeUpperBound = type.flexibleTypeUpperBound?.let { remap(it) } type.flexibleTypeUpperBound = type.flexibleTypeUpperBound?.let { remap(it) }
type.getExtensions().replaceAll(this::remap) type.annotations.replaceAll(this::remap)
return type return type
} }
@@ -117,7 +116,7 @@ class KotlinClassRemapper(private val remapper: Remapper) {
function.contextReceiverTypes.replaceAll(this::remap) function.contextReceiverTypes.replaceAll(this::remap)
function.valueParameters.replaceAll(this::remap) function.valueParameters.replaceAll(this::remap)
function.returnType = remap(function.returnType) function.returnType = remap(function.returnType)
function.getExtensions().replaceAll(this::remap) function.signature = function.signature?.let { remap(it) }
return function return function
} }
@@ -127,7 +126,11 @@ class KotlinClassRemapper(private val remapper: Remapper) {
property.contextReceiverTypes.replaceAll(this::remap) property.contextReceiverTypes.replaceAll(this::remap)
property.setterParameter = property.setterParameter?.let { remap(it) } property.setterParameter = property.setterParameter?.let { remap(it) }
property.returnType = remap(property.returnType) property.returnType = remap(property.returnType)
property.getExtensions().replaceAll(this::remap) property.fieldSignature = property.fieldSignature?.let { remap(it) }
property.getterSignature = property.getterSignature?.let { remap(it) }
property.setterSignature = property.setterSignature?.let { remap(it) }
property.syntheticMethodForAnnotations = property.syntheticMethodForAnnotations?.let { remap(it) }
property.syntheticMethodForDelegate = property.syntheticMethodForDelegate?.let { remap(it) }
return property return property
} }
@@ -136,19 +139,18 @@ class KotlinClassRemapper(private val remapper: Remapper) {
typeAlias.underlyingType = remap(typeAlias.underlyingType) typeAlias.underlyingType = remap(typeAlias.underlyingType)
typeAlias.expandedType = remap(typeAlias.expandedType) typeAlias.expandedType = remap(typeAlias.expandedType)
typeAlias.annotations.replaceAll(this::remap) typeAlias.annotations.replaceAll(this::remap)
typeAlias.getExtensions().replaceAll(this::remap)
return typeAlias return typeAlias
} }
private fun remap(constructor: KmConstructor): KmConstructor { private fun remap(constructor: KmConstructor): KmConstructor {
constructor.valueParameters.replaceAll(this::remap) constructor.valueParameters.replaceAll(this::remap)
constructor.getExtensions().replaceAll(this::remap) constructor.signature = constructor.signature?.let { remap(it) }
return constructor return constructor
} }
private fun remap(typeParameter: KmTypeParameter): KmTypeParameter { private fun remap(typeParameter: KmTypeParameter): KmTypeParameter {
typeParameter.upperBounds.replaceAll(this::remap) typeParameter.upperBounds.replaceAll(this::remap)
typeParameter.getExtensions().replaceAll(this::remap) typeParameter.annotations.replaceAll(this::remap)
return typeParameter return typeParameter
} }
@@ -163,7 +165,6 @@ class KotlinClassRemapper(private val remapper: Remapper) {
private fun remap(valueParameter: KmValueParameter): KmValueParameter { private fun remap(valueParameter: KmValueParameter): KmValueParameter {
valueParameter.type = remap(valueParameter.type) valueParameter.type = remap(valueParameter.type)
valueParameter.varargElementType = valueParameter.varargElementType?.let { remap(it) } valueParameter.varargElementType = valueParameter.varargElementType?.let { remap(it) }
valueParameter.getExtensions().replaceAll(this::remap)
return valueParameter return valueParameter
} }
@@ -171,76 +172,6 @@ class KotlinClassRemapper(private val remapper: Remapper) {
return KmAnnotation(remap(annotation.className), annotation.arguments) return KmAnnotation(remap(annotation.className), annotation.arguments)
} }
private fun remap(classExtension: KmClassExtension): KmClassExtension {
JvmExtensionWrapper.Class.get(classExtension)?.let {
it.localDelegatedProperties.replaceAll(this::remap)
return it.extension
}
return classExtension
}
private fun remap(packageExtension: KmPackageExtension): KmPackageExtension {
JvmExtensionWrapper.Package.get(packageExtension)?.let {
it.localDelegatedProperties.replaceAll(this::remap)
return it.extension
}
return packageExtension
}
private fun remap(typeExtension: KmTypeExtension): KmTypeExtension {
JvmExtensionWrapper.Type.get(typeExtension)?.let {
it.annotations.replaceAll(this::remap)
return it.extension
}
return typeExtension
}
private fun remap(functionExtension: KmFunctionExtension): KmFunctionExtension {
JvmExtensionWrapper.Function.get(functionExtension)?.let {
it.signature = it.signature?.let { sig -> remap(sig) }
return it.extension
}
return functionExtension
}
private fun remap(propertyExtension: KmPropertyExtension): KmPropertyExtension {
JvmExtensionWrapper.Property.get(propertyExtension)?.let {
it.fieldSignature = it.fieldSignature?.let { sig -> remap(sig) }
it.getterSignature = it.getterSignature?.let { sig -> remap(sig) }
it.setterSignature = it.setterSignature?.let { sig -> remap(sig) }
it.syntheticMethodForAnnotations = it.syntheticMethodForAnnotations?.let { sig -> remap(sig) }
it.syntheticMethodForDelegate = it.syntheticMethodForDelegate?.let { sig -> remap(sig) }
return it.extension
}
return propertyExtension
}
private fun remap(typeAliasExtension: KmTypeAliasExtension): KmTypeAliasExtension {
return typeAliasExtension
}
private fun remap(typeParameterExtension: KmTypeParameterExtension): KmTypeParameterExtension {
return typeParameterExtension
}
private fun remap(valueParameterExtension: KmValueParameterExtension): KmValueParameterExtension {
return valueParameterExtension
}
private fun remap(constructorExtension: KmConstructorExtension): KmConstructorExtension {
JvmExtensionWrapper.Constructor.get(constructorExtension)?.let {
it.signature = it.signature?.let { sig -> remap(sig) }
return it.extension
}
return constructorExtension
}
private fun remap(signature: JvmMethodSignature): JvmMethodSignature { private fun remap(signature: JvmMethodSignature): JvmMethodSignature {
return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor)) return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor))
} }

View File

@@ -1,100 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2023 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
@file:Suppress("UNCHECKED_CAST")
package net.fabricmc.loom.kotlin.remapping
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmPackage
import kotlinx.metadata.KmProperty
import kotlinx.metadata.KmType
import kotlinx.metadata.KmTypeAlias
import kotlinx.metadata.KmTypeParameter
import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.internal.extensions.KmClassExtension
import kotlinx.metadata.internal.extensions.KmConstructorExtension
import kotlinx.metadata.internal.extensions.KmFunctionExtension
import kotlinx.metadata.internal.extensions.KmPackageExtension
import kotlinx.metadata.internal.extensions.KmPropertyExtension
import kotlinx.metadata.internal.extensions.KmTypeAliasExtension
import kotlinx.metadata.internal.extensions.KmTypeExtension
import kotlinx.metadata.internal.extensions.KmTypeParameterExtension
import kotlinx.metadata.internal.extensions.KmValueParameterExtension
import java.lang.reflect.Field
import kotlin.reflect.KClass
val KM_CLASS_EXTENSIONS = getField(KmClass::class)
val KM_PACKAGE_EXTENSIONS = getField(KmPackage::class)
val KM_TYPE_EXTENSIONS = getField(KmType::class)
val KM_FUNCTION_EXTENSIONS = getField(KmFunction::class)
val KM_PROPERTY_EXTENSIONS = getField(KmProperty::class)
val KM_TYPE_ALIAS_EXTENSIONS = getField(KmTypeAlias::class)
val KM_TYPE_PARAMETER_EXTENSIONS = getField(KmTypeParameter::class)
val KM_VALUE_PARAMETER_EXTENSIONS = getField(KmValueParameter::class)
val KM_CONSTRUCTOR_EXTENSIONS = getField(KmConstructor::class)
fun KmClass.getExtensions(): MutableList<KmClassExtension> {
return KM_CLASS_EXTENSIONS.get(this) as MutableList<KmClassExtension>
}
fun KmPackage.getExtensions(): MutableList<KmPackageExtension> {
return KM_PACKAGE_EXTENSIONS.get(this) as MutableList<KmPackageExtension>
}
fun KmType.getExtensions(): MutableList<KmTypeExtension> {
return KM_TYPE_EXTENSIONS.get(this) as MutableList<KmTypeExtension>
}
fun KmFunction.getExtensions(): MutableList<KmFunctionExtension> {
return KM_FUNCTION_EXTENSIONS.get(this) as MutableList<KmFunctionExtension>
}
fun KmProperty.getExtensions(): MutableList<KmPropertyExtension> {
return KM_PROPERTY_EXTENSIONS.get(this) as MutableList<KmPropertyExtension>
}
fun KmTypeAlias.getExtensions(): MutableList<KmTypeAliasExtension> {
return KM_TYPE_ALIAS_EXTENSIONS.get(this) as MutableList<KmTypeAliasExtension>
}
fun KmTypeParameter.getExtensions(): MutableList<KmTypeParameterExtension> {
return KM_TYPE_PARAMETER_EXTENSIONS.get(this) as MutableList<KmTypeParameterExtension>
}
fun KmValueParameter.getExtensions(): MutableList<KmValueParameterExtension> {
return KM_VALUE_PARAMETER_EXTENSIONS.get(this) as MutableList<KmValueParameterExtension>
}
fun KmConstructor.getExtensions(): MutableList<KmConstructorExtension> {
return KM_CONSTRUCTOR_EXTENSIONS.get(this) as MutableList<KmConstructorExtension>
}
private fun getField(clazz: KClass<*>): Field {
val field = clazz.java.getDeclaredField("extensions")
field.isAccessible = true
return field
}

View File

@@ -57,7 +57,11 @@ class KotlinMetadataRemappingClassVisitor(private val remapper: Remapper, next:
var result: AnnotationVisitor? = super.visitAnnotation(descriptor, visible) var result: AnnotationVisitor? = super.visitAnnotation(descriptor, visible)
if (descriptor == ANNOTATION_DESCRIPTOR && result != null) { if (descriptor == ANNOTATION_DESCRIPTOR && result != null) {
result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className) try {
result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className)
} catch (e: Exception) {
throw RuntimeException("Failed to remap Kotlin metadata annotation in class $className", e)
}
} }
return result return result

View File

@@ -88,12 +88,15 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait
modDatagenImplementation fabricApi.module("fabric-data-generation-api-v1", "0.90.0+1.20.2") modDatagenImplementation fabricApi.module("fabric-data-generation-api-v1", "0.90.0+1.20.2")
} }
println("%%" + loom.runs.datagen.configName + "%%")
''' '''
when: when:
def result = gradle.run(task: "runDatagen") def result = gradle.run(task: "runDatagen")
then: then:
result.task(":runDatagen").outcome == SUCCESS result.task(":runDatagen").outcome == SUCCESS
result.output.contains("%%Data Generation%%")
where: where:
version << STANDARD_TEST_VERSIONS version << STANDARD_TEST_VERSIONS

View File

@@ -84,7 +84,7 @@ class DebugLineNumbersTest extends Specification implements GradleProjectTestTra
''' '''
when: when:
// First generate sources // First generate sources
def genSources = gradle.run(task: "genSources") def genSources = gradle.run(task: "genSources", args: ["--info"])
genSources.task(":genSources").outcome == SUCCESS genSources.task(":genSources").outcome == SUCCESS
// Print out the source of the file // Print out the source of the file

View File

@@ -74,4 +74,36 @@ class DecompileTest extends Specification implements GradleProjectTestTrait {
where: where:
version << STANDARD_TEST_VERSIONS version << STANDARD_TEST_VERSIONS
} }
def "decompile cache"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE, gradleHomeDir: File.createTempDir())
gradle.buildSrc("decompile")
gradle.buildGradle << '''
dependencies {
minecraft "com.mojang:minecraft:1.20.4"
mappings "net.fabricmc:yarn:1.20.4+build.3:v2"
}
'''
when:
def result = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
// Add fabric API to the project, this introduces some transitive access wideners
gradle.buildGradle << '''
dependencies {
modImplementation "net.fabricmc.fabric-api:fabric-api:0.96.4+1.20.4"
}
'''
def result2 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
// And run again, with no changes
def result3 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
then:
result.task(":genSourcesWithVineflower").outcome == SUCCESS
result2.task(":genSourcesWithVineflower").outcome == SUCCESS
result3.task(":genSourcesWithVineflower").outcome == SUCCESS
}
} }

View File

@@ -24,6 +24,8 @@
package net.fabricmc.loom.test.integration package net.fabricmc.loom.test.integration
import java.nio.file.Path
import spock.lang.Specification import spock.lang.Specification
import spock.lang.Unroll import spock.lang.Unroll
@@ -118,4 +120,31 @@ class LegacyProjectTest extends Specification implements GradleProjectTestTrait
'b1.8.1' | _ 'b1.8.1' | _
'a1.2.5' | _ 'a1.2.5' | _
} }
@Unroll
def "Legacy merged"() {
setup:
def mappings = Path.of("src/test/resources/mappings/1.2.5-intermediary.tiny.zip").toAbsolutePath()
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE)
gradle.buildGradle << """
dependencies {
minecraft "com.mojang:minecraft:1.2.5"
mappings loom.layered() {
// No names
}
modImplementation "net.fabricmc:fabric-loader:0.15.7"
}
"""
gradle.buildSrc("legacyMergedIntermediary")
when:
def result = gradle.run(task: "build", args: [
"-Ploom.test.legacyMergedIntermediary.mappingPath=${mappings}"
])
then:
result.task(":build").outcome == SUCCESS
}
} }

View File

@@ -0,0 +1,80 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration
import spock.lang.Specification
import spock.lang.Unroll
import net.fabricmc.loom.build.nesting.IncludedJarFactory
import net.fabricmc.loom.test.util.GradleProjectTestTrait
class SemVerParsingTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "test valid Semantic Versioning strings"() {
given:
IncludedJarFactory includedJarFactory = new IncludedJarFactory(null)
expect:
includedJarFactory.validSemVer(version) == true
where:
version | _
"1.0.0" | _
"2.5.3" | _
"3.0.0-beta.2" | _
"4.2.1-alpha+001" | _
"5.0.0-rc.1+build.1" | _
}
@Unroll
def "test non-Semantic Versioning strings"() {
given:
IncludedJarFactory includedJarFactory = new IncludedJarFactory(null)
expect:
includedJarFactory.validSemVer(version) == false
where:
version | _
"1.0" | _
"3.0.0.Beta1-120922-126" | _
"3.0.2.Final" | _
"4.2.1.4.RELEASE" | _
}
@Unroll
def "test '.Final' suffixed SemVer"() {
given:
IncludedJarFactory includedJarFactory = new IncludedJarFactory(null)
expect:
includedJarFactory.getVersion(metadata) == expectedVersion
where:
metadata | expectedVersion
new IncludedJarFactory.Metadata("group", "name", "1.0.0.Final", null) | "1.0.0"
new IncludedJarFactory.Metadata("group", "name", "2.5.3.final", null) | "2.5.3"
}
}

View File

@@ -26,6 +26,8 @@ package net.fabricmc.loom.test.integration.buildSrc.decompile
import java.nio.file.Path import java.nio.file.Path
import com.google.common.io.Files
import net.fabricmc.loom.api.decompilers.DecompilationMetadata import net.fabricmc.loom.api.decompilers.DecompilationMetadata
import net.fabricmc.loom.api.decompilers.LoomDecompiler import net.fabricmc.loom.api.decompilers.LoomDecompiler
@@ -33,5 +35,6 @@ class CustomDecompiler implements LoomDecompiler {
@Override @Override
void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
println("Running custom decompiler") println("Running custom decompiler")
Files.touch(sourcesDestination.toFile())
} }
} }

View File

@@ -0,0 +1,63 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration.buildSrc.legacyMergedIntermediary
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import net.fabricmc.loom.api.LoomGradleExtensionAPI
import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider
import net.fabricmc.loom.util.ZipUtils
class TestPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
LoomGradleExtensionAPI extension = project.getExtensions().getByName("loom")
extension.setIntermediateMappingsProvider(LegacyIntermediaryProvider.class) {
mappingPath.set(project.property("loom.test.legacyMergedIntermediary.mappingPath"))
}
}
abstract static class LegacyIntermediaryProvider extends IntermediateMappingsProvider {
final String name = "legacyMerged"
abstract Property<String> getMappingPath();
@Override
void provide(Path tinyMappings) throws IOException {
if (getMinecraftVersion().get() != "1.2.5") {
throw new IllegalStateException("This plugin only supports Minecraft 1.2.5")
}
byte[] data = ZipUtils.unpack(Paths.get(getMappingPath().get()), "1.2.5-intermediary.tiny")
Files.write(tinyMappings, data)
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import java.nio.file.Files
import java.nio.file.Path
import spock.lang.Specification
import net.fabricmc.loom.test.util.ZipTestUtils
import net.fabricmc.loom.util.AsyncZipProcessor
import net.fabricmc.loom.util.ZipUtils
class AsyncZipProcessorTest extends Specification {
def "process async"() {
given:
def inputZip = ZipTestUtils.createZip(createEntries())
def outputZip = ZipTestUtils.createZip(Collections.emptyMap())
Files.delete(outputZip)
when:
// Process the input zip asynchronously, converting all entries to uppercase
AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry ->
def str = Files.readString(inputEntry)
Files.writeString(outputEntry, str.toUpperCase())
}
then:
ZipUtils.unpack(outputZip, "file1.txt") == "FILE1".bytes
ZipUtils.unpack(outputZip, "file500.txt") == "FILE500".bytes
ZipUtils.unpack(outputZip, "file800.txt") == "FILE800".bytes
}
def "re throws"() {
given:
def inputZip = ZipTestUtils.createZip(createEntries())
def outputZip = ZipTestUtils.createZip(Collections.emptyMap())
Files.delete(outputZip)
when:
AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry ->
throw new IOException("Test exception")
}
then:
thrown(IOException)
}
Map<String, String> createEntries(int count = 10000) {
Map<String, String> entries = [:]
for (int i = 0; i < count; i++) {
entries.put("file" + i + ".txt", "file$i")
}
return entries
}
}

View File

@@ -0,0 +1,99 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2023 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import spock.lang.Specification
import net.fabricmc.loom.decompilers.ClassLineNumbers
class ClassLineNumbersTest extends Specification {
def "read linemap"() {
when:
def reader = new BufferedReader(new StringReader(LINE_MAP))
def lineNumbers = ClassLineNumbers.readMappings(reader)
def lineMap = lineNumbers.lineMap()
then:
lineMap.size() == 2
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].lineMap().size() == 39
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLine() == 203
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLineDest() == 187
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].lineMap().size() == 6
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLine() == 25
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLineDest() == 30
}
private static final String LINE_MAP = """
net/minecraft/server/dedicated/ServerPropertiesHandler\t203\t187
\t48\t187
\t91\t92
\t96\t97
\t110\t108
\t112\t109
\t113\t110
\t115\t111
\t116\t112
\t118\t113
\t119\t113
\t120\t113
\t122\t114
\t130\t115
\t147\t129
\t149\t131
\t151\t133
\t154\t136
\t158\t141
\t159\t142
\t163\t144
\t164\t145
\t165\t146
\t166\t147
\t168\t149
\t169\t150
\t170\t151
\t172\t153
\t175\t155
\t176\t156
\t177\t157
\t178\t158
\t181\t160
\t186\t165
\t187\t166
\t192\t171
\t194\t173
\t195\t174
\t197\t176
\t203\t182
net/minecraft/server/dedicated/ServerPropertiesLoader\t25\t30
\t11\t15
\t12\t16
\t16\t20
\t20\t24
\t24\t28
\t25\t30
"""
}

View File

@@ -0,0 +1,87 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import spock.lang.Specification
import net.fabricmc.loom.decompilers.cache.JarWalker
import net.fabricmc.loom.test.util.ZipTestUtils
import net.fabricmc.loom.util.FileSystemUtil
class JarWalkerTest extends Specification {
def "find classes in jar"() {
given:
def jar = ZipTestUtils.createZip([
"net/fabricmc/Test.class": "",
"net/fabricmc/other/Test.class": "",
"net/fabricmc/other/Test\$Inner.class": "",
"net/fabricmc/other/Test\$1.class": "",
])
when:
def entries = JarWalker.findClasses(jar)
then:
entries.size() == 2
entries[0].parentClass() == "net/fabricmc/Test.class"
entries[0].sourcesFileName() == "net/fabricmc/Test.java"
entries[0].innerClasses().size() == 0
entries[1].parentClass() == "net/fabricmc/other/Test.class"
entries[1].sourcesFileName() == "net/fabricmc/other/Test.java"
entries[1].innerClasses().size() == 2
entries[1].innerClasses()[0] == "net/fabricmc/other/Test\$1.class"
entries[1].innerClasses()[1] == "net/fabricmc/other/Test\$Inner.class"
}
def "Hash Classes"() {
given:
def jar = ZipTestUtils.createZip(zipEntries)
when:
def entries = JarWalker.findClasses(jar)
def hash = FileSystemUtil.getJarFileSystem(jar).withCloseable { fs ->
return entries[0].hash(fs.root)
}
then:
entries.size() == 1
hash == expectedHash
where:
expectedHash | zipEntries
"2339de144d8a4a1198adf8142b6d3421ec0baacea13c9ade42a93071b6d62e43" | [
"net/fabricmc/Test.class": "abc123",
]
"1053cfadf4e371ec89ff5b58d9b3bdb80373f3179e804b2e241171223709f4d1" | [
"net/fabricmc/other/Test.class": "Hello",
"net/fabricmc/other/Test\$Inner.class": "World",
"net/fabricmc/other/Test\$Inner\$2.class": "123",
"net/fabricmc/other/Test\$1.class": "test",
]
"f30b705f3a921b60103a4ee9951aff59b6db87cc289ba24563743d753acff433" | [
"net/fabricmc/other/Test.class": "Hello",
"net/fabricmc/other/Test\$Inner.class": "World",
"net/fabricmc/other/Test\$Inner\$2.class": "abc123",
"net/fabricmc/other/Test\$1.class": "test",
]
}
}

View File

@@ -0,0 +1,41 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import org.gradle.api.logging.LogLevel
import spock.lang.Specification
import net.fabricmc.loom.util.ProcessUtil
class ProcessUtilTest extends Specification {
def "print process info"() {
when:
def output = new ProcessUtil(LogLevel.DEBUG).printWithParents(ProcessHandle.current())
then:
// Just a simple check to see if the output is not empty
!output.isEmpty()
}
}

View File

@@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.time.ZoneId import java.time.ZoneId
import com.google.gson.JsonObject
import spock.lang.Specification import spock.lang.Specification
import net.fabricmc.loom.util.Checksum import net.fabricmc.loom.util.Checksum
@@ -188,4 +189,28 @@ class ZipUtilsTest extends Specification {
"Etc/GMT-6" | _ "Etc/GMT-6" | _
"Etc/GMT+9" | _ "Etc/GMT+9" | _
} }
def "transform json"() {
given:
def dir = File.createTempDir()
def zip = File.createTempFile("loom-zip-test", ".zip").toPath()
new File(dir, "test.json").text = """
{
"test": "This is a test of transforming"
}
"""
ZipUtils.pack(dir.toPath(), zip)
when:
ZipUtils.transformJson(JsonObject.class, zip, "test.json") { json ->
def test = json.get("test").getAsString()
json.addProperty("test", test.toUpperCase())
json
}
def transformed = ZipUtils.unpackJson(zip, "test.json", JsonObject.class)
then:
transformed.get("test").asString == "THIS IS A TEST OF TRANSFORMING"
}
} }

View File

@@ -0,0 +1,62 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.cache
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import spock.lang.Specification
import spock.lang.TempDir
import net.fabricmc.loom.decompilers.ClassLineNumbers
import net.fabricmc.loom.decompilers.cache.CachedData
class CachedDataTest extends Specification {
@TempDir
Path testPath
// Simple test to check if the CachedData class can be written and read from a file
def "Read + Write CachedData"() {
given:
def lineNumberEntry = new ClassLineNumbers.Entry("net/test/TestClass", 1, 2, [1: 2, 4: 7])
def cachedData = new CachedData("net/test/TestClass", "Example sources", lineNumberEntry)
def path = testPath.resolve("cachedData.bin")
when:
// Write the cachedData to a file
FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE).withCloseable {
cachedData.write(it)
}
// And read it back
def readCachedData = Files.newInputStream(path).withCloseable {
return CachedData.read(it)
}
then:
cachedData == readCachedData
}
}

View File

@@ -0,0 +1,132 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.cache
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import java.time.Duration
import java.time.Instant
import spock.lang.Specification
import spock.lang.TempDir
import net.fabricmc.loom.decompilers.cache.CachedFileStore
import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl
import net.fabricmc.loom.util.FileSystemUtil
class CachedFileStoreTest extends Specification {
@TempDir
Path testPath
FileSystemUtil.Delegate zipDelegate
Path root
void setup() {
zipDelegate = FileSystemUtil.getJarFileSystem(testPath.resolve("cache.zip"), true)
root = zipDelegate.get().getPath("/")
}
void cleanup() {
zipDelegate.close()
}
def "putEntry"() {
given:
def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7))
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
when:
store.putEntry("abc", "Hello world".bytes)
then:
Files.exists(root.resolve("abc"))
}
def "getEntry"() {
given:
def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7))
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
when:
store.putEntry("abc", "Hello world".bytes)
def entry = store.getEntry("abc")
def unknownEntry = store.getEntry("123")
then:
entry == "Hello world".bytes
unknownEntry == null
}
def "pruneManyFiles"() {
given:
def cacheRules = new CachedFileStoreImpl.CacheRules(250, Duration.ofDays(7))
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
when:
for (i in 0..<500) {
def key = "test_" + i
store.putEntry(key, "Hello world".bytes)
// Higher files are older and should be removed.
Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i)))
}
store.prune()
then:
Files.exists(root.resolve("test_0"))
Files.exists(root.resolve("test_100"))
Files.notExists(root.resolve("test_300"))
}
def "pruneOldFiles"() {
given:
def cacheRules = new CachedFileStoreImpl.CacheRules(1000, Duration.ofSeconds(250))
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
when:
for (i in 0..<500) {
def key = "test_" + i
store.putEntry(key, "Hello world".bytes)
// Higher files are older and should be removed.
Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i)))
}
store.prune()
then:
Files.exists(root.resolve("test_0"))
Files.exists(root.resolve("test_100"))
Files.notExists(root.resolve("test_300"))
}
private static CachedFileStore.EntrySerializer<byte[]> BYTE_ARRAY_SERIALIZER = new CachedFileStore.EntrySerializer<byte[]>() {
@Override
byte[] read(Path path) throws IOException {
return Files.readAllBytes(path)
}
@Override
void write(byte[] entry, Path path) throws IOException {
Files.write(path, entry)
}
}
}

View File

@@ -0,0 +1,241 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.cache
import java.nio.file.Files
import spock.lang.Specification
import net.fabricmc.loom.decompilers.ClassLineNumbers
import net.fabricmc.loom.decompilers.cache.CachedData
import net.fabricmc.loom.decompilers.cache.CachedFileStore
import net.fabricmc.loom.decompilers.cache.CachedJarProcessor
import net.fabricmc.loom.test.util.ZipTestUtils
import net.fabricmc.loom.util.ZipUtils
class CachedJarProcessorTest extends Specification {
static Map<String, String> jarEntries = [
"net/fabricmc/Example.class": "",
"net/fabricmc/other/Test.class": "",
"net/fabricmc/other/Test\$Inner.class": "",
"net/fabricmc/other/Test\$1.class": "",
]
static String ExampleHash = "abc123/cd372fb85148700fa88095e3492d3f9f5beb43e555e5ff26d95f5a6adc36f8e6"
static String TestHash = "abc123/ecd40b16ec50b636a390cb8da716a22606965f14e526e3051144dd567f336bc5"
static CachedData ExampleCachedData = new CachedData("net/fabricmc/Example", "Example sources", lineNumber("net/fabricmc/Example"))
static CachedData TestCachedData = new CachedData("net/fabricmc/other/Test", "Test sources", lineNumber("net/fabricmc/other/Test"))
def "prepare full work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob
then:
workRequest.lineNumbers() == null
workJob.outputNameMap().size() == 2
// Expect two calls looking for the existing entry in the cache
2 * cache.getEntry(_) >> null
0 * _ // Strict mock
}
def "prepare partial work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob
def lineMap = workRequest.lineNumbers().lineMap()
then:
lineMap.size() == 1
lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers()
workJob.outputNameMap().size() == 1
ZipUtils.unpackNullable(workJob.existing(), "net/fabricmc/Example.java") == "Example sources".bytes
// Provide one cached entry
// And then one call not finding the entry in the cache
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
1 * cache.getEntry(_) >> null
0 * _ // Strict mock
}
def "prepare completed work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob
def lineMap = workRequest.lineNumbers().lineMap()
then:
lineMap.size() == 2
lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers()
lineMap.get("net/fabricmc/other/Test") == TestCachedData.lineNumbers()
workJob.completed() != null
ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/Example.java") == "Example sources".bytes
ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/other/Test.java") == "Test sources".bytes
// Provide one cached entry
// And then two calls not finding the entry in the cache
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
1 * cache.getEntry(TestHash) >> TestCachedData
0 * _ // Strict mock
}
def "complete full work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob
// Do the work, such as decompiling.
ZipUtils.add(workJob.output(), "net/fabricmc/Example.java", "Example sources")
ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources")
def outputJar = Files.createTempFile("loom-test-output", ".jar")
Files.delete(outputJar)
ClassLineNumbers lineNumbers = lineNumbers([
"net/fabricmc/Example",
"net/fabricmc/other/Test"
])
processor.completeJob(outputJar, workJob, lineNumbers)
then:
workJob.outputNameMap().size() == 2
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
// Expect two calls looking for the existing entry in the cache
1 * cache.getEntry(ExampleHash) >> null
1 * cache.getEntry(TestHash) >> null
// Expect the new work to be put into the cache
1 * cache.putEntry(ExampleHash, ExampleCachedData)
1 * cache.putEntry(TestHash, TestCachedData)
0 * _ // Strict mock
}
def "complete partial work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob
// Do the work
ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources")
def outputJar = Files.createTempFile("loom-test-output", ".jar")
Files.delete(outputJar)
ClassLineNumbers lineNumbers = lineNumbers([
"net/fabricmc/Example",
"net/fabricmc/other/Test"
])
processor.completeJob(outputJar, workJob, lineNumbers)
then:
workJob.outputNameMap().size() == 1
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
// The cache already contains sources for example, but not for test
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
1 * cache.getEntry(TestHash) >> null
// Expect the new work to be put into the cache
1 * cache.putEntry(TestHash, TestCachedData)
0 * _ // Strict mock
}
def "complete completed work job"() {
given:
def jar = ZipTestUtils.createZip(jarEntries)
def cache = Mock(CachedFileStore)
def processor = new CachedJarProcessor(cache, "abc123")
when:
def workRequest = processor.prepareJob(jar)
def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob
def outputJar = Files.createTempFile("loom-test-output", ".jar")
Files.delete(outputJar)
ClassLineNumbers lineNumbers = lineNumbers([
"net/fabricmc/Example",
"net/fabricmc/other/Test"
])
processor.completeJob(outputJar, workJob, lineNumbers)
then:
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
// The cache already contains sources for example, but not for test
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
1 * cache.getEntry(TestHash) >> TestCachedData
0 * _ // Strict mock
}
private static ClassLineNumbers lineNumbers(List<String> names) {
return new ClassLineNumbers(names.collectEntries { [it, lineNumber(it)] })
}
private static ClassLineNumbers.Entry lineNumber(String name) {
return new ClassLineNumbers.Entry(name, 0, 0, [:])
}
}

View File

@@ -0,0 +1,128 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.fmj
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import org.intellij.lang.annotations.Language
import spock.lang.Specification
import net.fabricmc.loom.util.fmj.FabricModJsonUtils
class FabricModJsonUtilsTest extends Specification {
// Test that the schemaVersion is moved to the first position
def "optimize FMJ"() {
given:
// Matches LoomGradlePlugin
def gson = new GsonBuilder().setPrettyPrinting().create()
def json = gson.fromJson(INPUT_FMJ, JsonObject.class)
when:
def outputJson = FabricModJsonUtils.optimizeFmj(json)
def output = gson.toJson(outputJson)
then:
output == OUTPUT_FMJ
true
}
// schemaVersion is not first
@Language("json")
static String INPUT_FMJ = """
{
"id": "modid",
"version": "1.0.0",
"name": "Example mod",
"description": "This is an example description! Tell everyone what your mod is about!",
"license": "CC0-1.0",
"icon": "assets/modid/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"com.example.ExampleMod"
],
"client": [
"com.example.ExampleModClient"
]
},
"schemaVersion": 1,
"mixins": [
"modid.mixins.json",
{
"config": "modid.client.mixins.json",
"environment": "client"
}
],
"depends": {
"fabricloader": "\\u003e\\u003d0.15.0",
"minecraft": "~1.20.4",
"java": "\\u003e\\u003d17",
"fabric-api": "*"
},
"suggests": {
"another-mod": "*"
}
}
""".trim()
// schemaVersion is first, everything else is unchanged
@Language("json")
static String OUTPUT_FMJ = """
{
"schemaVersion": 1,
"id": "modid",
"version": "1.0.0",
"name": "Example mod",
"description": "This is an example description! Tell everyone what your mod is about!",
"license": "CC0-1.0",
"icon": "assets/modid/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"com.example.ExampleMod"
],
"client": [
"com.example.ExampleModClient"
]
},
"mixins": [
"modid.mixins.json",
{
"config": "modid.client.mixins.json",
"environment": "client"
}
],
"depends": {
"fabricloader": "\\u003e\\u003d0.15.0",
"minecraft": "~1.20.4",
"java": "\\u003e\\u003d17",
"fabric-api": "*"
},
"suggests": {
"another-mod": "*"
}
}
""".trim()
}

View File

@@ -30,6 +30,7 @@ class IntermediaryMappingLayerTest extends LayeredMappingsSpecification {
def "Read intermediary mappings" () { def "Read intermediary mappings" () {
setup: setup:
intermediaryUrl = INTERMEDIARY_1_17_URL intermediaryUrl = INTERMEDIARY_1_17_URL
mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17
when: when:
def mappings = getSingleMapping(new IntermediaryMappingsSpec()) def mappings = getSingleMapping(new IntermediaryMappingsSpec())
def tiny = getTiny(mappings) def tiny = getTiny(mappings)

View File

@@ -34,13 +34,13 @@ interface LayeredMappingsTestConstants {
client_mappings: new MinecraftVersionMeta.Download(null, "227d16f520848747a59bef6f490ae19dc290a804", 6431705, "https://launcher.mojang.com/v1/objects/227d16f520848747a59bef6f490ae19dc290a804/client.txt"), client_mappings: new MinecraftVersionMeta.Download(null, "227d16f520848747a59bef6f490ae19dc290a804", 6431705, "https://launcher.mojang.com/v1/objects/227d16f520848747a59bef6f490ae19dc290a804/client.txt"),
server_mappings: new MinecraftVersionMeta.Download(null, "84d80036e14bc5c7894a4fad9dd9f367d3000334", 4948536, "https://launcher.mojang.com/v1/objects/84d80036e14bc5c7894a4fad9dd9f367d3000334/server.txt") server_mappings: new MinecraftVersionMeta.Download(null, "84d80036e14bc5c7894a4fad9dd9f367d3000334", 4948536, "https://launcher.mojang.com/v1/objects/84d80036e14bc5c7894a4fad9dd9f367d3000334/server.txt")
] ]
public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, null, null, null) public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, "2021-06-08T11:00:40+00:00", null, null, null)
public static final Map<String, MinecraftVersionMeta.Download> DOWNLOADS_1_16_5 = [ public static final Map<String, MinecraftVersionMeta.Download> DOWNLOADS_1_16_5 = [
client_mappings: new MinecraftVersionMeta.Download(null, "e3dfb0001e1079a1af72ee21517330edf52e6192", 5746047, "https://launcher.mojang.com/v1/objects/e3dfb0001e1079a1af72ee21517330edf52e6192/client.txt"), client_mappings: new MinecraftVersionMeta.Download(null, "e3dfb0001e1079a1af72ee21517330edf52e6192", 5746047, "https://launcher.mojang.com/v1/objects/e3dfb0001e1079a1af72ee21517330edf52e6192/client.txt"),
server_mappings: new MinecraftVersionMeta.Download(null, "81d5c793695d8cde63afddb40dde88e3a88132ac", 4400926, "https://launcher.mojang.com/v1/objects/81d5c793695d8cde63afddb40dde88e3a88132ac/server.txt") server_mappings: new MinecraftVersionMeta.Download(null, "81d5c793695d8cde63afddb40dde88e3a88132ac", 4400926, "https://launcher.mojang.com/v1/objects/81d5c793695d8cde63afddb40dde88e3a88132ac/server.txt")
] ]
public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, null, null, null) public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, "2021-01-14T16:05:32+00:00", null, null, null)
public static final String PARCHMENT_NOTATION = "org.parchmentmc.data:parchment-1.16.5:20210608-SNAPSHOT@zip" public static final String PARCHMENT_NOTATION = "org.parchmentmc.data:parchment-1.16.5:20210608-SNAPSHOT@zip"
public static final String PARCHMENT_URL = "https://maven.parchmentmc.net/org/parchmentmc/data/parchment-1.16.5/20210608-SNAPSHOT/parchment-1.16.5-20210608-SNAPSHOT.zip" public static final String PARCHMENT_URL = "https://maven.parchmentmc.net/org/parchmentmc/data/parchment-1.16.5/20210608-SNAPSHOT/parchment-1.16.5-20210608-SNAPSHOT.zip"

View File

@@ -45,7 +45,7 @@ trait GradleProjectTestTrait {
String gradleVersion = options.version as String ?: LoomTestConstants.DEFAULT_GRADLE String gradleVersion = options.version as String ?: LoomTestConstants.DEFAULT_GRADLE
String warningMode = options.warningMode as String ?: "fail" String warningMode = options.warningMode as String ?: "fail"
File projectDir = options.projectDir as File ?: options.sharedFiles ? sharedProjectDir : File.createTempDir() File projectDir = options.projectDir as File ?: options.sharedFiles ? sharedProjectDir : File.createTempDir()
File gradleHomeDir = gradleHomeDir File gradleHomeDir = options.gradleHomeDir as File ?: gradleHomeDir
setupProject(options, projectDir) setupProject(options, projectDir)

View File

@@ -0,0 +1,2 @@
The file `1.2.5-intermediary.tiny` was taken from OrnitheMC's "[Calamus](https://github.com/OrnitheMC/calamus/blob/gen2/mappings/1.2.5.tiny)" intermediaries under the CC0 license.
The file was rewritten in Tiny V2 format, but the mappings are otherwise unmodified.

View File

@@ -49,4 +49,8 @@ dependencies {
base { base {
archivesName = "fabric-example-mod" archivesName = "fabric-example-mod"
}
runClient {
// Realise this task to ensure that the runConfig is lazily evaluated
} }