Merge with Fabric 0.13, stage 5

This commit is contained in:
Juuz
2022-08-09 17:37:04 +03:00
70 changed files with 2065 additions and 1007 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,11 +26,14 @@ package net.fabricmc.loom.api.mappings.intermediate;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Function;
import org.gradle.api.Named;
import org.gradle.api.provider.Property;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.loom.util.download.DownloadBuilder;
/**
* A simple API to allow 3rd party plugins.
* Implement by creating an abstract class overriding provide and getName
@@ -39,6 +42,8 @@ import org.jetbrains.annotations.ApiStatus;
public abstract class IntermediateMappingsProvider implements Named {
public abstract Property<String> getMinecraftVersion();
public abstract Property<Function<String, DownloadBuilder>> getDownloader();
/**
* Generate or download a tinyv2 mapping file with intermediary and named namespaces.
* @throws IOException

View File

@@ -32,6 +32,7 @@ import org.gradle.api.logging.Logger;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
@ApiStatus.Experimental /* Very Experimental and not cleanly separated from the impl atm */
@@ -54,4 +55,8 @@ public interface MappingContext {
Path workingDirectory(String name);
Logger getLogger();
DownloadBuilder download(String url);
boolean refreshDeps();
}

View File

@@ -115,7 +115,7 @@ public interface FileSpec {
}
static FileSpec createFromUrl(URL url) {
return new URLFileSpec(url);
return new URLFileSpec(url.toString());
}
// Note resolved instantly, this is not lazy

View File

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

View File

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

View File

@@ -25,8 +25,7 @@
package net.fabricmc.loom.configuration;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Map;
@@ -40,7 +39,7 @@ import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.util.DownloadUtil;
import net.fabricmc.loom.util.download.DownloadException;
public class FabricApiExtension {
private final Project project;
@@ -116,10 +115,11 @@ public class FabricApiExtension {
}
try {
URL url = new URL(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion));
DownloadUtil.downloadIfChanged(url, mavenPom, project.getLogger());
} catch (IOException e) {
throw new RuntimeException("Failed to download maven info for " + fabricApiVersion);
extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.pom", fabricApiVersion))
.defaultCache()
.downloadPath(mavenPom.toPath());
} catch (DownloadException e) {
throw new UncheckedIOException("Failed to download maven info for " + fabricApiVersion, e);
}
return mavenPom;

View File

@@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -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);
}

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.jar.Manifest;
@@ -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());

View File

@@ -37,6 +37,7 @@ import org.gradle.api.logging.Logger;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
public class GradleMappingContext implements MappingContext {
@@ -83,6 +84,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;
}

View File

@@ -25,7 +25,6 @@
package net.fabricmc.loom.configuration.providers.mappings;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -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);
}

View File

@@ -25,12 +25,13 @@
package net.fabricmc.loom.configuration.providers.mappings;
import java.util.List;
import java.util.Locale;
import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
public record LayeredMappingSpec(List<MappingsSpec<?>> layers) {
public String getVersion() {
// TODO something better?
return "layered+hash.%d".formatted(Math.abs(hashCode()));
return String.format(Locale.ENGLISH, "layered+hash.%d", Math.abs(hashCode()));
}
}

View File

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

View File

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

View File

@@ -26,7 +26,6 @@ package net.fabricmc.loom.configuration.providers.mappings.mojmap;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -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("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

View File

@@ -24,9 +24,13 @@
package net.fabricmc.loom.configuration.providers.mappings.mojmap;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.api.mappings.layered.spec.MappingsSpec;
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta;
import net.fabricmc.loom.util.download.DownloadException;
public record MojangMappingsSpec(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()

View File

@@ -24,10 +24,9 @@
package net.fabricmc.loom.configuration.providers.mappings.utils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import org.slf4j.Logger;
@@ -35,18 +34,20 @@ import org.slf4j.LoggerFactory;
import net.fabricmc.loom.api.mappings.layered.MappingContext;
import net.fabricmc.loom.api.mappings.layered.spec.FileSpec;
import net.fabricmc.loom.util.DownloadUtil;
import net.fabricmc.loom.util.download.DownloadException;
public record URLFileSpec(URL url) implements FileSpec {
public record URLFileSpec(String url) implements FileSpec {
private static final Logger LOGGER = LoggerFactory.getLogger(URLFileSpec.class);
@Override
public Path get(MappingContext context) {
try {
Path output = context.workingDirectory("%d.URLFileSpec".formatted(Objects.hash(url.toString())));
Path output = context.workingDirectory(String.format(Locale.ENGLISH, "%d.URLFileSpec", Objects.hash(url)));
LOGGER.info("Downloading {} to {}", url, output);
DownloadUtil.downloadIfChanged(url, output.toFile(), LOGGER);
context.download(url)
.defaultCache()
.downloadPath(output);
return output;
} catch (IOException e) {
} catch (DownloadException e) {
throw new UncheckedIOException("Failed to download: " + url, e);
}
}

View File

@@ -33,7 +33,6 @@ import java.util.Objects;
import org.gradle.api.Project;
import net.fabricmc.loom.util.HashedDownloadUtil;
import net.fabricmc.stitch.merge.JarMerger;
public 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);

View File

@@ -27,16 +27,11 @@ package net.fabricmc.loom.configuration.providers.minecraft;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.jetbrains.annotations.Nullable;
@@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
@@ -116,7 +117,7 @@ public final class LoomCFRDecompiler implements LoomDecompiler {
builder.append("\t").append(src).append("\t").append(dst).append("\n");
}
writer.write("%s\t%d\t%d\n".formatted(name, maxLine, maxLineDest));
writer.write(String.format(Locale.ENGLISH, "%s\t%d\t%d\n", name, maxLine, maxLineDest));
writer.write(builder.toString());
writer.write("\n");
}

View File

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

View File

@@ -24,6 +24,7 @@
package net.fabricmc.loom.extension;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -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

View File

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

View File

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

View File

@@ -25,22 +25,12 @@
package net.fabricmc.loom.task;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Deque;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
@@ -53,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();
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.download;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class DownloadExecutor implements AutoCloseable {
private final ExecutorService executorService;
private final List<DownloadException> downloadExceptions = Collections.synchronizedList(new ArrayList<>());
public DownloadExecutor(int threads) {
executorService = Executors.newFixedThreadPool(threads);
}
void runAsync(DownloadRunner downloadRunner) {
if (!downloadExceptions.isEmpty()) {
return;
}
executorService.execute(() -> {
try {
downloadRunner.run();
} catch (DownloadException e) {
executorService.shutdownNow();
downloadExceptions.add(e);
throw new UncheckedIOException(e);
}
});
}
@Override
public void close() throws DownloadException {
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!downloadExceptions.isEmpty()) {
DownloadException downloadException = new DownloadException("Failed to download");
for (DownloadException suppressed : downloadExceptions) {
downloadException.addSuppressed(suppressed);
}
throw downloadException;
}
}
@FunctionalInterface
public interface DownloadRunner {
void run() throws DownloadException;
}
}

View File

@@ -0,0 +1,47 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.download;
public interface DownloadProgressListener {
void onStart();
void onProgress(long bytesTransferred, long contentLength);
void onEnd(boolean success);
DownloadProgressListener NONE = new DownloadProgressListener() {
@Override
public void onStart() {
}
@Override
public void onProgress(long bytesTransferred, long contentLength) {
}
@Override
public void onEnd(boolean success) {
}
};
}

View File

@@ -0,0 +1,74 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.download;
import java.util.Objects;
import java.util.function.Function;
import org.gradle.internal.logging.progress.ProgressLogger;
import org.jetbrains.annotations.Nullable;
public class GradleDownloadProgressListener implements DownloadProgressListener {
private final String name;
private final Function<String, ProgressLogger> progressLoggerFactory;
@Nullable
private ProgressLogger progressLogger;
public GradleDownloadProgressListener(String name, Function<String, ProgressLogger> progressLoggerFactory) {
this.name = name;
this.progressLoggerFactory = progressLoggerFactory;
}
@Override
public void onStart() {
progressLogger = progressLoggerFactory.apply(this.name);
}
@Override
public void onProgress(long bytesTransferred, long contentLength) {
Objects.requireNonNull(progressLogger);
progressLogger.progress("Downloading %s - %s / %s".formatted(name, humanBytes(bytesTransferred), humanBytes(contentLength)));
}
@Override
public void onEnd(boolean success) {
Objects.requireNonNull(progressLogger);
progressLogger.completed();
progressLogger = null;
}
private static String humanBytes(long bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < 1024 * 1024) {
return (bytes / 1024) + " KB";
} else if (bytes < 1024 * 1024 * 1024) {
return String.format("%.2f MB", bytes / (1024.0 * 1024.0));
} else {
return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.gradle;
import java.io.Closeable;
import java.io.IOException;
import org.gradle.api.Project;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.internal.logging.progress.ProgressLogger;
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
public class ProgressGroup implements Closeable {
private final ProgressLoggerFactory progressLoggerFactory;
private final ProgressLogger progressGroup;
public ProgressGroup(Project project, String name) {
this.progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class);
this.progressGroup = this.progressLoggerFactory.newOperation(name).setDescription(name);
this.progressGroup.started();
}
public ProgressLogger createProgressLogger(String name) {
ProgressLogger progressLogger = this.progressLoggerFactory.newOperation(getClass(), progressGroup);
progressLogger.setDescription(name);
progressLogger.start(name, null);
return progressLogger;
}
@Override
public void close() throws IOException {
this.progressGroup.completed();
}
}

View File

@@ -1,234 +0,0 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2020 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.gradle;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.gradle.api.Project;
/**
* Wrapper to ProgressLogger internal API.
*/
public class ProgressLoggerHelper {
private final Object logger;
private final Method getDescription, setDescription, getShortDescription, setShortDescription, getLoggingHeader, setLoggingHeader, start, started, startedArg, progress, completed, completedArg;
private ProgressLoggerHelper(Object logger) {
this.logger = logger;
this.getDescription = getMethod("getDescription");
this.setDescription = getMethod("setDescription", String.class);
this.getShortDescription = getMethod("getShortDescription");
this.setShortDescription = getMethod("setShortDescription", String.class);
this.getLoggingHeader = getMethod("getLoggingHeader");
this.setLoggingHeader = getMethod("setLoggingHeader", String.class);
this.start = getMethod("start", String.class, String.class);
this.started = getMethod("started");
this.startedArg = getMethod("started", String.class);
this.progress = getMethod("progress", String.class);
this.completed = getMethod("completed");
this.completedArg = getMethod("completed", String.class);
}
private static Class<?> getFactoryClass() {
Class<?> progressLoggerFactoryClass = null;
try {
// Gradle 2.14 and higher
progressLoggerFactoryClass = Class.forName("org.gradle.internal.logging.progress.ProgressLoggerFactory");
} catch (ClassNotFoundException e) {
// prior to Gradle 2.14
try {
progressLoggerFactoryClass = Class.forName("org.gradle.logging.ProgressLoggerFactory");
} catch (ClassNotFoundException ignored) {
// Unsupported Gradle version
}
}
return progressLoggerFactoryClass;
}
private Method getMethod(String methodName, Class<?>... args) {
if (logger != null) {
try {
return logger.getClass().getMethod(methodName, args);
} catch (NoSuchMethodException ignored) {
// Nope
}
}
return null;
}
private Object invoke(Method method, Object... args) {
if (logger != null) {
try {
method.setAccessible(true);
return method.invoke(logger, args);
} catch (IllegalAccessException | InvocationTargetException ignored) {
// Nope
}
}
return null;
}
/**
* Get a Progress logger from the Gradle internal API.
*
* @param project The project
* @param category The logger category
* @return In any case a progress logger
*/
public static ProgressLoggerHelper getProgressFactory(Project project, String category) {
try {
Method getServices = project.getClass().getMethod("getServices");
Object serviceFactory = getServices.invoke(project);
Method get = serviceFactory.getClass().getMethod("get", Class.class);
Object progressLoggerFactory = get.invoke(serviceFactory, getFactoryClass());
Method newOperation = progressLoggerFactory.getClass().getMethod("newOperation", String.class);
return new ProgressLoggerHelper(newOperation.invoke(progressLoggerFactory, category));
} catch (Exception e) {
project.getLogger().error("Unable to get progress logger. Download progress will not be displayed.");
return new ProgressLoggerHelper(null);
}
}
/**
* Returns the description of the operation.
*
* @return the description, must not be empty.
*/
public String getDescription() {
return (String) invoke(getDescription);
}
/**
* Sets the description of the operation. This should be a full, stand-alone description of the operation.
*
* <p>This must be called before {@link #started()}
*
* @param description The description.
*/
public ProgressLoggerHelper setDescription(String description) {
invoke(setDescription, description);
return this;
}
/**
* Returns the short description of the operation. This is used in place of the full description when display space is limited.
*
* @return The short description, must not be empty.
*/
public String getShortDescription() {
return (String) invoke(getShortDescription);
}
/**
* Sets the short description of the operation. This is used in place of the full description when display space is limited.
*
* <p>This must be called before {@link #started()}
*
* @param description The short description.
*/
public ProgressLoggerHelper setShortDescription(String description) {
invoke(setShortDescription, description);
return this;
}
/**
* Returns the logging header for the operation. This is logged before any other log messages for this operation are logged. It is usually
* also logged at the end of the operation, along with the final status message. Defaults to null.
*
* <p>If not specified, no logging header is logged.
*
* @return The logging header, possibly empty.
*/
public String getLoggingHeader() {
return (String) invoke(getLoggingHeader);
}
/**
* Sets the logging header for the operation. This is logged before any other log messages for this operation are logged. It is usually
* also logged at the end of the operation, along with the final status message. Defaults to null.
*
* @param header The header. May be empty or null.
*/
public ProgressLoggerHelper setLoggingHeader(String header) {
invoke(setLoggingHeader, header);
return this;
}
/**
* Convenience method that sets descriptions and logs started() event.
*
* @return this logger instance
*/
public ProgressLoggerHelper start(String description, String shortDescription) {
invoke(start, description, shortDescription);
return this;
}
/**
* Logs the start of the operation, with no initial status.
*/
public void started() {
invoke(started);
}
/**
* Logs the start of the operation, with the given status.
*
* @param status The initial status message. Can be null or empty.
*/
public void started(String status) {
invoke(started, status);
}
/**
* Logs some progress, indicated by a new status.
*
* @param status The new status message. Can be null or empty.
*/
public void progress(String status) {
invoke(progress, status);
}
/**
* Logs the completion of the operation, with no final status.
*/
public void completed() {
invoke(completed);
}
/**
* Logs the completion of the operation, with a final status. This is generally logged along with the description.
*
* @param status The final status message. Can be null or empty.
*/
public void completed(String status) {
invoke(completed, status);
}
}

View File

@@ -25,12 +25,13 @@
package net.fabricmc.loom.util.gradle;
import java.io.IOException;
import java.util.Locale;
import net.fabricmc.loom.util.IOStringConsumer;
public record ThreadedSimpleProgressLogger(IOStringConsumer parent) implements IOStringConsumer {
@Override
public void accept(String data) throws IOException {
parent.accept("%d::%s".formatted(Thread.currentThread().getId(), data));
parent.accept(String.format(Locale.ENGLISH, "%d::%s", Thread.currentThread().getId(), data));
}
}

View File

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

View File

@@ -12,5 +12,6 @@
<envs>
%IDEA_ENV_VARS%
</envs>
<shortenClasspath name="ARGS_FILE" />
</configuration>
</component>

View File

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

View File

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

View File

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

View 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()

View File

@@ -26,6 +26,9 @@ package net.fabricmc.loom.test.unit
import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider
import net.fabricmc.loom.test.util.GradleTestUtil
import net.fabricmc.loom.util.download.Download
import java.util.function.Function
import static org.mockito.Mockito.spy
import static org.mockito.Mockito.when
@@ -34,12 +37,14 @@ class LoomMocks {
static IntermediaryMappingsProvider intermediaryMappingsProviderMock(String minecraftVersion, String intermediaryUrl) {
def minecraftVersionProperty = GradleTestUtil.mockProperty(minecraftVersion)
def intermediaryUrlProperty = GradleTestUtil.mockProperty(intermediaryUrl)
def downloaderProperty = GradleTestUtil.mockProperty(Download.&create as Function)
Objects.requireNonNull(minecraftVersionProperty.get())
def mock = spy(IntermediaryMappingsProvider.class)
when(mock.getMinecraftVersion()).thenReturn(minecraftVersionProperty)
when(mock.getIntermediaryUrl()).thenReturn(intermediaryUrlProperty)
when(mock.getDownloader()).thenReturn(downloaderProperty)
return mock
}
}

View File

@@ -0,0 +1,305 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.download
import io.javalin.http.HttpCode
import net.fabricmc.loom.util.download.Download
import net.fabricmc.loom.util.download.DownloadException
import net.fabricmc.loom.util.download.DownloadExecutor
import net.fabricmc.loom.util.download.DownloadProgressListener
import java.nio.file.Files
import java.nio.file.attribute.FileTime
import java.time.Duration
import java.time.Instant
class DownloadFileTest extends DownloadTest {
def "File: Simple"() {
setup:
server.get("/simpleFile") {
it.result("Hello World")
}
def output = new File(File.createTempDir(), "file.txt").toPath()
when:
def result = Download.create("$PATH/simpleFile").downloadPath(output)
then:
Files.readString(output) == "Hello World"
}
def "File: Not found"() {
setup:
server.get("/fileNotfound") {
it.status(404)
}
def output = new File(File.createTempDir(), "file.txt").toPath()
when:
def result = Download.create("$PATH/stringNotFound").downloadPath(output)
then:
thrown DownloadException
}
def "Cache: Sha1"() {
setup:
int requestCount = 0
server.get("/sha1.txt") {
it.result("Hello World")
requestCount ++
}
def output = new File(File.createTempDir(), "file.txt").toPath()
when:
for (i in 0..<2) {
Download.create("$PATH/sha1.txt")
.sha1("0a4d55a8d778e5022fab701977c5d840bbc486d0")
.downloadPath(output)
}
then:
requestCount == 1
}
def "Invalid Sha1"() {
setup:
server.get("/sha1.invalid") {
it.result("Hello World")
}
def output = new File(File.createTempDir(), "file.txt").toPath()
when:
Download.create("$PATH/sha1.invalid")
.sha1("d139cccf047a749691416ce385d3f168c1e28309")
.downloadPath(output)
then:
// Ensure the file we downloaded with the wrong hash was deleted
Files.notExists(output)
thrown DownloadException
}
def "Offline"() {
setup:
int requestCount = 0
server.get("/offline.txt") {
it.result("Hello World")
requestCount ++
}
def output = new File(File.createTempDir(), "offline.txt").toPath()
when:
Download.create("$PATH/offline.txt")
.downloadPath(output)
Download.create("$PATH/offline.txt")
.offline()
.downloadPath(output)
then:
requestCount == 1
}
def "Max Age"() {
setup:
int requestCount = 0
server.get("/maxage.txt") {
it.result("Hello World")
requestCount ++
}
def output = new File(File.createTempDir(), "maxage.txt").toPath()
when:
Download.create("$PATH/maxage.txt")
.maxAge(Duration.ofDays(1))
.downloadPath(output)
Download.create("$PATH/maxage.txt")
.maxAge(Duration.ofDays(1))
.downloadPath(output)
def twoDaysAgo = Instant.now() - Duration.ofDays(2)
Files.setLastModifiedTime(output, FileTime.from(twoDaysAgo))
Download.create("$PATH/maxage.txt")
.maxAge(Duration.ofDays(1))
.downloadPath(output)
then:
requestCount == 2
}
def "ETag"() {
setup:
int requestCount = 0
server.get("/etag") {
def clientEtag = it.req.getHeader("If-None-Match")
def result = "Hello world"
def etag = result.hashCode().toString()
it.header("ETag", etag)
if (clientEtag == etag) {
// Etag matches, no need to send the data.
it.status(HttpCode.NOT_MODIFIED)
return
}
it.result(result)
requestCount ++
}
def output = new File(File.createTempDir(), "etag.txt").toPath()
when:
for (i in 0..<2) {
Download.create("$PATH/etag")
.etag(true)
.downloadPath(output)
}
then:
requestCount == 1
}
def "Progress: File"() {
setup:
server.get("/progressFile") {
it.result("Hello World")
}
def output = new File(File.createTempDir(), "file.txt").toPath()
def started, ended = false
when:
Download.create("$PATH/progressFile")
.progress(new DownloadProgressListener() {
@Override
void onStart() {
started = true
}
@Override
void onProgress(long bytesTransferred, long contentLength) {
}
@Override
void onEnd(boolean success) {
ended = true
}
})
.downloadPath(output)
then:
started
ended
}
def "Progress: String"() {
setup:
server.get("/progressString") {
it.result("Hello World")
}
def started, ended = false
when:
Download.create("$PATH/progressFile")
.progress(new DownloadProgressListener() {
@Override
void onStart() {
started = true
}
@Override
void onProgress(long bytesTransferred, long contentLength) {
}
@Override
void onEnd(boolean success) {
ended = true
}
})
.downloadString()
then:
started
ended
}
def "File: Async"() {
setup:
server.get("/async1") {
it.result("Hello World")
}
def dir = File.createTempDir().toPath()
when:
new DownloadExecutor(2).withCloseable {
Download.create("$PATH/async1").downloadPathAsync(dir.resolve("1.txt"), it)
Download.create("$PATH/async1").downloadPathAsync(dir.resolve("2.txt"), it)
Download.create("$PATH/async1").downloadPathAsync(dir.resolve("3.txt"), it)
Download.create("$PATH/async1").downloadPathAsync(dir.resolve("4.txt"), it)
}
then:
Files.readString(dir.resolve("4.txt")) == "Hello World"
}
def "File: Async Error"() {
setup:
server.get("/async2") {
it.result("Hello World")
}
def dir = File.createTempDir().toPath()
when:
new DownloadExecutor(2).withCloseable {
Download.create("$PATH/async2").downloadPathAsync(dir.resolve("1.txt"), it)
Download.create("$PATH/async2").downloadPathAsync(dir.resolve("2.txt"), it)
Download.create("$PATH/async2").downloadPathAsync(dir.resolve("3.txt"), it)
Download.create("$PATH/async2").downloadPathAsync(dir.resolve("4.txt"), it)
Download.create("$PATH/asyncError").downloadPathAsync(dir.resolve("5.txt"), it)
Download.create("$PATH/asyncError2").downloadPathAsync(dir.resolve("6.txt"), it)
}
then:
thrown DownloadException
}
}

View File

@@ -0,0 +1,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"
}
}

View File

@@ -0,0 +1,42 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2022 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit.download
import io.javalin.Javalin
import spock.lang.Shared
import spock.lang.Specification
abstract class DownloadTest extends Specification {
static final String PATH = "http://localhost:9081"
@Shared
Javalin server = Javalin.create { config ->
config.enableDevLogging()
}.start(9081)
def cleanupSpec() {
server.stop()
}
}

View File

@@ -28,8 +28,8 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace
import net.fabricmc.loom.api.mappings.layered.spec.FileSpec
import net.fabricmc.loom.configuration.providers.mappings.file.FileMappingsSpecBuilderImpl
import net.fabricmc.loom.configuration.providers.mappings.intermediary.IntermediaryMappingsSpec
import net.fabricmc.loom.util.DownloadUtil
import net.fabricmc.loom.util.ZipUtils
import net.fabricmc.loom.util.download.Download
import spock.lang.Unroll
import java.nio.file.Path
@@ -72,7 +72,8 @@ class FileMappingLayerTest extends LayeredMappingsSpecification {
mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17
def mappingsDownload = VERSION_META_1_17.download('client_mappings')
def mappingsFile = new File(tempDir, 'mappings.txt')
DownloadUtil.downloadIfChanged(new URL(mappingsDownload.url()), mappingsFile, mappingContext.logger)
Download.create(mappingsDownload.url())
.downloadPath(mappingsFile.toPath())
when:
def mappings = getLayeredMappings(
new IntermediaryMappingsSpec(),

View File

@@ -34,6 +34,8 @@ import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsProcess
import net.fabricmc.loom.configuration.providers.mappings.extras.unpick.UnpickLayer
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider
import net.fabricmc.loom.test.unit.LoomMocks
import net.fabricmc.loom.util.download.Download
import net.fabricmc.loom.util.download.DownloadBuilder
import net.fabricmc.mappingio.adapter.MappingDstNsReorder
import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch
import net.fabricmc.mappingio.format.Tiny2Writer
@@ -148,5 +150,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
}
}
}

View File

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

View File

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