Rewrite download utils (#681)

- eTag support (It seems Mojang's CDN and our meta/maven do not support this right now)
- Age based caching.
- Sha1 based caching.
- HTTP(S) proxy settings should now be respected.
- The downloader has better awareness of offline mode and refresh deps, cleaning up the calling code a bit.
- Uses the new Java 11 HTTP client, provides async support for downloading multiple files
- Progress handling (TODO needs hooking up to gradle)
- Better compression support. Handled by [Methanol](https://mizosoft.github.io/methanol/) 
- Unit tested with a real web server.
This commit is contained in:
modmuss50
2022-07-25 18:01:11 +01:00
committed by GitHub
parent 4bc7e39149
commit c4c6de23e3
40 changed files with 1440 additions and 896 deletions

View File

@@ -77,9 +77,10 @@ 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')
// game handling utils
implementation ('net.fabricmc:stitch:0.6.1') {
implementation ('net.fabricmc:stitch:0.6.2') {
exclude module: 'enigma'
}

View File

@@ -46,6 +46,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMi
import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider;
import net.fabricmc.loom.extension.LoomFiles;
import net.fabricmc.loom.extension.MixinExtension;
import net.fabricmc.loom.util.download.DownloadBuilder;
public interface LoomGradleExtension extends LoomGradleExtensionAPI {
static LoomGradleExtension get(Project project) {
@@ -106,4 +107,6 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
List<AccessWidenerFile> getTransitiveAccessWideners();
void addTransitiveAccessWideners(List<AccessWidenerFile> accessWidenerFiles);
DownloadBuilder download(String url);
}

View File

@@ -56,6 +56,7 @@ public interface LoomGradleExtensionAPI {
RegularFileProperty getAccessWidenerPath();
@Deprecated(forRemoval = true)
Property<Boolean> getShareRemapCaches();
default void shareCaches() {

View File

@@ -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<String> getMinecraftVersion();
public abstract Property<Function<String, DownloadBuilder>> getDownloader();
/**
* Generate or download a tinyv2 mapping file with intermediary and named namespaces.
* @throws IOException

View File

@@ -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,6 @@ public interface MappingContext {
Path workingDirectory(String name);
Logger getLogger();
DownloadBuilder download(String url);
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
@@ -318,7 +319,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);
}

View File

@@ -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;
@@ -91,10 +92,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());

View File

@@ -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,11 @@ public class GradleMappingContext implements MappingContext {
return project.getLogger();
}
@Override
public DownloadBuilder download(String url) {
return extension.download(url);
}
public Project getProject() {
return project;
}

View File

@@ -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;
@@ -37,7 +36,6 @@ 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);
@@ -53,14 +51,17 @@ public abstract class IntermediaryMappingsProvider extends IntermediateMappingsP
// 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);
}

View File

@@ -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<MappingsSpec<?>> 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()));
}
}

View File

@@ -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;
@@ -39,25 +38,16 @@ import net.fabricmc.loom.api.mappings.layered.MappingLayer;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
import net.fabricmc.loom.configuration.providers.mappings.intermediary.IntermediaryMappingLayer;
import net.fabricmc.loom.configuration.providers.mappings.utils.DstNameFilterMappingVisitor;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.HashedDownloadUtil;
import net.fabricmc.mappingio.MappingVisitor;
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
import net.fabricmc.mappingio.format.ProGuardReader;
public record MojangMappingLayer(MinecraftVersionMeta.Download clientDownload,
MinecraftVersionMeta.Download serverDownload,
Path workingDir, boolean nameSyntheticMembers,
public record MojangMappingLayer(Path clientMappings, Path serverMappings, boolean nameSyntheticMembers,
Logger logger) 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("client.txt");
Path serverMappings = workingDir().resolve("server.txt");
download(clientMappings, serverMappings);
printMappingsLicense(clientMappings);
// Filter out field names matching the pattern
@@ -73,11 +63,6 @@ public record MojangMappingLayer(MinecraftVersionMeta.Download clientDownload,
}
}
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("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

View File

@@ -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(boolean nameSyntheticMembers) implements MappingsSpec<MojangMappingLayer> {
// Keys in dependency manifest
@@ -35,16 +39,32 @@ public record MojangMappingsSpec(boolean nameSyntheticMembers) implements Mappin
@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(
versionInfo.download(MANIFEST_CLIENT_MAPPINGS),
versionInfo.download(MANIFEST_SERVER_MAPPINGS),
context.workingDirectory("mojang"),
clientMappings,
serverMappings,
nameSyntheticMembers(),
context.getLogger()
);

View File

@@ -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);
}
}

View File

@@ -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 final class MergedMinecraftProvider extends MinecraftProvider {
@@ -66,8 +65,8 @@ public final class MergedMinecraftProvider extends MinecraftProvider {
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);

View File

@@ -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;
@@ -46,9 +41,11 @@ import net.fabricmc.loom.LoomGradlePlugin;
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;
@@ -86,35 +83,15 @@ public abstract class MinecraftProvider {
final DependencyInfo dependency = DependencyInfo.create(getProject(), Constants.Configurations.MINECRAFT);
minecraftVersion = dependency.getDependency().getVersion();
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());
@@ -141,131 +118,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<ManifestVersion.Versions> 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<ManifestVersion.Versions> 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<ManifestVersion.Versions> 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<ManifestVersion.Versions> 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);
}
}
}

View File

@@ -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");
}

View File

@@ -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;
@@ -48,6 +49,8 @@ import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider;
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;
@@ -206,9 +209,36 @@ 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();
}
// TODO
//builder.executor();
return builder;
}
@Override
protected <T extends IntermediateMappingsProvider> void configureIntermediateMappingsProviderInternal(T provider) {
provider.getMinecraftVersion().set(getProject().provider(() -> getMinecraftProvider().minecraftVersion()));
provider.getMinecraftVersion().disallowChanges();
provider.getDownloader().set(this::download);
provider.getDownloader().disallowChanges();
}
}

View File

@@ -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,14 +43,18 @@ 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;
public abstract class DownloadAssetsTask extends AbstractLoomTask {
@Input
public abstract Property<String> getAssetsHash();
@Input
public abstract Property<Integer> getDownloadThreads();
@OutputDirectory
public abstract RegularFileProperty getAssetsDirectory();
@@ -74,6 +68,7 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
getAssetsDirectory().set(assetsDir);
getAssetsHash().set(versionInfo.assetIndex().sha1());
getDownloadThreads().convention(Runtime.getRuntime().availableProcessors());
if (versionInfo.assets().equals("legacy")) {
getLegacyResourcesDirectory().set(new File(assetsDir, "/legacy/" + versionInfo.id()));
@@ -83,80 +78,27 @@ 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<ProgressLoggerHelper> 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))
.downloadPathAsync(getAssetsPath(object, assetIndex), executor);
}
final Supplier<ProgressLoggerHelper> 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, () -> {
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);
}
}
@@ -168,35 +110,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();
}
}

View File

@@ -183,7 +183,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());

View File

@@ -27,6 +27,7 @@ package net.fabricmc.loom.util;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
@@ -63,6 +64,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());

View File

@@ -1,232 +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 void downloadIfChanged(URL from, File to, Logger logger) throws IOException {
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 <code>true</code>) or everything
* @throws IOException If an exception occurs during the process
*/
public static void downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException {
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; //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);
}
}
/**
* Creates a new file in the same directory as the given file with <code>.etag</code> 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 <code>null</code> 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();
}
}
}

View File

@@ -1,146 +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 void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet) throws IOException {
downloadIfInvalid(from, to, expectedHash, logger, quiet, () -> { });
}
public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, Runnable startDownload) throws IOException {
if (LoomGradlePlugin.refreshDeps && !Boolean.getBoolean("loom.refresh")) {
delete(to);
}
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();
}
}
}

View File

@@ -37,20 +37,22 @@ 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;
import net.fabricmc.loom.api.RemapConfigurationSettings;
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
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;
public class SourceRemapper {
private final Project project;
private final boolean toNamed;
private final List<Consumer<ProgressLoggerHelper>> remapTasks = new ArrayList<>();
private final List<Consumer<ProgressLogger>> remapTasks = new ArrayList<>();
private Mercury mercury;
@@ -89,7 +91,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));

View File

@@ -0,0 +1,371 @@
/*
* 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 <T> HttpResponse<T> send(HttpRequest httpRequest, HttpResponse.BodyHandler<T> 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<String> 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<String> 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);
HttpResponse<Path> response = send(httpRequest, HttpResponse.BodyHandlers.ofFile(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) {
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);
} 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 (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<String> 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<String> 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<String> 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 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);
}
}

View File

@@ -0,0 +1,121 @@
/*
* 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;
@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 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 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 {
build().downloadPath(path);
}
public String downloadString() throws DownloadException {
return build().downloadString();
}
public String downloadString(Path cache) throws DownloadException {
downloadPath(cache);
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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<DownloadException> 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;
}
}

View File

@@ -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) {
}
};
}

View File

@@ -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<String, ProgressLogger> progressLoggerFactory;
@Nullable
private ProgressLogger progressLogger;
public GradleDownloadProgressListener(String name, Function<String, ProgressLogger> 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));
}
}
}

View File

@@ -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();
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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);
}
}

View File

@@ -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));
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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 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").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!"
}
}

View File

@@ -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()
}
}

View File

@@ -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(),

View File

@@ -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,10 @@ abstract class LayeredMappingsSpecification extends Specification implements Lay
Logger getLogger() {
return mockLogger
}
}
@Override
DownloadBuilder download(String url) {
return Download.create(url)
}
}
}