diff --git a/.github/workflows/test-push.yml b/.github/workflows/test-push.yml index c215edc8..278ef0ea 100644 --- a/.github/workflows/test-push.yml +++ b/.github/workflows/test-push.yml @@ -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 diff --git a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java index 8fbf513d..4f4a7625 100644 --- a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java +++ b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java @@ -14,7 +14,7 @@ import org.gradle.util.GradleVersion; */ @SuppressWarnings("unused") public class LoomGradlePluginBootstrap implements Plugin { - 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; diff --git a/build.gradle b/build.gradle index 0360efd5..845b200a 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79bb21a4..06c1187f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/gradle/runtime.libs.versions.toml b/gradle/runtime.libs.versions.toml index d1e4b11c..76c8f2a0 100644 --- a/gradle/runtime.libs.versions.toml +++ b/gradle/runtime.libs.versions.toml @@ -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" diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 3498517b..8cc5457c 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ea3535d..e7646dea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew index 0adc8e1a..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -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" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..25da30db 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -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 diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index 9df6097f..c870148e 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -153,6 +153,8 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { LoomProblemReporter getProblemReporter(); + boolean isConfigurationCacheActive(); + // =================== // Architectury Loom // =================== diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 5451ddf7..37053c9a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -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); } diff --git a/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java b/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java index f186476d..bef7b526 100644 --- a/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java +++ b/src/main/java/net/fabricmc/loom/configuration/DependencyInfo.java @@ -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 resolve() { - return sourceConfiguration.files(dependency); + return sourceConfiguration.getIncoming() + .artifactView(view -> view.componentFilter(this::matches)) + .getFiles() + .getFiles(); } public Optional resolveFile() { diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/DecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/DecompileConfiguration.java index 7c36d726..de9d11b2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/DecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/DecompileConfiguration.java @@ -37,6 +37,8 @@ import net.fabricmc.loom.task.GenerateSourcesTask; import net.fabricmc.loom.util.Constants; public abstract class DecompileConfiguration { + static final String DEFAULT_DECOMPILER = "Vineflower"; + protected final Project project; protected final T minecraftProvider; protected final LoomGradleExtension extension; diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java index 6ba46400..a26dfa8d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java @@ -78,7 +78,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration 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 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 diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java index c6a4933d..c7162ee7 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -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 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())); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 44ab21d0..27be5353 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -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 multiProjectOptimisation; + private final Provider multiProjectOptimisation; private final ListProperty 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 forgeExtension; private final Supplier 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); diff --git a/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java b/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java index 2ce41369..d7fcc667 100644 --- a/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java +++ b/src/main/java/net/fabricmc/loom/task/DownloadAssetsTask.java @@ -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 getMinecraftVersion(); + @Input + public abstract Property getResourcesBaseUrl(); + + @Input + protected abstract Property 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()); diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index fcf5b813..2de2d67a 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -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); } } diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 794f26ac..42896a63 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -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 -> { diff --git a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java index 5076220e..46e5482e 100644 --- a/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java +++ b/src/main/java/net/fabricmc/loom/task/launch/GenerateDLIConfigTask.java @@ -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 getVersionInfoJson(); + + @Input + protected abstract Property getMinecraftVersion(); + + @Input + protected abstract Property getSplitSourceSets(); + + @Input + protected abstract Property getPlainConsole(); + + @Input + protected abstract Property getANSISupportedIDE(); + + @Input + @Optional + protected abstract Property getClassPathGroups(); + + @Input + protected abstract Property getLog4jConfigPaths(); + + @Input + @Optional + protected abstract Property getClientGameJarPath(); + + @Input + @Optional + protected abstract Property getCommonGameJarPath(); + + @Input + protected abstract Property getAssetsDirectoryPath(); + + @Input + protected abstract Property 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> values = new HashMap<>(); diff --git a/src/main/java/net/fabricmc/loom/task/launch/GenerateLog4jConfigTask.java b/src/main/java/net/fabricmc/loom/task/launch/GenerateLog4jConfigTask.java index 6163eade..55974a62 100644 --- a/src/main/java/net/fabricmc/loom/task/launch/GenerateLog4jConfigTask.java +++ b/src/main/java/net/fabricmc/loom/task/launch/GenerateLog4jConfigTask.java @@ -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); diff --git a/src/main/java/net/fabricmc/loom/task/launch/GenerateRemapClasspathTask.java b/src/main/java/net/fabricmc/loom/task/launch/GenerateRemapClasspathTask.java index 388c8566..8a9263a1 100644 --- a/src/main/java/net/fabricmc/loom/task/launch/GenerateRemapClasspathTask.java +++ b/src/main/java/net/fabricmc/loom/task/launch/GenerateRemapClasspathTask.java @@ -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 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)); diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 68f27557..1d8da43c 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -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 { diff --git a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java index 2f6d91ea..71f36433 100644 --- a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java @@ -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) { diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java index 07fe08cf..3d457766 100644 --- a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -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 { + public record Delegate(FileSystemReference reference, URI uri) implements AutoCloseable, Supplier { 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); + } } } diff --git a/src/main/java/net/fabricmc/loom/util/download/DownloadFactory.java b/src/main/java/net/fabricmc/loom/util/download/DownloadFactory.java new file mode 100644 index 00000000..47506f4c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/download/DownloadFactory.java @@ -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 getIsOffline(); + + @Input + protected abstract Property 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; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java index 7a5296e4..3f90bea9 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java @@ -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); diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java b/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java index 02126565..7eef1d2a 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/ProgressGroup.java @@ -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(); diff --git a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java index 2bdbf248..b693a8a0 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/SourceSetHelper.java @@ -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)) diff --git a/src/main/java/net/fabricmc/loom/util/gradle/daemon/DaemonUtils.java b/src/main/java/net/fabricmc/loom/util/gradle/daemon/DaemonUtils.java new file mode 100644 index 00000000..79ff1bc6 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/daemon/DaemonUtils.java @@ -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 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 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; + } +} diff --git a/src/main/resources/log4j2.fabric.xml b/src/main/resources/log4j2.fabric.xml index 890e6c1f..cb27c401 100644 --- a/src/main/resources/log4j2.fabric.xml +++ b/src/main/resources/log4j2.fabric.xml @@ -1,5 +1,5 @@ - + diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy new file mode 100644 index 00000000..8b900f30 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/ConfigurationCacheTest.groovy @@ -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" | _ + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DaemonShutdownTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DaemonShutdownTest.groovy new file mode 100644 index 00000000..02d28a00 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DaemonShutdownTest.groovy @@ -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 + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy new file mode 100644 index 00000000..7fe886af --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/stopDaemon/TestPlugin.groovy @@ -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 { + 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 daemonConnection = new CompletableFuture<>() + + @Override + void handle(SynchronizedDispatchConnection 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 dispatchConnection + + TestDaemonConnection(SynchronizedDispatchConnection 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}") + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ArtifactMetadataTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ArtifactMetadataTest.groovy index 6625993b..9528d87f 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/ArtifactMetadataTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ArtifactMetadataTest.groovy @@ -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])]) } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ClosedZipFSReproducer.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ClosedZipFSReproducer.groovy new file mode 100644 index 00000000..e75df4a7 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ClosedZipFSReproducer.groovy @@ -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) + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy index 7798a5c0..349bfe91 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy @@ -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 + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy index 4062cfeb..988035d6 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy @@ -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() { diff --git a/src/test/resources/projects/mixinApAutoRefmap/build.gradle b/src/test/resources/projects/mixinApAutoRefmap/build.gradle index ae698037..1aba7606 100644 --- a/src/test/resources/projects/mixinApAutoRefmap/build.gradle +++ b/src/test/resources/projects/mixinApAutoRefmap/build.gradle @@ -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)