Merge 1.7, part 1

This commit is contained in:
Juuz
2024-07-01 19:20:21 +03:00
42 changed files with 997 additions and 139 deletions

View File

@@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
version: [8.6.0-jdk17]
version: [8.7.0-jdk17]
runs-on: ubuntu-22.04
container:
image: gradle:${{ matrix.version }}
@@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-22.04
container:
image: gradle:8.6.0-jdk17
image: gradle:8.7.0-jdk17
options: --user root
steps:
@@ -63,7 +63,7 @@ jobs:
strategy:
fail-fast: false
matrix:
version: [8.6.0-jdk17]
version: [8.7.0-jdk17]
test: ${{ fromJson(needs.prepare_test_matrix.outputs.matrix) }}
runs-on: ubuntu-22.04

View File

@@ -14,7 +14,7 @@ import org.gradle.util.GradleVersion;
*/
@SuppressWarnings("unused")
public class LoomGradlePluginBootstrap implements Plugin<PluginAware> {
private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.6";
private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.7";
private static final int MIN_SUPPORTED_MAJOR_JAVA_VERSION = 17;
private static final int MIN_SUPPORTED_MAJOR_IDEA_VERSION = 2021;

View File

@@ -6,7 +6,6 @@ plugins {
id 'eclipse'
id 'groovy'
id 'checkstyle'
id 'jacoco'
id 'codenarc'
alias(libs.plugins.kotlin) apply false // Delay this so we can perform magic 🪄 first.
alias(libs.plugins.spotless)
@@ -50,7 +49,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
}
group = "dev.architectury"
def baseVersion = '1.6'
def baseVersion = '1.7'
def ENV = System.getenv()
def runNumber = ENV.GITHUB_RUN_NUMBER ?: "9999"
@@ -299,20 +298,6 @@ gradlePlugin {
}
}
jacoco {
toolVersion = libs.versions.jacoco.get()
}
// Run to get test coverage.
jacocoTestReport {
dependsOn test
reports {
xml.required = false
csv.required = false
html.outputLocation = file("${layout.buildDirectory.get().asFile}/jacocoHtml")
}
}
test {
maxHeapSize = "2560m"
jvmArgs "-XX:+HeapDumpOnOutOfMemoryError"

View File

@@ -1,5 +1,5 @@
[versions]
kotlin = "1.9.20"
kotlin = "1.9.22"
asm = "9.6"
commons-io = "2.15.1"
gson = "2.10.1"
@@ -8,7 +8,7 @@ guava = "33.0.0-jre"
stitch = "0.6.2"
tiny-remapper = "0.10.1"
access-widener = "2.1.0"
mapping-io = "0.5.1"
mapping-io = "0.6.1"
lorenz-tiny = "4.0.2"
mercury = "0.1.4.17"
kotlinx-metadata = "0.9.0"

View File

@@ -2,7 +2,7 @@
# Decompilers
fernflower = "2.0.0"
cfr = "0.2.2"
vineflower = "1.9.3"
vineflower = "1.10.1"
# Runtime depedencies
mixin-compile-extensions = "0.6.0"

View File

@@ -1,14 +1,14 @@
[versions]
spock = "2.3-groovy-3.0"
junit = "5.10.2"
javalin = "6.1.0"
mockito = "5.10.0"
java-debug = "0.51.0"
javalin = "6.1.3"
mockito = "5.11.0"
java-debug = "0.52.0"
mixin = "0.12.5+mixin.0.8.5"
gradle-nightly = "8.8-20240224001421+0000"
fabric-loader = "0.15.6"
fabric-installer = "1.0.0"
gradle-nightly = "8.9-20240426001649+0000"
fabric-loader = "0.15.10"
fabric-installer = "1.0.1"
[libraries]
spock = { module = "org.spockframework:spock-core", version.ref = "spock" }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

14
gradlew vendored
View File

@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -202,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View File

@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

@@ -153,6 +153,8 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI {
LoomProblemReporter getProblemReporter();
boolean isConfigurationCacheActive();
// ===================
// Architectury Loom
// ===================

View File

@@ -131,7 +131,7 @@ public abstract class CompileConfiguration implements Runnable {
extension.setDependencyManager(dependencyManager);
dependencyManager.handleDependencies(getProject(), serviceManager);
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
disownLock();
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e);
}

View File

@@ -34,6 +34,8 @@ import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.artifacts.FileCollectionDependency;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.util.gradle.SelfResolvingDependencyUtils;
@@ -104,8 +106,21 @@ public class DependencyInfo {
return sourceConfiguration;
}
private boolean matches(ComponentIdentifier identifier) {
if (identifier instanceof ModuleComponentIdentifier moduleComponentIdentifier) {
return moduleComponentIdentifier.getGroup().equals(dependency.getGroup())
&& moduleComponentIdentifier.getModule().equals(dependency.getName())
&& moduleComponentIdentifier.getVersion().equals(dependency.getVersion());
}
return false;
}
public Set<File> resolve() {
return sourceConfiguration.files(dependency);
return sourceConfiguration.getIncoming()
.artifactView(view -> view.componentFilter(this::matches))
.getFiles()
.getFiles();
}
public Optional<File> resolveFile() {

View File

@@ -37,6 +37,8 @@ import net.fabricmc.loom.task.GenerateSourcesTask;
import net.fabricmc.loom.util.Constants;
public abstract class DecompileConfiguration<T extends MappedMinecraftProvider> {
static final String DEFAULT_DECOMPILER = "Vineflower";
protected final Project project;
protected final T minecraftProvider;
protected final LoomGradleExtension extension;

View File

@@ -78,7 +78,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration<Mapp
task.setDescription("Decompile minecraft using the default decompiler.");
task.setGroup(Constants.TaskGroup.FABRIC);
task.dependsOn(project.getTasks().named("genSourcesWithCfr"));
task.dependsOn(project.getTasks().named("genSourcesWith" + DecompileConfiguration.DEFAULT_DECOMPILER));
});
// TODO: Support for env-only jars?

View File

@@ -114,7 +114,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
task.setDescription("Decompile minecraft (%s) using the default decompiler.".formatted(name));
task.setGroup(Constants.TaskGroup.FABRIC);
task.dependsOn(project.getTasks().named("gen%sSourcesWithCfr".formatted(name)));
task.dependsOn(project.getTasks().named("gen%sSourcesWith%s".formatted(name, DecompileConfiguration.DEFAULT_DECOMPILER)));
});
}
}

View File

@@ -29,6 +29,9 @@ import java.io.IOException;
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.Locale;
import java.util.function.Predicate;
import java.util.jar.Attributes;
@@ -45,7 +48,7 @@ import net.fabricmc.loom.util.ModPlatform;
import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
// ARCH: isFabricMod means "is mod on current platform"
public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequirements, @Nullable InstallerData installerData, MixinRemapType mixinRemapType) {
public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequirements, @Nullable InstallerData installerData, MixinRemapType mixinRemapType, List<String> knownIdyBsms) {
private static final String INSTALLER_PATH = "fabric-installer.json";
// ARCH: Quilt support
@@ -60,6 +63,7 @@ public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequi
RemapRequirements remapRequirements = RemapRequirements.DEFAULT;
InstallerData installerData = null;
MixinRemapType refmapRemapType = MixinRemapType.MIXIN;
List<String> knownIndyBsms = new ArrayList<>();
// Force-remap all mods on Forge and NeoForge.
if (platform.isForgeLike()) {
@@ -76,6 +80,7 @@ public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequi
final String remapValue = mainAttributes.getValue(Constants.Manifest.REMAP_KEY);
final String loomVersion = mainAttributes.getValue(Constants.Manifest.LOOM_VERSION);
final String mixinRemapType = mainAttributes.getValue(Constants.Manifest.MIXIN_REMAP_TYPE);
final String knownIndyBsmsValue = mainAttributes.getValue(Constants.Manifest.KNOWN_IDY_BSMS);
if (remapValue != null) {
// Support opting into and out of remapping with "Fabric-Loom-Remap" manifest entry
@@ -98,6 +103,10 @@ public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequi
if (loomVersion != null && refmapRemapType != MixinRemapType.STATIC) {
validateLoomVersion(loomVersion, currentLoomVersion);
}
if (knownIndyBsmsValue != null) {
Collections.addAll(knownIndyBsms, knownIndyBsmsValue.split(","));
}
}
final String installerFile = platform == ModPlatform.QUILT ? QUILT_INSTALLER_PATH : INSTALLER_PATH;
@@ -109,7 +118,7 @@ public record ArtifactMetadata(boolean isFabricMod, RemapRequirements remapRequi
}
}
return new ArtifactMetadata(isFabricMod, remapRequirements, installerData, refmapRemapType);
return new ArtifactMetadata(isFabricMod, remapRequirements, installerData, refmapRemapType, Collections.unmodifiableList(knownIndyBsms));
}
// Validates that the version matches or is less than the current loom version

View File

@@ -166,14 +166,19 @@ public class ModProcessor {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final MappingConfiguration mappingConfiguration = extension.getMappingConfiguration();
String fromM = IntermediaryNamespaces.intermediary(project);
Stopwatch stopwatch = Stopwatch.createStarted();
Set<String> knownIndyBsms = new HashSet<>(extension.getKnownIndyBsms().get());
for (ModDependency modDependency : remapList) {
knownIndyBsms.addAll(modDependency.getMetadata().knownIdyBsms());
}
MappingOption mappingOption = MappingOption.forPlatform(extension).forNamespaces(fromM, toM);
MemoryMappingTree mappings = mappingConfiguration.getMappingsService(serviceManager, mappingOption).getMappingTree();
LoggerFilter.replaceSystemOut();
TinyRemapper.Builder builder = TinyRemapper.newRemapper()
.withKnownIndyBsm(extension.getKnownIndyBsms().get())
.withKnownIndyBsm(knownIndyBsms)
.withMappings(TinyRemapperHelper.create(mappings, fromM, toM, false))
.renameInvalidLocals(false)
.extraAnalyzeVisitor(AccessWidenerAnalyzeVisitorProvider.createFromMods(fromM, remapList, extension.getPlatform().get()));

View File

@@ -33,8 +33,11 @@ import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import javax.inject.Inject;
import com.google.common.base.Suppliers;
import org.gradle.api.Project;
import org.gradle.api.configuration.BuildFeatures;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.ListProperty;
@@ -66,7 +69,7 @@ import net.fabricmc.loom.util.download.Download;
import net.fabricmc.loom.util.download.DownloadBuilder;
import net.fabricmc.loom.util.gradle.GradleUtils;
public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implements LoomGradleExtension {
public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implements LoomGradleExtension {
private final Project project;
private final MixinExtension mixinApExtension;
private final LoomFiles loomFiles;
@@ -83,9 +86,11 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
private MojangMappedMinecraftProvider<?> mojangMappedMinecraftProvider;
private InstallerData installerData;
private boolean refreshDeps;
private Provider<Boolean> multiProjectOptimisation;
private final Provider<Boolean> multiProjectOptimisation;
private final ListProperty<LibraryProcessorManager.LibraryProcessorFactory> libraryProcessorFactories;
private final LoomProblemReporter problemReporter;
private final boolean configurationCacheActive;
private final boolean isolatedProjectsActive;
// +-------------------+
// | Architectury Loom |
@@ -95,6 +100,10 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
private final Supplier<ForgeExtensionAPI> forgeExtension;
private final Supplier<NeoForgeExtensionAPI> neoForgeExtension;
@Inject
protected abstract BuildFeatures getBuildFeatures();
@Inject
public LoomGradleExtensionImpl(Project project, LoomFiles files) {
super(project, files);
this.project = project;
@@ -120,6 +129,22 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
libraryProcessorFactories.addAll(LibraryProcessorManager.DEFAULT_LIBRARY_PROCESSORS);
libraryProcessorFactories.finalizeValueOnRead();
configurationCacheActive = getBuildFeatures().getConfigurationCache().getActive().get();
isolatedProjectsActive = getBuildFeatures().getIsolatedProjects().getActive().get();
// Fundamentally impossible to support multi-project optimisation with the configuration cache and/or isolated projects.
if (multiProjectOptimisation.get() && configurationCacheActive) {
throw new UnsupportedOperationException("Multi-project optimisation is not supported with the configuration cache");
}
if (multiProjectOptimisation.get() && isolatedProjectsActive) {
throw new UnsupportedOperationException("Isolated projects are not supported with multi-project optimisation");
}
if (configurationCacheActive) {
project.getLogger().warn("Loom support for the Gradle configuration cache is highly experimental and may not work as expected. Please report any issues you encounter.");
}
if (refreshDeps) {
project.getLogger().lifecycle("Refresh dependencies is in use, loom will be significantly slower.");
}
@@ -328,6 +353,11 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen
return problemReporter;
}
@Override
public boolean isConfigurationCacheActive() {
return configurationCacheActive;
}
@Override
public ForgeExtensionAPI getForge() {
ModPlatform.assertPlatform(this, ModPlatform.FORGE);

View File

@@ -34,17 +34,18 @@ import javax.inject.Inject;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin;
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.MirrorUtil;
import net.fabricmc.loom.util.download.DownloadExecutor;
import net.fabricmc.loom.util.download.DownloadFactory;
import net.fabricmc.loom.util.download.GradleDownloadProgressListener;
import net.fabricmc.loom.util.gradle.ProgressGroup;
@@ -59,12 +60,24 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
@Input
public abstract Property<String> getMinecraftVersion();
@Input
public abstract Property<String> getResourcesBaseUrl();
@Input
protected abstract Property<String> getAssetsIndexJson();
@OutputDirectory
public abstract RegularFileProperty getAssetsDirectory();
@OutputDirectory
public abstract RegularFileProperty getLegacyResourcesDirectory();
@Inject
protected abstract ProgressLoggerFactory getProgressLoggerFactory();
@Nested
protected abstract DownloadFactory getDownloadFactory();
@Inject
public DownloadAssetsTask() {
final MinecraftVersionMeta versionInfo = getExtension().getMinecraftProvider().getVersionInfo();
@@ -84,6 +97,11 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
getLegacyResourcesDirectory().set(new File(getProject().getProjectDir(), client.getRunDir() + "/resources"));
}
getResourcesBaseUrl().set(MirrorUtil.getResourcesBase(getProject()));
getResourcesBaseUrl().finalizeValue();
getAssetsIndexJson().set(LoomGradlePlugin.GSON.toJson(getExtension().getMinecraftProvider().getVersionInfo().assetIndex()));
getAssetsHash().finalizeValue();
getAssetsDirectory().finalizeValueOnRead();
getLegacyResourcesDirectory().finalizeValueOnRead();
@@ -93,13 +111,13 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
public void downloadAssets() throws IOException {
final AssetIndex assetIndex = getAssetIndex();
try (ProgressGroup progressGroup = new ProgressGroup(getProject(), "Download Assets");
try (ProgressGroup progressGroup = new ProgressGroup("Download Assets", getProgressLoggerFactory());
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;
final String url = getResourcesBaseUrl().get() + sha1.substring(0, 2) + "/" + sha1;
getExtension()
getDownloadFactory()
.download(url)
.sha1(sha1)
.progress(new GradleDownloadProgressListener(object.name(), progressGroup::createProgressLogger))
@@ -108,18 +126,11 @@ public abstract class DownloadAssetsTask extends AbstractLoomTask {
}
}
private MinecraftVersionMeta.AssetIndex getAssetIndexMeta() {
MinecraftVersionMeta versionInfo = getExtension().getMinecraftProvider().getVersionInfo();
return versionInfo.assetIndex();
}
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");
final MinecraftVersionMeta.AssetIndex assetIndex = LoomGradlePlugin.GSON.fromJson(getAssetsIndexJson().get(), MinecraftVersionMeta.AssetIndex.class);
final File indexFile = new File(getAssetsDirectory().get().getAsFile(), "indexes" + File.separator + assetIndex.fabricId(getMinecraftVersion().get()) + ".json");
final String json = extension.download(assetIndex.url())
final String json = getDownloadFactory().download(assetIndex.url())
.sha1(assetIndex.sha1())
.downloadString(indexFile.toPath());

View File

@@ -206,7 +206,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
try (var timer = new Timer("Decompiled sources")) {
runWithoutCache();
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
}
@@ -226,7 +226,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
runWithCache(fs.getRoot());
}
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
}
}

View File

@@ -56,18 +56,22 @@ public abstract class LoomTasks implements Runnable {
t.setDescription("Migrates mappings to a new version.");
t.getOutputs().upToDateWhen(o -> false);
});
var generateLog4jConfig = getTasks().register("generateLog4jConfig", GenerateLog4jConfigTask.class, t -> {
t.setDescription("Generate the log4j config file");
});
var generateRemapClasspath = getTasks().register("generateRemapClasspath", GenerateRemapClasspathTask.class, t -> {
t.setDescription("Generate the remap classpath file");
});
getTasks().register("generateDLIConfig", GenerateDLIConfigTask.class, t -> {
t.setDescription("Generate the DevLaunchInjector config file");
// Must allow these IDE files to be generated first
t.mustRunAfter(getTasks().named("eclipse"));
t.mustRunAfter(getTasks().named("idea"));
});
getTasks().register("generateLog4jConfig", GenerateLog4jConfigTask.class, t -> {
t.setDescription("Generate the log4j config file");
});
getTasks().register("generateRemapClasspath", GenerateRemapClasspathTask.class, t -> {
t.setDescription("Generate the remap classpath file");
t.dependsOn(generateLog4jConfig);
t.getRemapClasspathFile().set(generateRemapClasspath.get().getRemapClasspathFile());
});
getTasks().register("configureLaunch", task -> {

View File

@@ -37,9 +37,18 @@ import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.configuration.ConsoleOutput;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.build.IntermediaryNamespaces;
import net.fabricmc.loom.configuration.providers.forge.ConfigValue;
import net.fabricmc.loom.configuration.providers.forge.ForgeRunTemplate;
@@ -52,10 +61,75 @@ import net.fabricmc.loom.util.PropertyUtil;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
@Input
protected abstract Property<String> getVersionInfoJson();
@Input
protected abstract Property<String> getMinecraftVersion();
@Input
protected abstract Property<Boolean> getSplitSourceSets();
@Input
protected abstract Property<Boolean> getPlainConsole();
@Input
protected abstract Property<Boolean> getANSISupportedIDE();
@Input
@Optional
protected abstract Property<String> getClassPathGroups();
@Input
protected abstract Property<String> getLog4jConfigPaths();
@Input
@Optional
protected abstract Property<String> getClientGameJarPath();
@Input
@Optional
protected abstract Property<String> getCommonGameJarPath();
@Input
protected abstract Property<String> getAssetsDirectoryPath();
@Input
protected abstract Property<String> getNativesDirectoryPath();
@InputFile
public abstract RegularFileProperty getRemapClasspathFile();
@OutputFile
protected abstract RegularFileProperty getDevLauncherConfig();
public GenerateDLIConfigTask() {
getVersionInfoJson().set(LoomGradlePlugin.GSON.toJson(getExtension().getMinecraftProvider().getVersionInfo()));
getMinecraftVersion().set(getExtension().getMinecraftProvider().minecraftVersion());
getSplitSourceSets().set(getExtension().areEnvironmentSourceSetsSplit());
getANSISupportedIDE().set(ansiSupportedIde(getProject()));
getPlainConsole().set(getProject().getGradle().getStartParameter().getConsoleOutput() == ConsoleOutput.Plain);
if (!getExtension().getMods().isEmpty()) {
getClassPathGroups().set(buildClassPathGroups(getProject()));
}
getLog4jConfigPaths().set(getAllLog4JConfigFiles(getProject()));
if (getSplitSourceSets().get()) {
getClientGameJarPath().set(getGameJarPath("client"));
getCommonGameJarPath().set(getGameJarPath("common"));
}
getAssetsDirectoryPath().set(new File(getExtension().getFiles().getUserCache(), "assets").getAbsolutePath());
getNativesDirectoryPath().set(getExtension().getFiles().getNativesDirectory(getProject()).getAbsolutePath());
getDevLauncherConfig().set(getExtension().getFiles().getDevLauncherConfig());
}
@TaskAction
public void run() throws IOException {
final MinecraftVersionMeta versionInfo = getExtension().getMinecraftProvider().getVersionInfo();
File assetsDirectory = new File(getExtension().getFiles().getUserCache(), "assets");
final MinecraftVersionMeta versionInfo = LoomGradlePlugin.GSON.fromJson(getVersionInfoJson().get(), MinecraftVersionMeta.class);
File assetsDirectory = new File(getAssetsDirectoryPath().get());
if (versionInfo.assets().equals("legacy")) {
assetsDirectory = new File(assetsDirectory, "/legacy/" + versionInfo.id());
@@ -64,12 +138,12 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
boolean quilt = getExtension().isQuilt();
final LaunchConfig launchConfig = new LaunchConfig()
.property(!quilt ? "fabric.development" : "loader.development", "true")
.property(!quilt ? "fabric.remapClasspathFile" : "loader.remapClasspathFile", getExtension().getFiles().getRemapClasspathFile().getAbsolutePath())
.property("log4j.configurationFile", getAllLog4JConfigFiles())
.property(!quilt ? "fabric.remapClasspathFile" : "loader.remapClasspathFile", getRemapClasspathFile().get().getAsFile().getAbsolutePath())
.property("log4j.configurationFile", getLog4jConfigPaths().get())
.property("log4j2.formatMsgNoLookups", "true");
if (versionInfo.hasNativesToExtract()) {
String nativesPath = getExtension().getFiles().getNativesDirectory(getProject()).getAbsolutePath();
String nativesPath = getNativesDirectoryPath().get();
launchConfig
.property("client", "java.library.path", nativesPath)
@@ -79,17 +153,17 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
if (!getExtension().isForgeLike()) {
launchConfig
.argument("client", "--assetIndex")
.argument("client", getExtension().getMinecraftProvider().getVersionInfo().assetIndex().fabricId(getExtension().getMinecraftProvider().minecraftVersion()))
.argument("client", versionInfo.assetIndex().fabricId(getMinecraftVersion().get()))
.argument("client", "--assetsDir")
.argument("client", assetsDirectory.getAbsolutePath());
if (getExtension().areEnvironmentSourceSetsSplit()) {
launchConfig.property("client", !quilt ? "fabric.gameJarPath.client" : "loader.gameJarPath.client", getGameJarPath("client"));
launchConfig.property(!quilt ? "fabric.gameJarPath" : "loader.gameJarPath", getGameJarPath("common"));
if (getSplitSourceSets().get()) {
launchConfig.property("client", !quilt ? "fabric.gameJarPath.client" : "loader.gameJarPath.client", getClientGameJarPath().get());
launchConfig.property(!quilt ? "fabric.gameJarPath" : "loader.gameJarPath", getCommonGameJarPath().get());
}
if (!getExtension().getMods().isEmpty()) {
launchConfig.property(!quilt ? "fabric.classPathGroups" : "loader.classPathGroups", getClassPathGroups());
if (getClassPathGroups().isPresent()) {
launchConfig.property(!quilt ? "fabric.classPathGroups" : "loader.classPathGroups", getClassPathGroups().get());
}
}
@@ -166,22 +240,16 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
}
}
final boolean plainConsole = getProject().getGradle().getStartParameter().getConsoleOutput() == ConsoleOutput.Plain;
final boolean ansiSupportedIDE = new File(getProject().getRootDir(), ".vscode").exists()
|| new File(getProject().getRootDir(), ".idea").exists()
|| new File(getProject().getRootDir(), ".project").exists()
|| (Arrays.stream(getProject().getRootDir().listFiles()).anyMatch(file -> file.getName().endsWith(".iws")));
//Enable ansi by default for idea and vscode when gradle is not ran with plain console.
if (ansiSupportedIDE && !plainConsole) {
if (getANSISupportedIDE().get() && !getPlainConsole().get()) {
launchConfig.property("fabric.log.disableAnsi", "false");
}
FileUtils.writeStringToFile(getExtension().getFiles().getDevLauncherConfig(), launchConfig.asString(), StandardCharsets.UTF_8);
FileUtils.writeStringToFile(getDevLauncherConfig().getAsFile().get(), launchConfig.asString(), StandardCharsets.UTF_8);
}
private String getAllLog4JConfigFiles() {
return getExtension().getLog4jConfigs().getFiles().stream()
private static String getAllLog4JConfigFiles(Project project) {
return LoomGradleExtension.get(project).getLog4jConfigs().getFiles().stream()
.map(File::getAbsolutePath)
.collect(Collectors.joining(","));
}
@@ -199,16 +267,24 @@ public abstract class GenerateDLIConfigTask extends AbstractLoomTask {
/**
* See: https://github.com/FabricMC/fabric-loader/pull/585.
*/
private String getClassPathGroups() {
return getExtension().getMods().stream()
private static String buildClassPathGroups(Project project) {
return LoomGradleExtension.get(project).getMods().stream()
.map(modSettings ->
SourceSetHelper.getClasspath(modSettings, getProject()).stream()
SourceSetHelper.getClasspath(modSettings, project).stream()
.map(File::getAbsolutePath)
.collect(Collectors.joining(File.pathSeparator))
)
.collect(Collectors.joining(File.pathSeparator+File.pathSeparator));
}
private static boolean ansiSupportedIde(Project project) {
File rootDir = project.getRootDir();
return new File(rootDir, ".vscode").exists()
|| new File(rootDir, ".idea").exists()
|| new File(rootDir, ".project").exists()
|| (Arrays.stream(rootDir.listFiles()).anyMatch(file -> file.getName().endsWith(".iws")));
}
public static class LaunchConfig {
private final Map<String, List<String>> values = new HashMap<>();

View File

@@ -29,15 +29,27 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import dev.architectury.loom.util.ForgeLoggerConfig;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import net.fabricmc.loom.task.AbstractLoomTask;
public abstract class GenerateLog4jConfigTask extends AbstractLoomTask {
@OutputFile
public abstract RegularFileProperty getOutputFile();
@Inject
public GenerateLog4jConfigTask() {
getOutputFile().set(getExtension().getFiles().getDefaultLog4jConfigFile());
}
@TaskAction
public void run() {
Path outputFile = getExtension().getFiles().getDefaultLog4jConfigFile().toPath();
Path outputFile = getOutputFile().get().getAsFile().toPath();
if (getExtension().isForge() && getExtension().getForge().getUseForgeLoggerConfig().get()) {
ForgeLoggerConfig.copyToPath(getProject(), outputFile);

View File

@@ -60,6 +60,10 @@ public abstract class GenerateRemapClasspathTask extends AbstractLoomTask {
.map(configurations::named)
.forEach(getRemapClasspath()::from);
for (Path minecraftJar : getExtension().getMinecraftJars(MappingsNamespace.INTERMEDIARY)) {
getRemapClasspath().from(minecraftJar.toFile());
}
getRemapClasspathFile().set(getExtension().getFiles().getRemapClasspathFile());
}
@@ -67,10 +71,6 @@ public abstract class GenerateRemapClasspathTask extends AbstractLoomTask {
public void run() {
final List<File> remapClasspath = new ArrayList<>(getRemapClasspath().getFiles());
for (Path minecraftJar : getExtension().getMinecraftJars(MappingsNamespace.INTERMEDIARY)) {
remapClasspath.add(minecraftJar.toFile());
}
String str = remapClasspath.stream()
.map(File::getAbsolutePath)
.collect(Collectors.joining(File.pathSeparator));

View File

@@ -172,6 +172,7 @@ public class Constants {
public static final String FABRIC_LOADER_VERSION = "Fabric-Loader-Version";
public static final String MIXIN_VERSION = "Fabric-Mixin-Version";
public static final String MIXIN_GROUP = "Fabric-Mixin-Group";
public static final String KNOWN_IDY_BSMS = "Fabric-Loom-Known-Indy-BSMS";
}
public static final class Forge {

View File

@@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
import net.fabricmc.loom.nativeplatform.LoomNativePlatform;
import net.fabricmc.loom.nativeplatform.LoomNativePlatformException;
import net.fabricmc.loom.util.gradle.daemon.DaemonUtils;
public final class ExceptionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionUtil.class);
@@ -59,17 +60,24 @@ public final class ExceptionUtil {
return constructor.apply(descriptiveMessage, cause);
}
public static void printFileLocks(Throwable e, Project project) {
public static void processException(Throwable e, Project project) {
Throwable cause = e;
boolean unrecoverable = false;
while (cause != null) {
if (cause instanceof FileSystemException fse) {
if (cause instanceof FileSystemUtil.UnrecoverableZipException) {
unrecoverable = true;
} else if (cause instanceof FileSystemException fse) {
printFileLocks(fse.getFile(), project);
break;
}
cause = cause.getCause();
}
if (unrecoverable) {
DaemonUtils.tryStopGradleDaemon(project);
}
}
private static void printFileLocks(String filename, Project project) {

View File

@@ -28,8 +28,11 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@@ -38,7 +41,7 @@ import java.util.function.Supplier;
import net.fabricmc.tinyremapper.FileSystemReference;
public final class FileSystemUtil {
public record Delegate(FileSystemReference reference) implements AutoCloseable, Supplier<FileSystem> {
public record Delegate(FileSystemReference reference, URI uri) implements AutoCloseable, Supplier<FileSystem> {
public Path getPath(String path, String... more) {
return get().getPath(path, more);
}
@@ -69,7 +72,31 @@ public final class FileSystemUtil {
@Override
public void close() throws IOException {
reference.close();
try {
reference.close();
} catch (IOException e) {
// An IOException can only ever be thrown by the underlying FileSystem.close() call in tiny remapper
// This means that this reference was the last open
try {
// We would then almost always expect this to throw a FileSystemNotFoundException
FileSystem fileSystem = FileSystems.getFileSystem(uri);
if (fileSystem.isOpen()) {
// Or the unlikely chance that another thread opened a new reference
throw e;
}
// However if we end up here, the closed FileSystem was not removed from ZipFileSystemProvider.filesystems
// This leaves us in a broken state, preventing this JVM from ever being able to open a zip at this path.
// See: https://bugs.openjdk.org/browse/JDK-8291712
throw new UnrecoverableZipException(e.getMessage(), e);
} catch (FileSystemNotFoundException ignored) {
// This the "happy" case, where the zip FS failed to close but was
}
// Throw the normal exception, we can recover from this
throw e;
}
}
@Override
@@ -87,18 +114,34 @@ public final class FileSystemUtil {
}
public static Delegate getJarFileSystem(File file, boolean create) throws IOException {
return new Delegate(FileSystemReference.openJar(file.toPath(), create));
return new Delegate(FileSystemReference.openJar(file.toPath(), create), toJarUri(file.toPath()));
}
public static Delegate getJarFileSystem(Path path, boolean create) throws IOException {
return new Delegate(FileSystemReference.openJar(path, create));
return new Delegate(FileSystemReference.openJar(path, create), toJarUri(path));
}
public static Delegate getJarFileSystem(Path path) throws IOException {
return new Delegate(FileSystemReference.openJar(path));
return new Delegate(FileSystemReference.openJar(path), toJarUri(path));
}
public static Delegate getJarFileSystem(URI uri, boolean create) throws IOException {
return new Delegate(FileSystemReference.open(uri, create));
return new Delegate(FileSystemReference.open(uri, create), uri);
}
private static URI toJarUri(Path path) {
URI uri = path.toUri();
try {
return new URI("jar:" + uri.getScheme(), uri.getHost(), uri.getPath(), uri.getFragment());
} catch (URISyntaxException e) {
throw new RuntimeException("can't convert path "+path+" to uri", e);
}
}
public static class UnrecoverableZipException extends RuntimeException {
public UnrecoverableZipException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.download;
import java.net.URISyntaxException;
import javax.inject.Inject;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import net.fabricmc.loom.LoomGradleExtension;
/**
* Can be used to create a {@link DownloadBuilder} with the correct settings for the project within a task.
*/
public abstract class DownloadFactory {
@Input
protected abstract Property<Boolean> getIsOffline();
@Input
protected abstract Property<Boolean> getIsManualRefreshDependencies();
@Inject
public abstract Project getProject();
@Inject
public DownloadFactory() {
getIsOffline().set(getProject().getGradle().getStartParameter().isOffline());
getIsManualRefreshDependencies().set(LoomGradleExtension.get(getProject()).refreshDeps());
}
// Matches the logic in LoomGradleExtensionImpl
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 (getIsOffline().get()) {
builder.offline();
}
if (getIsManualRefreshDependencies().get()) {
builder.forceDownload();
}
return builder;
}
}

View File

@@ -94,6 +94,8 @@ public final class FabricModJsonFactory {
}
throw new UncheckedIOException("Failed to read fabric.mod.json file in zip: " + zipPath, e);
} catch (JsonSyntaxException e) {
throw new JsonSyntaxException("Failed to parse fabric.mod.json in zip: " + zipPath, e);
}
}
@@ -105,6 +107,8 @@ public final class FabricModJsonFactory {
jsonObject = ZipUtils.unpackGsonNullable(zipPath, FABRIC_MOD_JSON, JsonObject.class);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read zip: " + zipPath, e);
} catch (JsonSyntaxException e) {
throw new JsonSyntaxException("Failed to parse fabric.mod.json in zip: " + zipPath, e);
}
if (jsonObject == null) {
@@ -129,23 +133,6 @@ public final class FabricModJsonFactory {
return Optional.ofNullable(createFromZipNullable(zipPath));
}
public static FabricModJson createFromDirectory(Path directory) throws IOException {
final Path path = directory.resolve(FABRIC_MOD_JSON);
// Try another mod metadata file if fabric.mod.json wasn't found.
if (Files.notExists(path)) {
final @Nullable ModMetadataFile modMetadata = ModMetadataFiles.fromDirectory(directory);
if (modMetadata != null) {
return new ModMetadataFabricModJson(modMetadata, new FabricModJsonSource.DirectorySource(directory));
}
}
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
return create(LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class), new FabricModJsonSource.DirectorySource(directory));
}
}
@Nullable
public static FabricModJson createFromSourceSetsNullable(SourceSet... sourceSets) throws IOException {
final File file = SourceSetHelper.findFirstFileInResource(FABRIC_MOD_JSON, sourceSets);

View File

@@ -43,6 +43,11 @@ public class ProgressGroup implements Closeable {
this.progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class);
}
public ProgressGroup(String name, ProgressLoggerFactory progressLoggerFactory) {
this.name = name;
this.progressLoggerFactory = progressLoggerFactory;
}
private void start() {
this.progressGroup = this.progressLoggerFactory.newOperation(name).setDescription(name);
this.progressGroup.started();

View File

@@ -50,6 +50,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.xml.sax.InputSource;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.ModSettings;
import net.fabricmc.loom.configuration.ide.idea.IdeaUtils;
@@ -236,6 +237,15 @@ public final class SourceSetHelper {
Objects.requireNonNull(sourceSet);
Objects.requireNonNull(path);
final Project project = getSourceSetProject(sourceSet);
final LoomGradleExtension extension = LoomGradleExtension.get(project);
if (extension.isConfigurationCacheActive()) {
// TODO config cache, figure this out
project.getLogger().warn("Unable to find resource ({}) in source set ({}) when configuration cache is active", path, sourceSet.getName());
return null;
}
try {
return sourceSet.getResources()
.matching(patternFilterable -> patternFilterable.include(path))

View File

@@ -0,0 +1,124 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.util.gradle.daemon;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import org.gradle.api.Project;
import org.gradle.cache.FileLockManager;
import org.gradle.internal.file.Chmod;
import org.gradle.internal.remote.internal.RemoteConnection;
import org.gradle.internal.remote.internal.inet.TcpOutgoingConnector;
import org.gradle.internal.serialize.Serializers;
import org.gradle.internal.service.ServiceRegistry;
import org.gradle.invocation.DefaultGradle;
import org.gradle.launcher.daemon.client.DaemonClientConnection;
import org.gradle.launcher.daemon.client.StopDispatcher;
import org.gradle.launcher.daemon.protocol.DaemonMessageSerializer;
import org.gradle.launcher.daemon.protocol.Message;
import org.gradle.launcher.daemon.protocol.StopWhenIdle;
import org.gradle.launcher.daemon.registry.DaemonInfo;
import org.gradle.launcher.daemon.registry.DaemonRegistry;
import org.gradle.launcher.daemon.registry.PersistentDaemonRegistry;
import org.gradle.util.GradleVersion;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This uses a vast amount of Gradle internal APIs, however this is only used when the JVM is in an unrecoverable state.
* The alternative is to kill the JVM, using System.exit, which is not ideal and leaves scary messages in the log.
*/
public final class DaemonUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonUtils.class);
private DaemonUtils() {
}
/**
* Request the Gradle daemon to stop when it becomes idle.
*/
public static void tryStopGradleDaemon(Project project) {
try {
stopWhenIdle(project);
} catch (Throwable t) {
LOGGER.error("Failed to request the Gradle demon to stop", t);
}
}
@VisibleForTesting
public static boolean stopWhenIdle(Project project) {
DaemonInfo daemonInfo = findCurrentDaemon(project);
if (daemonInfo == null) {
return false;
}
RemoteConnection<Message> connection = null;
try {
// Gradle communicates with the daemon using a TCP connection, and a custom binary protocol.
// We connect to the daemon using the daemon's address, and then send a StopWhenIdle message.
connection = new TcpOutgoingConnector().connect(daemonInfo.getAddress()).create(Serializers.stateful(DaemonMessageSerializer.create(null)));
DaemonClientConnection daemonClientConnection = new DaemonClientConnection(connection, daemonInfo, null);
new StopDispatcher().dispatch(daemonClientConnection, new StopWhenIdle(UUID.randomUUID(), daemonInfo.getToken()));
} finally {
if (connection != null) {
connection.stop();
}
}
LOGGER.warn("Requested Gradle daemon to stop on exit.");
return true;
}
@Nullable
private static DaemonInfo findCurrentDaemon(Project project) {
// Gradle maintains a list of running daemons in a registry.bin file.
final Path registryBin = project.getGradle().getGradleUserHomeDir().toPath().resolve("daemon").resolve(GradleVersion.current().getVersion()).resolve("registry.bin");
project.getLogger().lifecycle("Looking for daemon in: " + registryBin);
// We can use a PersistentDaemonRegistry to read this
final ServiceRegistry services = ((DefaultGradle) project.getGradle()).getServices();
final DaemonRegistry registry = new PersistentDaemonRegistry(registryBin.toFile(), services.get(FileLockManager.class), services.get(Chmod.class));
final long pid = ProcessHandle.current().pid();
final List<DaemonInfo> runningDaemons = registry.getAll();
LOGGER.info("Found {} running Gradle daemons in registry: {}", runningDaemons.size(), registryBin);
for (DaemonInfo daemonInfo : runningDaemons) {
if (daemonInfo.getPid() == pid) {
return daemonInfo;
}
}
LOGGER.warn("Could not find current process in daemon registry: {}", registryBin);
return null;
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.mojang.util,net.minecrell.terminalconsole.util">
<Configuration status="WARN">
<Appenders>
<!-- System out -->

View File

@@ -0,0 +1,61 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration
import spock.lang.Specification
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE
import static org.gradle.testkit.runner.TaskOutcome.FAILED
class ConfigurationCacheTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "Configuration cache (task #task)"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE)
gradle.buildGradle << """
dependencies {
minecraft 'com.mojang:minecraft:1.20.4'
mappings 'net.fabricmc:yarn:1.20.4+build.3:v2'
modImplementation 'net.fabricmc:fabric-loader:0.15.6'
modImplementation 'net.fabricmc.fabric-api:fabric-api:0.95.4+1.20.4'
}
""".stripIndent()
when:
def result = gradle.run(task: task, configurationCache: true, isloatedProjects: false)
def result2 = gradle.run(task: task, configurationCache: true, isloatedProjects: false)
then:
result.task(":${task}").outcome != FAILED
result2.task(":${task}").outcome != FAILED
where:
task | _
"help" | _
"configureClientLaunch" | _
}
}

View File

@@ -0,0 +1,56 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration
import spock.lang.Specification
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class DaemonShutdownTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "custom decompiler (gradle #version)"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: version)
gradle.buildSrc("stopDaemon")
gradle.buildGradle << '''
dependencies {
minecraft "com.mojang:minecraft:1.20.4"
mappings "net.fabricmc:yarn:1.20.4+build.3:v2"
}
'''
when:
def result = gradle.run(task: "help")
then:
result.task(":help").outcome == SUCCESS
where:
version << STANDARD_TEST_VERSIONS
}
}

View File

@@ -0,0 +1,186 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.integration.buildSrc.stopDaemon
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.cache.FileLockManager
import org.gradle.internal.concurrent.DefaultExecutorFactory
import org.gradle.internal.concurrent.ExecutorFactory
import org.gradle.internal.file.Chmod
import org.gradle.internal.nativeintegration.services.NativeServices
import org.gradle.internal.remote.internal.inet.InetAddressFactory
import org.gradle.internal.service.ServiceRegistry
import org.gradle.invocation.DefaultGradle
import org.gradle.launcher.daemon.configuration.DaemonParameters
import org.gradle.launcher.daemon.context.DefaultDaemonContext
import org.gradle.launcher.daemon.protocol.DaemonMessageSerializer
import org.gradle.launcher.daemon.protocol.Finished
import org.gradle.launcher.daemon.protocol.Message
import org.gradle.launcher.daemon.protocol.StopWhenIdle
import org.gradle.launcher.daemon.protocol.Success
import org.gradle.launcher.daemon.registry.DaemonInfo
import org.gradle.launcher.daemon.registry.PersistentDaemonRegistry
import org.gradle.launcher.daemon.server.DaemonTcpServerConnector
import org.gradle.launcher.daemon.server.DefaultDaemonConnection
import org.gradle.launcher.daemon.server.IncomingConnectionHandler
import org.gradle.launcher.daemon.server.SynchronizedDispatchConnection
import org.gradle.launcher.daemon.server.api.DaemonStateControl
import org.gradle.util.GradleVersion
import net.fabricmc.loom.util.gradle.daemon.DaemonUtils
/**
* An integration test that runs a dummy gradle daemon TCP server, to test the daemon shutdown mechanism.
*/
class TestPlugin implements Plugin<Project> {
static ExecutorFactory executorFactory = new DefaultExecutorFactory()
@Override
void apply(Project project) {
final ServiceRegistry services = ((DefaultGradle) project.getGradle()).getServices()
final Path registryBin = project.getGradle().getGradleUserHomeDir().toPath()
.resolve("daemon")
.resolve(GradleVersion.current().getVersion())
.resolve("registry.bin")
// Start a dummy daemon process
def handler = new TestIncomingConnectionHandler()
def server = new DaemonTcpServerConnector(executorFactory, new InetAddressFactory(), DaemonMessageSerializer.create(null))
def address = server.start(handler, handler)
// Write it in the registry
def registry = new PersistentDaemonRegistry(registryBin.toFile(), services.get(FileLockManager.class), services.get(Chmod.class))
def daemonInfo = new DaemonInfo(address, createDaemonContext(), "token".bytes, DaemonStateControl.State.Busy)
registry.store(daemonInfo)
// When we get a connection, wait for a stop message and process it by responding with a success message
def future = handler.daemonConnection.thenAccept { it.waitForAndProcessStop() }
// Stop the daemon
def result = DaemonUtils.stopWhenIdle(project)
// Wait for the connection to be processed, this should have already happened, as the above call is blocking
future.join()
// And clean up
server.stop()
registry.remove(address)
if (!result) {
throw new IllegalStateException("Failed to stop daemon")
}
}
// Thanks groovy for allowing me to do this :D
static DefaultDaemonContext createDaemonContext() {
int constructorArgsCount = DefaultDaemonContext.class.getConstructors()[0].getParameterCount()
if (constructorArgsCount == 10) {
// Gradle 8.9+ adds a JavaVersion and NativeServicesMode parameter to the constructor
//noinspection GroovyAssignabilityCheck
return new DefaultDaemonContext(
UUID.randomUUID().toString(),
new File("."),
JavaVersion.current(),
new File("."),
ProcessHandle.current().pid(),
0,
List.of(),
false,
NativeServices.NativeServicesMode.NOT_SET,
DaemonParameters.Priority.NORMAL
)
}
return new DefaultDaemonContext(
UUID.randomUUID().toString(),
new File("."),
new File("."),
ProcessHandle.current().pid(),
0,
List.of(),
false,
DaemonParameters.Priority.NORMAL
)
}
class TestIncomingConnectionHandler implements IncomingConnectionHandler, Runnable, AutoCloseable {
CompletableFuture<TestDaemonConnection> daemonConnection = new CompletableFuture<>()
@Override
void handle(SynchronizedDispatchConnection<Message> connection) {
if (daemonConnection.isDone()) {
throw new IllegalStateException("Already have a connection?")
}
daemonConnection.complete(new TestDaemonConnection(connection, executorFactory))
}
@Override
void run() {
throw new IllegalStateException("Should not be called")
}
@Override
void close() throws Exception {
if (daemonConnection.isDone()) {
daemonConnection.get().stop()
}
}
}
class TestDaemonConnection extends DefaultDaemonConnection {
SynchronizedDispatchConnection<Message> dispatchConnection
TestDaemonConnection(SynchronizedDispatchConnection<Message> connection, ExecutorFactory executorFactory) {
super(connection, executorFactory)
this.dispatchConnection = connection
}
void waitForAndProcessStop() {
def response = receive(1, TimeUnit.MINUTES)
if (!(response instanceof StopWhenIdle)) {
throw new IllegalStateException("Expected StopWhenIdle, got ${response}")
}
println("Received stop message ${response}")
dispatchConnection.dispatchAndFlush(new Success("Ok"))
response = receive(1, TimeUnit.MINUTES)
if (!(response instanceof Finished)) {
throw new IllegalStateException("Expected Finished, got ${response}")
}
println("Received finished message ${response}")
}
}
}

View File

@@ -184,6 +184,23 @@ class ArtifactMetadataTest extends Specification {
"1.4" | "2.4"
}
def "known indy BSMs"() {
given:
def zip = createZip(entries)
when:
def metadata = createMetadata(zip)
then:
knownBSMs == metadata.knownIdyBsms()
where:
knownBSMs | entries
[] | ["fabric.mod.json": "{}"] // Default
["com/example/Class"] | ["META-INF/MANIFEST.MF": manifest("Fabric-Loom-Known-Indy-BSMS", "com/example/Class")] // single bsm
[
"com/example/Class",
"com/example/Another"
] | ["META-INF/MANIFEST.MF": manifest("Fabric-Loom-Known-Indy-BSMS", "com/example/Class,com/example/Another")] // two bsms
}
private static Path createMod(String loomVersion, String remapType) {
return createZip(["fabric.mod.json": "{}", "META-INF/MANIFEST.MF": manifest(["Fabric-Loom-Version": loomVersion, "Fabric-Loom-Mixin-Remap-Type": remapType])])
}

View File

@@ -0,0 +1,95 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.fabricmc.loom.test.unit
import java.nio.file.FileSystem
import java.nio.file.FileSystemAlreadyExistsException
import java.nio.file.FileSystemException
import java.nio.file.FileSystemNotFoundException
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import spock.lang.Specification
// Test to prove https://bugs.openjdk.org/browse/JDK-8291712
// If this test starts failing on a new JDK, it is likely that the bug has been fixed!
class ClosedZipFSReproducer extends Specification {
def "JDK-8291712"() {
when:
Path tempDir = Files.createTempDirectory("test")
Path zipFile = tempDir.resolve("example.zip")
// Create a new ZipFileSystem, and prevent it from being written on close
def fs = openZipFS(zipFile, true)
Files.writeString(fs.getPath("test.txt"), "Hello, World!")
// Before we close the ZipFS do something to prevent the zip from being written on close
// E.G lock the file
Files.delete(zipFile)
Files.createDirectories(zipFile)
Files.createFile(zipFile.resolve("lock"))
try {
fs.close()
throw new IllegalStateException("Expected FileSystemException")
} catch (FileSystemException ignored) {
// Expected
}
// Remove the "lock"
Files.delete(zipFile.resolve("lock"))
// We would expect a new FileSystem to be created, but instead we get the old one
// That is in a broken state
fs = openZipFS(zipFile, true)
then:
!fs.isOpen()
}
private static FileSystem openZipFS(Path path, boolean create) throws IOException {
URI uri = toJarUri(path)
try {
return FileSystems.getFileSystem(uri)
} catch (FileSystemNotFoundException e) {
try {
return FileSystems.newFileSystem(uri, create ? Collections.singletonMap("create", "true") : Collections.emptyMap())
} catch (FileSystemAlreadyExistsException f) {
return FileSystems.getFileSystem(uri)
}
}
}
private static URI toJarUri(Path path) {
URI uri = path.toUri()
try {
return new URI("jar:" + uri.getScheme(), uri.getHost(), uri.getPath(), uri.getFragment())
} catch (URISyntaxException e) {
throw new RuntimeException("can't convert path " + path + " to uri", e)
}
}
}

View File

@@ -32,6 +32,7 @@ import com.google.gson.JsonObject
import spock.lang.Specification
import net.fabricmc.loom.util.Checksum
import net.fabricmc.loom.util.FileSystemUtil
import net.fabricmc.loom.util.Pair
import net.fabricmc.loom.util.ZipReprocessorUtil
import net.fabricmc.loom.util.ZipUtils
@@ -213,4 +214,30 @@ class ZipUtilsTest extends Specification {
then:
transformed.get("test").asString == "THIS IS A TEST OF TRANSFORMING"
}
// Also see: ClosedZipFSReproducer
def "unrecoverable error"() {
given:
def dir = File.createTempDir()
def zip = File.createTempFile("loom-zip-test", ".zip").toPath()
new File(dir, "test.json").text = """
{
"test": "This is a test of transforming"
}
"""
ZipUtils.pack(dir.toPath(), zip)
when:
ZipUtils.transformJson(JsonObject.class, zip, "test.json") { json ->
// Before we close the ZipFS do something to prevent the zip from being written on close
// E.G lock the file
Files.delete(zip)
Files.createDirectories(zip)
Files.createFile(zip.resolve("lock"))
json
}
then:
thrown FileSystemUtil.UnrecoverableZipException
}
}

View File

@@ -150,6 +150,7 @@ trait GradleProjectTestTrait {
private String gradleHomeDir
private String warningMode
private boolean useBuildSrc
private boolean enableDebugging = true
BuildResult run(Map options) {
// Setup the system props to tell loom that its running in a test env
@@ -165,6 +166,14 @@ trait GradleProjectTestTrait {
args << options.task
}
if (options.configurationCache || System.getenv("LOOM_TEST_CONFIGURATION_CACHE") != null) {
args << "--configuration-cache"
}
if (options.isloatedProjects) {
args << "-Dorg.gradle.unsafe.isolated-projects=true"
}
args.addAll(options.tasks ?: [])
args << "--stacktrace"
@@ -179,6 +188,10 @@ trait GradleProjectTestTrait {
writeBuildSrcDeps(runner)
}
if (options.disableDebugging) {
enableDebugging = false
}
return options.expectFailure ? runner.buildAndFail() : runner.build()
}
@@ -188,7 +201,7 @@ trait GradleProjectTestTrait {
.withPluginClasspath()
.withGradleVersion(gradleVersion)
.forwardOutput()
.withDebug(true)
.withDebug(enableDebugging)
}
File getProjectDir() {

View File

@@ -1,6 +1,5 @@
plugins {
id 'dev.architectury.loom'
id 'com.github.johnrengelman.shadow' version '7.0.0'
id 'maven-publish'
}
@@ -83,9 +82,8 @@ loom {
}
}
shadowJar {
task shadowJar(type: Jar) {
archiveClassifier.set("universal-dev")
configurations = []
from(sourceSets["main"].output)
from(sourceSets["mixin"].output)