mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-03-28 04:07:01 -05:00
Merge with Fabric 0.13, stage 5
This commit is contained in:
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [7.4.0-jdk17]
|
||||
runs-on: ubuntu-20.04
|
||||
version: [7.5.0-jdk17]
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: gradle:${{ matrix.version }}
|
||||
options: --user root
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
# Lets wait to ensure it builds before going running tests
|
||||
needs: build
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: gradle:7.4.0-jdk17
|
||||
image: gradle:7.5.0-jdk17
|
||||
options: --user root
|
||||
|
||||
steps:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
version: [7.4.0-jdk17]
|
||||
test: ${{ fromJson(needs.prepare_test_matrix.outputs.matrix) }}
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: gradle:${{ matrix.version }}
|
||||
options: --user root
|
||||
@@ -100,8 +100,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java: [ 17 ]
|
||||
os: [ windows-2022, ubuntu-20.04, macos-11 ]
|
||||
java: [ 17, 18 ]
|
||||
os: [ windows-2022, ubuntu-22.04, macos-11 ]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
15
build.gradle
15
build.gradle
@@ -8,8 +8,8 @@ plugins {
|
||||
id 'checkstyle'
|
||||
id 'jacoco'
|
||||
id 'codenarc'
|
||||
id "org.jetbrains.kotlin.jvm" version "1.5.31" // Must match the version included with gradle.
|
||||
id "com.diffplug.spotless" version "6.3.0"
|
||||
id "org.jetbrains.kotlin.jvm" version "1.6.10" // Must match the version included with gradle.
|
||||
id "com.diffplug.spotless" version "6.8.0"
|
||||
}
|
||||
|
||||
java {
|
||||
@@ -24,7 +24,7 @@ tasks.withType(JavaCompile).configureEach {
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "16" // Change to 17 when updating gradle/kotlin to 1.6.10
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,11 @@ dependencies {
|
||||
implementation ('org.ow2.asm:asm-commons:9.3')
|
||||
implementation ('org.ow2.asm:asm-tree:9.3')
|
||||
implementation ('org.ow2.asm:asm-util:9.3')
|
||||
implementation ('com.github.mizosoft.methanol:methanol:1.7.0')
|
||||
implementation ('me.tongfei:progressbar:0.9.0')
|
||||
|
||||
// game handling utils
|
||||
implementation ('net.fabricmc:stitch:0.6.1') {
|
||||
implementation ('net.fabricmc:stitch:0.6.2') {
|
||||
exclude module: 'mercury'
|
||||
exclude module: 'enigma'
|
||||
}
|
||||
@@ -120,7 +121,7 @@ dependencies {
|
||||
}
|
||||
|
||||
// Kapt integration
|
||||
compileOnly('org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31') // Must match the version included with gradle.
|
||||
compileOnly('org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10') // Must match the version included with gradle.
|
||||
|
||||
// Forge patches
|
||||
implementation ('net.minecraftforge:installertools:1.2.0')
|
||||
@@ -177,11 +178,11 @@ spotless {
|
||||
|
||||
checkstyle {
|
||||
configFile = file('checkstyle.xml')
|
||||
toolVersion = '9.3'
|
||||
toolVersion = '10.3.1'
|
||||
}
|
||||
|
||||
codenarc {
|
||||
toolVersion = "2.2.0"
|
||||
toolVersion = "3.1.0"
|
||||
configFile = file("codenarc.groovy")
|
||||
}
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -55,6 +55,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftPr
|
||||
import net.fabricmc.loom.extension.LoomFiles;
|
||||
import net.fabricmc.loom.extension.MixinExtension;
|
||||
import net.fabricmc.loom.util.ModPlatform;
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder;
|
||||
|
||||
public interface LoomGradleExtension extends LoomGradleExtensionAPI {
|
||||
static LoomGradleExtension get(Project project) {
|
||||
@@ -124,6 +125,12 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
|
||||
|
||||
void addTransitiveAccessWideners(List<AccessWidenerFile> accessWidenerFiles);
|
||||
|
||||
DownloadBuilder download(String url);
|
||||
|
||||
boolean refreshDeps();
|
||||
|
||||
void setRefreshDeps(boolean refreshDeps);
|
||||
|
||||
// ===================
|
||||
// Architectury Loom
|
||||
// ===================
|
||||
|
||||
@@ -51,7 +51,6 @@ import net.fabricmc.loom.task.LoomTasks;
|
||||
import net.fabricmc.loom.util.LibraryLocationLogger;
|
||||
|
||||
public class LoomGradlePlugin implements BootstrappedPlugin {
|
||||
public static boolean refreshDeps;
|
||||
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
public static final String LOOM_VERSION = Objects.requireNonNullElse(LoomGradlePlugin.class.getPackage().getImplementationVersion(), "0.0.0+unknown");
|
||||
@@ -76,12 +75,6 @@ public class LoomGradlePlugin implements BootstrappedPlugin {
|
||||
|
||||
LibraryLocationLogger.logLibraryVersions();
|
||||
|
||||
refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh");
|
||||
|
||||
if (refreshDeps) {
|
||||
project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower.");
|
||||
}
|
||||
|
||||
// Apply default plugins
|
||||
project.apply(ImmutableMap.of("plugin", "java-library"));
|
||||
project.apply(ImmutableMap.of("plugin", "eclipse"));
|
||||
|
||||
@@ -61,12 +61,6 @@ public interface LoomGradleExtensionAPI {
|
||||
|
||||
RegularFileProperty getAccessWidenerPath();
|
||||
|
||||
Property<Boolean> getShareRemapCaches();
|
||||
|
||||
default void shareCaches() {
|
||||
getShareRemapCaches().set(true);
|
||||
}
|
||||
|
||||
NamedDomainObjectContainer<DecompilerOptions> getDecompilerOptions();
|
||||
|
||||
void decompilers(Action<NamedDomainObjectContainer<DecompilerOptions>> action);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
package net.fabricmc.loom.api;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.util.PatternSet;
|
||||
@@ -38,6 +39,10 @@ public interface MixinExtensionAPI {
|
||||
|
||||
Property<String> getRefmapTargetNamespace();
|
||||
|
||||
MapProperty<String, String> getMessages();
|
||||
|
||||
Property<Boolean> getShowMessageTypes();
|
||||
|
||||
/**
|
||||
* Apply Mixin AP to sourceSet.
|
||||
* @param sourceSet the sourceSet that applies Mixin AP.
|
||||
@@ -95,4 +100,6 @@ public interface MixinExtensionAPI {
|
||||
* @param sourceSetName the name of sourceSet that applies Mixin AP.
|
||||
*/
|
||||
void add(String sourceSetName);
|
||||
|
||||
void messages(Action<MapProperty<String, String>> action);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.gradle.api.logging.Logger;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
@ApiStatus.Experimental /* Very Experimental and not cleanly separated from the impl atm */
|
||||
@@ -54,4 +55,8 @@ public interface MappingContext {
|
||||
Path workingDirectory(String name);
|
||||
|
||||
Logger getLogger();
|
||||
|
||||
DownloadBuilder download(String url);
|
||||
|
||||
boolean refreshDeps();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,6 +32,8 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
@@ -58,7 +60,11 @@ public abstract class AnnotationProcessorInvoker<T extends Task> {
|
||||
public static final String SCALA = "scala";
|
||||
public static final String GROOVY = "groovy";
|
||||
|
||||
private static final Pattern MSG_KEY_PATTERN = Pattern.compile("^[A-Z]+[A-Z_]+$");
|
||||
private static final Pattern MSG_VALUE_PATTERN = Pattern.compile("^(note|warning|error|disabled)$");
|
||||
|
||||
protected final Project project;
|
||||
protected final MixinExtension mixinExtension;
|
||||
protected final Map<SourceSet, T> invokerTasks;
|
||||
private final Collection<Configuration> apConfigurations;
|
||||
|
||||
@@ -66,6 +72,7 @@ public abstract class AnnotationProcessorInvoker<T extends Task> {
|
||||
Collection<Configuration> apConfigurations,
|
||||
Map<SourceSet, T> invokerTasks) {
|
||||
this.project = project;
|
||||
this.mixinExtension = LoomGradleExtension.get(project).getMixin();
|
||||
this.apConfigurations = apConfigurations;
|
||||
this.invokerTasks = invokerTasks;
|
||||
}
|
||||
@@ -96,6 +103,17 @@ public abstract class AnnotationProcessorInvoker<T extends Task> {
|
||||
put(Constants.MixinArguments.QUIET, "true");
|
||||
}};
|
||||
|
||||
if (mixinExtension.getShowMessageTypes().get()) {
|
||||
args.put(Constants.MixinArguments.SHOW_MESSAGE_TYPES, "true");
|
||||
}
|
||||
|
||||
mixinExtension.getMessages().get().forEach((key, value) -> {
|
||||
checkPattern(key, MSG_KEY_PATTERN);
|
||||
checkPattern(value, MSG_VALUE_PATTERN);
|
||||
|
||||
args.put("AMSG_" + key, value);
|
||||
});
|
||||
|
||||
project.getLogger().debug("Outputting refmap to dir: " + getRefmapDestinationDir(task) + " for compile task: " + task);
|
||||
args.forEach((k, v) -> passArgument(task, k, v));
|
||||
} catch (IOException e) {
|
||||
@@ -132,4 +150,12 @@ public abstract class AnnotationProcessorInvoker<T extends Task> {
|
||||
passMixinArguments(entry.getValue(), entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkPattern(String input, Pattern pattern) {
|
||||
final Matcher matcher = pattern.matcher(input);
|
||||
|
||||
if (!matcher.find()) {
|
||||
throw new IllegalArgumentException("Mixin argument (%s) does not match pattern (%s)".formatted(input, pattern.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ package net.fabricmc.loom.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -35,7 +38,6 @@ import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.AbstractCopyTask;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
@@ -181,7 +183,6 @@ public final class CompileConfiguration {
|
||||
}
|
||||
|
||||
public static void configureCompile(Project p) {
|
||||
final JavaPluginExtension javaPluginExtension = p.getExtensions().getByType(JavaPluginExtension.class);
|
||||
LoomGradleExtension extension = LoomGradleExtension.get(p);
|
||||
|
||||
p.getTasks().named(JavaPlugin.JAVADOC_TASK_NAME, Javadoc.class).configure(javadoc -> {
|
||||
@@ -192,6 +193,13 @@ public final class CompileConfiguration {
|
||||
p.afterEvaluate(project -> {
|
||||
MinecraftSourceSets.get(project).afterEvaluate(project);
|
||||
|
||||
final boolean previousRefreshDeps = extension.refreshDeps();
|
||||
|
||||
if (getAndLock(project)) {
|
||||
project.getLogger().lifecycle("Found existing cache lock file, rebuilding loom cache. This may have been caused by a failed or canceled build.");
|
||||
extension.setRefreshDeps(true);
|
||||
}
|
||||
|
||||
try {
|
||||
setupMinecraft(project);
|
||||
} catch (Exception e) {
|
||||
@@ -202,6 +210,9 @@ public final class CompileConfiguration {
|
||||
extension.setDependencyManager(dependencyManager);
|
||||
dependencyManager.handleDependencies(project);
|
||||
|
||||
releaseLock(project);
|
||||
extension.setRefreshDeps(previousRefreshDeps);
|
||||
|
||||
MixinExtension mixin = LoomGradleExtension.get(project).getMixin();
|
||||
|
||||
if (mixin.getUseLegacyMixinAp().get()) {
|
||||
@@ -396,6 +407,38 @@ public final class CompileConfiguration {
|
||||
.apply(project, extension.getNamedMinecraftProvider()).afterEvaluation();
|
||||
}
|
||||
|
||||
private static Path getLockFile(Project project) {
|
||||
final LoomGradleExtension extension = LoomGradleExtension.get(project);
|
||||
final Path cacheDirectory = extension.getFiles().getProjectPersistentCache().toPath();
|
||||
return cacheDirectory.resolve("configuration.lock");
|
||||
}
|
||||
|
||||
private static boolean getAndLock(Project project) {
|
||||
final Path lock = getLockFile(project);
|
||||
|
||||
if (Files.exists(lock)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createFile(lock);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to acquire project configuration lock", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void releaseLock(Project project) {
|
||||
final Path lock = getLockFile(project);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(lock);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to release project configuration lock", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void extendsFrom(List<String> parents, String b, Project project) {
|
||||
for (String parent : parents) {
|
||||
extendsFrom(parent, b, project);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -326,7 +327,7 @@ public class RunConfig {
|
||||
char c = s.charAt(i);
|
||||
|
||||
if (c == '@' && i > 0 && s.charAt(i - 1) == '@' || c == ' ') {
|
||||
ret.append(String.format("@@%04x", (int) c));
|
||||
ret.append(String.format(Locale.ENGLISH, "@@%04x", (int) c));
|
||||
} else {
|
||||
ret.append(c);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.hash.Hasher;
|
||||
@@ -180,19 +182,22 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource
|
||||
return result;
|
||||
}
|
||||
|
||||
// Find the injected interfaces from mods that are both on the compile and runtime classpath.
|
||||
// Runtime is also required to ensure that the interface and it's impl is present when running the mc jar.
|
||||
private List<InjectedInterface> getDependencyInjectedInterfaces() {
|
||||
List<InjectedInterface> result = new ArrayList<>();
|
||||
final Function<RemapConfigurationSettings, Stream<Path>> resolve = settings ->
|
||||
settings.getSourceConfiguration().get().resolve().stream()
|
||||
.map(File::toPath);
|
||||
|
||||
// Only apply injected interfaces from mods that are part of the compile classpath
|
||||
for (RemapConfigurationSettings entry : extension.getCompileRemapConfigurations()) {
|
||||
final Set<File> artifacts = entry.getSourceConfiguration().get().resolve();
|
||||
final List<Path> runtimeEntries = extension.getRuntimeRemapConfigurations().stream()
|
||||
.flatMap(resolve)
|
||||
.toList();
|
||||
|
||||
for (File artifact : artifacts) {
|
||||
result.addAll(InjectedInterface.fromModJar(artifact.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return extension.getCompileRemapConfigurations().stream()
|
||||
.flatMap(resolve)
|
||||
.filter(runtimeEntries::contains) // Use the intersection of the two configurations.
|
||||
.flatMap(path -> InjectedInterface.fromModJar(path).stream())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<InjectedInterface> getSourceInjectedInterface(SourceSet sourceSet) {
|
||||
|
||||
@@ -49,7 +49,6 @@ import org.gradle.language.base.artifact.SourcesArtifact;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
import net.fabricmc.loom.api.RemapConfigurationSettings;
|
||||
import net.fabricmc.loom.configuration.processors.dependency.ModDependencyInfo;
|
||||
import net.fabricmc.loom.configuration.processors.dependency.RemapData;
|
||||
@@ -67,7 +66,6 @@ public class ModConfigurationRemapper {
|
||||
|
||||
public static void supplyModConfigurations(Project project, String mappingsSuffix, LoomGradleExtension extension, SourceRemapper sourceRemapper) {
|
||||
final DependencyHandler dependencies = project.getDependencies();
|
||||
final boolean refreshDeps = LoomGradlePlugin.refreshDeps;
|
||||
|
||||
final File modStore = extension.getFiles().getRemappedModCache();
|
||||
final RemapData remapData = new RemapData(mappingsSuffix, modStore);
|
||||
@@ -99,7 +97,7 @@ public class ModConfigurationRemapper {
|
||||
|
||||
final ModDependencyInfo info = new ModDependencyInfo(artifact, remappedConfig, clientRemappedConfig, remapData);
|
||||
|
||||
if (refreshDeps) {
|
||||
if (extension.refreshDeps()) {
|
||||
info.forceRemap();
|
||||
}
|
||||
|
||||
@@ -198,7 +196,7 @@ public class ModConfigurationRemapper {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!output.exists() || input.lastModified() <= 0 || input.lastModified() > output.lastModified() || LoomGradlePlugin.refreshDeps) {
|
||||
if (!output.exists() || input.lastModified() <= 0 || input.lastModified() > output.lastModified() || LoomGradleExtension.get(project).refreshDeps()) {
|
||||
sourceRemapper.scheduleRemapSources(input, output, false, true); // Depenedency sources are used in ide only so don't need to be reproducable
|
||||
} else {
|
||||
project.getLogger().info(output.getName() + " is up to date with " + input.getName());
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
@@ -96,10 +97,10 @@ public class ModProcessor {
|
||||
}
|
||||
|
||||
try {
|
||||
project.getLogger().lifecycle(":remapping %d mods from %s".formatted(remapList.size(), sourceConfiguration.getName()));
|
||||
project.getLogger().lifecycle(":remapping {} mods from {}", remapList.size(), sourceConfiguration.getName());
|
||||
remapJars(remapList);
|
||||
} catch (Exception e) {
|
||||
project.getLogger().error("Failed to remap %d mods".formatted(remapList.size()), e);
|
||||
project.getLogger().error(String.format(Locale.ENGLISH, "Failed to remap %d mods", remapList.size()), e);
|
||||
|
||||
for (ModDependencyInfo info : remapList) {
|
||||
Files.deleteIfExists(info.getRemappedOutput().toPath());
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.gradle.api.logging.Logger;
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.api.mappings.layered.MappingContext;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
public class GradleMappingContext implements MappingContext {
|
||||
@@ -83,6 +84,16 @@ public class GradleMappingContext implements MappingContext {
|
||||
return project.getLogger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadBuilder download(String url) {
|
||||
return extension.download(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshDeps() {
|
||||
return extension.refreshDeps();
|
||||
}
|
||||
|
||||
public Project getProject() {
|
||||
return project;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
package net.fabricmc.loom.configuration.providers.mappings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -35,32 +34,35 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider;
|
||||
import net.fabricmc.loom.util.DownloadUtil;
|
||||
|
||||
public abstract class IntermediaryMappingsProvider extends IntermediateMappingsProvider {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(IntermediateMappingsProvider.class);
|
||||
|
||||
public abstract Property<String> getIntermediaryUrl();
|
||||
|
||||
public abstract Property<Boolean> getRefreshDeps();
|
||||
|
||||
@Override
|
||||
public void provide(Path tinyMappings) throws IOException {
|
||||
if (Files.exists(tinyMappings) && !LoomGradlePlugin.refreshDeps) {
|
||||
if (Files.exists(tinyMappings) && !getRefreshDeps().get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download and extract intermediary
|
||||
final Path intermediaryJarPath = Files.createTempFile(getName(), ".jar");
|
||||
final String encodedMcVersion = UrlEscapers.urlFragmentEscaper().escape(getMinecraftVersion().get());
|
||||
final URL url = new URL(getIntermediaryUrl().get().formatted(encodedMcVersion));
|
||||
final String url = getIntermediaryUrl().get().formatted(encodedMcVersion);
|
||||
|
||||
LOGGER.info("Downloading intermediary from {}", url);
|
||||
|
||||
Files.deleteIfExists(tinyMappings);
|
||||
Files.deleteIfExists(intermediaryJarPath);
|
||||
|
||||
DownloadUtil.downloadIfChanged(url, intermediaryJarPath.toFile(), LOGGER);
|
||||
getDownloader().get().apply(url)
|
||||
.defaultCache()
|
||||
.downloadPath(intermediaryJarPath);
|
||||
|
||||
MappingsProviderImpl.extractMappings(intermediaryJarPath, tinyMappings);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class LayeredMappingsDependency extends AbstractModuleDependency implemen
|
||||
Path mappingsDir = mappingContext.minecraftProvider().dir("layered").toPath();
|
||||
Path mappingsFile = mappingsDir.resolve(String.format("%s.%s-%s.tiny", GROUP, MODULE, getVersion()));
|
||||
|
||||
if (!Files.exists(mappingsFile) || LoomGradlePlugin.refreshDeps) {
|
||||
if (!Files.exists(mappingsFile) || mappingContext.refreshDeps()) {
|
||||
try {
|
||||
var processor = new LayeredMappingsProcessor(layeredMappingSpec);
|
||||
List<MappingLayer> layers = processor.resolveLayers(mappingContext);
|
||||
|
||||
@@ -191,11 +191,11 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService {
|
||||
}
|
||||
|
||||
protected void setup(Project project, MinecraftProvider minecraftProvider, Path inputJar) throws IOException {
|
||||
if (isRefreshDeps()) {
|
||||
if (minecraftProvider.refreshDeps()) {
|
||||
cleanWorkingDirectory(mappingsWorkingDir);
|
||||
}
|
||||
|
||||
if (Files.notExists(tinyMappings) || isRefreshDeps()) {
|
||||
if (Files.notExists(tinyMappings) || minecraftProvider.refreshDeps()) {
|
||||
storeMappings(project, minecraftProvider, inputJar);
|
||||
} else {
|
||||
try (FileSystem fileSystem = FileSystems.newFileSystem(inputJar, (ClassLoader) null)) {
|
||||
@@ -203,7 +203,7 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService {
|
||||
}
|
||||
}
|
||||
|
||||
if (Files.notExists(tinyMappingsJar) || isRefreshDeps()) {
|
||||
if (Files.notExists(tinyMappingsJar) || minecraftProvider.refreshDeps()) {
|
||||
Files.deleteIfExists(tinyMappingsJar);
|
||||
ZipUtils.add(tinyMappingsJar, "mappings/mappings.tiny", Files.readAllBytes(tinyMappings));
|
||||
}
|
||||
@@ -547,10 +547,6 @@ public class MappingsProviderImpl implements MappingsProvider, SharedService {
|
||||
public record UnpickMetadata(String unpickGroup, String unpickVersion) {
|
||||
}
|
||||
|
||||
protected static boolean isRefreshDeps() {
|
||||
return LoomGradlePlugin.refreshDeps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mappingTree = null;
|
||||
|
||||
@@ -26,7 +26,6 @@ package net.fabricmc.loom.configuration.providers.mappings.mojmap;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -46,19 +45,14 @@ import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch;
|
||||
import net.fabricmc.mappingio.format.ProGuardReader;
|
||||
|
||||
public record MojangMappingLayer(String minecraftVersion,
|
||||
MinecraftVersionMeta.Download clientDownload,
|
||||
MinecraftVersionMeta.Download serverDownload,
|
||||
Path workingDir, boolean nameSyntheticMembers,
|
||||
Path clientMappings,
|
||||
Path serverMappings,
|
||||
boolean nameSyntheticMembers,
|
||||
Logger logger,
|
||||
MojangMappingsSpec.SilenceLicenseOption silenceLicense) implements MappingLayer {
|
||||
private static final Pattern SYNTHETIC_NAME_PATTERN = Pattern.compile("^(access|this|val\\$this|lambda\\$.*)\\$[0-9]+$");
|
||||
@Override
|
||||
public void visit(MappingVisitor mappingVisitor) throws IOException {
|
||||
Path clientMappings = workingDir().resolve("%s.client.txt".formatted(minecraftVersion));
|
||||
Path serverMappings = workingDir().resolve("%s.server.txt".formatted(minecraftVersion));
|
||||
|
||||
download(clientMappings, serverMappings);
|
||||
|
||||
if (!silenceLicense.isSilent()) {
|
||||
printMappingsLicense(clientMappings);
|
||||
}
|
||||
@@ -76,11 +70,6 @@ public record MojangMappingLayer(String minecraftVersion,
|
||||
}
|
||||
}
|
||||
|
||||
private void download(Path clientMappings, Path serverMappings) throws IOException {
|
||||
HashedDownloadUtil.downloadIfInvalid(new URL(clientDownload().url()), clientMappings.toFile(), clientDownload().sha1(), logger(), false);
|
||||
HashedDownloadUtil.downloadIfInvalid(new URL(serverDownload().url()), serverMappings.toFile(), serverDownload().sha1(), logger(), false);
|
||||
}
|
||||
|
||||
private void printMappingsLicense(Path clientMappings) {
|
||||
try (BufferedReader clientBufferedReader = Files.newBufferedReader(clientMappings, StandardCharsets.UTF_8)) {
|
||||
logger().warn("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
||||
|
||||
@@ -24,9 +24,13 @@
|
||||
|
||||
package net.fabricmc.loom.configuration.providers.mappings.mojmap;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import net.fabricmc.loom.api.mappings.layered.MappingContext;
|
||||
import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
|
||||
import net.fabricmc.loom.util.download.DownloadException;
|
||||
|
||||
public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean nameSyntheticMembers) implements MappingsSpec<MojangMappingLayer> {
|
||||
// Keys in dependency manifest
|
||||
@@ -71,17 +75,33 @@ public record MojangMappingsSpec(SilenceLicenseOption silenceLicense, boolean na
|
||||
|
||||
@Override
|
||||
public MojangMappingLayer createLayer(MappingContext context) {
|
||||
MinecraftVersionMeta versionInfo = context.minecraftProvider().getVersionInfo();
|
||||
final MinecraftVersionMeta versionInfo = context.minecraftProvider().getVersionInfo();
|
||||
final MinecraftVersionMeta.Download clientDownload = versionInfo.download(MANIFEST_CLIENT_MAPPINGS);
|
||||
final MinecraftVersionMeta.Download serverDownload = versionInfo.download(MANIFEST_SERVER_MAPPINGS);
|
||||
|
||||
if (versionInfo.download(MANIFEST_CLIENT_MAPPINGS) == null) {
|
||||
if (clientDownload == null) {
|
||||
throw new RuntimeException("Failed to find official mojang mappings for " + context.minecraftVersion());
|
||||
}
|
||||
|
||||
final Path clientMappings = context.workingDirectory("mojang").resolve("client.txt");
|
||||
final Path serverMappings = context.workingDirectory("mojang").resolve("server.txt");
|
||||
|
||||
try {
|
||||
context.download(clientDownload.url())
|
||||
.sha1(clientDownload.sha1())
|
||||
.downloadPath(clientMappings);
|
||||
|
||||
context.download(serverDownload.url())
|
||||
.sha1(serverDownload.sha1())
|
||||
.downloadPath(serverMappings);
|
||||
} catch (DownloadException e) {
|
||||
throw new UncheckedIOException("Failed to download mappings", e);
|
||||
}
|
||||
|
||||
return new MojangMappingLayer(
|
||||
context.minecraftVersion(),
|
||||
versionInfo.download(MANIFEST_CLIENT_MAPPINGS),
|
||||
versionInfo.download(MANIFEST_SERVER_MAPPINGS),
|
||||
context.workingDirectory("mojang"),
|
||||
clientMappings,
|
||||
serverMappings,
|
||||
nameSyntheticMembers(),
|
||||
context.getLogger(),
|
||||
silenceLicense()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import java.util.Objects;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import net.fabricmc.loom.util.HashedDownloadUtil;
|
||||
import net.fabricmc.stitch.merge.JarMerger;
|
||||
|
||||
public class MergedMinecraftProvider extends MinecraftProvider {
|
||||
@@ -62,12 +61,12 @@ public class MergedMinecraftProvider extends MinecraftProvider {
|
||||
throw new UnsupportedOperationException("Minecraft versions 1.2.5 and older cannot be merged. Please use `loom { server/clientOnlyMinecraftJar() }`");
|
||||
}
|
||||
|
||||
if (!Files.exists(minecraftMergedJar) || isRefreshDeps()) {
|
||||
if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) {
|
||||
try {
|
||||
mergeJars();
|
||||
} catch (Throwable e) {
|
||||
HashedDownloadUtil.delete(getMinecraftClientJar());
|
||||
HashedDownloadUtil.delete(getMinecraftServerJar());
|
||||
Files.deleteIfExists(getMinecraftClientJar().toPath());
|
||||
Files.deleteIfExists(getMinecraftServerJar().toPath());
|
||||
Files.deleteIfExists(minecraftMergedJar);
|
||||
|
||||
getProject().getLogger().error("Could not merge JARs! Deleting source JARs - please re-run the command and move on.", e);
|
||||
|
||||
@@ -27,16 +27,11 @@ package net.fabricmc.loom.configuration.providers.minecraft;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.Files;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -47,9 +42,11 @@ import net.fabricmc.loom.configuration.CompileConfiguration;
|
||||
import net.fabricmc.loom.configuration.DependencyInfo;
|
||||
import net.fabricmc.loom.configuration.providers.BundleMetadata;
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
import net.fabricmc.loom.util.DownloadUtil;
|
||||
import net.fabricmc.loom.util.HashedDownloadUtil;
|
||||
import net.fabricmc.loom.util.MirrorUtil;
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder;
|
||||
import net.fabricmc.loom.util.download.DownloadExecutor;
|
||||
import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
|
||||
import net.fabricmc.loom.util.gradle.ProgressGroup;
|
||||
|
||||
public abstract class MinecraftProvider {
|
||||
private String minecraftVersion;
|
||||
@@ -92,35 +89,15 @@ public abstract class MinecraftProvider {
|
||||
getProject().getDependencies().add(Constants.Configurations.SRG, "de.oceanlabs.mcp:mcp_config:" + minecraftVersion);
|
||||
}
|
||||
|
||||
boolean offline = getProject().getGradle().getStartParameter().isOffline();
|
||||
|
||||
initFiles();
|
||||
|
||||
downloadMcJson(offline);
|
||||
downloadMcJson();
|
||||
|
||||
try (FileReader reader = new FileReader(minecraftJson)) {
|
||||
versionInfo = LoomGradlePlugin.OBJECT_MAPPER.readValue(reader, MinecraftVersionMeta.class);
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
boolean exists = true;
|
||||
|
||||
if (provideServer() && !minecraftServerJar.exists()) {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
if (provideClient() && !minecraftClientJar.exists()) {
|
||||
exists = false;
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
getProject().getLogger().debug("Found client and server jars, presuming up-to-date");
|
||||
} else {
|
||||
throw new GradleException("Missing jar(s); Client: " + minecraftClientJar.exists() + ", Server: " + minecraftServerJar.exists());
|
||||
}
|
||||
} else {
|
||||
downloadJars(getProject().getLogger());
|
||||
}
|
||||
downloadJars();
|
||||
|
||||
if (provideServer()) {
|
||||
serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath());
|
||||
@@ -155,131 +132,86 @@ public abstract class MinecraftProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadMcJson(boolean offline) throws IOException {
|
||||
if (getExtension().getShareRemapCaches().get() && !getExtension().isRootProject() && versionManifestJson.exists() && !isRefreshDeps()) {
|
||||
return;
|
||||
}
|
||||
private void downloadMcJson() throws IOException {
|
||||
final String versionManifestUrl = MirrorUtil.getVersionManifests(getProject());
|
||||
final String versionManifest = getExtension().download(versionManifestUrl)
|
||||
.defaultCache()
|
||||
.downloadString(versionManifestJson.toPath());
|
||||
|
||||
if (!offline && !isRefreshDeps() && hasRecentValidManifest()) {
|
||||
// We have a recent valid manifest file, so do nothing
|
||||
} else if (offline) {
|
||||
if (versionManifestJson.exists()) {
|
||||
// If there is the manifests already we'll presume that's good enough
|
||||
getProject().getLogger().debug("Found version manifests, presuming up-to-date");
|
||||
} else {
|
||||
// If we don't have the manifests then there's nothing more we can do
|
||||
throw new GradleException("Version manifests not found at " + versionManifestJson.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
getProject().getLogger().debug("Downloading version manifests");
|
||||
DownloadUtil.downloadIfChanged(new URL(MirrorUtil.getVersionManifests(getProject())), versionManifestJson, getProject().getLogger());
|
||||
}
|
||||
|
||||
String versionManifest = Files.asCharSource(versionManifestJson, StandardCharsets.UTF_8).read();
|
||||
ManifestVersion mcManifest = LoomGradlePlugin.OBJECT_MAPPER.readValue(versionManifest, ManifestVersion.class);
|
||||
|
||||
Optional<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +299,7 @@ public abstract class MinecraftProvider {
|
||||
return LoomGradleExtension.get(getProject());
|
||||
}
|
||||
|
||||
protected boolean isRefreshDeps() {
|
||||
return LoomGradlePlugin.refreshDeps;
|
||||
public boolean refreshDeps() {
|
||||
return getExtension().refreshDeps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class SingleJarMinecraftProvider extends MinecraftProvider {
|
||||
}
|
||||
|
||||
protected void processJar() throws Exception {
|
||||
boolean requiresRefresh = isRefreshDeps() || Files.notExists(minecraftEnvOnlyJar);
|
||||
boolean requiresRefresh = getExtension().refreshDeps() || Files.notExists(minecraftEnvOnlyJar);
|
||||
|
||||
if (!requiresRefresh) {
|
||||
return;
|
||||
|
||||
@@ -57,7 +57,7 @@ public final class SplitMinecraftProvider extends MinecraftProvider {
|
||||
public void provide() throws Exception {
|
||||
super.provide();
|
||||
|
||||
boolean requiresRefresh = isRefreshDeps() || Files.notExists(minecraftClientOnlyJar) || Files.notExists(minecraftCommonJar);
|
||||
boolean requiresRefresh = getExtension().refreshDeps() || Files.notExists(minecraftClientOnlyJar) || Files.notExists(minecraftCommonJar);
|
||||
|
||||
if (!requiresRefresh) {
|
||||
return;
|
||||
|
||||
@@ -37,7 +37,6 @@ import dev.architectury.tinyremapper.TinyRemapper;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
|
||||
@@ -71,7 +70,7 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
|
||||
final List<RemappedJars> remappedJars = getRemappedJars();
|
||||
assert !remappedJars.isEmpty();
|
||||
|
||||
if (!areOutputsValid(remappedJars) || LoomGradlePlugin.refreshDeps) {
|
||||
if (!areOutputsValid(remappedJars) || extension.refreshDeps()) {
|
||||
try {
|
||||
remapInputs(remappedJars);
|
||||
} catch (Throwable t) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
import net.fabricmc.loom.configuration.processors.JarProcessorManager;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
|
||||
@@ -64,7 +63,7 @@ public abstract class ProcessedNamedMinecraftProvider<M extends MinecraftProvide
|
||||
parentMinecraftProvider.provide(false);
|
||||
|
||||
final List<Path> inputJars = parentMinecraftProvider.getMinecraftJars();
|
||||
boolean requiresProcessing = LoomGradlePlugin.refreshDeps || inputJars.stream()
|
||||
boolean requiresProcessing = extension.refreshDeps() || inputJars.stream()
|
||||
.map(this::getProcessedPath)
|
||||
.map(Path::toFile)
|
||||
.anyMatch(jarProcessorManager::isInvalid);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
|
||||
protected final ListProperty<JarProcessor> jarProcessors;
|
||||
protected final ConfigurableFileCollection log4jConfigs;
|
||||
protected final RegularFileProperty accessWidener;
|
||||
protected final Property<Boolean> shareCaches;
|
||||
protected final Property<String> customManifest;
|
||||
protected final Property<Boolean> transitiveAccessWideners;
|
||||
protected final Property<Boolean> modProvidedJavadoc;
|
||||
@@ -117,8 +116,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
|
||||
.empty();
|
||||
this.log4jConfigs = project.files(directories.getDefaultLog4jConfigFile());
|
||||
this.accessWidener = project.getObjects().fileProperty();
|
||||
this.shareCaches = project.getObjects().property(Boolean.class)
|
||||
.convention(false);
|
||||
this.customManifest = project.getObjects().property(String.class);
|
||||
this.transitiveAccessWideners = project.getObjects().property(Boolean.class)
|
||||
.convention(true);
|
||||
@@ -200,11 +197,6 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
|
||||
return accessWidener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Property<Boolean> getShareRemapCaches() {
|
||||
return shareCaches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NamedDomainObjectContainer<DecompilerOptions> getDecompilerOptions() {
|
||||
return decompilers;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package net.fabricmc.loom.extension;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -53,6 +54,8 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMi
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.mapped.SrgMinecraftProvider;
|
||||
import net.fabricmc.loom.util.ModPlatform;
|
||||
import net.fabricmc.loom.util.download.Download;
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder;
|
||||
|
||||
public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implements LoomGradleExtension {
|
||||
private final Project project;
|
||||
@@ -73,6 +76,7 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
|
||||
private IntermediaryMinecraftProvider<?> intermediaryMinecraftProvider;
|
||||
private SrgMinecraftProvider<?> srgMinecraftProvider;
|
||||
private InstallerData installerData;
|
||||
private boolean refreshDeps;
|
||||
|
||||
// +-------------------+
|
||||
// | Architectury Loom |
|
||||
@@ -93,7 +97,15 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
|
||||
provider.getIntermediaryUrl()
|
||||
.convention(getIntermediaryUrl())
|
||||
.finalizeValueOnRead();
|
||||
|
||||
provider.getRefreshDeps().set(project.provider(() -> LoomGradleExtension.get(project).refreshDeps()));
|
||||
});
|
||||
|
||||
refreshDeps = project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh");
|
||||
|
||||
if (refreshDeps) {
|
||||
project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -231,10 +243,44 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
|
||||
transitiveAccessWideners.addAll(accessWidenerFiles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DownloadBuilder download(String url) {
|
||||
DownloadBuilder builder;
|
||||
|
||||
try {
|
||||
builder = Download.create(url);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException("Failed to create downloader for: " + e);
|
||||
}
|
||||
|
||||
if (project.getGradle().getStartParameter().isOffline()) {
|
||||
builder.offline();
|
||||
}
|
||||
|
||||
if (project.getGradle().getStartParameter().isRefreshDependencies() || Boolean.getBoolean("loom.refresh")) {
|
||||
builder.forceDownload();
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean refreshDeps() {
|
||||
return refreshDeps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRefreshDeps(boolean refreshDeps) {
|
||||
this.refreshDeps = refreshDeps;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <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();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.gradle.api.Action;
|
||||
import org.gradle.api.InvalidUserDataException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
@@ -42,6 +43,8 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI {
|
||||
protected final Project project;
|
||||
protected final Property<Boolean> useMixinAp;
|
||||
private final Property<String> refmapTargetNamespace;
|
||||
private final MapProperty<String, String> messages;
|
||||
private final Property<Boolean> showMessageTypes;
|
||||
|
||||
public MixinExtensionApiImpl(Project project) {
|
||||
this.project = Objects.requireNonNull(project);
|
||||
@@ -52,6 +55,12 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI {
|
||||
this.refmapTargetNamespace = project.getObjects().property(String.class)
|
||||
.convention(project.provider(() -> IntermediaryNamespaces.intermediary(project)));
|
||||
this.refmapTargetNamespace.finalizeValueOnRead();
|
||||
|
||||
this.messages = project.getObjects().mapProperty(String.class, String.class);
|
||||
this.messages.finalizeValueOnRead();
|
||||
|
||||
this.showMessageTypes = project.getObjects().property(Boolean.class);
|
||||
this.showMessageTypes.convention(false).finalizeValueOnRead();
|
||||
}
|
||||
|
||||
protected final PatternSet add0(SourceSet sourceSet, String refmapName) {
|
||||
@@ -122,6 +131,21 @@ public abstract class MixinExtensionApiImpl implements MixinExtensionAPI {
|
||||
add(sourceSetName, x -> { });
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapProperty<String, String> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Property<Boolean> getShowMessageTypes() {
|
||||
return showMessageTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messages(Action<MapProperty<String, String>> action) {
|
||||
action.execute(messages);
|
||||
}
|
||||
|
||||
private SourceSet resolveSourceSet(String sourceSetName) {
|
||||
// try to find sourceSet with name sourceSetName in this project
|
||||
SourceSet sourceSet = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().findByName(sourceSetName);
|
||||
|
||||
@@ -25,20 +25,30 @@
|
||||
package net.fabricmc.loom.task;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.specs.Spec;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import net.fabricmc.loom.configuration.ide.RunConfig;
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
|
||||
public abstract class AbstractRunTask extends JavaExec {
|
||||
private final RunConfig config;
|
||||
// We control the classpath, as we use a ArgFile to pass it over the command line: https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile
|
||||
private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection();
|
||||
|
||||
public AbstractRunTask(Function<Project, RunConfig> configProvider) {
|
||||
super();
|
||||
@@ -46,6 +56,9 @@ public abstract class AbstractRunTask extends JavaExec {
|
||||
this.config = configProvider.apply(getProject());
|
||||
|
||||
setClasspath(config.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter()));
|
||||
// Pass an empty classpath to the super JavaExec.
|
||||
super.setClasspath(getProject().files());
|
||||
|
||||
args(config.programArgs);
|
||||
getMainClass().set(config.mainClass);
|
||||
}
|
||||
@@ -69,12 +82,46 @@ public abstract class AbstractRunTask extends JavaExec {
|
||||
|
||||
@Override
|
||||
public List<String> getJvmArgs() {
|
||||
List<String> superArgs = super.getJvmArgs();
|
||||
List<String> args = new ArrayList<>(superArgs != null ? superArgs : Collections.emptyList());
|
||||
final List<String> superArgs = super.getJvmArgs();
|
||||
final List<String> args = new ArrayList<>();
|
||||
|
||||
final String content = "-classpath\n" + this.classpath.getFiles().stream()
|
||||
.map(File::getAbsolutePath)
|
||||
.collect(Collectors.joining(System.getProperty("path.separator")));
|
||||
|
||||
try {
|
||||
final Path argsFile = Files.createTempFile("loom-classpath", ".args");
|
||||
Files.writeString(argsFile, content, StandardCharsets.UTF_8);
|
||||
args.add("@" + argsFile.toAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to create classpath file", e);
|
||||
}
|
||||
|
||||
if (superArgs != null) {
|
||||
args.addAll(superArgs);
|
||||
}
|
||||
|
||||
args.addAll(config.vmArgs);
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JavaExec setClasspath(@NotNull FileCollection classpath) {
|
||||
this.classpath.setFrom(classpath);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JavaExec classpath(Object @NotNull... paths) {
|
||||
this.classpath.from(paths);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FileCollection getClasspath() {
|
||||
return this.classpath;
|
||||
}
|
||||
|
||||
private class LibraryFilter implements Spec<File> {
|
||||
private List<String> excludedLibraryPaths = null;
|
||||
|
||||
|
||||
@@ -25,22 +25,12 @@
|
||||
package net.fabricmc.loom.task;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.util.Deque;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
@@ -53,9 +43,10 @@ import net.fabricmc.loom.configuration.ide.RunConfigSettings;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.assets.AssetIndex;
|
||||
import net.fabricmc.loom.util.HashedDownloadUtil;
|
||||
import net.fabricmc.loom.util.MirrorUtil;
|
||||
import net.fabricmc.loom.util.gradle.ProgressLoggerHelper;
|
||||
import net.fabricmc.loom.util.download.DownloadExecutor;
|
||||
import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
|
||||
import net.fabricmc.loom.util.gradle.ProgressGroup;
|
||||
|
||||
// TODO: Reintroduce the progress bar.
|
||||
public abstract class DownloadAssetsTask extends AbstractLoomTask {
|
||||
@@ -65,6 +56,9 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
|
||||
@Input
|
||||
public abstract Property<String> getMinecraftVersion();
|
||||
|
||||
@Input
|
||||
public abstract Property<Integer> getDownloadThreads();
|
||||
|
||||
@OutputDirectory
|
||||
public abstract RegularFileProperty getAssetsDirectory();
|
||||
|
||||
@@ -80,6 +74,7 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
|
||||
getAssetsHash().set(versionInfo.assetIndex().sha1());
|
||||
getMinecraftVersion().set(versionInfo.id());
|
||||
getMinecraftVersion().finalizeValue();
|
||||
getDownloadThreads().convention(Runtime.getRuntime().availableProcessors());
|
||||
|
||||
if (versionInfo.assets().equals("legacy")) {
|
||||
getLegacyResourcesDirectory().set(new File(assetsDir, "/legacy/" + versionInfo.id()));
|
||||
@@ -89,80 +84,28 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
|
||||
getLegacyResourcesDirectory().set(new File(getProject().getProjectDir(), client.getRunDir() + "/resources"));
|
||||
}
|
||||
|
||||
getAssetsHash().finalizeValueOnRead();
|
||||
getAssetsHash().finalizeValue();
|
||||
getAssetsDirectory().finalizeValueOnRead();
|
||||
getLegacyResourcesDirectory().finalizeValueOnRead();
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void downloadAssets() throws IOException {
|
||||
final Project project = this.getProject();
|
||||
final File assetsDirectory = getAssetsDirectory().get().getAsFile();
|
||||
final Deque<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))
|
||||
.maxRetries(3)
|
||||
.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, false, () -> {
|
||||
project.getLogger().debug("downloading asset " + object.name());
|
||||
logger.progress(String.format("%-30.30s", object.name()) + " - " + sha1);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to download: " + object.name(), e);
|
||||
}
|
||||
|
||||
// Give this logger back
|
||||
loggers.add(logger);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for the assets to all download
|
||||
try {
|
||||
executor.shutdown();
|
||||
|
||||
if (executor.awaitTermination(2, TimeUnit.HOURS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
loggers.forEach(ProgressLoggerHelper::completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,35 +117,22 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
|
||||
private AssetIndex getAssetIndex() throws IOException {
|
||||
final LoomGradleExtension extension = getExtension();
|
||||
final MinecraftProvider minecraftProvider = extension.getMinecraftProvider();
|
||||
final MinecraftVersionMeta.AssetIndex assetIndex = getAssetIndexMeta();
|
||||
final File indexFile = new File(getAssetsDirectory().get().getAsFile(), "indexes" + File.separator + assetIndex.fabricId(minecraftProvider.minecraftVersion()) + ".json");
|
||||
|
||||
MinecraftVersionMeta.AssetIndex assetIndex = getAssetIndexMeta();
|
||||
File assetsInfo = new File(getAssetsDirectory().get().getAsFile(), "indexes" + File.separator + assetIndex.fabricId(minecraftProvider.minecraftVersion()) + ".json");
|
||||
final String json = extension.download(assetIndex.url())
|
||||
.sha1(assetIndex.sha1())
|
||||
.downloadString(indexFile.toPath());
|
||||
|
||||
getProject().getLogger().info(":downloading asset index");
|
||||
|
||||
if (getProject().getGradle().getStartParameter().isOffline()) {
|
||||
if (assetsInfo.exists()) {
|
||||
// We know it's outdated but can't do anything about it, oh well
|
||||
getProject().getLogger().warn("Asset index outdated");
|
||||
} else {
|
||||
// We don't know what assets we need, just that we don't have any
|
||||
throw new GradleException("Asset index not found at " + assetsInfo.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
HashedDownloadUtil.downloadIfInvalid(new URL(assetIndex.url()), assetsInfo, assetIndex.sha1(), getProject().getLogger(), false);
|
||||
}
|
||||
|
||||
try (FileReader fileReader = new FileReader(assetsInfo)) {
|
||||
return LoomGradlePlugin.OBJECT_MAPPER.readValue(fileReader, AssetIndex.class);
|
||||
}
|
||||
return LoomGradlePlugin.OBJECT_MAPPER.readValue(json, AssetIndex.class);
|
||||
}
|
||||
|
||||
private File getAssetsFile(AssetIndex.Object object, AssetIndex index) {
|
||||
private Path getAssetsPath(AssetIndex.Object object, AssetIndex index) {
|
||||
if (index.mapToResources() || index.virtual()) {
|
||||
return new File(getLegacyResourcesDirectory().get().getAsFile(), object.path());
|
||||
return new File(getLegacyResourcesDirectory().get().getAsFile(), object.path()).toPath();
|
||||
}
|
||||
|
||||
final String filename = "objects" + File.separator + object.hash().substring(0, 2) + File.separator + object.hash();
|
||||
return new File(getAssetsDirectory().get().getAsFile(), filename);
|
||||
return new File(getAssetsDirectory().get().getAsFile(), filename).toPath();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
|
||||
return getWorkerExecutor().processIsolation(spec -> {
|
||||
spec.forkOptions(forkOptions -> {
|
||||
forkOptions.setMaxHeapSize("%dm".formatted(decompilerOptions.getMemory().get()));
|
||||
forkOptions.setMaxHeapSize(String.format(Locale.ENGLISH, "%dm", decompilerOptions.getMemory().get()));
|
||||
forkOptions.systemProperty(WorkerDaemonClientsManagerHelper.MARKER_PROP, jvmMarkerValue);
|
||||
});
|
||||
spec.getClasspath().from(getClasspath());
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.gradle.api.tasks.TaskProvider;
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.configuration.ide.RunConfigSettings;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
|
||||
import net.fabricmc.loom.task.launch.GenerateDLIConfigTask;
|
||||
import net.fabricmc.loom.task.launch.GenerateLog4jConfigTask;
|
||||
import net.fabricmc.loom.task.launch.GenerateRemapClasspathTask;
|
||||
@@ -95,7 +96,14 @@ public final class LoomTasks {
|
||||
return;
|
||||
}
|
||||
|
||||
registerClientSetupTasks(project.getTasks(), extension.getMinecraftProvider().getVersionInfo().hasNativesToExtract());
|
||||
final MinecraftVersionMeta versionInfo = extension.getMinecraftProvider().getVersionInfo();
|
||||
|
||||
if (versionInfo == null) {
|
||||
// Something has gone wrong, don't register the task.
|
||||
return;
|
||||
}
|
||||
|
||||
registerClientSetupTasks(project.getTasks(), versionInfo.hasNativesToExtract());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,13 @@ import org.gradle.api.IllegalDependencyNotation;
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Dependency;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.options.Option;
|
||||
import org.gradle.work.DisableCachingByDefault;
|
||||
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
|
||||
@@ -55,7 +59,8 @@ import net.fabricmc.lorenztiny.TinyMappingsJoiner;
|
||||
import net.fabricmc.mappingio.MappingReader;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
public class MigrateMappingsTask extends AbstractLoomTask {
|
||||
@DisableCachingByDefault(because = "Always rerun this task.")
|
||||
public abstract class MigrateMappingsTask extends AbstractLoomTask {
|
||||
private Path inputDir;
|
||||
private Path outputDir;
|
||||
private String mappings;
|
||||
@@ -63,6 +68,9 @@ public class MigrateMappingsTask extends AbstractLoomTask {
|
||||
public MigrateMappingsTask() {
|
||||
inputDir = getProject().file("src/main/java").toPath();
|
||||
outputDir = getProject().file("remappedSrc").toPath();
|
||||
|
||||
// Ensure we resolve the classpath inputs before running the task.
|
||||
getCompileClasspath().from(getProject().getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME));
|
||||
}
|
||||
|
||||
@Option(option = "input", description = "Java source file directory")
|
||||
@@ -80,6 +88,9 @@ public class MigrateMappingsTask extends AbstractLoomTask {
|
||||
this.mappings = mappings;
|
||||
}
|
||||
|
||||
@InputFiles
|
||||
public abstract ConfigurableFileCollection getCompileClasspath();
|
||||
|
||||
@TaskAction
|
||||
public void doTask() throws Throwable {
|
||||
Project project = getProject();
|
||||
|
||||
@@ -94,7 +94,7 @@ public abstract class ValidateAccessWidenerTask extends DefaultTask {
|
||||
/**
|
||||
* Validates that all entries in an access-widner file relate to a class/method/field in the mc jar.
|
||||
*/
|
||||
private static record AccessWidenerValidator(TrEnvironment environment) implements AccessWidenerVisitor {
|
||||
private record AccessWidenerValidator(TrEnvironment environment) implements AccessWidenerVisitor {
|
||||
@Override
|
||||
public void visitClass(String name, AccessWidenerReader.AccessType access, boolean transitive) {
|
||||
if (environment().getClass(name) == null) {
|
||||
|
||||
250
src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java
Normal file
250
src/main/java/net/fabricmc/loom/task/ValidateMixinNameTask.java
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2022 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.task;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.SourceTask;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.workers.WorkAction;
|
||||
import org.gradle.workers.WorkParameters;
|
||||
import org.gradle.workers.WorkQueue;
|
||||
import org.gradle.workers.WorkerExecutor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.objectweb.asm.AnnotationVisitor;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
import net.fabricmc.tinyremapper.extension.mixin.common.data.Constant;
|
||||
|
||||
/**
|
||||
* Task to validate mixin names.
|
||||
*
|
||||
* <pre>{@code
|
||||
* task validateMixinNames(type: net.fabricmc.loom.task.ValidateMixinNameTask) {
|
||||
* source(sourceSets.main.output)
|
||||
* softFailures = false
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public abstract class ValidateMixinNameTask extends SourceTask {
|
||||
@Input
|
||||
abstract Property<Boolean> getSoftFailures();
|
||||
|
||||
@Inject
|
||||
protected abstract WorkerExecutor getWorkerExecutor();
|
||||
|
||||
@Inject
|
||||
public ValidateMixinNameTask() {
|
||||
setGroup("verification");
|
||||
getProject().getTasks().getByName("check").dependsOn(this);
|
||||
getSoftFailures().convention(false);
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void run() {
|
||||
final WorkQueue workQueue = getWorkerExecutor().noIsolation();
|
||||
|
||||
workQueue.submit(ValidateMixinAction.class, params -> {
|
||||
params.getInputClasses().from(getSource().matching(pattern -> pattern.include("**/*.class")));
|
||||
params.getSoftFailures().set(getSoftFailures());
|
||||
});
|
||||
}
|
||||
|
||||
public interface ValidateMixinsParams extends WorkParameters {
|
||||
ConfigurableFileCollection getInputClasses();
|
||||
Property<Boolean> getSoftFailures();
|
||||
}
|
||||
|
||||
public abstract static class ValidateMixinAction implements WorkAction<ValidateMixinsParams> {
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(ValidateMixinAction.class);
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
final Set<File> files = getParameters().getInputClasses().getAsFileTree().getFiles();
|
||||
final List<String> errors = new LinkedList<>();
|
||||
|
||||
for (File file : files) {
|
||||
final Mixin mixin = getMixin(file);
|
||||
|
||||
if (mixin == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String mixinClassName = toSimpleName(mixin.className);
|
||||
final String expectedMixinClassName = toSimpleName(mixin.target.getInternalName()).replace("$", "") + (mixin.accessor ? "Accessor" : "Mixin");
|
||||
|
||||
if (expectedMixinClassName.startsWith("class_")) {
|
||||
// Don't enforce intermediary named mixins.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!expectedMixinClassName.equals(mixinClassName)) {
|
||||
errors.add("%s -> %s".formatted(mixin.className, expectedMixinClassName));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String message = "Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator()));
|
||||
|
||||
if (getParameters().getSoftFailures().get()) {
|
||||
LOGGER.warn(message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new GradleException("Mixin name validation failed: " + errors.stream().collect(Collectors.joining(System.lineSeparator())));
|
||||
}
|
||||
|
||||
private static String toSimpleName(String internalName) {
|
||||
return internalName.substring(internalName.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Mixin getMixin(File file) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
ClassReader reader = new ClassReader(is);
|
||||
|
||||
var classVisitor = new MixinTargetClassVisitor();
|
||||
reader.accept(classVisitor, ClassReader.SKIP_CODE);
|
||||
|
||||
if (classVisitor.mixinTarget != null) {
|
||||
return new Mixin(classVisitor.className, classVisitor.mixinTarget, classVisitor.accessor);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to read input file: " + file, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record Mixin(String className, Type target, boolean accessor) { }
|
||||
|
||||
private static class MixinTargetClassVisitor extends ClassVisitor {
|
||||
Type mixinTarget;
|
||||
String className;
|
||||
boolean accessor;
|
||||
|
||||
boolean isInterface;
|
||||
|
||||
protected MixinTargetClassVisitor() {
|
||||
super(Constants.ASM_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||
this.className = name;
|
||||
this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
|
||||
super.visit(version, access, name, signature, superName, interfaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
|
||||
AnnotationVisitor av = super.visitAnnotation(descriptor, visible);
|
||||
|
||||
if ("Lorg/spongepowered/asm/mixin/Mixin;".equals(descriptor)) {
|
||||
av = new MixinAnnotationVisitor(av);
|
||||
}
|
||||
|
||||
return av;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
|
||||
|
||||
if (mixinTarget != null) {
|
||||
mv = new MixinMethodVisitor(mv);
|
||||
}
|
||||
|
||||
return mv;
|
||||
}
|
||||
|
||||
private class MixinAnnotationVisitor extends AnnotationVisitor {
|
||||
MixinAnnotationVisitor(AnnotationVisitor annotationVisitor) {
|
||||
super(Constants.ASM_VERSION, annotationVisitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitArray(String name) {
|
||||
final AnnotationVisitor av = super.visitArray(name);
|
||||
|
||||
if ("value".equals(name)) {
|
||||
return new AnnotationVisitor(Constant.ASM_VERSION, av) {
|
||||
@Override
|
||||
public void visit(String name, Object value) {
|
||||
mixinTarget = Objects.requireNonNull((Type) value);
|
||||
|
||||
super.visit(name, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return av;
|
||||
}
|
||||
}
|
||||
|
||||
private class MixinMethodVisitor extends MethodVisitor {
|
||||
protected MixinMethodVisitor(MethodVisitor methodVisitor) {
|
||||
super(Constants.ASM_VERSION, methodVisitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
|
||||
if ("Lorg/spongepowered/asm/mixin/gen/Accessor;".equals(descriptor)) {
|
||||
accessor = true;
|
||||
} else if ("Lorg/spongepowered/asm/mixin/gen/Invoker;".equals(descriptor)) {
|
||||
accessor = true;
|
||||
}
|
||||
|
||||
return super.visitAnnotation(descriptor, visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.hash.Hashing;
|
||||
@@ -65,6 +66,11 @@ public class Checksum {
|
||||
}
|
||||
}
|
||||
|
||||
public static String sha1Hex(Path path) throws IOException {
|
||||
HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1());
|
||||
return toHex(hash.asBytes());
|
||||
}
|
||||
|
||||
public static String truncatedSha256(File file) {
|
||||
try {
|
||||
HashCode hash = Files.asByteSource(file).hash(Hashing.sha256());
|
||||
|
||||
@@ -149,6 +149,7 @@ public class Constants {
|
||||
public static final String OUT_REFMAP_FILE = "outRefMapFile";
|
||||
public static final String DEFAULT_OBFUSCATION_ENV = "defaultObfuscationEnv";
|
||||
public static final String QUIET = "quiet";
|
||||
public static final String SHOW_MESSAGE_TYPES = "showMessageTypes";
|
||||
|
||||
private MixinArguments() {
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2019 Chocohead
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.gradle.api.Project;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
|
||||
public class DownloadUtil {
|
||||
/**
|
||||
* Download from the given {@link URL} to the given {@link File} so long as there are differences between them.
|
||||
*
|
||||
* @param from The URL of the file to be downloaded
|
||||
* @param to The destination to be saved to, and compared against if it exists
|
||||
* @param logger The logger to print everything to, typically from {@link Project#getLogger()}
|
||||
* @throws IOException If an exception occurs during the process
|
||||
*/
|
||||
public static boolean downloadIfChanged(URL from, File to, Logger logger) throws IOException {
|
||||
return downloadIfChanged(from, to, logger, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download from the given {@link URL} to the given {@link File} so long as there are differences between them.
|
||||
*
|
||||
* @param from The URL of the file to be downloaded
|
||||
* @param to The destination to be saved to, and compared against if it exists
|
||||
* @param logger The logger to print information to, typically from {@link Project#getLogger()}
|
||||
* @param quiet Whether to only print warnings (when <code>true</code>) or everything
|
||||
* @throws IOException If an exception occurs during the process
|
||||
*/
|
||||
public static boolean downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) from.openConnection();
|
||||
|
||||
if (LoomGradlePlugin.refreshDeps) {
|
||||
getETagFile(to).delete();
|
||||
to.delete();
|
||||
}
|
||||
|
||||
// If the output already exists we'll use it's last modified time
|
||||
if (to.exists()) {
|
||||
connection.setIfModifiedSince(to.lastModified());
|
||||
}
|
||||
|
||||
//Try use the ETag if there's one for the file we're downloading
|
||||
String etag = loadETag(to, logger);
|
||||
|
||||
if (etag != null) {
|
||||
connection.setRequestProperty("If-None-Match", etag);
|
||||
}
|
||||
|
||||
// We want to download gzip compressed stuff
|
||||
connection.setRequestProperty("Accept-Encoding", "gzip");
|
||||
|
||||
// Try make the connection, it will hang here if the connection is bad
|
||||
connection.connect();
|
||||
|
||||
int code = connection.getResponseCode();
|
||||
|
||||
if ((code < 200 || code > 299) && code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
//Didn't get what we expected
|
||||
delete(to);
|
||||
throw new IOException(connection.getResponseMessage() + " for " + from);
|
||||
}
|
||||
|
||||
long modifyTime = connection.getHeaderFieldDate("Last-Modified", -1);
|
||||
|
||||
if (to.exists() && (code == HttpURLConnection.HTTP_NOT_MODIFIED || modifyTime > 0 && to.lastModified() >= modifyTime)) {
|
||||
if (!quiet) {
|
||||
logger.info("'{}' Not Modified, skipping.", to);
|
||||
}
|
||||
|
||||
return false; //What we've got is already fine
|
||||
}
|
||||
|
||||
long contentLength = connection.getContentLengthLong();
|
||||
|
||||
if (!quiet && contentLength >= 0) {
|
||||
logger.info("'{}' Changed, downloading {}", to, toNiceSize(contentLength));
|
||||
}
|
||||
|
||||
try { // Try download to the output
|
||||
InputStream inputStream = connection.getInputStream();
|
||||
|
||||
if ("gzip".equals(connection.getContentEncoding())) {
|
||||
inputStream = new GZIPInputStream(inputStream);
|
||||
}
|
||||
|
||||
FileUtils.copyInputStreamToFile(inputStream, to);
|
||||
} catch (IOException e) {
|
||||
delete(to); // Probably isn't good if it fails to copy/save
|
||||
throw e;
|
||||
}
|
||||
|
||||
//Set the modify time to match the server's (if we know it)
|
||||
if (modifyTime > 0) {
|
||||
to.setLastModified(modifyTime);
|
||||
}
|
||||
|
||||
//Save the ETag (if we know it)
|
||||
String eTag = connection.getHeaderField("ETag");
|
||||
|
||||
if (eTag != null) {
|
||||
//Log if we get a weak ETag and we're not on quiet
|
||||
if (!quiet && eTag.startsWith("W/")) {
|
||||
logger.warn("Weak ETag found.");
|
||||
}
|
||||
|
||||
saveETag(to, eTag, logger);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file in the same directory as the given file with <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();
|
||||
}
|
||||
|
||||
HashedDownloadUtil.delete(file);
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2020-2021 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.gradle.api.logging.Logger;
|
||||
|
||||
import net.fabricmc.loom.LoomGradlePlugin;
|
||||
|
||||
public class HashedDownloadUtil {
|
||||
public static boolean requiresDownload(File to, String expectedHash, Logger logger) {
|
||||
if (LoomGradlePlugin.refreshDeps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (to.exists()) {
|
||||
String sha1 = getSha1(to, logger);
|
||||
|
||||
// The hash in the sha1 file matches
|
||||
return !expectedHash.equals(sha1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet) throws IOException {
|
||||
downloadIfInvalid(from, to, expectedHash, logger, quiet, true);
|
||||
}
|
||||
|
||||
public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict) throws IOException {
|
||||
downloadIfInvalid(from, to, expectedHash, logger, quiet, strict, () -> { });
|
||||
}
|
||||
|
||||
public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict, Runnable startDownload) throws IOException {
|
||||
if (LoomGradlePlugin.refreshDeps && !Boolean.getBoolean("loom.refresh")) {
|
||||
delete(to);
|
||||
}
|
||||
|
||||
if (to.exists()) {
|
||||
if (strict) {
|
||||
if (Checksum.equals(to, expectedHash)) {
|
||||
// The hash matches the target file
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String sha1 = getSha1(to, logger);
|
||||
|
||||
if (expectedHash.equals(sha1)) {
|
||||
// The hash in the sha1 file matches
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startDownload.run();
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) from.openConnection();
|
||||
connection.setRequestProperty("Accept-Encoding", "gzip");
|
||||
connection.connect();
|
||||
|
||||
int code = connection.getResponseCode();
|
||||
|
||||
if ((code < 200 || code > 299) && code != HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
//Didn't get what we expected
|
||||
delete(to);
|
||||
throw new IOException(connection.getResponseMessage() + " for " + from);
|
||||
}
|
||||
|
||||
long contentLength = connection.getContentLengthLong();
|
||||
|
||||
if (!quiet && contentLength >= 0) {
|
||||
logger.info("'{}' Changed, downloading {}", to, DownloadUtil.toNiceSize(contentLength));
|
||||
}
|
||||
|
||||
try { // Try download to the output
|
||||
InputStream inputStream = connection.getInputStream();
|
||||
|
||||
if ("gzip".equals(connection.getContentEncoding())) {
|
||||
inputStream = new GZIPInputStream(inputStream);
|
||||
}
|
||||
|
||||
FileUtils.copyInputStreamToFile(inputStream, to);
|
||||
} catch (IOException e) {
|
||||
delete(to); // Probably isn't good if it fails to copy/save
|
||||
throw e;
|
||||
}
|
||||
|
||||
saveSha1(to, expectedHash, logger);
|
||||
}
|
||||
|
||||
private static File getSha1File(File file) {
|
||||
return new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".sha1");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getSha1(File to, Logger logger) {
|
||||
if (!to.exists()) {
|
||||
delete(to);
|
||||
return null;
|
||||
}
|
||||
|
||||
File sha1File = getSha1File(to);
|
||||
|
||||
try {
|
||||
return Files.asCharSource(sha1File, StandardCharsets.UTF_8).read();
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// Quicker to catch this than do an exists check before.
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error reading sha1 file '{}'.", sha1File);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveSha1(File to, String sha1, Logger logger) {
|
||||
File sha1File = getSha1File(to);
|
||||
|
||||
try {
|
||||
if (!sha1File.exists()) {
|
||||
sha1File.createNewFile();
|
||||
}
|
||||
|
||||
Files.asCharSink(sha1File, StandardCharsets.UTF_8).write(sha1);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error saving sha1 file '{}'.", sha1File, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void delete(File file) {
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
File sha1File = getSha1File(file);
|
||||
|
||||
if (sha1File.exists()) {
|
||||
sha1File.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ import org.cadixdev.lorenz.MappingSet;
|
||||
import org.cadixdev.mercury.Mercury;
|
||||
import org.cadixdev.mercury.remapper.MercuryRemapper;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.internal.project.ProjectInternal;
|
||||
import org.gradle.internal.logging.progress.ProgressLogger;
|
||||
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import net.fabricmc.loom.LoomGradleExtension;
|
||||
@@ -44,7 +47,6 @@ import net.fabricmc.loom.api.RemapConfigurationSettings;
|
||||
import net.fabricmc.loom.api.mappings.layered.MappingsNamespace;
|
||||
import net.fabricmc.loom.build.IntermediaryNamespaces;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl;
|
||||
import net.fabricmc.loom.util.gradle.ProgressLoggerHelper;
|
||||
import net.fabricmc.lorenztiny.TinyMappingsReader;
|
||||
import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
@@ -52,7 +54,7 @@ public class SourceRemapper {
|
||||
private final Project project;
|
||||
private String from;
|
||||
private String to;
|
||||
private final List<Consumer<ProgressLoggerHelper>> remapTasks = new ArrayList<>();
|
||||
private final List<Consumer<ProgressLogger>> remapTasks = new ArrayList<>();
|
||||
|
||||
private Mercury mercury;
|
||||
|
||||
@@ -96,7 +98,8 @@ public class SourceRemapper {
|
||||
|
||||
project.getLogger().lifecycle(":remapping sources");
|
||||
|
||||
ProgressLoggerHelper progressLogger = ProgressLoggerHelper.getProgressFactory(project, SourceRemapper.class.getName());
|
||||
ProgressLoggerFactory progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class);
|
||||
ProgressLogger progressLogger = progressLoggerFactory.newOperation(SourceRemapper.class.getName());
|
||||
progressLogger.start("Remapping dependency sources", "sources");
|
||||
|
||||
remapTasks.forEach(consumer -> consumer.accept(progressLogger));
|
||||
|
||||
413
src/main/java/net/fabricmc/loom/util/download/Download.java
Normal file
413
src/main/java/net/fabricmc/loom/util/download/Download.java
Normal file
@@ -0,0 +1,413 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2022 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.util.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.nio.file.attribute.UserDefinedFileAttributeView;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.github.mizosoft.methanol.Methanol;
|
||||
import com.github.mizosoft.methanol.ProgressTracker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.util.Checksum;
|
||||
|
||||
public class Download {
|
||||
private static final String E_TAG = "ETag";
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Download.class);
|
||||
|
||||
public static DownloadBuilder create(String url) throws URISyntaxException {
|
||||
return DownloadBuilder.create(url);
|
||||
}
|
||||
|
||||
private final URI url;
|
||||
private final String expectedHash;
|
||||
private final boolean useEtag;
|
||||
private final boolean forceDownload;
|
||||
private final boolean offline;
|
||||
private final Duration maxAge;
|
||||
private final DownloadProgressListener progressListener;
|
||||
|
||||
Download(URI url, String expectedHash, boolean useEtag, boolean forceDownload, boolean offline, Duration maxAge, DownloadProgressListener progressListener) {
|
||||
this.url = url;
|
||||
this.expectedHash = expectedHash;
|
||||
this.useEtag = useEtag;
|
||||
this.forceDownload = forceDownload;
|
||||
this.offline = offline;
|
||||
this.maxAge = maxAge;
|
||||
this.progressListener = progressListener;
|
||||
}
|
||||
|
||||
private HttpClient getHttpClient() throws DownloadException {
|
||||
if (offline) {
|
||||
throw error("Unable to download %s in offline mode", this.url);
|
||||
}
|
||||
|
||||
return Methanol.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.proxy(ProxySelector.getDefault())
|
||||
.autoAcceptEncoding(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private HttpRequest getRequest() {
|
||||
return HttpRequest.newBuilder(url)
|
||||
.GET()
|
||||
.build();
|
||||
}
|
||||
|
||||
private HttpRequest getETagRequest(String etag) {
|
||||
return HttpRequest.newBuilder(url)
|
||||
.GET()
|
||||
.header("If-None-Match", etag)
|
||||
.build();
|
||||
}
|
||||
|
||||
private <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);
|
||||
|
||||
// Create a .lock file, this allows us to re-download if the download was forcefully aborted part way through.
|
||||
createLock(output);
|
||||
HttpResponse<Path> response = send(httpRequest, HttpResponse.BodyHandlers.ofFile(output));
|
||||
getAndResetLock(output);
|
||||
|
||||
final int statusCode = response.statusCode();
|
||||
boolean success = statusCode == HttpURLConnection.HTTP_NOT_MODIFIED || (statusCode >= 200 && statusCode < 300);
|
||||
|
||||
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
// Success, etag matched.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
try {
|
||||
Files.deleteIfExists(output);
|
||||
} catch (IOException ignored) {
|
||||
// We tried.
|
||||
}
|
||||
|
||||
throw error("HTTP request to (%s) returned unsuccessful status (%d)", url, statusCode);
|
||||
}
|
||||
|
||||
if (useEtag) {
|
||||
final HttpHeaders headers = response.headers();
|
||||
final String responseETag = headers.firstValue(E_TAG.toLowerCase(Locale.ROOT)).orElse(null);
|
||||
|
||||
if (responseETag != null) {
|
||||
writeEtag(output, responseETag);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedHash != null) {
|
||||
// Ensure we downloaded the expected hash.
|
||||
if (!isHashValid(output)) {
|
||||
String downloadedHash;
|
||||
|
||||
try {
|
||||
downloadedHash = Checksum.sha1Hex(output);
|
||||
Files.deleteIfExists(output);
|
||||
} catch (IOException e) {
|
||||
downloadedHash = "unknown hash";
|
||||
}
|
||||
|
||||
throw error("Failed to download (%s) with expected hash: %s got %s", url, expectedHash, downloadedHash);
|
||||
}
|
||||
|
||||
// Write the hash to the file attribute, saves a lot of time trying to re-compute the hash when re-visiting this file.
|
||||
writeHash(output, expectedHash);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean requiresDownload(Path output) throws DownloadException {
|
||||
if (forceDownload || !exists(output)) {
|
||||
// File does not exist, or we are forced to download again.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (offline) {
|
||||
// We know the file exists, nothing more we can do.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getAndResetLock(output)) {
|
||||
LOGGER.warn("Forcing downloading {} as existing lock file was found. This may happen if the gradle build was forcefully canceled.", output);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (expectedHash != null) {
|
||||
final String hashAttribute = readHash(output).orElse("");
|
||||
|
||||
if (expectedHash.equalsIgnoreCase(hashAttribute)) {
|
||||
// File has a matching hash attribute, assume file intact.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isHashValid(output)) {
|
||||
// Valid hash, no need to re-download
|
||||
return false;
|
||||
}
|
||||
|
||||
if (System.getProperty("fabric.loom.test") != null) {
|
||||
// This should never happen in an ideal world.
|
||||
// It means that something has altered a file that should be cached.
|
||||
throw error("Download file (%s) may have been modified", output);
|
||||
}
|
||||
|
||||
LOGGER.info("Found existing file ({}) to download with unexpected hash.", output);
|
||||
}
|
||||
|
||||
//noinspection RedundantIfStatement
|
||||
if (!maxAge.equals(Duration.ZERO) && !isOutdated(output)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default to re-downloading, may check the etag
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isHashValid(Path path) {
|
||||
int i = expectedHash.indexOf(':');
|
||||
String algorithm = expectedHash.substring(0, i);
|
||||
String hash = expectedHash.substring(i + 1);
|
||||
|
||||
try {
|
||||
String computedHash = switch (algorithm) {
|
||||
case "sha1" -> Checksum.sha1Hex(path);
|
||||
default -> throw error("Unsupported hash algorithm (%s)", algorithm);
|
||||
};
|
||||
|
||||
return computedHash.equalsIgnoreCase(hash);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOutdated(Path path) throws DownloadException {
|
||||
try {
|
||||
final FileTime lastModified = getLastModified(path);
|
||||
return lastModified.toInstant().plus(maxAge)
|
||||
.isBefore(Instant.now());
|
||||
} catch (IOException e) {
|
||||
throw error(e, "Failed to check if (%s) is outdated", path);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<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 Path getLockFile(Path output) {
|
||||
return output.resolveSibling(output.getFileName() + ".lock");
|
||||
}
|
||||
|
||||
private boolean getAndResetLock(Path output) throws DownloadException {
|
||||
final Path lock = getLockFile(output);
|
||||
final boolean exists = Files.exists(lock);
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(lock);
|
||||
} catch (IOException e) {
|
||||
throw error(e, "Failed to release lock on %s", lock);
|
||||
}
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
private void createLock(Path output) throws DownloadException {
|
||||
final Path lock = getLockFile(output);
|
||||
|
||||
try {
|
||||
Files.createFile(lock);
|
||||
} catch (IOException e) {
|
||||
throw error(e, "Failed to acquire lock on %s", lock);
|
||||
}
|
||||
}
|
||||
|
||||
private DownloadException error(String message, Object... args) {
|
||||
return new DownloadException(String.format(Locale.ENGLISH, message, args));
|
||||
}
|
||||
|
||||
private DownloadException error(Throwable throwable) {
|
||||
return new DownloadException(throwable);
|
||||
}
|
||||
|
||||
private DownloadException error(Throwable throwable, String message, Object... args) {
|
||||
return new DownloadException(message.formatted(args), throwable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2022 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.util.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public class DownloadBuilder {
|
||||
private static final Duration ONE_DAY = Duration.ofDays(1);
|
||||
|
||||
private final URI url;
|
||||
private String expectedHash = null;
|
||||
private boolean useEtag = true;
|
||||
private boolean forceDownload = false;
|
||||
private boolean offline = false;
|
||||
private Duration maxAge = Duration.ZERO;
|
||||
private DownloadProgressListener progressListener = DownloadProgressListener.NONE;
|
||||
private int maxRetries = 1;
|
||||
|
||||
private DownloadBuilder(URI url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
static DownloadBuilder create(String url) throws URISyntaxException {
|
||||
return new DownloadBuilder(new URI(url));
|
||||
}
|
||||
|
||||
public DownloadBuilder sha1(String sha1) {
|
||||
this.expectedHash = "sha1:" + sha1;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder etag(boolean useEtag) {
|
||||
this.useEtag = useEtag;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder forceDownload() {
|
||||
forceDownload = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder offline() {
|
||||
offline = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder maxAge(Duration duration) {
|
||||
this.maxAge = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder progress(DownloadProgressListener progressListener) {
|
||||
this.progressListener = progressListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder maxRetries(int maxRetries) {
|
||||
this.maxRetries = maxRetries;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadBuilder defaultCache() {
|
||||
etag(true);
|
||||
return maxAge(ONE_DAY);
|
||||
}
|
||||
|
||||
private Download build() {
|
||||
return new Download(this.url, this.expectedHash, this.useEtag, this.forceDownload, this.offline, maxAge, progressListener);
|
||||
}
|
||||
|
||||
public void downloadPathAsync(Path path, DownloadExecutor executor) {
|
||||
executor.runAsync(() -> downloadPath(path));
|
||||
}
|
||||
|
||||
public void downloadPath(Path path) throws DownloadException {
|
||||
withRetries(() -> {
|
||||
build().downloadPath(path);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public String downloadString() throws DownloadException {
|
||||
return withRetries(() -> build().downloadString());
|
||||
}
|
||||
|
||||
public String downloadString(Path cache) throws DownloadException {
|
||||
withRetries(() -> {
|
||||
build().downloadPath(cache);
|
||||
return null;
|
||||
});
|
||||
|
||||
try {
|
||||
return Files.readString(cache, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.delete(cache);
|
||||
} catch (IOException ex) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
throw new DownloadException("Failed to download and read string", e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T withRetries(DownloadSupplier<T> supplier) throws DownloadException {
|
||||
for (int i = 1; i <= maxRetries; i++) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (DownloadException e) {
|
||||
if (i == maxRetries) {
|
||||
throw new DownloadException(String.format(Locale.ENGLISH, "Failed download after %d attempts", maxRetries), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface DownloadSupplier<T> {
|
||||
T get() throws DownloadException;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@
|
||||
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="%ECLIPSE_PROJECT%"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="%VM_ARGS%"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="${workspace_loc:%ECLIPSE_PROJECT%}/%RUN_DIRECTORY%"/>
|
||||
<booleanAttribute key="org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE" value="true"/>
|
||||
</launchConfiguration>
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
<envs>
|
||||
%IDEA_ENV_VARS%
|
||||
</envs>
|
||||
<shortenClasspath name="ARGS_FILE" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
||||
@@ -27,7 +27,7 @@ package net.fabricmc.loom.test
|
||||
import org.gradle.util.GradleVersion
|
||||
|
||||
class LoomTestConstants {
|
||||
private final static String NIGHTLY_VERSION = "7.6-20220620222921+0000"
|
||||
private final static String NIGHTLY_VERSION = "7.6-20220722221130+0000"
|
||||
private final static boolean NIGHTLY_EXISTS = nightlyExists(NIGHTLY_VERSION)
|
||||
|
||||
public final static String DEFAULT_GRADLE = GradleVersion.current().getVersion()
|
||||
|
||||
@@ -27,7 +27,7 @@ package net.fabricmc.loom.test.integration
|
||||
import net.fabricmc.loom.test.util.GradleProjectTestTrait
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonParser
|
||||
import java.util.jar.JarFile
|
||||
|
||||
import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
|
||||
|
||||
@@ -54,8 +54,8 @@ class ReproducibleBuildTest extends Specification implements GradleProjectTestTr
|
||||
|
||||
where:
|
||||
version | modHash | sourceHash
|
||||
DEFAULT_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["be31766e6cafbe4ae3bca9e35ba63169", "7348b0bd87d36d7ec6f3bca9c2b66062"]
|
||||
PRE_RELEASE_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["be31766e6cafbe4ae3bca9e35ba63169", "7348b0bd87d36d7ec6f3bca9c2b66062"]
|
||||
DEFAULT_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["0d9eec9248d93eb6ec4a1cd4d927e609", "436bf54ef015576b0a338d55d9a0bb82"]
|
||||
PRE_RELEASE_GRADLE | "ed3306ef60f434c55048cba8de5dab95" | ["0d9eec9248d93eb6ec4a1cd4d927e609", "436bf54ef015576b0a338d55d9a0bb82"]
|
||||
}
|
||||
|
||||
String generateMD5(File file) {
|
||||
|
||||
@@ -84,6 +84,10 @@ class IdeaClasspathModificationsTest extends Specification {
|
||||
<method v="2">
|
||||
<option enabled="true" name="Make"/>
|
||||
</method>
|
||||
<envs>
|
||||
%IDEA_ENV_VARS%
|
||||
</envs>
|
||||
<shortenClasspath name="ARGS_FILE" />
|
||||
<classpathModifications><entry exclude="true" path="/path/to/file.jar"/></classpathModifications></configuration>
|
||||
</component>
|
||||
'''.trim()
|
||||
@@ -101,6 +105,10 @@ class IdeaClasspathModificationsTest extends Specification {
|
||||
<method v="2">
|
||||
<option enabled="true" name="Make"/>
|
||||
</method>
|
||||
<envs>
|
||||
%IDEA_ENV_VARS%
|
||||
</envs>
|
||||
<shortenClasspath name="ARGS_FILE" />
|
||||
<classpathModifications><entry exclude="true" path="/path/to/file.jar"/><entry exclude="true" path="/path/to/another.jar"/></classpathModifications></configuration>
|
||||
</component>
|
||||
'''.trim()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2022 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.download
|
||||
|
||||
import io.javalin.http.HttpCode
|
||||
import net.fabricmc.loom.util.download.Download
|
||||
import net.fabricmc.loom.util.download.DownloadException
|
||||
|
||||
class DownloadStringTest extends DownloadTest {
|
||||
def "String: Download"() {
|
||||
setup:
|
||||
server.get("/downloadString") {
|
||||
it.result("Hello World!")
|
||||
}
|
||||
|
||||
when:
|
||||
def result = Download.create("$PATH/downloadString").downloadString()
|
||||
|
||||
then:
|
||||
result == "Hello World!"
|
||||
}
|
||||
|
||||
def "String: Not found"() {
|
||||
setup:
|
||||
server.get("/stringNotFound") {
|
||||
it.status(404)
|
||||
}
|
||||
|
||||
when:
|
||||
def result = Download.create("$PATH/stringNotFound")
|
||||
.maxRetries(3) // Ensure we still error as expected when retrying
|
||||
.downloadString()
|
||||
|
||||
then:
|
||||
thrown DownloadException
|
||||
}
|
||||
|
||||
def "String: Redirect"() {
|
||||
setup:
|
||||
server.get("/redirectString2") {
|
||||
it.result("Hello World!")
|
||||
}
|
||||
server.get("/redirectString") {
|
||||
it.redirect("$PATH/redirectString2")
|
||||
}
|
||||
|
||||
when:
|
||||
def result = Download.create("$PATH/redirectString").downloadString()
|
||||
|
||||
then:
|
||||
result == "Hello World!"
|
||||
}
|
||||
|
||||
def "String: Retries"() {
|
||||
setup:
|
||||
int requests = 0
|
||||
server.get("/retryString") {
|
||||
requests ++
|
||||
|
||||
if (requests < 3) {
|
||||
it.status(HttpCode.INTERNAL_SERVER_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
it.result("Hello World " + requests)
|
||||
}
|
||||
|
||||
when:
|
||||
def result = Download.create("$PATH/retryString")
|
||||
.maxRetries(3)
|
||||
.downloadString()
|
||||
|
||||
then:
|
||||
result == "Hello World 3"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -34,6 +34,8 @@ import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsProcess
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider
|
||||
import net.fabricmc.loom.test.unit.LoomMocks
|
||||
import net.fabricmc.loom.util.download.Download
|
||||
import net.fabricmc.loom.util.download.DownloadBuilder
|
||||
import net.fabricmc.mappingio.adapter.MappingDstNsReorder
|
||||
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch
|
||||
import net.fabricmc.mappingio.format.Tiny2Writer
|
||||
@@ -148,5 +150,15 @@ abstract class LayeredMappingsSpecification extends Specification implements Lay
|
||||
Logger getLogger() {
|
||||
return mockLogger
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
DownloadBuilder download(String url) {
|
||||
return Download.create(url)
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean refreshDeps() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ dependencies {
|
||||
mappings "net.fabricmc:yarn:1.17.1+build.59:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:0.11.6"
|
||||
|
||||
modImplementation files("dummy.jar")
|
||||
// I-faces will still be applied as the jar is on both configurations.
|
||||
modCompileOnly files("dummy.jar")
|
||||
modRuntimeOnly files("dummy.jar")
|
||||
}
|
||||
@@ -73,6 +73,10 @@ loom {
|
||||
defaultRefmapName = "default-refmap0000.json"
|
||||
add(sourceSets["main"], "main-refmap0000.json")
|
||||
add(sourceSets["mixin"])
|
||||
|
||||
messages {
|
||||
MIXIN_SOFT_TARGET_IS_PUBLIC = 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user