diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 561962a6..e3e2d2d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,8 +10,8 @@ jobs: strategy: fail-fast: false matrix: - version: [7.4.0-jdk17] - runs-on: ubuntu-20.04 + version: [7.5.0-jdk17] + runs-on: ubuntu-22.04 container: image: gradle:${{ matrix.version }} options: --user root @@ -25,9 +25,9 @@ jobs: # Lets wait to ensure it builds before going running tests needs: build - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: - image: gradle:7.4.0-jdk17 + image: gradle:7.5.0-jdk17 options: --user root steps: @@ -49,7 +49,7 @@ jobs: version: [7.4.0-jdk17] test: ${{ fromJson(needs.prepare_test_matrix.outputs.matrix) }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: image: gradle:${{ matrix.version }} options: --user root @@ -100,8 +100,8 @@ jobs: strategy: fail-fast: false matrix: - java: [ 17 ] - os: [ windows-2022, ubuntu-20.04, macos-11 ] + java: [ 17, 18 ] + os: [ windows-2022, ubuntu-22.04, macos-11 ] runs-on: ${{ matrix.os }} steps: diff --git a/build.gradle b/build.gradle index 65e365e3..20811d06 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,8 @@ plugins { id 'checkstyle' id 'jacoco' id 'codenarc' - id "org.jetbrains.kotlin.jvm" version "1.5.31" // Must match the version included with gradle. - id "com.diffplug.spotless" version "6.3.0" + id "org.jetbrains.kotlin.jvm" version "1.6.10" // Must match the version included with gradle. + id "com.diffplug.spotless" version "6.8.0" } java { @@ -24,7 +24,7 @@ tasks.withType(JavaCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - jvmTarget = "16" // Change to 17 when updating gradle/kotlin to 1.6.10 + jvmTarget = "17" } } @@ -88,10 +88,11 @@ dependencies { implementation ('org.ow2.asm:asm-commons:9.3') implementation ('org.ow2.asm:asm-tree:9.3') implementation ('org.ow2.asm:asm-util:9.3') + implementation ('com.github.mizosoft.methanol:methanol:1.7.0') implementation ('me.tongfei:progressbar:0.9.0') // game handling utils - implementation ('net.fabricmc:stitch:0.6.1') { + implementation ('net.fabricmc:stitch:0.6.2') { exclude module: 'mercury' exclude module: 'enigma' } @@ -120,7 +121,7 @@ dependencies { } // Kapt integration - compileOnly('org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31') // Must match the version included with gradle. + compileOnly('org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10') // Must match the version included with gradle. // Forge patches implementation ('net.minecraftforge:installertools:1.2.0') @@ -177,11 +178,11 @@ spotless { checkstyle { configFile = file('checkstyle.xml') - toolVersion = '9.3' + toolVersion = '10.3.1' } codenarc { - toolVersion = "2.2.0" + toolVersion = "3.1.0" configFile = file("codenarc.groovy") } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d7e66b5c..2ec77e51 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index e6764ca4..35401ac2 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -55,6 +55,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftPr import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.util.ModPlatform; +import net.fabricmc.loom.util.download.DownloadBuilder; public interface LoomGradleExtension extends LoomGradleExtensionAPI { static LoomGradleExtension get(Project project) { @@ -124,6 +125,12 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { void addTransitiveAccessWideners(List accessWidenerFiles); + DownloadBuilder download(String url); + + boolean refreshDeps(); + + void setRefreshDeps(boolean refreshDeps); + // =================== // Architectury Loom // =================== diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index d9f78437..0184f581 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -51,7 +51,6 @@ import net.fabricmc.loom.task.LoomTasks; import net.fabricmc.loom.util.LibraryLocationLogger; public class LoomGradlePlugin implements BootstrappedPlugin { - public static boolean refreshDeps; public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); public static final String LOOM_VERSION = Objects.requireNonNullElse(LoomGradlePlugin.class.getPackage().getImplementationVersion(), "0.0.0+unknown"); @@ -76,12 +75,6 @@ public class LoomGradlePlugin implements BootstrappedPlugin { LibraryLocationLogger.logLibraryVersions(); - refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh"); - - if (refreshDeps) { - project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower."); - } - // Apply default plugins project.apply(ImmutableMap.of("plugin", "java-library")); project.apply(ImmutableMap.of("plugin", "eclipse")); diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index c35be5a2..3aea7ff4 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -61,12 +61,6 @@ public interface LoomGradleExtensionAPI { RegularFileProperty getAccessWidenerPath(); - Property getShareRemapCaches(); - - default void shareCaches() { - getShareRemapCaches().set(true); - } - NamedDomainObjectContainer getDecompilerOptions(); void decompilers(Action> action); diff --git a/src/main/java/net/fabricmc/loom/api/MixinExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/MixinExtensionAPI.java index 2de8e8da..6b0f41a4 100644 --- a/src/main/java/net/fabricmc/loom/api/MixinExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/MixinExtensionAPI.java @@ -25,6 +25,7 @@ package net.fabricmc.loom.api; import org.gradle.api.Action; +import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.util.PatternSet; @@ -38,6 +39,10 @@ public interface MixinExtensionAPI { Property getRefmapTargetNamespace(); + MapProperty getMessages(); + + Property getShowMessageTypes(); + /** * Apply Mixin AP to sourceSet. * @param sourceSet the sourceSet that applies Mixin AP. @@ -95,4 +100,6 @@ public interface MixinExtensionAPI { * @param sourceSetName the name of sourceSet that applies Mixin AP. */ void add(String sourceSetName); + + void messages(Action> action); } diff --git a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java index f6b578a3..594a74b8 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java @@ -26,11 +26,14 @@ package net.fabricmc.loom.api.mappings.intermediate; import java.io.IOException; import java.nio.file.Path; +import java.util.function.Function; import org.gradle.api.Named; import org.gradle.api.provider.Property; import org.jetbrains.annotations.ApiStatus; +import net.fabricmc.loom.util.download.DownloadBuilder; + /** * A simple API to allow 3rd party plugins. * Implement by creating an abstract class overriding provide and getName @@ -39,6 +42,8 @@ import org.jetbrains.annotations.ApiStatus; public abstract class IntermediateMappingsProvider implements Named { public abstract Property getMinecraftVersion(); + public abstract Property> getDownloader(); + /** * Generate or download a tinyv2 mapping file with intermediary and named namespaces. * @throws IOException diff --git a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingContext.java b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingContext.java index ffb96d59..6c5c0157 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingContext.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingContext.java @@ -32,6 +32,7 @@ import org.gradle.api.logging.Logger; import org.jetbrains.annotations.ApiStatus; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; +import net.fabricmc.loom.util.download.DownloadBuilder; import net.fabricmc.mappingio.tree.MemoryMappingTree; @ApiStatus.Experimental /* Very Experimental and not cleanly separated from the impl atm */ @@ -54,4 +55,8 @@ public interface MappingContext { Path workingDirectory(String name); Logger getLogger(); + + DownloadBuilder download(String url); + + boolean refreshDeps(); } diff --git a/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileSpec.java b/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileSpec.java index faff73e8..c8b6157e 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileSpec.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/layered/spec/FileSpec.java @@ -115,7 +115,7 @@ public interface FileSpec { } static FileSpec createFromUrl(URL url) { - return new URLFileSpec(url); + return new URLFileSpec(url.toString()); } // Note resolved instantly, this is not lazy diff --git a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java index 472c35ff..e98a98b5 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java @@ -32,6 +32,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.gradle.api.Project; @@ -58,7 +60,11 @@ public abstract class AnnotationProcessorInvoker { public static final String SCALA = "scala"; public static final String GROOVY = "groovy"; + private static final Pattern MSG_KEY_PATTERN = Pattern.compile("^[A-Z]+[A-Z_]+$"); + private static final Pattern MSG_VALUE_PATTERN = Pattern.compile("^(note|warning|error|disabled)$"); + protected final Project project; + protected final MixinExtension mixinExtension; protected final Map invokerTasks; private final Collection apConfigurations; @@ -66,6 +72,7 @@ public abstract class AnnotationProcessorInvoker { Collection apConfigurations, Map invokerTasks) { this.project = project; + this.mixinExtension = LoomGradleExtension.get(project).getMixin(); this.apConfigurations = apConfigurations; this.invokerTasks = invokerTasks; } @@ -96,6 +103,17 @@ public abstract class AnnotationProcessorInvoker { put(Constants.MixinArguments.QUIET, "true"); }}; + if (mixinExtension.getShowMessageTypes().get()) { + args.put(Constants.MixinArguments.SHOW_MESSAGE_TYPES, "true"); + } + + mixinExtension.getMessages().get().forEach((key, value) -> { + checkPattern(key, MSG_KEY_PATTERN); + checkPattern(value, MSG_VALUE_PATTERN); + + args.put("AMSG_" + key, value); + }); + project.getLogger().debug("Outputting refmap to dir: " + getRefmapDestinationDir(task) + " for compile task: " + task); args.forEach((k, v) -> passArgument(task, k, v)); } catch (IOException e) { @@ -132,4 +150,12 @@ public abstract class AnnotationProcessorInvoker { passMixinArguments(entry.getValue(), entry.getKey()); } } + + private static void checkPattern(String input, Pattern pattern) { + final Matcher matcher = pattern.matcher(input); + + if (!matcher.find()) { + throw new IllegalArgumentException("Mixin argument (%s) does not match pattern (%s)".formatted(input, pattern.toString())); + } + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 35a3f939..c4049e3b 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -26,7 +26,10 @@ package net.fabricmc.loom.configuration; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Set; @@ -35,7 +38,6 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.AbstractCopyTask; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.compile.JavaCompile; @@ -181,7 +183,6 @@ public final class CompileConfiguration { } public static void configureCompile(Project p) { - final JavaPluginExtension javaPluginExtension = p.getExtensions().getByType(JavaPluginExtension.class); LoomGradleExtension extension = LoomGradleExtension.get(p); p.getTasks().named(JavaPlugin.JAVADOC_TASK_NAME, Javadoc.class).configure(javadoc -> { @@ -192,6 +193,13 @@ public final class CompileConfiguration { p.afterEvaluate(project -> { MinecraftSourceSets.get(project).afterEvaluate(project); + final boolean previousRefreshDeps = extension.refreshDeps(); + + if (getAndLock(project)) { + project.getLogger().lifecycle("Found existing cache lock file, rebuilding loom cache. This may have been caused by a failed or canceled build."); + extension.setRefreshDeps(true); + } + try { setupMinecraft(project); } catch (Exception e) { @@ -202,6 +210,9 @@ public final class CompileConfiguration { extension.setDependencyManager(dependencyManager); dependencyManager.handleDependencies(project); + releaseLock(project); + extension.setRefreshDeps(previousRefreshDeps); + MixinExtension mixin = LoomGradleExtension.get(project).getMixin(); if (mixin.getUseLegacyMixinAp().get()) { @@ -396,6 +407,38 @@ public final class CompileConfiguration { .apply(project, extension.getNamedMinecraftProvider()).afterEvaluation(); } + private static Path getLockFile(Project project) { + final LoomGradleExtension extension = LoomGradleExtension.get(project); + final Path cacheDirectory = extension.getFiles().getProjectPersistentCache().toPath(); + return cacheDirectory.resolve("configuration.lock"); + } + + private static boolean getAndLock(Project project) { + final Path lock = getLockFile(project); + + if (Files.exists(lock)) { + return true; + } + + try { + Files.createFile(lock); + } catch (IOException e) { + throw new UncheckedIOException("Failed to acquire project configuration lock", e); + } + + return false; + } + + private static void releaseLock(Project project) { + final Path lock = getLockFile(project); + + try { + Files.deleteIfExists(lock); + } catch (IOException e) { + throw new UncheckedIOException("Failed to release project configuration lock", e); + } + } + public static void extendsFrom(List parents, String b, Project project) { for (String parent : parents) { extendsFrom(parent, b, project); diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java index 15f5d2a0..e043fef9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java @@ -25,8 +25,7 @@ package net.fabricmc.loom.configuration; import java.io.File; -import java.io.IOException; -import java.net.URL; +import java.io.UncheckedIOException; import java.util.HashMap; import java.util.Map; @@ -40,7 +39,7 @@ import org.w3c.dom.Element; import org.w3c.dom.NodeList; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.download.DownloadException; public class FabricApiExtension { private final Project project; @@ -116,10 +115,11 @@ public class FabricApiExtension { } try { - URL url = new URL(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion)); - DownloadUtil.downloadIfChanged(url, mavenPom, project.getLogger()); - } catch (IOException e) { - throw new RuntimeException("Failed to download maven info for " + fabricApiVersion); + extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion)) + .defaultCache() + .downloadPath(mavenPom.toPath()); + } catch (DownloadException e) { + throw new UncheckedIOException("Failed to download maven info for " + fabricApiVersion, e); } return mavenPom; diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java index c5228ad6..1ff85372 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -326,7 +327,7 @@ public class RunConfig { char c = s.charAt(i); if (c == '@' && i > 0 && s.charAt(i - 1) == '@' || c == ' ') { - ret.append(String.format("@@%04x", (int) c)); + ret.append(String.format(Locale.ENGLISH, "@@%04x", (int) c)); } else { ret.append(c); } diff --git a/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java b/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java index 40be05c2..69b28cdf 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java @@ -37,7 +37,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.base.Preconditions; import com.google.common.hash.Hasher; @@ -180,19 +182,22 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource return result; } + // Find the injected interfaces from mods that are both on the compile and runtime classpath. + // Runtime is also required to ensure that the interface and it's impl is present when running the mc jar. private List getDependencyInjectedInterfaces() { - List result = new ArrayList<>(); + final Function> resolve = settings -> + settings.getSourceConfiguration().get().resolve().stream() + .map(File::toPath); - // Only apply injected interfaces from mods that are part of the compile classpath - for (RemapConfigurationSettings entry : extension.getCompileRemapConfigurations()) { - final Set artifacts = entry.getSourceConfiguration().get().resolve(); + final List runtimeEntries = extension.getRuntimeRemapConfigurations().stream() + .flatMap(resolve) + .toList(); - for (File artifact : artifacts) { - result.addAll(InjectedInterface.fromModJar(artifact.toPath())); - } - } - - return result; + return extension.getCompileRemapConfigurations().stream() + .flatMap(resolve) + .filter(runtimeEntries::contains) // Use the intersection of the two configurations. + .flatMap(path -> InjectedInterface.fromModJar(path).stream()) + .toList(); } private List getSourceInjectedInterface(SourceSet sourceSet) { diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java index 984cff20..a33e41fe 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java @@ -49,7 +49,6 @@ import org.gradle.language.base.artifact.SourcesArtifact; import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.configuration.processors.dependency.ModDependencyInfo; import net.fabricmc.loom.configuration.processors.dependency.RemapData; @@ -67,7 +66,6 @@ public class ModConfigurationRemapper { public static void supplyModConfigurations(Project project, String mappingsSuffix, LoomGradleExtension extension, SourceRemapper sourceRemapper) { final DependencyHandler dependencies = project.getDependencies(); - final boolean refreshDeps = LoomGradlePlugin.refreshDeps; final File modStore = extension.getFiles().getRemappedModCache(); final RemapData remapData = new RemapData(mappingsSuffix, modStore); @@ -99,7 +97,7 @@ public class ModConfigurationRemapper { final ModDependencyInfo info = new ModDependencyInfo(artifact, remappedConfig, clientRemappedConfig, remapData); - if (refreshDeps) { + if (extension.refreshDeps()) { info.forceRemap(); } @@ -198,7 +196,7 @@ public class ModConfigurationRemapper { return; } - if (!output.exists() || input.lastModified() <= 0 || input.lastModified() > output.lastModified() || LoomGradlePlugin.refreshDeps) { + if (!output.exists() || input.lastModified() <= 0 || input.lastModified() > output.lastModified() || LoomGradleExtension.get(project).refreshDeps()) { sourceRemapper.scheduleRemapSources(input, output, false, true); // Depenedency sources are used in ide only so don't need to be reproducable } else { project.getLogger().info(output.getName() + " is up to date with " + input.getName()); diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java index 86dde13d..a78f0bf0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -34,6 +34,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.jar.Manifest; @@ -96,10 +97,10 @@ public class ModProcessor { } try { - project.getLogger().lifecycle(":remapping %d mods from %s".formatted(remapList.size(), sourceConfiguration.getName())); + project.getLogger().lifecycle(":remapping {} mods from {}", remapList.size(), sourceConfiguration.getName()); remapJars(remapList); } catch (Exception e) { - project.getLogger().error("Failed to remap %d mods".formatted(remapList.size()), e); + project.getLogger().error(String.format(Locale.ENGLISH, "Failed to remap %d mods", remapList.size()), e); for (ModDependencyInfo info : remapList) { Files.deleteIfExists(info.getRemappedOutput().toPath()); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/GradleMappingContext.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/GradleMappingContext.java index 39fe06db..fdbd27d0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/GradleMappingContext.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/GradleMappingContext.java @@ -37,6 +37,7 @@ import org.gradle.api.logging.Logger; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingContext; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; +import net.fabricmc.loom.util.download.DownloadBuilder; import net.fabricmc.mappingio.tree.MemoryMappingTree; public class GradleMappingContext implements MappingContext { @@ -83,6 +84,16 @@ public class GradleMappingContext implements MappingContext { return project.getLogger(); } + @Override + public DownloadBuilder download(String url) { + return extension.download(url); + } + + @Override + public boolean refreshDeps() { + return extension.refreshDeps(); + } + public Project getProject() { return project; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java index b29c70f3..d7aeb51f 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediaryMappingsProvider.java @@ -25,7 +25,6 @@ package net.fabricmc.loom.configuration.providers.mappings; import java.io.IOException; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -35,32 +34,35 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; -import net.fabricmc.loom.util.DownloadUtil; public abstract class IntermediaryMappingsProvider extends IntermediateMappingsProvider { private static final Logger LOGGER = LoggerFactory.getLogger(IntermediateMappingsProvider.class); public abstract Property getIntermediaryUrl(); + public abstract Property getRefreshDeps(); + @Override public void provide(Path tinyMappings) throws IOException { - if (Files.exists(tinyMappings) && !LoomGradlePlugin.refreshDeps) { + if (Files.exists(tinyMappings) && !getRefreshDeps().get()) { return; } // Download and extract intermediary final Path intermediaryJarPath = Files.createTempFile(getName(), ".jar"); final String encodedMcVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftVersion().get()); - final URL url = new URL(getIntermediaryUrl().get().formatted(encodedMcVersion)); + final String url = getIntermediaryUrl().get().formatted(encodedMcVersion); LOGGER.info("Downloading intermediary from {}", url); Files.deleteIfExists(tinyMappings); Files.deleteIfExists(intermediaryJarPath); - DownloadUtil.downloadIfChanged(url, intermediaryJarPath.toFile(), LOGGER); + getDownloader().get().apply(url) + .defaultCache() + .downloadPath(intermediaryJarPath); + MappingsProviderImpl.extractMappings(intermediaryJarPath, tinyMappings); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpec.java index cb09d71f..9f49556a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpec.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingSpec.java @@ -25,12 +25,13 @@ package net.fabricmc.loom.configuration.providers.mappings; import java.util.List; +import java.util.Locale; import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec; public record LayeredMappingSpec(List> layers) { public String getVersion() { // TODO something better? - return "layered+hash.%d".formatted(Math.abs(hashCode())); + return String.format(Locale.ENGLISH, "layered+hash.%d", Math.abs(hashCode())); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsDependency.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsDependency.java index d529a333..2d54b4af 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsDependency.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/LayeredMappingsDependency.java @@ -88,7 +88,7 @@ public class LayeredMappingsDependency extends AbstractModuleDependency implemen Path mappingsDir = mappingContext.minecraftProvider().dir("layered").toPath(); Path mappingsFile = mappingsDir.resolve(String.format("%s.%s-%s.tiny", GROUP, MODULE, getVersion())); - if (!Files.exists(mappingsFile) || LoomGradlePlugin.refreshDeps) { + if (!Files.exists(mappingsFile) || mappingContext.refreshDeps()) { try { var processor = new LayeredMappingsProcessor(layeredMappingSpec); List layers = processor.resolveLayers(mappingContext); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java index 3fa50eec..6ac1e996 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingsProviderImpl.java @@ -191,11 +191,11 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService { } protected void setup(Project project, MinecraftProvider minecraftProvider, Path inputJar) throws IOException { - if (isRefreshDeps()) { + if (minecraftProvider.refreshDeps()) { cleanWorkingDirectory(mappingsWorkingDir); } - if (Files.notExists(tinyMappings) || isRefreshDeps()) { + if (Files.notExists(tinyMappings) || minecraftProvider.refreshDeps()) { storeMappings(project, minecraftProvider, inputJar); } else { try (FileSystem fileSystem = FileSystems.newFileSystem(inputJar, (ClassLoader) null)) { @@ -203,7 +203,7 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService { } } - if (Files.notExists(tinyMappingsJar) || isRefreshDeps()) { + if (Files.notExists(tinyMappingsJar) || minecraftProvider.refreshDeps()) { Files.deleteIfExists(tinyMappingsJar); ZipUtils.add(tinyMappingsJar, "mappings/mappings.tiny", Files.readAllBytes(tinyMappings)); } @@ -547,10 +547,6 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService { public record UnpickMetadata(String unpickGroup, String unpickVersion) { } - protected static boolean isRefreshDeps() { - return LoomGradlePlugin.refreshDeps; - } - @Override public void close() throws IOException { mappingTree = null; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java index 5d970e5e..f8a7b8ae 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingLayer.java @@ -26,7 +26,6 @@ package net.fabricmc.loom.configuration.providers.mappings.mojmap; import java.io.BufferedReader; import java.io.IOException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -46,19 +45,14 @@ import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.format.ProGuardReader; public record MojangMappingLayer(String minecraftVersion, - MinecraftVersionMeta.Download clientDownload, - MinecraftVersionMeta.Download serverDownload, - Path workingDir, boolean nameSyntheticMembers, + Path clientMappings, + Path serverMappings, + boolean nameSyntheticMembers, Logger logger, MojangMappingsSpec.SilenceLicenseOption silenceLicense) implements MappingLayer { private static final Pattern SYNTHETIC_NAME_PATTERN = Pattern.compile("^(access|this|val\\$this|lambda\\$.*)\\$[0-9]+$"); @Override public void visit(MappingVisitor mappingVisitor) throws IOException { - Path clientMappings = workingDir().resolve("%s.client.txt".formatted(minecraftVersion)); - Path serverMappings = workingDir().resolve("%s.server.txt".formatted(minecraftVersion)); - - download(clientMappings, serverMappings); - if (!silenceLicense.isSilent()) { printMappingsLicense(clientMappings); } @@ -76,11 +70,6 @@ public record MojangMappingLayer(String minecraftVersion, } } - private void download(Path clientMappings, Path serverMappings) throws IOException { - HashedDownloadUtil.downloadIfInvalid(new URL(clientDownload().url()), clientMappings.toFile(), clientDownload().sha1(), logger(), false); - HashedDownloadUtil.downloadIfInvalid(new URL(serverDownload().url()), serverMappings.toFile(), serverDownload().sha1(), logger(), false); - } - private void printMappingsLicense(Path clientMappings) { try (BufferedReader clientBufferedReader = Files.newBufferedReader(clientMappings, StandardCharsets.UTF_8)) { logger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java index 43b33c24..37eae9da 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/mojmap/MojangMappingsSpec.java @@ -24,9 +24,13 @@ package net.fabricmc.loom.configuration.providers.mappings.mojmap; +import java.io.UncheckedIOException; +import java.nio.file.Path; + import net.fabricmc.loom.api.mappings.layered.MappingContext; import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; +import net.fabricmc.loom.util.download.DownloadException; public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean nameSyntheticMembers) implements MappingsSpec { // Keys in dependency manifest @@ -71,17 +75,33 @@ public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean na @Override public MojangMappingLayer createLayer(MappingContext context) { - MinecraftVersionMeta versionInfo = context.minecraftProvider().getVersionInfo(); + final MinecraftVersionMeta versionInfo = context.minecraftProvider().getVersionInfo(); + final MinecraftVersionMeta.Download clientDownload = versionInfo.download(MANIFEST_CLIENT_MAPPINGS); + final MinecraftVersionMeta.Download serverDownload = versionInfo.download(MANIFEST_SERVER_MAPPINGS); - if (versionInfo.download(MANIFEST_CLIENT_MAPPINGS) == null) { + if (clientDownload == null) { throw new RuntimeException("Failed to find official mojang mappings for " + context.minecraftVersion()); } + final Path clientMappings = context.workingDirectory("mojang").resolve("client.txt"); + final Path serverMappings = context.workingDirectory("mojang").resolve("server.txt"); + + try { + context.download(clientDownload.url()) + .sha1(clientDownload.sha1()) + .downloadPath(clientMappings); + + context.download(serverDownload.url()) + .sha1(serverDownload.sha1()) + .downloadPath(serverMappings); + } catch (DownloadException e) { + throw new UncheckedIOException("Failed to download mappings", e); + } + return new MojangMappingLayer( context.minecraftVersion(), - versionInfo.download(MANIFEST_CLIENT_MAPPINGS), - versionInfo.download(MANIFEST_SERVER_MAPPINGS), - context.workingDirectory("mojang"), + clientMappings, + serverMappings, nameSyntheticMembers(), context.getLogger(), silenceLicense() diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/utils/URLFileSpec.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/utils/URLFileSpec.java index 88a6808a..5088c463 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/utils/URLFileSpec.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/utils/URLFileSpec.java @@ -24,10 +24,9 @@ package net.fabricmc.loom.configuration.providers.mappings.utils; -import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URL; import java.nio.file.Path; +import java.util.Locale; import java.util.Objects; import org.slf4j.Logger; @@ -35,18 +34,20 @@ import org.slf4j.LoggerFactory; import net.fabricmc.loom.api.mappings.layered.MappingContext; import net.fabricmc.loom.api.mappings.layered.spec.FileSpec; -import net.fabricmc.loom.util.DownloadUtil; +import net.fabricmc.loom.util.download.DownloadException; -public record URLFileSpec(URL url) implements FileSpec { +public record URLFileSpec(String url) implements FileSpec { private static final Logger LOGGER = LoggerFactory.getLogger(URLFileSpec.class); @Override public Path get(MappingContext context) { try { - Path output = context.workingDirectory("%d.URLFileSpec".formatted(Objects.hash(url.toString()))); + Path output = context.workingDirectory(String.format(Locale.ENGLISH, "%d.URLFileSpec", Objects.hash(url))); LOGGER.info("Downloading {} to {}", url, output); - DownloadUtil.downloadIfChanged(url, output.toFile(), LOGGER); + context.download(url) + .defaultCache() + .downloadPath(output); return output; - } catch (IOException e) { + } catch (DownloadException e) { throw new UncheckedIOException("Failed to download: " + url, e); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java index 3d386ec2..c40c087a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java @@ -33,7 +33,6 @@ import java.util.Objects; import org.gradle.api.Project; -import net.fabricmc.loom.util.HashedDownloadUtil; import net.fabricmc.stitch.merge.JarMerger; public class MergedMinecraftProvider extends MinecraftProvider { @@ -62,12 +61,12 @@ public class MergedMinecraftProvider extends MinecraftProvider { throw new UnsupportedOperationException("Minecraft versions 1.2.5 and older cannot be merged. Please use `loom { server/clientOnlyMinecraftJar() }`"); } - if (!Files.exists(minecraftMergedJar) || isRefreshDeps()) { + if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) { try { mergeJars(); } catch (Throwable e) { - HashedDownloadUtil.delete(getMinecraftClientJar()); - HashedDownloadUtil.delete(getMinecraftServerJar()); + Files.deleteIfExists(getMinecraftClientJar().toPath()); + Files.deleteIfExists(getMinecraftServerJar().toPath()); Files.deleteIfExists(minecraftMergedJar); getProject().getLogger().error("Could not merge JARs! Deleting source JARs - please re-run the command and move on.", e); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index a2cfcb3c..1b5b61db 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -27,16 +27,11 @@ package net.fabricmc.loom.configuration.providers.minecraft; import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.List; import java.util.Objects; -import java.util.Optional; import com.google.common.base.Preconditions; -import com.google.common.io.Files; -import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.logging.Logger; import org.jetbrains.annotations.Nullable; @@ -47,9 +42,11 @@ import net.fabricmc.loom.configuration.CompileConfiguration; import net.fabricmc.loom.configuration.DependencyInfo; import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.util.Constants; -import net.fabricmc.loom.util.DownloadUtil; -import net.fabricmc.loom.util.HashedDownloadUtil; import net.fabricmc.loom.util.MirrorUtil; +import net.fabricmc.loom.util.download.DownloadBuilder; +import net.fabricmc.loom.util.download.DownloadExecutor; +import net.fabricmc.loom.util.download.GradleDownloadProgressListener; +import net.fabricmc.loom.util.gradle.ProgressGroup; public abstract class MinecraftProvider { private String minecraftVersion; @@ -92,35 +89,15 @@ public abstract class MinecraftProvider { getProject().getDependencies().add(Constants.Configurations.SRG, "de.oceanlabs.mcp:mcp_config:" + minecraftVersion); } - boolean offline = getProject().getGradle().getStartParameter().isOffline(); - initFiles(); - downloadMcJson(offline); + downloadMcJson(); try (FileReader reader = new FileReader(minecraftJson)) { versionInfo = LoomGradlePlugin.OBJECT_MAPPER.readValue(reader, MinecraftVersionMeta.class); } - if (offline) { - boolean exists = true; - - if (provideServer() && !minecraftServerJar.exists()) { - exists = false; - } - - if (provideClient() && !minecraftClientJar.exists()) { - exists = false; - } - - if (exists) { - getProject().getLogger().debug("Found client and server jars, presuming up-to-date"); - } else { - throw new GradleException("Missing jar(s); Client: " + minecraftClientJar.exists() + ", Server: " + minecraftServerJar.exists()); - } - } else { - downloadJars(getProject().getLogger()); - } + downloadJars(); if (provideServer()) { serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath()); @@ -155,131 +132,86 @@ public abstract class MinecraftProvider { } } - private void downloadMcJson(boolean offline) throws IOException { - if (getExtension().getShareRemapCaches().get() && !getExtension().isRootProject() && versionManifestJson.exists() && !isRefreshDeps()) { - return; - } + private void downloadMcJson() throws IOException { + final String versionManifestUrl = MirrorUtil.getVersionManifests(getProject()); + final String versionManifest = getExtension().download(versionManifestUrl) + .defaultCache() + .downloadString(versionManifestJson.toPath()); - if (!offline && !isRefreshDeps() && hasRecentValidManifest()) { - // We have a recent valid manifest file, so do nothing - } else if (offline) { - if (versionManifestJson.exists()) { - // If there is the manifests already we'll presume that's good enough - getProject().getLogger().debug("Found version manifests, presuming up-to-date"); - } else { - // If we don't have the manifests then there's nothing more we can do - throw new GradleException("Version manifests not found at " + versionManifestJson.getAbsolutePath()); - } - } else { - getProject().getLogger().debug("Downloading version manifests"); - DownloadUtil.downloadIfChanged(new URL(MirrorUtil.getVersionManifests(getProject())), versionManifestJson, getProject().getLogger()); - } - - String versionManifest = Files.asCharSource(versionManifestJson, StandardCharsets.UTF_8).read(); - ManifestVersion mcManifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(versionManifest, ManifestVersion.class); - - Optional optionalVersion = Optional.empty(); + final ManifestVersion mcManifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(versionManifest, ManifestVersion.class); + ManifestVersion.Versions version = null; if (getExtension().getCustomMinecraftManifest().isPresent()) { ManifestVersion.Versions customVersion = new ManifestVersion.Versions(); customVersion.id = minecraftVersion; customVersion.url = getExtension().getCustomMinecraftManifest().get(); - optionalVersion = Optional.of(customVersion); + version = customVersion; getProject().getLogger().lifecycle("Using custom minecraft manifest"); } - if (optionalVersion.isEmpty()) { - optionalVersion = mcManifest.versions().stream().filter(versions -> versions.id.equalsIgnoreCase(minecraftVersion)).findFirst(); - - if (optionalVersion.isEmpty()) { - optionalVersion = findExperimentalVersion(offline); - } + if (version == null) { + version = mcManifest.versions().stream() + .filter(versions -> versions.id.equalsIgnoreCase(minecraftVersion)) + .findFirst().orElse(null); } - if (optionalVersion.isPresent()) { - if (offline) { - if (minecraftJson.exists()) { - //If there is the manifest already we'll presume that's good enough - getProject().getLogger().debug("Found Minecraft {} manifest, presuming up-to-date", minecraftVersion); - } else { - //If we don't have the manifests then there's nothing more we can do - throw new GradleException("Minecraft " + minecraftVersion + " manifest not found at " + minecraftJson.getAbsolutePath()); - } - } else { - getProject().getLogger().debug("Downloading Minecraft {} manifest", minecraftVersion); + if (version == null) { + version = findExperimentalVersion(); + } - ManifestVersion.Versions version = optionalVersion.get(); - String url = version.url; - - if (version.sha1 != null) { - HashedDownloadUtil.downloadIfInvalid(new URL(url), minecraftJson, version.sha1, getProject().getLogger(), true); - } else { - // Use the etag if no hash found from url - DownloadUtil.downloadIfChanged(new URL(url), minecraftJson, getProject().getLogger()); - } - } - } else { + if (version == null) { throw new RuntimeException("Failed to find minecraft version: " + minecraftVersion); } + + getProject().getLogger().debug("Downloading Minecraft {} manifest", minecraftVersion); + final DownloadBuilder download = getExtension().download(version.url); + + if (version.sha1 != null) { + download.sha1(version.sha1); + } else { + download.defaultCache(); + } + + download.downloadPath(minecraftJson.toPath()); } // This attempts to find the version from fabric's own fallback version manifest json. - private Optional findExperimentalVersion(boolean offline) throws IOException { - if (offline) { - if (!experimentalVersionsJson.exists()) { - getProject().getLogger().warn("Skipping download of experimental versions jsons due to being offline."); - return Optional.empty(); - } - } else { - DownloadUtil.downloadIfChanged(new URL(MirrorUtil.getExperimentalVersions(getProject())), experimentalVersionsJson, getProject().getLogger()); - } + private ManifestVersion.Versions findExperimentalVersion() throws IOException { + final String expVersionManifest = getExtension().download(MirrorUtil.getExperimentalVersions(getProject())) + .defaultCache() + .downloadString(experimentalVersionsJson.toPath()); - String expVersionManifest = Files.asCharSource(experimentalVersionsJson, StandardCharsets.UTF_8).read(); - ManifestVersion expManifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(expVersionManifest, ManifestVersion.class); + final ManifestVersion expManifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(expVersionManifest, ManifestVersion.class); + final ManifestVersion.Versions result = expManifest.versions().stream() + .filter(versions -> versions.id.equalsIgnoreCase(minecraftVersion)) + .findFirst() + .orElse(null); - Optional result = expManifest.versions().stream().filter(versions -> versions.id.equalsIgnoreCase(minecraftVersion)).findFirst(); - - if (result.isPresent()) { + if (result != null) { getProject().getLogger().lifecycle("Using fallback experimental version {}", minecraftVersion); } return result; } - private boolean hasRecentValidManifest() throws IOException { - if (getExtension().getCustomMinecraftManifest().isPresent()) { - return false; - } + private void downloadJars() throws IOException { + try (ProgressGroup progressGroup = new ProgressGroup(getProject(), "Download Minecraft jars"); + DownloadExecutor executor = new DownloadExecutor(2)) { + if (provideClient()) { + final MinecraftVersionMeta.Download client = versionInfo.download("client"); + getExtension().download(client.url()) + .sha1(client.sha1()) + .progress(new GradleDownloadProgressListener("Minecraft client", progressGroup::createProgressLogger)) + .downloadPathAsync(minecraftClientJar.toPath(), executor); + } - if (!versionManifestJson.exists() || !minecraftJson.exists()) { - return false; - } - - if (versionManifestJson.lastModified() > System.currentTimeMillis() - 24 * 3_600_000) { - // Version manifest hasn't been modified in 24 hours, time to get a new one. - return false; - } - - ManifestVersion manifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(Files.asCharSource(versionManifestJson, StandardCharsets.UTF_8).read(), ManifestVersion.class); - Optional version = manifest.versions().stream().filter(versions -> versions.id.equalsIgnoreCase(minecraftVersion)).findFirst(); - - // fail if the expected mc version was not found, will download the file again. - return version.isPresent(); - } - - private void downloadJars(Logger logger) throws IOException { - if (getExtension().getShareRemapCaches().get() && !getExtension().isRootProject() && minecraftClientJar.exists() && minecraftServerJar.exists() && !isRefreshDeps()) { - return; - } - - if (provideClient()) { - MinecraftVersionMeta.Download client = versionInfo.download("client"); - HashedDownloadUtil.downloadIfInvalid(new URL(client.url()), minecraftClientJar, client.sha1(), logger, false); - } - - if (provideServer()) { - MinecraftVersionMeta.Download server = versionInfo.download("server"); - HashedDownloadUtil.downloadIfInvalid(new URL(server.url()), minecraftServerJar, server.sha1(), logger, false); + if (provideServer()) { + final MinecraftVersionMeta.Download server = versionInfo.download("server"); + getExtension().download(server.url()) + .sha1(server.sha1()) + .progress(new GradleDownloadProgressListener("Minecraft server", progressGroup::createProgressLogger)) + .downloadPathAsync(minecraftServerJar.toPath(), executor); + } } } @@ -367,7 +299,7 @@ public abstract class MinecraftProvider { return LoomGradleExtension.get(getProject()); } - protected boolean isRefreshDeps() { - return LoomGradlePlugin.refreshDeps; + public boolean refreshDeps() { + return getExtension().refreshDeps(); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 0f174fd2..961f9ae6 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -72,7 +72,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider { } protected void processJar() throws Exception { - boolean requiresRefresh = isRefreshDeps() || Files.notExists(minecraftEnvOnlyJar); + boolean requiresRefresh = getExtension().refreshDeps() || Files.notExists(minecraftEnvOnlyJar); if (!requiresRefresh) { return; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java index b63f4796..1da1c36a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java @@ -57,7 +57,7 @@ public final class SplitMinecraftProvider extends MinecraftProvider { public void provide() throws Exception { super.provide(); - boolean requiresRefresh = isRefreshDeps() || Files.notExists(minecraftClientOnlyJar) || Files.notExists(minecraftCommonJar); + boolean requiresRefresh = getExtension().refreshDeps() || Files.notExists(minecraftClientOnlyJar) || Files.notExists(minecraftCommonJar); if (!requiresRefresh) { return; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index 5ab01c5c..307659ba 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -37,7 +37,6 @@ import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Project; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; @@ -71,7 +70,7 @@ public abstract class AbstractMappedMinecraftProvider remappedJars = getRemappedJars(); assert !remappedJars.isEmpty(); - if (!areOutputsValid(remappedJars) || LoomGradlePlugin.refreshDeps) { + if (!areOutputsValid(remappedJars) || extension.refreshDeps()) { try { remapInputs(remappedJars); } catch (Throwable t) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java index 66ae93cc..88bfbd8d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java @@ -32,7 +32,6 @@ import java.nio.file.StandardCopyOption; import java.util.List; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.configuration.processors.JarProcessorManager; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; @@ -64,7 +63,7 @@ public abstract class ProcessedNamedMinecraftProvider inputJars = parentMinecraftProvider.getMinecraftJars(); - boolean requiresProcessing = LoomGradlePlugin.refreshDeps || inputJars.stream() + boolean requiresProcessing = extension.refreshDeps() || inputJars.stream() .map(this::getProcessedPath) .map(Path::toFile) .anyMatch(jarProcessorManager::isInvalid); diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java index faf53b1e..de0b74d8 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java @@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; @@ -116,7 +117,7 @@ public final class LoomCFRDecompiler implements LoomDecompiler { builder.append("\t").append(src).append("\t").append(dst).append("\n"); } - writer.write("%s\t%d\t%d\n".formatted(name, maxLine, maxLineDest)); + writer.write(String.format(Locale.ENGLISH, "%s\t%d\t%d\n", name, maxLine, maxLineDest)); writer.write(builder.toString()); writer.write("\n"); } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index c749b2b3..99c71700 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -81,7 +81,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final ListProperty jarProcessors; protected final ConfigurableFileCollection log4jConfigs; protected final RegularFileProperty accessWidener; - protected final Property shareCaches; protected final Property customManifest; protected final Property transitiveAccessWideners; protected final Property modProvidedJavadoc; @@ -117,8 +116,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA .empty(); this.log4jConfigs = project.files(directories.getDefaultLog4jConfigFile()); this.accessWidener = project.getObjects().fileProperty(); - this.shareCaches = project.getObjects().property(Boolean.class) - .convention(false); this.customManifest = project.getObjects().property(String.class); this.transitiveAccessWideners = project.getObjects().property(Boolean.class) .convention(true); @@ -200,11 +197,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA return accessWidener; } - @Override - public Property getShareRemapCaches() { - return shareCaches; - } - @Override public NamedDomainObjectContainer getDecompilerOptions() { return decompilers; diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 7710bd83..cbc5b3e2 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -24,6 +24,7 @@ package net.fabricmc.loom.extension; +import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -53,6 +54,8 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMi import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider; import net.fabricmc.loom.util.ModPlatform; +import net.fabricmc.loom.util.download.Download; +import net.fabricmc.loom.util.download.DownloadBuilder; public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implements LoomGradleExtension { private final Project project; @@ -73,6 +76,7 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen private IntermediaryMinecraftProvider intermediaryMinecraftProvider; private SrgMinecraftProvider srgMinecraftProvider; private InstallerData installerData; + private boolean refreshDeps; // +-------------------+ // | Architectury Loom | @@ -93,7 +97,15 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen provider.getIntermediaryUrl() .convention(getIntermediaryUrl()) .finalizeValueOnRead(); + + provider.getRefreshDeps().set(project.provider(() -> LoomGradleExtension.get(project).refreshDeps())); }); + + refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh"); + + if (refreshDeps) { + project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower."); + } } @Override @@ -231,10 +243,44 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen transitiveAccessWideners.addAll(accessWidenerFiles); } + @Override + public DownloadBuilder download(String url) { + DownloadBuilder builder; + + try { + builder = Download.create(url); + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to create downloader for: " + e); + } + + if (project.getGradle().getStartParameter().isOffline()) { + builder.offline(); + } + + if (project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh")) { + builder.forceDownload(); + } + + return builder; + } + + @Override + public boolean refreshDeps() { + return refreshDeps; + } + + @Override + public void setRefreshDeps(boolean refreshDeps) { + this.refreshDeps = refreshDeps; + } + @Override protected void configureIntermediateMappingsProviderInternal(T provider) { provider.getMinecraftVersion().set(getProject().provider(() -> getMinecraftProvider().minecraftVersion())); provider.getMinecraftVersion().disallowChanges(); + + provider.getDownloader().set(this::download); + provider.getDownloader().disallowChanges(); } @Override diff --git a/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java index d401e88a..970daa7a 100644 --- a/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/MixinExtensionApiImpl.java @@ -30,6 +30,7 @@ import org.gradle.api.Action; import org.gradle.api.InvalidUserDataException; import org.gradle.api.Project; import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; @@ -42,6 +43,8 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI { protected final Project project; protected final Property useMixinAp; private final Property refmapTargetNamespace; + private final MapProperty messages; + private final Property showMessageTypes; public MixinExtensionApiImpl(Project project) { this.project = Objects.requireNonNull(project); @@ -52,6 +55,12 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI { this.refmapTargetNamespace = project.getObjects().property(String.class) .convention(project.provider(() -> IntermediaryNamespaces.intermediary(project))); this.refmapTargetNamespace.finalizeValueOnRead(); + + this.messages = project.getObjects().mapProperty(String.class, String.class); + this.messages.finalizeValueOnRead(); + + this.showMessageTypes = project.getObjects().property(Boolean.class); + this.showMessageTypes.convention(false).finalizeValueOnRead(); } protected final PatternSet add0(SourceSet sourceSet, String refmapName) { @@ -122,6 +131,21 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI { add(sourceSetName, x -> { }); } + @Override + public MapProperty getMessages() { + return messages; + } + + @Override + public Property getShowMessageTypes() { + return showMessageTypes; + } + + @Override + public void messages(Action> action) { + action.execute(messages); + } + private SourceSet resolveSourceSet(String sourceSetName) { // try to find sourceSet with name sourceSetName in this project SourceSet sourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().findByName(sourceSetName); diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java index 6b14f5bd..276c6d51 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java @@ -25,20 +25,30 @@ package net.fabricmc.loom.task; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.function.Function; +import java.util.stream.Collectors; import org.gradle.api.Project; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.JavaExec; +import org.jetbrains.annotations.NotNull; import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.util.Constants; public abstract class AbstractRunTask extends JavaExec { private final 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 + private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection(); public AbstractRunTask(Function configProvider) { super(); @@ -46,6 +56,9 @@ public abstract class AbstractRunTask extends JavaExec { this.config = configProvider.apply(getProject()); setClasspath(config.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter())); + // Pass an empty classpath to the super JavaExec. + super.setClasspath(getProject().files()); + args(config.programArgs); getMainClass().set(config.mainClass); } @@ -69,12 +82,46 @@ public abstract class AbstractRunTask extends JavaExec { @Override public List getJvmArgs() { - List superArgs = super.getJvmArgs(); - List args = new ArrayList<>(superArgs != null ? superArgs : Collections.emptyList()); + final List superArgs = super.getJvmArgs(); + final List args = new ArrayList<>(); + + final String content = "-classpath\n" + this.classpath.getFiles().stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining(System.getProperty("path.separator"))); + + try { + final Path argsFile = Files.createTempFile("loom-classpath", ".args"); + Files.writeString(argsFile, content, StandardCharsets.UTF_8); + args.add("@" + argsFile.toAbsolutePath()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create classpath file", e); + } + + if (superArgs != null) { + args.addAll(superArgs); + } + args.addAll(config.vmArgs); return args; } + @Override + public @NotNull JavaExec setClasspath(@NotNull FileCollection classpath) { + this.classpath.setFrom(classpath); + return this; + } + + @Override + public @NotNull JavaExec classpath(Object @NotNull... paths) { + this.classpath.from(paths); + return this; + } + + @Override + public @NotNull FileCollection getClasspath() { + return this.classpath; + } + private class LibraryFilter implements Spec { private List excludedLibraryPaths = null; diff --git a/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java b/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java index dd8e2b9d..da16ca3c 100644 --- a/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java +++ b/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java @@ -25,22 +25,12 @@ package net.fabricmc.loom.task; import java.io.File; -import java.io.FileReader; import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URL; -import java.util.Deque; +import java.nio.file.Path; import java.util.Objects; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import javax.inject.Inject; -import org.gradle.api.GradleException; -import org.gradle.api.Project; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; @@ -53,9 +43,10 @@ import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.configuration.providers.minecraft.assets.AssetIndex; -import net.fabricmc.loom.util.HashedDownloadUtil; import net.fabricmc.loom.util.MirrorUtil; -import net.fabricmc.loom.util.gradle.ProgressLoggerHelper; +import net.fabricmc.loom.util.download.DownloadExecutor; +import net.fabricmc.loom.util.download.GradleDownloadProgressListener; +import net.fabricmc.loom.util.gradle.ProgressGroup; // TODO: Reintroduce the progress bar. public abstract class DownloadAssetsTask extends AbstractLoomTask { @@ -65,6 +56,9 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask { @Input public abstract Property getMinecraftVersion(); + @Input + public abstract Property getDownloadThreads(); + @OutputDirectory public abstract RegularFileProperty getAssetsDirectory(); @@ -80,6 +74,7 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask { getAssetsHash().set(versionInfo.assetIndex().sha1()); getMinecraftVersion().set(versionInfo.id()); getMinecraftVersion().finalizeValue(); + getDownloadThreads().convention(Runtime.getRuntime().availableProcessors()); if (versionInfo.assets().equals("legacy")) { getLegacyResourcesDirectory().set(new File(assetsDir, "/legacy/" + versionInfo.id())); @@ -89,80 +84,28 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask { getLegacyResourcesDirectory().set(new File(getProject().getProjectDir(), client.getRunDir() + "/resources")); } - getAssetsHash().finalizeValueOnRead(); + getAssetsHash().finalizeValue(); getAssetsDirectory().finalizeValueOnRead(); getLegacyResourcesDirectory().finalizeValueOnRead(); } @TaskAction public void downloadAssets() throws IOException { - final Project project = this.getProject(); - final File assetsDirectory = getAssetsDirectory().get().getAsFile(); - final Deque loggers = new ConcurrentLinkedDeque<>(); - final ExecutorService executor = Executors.newFixedThreadPool(Math.min(10, Math.max(Runtime.getRuntime().availableProcessors() / 2, 1))); final AssetIndex assetIndex = getAssetIndex(); - if (!assetsDirectory.exists()) { - assetsDirectory.mkdirs(); - } + try (ProgressGroup progressGroup = new ProgressGroup(getProject(), "Download Assets"); + DownloadExecutor executor = new DownloadExecutor(getDownloadThreads().get())) { + for (AssetIndex.Object object : assetIndex.getObjects()) { + final String sha1 = object.hash(); + final String url = MirrorUtil.getResourcesBase(getProject()) + sha1.substring(0, 2) + "/" + sha1; - if (assetIndex.mapToResources()) { - getLegacyResourcesDirectory().get().getAsFile().mkdirs(); - } - - for (AssetIndex.Object object : assetIndex.getObjects()) { - final String path = object.path(); - final String sha1 = object.hash(); - final File file = getAssetsFile(object, assetIndex); - - if (getProject().getGradle().getStartParameter().isOffline()) { - if (!file.exists()) { - throw new GradleException("Asset " + path + " not found at " + file.getAbsolutePath()); - } - - continue; + getExtension() + .download(url) + .sha1(sha1) + .progress(new GradleDownloadProgressListener(object.name(), progressGroup::createProgressLogger)) + .maxRetries(3) + .downloadPathAsync(getAssetsPath(object, assetIndex), executor); } - - final Supplier getOrCreateLogger = () -> { - ProgressLoggerHelper logger = loggers.pollFirst(); - - if (logger == null) { - // No logger available, create a new one - logger = ProgressLoggerHelper.getProgressFactory(project, DownloadAssetsTask.class.getName()); - logger.start("Downloading assets...", "assets"); - } - - return logger; - }; - - executor.execute(() -> { - final ProgressLoggerHelper logger = getOrCreateLogger.get(); - - try { - HashedDownloadUtil.downloadIfInvalid(new URL(MirrorUtil.getResourcesBase(project) + sha1.substring(0, 2) + "/" + sha1), file, sha1, project.getLogger(), true, false, () -> { - project.getLogger().debug("downloading asset " + object.name()); - logger.progress(String.format("%-30.30s", object.name()) + " - " + sha1); - }); - } catch (IOException e) { - throw new UncheckedIOException("Failed to download: " + object.name(), e); - } - - // Give this logger back - loggers.add(logger); - }); - } - - // Wait for the assets to all download - try { - executor.shutdown(); - - if (executor.awaitTermination(2, TimeUnit.HOURS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - loggers.forEach(ProgressLoggerHelper::completed); } } @@ -174,35 +117,22 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask { private AssetIndex getAssetIndex() throws IOException { final LoomGradleExtension extension = getExtension(); final MinecraftProvider minecraftProvider = extension.getMinecraftProvider(); + final MinecraftVersionMeta.AssetIndex assetIndex = getAssetIndexMeta(); + final File indexFile = new File(getAssetsDirectory().get().getAsFile(), "indexes" + File.separator + assetIndex.fabricId(minecraftProvider.minecraftVersion()) + ".json"); - MinecraftVersionMeta.AssetIndex assetIndex = getAssetIndexMeta(); - File assetsInfo = new File(getAssetsDirectory().get().getAsFile(), "indexes" + File.separator + assetIndex.fabricId(minecraftProvider.minecraftVersion()) + ".json"); + final String json = extension.download(assetIndex.url()) + .sha1(assetIndex.sha1()) + .downloadString(indexFile.toPath()); - getProject().getLogger().info(":downloading asset index"); - - if (getProject().getGradle().getStartParameter().isOffline()) { - if (assetsInfo.exists()) { - // We know it's outdated but can't do anything about it, oh well - getProject().getLogger().warn("Asset index outdated"); - } else { - // We don't know what assets we need, just that we don't have any - throw new GradleException("Asset index not found at " + assetsInfo.getAbsolutePath()); - } - } else { - HashedDownloadUtil.downloadIfInvalid(new URL(assetIndex.url()), assetsInfo, assetIndex.sha1(), getProject().getLogger(), false); - } - - try (FileReader fileReader = new FileReader(assetsInfo)) { - return LoomGradlePlugin.OBJECT_MAPPER.readValue(fileReader, AssetIndex.class); - } + return LoomGradlePlugin.OBJECT_MAPPER.readValue(json, AssetIndex.class); } - private File getAssetsFile(AssetIndex.Object object, AssetIndex index) { + private Path getAssetsPath(AssetIndex.Object object, AssetIndex index) { if (index.mapToResources() || index.virtual()) { - return new File(getLegacyResourcesDirectory().get().getAsFile(), object.path()); + return new File(getLegacyResourcesDirectory().get().getAsFile(), object.path()).toPath(); } final String filename = "objects" + File.separator + object.hash().substring(0, 2) + File.separator + object.hash(); - return new File(getAssetsDirectory().get().getAsFile(), filename); + return new File(getAssetsDirectory().get().getAsFile(), filename).toPath(); } } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 1d05f4d1..07290e31 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -184,7 +184,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return getWorkerExecutor().processIsolation(spec -> { spec.forkOptions(forkOptions -> { - forkOptions.setMaxHeapSize("%dm".formatted(decompilerOptions.getMemory().get())); + forkOptions.setMaxHeapSize(String.format(Locale.ENGLISH, "%dm", decompilerOptions.getMemory().get())); forkOptions.systemProperty(WorkerDaemonClientsManagerHelper.MARKER_PROP, jvmMarkerValue); }); spec.getClasspath().from(getClasspath()); diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 0df1e4e9..60b9ca81 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -34,6 +34,7 @@ import org.gradle.api.tasks.TaskProvider; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.task.launch.GenerateDLIConfigTask; import net.fabricmc.loom.task.launch.GenerateLog4jConfigTask; import net.fabricmc.loom.task.launch.GenerateRemapClasspathTask; @@ -95,7 +96,14 @@ public final class LoomTasks { return; } - registerClientSetupTasks(project.getTasks(), extension.getMinecraftProvider().getVersionInfo().hasNativesToExtract()); + final MinecraftVersionMeta versionInfo = extension.getMinecraftProvider().getVersionInfo(); + + if (versionInfo == null) { + // Something has gone wrong, don't register the task. + return; + } + + registerClientSetupTasks(project.getTasks(), versionInfo.hasNativesToExtract()); }); } diff --git a/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java b/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java index f09bfe2e..cdf65d65 100644 --- a/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java +++ b/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java @@ -40,9 +40,13 @@ import org.gradle.api.IllegalDependencyNotation; import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; +import org.gradle.work.DisableCachingByDefault; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; @@ -55,7 +59,8 @@ import net.fabricmc.lorenztiny.TinyMappingsJoiner; import net.fabricmc.mappingio.MappingReader; import net.fabricmc.mappingio.tree.MemoryMappingTree; -public class MigrateMappingsTask extends AbstractLoomTask { +@DisableCachingByDefault(because = "Always rerun this task.") +public abstract class MigrateMappingsTask extends AbstractLoomTask { private Path inputDir; private Path outputDir; private String mappings; @@ -63,6 +68,9 @@ public class MigrateMappingsTask extends AbstractLoomTask { public MigrateMappingsTask() { inputDir = getProject().file("src/main/java").toPath(); outputDir = getProject().file("remappedSrc").toPath(); + + // Ensure we resolve the classpath inputs before running the task. + getCompileClasspath().from(getProject().getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); } @Option(option = "input", description = "Java source file directory") @@ -80,6 +88,9 @@ public class MigrateMappingsTask extends AbstractLoomTask { this.mappings = mappings; } + @InputFiles + public abstract ConfigurableFileCollection getCompileClasspath(); + @TaskAction public void doTask() throws Throwable { Project project = getProject(); diff --git a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java index 1f7422ac..76717e16 100644 --- a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java +++ b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java @@ -94,7 +94,7 @@ public abstract class ValidateAccessWidenerTask extends DefaultTask { /** * Validates that all entries in an access-widner file relate to a class/method/field in the mc jar. */ - private static record AccessWidenerValidator(TrEnvironment environment) implements AccessWidenerVisitor { + private record AccessWidenerValidator(TrEnvironment environment) implements AccessWidenerVisitor { @Override public void visitClass(String name, AccessWidenerReader.AccessType access, boolean transitive) { if (environment().getClass(name) == null) { diff --git a/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java b/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java new file mode 100644 index 00000000..2d99b4b9 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java @@ -0,0 +1,250 @@ +/* + * 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.task; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.gradle.api.GradleException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.SourceTask; +import org.gradle.api.tasks.TaskAction; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.Constants; +import net.fabricmc.tinyremapper.extension.mixin.common.data.Constant; + +/** + * Task to validate mixin names. + * + *
{@code
+ * task validateMixinNames(type: net.fabricmc.loom.task.ValidateMixinNameTask) {
+ * 		source(sourceSets.main.output)
+ * 		softFailures = false
+ * }
+ * }
+ */ +public abstract class ValidateMixinNameTask extends SourceTask { + @Input + abstract Property getSoftFailures(); + + @Inject + protected abstract WorkerExecutor getWorkerExecutor(); + + @Inject + public ValidateMixinNameTask() { + setGroup("verification"); + getProject().getTasks().getByName("check").dependsOn(this); + getSoftFailures().convention(false); + } + + @TaskAction + public void run() { + final WorkQueue workQueue = getWorkerExecutor().noIsolation(); + + workQueue.submit(ValidateMixinAction.class, params -> { + params.getInputClasses().from(getSource().matching(pattern -> pattern.include("**/*.class"))); + params.getSoftFailures().set(getSoftFailures()); + }); + } + + public interface ValidateMixinsParams extends WorkParameters { + ConfigurableFileCollection getInputClasses(); + Property getSoftFailures(); + } + + public abstract static class ValidateMixinAction implements WorkAction { + public static final Logger LOGGER = LoggerFactory.getLogger(ValidateMixinAction.class); + + @Override + public void execute() { + final Set files = getParameters().getInputClasses().getAsFileTree().getFiles(); + final List errors = new LinkedList<>(); + + for (File file : files) { + final Mixin mixin = getMixin(file); + + if (mixin == null) { + continue; + } + + final String mixinClassName = toSimpleName(mixin.className); + final String expectedMixinClassName = toSimpleName(mixin.target.getInternalName()).replace("$", "") + (mixin.accessor ? "Accessor" : "Mixin"); + + if (expectedMixinClassName.startsWith("class_")) { + // Don't enforce intermediary named mixins. + continue; + } + + if (!expectedMixinClassName.equals(mixinClassName)) { + errors.add("%s -> %s".formatted(mixin.className, expectedMixinClassName)); + } + } + + if (errors.isEmpty()) { + return; + } + + final String message = "Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator())); + + if (getParameters().getSoftFailures().get()) { + LOGGER.warn(message); + return; + } + + throw new GradleException("Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator()))); + } + + private static String toSimpleName(String internalName) { + return internalName.substring(internalName.lastIndexOf("/") + 1); + } + + @Nullable + private Mixin getMixin(File file) { + try (InputStream is = new FileInputStream(file)) { + ClassReader reader = new ClassReader(is); + + var classVisitor = new MixinTargetClassVisitor(); + reader.accept(classVisitor, ClassReader.SKIP_CODE); + + if (classVisitor.mixinTarget != null) { + return new Mixin(classVisitor.className, classVisitor.mixinTarget, classVisitor.accessor); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to read input file: " + file, e); + } + + return null; + } + } + + private record Mixin(String className, Type target, boolean accessor) { } + + private static class MixinTargetClassVisitor extends ClassVisitor { + Type mixinTarget; + String className; + boolean accessor; + + boolean isInterface; + + protected MixinTargetClassVisitor() { + super(Constants.ASM_VERSION); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.className = name; + this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + AnnotationVisitor av = super.visitAnnotation(descriptor, visible); + + if ("Lorg/spongepowered/asm/mixin/Mixin;".equals(descriptor)) { + av = new MixinAnnotationVisitor(av); + } + + return av; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + + if (mixinTarget != null) { + mv = new MixinMethodVisitor(mv); + } + + return mv; + } + + private class MixinAnnotationVisitor extends AnnotationVisitor { + MixinAnnotationVisitor(AnnotationVisitor annotationVisitor) { + super(Constants.ASM_VERSION, annotationVisitor); + } + + @Override + public AnnotationVisitor visitArray(String name) { + final AnnotationVisitor av = super.visitArray(name); + + if ("value".equals(name)) { + return new AnnotationVisitor(Constant.ASM_VERSION, av) { + @Override + public void visit(String name, Object value) { + mixinTarget = Objects.requireNonNull((Type) value); + + super.visit(name, value); + } + }; + } + + return av; + } + } + + private class MixinMethodVisitor extends MethodVisitor { + protected MixinMethodVisitor(MethodVisitor methodVisitor) { + super(Constants.ASM_VERSION, methodVisitor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if ("Lorg/spongepowered/asm/mixin/gen/Accessor;".equals(descriptor)) { + accessor = true; + } else if ("Lorg/spongepowered/asm/mixin/gen/Invoker;".equals(descriptor)) { + accessor = true; + } + + return super.visitAnnotation(descriptor, visible); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index 06a73a15..6e44af12 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -28,6 +28,7 @@ import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -65,6 +66,11 @@ public class Checksum { } } + public static String sha1Hex(Path path) throws IOException { + HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1()); + return toHex(hash.asBytes()); + } + public static String truncatedSha256(File file) { try { HashCode hash = Files.asByteSource(file).hash(Hashing.sha256()); diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 10712160..4547c88f 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -149,6 +149,7 @@ public class Constants { public static final String OUT_REFMAP_FILE = "outRefMapFile"; public static final String DEFAULT_OBFUSCATION_ENV = "defaultObfuscationEnv"; public static final String QUIET = "quiet"; + public static final String SHOW_MESSAGE_TYPES = "showMessageTypes"; private MixinArguments() { } diff --git a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java b/src/main/java/net/fabricmc/loom/util/DownloadUtil.java deleted file mode 100644 index 90303d9b..00000000 --- a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2019 Chocohead - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package net.fabricmc.loom.util; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.zip.GZIPInputStream; - -import com.google.common.io.Files; -import org.apache.commons.io.FileUtils; -import org.gradle.api.Project; -import org.slf4j.Logger; - -import net.fabricmc.loom.LoomGradlePlugin; - -public class DownloadUtil { - /** - * Download from the given {@link URL} to the given {@link File} so long as there are differences between them. - * - * @param from The URL of the file to be downloaded - * @param to The destination to be saved to, and compared against if it exists - * @param logger The logger to print everything to, typically from {@link Project#getLogger()} - * @throws IOException If an exception occurs during the process - */ - public static boolean downloadIfChanged(URL from, File to, Logger logger) throws IOException { - return downloadIfChanged(from, to, logger, false); - } - - /** - * Download from the given {@link URL} to the given {@link File} so long as there are differences between them. - * - * @param from The URL of the file to be downloaded - * @param to The destination to be saved to, and compared against if it exists - * @param logger The logger to print information to, typically from {@link Project#getLogger()} - * @param quiet Whether to only print warnings (when true) or everything - * @throws IOException If an exception occurs during the process - */ - public static boolean downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException { - HttpURLConnection connection = (HttpURLConnection) from.openConnection(); - - if (LoomGradlePlugin.refreshDeps) { - getETagFile(to).delete(); - to.delete(); - } - - // If the output already exists we'll use it's last modified time - if (to.exists()) { - connection.setIfModifiedSince(to.lastModified()); - } - - //Try use the ETag if there's one for the file we're downloading - String etag = loadETag(to, logger); - - if (etag != null) { - connection.setRequestProperty("If-None-Match", etag); - } - - // We want to download gzip compressed stuff - connection.setRequestProperty("Accept-Encoding", "gzip"); - - // Try make the connection, it will hang here if the connection is bad - connection.connect(); - - int code = connection.getResponseCode(); - - if ((code < 200 || code > 299) && code != HttpURLConnection.HTTP_NOT_MODIFIED) { - //Didn't get what we expected - delete(to); - throw new IOException(connection.getResponseMessage() + " for " + from); - } - - long modifyTime = connection.getHeaderFieldDate("Last-Modified", -1); - - if (to.exists() && (code == HttpURLConnection.HTTP_NOT_MODIFIED || modifyTime > 0 && to.lastModified() >= modifyTime)) { - if (!quiet) { - logger.info("'{}' Not Modified, skipping.", to); - } - - return false; //What we've got is already fine - } - - long contentLength = connection.getContentLengthLong(); - - if (!quiet && contentLength >= 0) { - logger.info("'{}' Changed, downloading {}", to, toNiceSize(contentLength)); - } - - try { // Try download to the output - InputStream inputStream = connection.getInputStream(); - - if ("gzip".equals(connection.getContentEncoding())) { - inputStream = new GZIPInputStream(inputStream); - } - - FileUtils.copyInputStreamToFile(inputStream, to); - } catch (IOException e) { - delete(to); // Probably isn't good if it fails to copy/save - throw e; - } - - //Set the modify time to match the server's (if we know it) - if (modifyTime > 0) { - to.setLastModified(modifyTime); - } - - //Save the ETag (if we know it) - String eTag = connection.getHeaderField("ETag"); - - if (eTag != null) { - //Log if we get a weak ETag and we're not on quiet - if (!quiet && eTag.startsWith("W/")) { - logger.warn("Weak ETag found."); - } - - saveETag(to, eTag, logger); - } - - return true; - } - - /** - * Creates a new file in the same directory as the given file with .etag on the end of the name. - * - * @param file The file to produce the ETag for - * @return The (uncreated) ETag file for the given file - */ - private static File getETagFile(File file) { - return new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".etag"); - } - - /** - * Attempt to load an ETag for the given file, if it exists. - * - * @param to The file to load an ETag for - * @param logger The logger to print errors to if it goes wrong - * @return The ETag for the given file, or null if it doesn't exist - */ - private static String loadETag(File to, Logger logger) { - File eTagFile = getETagFile(to); - - if (!eTagFile.exists()) { - return null; - } - - try { - return Files.asCharSource(eTagFile, StandardCharsets.UTF_8).read(); - } catch (IOException e) { - logger.warn("Error reading ETag file '{}'.", eTagFile); - return null; - } - } - - /** - * Saves the given ETag for the given file, replacing it if it already exists. - * - * @param to The file to save the ETag for - * @param eTag The ETag to be saved - * @param logger The logger to print errors to if it goes wrong - */ - private static void saveETag(File to, String eTag, Logger logger) { - File eTagFile = getETagFile(to); - - try { - if (!eTagFile.exists()) { - eTagFile.createNewFile(); - } - - Files.asCharSink(eTagFile, StandardCharsets.UTF_8).write(eTag); - } catch (IOException e) { - logger.warn("Error saving ETag file '{}'.", eTagFile, e); - } - } - - /** - * Format the given number of bytes as a more human readable string. - * - * @param bytes The number of bytes - * @return The given number of bytes formatted to kilobytes, megabytes or gigabytes if appropriate - */ - public static String toNiceSize(long bytes) { - if (bytes < 1024) { - return bytes + " B"; - } else if (bytes < 1024 * 1024) { - return bytes / 1024 + " KB"; - } else if (bytes < 1024 * 1024 * 1024) { - return String.format("%.2f MB", bytes / (1024.0 * 1024.0)); - } else { - return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); - } - } - - /** - * Delete the file along with the corresponding ETag, if it exists. - * - * @param file The file to delete. - */ - public static void delete(File file) { - if (file.exists()) { - file.delete(); - } - - File etagFile = getETagFile(file); - - if (etagFile.exists()) { - etagFile.delete(); - } - - HashedDownloadUtil.delete(file); - } -} diff --git a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java b/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java deleted file mode 100644 index 354c7238..00000000 --- a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2020-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.util; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.zip.GZIPInputStream; - -import javax.annotation.Nullable; - -import com.google.common.io.Files; -import org.apache.commons.io.FileUtils; -import org.gradle.api.logging.Logger; - -import net.fabricmc.loom.LoomGradlePlugin; - -public class HashedDownloadUtil { - public static boolean requiresDownload(File to, String expectedHash, Logger logger) { - if (LoomGradlePlugin.refreshDeps) { - return true; - } - - if (to.exists()) { - String sha1 = getSha1(to, logger); - - // The hash in the sha1 file matches - return !expectedHash.equals(sha1); - } - - return true; - } - - public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet) throws IOException { - downloadIfInvalid(from, to, expectedHash, logger, quiet, true); - } - - public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict) throws IOException { - downloadIfInvalid(from, to, expectedHash, logger, quiet, strict, () -> { }); - } - - public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict, Runnable startDownload) throws IOException { - if (LoomGradlePlugin.refreshDeps && !Boolean.getBoolean("loom.refresh")) { - delete(to); - } - - if (to.exists()) { - if (strict) { - if (Checksum.equals(to, expectedHash)) { - // The hash matches the target file - return; - } - } else { - String sha1 = getSha1(to, logger); - - if (expectedHash.equals(sha1)) { - // The hash in the sha1 file matches - return; - } - } - } - - startDownload.run(); - - HttpURLConnection connection = (HttpURLConnection) from.openConnection(); - connection.setRequestProperty("Accept-Encoding", "gzip"); - connection.connect(); - - int code = connection.getResponseCode(); - - if ((code < 200 || code > 299) && code != HttpURLConnection.HTTP_NOT_MODIFIED) { - //Didn't get what we expected - delete(to); - throw new IOException(connection.getResponseMessage() + " for " + from); - } - - long contentLength = connection.getContentLengthLong(); - - if (!quiet && contentLength >= 0) { - logger.info("'{}' Changed, downloading {}", to, DownloadUtil.toNiceSize(contentLength)); - } - - try { // Try download to the output - InputStream inputStream = connection.getInputStream(); - - if ("gzip".equals(connection.getContentEncoding())) { - inputStream = new GZIPInputStream(inputStream); - } - - FileUtils.copyInputStreamToFile(inputStream, to); - } catch (IOException e) { - delete(to); // Probably isn't good if it fails to copy/save - throw e; - } - - saveSha1(to, expectedHash, logger); - } - - private static File getSha1File(File file) { - return new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".sha1"); - } - - @Nullable - private static String getSha1(File to, Logger logger) { - if (!to.exists()) { - delete(to); - return null; - } - - File sha1File = getSha1File(to); - - try { - return Files.asCharSource(sha1File, StandardCharsets.UTF_8).read(); - } catch (FileNotFoundException ignored) { - // Quicker to catch this than do an exists check before. - return null; - } catch (IOException e) { - logger.warn("Error reading sha1 file '{}'.", sha1File); - return null; - } - } - - private static void saveSha1(File to, String sha1, Logger logger) { - File sha1File = getSha1File(to); - - try { - if (!sha1File.exists()) { - sha1File.createNewFile(); - } - - Files.asCharSink(sha1File, StandardCharsets.UTF_8).write(sha1); - } catch (IOException e) { - logger.warn("Error saving sha1 file '{}'.", sha1File, e); - } - } - - public static void delete(File file) { - if (file.exists()) { - file.delete(); - } - - File sha1File = getSha1File(file); - - if (sha1File.exists()) { - sha1File.delete(); - } - } -} diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index bc90fd1a..a3e76bc4 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -37,6 +37,9 @@ import org.cadixdev.lorenz.MappingSet; import org.cadixdev.mercury.Mercury; import org.cadixdev.mercury.remapper.MercuryRemapper; import org.gradle.api.Project; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.internal.logging.progress.ProgressLogger; +import org.gradle.internal.logging.progress.ProgressLoggerFactory; import org.slf4j.Logger; import net.fabricmc.loom.LoomGradleExtension; @@ -44,7 +47,6 @@ import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.build.IntermediaryNamespaces; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; -import net.fabricmc.loom.util.gradle.ProgressLoggerHelper; import net.fabricmc.lorenztiny.TinyMappingsReader; import net.fabricmc.mappingio.tree.MemoryMappingTree; @@ -52,7 +54,7 @@ public class SourceRemapper { private final Project project; private String from; private String to; - private final List> remapTasks = new ArrayList<>(); + private final List> remapTasks = new ArrayList<>(); private Mercury mercury; @@ -96,7 +98,8 @@ public class SourceRemapper { project.getLogger().lifecycle(":remapping sources"); - ProgressLoggerHelper progressLogger = ProgressLoggerHelper.getProgressFactory(project, SourceRemapper.class.getName()); + ProgressLoggerFactory progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class); + ProgressLogger progressLogger = progressLoggerFactory.newOperation(SourceRemapper.class.getName()); progressLogger.start("Remapping dependency sources", "sources"); remapTasks.forEach(consumer -> consumer.accept(progressLogger)); diff --git a/src/main/java/net/fabricmc/loom/util/download/Download.java b/src/main/java/net/fabricmc/loom/util/download/Download.java new file mode 100644 index 00000000..21fb99ea --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/Download.java @@ -0,0 +1,413 @@ +/* + * 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.util.download; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.UserDefinedFileAttributeView; +import java.time.Duration; +import java.time.Instant; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.github.mizosoft.methanol.Methanol; +import com.github.mizosoft.methanol.ProgressTracker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.Checksum; + +public class Download { + private static final String E_TAG = "ETag"; + private static final Logger LOGGER = LoggerFactory.getLogger(Download.class); + + public static DownloadBuilder create(String url) throws URISyntaxException { + return DownloadBuilder.create(url); + } + + private final URI url; + private final String expectedHash; + private final boolean useEtag; + private final boolean forceDownload; + private final boolean offline; + private final Duration maxAge; + private final DownloadProgressListener progressListener; + + Download(URI url, String expectedHash, boolean useEtag, boolean forceDownload, boolean offline, Duration maxAge, DownloadProgressListener progressListener) { + this.url = url; + this.expectedHash = expectedHash; + this.useEtag = useEtag; + this.forceDownload = forceDownload; + this.offline = offline; + this.maxAge = maxAge; + this.progressListener = progressListener; + } + + private HttpClient getHttpClient() throws DownloadException { + if (offline) { + throw error("Unable to download %s in offline mode", this.url); + } + + return Methanol.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .proxy(ProxySelector.getDefault()) + .autoAcceptEncoding(true) + .build(); + } + + private HttpRequest getRequest() { + return HttpRequest.newBuilder(url) + .GET() + .build(); + } + + private HttpRequest getETagRequest(String etag) { + return HttpRequest.newBuilder(url) + .GET() + .header("If-None-Match", etag) + .build(); + } + + private HttpResponse send(HttpRequest httpRequest, HttpResponse.BodyHandler bodyHandler) throws DownloadException { + final ProgressTracker tracker = ProgressTracker.create(); + final AtomicBoolean started = new AtomicBoolean(false); + + try { + return getHttpClient().send(httpRequest, tracker.tracking(bodyHandler, progress -> { + if (started.compareAndSet(false, true)) { + progressListener.onStart(); + } + + progressListener.onProgress(progress.totalBytesTransferred(), progress.contentLength()); + + if (progress.done()) { + progressListener.onEnd(true); + } + })); + } catch (IOException | InterruptedException e) { + throw error(e, "Failed to download (%s)", url); + } + } + + String downloadString() throws DownloadException { + final HttpResponse response = send(getRequest(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + final int statusCode = response.statusCode(); + final boolean successful = statusCode >= 200 && statusCode < 300; + + if (!successful) { + throw error("HTTP request to (%s) returned unsuccessful status (%d)", url, statusCode); + } + + return response.body(); + } + + void downloadPath(Path output) throws DownloadException { + boolean downloadRequired = requiresDownload(output); + + if (!downloadRequired) { + // Does not require download, we are done here. + return; + } + + try { + doDownload(output); + } catch (Throwable throwable) { + tryCleanup(output); + throw error(throwable, "Failed to download (%s) to (%s)", url, output); + } + } + + private void doDownload(Path output) throws DownloadException { + Optional eTag = Optional.empty(); + + if (!forceDownload && useEtag && exists(output)) { + eTag = readEtag(output); + } + + try { + Files.createDirectories(output.getParent()); + Files.deleteIfExists(output); + } catch (IOException e) { + throw error(e, "Failed to prepare path for download"); + } + + final HttpRequest httpRequest = eTag + .map(this::getETagRequest) + .orElseGet(this::getRequest); + + // Create a .lock file, this allows us to re-download if the download was forcefully aborted part way through. + createLock(output); + HttpResponse response = send(httpRequest, HttpResponse.BodyHandlers.ofFile(output)); + getAndResetLock(output); + + final int statusCode = response.statusCode(); + boolean success = statusCode == HttpURLConnection.HTTP_NOT_MODIFIED || (statusCode >= 200 && statusCode < 300); + + if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + // Success, etag matched. + return; + } + + if (!success) { + try { + Files.deleteIfExists(output); + } catch (IOException ignored) { + // We tried. + } + + throw error("HTTP request to (%s) returned unsuccessful status (%d)", url, statusCode); + } + + if (useEtag) { + final HttpHeaders headers = response.headers(); + final String responseETag = headers.firstValue(E_TAG.toLowerCase(Locale.ROOT)).orElse(null); + + if (responseETag != null) { + writeEtag(output, responseETag); + } + } + + if (expectedHash != null) { + // Ensure we downloaded the expected hash. + if (!isHashValid(output)) { + String downloadedHash; + + try { + downloadedHash = Checksum.sha1Hex(output); + Files.deleteIfExists(output); + } catch (IOException e) { + downloadedHash = "unknown hash"; + } + + throw error("Failed to download (%s) with expected hash: %s got %s", url, expectedHash, downloadedHash); + } + + // Write the hash to the file attribute, saves a lot of time trying to re-compute the hash when re-visiting this file. + writeHash(output, expectedHash); + } + } + + private boolean requiresDownload(Path output) throws DownloadException { + if (forceDownload || !exists(output)) { + // File does not exist, or we are forced to download again. + return true; + } + + if (offline) { + // We know the file exists, nothing more we can do. + return false; + } + + if (getAndResetLock(output)) { + LOGGER.warn("Forcing downloading {} as existing lock file was found. This may happen if the gradle build was forcefully canceled.", output); + return true; + } + + if (expectedHash != null) { + final String hashAttribute = readHash(output).orElse(""); + + if (expectedHash.equalsIgnoreCase(hashAttribute)) { + // File has a matching hash attribute, assume file intact. + return false; + } + + if (isHashValid(output)) { + // Valid hash, no need to re-download + return false; + } + + if (System.getProperty("fabric.loom.test") != null) { + // This should never happen in an ideal world. + // It means that something has altered a file that should be cached. + throw error("Download file (%s) may have been modified", output); + } + + LOGGER.info("Found existing file ({}) to download with unexpected hash.", output); + } + + //noinspection RedundantIfStatement + if (!maxAge.equals(Duration.ZERO) && !isOutdated(output)) { + return false; + } + + // Default to re-downloading, may check the etag + return true; + } + + private boolean isHashValid(Path path) { + int i = expectedHash.indexOf(':'); + String algorithm = expectedHash.substring(0, i); + String hash = expectedHash.substring(i + 1); + + try { + String computedHash = switch (algorithm) { + case "sha1" -> Checksum.sha1Hex(path); + default -> throw error("Unsupported hash algorithm (%s)", algorithm); + }; + + return computedHash.equalsIgnoreCase(hash); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private boolean isOutdated(Path path) throws DownloadException { + try { + final FileTime lastModified = getLastModified(path); + return lastModified.toInstant().plus(maxAge) + .isBefore(Instant.now()); + } catch (IOException e) { + throw error(e, "Failed to check if (%s) is outdated", path); + } + } + + private Optional readEtag(Path output) { + try { + return readAttribute(output, E_TAG); + } catch (IOException e) { + return Optional.empty(); + } + } + + private void writeEtag(Path output, String eTag) throws DownloadException { + try { + writeAttribute(output, E_TAG, eTag); + } catch (IOException e) { + throw error(e, "Failed to write etag to (%s)", output); + } + } + + private Optional readHash(Path output) { + try { + return readAttribute(output, "LoomHash"); + } catch (IOException e) { + return Optional.empty(); + } + } + + private void writeHash(Path output, String eTag) throws DownloadException { + try { + writeAttribute(output, "LoomHash", eTag); + } catch (IOException e) { + throw error(e, "Failed to write hash to (%s)", output); + } + } + + private void tryCleanup(Path output) { + try { + Files.deleteIfExists(output); + } catch (IOException ignored) { + // ignored + } + } + + // A faster exists check + private static boolean exists(Path path) { + return path.getFileSystem() == FileSystems.getDefault() ? path.toFile().exists() : Files.exists(path); + } + + private static Optional readAttribute(Path path, String key) throws IOException { + final UserDefinedFileAttributeView attributeView = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); + + if (!attributeView.list().contains(key)) { + return Optional.empty(); + } + + final ByteBuffer buffer = ByteBuffer.allocate(attributeView.size(key)); + attributeView.read(key, buffer); + buffer.flip(); + final String value = StandardCharsets.UTF_8.decode(buffer).toString(); + return Optional.of(value); + } + + private static void writeAttribute(Path path, String key, String value) throws IOException { + // TODO may need to fallback to creating a separate file if this isnt supported. + final UserDefinedFileAttributeView attributeView = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); + final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + final ByteBuffer buffer = ByteBuffer.wrap(bytes); + final int written = attributeView.write(key, buffer); + assert written == bytes.length; + } + + private FileTime getLastModified(Path path) throws IOException { + final BasicFileAttributeView basicView = Files.getFileAttributeView(path, BasicFileAttributeView.class); + return basicView.readAttributes().lastModifiedTime(); + } + + private Path getLockFile(Path output) { + return output.resolveSibling(output.getFileName() + ".lock"); + } + + private boolean getAndResetLock(Path output) throws DownloadException { + final Path lock = getLockFile(output); + final boolean exists = Files.exists(lock); + + try { + Files.deleteIfExists(lock); + } catch (IOException e) { + throw error(e, "Failed to release lock on %s", lock); + } + + return exists; + } + + private void createLock(Path output) throws DownloadException { + final Path lock = getLockFile(output); + + try { + Files.createFile(lock); + } catch (IOException e) { + throw error(e, "Failed to acquire lock on %s", lock); + } + } + + private DownloadException error(String message, Object... args) { + return new DownloadException(String.format(Locale.ENGLISH, message, args)); + } + + private DownloadException error(Throwable throwable) { + return new DownloadException(throwable); + } + + private DownloadException error(Throwable throwable, String message, Object... args) { + return new DownloadException(message.formatted(args), throwable); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java new file mode 100644 index 00000000..5a10f413 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadBuilder.java @@ -0,0 +1,153 @@ +/* + * 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.util.download; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Locale; + +@SuppressWarnings("UnusedReturnValue") +public class DownloadBuilder { + private static final Duration ONE_DAY = Duration.ofDays(1); + + private final URI url; + private String expectedHash = null; + private boolean useEtag = true; + private boolean forceDownload = false; + private boolean offline = false; + private Duration maxAge = Duration.ZERO; + private DownloadProgressListener progressListener = DownloadProgressListener.NONE; + private int maxRetries = 1; + + private DownloadBuilder(URI url) { + this.url = url; + } + + static DownloadBuilder create(String url) throws URISyntaxException { + return new DownloadBuilder(new URI(url)); + } + + public DownloadBuilder sha1(String sha1) { + this.expectedHash = "sha1:" + sha1; + return this; + } + + public DownloadBuilder etag(boolean useEtag) { + this.useEtag = useEtag; + return this; + } + + public DownloadBuilder forceDownload() { + forceDownload = true; + return this; + } + + public DownloadBuilder offline() { + offline = true; + return this; + } + + public DownloadBuilder maxAge(Duration duration) { + this.maxAge = duration; + return this; + } + + public DownloadBuilder progress(DownloadProgressListener progressListener) { + this.progressListener = progressListener; + return this; + } + + public DownloadBuilder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public DownloadBuilder defaultCache() { + etag(true); + return maxAge(ONE_DAY); + } + + private Download build() { + return new Download(this.url, this.expectedHash, this.useEtag, this.forceDownload, this.offline, maxAge, progressListener); + } + + public void downloadPathAsync(Path path, DownloadExecutor executor) { + executor.runAsync(() -> downloadPath(path)); + } + + public void downloadPath(Path path) throws DownloadException { + withRetries(() -> { + build().downloadPath(path); + return null; + }); + } + + public String downloadString() throws DownloadException { + return withRetries(() -> build().downloadString()); + } + + public String downloadString(Path cache) throws DownloadException { + withRetries(() -> { + build().downloadPath(cache); + return null; + }); + + try { + return Files.readString(cache, StandardCharsets.UTF_8); + } catch (IOException e) { + try { + Files.delete(cache); + } catch (IOException ex) { + // Ignored + } + + throw new DownloadException("Failed to download and read string", e); + } + } + + private T withRetries(DownloadSupplier supplier) throws DownloadException { + for (int i = 1; i <= maxRetries; i++) { + try { + return supplier.get(); + } catch (DownloadException e) { + if (i == maxRetries) { + throw new DownloadException(String.format(Locale.ENGLISH, "Failed download after %d attempts", maxRetries), e); + } + } + } + + throw new IllegalStateException(); + } + + @FunctionalInterface + private interface DownloadSupplier { + T get() throws DownloadException; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadException.java b/src/main/java/net/fabricmc/loom/util/download/DownloadException.java new file mode 100644 index 00000000..993fa257 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadException.java @@ -0,0 +1,41 @@ +/* + * 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.util.download; + +import java.io.IOException; + +public class DownloadException extends IOException { + public DownloadException(String message) { + super(message); + } + + public DownloadException(String message, Throwable cause) { + super(message, cause); + } + + public DownloadException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java b/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java new file mode 100644 index 00000000..f1a4606e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadExecutor.java @@ -0,0 +1,84 @@ +/* + * 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.util.download; + +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class DownloadExecutor implements AutoCloseable { + private final ExecutorService executorService; + private final List downloadExceptions = Collections.synchronizedList(new ArrayList<>()); + + public DownloadExecutor(int threads) { + executorService = Executors.newFixedThreadPool(threads); + } + + void runAsync(DownloadRunner downloadRunner) { + if (!downloadExceptions.isEmpty()) { + return; + } + + executorService.execute(() -> { + try { + downloadRunner.run(); + } catch (DownloadException e) { + executorService.shutdownNow(); + downloadExceptions.add(e); + throw new UncheckedIOException(e); + } + }); + } + + @Override + public void close() throws DownloadException { + executorService.shutdown(); + + try { + executorService.awaitTermination(1, TimeUnit.DAYS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + if (!downloadExceptions.isEmpty()) { + DownloadException downloadException = new DownloadException("Failed to download"); + + for (DownloadException suppressed : downloadExceptions) { + downloadException.addSuppressed(suppressed); + } + + throw downloadException; + } + } + + @FunctionalInterface + public interface DownloadRunner { + void run() throws DownloadException; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadProgressListener.java b/src/main/java/net/fabricmc/loom/util/download/DownloadProgressListener.java new file mode 100644 index 00000000..982db3aa --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadProgressListener.java @@ -0,0 +1,47 @@ +/* + * 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.util.download; + +public interface DownloadProgressListener { + void onStart(); + + void onProgress(long bytesTransferred, long contentLength); + + void onEnd(boolean success); + + DownloadProgressListener NONE = new DownloadProgressListener() { + @Override + public void onStart() { + } + + @Override + public void onProgress(long bytesTransferred, long contentLength) { + } + + @Override + public void onEnd(boolean success) { + } + }; +} diff --git a/src/main/java/net/fabricmc/loom/util/download/GradleDownloadProgressListener.java b/src/main/java/net/fabricmc/loom/util/download/GradleDownloadProgressListener.java new file mode 100644 index 00000000..0bd3e54f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/GradleDownloadProgressListener.java @@ -0,0 +1,74 @@ +/* + * 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.util.download; + +import java.util.Objects; +import java.util.function.Function; + +import org.gradle.internal.logging.progress.ProgressLogger; +import org.jetbrains.annotations.Nullable; + +public class GradleDownloadProgressListener implements DownloadProgressListener { + private final String name; + private final Function progressLoggerFactory; + + @Nullable + private ProgressLogger progressLogger; + + public GradleDownloadProgressListener(String name, Function progressLoggerFactory) { + this.name = name; + this.progressLoggerFactory = progressLoggerFactory; + } + + @Override + public void onStart() { + progressLogger = progressLoggerFactory.apply(this.name); + } + + @Override + public void onProgress(long bytesTransferred, long contentLength) { + Objects.requireNonNull(progressLogger); + progressLogger.progress("Downloading %s - %s / %s".formatted(name, humanBytes(bytesTransferred), humanBytes(contentLength))); + } + + @Override + public void onEnd(boolean success) { + Objects.requireNonNull(progressLogger); + progressLogger.completed(); + progressLogger = null; + } + + private static String humanBytes(long bytes) { + if (bytes < 1024) { + return bytes + " B"; + } else if (bytes < 1024 * 1024) { + return (bytes / 1024) + " KB"; + } else if (bytes < 1024 * 1024 * 1024) { + return String.format("%.2f MB", bytes / (1024.0 * 1024.0)); + } else { + return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java b/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java new file mode 100644 index 00000000..8248f9bd --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java @@ -0,0 +1,56 @@ +/* + * 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.util.gradle; + +import java.io.Closeable; +import java.io.IOException; + +import org.gradle.api.Project; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.internal.logging.progress.ProgressLogger; +import org.gradle.internal.logging.progress.ProgressLoggerFactory; + +public class ProgressGroup implements Closeable { + private final ProgressLoggerFactory progressLoggerFactory; + private final ProgressLogger progressGroup; + + public ProgressGroup(Project project, String name) { + this.progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class); + this.progressGroup = this.progressLoggerFactory.newOperation(name).setDescription(name); + this.progressGroup.started(); + } + + public ProgressLogger createProgressLogger(String name) { + ProgressLogger progressLogger = this.progressLoggerFactory.newOperation(getClass(), progressGroup); + progressLogger.setDescription(name); + progressLogger.start(name, null); + return progressLogger; + } + + @Override + public void close() throws IOException { + this.progressGroup.completed(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java b/src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java deleted file mode 100644 index d2fb1a4a..00000000 --- a/src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2016-2020 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.gradle; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import org.gradle.api.Project; - -/** - * Wrapper to ProgressLogger internal API. - */ -public class ProgressLoggerHelper { - private final Object logger; - private final Method getDescription, setDescription, getShortDescription, setShortDescription, getLoggingHeader, setLoggingHeader, start, started, startedArg, progress, completed, completedArg; - - private ProgressLoggerHelper(Object logger) { - this.logger = logger; - this.getDescription = getMethod("getDescription"); - this.setDescription = getMethod("setDescription", String.class); - this.getShortDescription = getMethod("getShortDescription"); - this.setShortDescription = getMethod("setShortDescription", String.class); - this.getLoggingHeader = getMethod("getLoggingHeader"); - this.setLoggingHeader = getMethod("setLoggingHeader", String.class); - this.start = getMethod("start", String.class, String.class); - this.started = getMethod("started"); - this.startedArg = getMethod("started", String.class); - this.progress = getMethod("progress", String.class); - this.completed = getMethod("completed"); - this.completedArg = getMethod("completed", String.class); - } - - private static Class getFactoryClass() { - Class progressLoggerFactoryClass = null; - - try { - // Gradle 2.14 and higher - progressLoggerFactoryClass = Class.forName("org.gradle.internal.logging.progress.ProgressLoggerFactory"); - } catch (ClassNotFoundException e) { - // prior to Gradle 2.14 - try { - progressLoggerFactoryClass = Class.forName("org.gradle.logging.ProgressLoggerFactory"); - } catch (ClassNotFoundException ignored) { - // Unsupported Gradle version - } - } - - return progressLoggerFactoryClass; - } - - private Method getMethod(String methodName, Class... args) { - if (logger != null) { - try { - return logger.getClass().getMethod(methodName, args); - } catch (NoSuchMethodException ignored) { - // Nope - } - } - - return null; - } - - private Object invoke(Method method, Object... args) { - if (logger != null) { - try { - method.setAccessible(true); - return method.invoke(logger, args); - } catch (IllegalAccessException | InvocationTargetException ignored) { - // Nope - } - } - - return null; - } - - /** - * Get a Progress logger from the Gradle internal API. - * - * @param project The project - * @param category The logger category - * @return In any case a progress logger - */ - public static ProgressLoggerHelper getProgressFactory(Project project, String category) { - try { - Method getServices = project.getClass().getMethod("getServices"); - Object serviceFactory = getServices.invoke(project); - Method get = serviceFactory.getClass().getMethod("get", Class.class); - Object progressLoggerFactory = get.invoke(serviceFactory, getFactoryClass()); - Method newOperation = progressLoggerFactory.getClass().getMethod("newOperation", String.class); - return new ProgressLoggerHelper(newOperation.invoke(progressLoggerFactory, category)); - } catch (Exception e) { - project.getLogger().error("Unable to get progress logger. Download progress will not be displayed."); - return new ProgressLoggerHelper(null); - } - } - - /** - * Returns the description of the operation. - * - * @return the description, must not be empty. - */ - public String getDescription() { - return (String) invoke(getDescription); - } - - /** - * Sets the description of the operation. This should be a full, stand-alone description of the operation. - * - *

This must be called before {@link #started()} - * - * @param description The description. - */ - public ProgressLoggerHelper setDescription(String description) { - invoke(setDescription, description); - return this; - } - - /** - * Returns the short description of the operation. This is used in place of the full description when display space is limited. - * - * @return The short description, must not be empty. - */ - public String getShortDescription() { - return (String) invoke(getShortDescription); - } - - /** - * Sets the short description of the operation. This is used in place of the full description when display space is limited. - * - *

This must be called before {@link #started()} - * - * @param description The short description. - */ - public ProgressLoggerHelper setShortDescription(String description) { - invoke(setShortDescription, description); - return this; - } - - /** - * Returns the logging header for the operation. This is logged before any other log messages for this operation are logged. It is usually - * also logged at the end of the operation, along with the final status message. Defaults to null. - * - *

If not specified, no logging header is logged. - * - * @return The logging header, possibly empty. - */ - public String getLoggingHeader() { - return (String) invoke(getLoggingHeader); - } - - /** - * Sets the logging header for the operation. This is logged before any other log messages for this operation are logged. It is usually - * also logged at the end of the operation, along with the final status message. Defaults to null. - * - * @param header The header. May be empty or null. - */ - public ProgressLoggerHelper setLoggingHeader(String header) { - invoke(setLoggingHeader, header); - return this; - } - - /** - * Convenience method that sets descriptions and logs started() event. - * - * @return this logger instance - */ - public ProgressLoggerHelper start(String description, String shortDescription) { - invoke(start, description, shortDescription); - return this; - } - - /** - * Logs the start of the operation, with no initial status. - */ - public void started() { - invoke(started); - } - - /** - * Logs the start of the operation, with the given status. - * - * @param status The initial status message. Can be null or empty. - */ - public void started(String status) { - invoke(started, status); - } - - /** - * Logs some progress, indicated by a new status. - * - * @param status The new status message. Can be null or empty. - */ - public void progress(String status) { - invoke(progress, status); - } - - /** - * Logs the completion of the operation, with no final status. - */ - public void completed() { - invoke(completed); - } - - /** - * Logs the completion of the operation, with a final status. This is generally logged along with the description. - * - * @param status The final status message. Can be null or empty. - */ - public void completed(String status) { - invoke(completed, status); - } -} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java index 605568a9..f85e2ed2 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java @@ -25,12 +25,13 @@ package net.fabricmc.loom.util.gradle; import java.io.IOException; +import java.util.Locale; import net.fabricmc.loom.util.IOStringConsumer; public record ThreadedSimpleProgressLogger(IOStringConsumer parent) implements IOStringConsumer { @Override public void accept(String data) throws IOException { - parent.accept("%d::%s".formatted(Thread.currentThread().getId(), data)); + parent.accept(String.format(Locale.ENGLISH, "%d::%s", Thread.currentThread().getId(), data)); } } diff --git a/src/main/resources/eclipse_run_config_template.xml b/src/main/resources/eclipse_run_config_template.xml index 737e9cd3..a8c380fd 100644 --- a/src/main/resources/eclipse_run_config_template.xml +++ b/src/main/resources/eclipse_run_config_template.xml @@ -16,4 +16,5 @@ + diff --git a/src/main/resources/idea_run_config_template.xml b/src/main/resources/idea_run_config_template.xml index 963caef1..be6e72ec 100644 --- a/src/main/resources/idea_run_config_template.xml +++ b/src/main/resources/idea_run_config_template.xml @@ -12,5 +12,6 @@ %IDEA_ENV_VARS% + diff --git a/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy b/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy index e98e82a2..78e1cf28 100644 --- a/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/LoomTestConstants.groovy @@ -27,7 +27,7 @@ package net.fabricmc.loom.test import org.gradle.util.GradleVersion class LoomTestConstants { - private final static String NIGHTLY_VERSION = "7.6-20220620222921+0000" + private final static String NIGHTLY_VERSION = "7.6-20220722221130+0000" private final static boolean NIGHTLY_EXISTS = nightlyExists(NIGHTLY_VERSION) public final static String DEFAULT_GRADLE = GradleVersion.current().getVersion() diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/MixinApAutoRefmapTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/MixinApAutoRefmapTest.groovy index 69bfa93c..14981f93 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/MixinApAutoRefmapTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/MixinApAutoRefmapTest.groovy @@ -27,7 +27,7 @@ package net.fabricmc.loom.test.integration import net.fabricmc.loom.test.util.GradleProjectTestTrait import spock.lang.Specification import spock.lang.Unroll -import com.google.gson.JsonParser; +import com.google.gson.JsonParser import java.util.jar.JarFile import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy index ff4af2cb..75412ade 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/ReproducibleBuildTest.groovy @@ -54,8 +54,8 @@ class ReproducibleBuildTest extends Specification implements GradleProjectTestTr where: version | modHash | sourceHash - DEFAULT_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["be31766e6cafbe4ae3bca9e35ba63169", "7348b0bd87d36d7ec6f3bca9c2b66062"] - PRE_RELEASE_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["be31766e6cafbe4ae3bca9e35ba63169", "7348b0bd87d36d7ec6f3bca9c2b66062"] + DEFAULT_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["0d9eec9248d93eb6ec4a1cd4d927e609", "436bf54ef015576b0a338d55d9a0bb82"] + PRE_RELEASE_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["0d9eec9248d93eb6ec4a1cd4d927e609", "436bf54ef015576b0a338d55d9a0bb82"] } String generateMD5(File file) { diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/IdeaClasspathModificationsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/IdeaClasspathModificationsTest.groovy index 7dc933e0..f02a5643 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/IdeaClasspathModificationsTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/IdeaClasspathModificationsTest.groovy @@ -84,6 +84,10 @@ class IdeaClasspathModificationsTest extends Specification { + + %IDEA_ENV_VARS% + + '''.trim() @@ -101,6 +105,10 @@ class IdeaClasspathModificationsTest extends Specification { + + %IDEA_ENV_VARS% + + '''.trim() diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/LoomMocks.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/LoomMocks.groovy index 1586abdb..54b70b8d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/LoomMocks.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/LoomMocks.groovy @@ -26,6 +26,9 @@ package net.fabricmc.loom.test.unit import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider import net.fabricmc.loom.test.util.GradleTestUtil +import net.fabricmc.loom.util.download.Download + +import java.util.function.Function import static org.mockito.Mockito.spy import static org.mockito.Mockito.when @@ -34,12 +37,14 @@ class LoomMocks { static IntermediaryMappingsProvider intermediaryMappingsProviderMock(String minecraftVersion, String intermediaryUrl) { def minecraftVersionProperty = GradleTestUtil.mockProperty(minecraftVersion) def intermediaryUrlProperty = GradleTestUtil.mockProperty(intermediaryUrl) + def downloaderProperty = GradleTestUtil.mockProperty(Download.&create as Function) Objects.requireNonNull(minecraftVersionProperty.get()) def mock = spy(IntermediaryMappingsProvider.class) when(mock.getMinecraftVersion()).thenReturn(minecraftVersionProperty) when(mock.getIntermediaryUrl()).thenReturn(intermediaryUrlProperty) + when(mock.getDownloader()).thenReturn(downloaderProperty) return mock } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy new file mode 100644 index 00000000..451c3edc --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadFileTest.groovy @@ -0,0 +1,305 @@ +/* + * 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.download + +import io.javalin.http.HttpCode +import net.fabricmc.loom.util.download.Download +import net.fabricmc.loom.util.download.DownloadException +import net.fabricmc.loom.util.download.DownloadExecutor +import net.fabricmc.loom.util.download.DownloadProgressListener + +import java.nio.file.Files +import java.nio.file.attribute.FileTime +import java.time.Duration +import java.time.Instant + +class DownloadFileTest extends DownloadTest { + def "File: Simple"() { + setup: + server.get("/simpleFile") { + it.result("Hello World") + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + + when: + def result = Download.create("$PATH/simpleFile").downloadPath(output) + + then: + Files.readString(output) == "Hello World" + } + + def "File: Not found"() { + setup: + server.get("/fileNotfound") { + it.status(404) + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + + when: + def result = Download.create("$PATH/stringNotFound").downloadPath(output) + + then: + thrown DownloadException + } + + def "Cache: Sha1"() { + setup: + int requestCount = 0 + + server.get("/sha1.txt") { + it.result("Hello World") + requestCount ++ + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + + when: + for (i in 0..<2) { + Download.create("$PATH/sha1.txt") + .sha1("0a4d55a8d778e5022fab701977c5d840bbc486d0") + .downloadPath(output) + } + + then: + requestCount == 1 + } + + def "Invalid Sha1"() { + setup: + server.get("/sha1.invalid") { + it.result("Hello World") + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + + when: + Download.create("$PATH/sha1.invalid") + .sha1("d139cccf047a749691416ce385d3f168c1e28309") + .downloadPath(output) + + then: + // Ensure the file we downloaded with the wrong hash was deleted + Files.notExists(output) + thrown DownloadException + } + + def "Offline"() { + setup: + int requestCount = 0 + + server.get("/offline.txt") { + it.result("Hello World") + requestCount ++ + } + + def output = new File(File.createTempDir(), "offline.txt").toPath() + + when: + Download.create("$PATH/offline.txt") + .downloadPath(output) + + Download.create("$PATH/offline.txt") + .offline() + .downloadPath(output) + + then: + requestCount == 1 + } + + def "Max Age"() { + setup: + int requestCount = 0 + + server.get("/maxage.txt") { + it.result("Hello World") + requestCount ++ + } + + def output = new File(File.createTempDir(), "maxage.txt").toPath() + + when: + Download.create("$PATH/maxage.txt") + .maxAge(Duration.ofDays(1)) + .downloadPath(output) + + Download.create("$PATH/maxage.txt") + .maxAge(Duration.ofDays(1)) + .downloadPath(output) + + def twoDaysAgo = Instant.now() - Duration.ofDays(2) + Files.setLastModifiedTime(output, FileTime.from(twoDaysAgo)) + + Download.create("$PATH/maxage.txt") + .maxAge(Duration.ofDays(1)) + .downloadPath(output) + + then: + requestCount == 2 + } + + def "ETag"() { + setup: + int requestCount = 0 + + server.get("/etag") { + def clientEtag = it.req.getHeader("If-None-Match") + + def result = "Hello world" + def etag = result.hashCode().toString() + it.header("ETag", etag) + + if (clientEtag == etag) { + // Etag matches, no need to send the data. + it.status(HttpCode.NOT_MODIFIED) + return + } + + it.result(result) + requestCount ++ + } + + def output = new File(File.createTempDir(), "etag.txt").toPath() + + when: + for (i in 0..<2) { + Download.create("$PATH/etag") + .etag(true) + .downloadPath(output) + } + + then: + requestCount == 1 + } + + def "Progress: File"() { + setup: + server.get("/progressFile") { + it.result("Hello World") + } + + def output = new File(File.createTempDir(), "file.txt").toPath() + def started, ended = false + + when: + Download.create("$PATH/progressFile") + .progress(new DownloadProgressListener() { + @Override + void onStart() { + started = true + } + + @Override + void onProgress(long bytesTransferred, long contentLength) { + } + + @Override + void onEnd(boolean success) { + ended = true + } + }) + .downloadPath(output) + + then: + started + ended + } + + def "Progress: String"() { + setup: + server.get("/progressString") { + it.result("Hello World") + } + + def started, ended = false + + when: + Download.create("$PATH/progressFile") + .progress(new DownloadProgressListener() { + @Override + void onStart() { + started = true + } + + @Override + void onProgress(long bytesTransferred, long contentLength) { + } + + @Override + void onEnd(boolean success) { + ended = true + } + }) + .downloadString() + + then: + started + ended + } + + def "File: Async"() { + setup: + server.get("/async1") { + it.result("Hello World") + } + + def dir = File.createTempDir().toPath() + + when: + new DownloadExecutor(2).withCloseable { + Download.create("$PATH/async1").downloadPathAsync(dir.resolve("1.txt"), it) + Download.create("$PATH/async1").downloadPathAsync(dir.resolve("2.txt"), it) + Download.create("$PATH/async1").downloadPathAsync(dir.resolve("3.txt"), it) + Download.create("$PATH/async1").downloadPathAsync(dir.resolve("4.txt"), it) + } + + then: + Files.readString(dir.resolve("4.txt")) == "Hello World" + } + + def "File: Async Error"() { + setup: + server.get("/async2") { + it.result("Hello World") + } + + def dir = File.createTempDir().toPath() + + when: + new DownloadExecutor(2).withCloseable { + Download.create("$PATH/async2").downloadPathAsync(dir.resolve("1.txt"), it) + Download.create("$PATH/async2").downloadPathAsync(dir.resolve("2.txt"), it) + Download.create("$PATH/async2").downloadPathAsync(dir.resolve("3.txt"), it) + Download.create("$PATH/async2").downloadPathAsync(dir.resolve("4.txt"), it) + + Download.create("$PATH/asyncError").downloadPathAsync(dir.resolve("5.txt"), it) + Download.create("$PATH/asyncError2").downloadPathAsync(dir.resolve("6.txt"), it) + } + + then: + thrown DownloadException + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy new file mode 100644 index 00000000..66f58dd6 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadStringTest.groovy @@ -0,0 +1,98 @@ +/* + * 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.download + +import io.javalin.http.HttpCode +import net.fabricmc.loom.util.download.Download +import net.fabricmc.loom.util.download.DownloadException + +class DownloadStringTest extends DownloadTest { + def "String: Download"() { + setup: + server.get("/downloadString") { + it.result("Hello World!") + } + + when: + def result = Download.create("$PATH/downloadString").downloadString() + + then: + result == "Hello World!" + } + + def "String: Not found"() { + setup: + server.get("/stringNotFound") { + it.status(404) + } + + when: + def result = Download.create("$PATH/stringNotFound") + .maxRetries(3) // Ensure we still error as expected when retrying + .downloadString() + + then: + thrown DownloadException + } + + def "String: Redirect"() { + setup: + server.get("/redirectString2") { + it.result("Hello World!") + } + server.get("/redirectString") { + it.redirect("$PATH/redirectString2") + } + + when: + def result = Download.create("$PATH/redirectString").downloadString() + + then: + result == "Hello World!" + } + + def "String: Retries"() { + setup: + int requests = 0 + server.get("/retryString") { + requests ++ + + if (requests < 3) { + it.status(HttpCode.INTERNAL_SERVER_ERROR) + return + } + + it.result("Hello World " + requests) + } + + when: + def result = Download.create("$PATH/retryString") + .maxRetries(3) + .downloadString() + + then: + result == "Hello World 3" + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadTest.groovy new file mode 100644 index 00000000..5f8c1490 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/download/DownloadTest.groovy @@ -0,0 +1,42 @@ +/* + * 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.download + +import io.javalin.Javalin +import spock.lang.Shared +import spock.lang.Specification + +abstract class DownloadTest extends Specification { + static final String PATH = "http://localhost:9081" + + @Shared + Javalin server = Javalin.create { config -> + config.enableDevLogging() + }.start(9081) + + def cleanupSpec() { + server.stop() + } +} \ No newline at end of file diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/FileMappingLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/FileMappingLayerTest.groovy index 087ed059..37e7352e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/FileMappingLayerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/FileMappingLayerTest.groovy @@ -28,8 +28,8 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace import net.fabricmc.loom.api.mappings.layered.spec.FileSpec import net.fabricmc.loom.configuration.providers.mappings.file.FileMappingsSpecBuilderImpl import net.fabricmc.loom.configuration.providers.mappings.intermediary.IntermediaryMappingsSpec -import net.fabricmc.loom.util.DownloadUtil import net.fabricmc.loom.util.ZipUtils +import net.fabricmc.loom.util.download.Download import spock.lang.Unroll import java.nio.file.Path @@ -72,7 +72,8 @@ class FileMappingLayerTest extends LayeredMappingsSpecification { mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17 def mappingsDownload = VERSION_META_1_17.download('client_mappings') def mappingsFile = new File(tempDir, 'mappings.txt') - DownloadUtil.downloadIfChanged(new URL(mappingsDownload.url()), mappingsFile, mappingContext.logger) + Download.create(mappingsDownload.url()) + .downloadPath(mappingsFile.toPath()) when: def mappings = getLayeredMappings( new IntermediaryMappingsSpec(), diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy index 7cd139ff..da3d5b56 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsSpecification.groovy @@ -34,6 +34,8 @@ import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsProcess import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider import net.fabricmc.loom.test.unit.LoomMocks +import net.fabricmc.loom.util.download.Download +import net.fabricmc.loom.util.download.DownloadBuilder import net.fabricmc.mappingio.adapter.MappingDstNsReorder import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch import net.fabricmc.mappingio.format.Tiny2Writer @@ -148,5 +150,15 @@ abstract class LayeredMappingsSpecification extends Specification implements Lay Logger getLogger() { return mockLogger } - } + + @Override + DownloadBuilder download(String url) { + return Download.create(url) + } + + @Override + boolean refreshDeps() { + return false + } + } } diff --git a/src/test/resources/projects/interfaceInjection/build.gradle b/src/test/resources/projects/interfaceInjection/build.gradle index 24118140..f249f408 100644 --- a/src/test/resources/projects/interfaceInjection/build.gradle +++ b/src/test/resources/projects/interfaceInjection/build.gradle @@ -14,5 +14,7 @@ dependencies { mappings "net.fabricmc:yarn:1.17.1+build.59:v2" modImplementation "net.fabricmc:fabric-loader:0.11.6" - modImplementation files("dummy.jar") + // I-faces will still be applied as the jar is on both configurations. + modCompileOnly files("dummy.jar") + modRuntimeOnly files("dummy.jar") } \ No newline at end of file diff --git a/src/test/resources/projects/mixinApSimple/build.gradle b/src/test/resources/projects/mixinApSimple/build.gradle index 72f51f63..69a64bbf 100644 --- a/src/test/resources/projects/mixinApSimple/build.gradle +++ b/src/test/resources/projects/mixinApSimple/build.gradle @@ -73,6 +73,10 @@ loom { defaultRefmapName = "default-refmap0000.json" add(sourceSets["main"], "main-refmap0000.json") add(sourceSets["mixin"]) + + messages { + MIXIN_SOFT_TARGET_IS_PUBLIC = 'error' + } } }