diff --git a/bootstrap/.gitignore b/bootstrap/.gitignore deleted file mode 100644 index f8dc37c9..00000000 --- a/bootstrap/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Ignore everything -/* - -!/src -!/build.gradle -!/.gitignore -!/test-project \ No newline at end of file diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle deleted file mode 100644 index 30b987a2..00000000 --- a/bootstrap/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -plugins { - id 'java' - id 'groovy' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -tasks.withType(JavaCompile).configureEach { - it.options.encoding = "UTF-8" - it.options.release = 8 -} - -repositories { - mavenCentral() -} - -dependencies { - implementation gradleApi() - - testImplementation(gradleTestKit()) - testImplementation('org.spockframework:spock-core:2.3-groovy-3.0') { - exclude module: 'groovy-all' - } -} - -test { - maxHeapSize = "4096m" - useJUnitPlatform() -} \ No newline at end of file diff --git a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/BootstrappedPlugin.java b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/BootstrappedPlugin.java deleted file mode 100644 index c64246ef..00000000 --- a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/BootstrappedPlugin.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.fabricmc.loom.bootstrap; - -import org.gradle.api.plugins.PluginAware; - -public interface BootstrappedPlugin { - void apply(PluginAware project); -} diff --git a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java b/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java deleted file mode 100644 index 0b852973..00000000 --- a/bootstrap/src/main/java/net/fabricmc/loom/bootstrap/LoomGradlePluginBootstrap.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.fabricmc.loom.bootstrap; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import org.gradle.api.JavaVersion; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.configuration.BuildFeatures; -import org.gradle.api.plugins.PluginAware; -import org.gradle.util.GradleVersion; - -/** - * This bootstrap is compiled against a minimal gradle API and java 8, this allows us to show a nice error to users who run on unsupported configurations. - */ -@SuppressWarnings("unused") -public abstract class LoomGradlePluginBootstrap implements Plugin { - private static final String MIN_SUPPORTED_GRADLE_VERSION = "8.11"; - private static final int MIN_SUPPORTED_MAJOR_JAVA_VERSION = 17; - private static final int MIN_SUPPORTED_MAJOR_IDEA_VERSION = 2022; - - private static final String PLUGIN_CLASS_NAME = "net.fabricmc.loom.LoomGradlePlugin"; - private static final String IDEA_VERSION_PROP_KEY = "idea.version"; - - @Inject - protected abstract BuildFeatures getBuildFeatures(); - - @Override - public void apply(PluginAware pluginAware) { - if (pluginAware instanceof Project) { - Project project = (Project) pluginAware; - - if (getBuildFeatures().getIsolatedProjects().getActive().get() || project.findProperty("fabric.loom.skip-env-validation") == null) { - validateEnvironment(); - } else { - project.getLogger().lifecycle("Loom environment validation disabled. Please re-enable before reporting any issues."); - } - } - - getActivePlugin().apply(pluginAware); - } - - private void validateEnvironment() { - List errors = new ArrayList<>(); - - if (!isValidGradleRuntime()) { - errors.add(String.format("You are using an outdated version of Gradle (%s). Gradle %s or higher is required.", GradleVersion.current().getVersion(), MIN_SUPPORTED_GRADLE_VERSION)); - } - - if (!isValidJavaRuntime()) { - errors.add(String.format("You are using an outdated version of Java (%s). Java %d or higher is required.", JavaVersion.current().getMajorVersion(), MIN_SUPPORTED_MAJOR_JAVA_VERSION)); - - if (Boolean.getBoolean("idea.active")) { - // Idea specific error - errors.add("You can change the Java version in the Gradle settings dialog."); - } else { - String javaHome = System.getenv("JAVA_HOME"); - - if (javaHome != null) { - errors.add(String.format("The JAVA_HOME environment variable is currently set to (%s).", javaHome)); - } - } - } - - if (!isValidIdeaRuntime()) { - errors.add(String.format("You are using an outdated version of intellij idea (%s). Intellij idea %d or higher is required.", System.getProperty(IDEA_VERSION_PROP_KEY), MIN_SUPPORTED_MAJOR_IDEA_VERSION)); - } - - if (!errors.isEmpty()) { - throw new UnsupportedOperationException(String.join("\n", errors)); - } - } - - private static boolean isValidJavaRuntime() { - // Note use compareTo to ensure compatibility with gradle < 6.0 - return JavaVersion.current().compareTo(JavaVersion.toVersion(MIN_SUPPORTED_MAJOR_JAVA_VERSION)) >= 0; - } - - private static boolean isValidGradleRuntime() { - return GradleVersion.current().compareTo(GradleVersion.version(MIN_SUPPORTED_GRADLE_VERSION)) >= 0; - } - - private static boolean isValidIdeaRuntime() { - String version = System.getProperty(IDEA_VERSION_PROP_KEY); - - if (version == null) { - return true; - } - - int ideaYear = Integer.parseInt(version.substring(0, version.indexOf("."))); - return ideaYear >= MIN_SUPPORTED_MAJOR_IDEA_VERSION; - } - - BootstrappedPlugin getActivePlugin() { - try { - return (BootstrappedPlugin) Class.forName(PLUGIN_CLASS_NAME).getConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException("Failed to bootstrap loom", e); - } - } -} diff --git a/bootstrap/test-project/build.gradle b/bootstrap/test-project/build.gradle deleted file mode 100644 index 6be442a1..00000000 --- a/bootstrap/test-project/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id 'dev.architectury.loom' version '0.13.local' -} - -dependencies { - minecraft "com.mojang:minecraft:1.16.5" - mappings loom.officialMojangMappings() -} \ No newline at end of file diff --git a/bootstrap/test-project/settings.gradle b/bootstrap/test-project/settings.gradle deleted file mode 100644 index 37a1b9e0..00000000 --- a/bootstrap/test-project/settings.gradle +++ /dev/null @@ -1,10 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - mavenLocal() - } -} diff --git a/build.gradle b/build.gradle index 03de0ef0..1358e618 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } group = "dev.architectury" -def baseVersion = '1.9' +def baseVersion = '1.10' def ENV = System.getenv() def runNumber = ENV.GITHUB_RUN_NUMBER ?: "9999" @@ -60,15 +60,6 @@ repositories { } } -configurations { - bootstrap { - transitive false - } - compileClasspath.extendsFrom bootstrap - runtimeClasspath.extendsFrom bootstrap - testRuntimeClasspath.extendsFrom bootstrap -} - configurations.configureEach { resolutionStrategy { // I am sorry, for now @@ -108,8 +99,6 @@ sourceSets { dependencies { implementation gradleApi() - bootstrap project(":bootstrap") - // libraries implementation libs.commons.io implementation libs.gson @@ -201,7 +190,6 @@ jar { attributes 'Implementation-Version': project.version } - from configurations.bootstrap.collect { it.isDirectory() ? it : zipTree(it) } from sourceSets.commonDecompiler.output.classesDirs from sourceSets.cfr.output.classesDirs from sourceSets.fernflower.output.classesDirs @@ -277,7 +265,7 @@ gradlePlugin { plugins { fabricLoom { id = 'dev.architectury.loom' - implementationClass = 'net.fabricmc.loom.bootstrap.LoomGradlePluginBootstrap' + implementationClass = 'net.fabricmc.loom.LoomGradlePlugin' } } } @@ -298,6 +286,11 @@ test { maxRetries = 3 } } + + testLogging { + // Log everything to the console + setEvents(TestLogEvent.values().toList()) + } } // Workaround https://github.com/gradle/gradle/issues/25898 @@ -311,7 +304,7 @@ tasks.withType(Test).configureEach { } -import org.gradle.api.internal.artifacts.configurations.ConfigurationRoles +import org.gradle.api.tasks.testing.logging.TestLogEvent import org.gradle.util.GradleVersion import org.w3c.dom.Document import org.w3c.dom.Element @@ -322,18 +315,18 @@ publishing { if (!isSnapshot && !ENV.EXPERIMENTAL) { // Also publish a snapshot so people can use the latest version if they wish snapshot(MavenPublication) { publication -> - groupId project.group - artifactId project.base.archivesName.get() - version baseVersion + '-SNAPSHOT' + groupId = project.group + artifactId = project.base.archivesName.get() + version = baseVersion + '-SNAPSHOT' from components.java } // Manually crate the plugin marker for snapshot versions snapshotPlugin(MavenPublication) { - groupId 'dev.architectury.loom' - artifactId 'dev.architectury.loom.gradle.plugin' - version baseVersion + '-SNAPSHOT' + groupId = 'dev.architectury.loom' + artifactId = 'dev.architectury.loom.gradle.plugin' + version = baseVersion + '-SNAPSHOT' pom.withXml({ // Based off org.gradle.plugin.devel.plugins.MavenPluginPublishPlugin @@ -365,21 +358,6 @@ publishing { } } -// Need to tweak this file to pretend we are compatible with j8 so the bootstrap will run. -tasks.withType(GenerateModuleMetadata).configureEach { - doLast { - def file = outputFile.get().asFile - - def metadata = new groovy.json.JsonSlurper().parseText(file.text) - - metadata.variants.each { - it.attributes["org.gradle.jvm.version"] = 8 - } - - file.text = groovy.json.JsonOutput.toJson(metadata) - } -} - // A task to output a json file with a list of all the test to run tasks.register('writeActionsTestMatrix') { doLast { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05ac7bb5..bbd462a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -kotlin = "2.0.20" +kotlin = "2.0.21" asm = "9.7.1" commons-io = "2.15.1" gson = "2.10.1" guava = "33.0.0-jre" stitch = "0.6.2" -tiny-remapper = "0.10.4" +tiny-remapper = "0.11.0" access-widener = "2.1.0" -mapping-io = "0.6.1" +mapping-io = "0.7.1" lorenz-tiny = "4.0.2" mercury = "0.1.4.17" loom-native = "0.2.0" diff --git a/gradle/runtime.libs.versions.toml b/gradle/runtime.libs.versions.toml index 342a0c0e..0d15d2f6 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.10.1" +vineflower = "1.11.0" # Runtime depedencies mixin-compile-extensions = "0.6.0" @@ -10,6 +10,7 @@ dev-launch-injector = "0.2.1+build.8" terminal-console-appender = "1.3.0" jetbrains-annotations = "25.0.0" native-support = "1.0.1" +fabric-installer = "1.0.1" # Forge Runtime depedencies javax-annotations = "3.0.2" @@ -33,6 +34,7 @@ dev-launch-injector = { module = "net.fabricmc:dev-launch-injector", version.ref terminal-console-appender = { module = "net.minecrell:terminalconsoleappender", version.ref = "terminal-console-appender" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } native-support = { module = "net.fabricmc:fabric-loom-native-support", version.ref = "native-support" } +fabric-installer = { module = "net.fabricmc:fabric-installer", version.ref = "fabric-installer" } # Forge Runtime depedencies javax-annotations = { module = "com.google.code.findbugs:jsr305", version.ref = "javax-annotations" } diff --git a/gradle/test.libs.versions.toml b/gradle/test.libs.versions.toml index 4bc6312a..6a854144 100644 --- a/gradle/test.libs.versions.toml +++ b/gradle/test.libs.versions.toml @@ -6,9 +6,8 @@ mockito = "5.14.2" java-debug = "0.52.0" mixin = "0.15.3+mixin.0.8.7" -gradle-nightly = "8.12-20241110002642+0000" +gradle-nightly = "8.14-20250208001853+0000" fabric-loader = "0.16.9" -fabric-installer = "1.0.1" [libraries] spock = { module = "org.spockframework:spock-core", version.ref = "spock" } @@ -19,5 +18,4 @@ mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } java-debug = { module = "com.microsoft.java:com.microsoft.java.debug.core", version.ref = "java-debug" } mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" } gradle-nightly = { module = "org.gradle:dummy", version.ref = "gradle-nightly" } -fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } -fabric-installer = { module = "net.fabricmc:fabric-installer", version.ref = "fabric-installer" } \ No newline at end of file +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7cf748e7..e0fd0202 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.11-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d..f3b75f3b 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/settings.gradle b/settings.gradle index 18567646..6b14589d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,4 @@ dependencyResolutionManagement { from(files("gradle/runtime.libs.versions.toml")) } } -} - -include "bootstrap" +} \ No newline at end of file diff --git a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java index dff2b351..edfd4c27 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java +++ b/src/main/java/net/fabricmc/loom/LoomGradlePlugin.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2016-2023 FabricMC + * Copyright (c) 2016-2025 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 @@ -33,15 +33,16 @@ import java.util.Set; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.plugins.PluginAware; import net.fabricmc.loom.api.LoomGradleExtensionAPI; -import net.fabricmc.loom.bootstrap.BootstrappedPlugin; +import net.fabricmc.loom.api.fabricapi.FabricApiExtension; import net.fabricmc.loom.configuration.CompileConfiguration; -import net.fabricmc.loom.configuration.FabricApiExtension; import net.fabricmc.loom.configuration.LoomConfigurations; import net.fabricmc.loom.configuration.MavenPublication; +import net.fabricmc.loom.configuration.fabricapi.FabricApiExtensionImpl; import net.fabricmc.loom.configuration.ide.idea.IdeaConfiguration; import net.fabricmc.loom.configuration.sandbox.SandboxConfiguration; import net.fabricmc.loom.decompilers.DecompilerConfiguration; @@ -52,7 +53,7 @@ import net.fabricmc.loom.task.RemapTaskConfiguration; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.LibraryLocationLogger; -public class LoomGradlePlugin implements BootstrappedPlugin { +public class LoomGradlePlugin implements Plugin { public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public static final String LOOM_VERSION = Objects.requireNonNullElse(LoomGradlePlugin.class.getPackage().getImplementationVersion(), "0.0.0+unknown"); @@ -79,7 +80,7 @@ public class LoomGradlePlugin implements BootstrappedPlugin { } } - public void apply(Project project) { + private void apply(Project project) { Set loggedVersions = new HashSet<>(Arrays.asList(System.getProperty("loom.printed.logged", "").split(","))); if (!loggedVersions.contains(LOOM_VERSION)) { @@ -93,7 +94,6 @@ public class LoomGradlePlugin implements BootstrappedPlugin { project.getLogger().lifecycle("You are using an outdated version of Architectury Loom! This version will not receive any support, please consider updating!"); } } - LibraryLocationLogger.logLibraryVersions(); // Apply default plugins @@ -102,7 +102,7 @@ public class LoomGradlePlugin implements BootstrappedPlugin { // Setup extensions project.getExtensions().create(LoomGradleExtensionAPI.class, "loom", LoomGradleExtensionImpl.class, project, LoomFiles.create(project)); - project.getExtensions().create("fabricApi", FabricApiExtension.class); + project.getExtensions().create(FabricApiExtension.class, "fabricApi", FabricApiExtensionImpl.class); for (Class jobClass : SETUP_JOBS) { project.getObjects().newInstance(jobClass).run(); diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java new file mode 100644 index 00000000..4b86632c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java @@ -0,0 +1,70 @@ +/* + * 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.api.fabricapi; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; + +/** + * Represents the settings for data generation. + */ +public interface DataGenerationSettings { + /** + * Contains the output directory where generated data files will be stored. + */ + RegularFileProperty getOutputDirectory(); + + /** + * Contains a boolean indicating whether a run configuration should be created for the data generation process. + */ + Property getCreateRunConfiguration(); + + /** + * Contains a boolean property indicating whether a new source set should be created for the data generation process. + */ + Property getCreateSourceSet(); + + /** + * Contains a string property representing the mod ID associated with the data generation process. + * + *

This must be set when {@link #getCreateRunConfiguration()} is set. + */ + Property getModId(); + + /** + * Contains a boolean property indicating whether strict validation is enabled. + */ + Property getStrictValidation(); + + /** + * Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset. + */ + Property getAddToResources(); + + /** + * Contains a boolean property indicating whether data generation will be compiled and ran with the client. + */ + Property getClient(); +} diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java new file mode 100644 index 00000000..c308f59e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/FabricApiExtension.java @@ -0,0 +1,75 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2024-2025 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.api.fabricapi; + +import org.gradle.api.Action; +import org.gradle.api.artifacts.Dependency; +import org.jetbrains.annotations.ApiStatus; + +/** + * A gradle extension with specific functionality related to Fabric API. + */ +public interface FabricApiExtension { + /** + * Get a {@link Dependency} for a given Fabric API module. + * + * @param moduleName The name of the module. + * @param fabricApiVersion The main Fabric API version. + * @return A {@link Dependency} for the module. + */ + Dependency module(String moduleName, String fabricApiVersion); + + /** + * Get the version of a Fabric API module. + * @param moduleName The name of the module. + * @param fabricApiVersion The main Fabric API version. + * @return The version of the module. + */ + String moduleVersion(String moduleName, String fabricApiVersion); + + /** + * Configuration data generation using the default settings. + */ + void configureDataGeneration(); + + /** + * Configuration data generation using the specified settings. + * @param action An action to configure specific data generation settings. See {@link DataGenerationSettings} for more information. + */ + void configureDataGeneration(Action action); + + /** + * Configuration of game and client tests using the default settings. + */ + @ApiStatus.Experimental + void configureTests(); + + /** + * Configuration of game and/or client tests using the specified settings. + * @param action An action to configure specific game test settings. See {@link GameTestSettings} for more information. + */ + @ApiStatus.Experimental + void configureTests(Action action); +} diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java new file mode 100644 index 00000000..1e556191 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java @@ -0,0 +1,92 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 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.api.fabricapi; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Optional; +import org.jetbrains.annotations.ApiStatus; + +/** + * Represents the settings for game and/or client tests. + */ +@ApiStatus.Experimental +public interface GameTestSettings { + /** + * Contains a boolean property indicating whether a new source set should be created for the tests. + * + *

Default: false + */ + Property getCreateSourceSet(); + + /** + * Contains a string property representing the mod ID associated with the tests. + * + *

This must be set when {@link #getCreateSourceSet()} is set. + */ + @Optional + Property getModId(); + + /** + * Contains a boolean property indicating whether a run configuration will be created for the server side game tests, using Vanilla Game Test framework. + * + *

Default: true + */ + Property getEnableGameTests(); + + /** + * Contains a boolean property indicating whether a run configuration will be created for the client side game tests, using the Fabric API Client Test framework. + * + *

Default: true + */ + Property getEnableClientGameTests(); + + /** + * Contains a boolean property indicating whether the eula has been accepted. By enabling this you agree to the Minecraft EULA located at https://aka.ms/MinecraftEULA. + * + *

This only works when {@link #getEnableClientGameTests()} is enabled. + * + *

Default: false + */ + Property getEula(); + + /** + * Contains a boolean property indicating whether the run directories should be cleared before running the tests. + * + *

This only works when {@link #getEnableClientGameTests()} is enabled. + * + *

Default: true + */ + Property getClearRunDirectory(); + + /** + * Contains a string property representing the username to use for the client side game tests. + * + *

This only works when {@link #getEnableClientGameTests()} is enabled. + * + *

Default: Player0 + */ + @Optional + Property getUsername(); +} diff --git a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java index 4429ac6e..69dce3cf 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/AnnotationProcessorInvoker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2020-2022 FabricMC + * Copyright (c) 2020-2025 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 @@ -41,6 +41,7 @@ import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.build.IntermediaryNamespaces; @@ -66,13 +67,13 @@ public abstract class AnnotationProcessorInvoker { protected final Project project; private final LoomGradleExtension loomExtension; protected final MixinExtension mixinExtension; - protected final Map invokerTasks; + protected final Map> invokerTasks; private final String name; private final Collection apConfigurations; protected AnnotationProcessorInvoker(Project project, Collection apConfigurations, - Map invokerTasks, String name) { + Map> invokerTasks, String name) { this.project = project; this.loomExtension = LoomGradleExtension.get(project); this.mixinExtension = loomExtension.getMixin(); @@ -149,8 +150,8 @@ public abstract class AnnotationProcessorInvoker { } } - for (Map.Entry entry : invokerTasks.entrySet()) { - passMixinArguments(entry.getValue(), entry.getKey()); + for (Map.Entry> entry : invokerTasks.entrySet()) { + entry.getValue().configure(t -> passMixinArguments(t, entry.getKey())); } } diff --git a/src/main/java/net/fabricmc/loom/build/mixin/GroovyApInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/GroovyApInvoker.java index ab85b465..742bab84 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/GroovyApInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/GroovyApInvoker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2022 FabricMC + * Copyright (c) 2022-2025 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 @@ -26,12 +26,12 @@ package net.fabricmc.loom.build.mixin; import java.io.File; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import org.gradle.api.Project; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.compile.GroovyCompile; import net.fabricmc.loom.LoomGradleExtension; @@ -46,11 +46,10 @@ public class GroovyApInvoker extends AnnotationProcessorInvoker { AnnotationProcessorInvoker.GROOVY); } - private static Map getInvokerTasks(Project project) { + private static Map> getInvokerTasks(Project project) { MixinExtension mixin = LoomGradleExtension.get(project).getMixin(); - return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.GROOVY).collect( - Collectors.toMap(Map.Entry::getKey, - entry -> Objects.requireNonNull((GroovyCompile) entry.getValue()))); + return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.GROOVY, GroovyCompile.class).collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override diff --git a/src/main/java/net/fabricmc/loom/build/mixin/JavaApInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/JavaApInvoker.java index 5450009d..1327fcf2 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/JavaApInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/JavaApInvoker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2016-2022 FabricMC + * Copyright (c) 2016-2025 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 @@ -26,11 +26,11 @@ package net.fabricmc.loom.build.mixin; import java.io.File; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import org.gradle.api.Project; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.compile.JavaCompile; import net.fabricmc.loom.LoomGradleExtension; @@ -45,10 +45,10 @@ public class JavaApInvoker extends AnnotationProcessorInvoker { AnnotationProcessorInvoker.JAVA); } - private static Map getInvokerTasks(Project project) { + private static Map> getInvokerTasks(Project project) { MixinExtension mixin = LoomGradleExtension.get(project).getMixin(); - return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.JAVA) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> Objects.requireNonNull((JavaCompile) entry.getValue()))); + return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.JAVA, JavaCompile.class) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override diff --git a/src/main/java/net/fabricmc/loom/build/mixin/KaptApInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/KaptApInvoker.java index f5ae7554..e6ebd259 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/KaptApInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/KaptApInvoker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2020-2022 FabricMC + * Copyright (c) 2020-2025 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 @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import kotlin.Unit; import org.gradle.api.Project; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.compile.JavaCompile; import org.jetbrains.kotlin.gradle.plugin.KaptExtension; @@ -66,35 +67,36 @@ public class KaptApInvoker extends AnnotationProcessorInvoker { kaptExtension.setIncludeCompileClasspath(false); } - private static Map getInvokerTasks(Project project) { + private static Map> getInvokerTasks(Project project) { MixinExtension mixin = LoomGradleExtension.get(project).getMixin(); - return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.JAVA) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> Objects.requireNonNull((JavaCompile) entry.getValue()))); + return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.JAVA, JavaCompile.class) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override public void configureMixin() { super.configureMixin(); - for (Map.Entry entry : invokerTasks.entrySet()) { + for (Map.Entry> entry : invokerTasks.entrySet()) { // Kapt only allows specifying javac args to all annotation processors at once. So we need to specify some dummy // target location for the refmap and then move it to the correct place for each sourceset - JavaCompile task = entry.getValue(); - SourceSet sourceSet = entry.getKey(); - task.doLast(t -> { - try { - String refmapName = Objects.requireNonNull(MixinExtension.getMixinInformationContainer(sourceSet)).refmapNameProvider().get(); - Path src = Paths.get(getRefmapDestination(task, refmapName)); - Path dest = Paths.get(task.getDestinationDirectory().get().getAsFile().toString(), refmapName); + entry.getValue().configure(task -> { + SourceSet sourceSet = entry.getKey(); + task.doLast(t -> { + try { + String refmapName = Objects.requireNonNull(MixinExtension.getMixinInformationContainer(sourceSet)).refmapNameProvider().get(); + Path src = Paths.get(getRefmapDestination(task, refmapName)); + Path dest = Paths.get(task.getDestinationDirectory().get().getAsFile().toString(), refmapName); - // Possible that no mixin annotations exist - if (Files.exists(src)) { - project.getLogger().info("Copying refmap from " + src + " to " + dest); - Files.move(src, dest); + // Possible that no mixin annotations exist + if (Files.exists(src)) { + project.getLogger().info("Copying refmap from " + src + " to " + dest); + Files.move(src, dest); + } + } catch (IOException e) { + project.getLogger().warn("Could not move refmap generated by kapt for task " + task, e); } - } catch (IOException e) { - project.getLogger().warn("Could not move refmap generated by kapt for task " + task, e); - } + }); }); } } diff --git a/src/main/java/net/fabricmc/loom/build/mixin/ScalaApInvoker.java b/src/main/java/net/fabricmc/loom/build/mixin/ScalaApInvoker.java index 81133208..b6fa1465 100644 --- a/src/main/java/net/fabricmc/loom/build/mixin/ScalaApInvoker.java +++ b/src/main/java/net/fabricmc/loom/build/mixin/ScalaApInvoker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2016-2020 FabricMC + * Copyright (c) 2016-2025 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 @@ -26,12 +26,12 @@ package net.fabricmc.loom.build.mixin; import java.io.File; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import org.gradle.api.Project; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.scala.ScalaCompile; import net.fabricmc.loom.LoomGradleExtension; @@ -47,10 +47,10 @@ public class ScalaApInvoker extends AnnotationProcessorInvoker { AnnotationProcessorInvoker.SCALA); } - private static Map getInvokerTasks(Project project) { + private static Map> getInvokerTasks(Project project) { MixinExtension mixin = LoomGradleExtension.get(project).getMixin(); - return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.SCALA) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> Objects.requireNonNull((ScalaCompile) entry.getValue()))); + return mixin.getInvokerTasksStream(AnnotationProcessorInvoker.SCALA, ScalaCompile.class) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } @Override diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index a7f23cde..830c5985 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -215,6 +215,9 @@ public abstract class CompileConfiguration implements Runnable { extension.setMinecraftProvider(minecraftProvider); minecraftProvider.provide(); + // Realise the dependencies without actually resolving them, this forces any lazy providers to be created, populating the layered mapping factories. + project.getConfigurations().getByName(Configurations.MAPPINGS).getDependencies().toArray(); + // Created any layered mapping files. LayeredMappingsFactory.afterEvaluate(configContext); @@ -222,6 +225,7 @@ public abstract class CompileConfiguration implements Runnable { // but before MinecraftPatchedProvider.provide. setupDependencyProviders(project, extension); + // Resolve the mapping files from the configuration final DependencyInfo mappingsDep = DependencyInfo.create(getProject(), Configurations.MAPPINGS); final MappingConfiguration mappingConfiguration = MappingConfiguration.create(getProject(), configContext.serviceFactory(), mappingsDep, minecraftProvider); extension.setMappingConfiguration(mappingConfiguration); diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java deleted file mode 100644 index 26d6b271..00000000 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2020-2023 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.configuration; - -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -import org.gradle.api.Action; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.SourceSetContainer; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.jvm.tasks.Jar; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; -import net.fabricmc.loom.util.download.DownloadException; -import net.fabricmc.loom.util.fmj.FabricModJson; -import net.fabricmc.loom.util.fmj.FabricModJsonFactory; -import net.fabricmc.loom.util.gradle.SourceSetHelper; - -public abstract class FabricApiExtension { - @Inject - public abstract Project getProject(); - - private static final String DATAGEN_SOURCESET_NAME = "datagen"; - - private static final HashMap> moduleVersionCache = new HashMap<>(); - private static final HashMap> deprecatedModuleVersionCache = new HashMap<>(); - - public Dependency module(String moduleName, String fabricApiVersion) { - return getProject().getDependencies() - .create(getDependencyNotation(moduleName, fabricApiVersion)); - } - - public String moduleVersion(String moduleName, String fabricApiVersion) { - String moduleVersion = moduleVersionCache - .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions) - .get(moduleName); - - if (moduleVersion == null) { - moduleVersion = deprecatedModuleVersionCache - .computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions) - .get(moduleName); - } - - if (moduleVersion == null) { - throw new RuntimeException("Failed to find module version for module: " + moduleName); - } - - return moduleVersion; - } - - /** - * Configure data generation with the default options. - */ - public void configureDataGeneration() { - configureDataGeneration(dataGenerationSettings -> { }); - } - - /** - * Configure data generation with custom options. - */ - public void configureDataGeneration(Action action) { - final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); - final TaskContainer taskContainer = getProject().getTasks(); - - DataGenerationSettings settings = getProject().getObjects().newInstance(DataGenerationSettings.class); - settings.getOutputDirectory().set(getProject().file("src/main/generated")); - settings.getCreateRunConfiguration().convention(true); - settings.getCreateSourceSet().convention(false); - settings.getStrictValidation().convention(false); - settings.getAddToResources().convention(true); - settings.getClient().convention(false); - - action.execute(settings); - - final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); - final File outputDirectory = settings.getOutputDirectory().getAsFile().get(); - - if (settings.getAddToResources().get()) { - mainSourceSet.resources(files -> { - // Add the src/main/generated to the main sourceset's resources. - Set srcDirs = new HashSet<>(files.getSrcDirs()); - srcDirs.add(outputDirectory); - files.setSrcDirs(srcDirs); - }); - } - - // Exclude the cache dir from the output jar to ensure reproducibility. - taskContainer.getByName(JavaPlugin.JAR_TASK_NAME, task -> { - Jar jar = (Jar) task; - jar.exclude(".cache/**"); - }); - - if (settings.getCreateSourceSet().get()) { - final boolean isClientAndSplit = extension.areEnvironmentSourceSetsSplit() && settings.getClient().get(); - - SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(getProject()); - - // Create the new datagen sourceset, depend on the main or client sourceset. - SourceSet dataGenSourceSet = sourceSets.create(DATAGEN_SOURCESET_NAME, sourceSet -> { - dependsOn(sourceSet, mainSourceSet); - - if (isClientAndSplit) { - dependsOn(sourceSet, SourceSetHelper.getSourceSetByName(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME, getProject())); - } - }); - - settings.getModId().convention(getProject().provider(() -> { - try { - final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), dataGenSourceSet); - - if (fabricModJson == null) { - throw new RuntimeException("Could not find a fabric.mod.json file in the data source set or a value for DataGenerationSettings.getModId()"); - } - - return fabricModJson.getId(); - } catch (IOException e) { - throw new org.gradle.api.UncheckedIOException("Failed to read mod id from the datagen source set.", e); - } - })); - - extension.getMods().create(settings.getModId().get(), mod -> { - // Create a classpath group for this mod. Assume that the main sourceset is already in a group. - mod.sourceSet(DATAGEN_SOURCESET_NAME); - }); - - extension.createRemapConfigurations(sourceSets.getByName(DATAGEN_SOURCESET_NAME)); - } - - if (settings.getCreateRunConfiguration().get()) { - extension.getRunConfigs().create("datagen", run -> { - run.inherit(extension.getRunConfigs().getByName(settings.getClient().get() ? "client" : "server")); - run.setConfigName("Data Generation"); - - run.property("fabric-api.datagen"); - run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); - run.runDir("build/datagen"); - - if (settings.getModId().isPresent()) { - run.property("fabric-api.datagen.modid", settings.getModId().get()); - } - - if (settings.getStrictValidation().get()) { - run.property("fabric-api.datagen.strict-validation", "true"); - } - - if (settings.getCreateSourceSet().get()) { - run.source(DATAGEN_SOURCESET_NAME); - } - }); - - // Add the output directory as an output allowing the task to be skipped. - getProject().getTasks().named("runDatagen", task -> { - task.getOutputs().dir(outputDirectory); - }); - } - } - - public interface DataGenerationSettings { - /** - * Contains the output directory where generated data files will be stored. - */ - RegularFileProperty getOutputDirectory(); - - /** - * Contains a boolean indicating whether a run configuration should be created for the data generation process. - */ - Property getCreateRunConfiguration(); - - /** - * Contains a boolean property indicating whether a new source set should be created for the data generation process. - */ - Property getCreateSourceSet(); - - /** - * Contains a string property representing the mod ID associated with the data generation process. - * - *

This must be set when {@link #getCreateRunConfiguration()} is set. - */ - Property getModId(); - - /** - * Contains a boolean property indicating whether strict validation is enabled. - */ - Property getStrictValidation(); - - /** - * Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset. - */ - Property getAddToResources(); - - /** - * Contains a boolean property indicating whether data generation will be compiled and ran with the client. - */ - Property getClient(); - } - - private String getDependencyNotation(String moduleName, String fabricApiVersion) { - return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); - } - - private Map getApiModuleVersions(String fabricApiVersion) { - try { - return populateModuleVersionMap(getApiMavenPom(fabricApiVersion)); - } catch (PomNotFoundException e) { - throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion); - } - } - - private Map getDeprecatedApiModuleVersions(String fabricApiVersion) { - try { - return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion)); - } catch (PomNotFoundException e) { - // Not all fabric-api versions have deprecated modules, return an empty map to cache this fact. - return Collections.emptyMap(); - } - } - - private Map populateModuleVersionMap(File pomFile) { - try { - DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); - Document pom = docBuilder.parse(pomFile); - - Map versionMap = new HashMap<>(); - - NodeList dependencies = ((Element) pom.getElementsByTagName("dependencies").item(0)).getElementsByTagName("dependency"); - - for (int i = 0; i < dependencies.getLength(); i++) { - Element dep = (Element) dependencies.item(i); - Element artifact = (Element) dep.getElementsByTagName("artifactId").item(0); - Element version = (Element) dep.getElementsByTagName("version").item(0); - - if (artifact == null || version == null) { - throw new RuntimeException("Failed to find artifact or version"); - } - - versionMap.put(artifact.getTextContent(), version.getTextContent()); - } - - return versionMap; - } catch (Exception e) { - throw new RuntimeException("Failed to parse " + pomFile.getName(), e); - } - } - - private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException { - return getPom("fabric-api", fabricApiVersion); - } - - private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException { - return getPom("fabric-api-deprecated", fabricApiVersion); - } - - private File getPom(String name, String version) throws PomNotFoundException { - final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); - final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version)); - - try { - extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name)) - .defaultCache() - .downloadPath(mavenPom.toPath()); - } catch (DownloadException e) { - if (e.getStatusCode() == 404) { - throw new PomNotFoundException(e); - } - - throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e); - } - - return mavenPom; - } - - private static class PomNotFoundException extends Exception { - PomNotFoundException(Throwable cause) { - super(cause); - } - } - - private static void extendsFrom(Project project, String name, String extendsFrom) { - final ConfigurationContainer configurations = project.getConfigurations(); - - configurations.named(name, configuration -> { - configuration.extendsFrom(configurations.getByName(extendsFrom)); - }); - } - - private void dependsOn(SourceSet sourceSet, SourceSet other) { - sourceSet.setCompileClasspath( - sourceSet.getCompileClasspath() - .plus(other.getOutput()) - ); - - sourceSet.setRuntimeClasspath( - sourceSet.getRuntimeClasspath() - .plus(other.getOutput()) - ); - - extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); - extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); - } -} diff --git a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java index 6d220b91..2c5755c1 100644 --- a/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java +++ b/src/main/java/net/fabricmc/loom/configuration/LoomConfigurations.java @@ -155,6 +155,13 @@ public abstract class LoomConfigurations implements Runnable { getDependencies().add(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()); getDependencies().add(JavaPlugin.TEST_COMPILE_ONLY_CONFIGURATION_NAME, LoomVersions.JETBRAINS_ANNOTATIONS.mavenNotation()); + register(Constants.Configurations.MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES, Role.RESOLVABLE); + extendsFrom(Constants.Configurations.MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES, Constants.Configurations.MINECRAFT_NATIVES); + extendsFrom(Constants.Configurations.MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES, Constants.Configurations.MINECRAFT_CLIENT_RUNTIME_LIBRARIES); + extendsFrom(Constants.Configurations.MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES, Constants.Configurations.LOADER_DEPENDENCIES); + + register(Constants.Configurations.PRODUCTION_RUNTIME_MODS, Role.RESOLVABLE); + GradleUtils.afterSuccessfulEvaluation(getProject(), () -> { if (extension.shouldGenerateSrgTiny()) { registerNonTransitive(Constants.Configurations.SRG, Role.RESOLVABLE); diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java new file mode 100644 index 00000000..22bec596 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java @@ -0,0 +1,112 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 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.configuration.fabricapi; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; +import net.fabricmc.loom.util.fmj.FabricModJson; +import net.fabricmc.loom.util.fmj.FabricModJsonFactory; +import net.fabricmc.loom.util.gradle.SourceSetHelper; + +abstract class FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + protected abstract String getSourceSetName(); + + protected SourceSet configureSourceSet(Property modId, boolean isClient) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); + + final boolean isClientAndSplit = extension.areEnvironmentSourceSetsSplit() && isClient; + + SourceSetContainer sourceSets = SourceSetHelper.getSourceSets(getProject()); + + // Create the new sourceset, depend on the main or client sourceset. + SourceSet sourceSet = sourceSets.create(getSourceSetName(), ss -> { + dependsOn(ss, mainSourceSet); + + if (isClientAndSplit) { + dependsOn(ss, SourceSetHelper.getSourceSetByName(MinecraftSourceSets.Split.CLIENT_ONLY_SOURCE_SET_NAME, getProject())); + } + }); + + modId.convention(getProject().provider(() -> { + try { + final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), sourceSet); + + if (fabricModJson == null) { + throw new RuntimeException("Could not find a fabric.mod.json file in the data source set or a value for DataGenerationSettings.getModId()"); + } + + return fabricModJson.getId(); + } catch (IOException e) { + throw new org.gradle.api.UncheckedIOException("Failed to read mod id from the datagen source set.", e); + } + })); + + extension.getMods().create(modId.get(), mod -> { + // Create a classpath group for this mod. Assume that the main sourceset is already in a group. + mod.sourceSet(getSourceSetName()); + }); + + extension.createRemapConfigurations(sourceSets.getByName(getSourceSetName())); + + return sourceSet; + } + + private static void extendsFrom(Project project, String name, String extendsFrom) { + final ConfigurationContainer configurations = project.getConfigurations(); + + configurations.named(name, configuration -> { + configuration.extendsFrom(configurations.getByName(extendsFrom)); + }); + } + + private void dependsOn(SourceSet sourceSet, SourceSet other) { + sourceSet.setCompileClasspath( + sourceSet.getCompileClasspath() + .plus(other.getOutput()) + ); + + sourceSet.setRuntimeClasspath( + sourceSet.getRuntimeClasspath() + .plus(other.getOutput()) + ); + + extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); + extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java new file mode 100644 index 00000000..836df856 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiDataGeneration.java @@ -0,0 +1,145 @@ +/* + * 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.configuration.fabricapi; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.jvm.tasks.Jar; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.fabricapi.DataGenerationSettings; +import net.fabricmc.loom.util.gradle.SourceSetHelper; + +public abstract class FabricApiDataGeneration extends FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + @Inject + public FabricApiDataGeneration() { + } + + @Override + protected String getSourceSetName() { + return "datagen"; + } + + void configureDataGeneration(Action action) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final TaskContainer taskContainer = getProject().getTasks(); + + DataGenerationSettings settings = getProject().getObjects().newInstance(DataGenerationSettings.class); + settings.getOutputDirectory().set(getProject().file("src/main/generated")); + settings.getCreateRunConfiguration().convention(true); + settings.getCreateSourceSet().convention(false); + settings.getStrictValidation().convention(false); + settings.getAddToResources().convention(true); + settings.getClient().convention(false); + + action.execute(settings); + + final SourceSet mainSourceSet = SourceSetHelper.getMainSourceSet(getProject()); + final File outputDirectory = settings.getOutputDirectory().getAsFile().get(); + + if (settings.getAddToResources().get()) { + mainSourceSet.resources(files -> { + // Add the src/main/generated to the main sourceset's resources. + Set srcDirs = new HashSet<>(files.getSrcDirs()); + srcDirs.add(outputDirectory); + files.setSrcDirs(srcDirs); + }); + } + + // Exclude the cache dir from the output jar to ensure reproducibility. + taskContainer.getByName(JavaPlugin.JAR_TASK_NAME, task -> { + Jar jar = (Jar) task; + jar.exclude(".cache/**"); + }); + + if (settings.getCreateSourceSet().get()) { + configureSourceSet(settings.getModId(), settings.getClient().get()); + } + + if (settings.getCreateRunConfiguration().get()) { + extension.getRunConfigs().create("datagen", run -> { + run.inherit(extension.getRunConfigs().getByName(settings.getClient().get() ? "client" : "server")); + run.setConfigName("Data Generation"); + + run.property("fabric-api.datagen"); + run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); + run.runDir("build/datagen"); + + if (settings.getModId().isPresent()) { + run.property("fabric-api.datagen.modid", settings.getModId().get()); + } + + if (settings.getStrictValidation().get()) { + run.property("fabric-api.datagen.strict-validation", "true"); + } + + if (settings.getCreateSourceSet().get()) { + run.source(getSourceSetName()); + } + }); + + // Add the output directory as an output allowing the task to be skipped. + getProject().getTasks().named("runDatagen", task -> { + task.getOutputs().dir(outputDirectory); + }); + } + } + + private static void extendsFrom(Project project, String name, String extendsFrom) { + final ConfigurationContainer configurations = project.getConfigurations(); + + configurations.named(name, configuration -> { + configuration.extendsFrom(configurations.getByName(extendsFrom)); + }); + } + + private void dependsOn(SourceSet sourceSet, SourceSet other) { + sourceSet.setCompileClasspath( + sourceSet.getCompileClasspath() + .plus(other.getOutput()) + ); + + sourceSet.setRuntimeClasspath( + sourceSet.getRuntimeClasspath() + .plus(other.getOutput()) + ); + + extendsFrom(getProject(), sourceSet.getCompileClasspathConfigurationName(), other.getCompileClasspathConfigurationName()); + extendsFrom(getProject(), sourceSet.getRuntimeClasspathConfigurationName(), other.getRuntimeClasspathConfigurationName()); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java new file mode 100644 index 00000000..0e7de09e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiExtensionImpl.java @@ -0,0 +1,80 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2025 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.configuration.fabricapi; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.model.ObjectFactory; + +import net.fabricmc.loom.api.fabricapi.DataGenerationSettings; +import net.fabricmc.loom.api.fabricapi.FabricApiExtension; +import net.fabricmc.loom.api.fabricapi.GameTestSettings; + +public abstract class FabricApiExtensionImpl implements FabricApiExtension { + @Inject + protected abstract ObjectFactory getObjectFactory(); + + private final FabricApiVersions versions; + private final FabricApiDataGeneration dataGeneration; + private final FabricApiTesting testing; + + public FabricApiExtensionImpl() { + versions = getObjectFactory().newInstance(FabricApiVersions.class); + dataGeneration = getObjectFactory().newInstance(FabricApiDataGeneration.class); + testing = getObjectFactory().newInstance(FabricApiTesting.class); + } + + @Override + public Dependency module(String moduleName, String fabricApiVersion) { + return versions.module(moduleName, fabricApiVersion); + } + + @Override + public String moduleVersion(String moduleName, String fabricApiVersion) { + return versions.moduleVersion(moduleName, fabricApiVersion); + } + + @Override + public void configureDataGeneration() { + configureDataGeneration(dataGenerationSettings -> { }); + } + + @Override + public void configureDataGeneration(Action action) { + dataGeneration.configureDataGeneration(action); + } + + @Override + public void configureTests() { + configureTests(gameTestSettings -> { }); + } + + @Override + public void configureTests(Action action) { + testing.configureTests(action); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java new file mode 100644 index 00000000..8876e045 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiTesting.java @@ -0,0 +1,165 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 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.configuration.fabricapi; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Delete; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskContainer; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.fabricapi.GameTestSettings; +import net.fabricmc.loom.configuration.ide.RunConfigSettings; +import net.fabricmc.loom.task.AbstractLoomTask; +import net.fabricmc.loom.task.LoomTasks; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.gradle.SourceSetHelper; + +public abstract class FabricApiTesting extends FabricApiAbstractSourceSet { + @Inject + protected abstract Project getProject(); + + @Inject + public FabricApiTesting() { + } + + @Override + protected String getSourceSetName() { + return "gametest"; + } + + void configureTests(Action action) { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final TaskContainer tasks = getProject().getTasks(); + + GameTestSettings settings = getProject().getObjects().newInstance(GameTestSettings.class); + settings.getCreateSourceSet().convention(false); + settings.getEnableGameTests().convention(true); + settings.getEnableClientGameTests().convention(true); + settings.getEula().convention(false); + settings.getClearRunDirectory().convention(true); + settings.getUsername().convention("Player0"); + + action.execute(settings); + + final SourceSet testSourceSet; + + if (settings.getCreateSourceSet().get()) { + testSourceSet = configureSourceSet(settings.getModId(), true); + } else { + testSourceSet = SourceSetHelper.getMainSourceSet(getProject()); + } + + Consumer configureBase = run -> { + if (settings.getCreateSourceSet().get()) { + run.source(getSourceSetName()); + } + }; + + if (settings.getEnableGameTests().get()) { + RunConfigSettings gameTest = extension.getRunConfigs().create("gameTest", run -> { + run.inherit(extension.getRunConfigs().getByName("server")); + run.property("fabric-api.gametest"); + run.runDir("build/run/gameTest"); + configureBase.accept(run); + }); + + tasks.named("test", task -> task.dependsOn(LoomTasks.getRunConfigTaskName(gameTest))); + } + + if (settings.getEnableClientGameTests().get()) { + // Not ideal as there may be multiple resources directories, if this isnt correct the mod will need to override this. + final File resourcesDir = testSourceSet.getResources().getSrcDirs().stream().findFirst().orElse(null); + + RunConfigSettings clientGameTest = extension.getRunConfigs().create("clientGameTest", run -> { + run.inherit(extension.getRunConfigs().getByName("client")); + run.property("fabric.client.gametest"); + + if (resourcesDir != null) { + run.property("fabric.client.gametest.testModResourcesPath", resourcesDir.getAbsolutePath()); + } + + run.runDir("build/run/clientGameTest"); + + if (settings.getUsername().isPresent()) { + run.programArgs("--username", settings.getUsername().get()); + } + + configureBase.accept(run); + }); + + if (settings.getClearRunDirectory().get()) { + var deleteGameTestRunDir = tasks.register("deleteGameTestRunDir", Delete.class, task -> { + task.setGroup(Constants.TaskGroup.FABRIC); + task.delete(clientGameTest.getRunDir()); + }); + + tasks.named(LoomTasks.getRunConfigTaskName(clientGameTest), task -> task.dependsOn(deleteGameTestRunDir)); + } + + if (settings.getEula().get()) { + var acceptEula = tasks.register("acceptGameTestEula", AcceptEulaTask.class, task -> { + task.getEulaFile().set(getProject().file(clientGameTest.getRunDir() + "/eula.txt")); + + if (settings.getClearRunDirectory().get()) { + // Ensure that the eula is accepted after the run directory is cleared + task.dependsOn(tasks.named("deleteGameTestRunDir")); + } + }); + + tasks.named("configureLaunch", task -> task.dependsOn(acceptEula)); + } + } + } + + public abstract static class AcceptEulaTask extends AbstractLoomTask { + @OutputFile + public abstract RegularFileProperty getEulaFile(); + + @TaskAction + public void acceptEula() throws IOException { + final Path eula = getEulaFile().get().getAsFile().toPath(); + + if (Files.notExists(eula)) { + Files.writeString(eula, """ + #This file was generated by the Fabric Loom Gradle plugin. As the user opted into accepting the EULA. + eula=true + """); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java new file mode 100644 index 00000000..937a0733 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiVersions.java @@ -0,0 +1,157 @@ +/* + * 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.configuration.fabricapi; + +import java.io.File; +import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.util.download.DownloadException; + +public abstract class FabricApiVersions { + @Inject + protected abstract Project getProject(); + + private final HashMap> moduleVersionCache = new HashMap<>(); + private final HashMap> deprecatedModuleVersionCache = new HashMap<>(); + + public Dependency module(String moduleName, String fabricApiVersion) { + return getProject().getDependencies() + .create(getDependencyNotation(moduleName, fabricApiVersion)); + } + + public String moduleVersion(String moduleName, String fabricApiVersion) { + String moduleVersion = moduleVersionCache + .computeIfAbsent(fabricApiVersion, this::getApiModuleVersions) + .get(moduleName); + + if (moduleVersion == null) { + moduleVersion = deprecatedModuleVersionCache + .computeIfAbsent(fabricApiVersion, this::getDeprecatedApiModuleVersions) + .get(moduleName); + } + + if (moduleVersion == null) { + throw new RuntimeException("Failed to find module version for module: " + moduleName); + } + + return moduleVersion; + } + + private String getDependencyNotation(String moduleName, String fabricApiVersion) { + return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion)); + } + + private Map getApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion); + } + } + + private Map getDeprecatedApiModuleVersions(String fabricApiVersion) { + try { + return populateModuleVersionMap(getDeprecatedApiMavenPom(fabricApiVersion)); + } catch (PomNotFoundException e) { + // Not all fabric-api versions have deprecated modules, return an empty map to cache this fact. + return Collections.emptyMap(); + } + } + + private Map populateModuleVersionMap(File pomFile) { + try { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + Document pom = docBuilder.parse(pomFile); + + Map versionMap = new HashMap<>(); + + NodeList dependencies = ((Element) pom.getElementsByTagName("dependencies").item(0)).getElementsByTagName("dependency"); + + for (int i = 0; i < dependencies.getLength(); i++) { + Element dep = (Element) dependencies.item(i); + Element artifact = (Element) dep.getElementsByTagName("artifactId").item(0); + Element version = (Element) dep.getElementsByTagName("version").item(0); + + if (artifact == null || version == null) { + throw new RuntimeException("Failed to find artifact or version"); + } + + versionMap.put(artifact.getTextContent(), version.getTextContent()); + } + + return versionMap; + } catch (Exception e) { + throw new RuntimeException("Failed to parse " + pomFile.getName(), e); + } + } + + private File getApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api", fabricApiVersion); + } + + private File getDeprecatedApiMavenPom(String fabricApiVersion) throws PomNotFoundException { + return getPom("fabric-api-deprecated", fabricApiVersion); + } + + private File getPom(String name, String version) throws PomNotFoundException { + final LoomGradleExtension extension = LoomGradleExtension.get(getProject()); + final var mavenPom = new File(extension.getFiles().getUserCache(), "fabric-api/%s-%s.pom".formatted(name, version)); + + try { + extension.download(String.format("https://maven.fabricmc.net/net/fabricmc/fabric-api/%2$s/%1$s/%2$s-%1$s.pom", version, name)) + .defaultCache() + .downloadPath(mavenPom.toPath()); + } catch (DownloadException e) { + if (e.getStatusCode() == 404) { + throw new PomNotFoundException(e); + } + + throw new UncheckedIOException("Failed to download maven info to " + mavenPom.getName(), e); + } + + return mavenPom; + } + + private static class PomNotFoundException extends Exception { + PomNotFoundException(Throwable cause) { + super(cause); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java index 4fae7e3b..f3a55e8a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java @@ -110,6 +110,8 @@ public abstract class IdeaSyncTask extends AbstractLoomTask { irc.getExcludedLibraryPaths().set(excludedLibraryPaths); irc.getLaunchFile().set(runConfigFile); configs.add(irc); + + settings.makeRunDir(); } return configs; diff --git a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java index 6a90736e..87fbe05d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModConfigurationRemapper.java @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; import com.google.common.collect.ImmutableMap; @@ -43,6 +44,8 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.FileCollectionDependency; import org.gradle.api.artifacts.MutableVersionConstraint; import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.artifacts.query.ArtifactResolutionQuery; import org.gradle.api.artifacts.result.ArtifactResult; @@ -244,7 +247,10 @@ public class ModConfigurationRemapper { private static List resolveArtifacts(Project project, Configuration configuration) { final List artifacts = new ArrayList<>(); - for (ResolvedArtifact artifact : configuration.getResolvedConfiguration().getResolvedArtifacts()) { + final Set resolvedArtifacts = configuration.getResolvedConfiguration().getResolvedArtifacts(); + downloadAllSources(project, resolvedArtifacts); + + for (ResolvedArtifact artifact : resolvedArtifacts) { final Path sources = findSources(project, artifact); artifacts.add(new ArtifactRef.ResolvedArtifactRef(artifact, sources)); } @@ -271,6 +277,27 @@ public class ModConfigurationRemapper { return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); } + private static void downloadAllSources(Project project, Set resolvedArtifacts) { + if (isCIBuild()) { + return; + } + + final DependencyHandler dependencies = project.getDependencies(); + + List componentIdentifiers = resolvedArtifacts.stream() + .map(ResolvedArtifact::getId) + .map(ComponentArtifactIdentifier::getComponentIdentifier) + .toList(); + + //noinspection unchecked + ArtifactResolutionQuery query = dependencies.createArtifactResolutionQuery() + .forComponents(componentIdentifiers) + .withArtifacts(JvmLibrary.class, SourcesArtifact.class); + + // Run a single query for all of the artifacts, this will allow them to be resolved in parallel before they are queried individually + query.execute(); + } + @Nullable public static Path findSources(Project project, ResolvedArtifact artifact) { if (isCIBuild()) { 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 9d7dc9c4..62722d47 100644 --- a/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/mods/ModProcessor.java @@ -61,6 +61,7 @@ import net.fabricmc.loom.util.LoggerFilter; import net.fabricmc.loom.util.ModPlatform; import net.fabricmc.loom.util.Pair; import net.fabricmc.loom.util.TinyRemapperHelper; +import net.fabricmc.loom.util.TinyRemapperLoggerAdapter; import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.kotlin.KotlinClasspathService; import net.fabricmc.loom.util.kotlin.KotlinRemapperClassloader; @@ -177,7 +178,7 @@ public class ModProcessor { MemoryMappingTree mappings = mappingConfiguration.getMappingsService(project, serviceFactory, mappingOption).getMappingTree(); LoggerFilter.replaceSystemOut(); - TinyRemapper.Builder builder = TinyRemapper.newRemapper() + TinyRemapper.Builder builder = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE) .withKnownIndyBsm(knownIndyBsms) .withMappings(TinyRemapperHelper.create(mappings, fromM, toM, false)) .renameInvalidLocals(false) diff --git a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java index d9d2aac8..e8496be2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/processors/SpecContextImpl.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.gradle.api.Project; @@ -42,6 +43,7 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.attributes.Usage; import org.gradle.api.plugins.JavaPlugin; +import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.RemapConfigurationSettings; @@ -121,27 +123,37 @@ public record SpecContextImpl(List modDependencies, List getCompileRuntimeModsFromRemapConfigs(Project project, Map> fmjCache) { final LoomGradleExtension extension = LoomGradleExtension.get(project); - final List runtimeEntries = extension.getRuntimeRemapConfigurations().stream() + final Set runtimeModIds = extension.getRuntimeRemapConfigurations().stream() .filter(settings -> settings.getApplyDependencyTransforms().get()) .flatMap(resolveArtifacts(project, true)) - .toList(); + .map(modFromZip(fmjCache)) + .filter(Objects::nonNull) + .map(FabricModJson::getId) + .collect(Collectors.toSet()); return extension.getCompileRemapConfigurations().stream() .filter(settings -> settings.getApplyDependencyTransforms().get()) - .flatMap(resolveArtifacts(project, false)) - .filter(runtimeEntries::contains) // Use the intersection of the two configurations. - .map(zipPath -> { - final List list = fmjCache.computeIfAbsent(zipPath.toAbsolutePath().toString(), $ -> { - return FabricModJsonFactory.createFromZipOptional(zipPath) - .map(List::of) - .orElseGet(List::of); - }); - return list.isEmpty() ? null : list.get(0); - }) + .flatMap(resolveArtifacts(project, false))// Use the intersection of the two configurations. + .map(modFromZip(fmjCache)) .filter(Objects::nonNull) + // Only check based on the modid, as there may be differing versions used between the compile and runtime classpath. + // We assume that the version used at runtime will be binary compatible with the version used to compile against. + // It's not perfect but better than silently not supplying the mod, and this could happen with regular API that you compile against anyway. + .filter(fabricModJson -> runtimeModIds.contains(fabricModJson.getId())) .sorted(Comparator.comparing(FabricModJson::getId)); } + private static Function modFromZip(Map> fmjCache) { + return zipPath -> { + final List list = fmjCache.computeIfAbsent(zipPath.toAbsolutePath().toString(), $ -> { + return FabricModJsonFactory.createFromZipOptional(zipPath) + .map(List::of) + .orElseGet(List::of); + }); + return list.isEmpty() ? null : list.get(0); + }; + } + private static Function> resolveArtifacts(Project project, boolean runtime) { final Usage usage = project.getObjects().named(Usage.class, runtime ? Usage.JAVA_RUNTIME : Usage.JAVA_API); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java index d198baf9..f1c6b997 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java @@ -38,7 +38,7 @@ import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; */ public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappingsProvider { private static final String HEADER_OFFICIAL_MERGED = "tiny\t2\t0\tofficial\tintermediary"; - private static final String HEADER_OFFICIAL_LEGACY_MERGED = "tiny\t2\t0\tintermediary\tclientOfficial\tserverOfficial\t"; + private static final String HEADER_OFFICIAL_LEGACY_MERGED = "tiny\t2\t0\tintermediary\tclientOfficial\tserverOfficial"; @Override public void provide(Path tinyMappings) throws IOException { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java index de9a80fd..1f6c8404 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java @@ -109,12 +109,7 @@ public class MinecraftLibraryProvider { private void provideServerLibraries() { final BundleMetadata serverBundleMetadata = minecraftProvider.getServerBundleMetadata(); - - if (serverBundleMetadata == null) { - return; - } - - final List libraries = MinecraftLibraryHelper.getServerLibraries(serverBundleMetadata); + final List libraries = serverBundleMetadata != null ? MinecraftLibraryHelper.getServerLibraries(serverBundleMetadata) : Collections.emptyList(); final List processLibraries = processLibraries(libraries); processLibraries.forEach(this::applyServerLibrary); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 684527e9..f58e6e7d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -32,6 +32,7 @@ import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.TinyRemapperLoggerAdapter; import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; @@ -98,7 +99,7 @@ public abstract class SingleJarMinecraftProvider extends MinecraftProvider { TinyRemapper remapper = null; try { - remapper = TinyRemapper.newRemapper().build(); + remapper = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE).build(); Files.deleteIfExists(minecraftEnvOnlyJar); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index 3c49e258..243e8100 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -39,6 +39,8 @@ import java.util.function.Function; import dev.architectury.loom.util.MappingOption; import org.gradle.api.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; @@ -65,6 +67,8 @@ import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; public abstract class AbstractMappedMinecraftProvider implements MappedMinecraftProvider.ProviderImpl { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMappedMinecraftProvider.class); + protected final M minecraftProvider; private final Project project; protected final LoomGradleExtension extension; @@ -77,8 +81,18 @@ public abstract class AbstractMappedMinecraftProvider getRemappedJars(); + /** + * @return A list of output jars that this provider generates + */ + public List getOutputJars() { + return getRemappedJars(); + } + // Returns a list of MinecraftJar.Type's that this provider exports to be used as a dependency public List getDependencyTypes() { return Collections.emptyList(); @@ -94,7 +108,7 @@ public abstract class AbstractMappedMinecraftProvider minecraftJars) { - for (MinecraftJar minecraftJar : minecraftJars) { - if (!Files.exists(getBackupJarPath(minecraftJar))) { - return false; - } - } - - return true; - } - protected void createBackupJars(List minecraftJars) throws IOException { for (MinecraftJar minecraftJar : minecraftJars) { Files.copy(minecraftJar.getPath(), getBackupJarPath(minecraftJar), StandardCopyOption.REPLACE_EXISTING); @@ -202,11 +206,16 @@ public abstract class AbstractMappedMinecraftProvider remappedJars) { - for (RemappedJars remappedJar : remappedJars) { - if (!getMavenHelper(remappedJar.type()).exists(null)) { - return false; - } + protected boolean shouldRefreshOutputs(ProvideContext context) { + if (context.refreshOutputs()) { + LOGGER.info("Refreshing outputs for mapped jar, as refresh outputs was requested"); + return true; + } + + final List outputJars = getOutputJars(); + + if (outputJars.isEmpty()) { + throw new IllegalStateException("No output jars provided"); } // Architectury: regenerate jars if patches have changed. @@ -214,7 +223,22 @@ public abstract class AbstractMappedMinecraftProvider remappedJars, ConfigContext configContext) throws IOException { @@ -306,7 +330,15 @@ public abstract class AbstractMappedMinecraftProvider provide(ProvideContext context) throws Exception { + final List minecraftJars = List.of(getMergedJar()); + + // this check must be done before the client and server impls are provided + // because the merging only needs to happen if the remapping step is run + final boolean refreshOutputs = client.shouldRefreshOutputs(context) + || server.shouldRefreshOutputs(context) + || this.shouldRefreshOutputs(context); + // Map the client and server jars separately server.provide(context); client.provide(context); - // then merge them - MergedMinecraftProvider.mergeJars( - client.getEnvOnlyJar().toFile(), - server.getEnvOnlyJar().toFile(), - getMergedJar().toFile() - ); + if (refreshOutputs) { + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); - return List.of(getMergedJar()); + createBackupJars(minecraftJars); + } + + return minecraftJars; } @Override @@ -98,6 +110,13 @@ public abstract sealed class IntermediaryMinecraftProvider getOutputJars() { + return List.of( + new SimpleOutputJar(getMergedJar()) + ); + } + @Override public List getDependencyTypes() { return List.of(MinecraftJar.Type.MERGED); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java index f2753609..02077a98 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java @@ -85,17 +85,28 @@ public abstract class NamedMinecraftProvider extend @Override public List provide(ProvideContext context) throws Exception { final ProvideContext childContext = context.withApplyDependencies(false); + final List minecraftJars = List.of(getMergedJar()); + + // this check must be done before the client and server impls are provided + // because the merging only needs to happen if the remapping step is run + final boolean refreshOutputs = client.shouldRefreshOutputs(childContext) + || server.shouldRefreshOutputs(childContext) + || this.shouldRefreshOutputs(childContext); // Map the client and server jars separately server.provide(childContext); client.provide(childContext); - // then merge them - MergedMinecraftProvider.mergeJars( - client.getEnvOnlyJar().toFile(), - server.getEnvOnlyJar().toFile(), - getMergedJar().toFile() - ); + if (refreshOutputs) { + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); + + createBackupJars(minecraftJars); + } getMavenHelper(MinecraftJar.Type.MERGED).savePom(); @@ -106,7 +117,7 @@ public abstract class NamedMinecraftProvider extend ); } - return List.of(getMergedJar()); + return minecraftJars; } @Override @@ -115,6 +126,13 @@ public abstract class NamedMinecraftProvider extend throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars"); } + @Override + public List getOutputJars() { + return List.of( + new SimpleOutputJar(getMergedJar()) + ); + } + @Override public List getDependencyTypes() { return List.of(MinecraftJar.Type.MERGED); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java index 53604c62..ef52bdba 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java @@ -65,7 +65,7 @@ public abstract class ProcessedNamedMinecraftProvider getOutputJars() { + return parentMinecraftProvider.getMinecraftJars().stream() + .map(this::getProcessedJar) + .map(SimpleOutputJar::new) + .toList(); + } + @Override public MavenScope getMavenScope() { return MavenScope.LOCAL; diff --git a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java index 9b3ab78f..7eca471d 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java +++ b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java @@ -106,20 +106,16 @@ public record LineNumberRemapper(ClassLineNumbers lineNumbers) { return new MethodVisitor(api, super.visitMethod(access, name, descriptor, signature, exceptions)) { @Override public void visitLineNumber(int line, Label start) { - int tLine = line; - - if (tLine <= 0) { + if (line <= 0) { super.visitLineNumber(line, start); - } else if (tLine >= lineNumbers.maxLine()) { + } else if (line >= lineNumbers.maxLine()) { super.visitLineNumber(lineNumbers.maxLineDest(), start); } else { - Integer matchedLine = null; + Integer matchedLine = lineNumbers.lineMap().get(line); - while (tLine <= lineNumbers.maxLine() && ((matchedLine = lineNumbers.lineMap().get(tLine)) == null)) { - tLine++; + if (matchedLine != null) { + super.visitLineNumber(matchedLine, start); } - - super.visitLineNumber(matchedLine != null ? matchedLine : lineNumbers.maxLineDest(), start); } } }; diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java index 666ae2c2..4fe53faa 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2024 FabricMC + * Copyright (c) 2024-2025 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 @@ -45,6 +45,34 @@ import net.fabricmc.loom.util.Checksum; public record ClassEntry(String name, List innerClasses, List superClasses) { private static final Logger LOGGER = LoggerFactory.getLogger(ClassEntry.class); + public ClassEntry { + if (!name.endsWith(".class")) { + throw new IllegalArgumentException("Class name must end with '.class': " + name); + } + + if (!name.contains("/")) { + throw new IllegalArgumentException("Class name must be in a package: " + name); + } + + String className = name.replace(".class", ""); + + for (String innerClass : innerClasses) { + if (!innerClass.endsWith(".class")) { + throw new IllegalArgumentException("Inner class name must end with '.class': " + name); + } + + if (!innerClass.startsWith(className)) { + throw new IllegalArgumentException("Inner class (" + innerClass + ") does not have the parent class name as a prefix: " + name); + } + } + + for (String superClass : superClasses) { + if (!superClass.endsWith(".class")) { + throw new IllegalArgumentException("Super class name must end with '.class': " + superClass); + } + } + } + /** * Copy the class and its inner classes to the target root. * @param sourceRoot The root of the source jar @@ -55,13 +83,18 @@ public record ClassEntry(String name, List innerClasses, List su public void copyTo(Path sourceRoot, Path targetRoot) throws IOException { Path targetPath = targetRoot.resolve(name); Files.createDirectories(targetPath.getParent()); - Files.copy(sourceRoot.resolve(name), targetPath); + copy(sourceRoot.resolve(name), targetPath); for (String innerClass : innerClasses) { - Files.copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass)); + copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass)); } } + private void copy(Path source, Path target) throws IOException { + LOGGER.debug("Copying class entry `{}` from `{}` to `{}`", name, source, target); + Files.copy(source, target); + } + /** * Hash the class and its inner classes using sha256. * @param root The root of the jar @@ -95,7 +128,7 @@ public record ClassEntry(String name, List innerClasses, List su joiner.add(selfHash); for (String superClass : superClasses) { - final String superHash = hashes.get(superClass + ".class"); + final String superHash = hashes.get(superClass); if (superHash != null) { joiner.add(superHash); diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java index a3931281..ad2e16b5 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2024 FabricMC + * Copyright (c) 2024-2025 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 @@ -194,11 +194,14 @@ public final class JarWalker { List parentClasses = new ArrayList<>(); String superName = reader.getSuperName(); - if (superName != null) { - parentClasses.add(superName); + if (superName != null && !superName.equals("java/lang/Object")) { + parentClasses.add(superName + ".class"); + } + + for (String iface : reader.getInterfaces()) { + parentClasses.add(iface + ".class"); } - Collections.addAll(parentClasses, reader.getInterfaces()); return Collections.unmodifiableList(parentClasses); } catch (IOException e) { throw new UncheckedIOException("Failed to read class file: " + classFile, e); diff --git a/src/main/java/net/fabricmc/loom/extension/MixinExtension.java b/src/main/java/net/fabricmc/loom/extension/MixinExtension.java index 40795d54..e7c428fe 100644 --- a/src/main/java/net/fabricmc/loom/extension/MixinExtension.java +++ b/src/main/java/net/fabricmc/loom/extension/MixinExtension.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2016-2012 FabricMC + * Copyright (c) 2016-2025 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 @@ -36,6 +36,7 @@ import org.gradle.api.plugins.ExtraPropertiesExtension; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.util.PatternSet; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -81,7 +82,7 @@ public interface MixinExtension extends MixinExtensionAPI { Stream getApConfigurationsStream(Function getApConfigNameFunc); @NotNull - Stream> getInvokerTasksStream(String compileTaskLanguage); + Stream>> getInvokerTasksStream(String compileTaskLanguage, Class taskType); @NotNull @Input diff --git a/src/main/java/net/fabricmc/loom/extension/MixinExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/MixinExtensionImpl.java index 796b6e35..5a2b0601 100644 --- a/src/main/java/net/fabricmc/loom/extension/MixinExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/MixinExtensionImpl.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2021-2022 FabricMC + * Copyright (c) 2021-2025 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 @@ -44,6 +44,7 @@ import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.util.PatternSet; import org.jetbrains.annotations.NotNull; @@ -57,6 +58,7 @@ public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinEx this.isDefault = true; this.defaultRefmapName = project.getObjects().property(String.class) .convention(project.provider(this::getDefaultMixinRefmapName)); + this.defaultRefmapName.finalizeValueOnRead(); } @Override @@ -107,11 +109,11 @@ public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinEx @Override @NotNull - public Stream> getInvokerTasksStream(String compileTaskLanguage) { + public Stream>> getInvokerTasksStream(String compileTaskLanguage, Class taskType) { return getMixinSourceSetsStream() .flatMap(sourceSet -> { try { - Task task = project.getTasks().getByName(sourceSet.getCompileTaskName(compileTaskLanguage)); + TaskProvider task = project.getTasks().named(sourceSet.getCompileTaskName(compileTaskLanguage), taskType); return Stream.of(new AbstractMap.SimpleEntry<>(sourceSet, task)); } catch (UnknownTaskException ignored) { return Stream.empty(); @@ -140,7 +142,7 @@ public class MixinExtensionImpl extends MixinExtensionApiImpl implements MixinEx if (sourceSet.getName().equals("main")) { add(sourceSet); } else { - add(sourceSet, sourceSet.getName() + "-" + getDefaultRefmapName().get()); + add(sourceSet, getDefaultRefmapName().map(defaultRefmapName -> "%s-%s".formatted(sourceSet.getName(), defaultRefmapName)), x -> { }); } }); } diff --git a/src/main/java/net/fabricmc/loom/task/DownloadTask.java b/src/main/java/net/fabricmc/loom/task/DownloadTask.java new file mode 100644 index 00000000..beddbe34 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/DownloadTask.java @@ -0,0 +1,145 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task; + +import java.net.URISyntaxException; +import java.time.Duration; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; +import org.jetbrains.annotations.ApiStatus; + +import net.fabricmc.loom.util.ExceptionUtil; +import net.fabricmc.loom.util.download.Download; +import net.fabricmc.loom.util.download.DownloadBuilder; +import net.fabricmc.loom.util.download.DownloadException; + +/** + * A general purpose task for downloading files from a URL, using the loom {@link Download} utility. + */ +public abstract class DownloadTask extends DefaultTask { + /** + * The URL to download the file from. + */ + @Input + public abstract Property getUrl(); + + /** + * The expected SHA-1 hash of the downloaded file. + */ + @Optional + @Input + public abstract Property getSha1(); + + /** + * The maximum age of the downloaded file in days. When not provided the downloaded file will never be considered stale. + */ + @Optional + @Input + public abstract Property getMaxAge(); + + /** + * The file to download to. + */ + @OutputFile + public abstract RegularFileProperty getOutput(); + + // Internal stuff: + + @ApiStatus.Internal + @Input + protected abstract Property getIsOffline(); + + @Inject + protected abstract WorkerExecutor getWorkerExecutor(); + + @Inject + public DownloadTask() { + getIsOffline().set(getProject().getGradle().getStartParameter().isOffline()); + } + + @TaskAction + public void run() { + final WorkQueue workQueue = getWorkerExecutor().noIsolation(); + + workQueue.submit(DownloadAction.class, params -> { + params.getUrl().set(getUrl()); + params.getSha1().set(getSha1()); + params.getMaxAge().set(getMaxAge()); + params.getOutputFile().set(getOutput()); + params.getIsOffline().set(getIsOffline()); + }); + } + + public interface DownloadWorkParameters extends WorkParameters { + Property getUrl(); + Property getSha1(); + Property getMaxAge(); + RegularFileProperty getOutputFile(); + Property getIsOffline(); + } + + public abstract static class DownloadAction implements WorkAction { + @Override + public void execute() { + DownloadBuilder builder; + + try { + builder = Download.create(getParameters().getUrl().get()).defaultCache(); + } catch (URISyntaxException e) { + throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Invalid URL", e); + } + + if (getParameters().getMaxAge().isPresent()) { + builder.maxAge(getParameters().getMaxAge().get()); + } + + if (getParameters().getSha1().isPresent()) { + builder.sha1(getParameters().getSha1().get()); + } + + if (getParameters().getIsOffline().get()) { + builder.offline(); + } + + try { + builder.downloadPath(getParameters().getOutputFile().get().getAsFile().toPath()); + } catch (DownloadException e) { + throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to download file", e); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java index 6684e5ce..398bba75 100644 --- a/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenVsCodeProjectTask.java @@ -118,7 +118,8 @@ public abstract class GenVsCodeProjectTask extends AbstractLoomTask { } for (VsCodeConfiguration configuration : getLaunchConfigurations().get()) { - final JsonElement configurationJson = LoomGradlePlugin.GSON.toJsonTree(configuration); + JsonObject configurationJson = LoomGradlePlugin.GSON.toJsonTree(configuration).getAsJsonObject(); + configurationJson.remove("runDir"); final List toRemove = new LinkedList<>(); @@ -161,11 +162,14 @@ public abstract class GenVsCodeProjectTask extends AbstractLoomTask { String projectName, String runDir) implements Serializable { public static VsCodeConfiguration fromRunConfig(Project project, RunConfig runConfig) { + Path rootPath = project.getRootDir().toPath(); + Path projectPath = project.getProjectDir().toPath(); + String relativeRunDir = rootPath.relativize(projectPath).resolve(runConfig.runDir).toString(); return new VsCodeConfiguration( "java", runConfig.configName, "launch", - "${workspaceFolder}/" + runConfig.runDir, + "${workspaceFolder}/" + relativeRunDir, "integratedTerminal", false, runConfig.mainClass, @@ -173,7 +177,7 @@ public abstract class GenVsCodeProjectTask extends AbstractLoomTask { RunConfig.joinArguments(runConfig.programArgs), new HashMap<>(runConfig.environmentVariables), runConfig.projectName, - project.getProjectDir().toPath().resolve(runConfig.runDir).toAbsolutePath().toString() + rootPath.resolve(relativeRunDir).toAbsolutePath().toString() ); } } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index d86a4f12..036b8e77 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -66,11 +66,11 @@ import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.UntrackedTask; import org.gradle.api.tasks.options.Option; import org.gradle.internal.logging.progress.ProgressLoggerFactory; import org.gradle.process.ExecOperations; import org.gradle.process.ExecResult; -import org.gradle.work.DisableCachingByDefault; import org.gradle.workers.WorkAction; import org.gradle.workers.WorkParameters; import org.gradle.workers.WorkQueue; @@ -108,7 +108,7 @@ import net.fabricmc.loom.util.ipc.IPCServer; import net.fabricmc.loom.util.service.ScopedServiceFactory; import net.fabricmc.mappingio.tree.MemoryMappingTree; -@DisableCachingByDefault +@UntrackedTask(because = "Manually invoked, has internal caching") public abstract class GenerateSourcesTask extends AbstractLoomTask { private static final String CACHE_VERSION = "v1"; private final DecompilerOptions decompilerOptions; @@ -241,7 +241,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { throw new IllegalStateException("Input minecraft jar not found: " + getInputJarName().get()); })); - getOutputs().upToDateWhen((o) -> false); getClasspath().from(decompilerOptions.getClasspath()).finalizeValueOnRead(); dependsOn(decompilerOptions.getClasspath().getBuiltBy()); diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index 44d1b5f6..97d4df71 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -54,7 +54,6 @@ public abstract class LoomTasks implements Runnable { public void run() { getTasks().register("migrateMappings", MigrateMappingsTask.class, t -> { t.setDescription("Migrates mappings to a new version."); - t.getOutputs().upToDateWhen(o -> false); }); var generateLog4jConfig = getTasks().register("generateLog4jConfig", GenerateLog4jConfigTask.class, t -> { @@ -126,7 +125,7 @@ public abstract class LoomTasks implements Runnable { }); } - private static String getRunConfigTaskName(RunConfigSettings config) { + public static String getRunConfigTaskName(RunConfigSettings config) { String configName = config.getName(); return "run" + configName.substring(0, 1).toUpperCase() + configName.substring(1); } diff --git a/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java b/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java index 123a5404..0024570d 100644 --- a/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java +++ b/src/main/java/net/fabricmc/loom/task/MigrateMappingsTask.java @@ -31,13 +31,13 @@ import org.gradle.api.tasks.InputDirectory; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.UntrackedTask; import org.gradle.api.tasks.options.Option; -import org.gradle.work.DisableCachingByDefault; import net.fabricmc.loom.task.service.MigrateMappingsService; import net.fabricmc.loom.util.service.ScopedServiceFactory; -@DisableCachingByDefault(because = "Always rerun this task.") +@UntrackedTask(because = "Always rerun this task.") public abstract class MigrateMappingsTask extends AbstractLoomTask { @Input @Option(option = "mappings", description = "Target mappings") diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index b8dd8c5a..6ab55d72 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -159,7 +159,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { getInjectAccessWidener().convention(false); TaskProvider processIncludeJars = getProject().getTasks().named(Constants.Task.PROCESS_INCLUDE_JARS, NestableJarGenerationTask.class); - getNestedJars().from(getProject().fileTree(processIncludeJars.get().getOutputDirectory())); + getNestedJars().from(processIncludeJars.map(task -> getProject().fileTree(task.getOutputDirectory()))); getNestedJars().builtBy(processIncludeJars); getUseMixinAP().set(LoomGradleExtension.get(getProject()).getMixin().getUseLegacyMixinAp()); diff --git a/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java b/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java index 31c8c0f2..bc9bda43 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java +++ b/src/main/java/net/fabricmc/loom/task/RemapTaskConfiguration.java @@ -84,7 +84,7 @@ public abstract class RemapTaskConfiguration implements Runnable { }); Action remapJarTaskAction = task -> { - final AbstractArchiveTask jarTask = getTasks().named(JavaPlugin.JAR_TASK_NAME, AbstractArchiveTask.class).get(); + final TaskProvider jarTask = getTasks().named(JavaPlugin.JAR_TASK_NAME, AbstractArchiveTask.class); // Basic task setup task.dependsOn(jarTask); @@ -94,7 +94,7 @@ public abstract class RemapTaskConfiguration implements Runnable { getArtifacts().add(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME, task); // Setup the input file and the nested deps - task.getInputFile().convention(jarTask.getArchiveFile()); + task.getInputFile().convention(jarTask.flatMap(AbstractArchiveTask::getArchiveFile)); task.dependsOn(getTasks().named(JavaPlugin.JAR_TASK_NAME)); task.getIncludesClientOnlyClasses().set(getProject().provider(extension::areEnvironmentSourceSetsSplit)); }; diff --git a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java index 9b4f3c59..d768ff95 100644 --- a/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java +++ b/src/main/java/net/fabricmc/loom/task/ValidateAccessWidenerTask.java @@ -46,6 +46,7 @@ import net.fabricmc.accesswidener.AccessWidenerReader; import net.fabricmc.accesswidener.AccessWidenerVisitor; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.util.TinyRemapperLoggerAdapter; import net.fabricmc.tinyremapper.TinyRemapper; import net.fabricmc.tinyremapper.api.TrEnvironment; @@ -70,7 +71,7 @@ public abstract class ValidateAccessWidenerTask extends DefaultTask { @TaskAction public void run() { - final TinyRemapper tinyRemapper = TinyRemapper.newRemapper().build(); + final TinyRemapper tinyRemapper = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE).build(); for (File file : getTargetJars().getFiles()) { tinyRemapper.readClassPath(file.toPath()); diff --git a/src/main/java/net/fabricmc/loom/task/prod/AbstractProductionRunTask.java b/src/main/java/net/fabricmc/loom/task/prod/AbstractProductionRunTask.java new file mode 100644 index 00000000..55a28789 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/prod/AbstractProductionRunTask.java @@ -0,0 +1,207 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task.prod; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.inject.Inject; + +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.UntrackedTask; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.gradle.process.ExecOperations; +import org.gradle.process.ExecResult; +import org.gradle.process.ExecSpec; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.configuration.InstallerData; +import net.fabricmc.loom.task.AbstractLoomTask; +import net.fabricmc.loom.task.RemapTaskConfiguration; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.gradle.GradleUtils; + +/** + * This is the base task for running the game in a "production" like environment. Using intermediary names, and not enabling development only features. + * + *

Do not use this task directly, use {@link ClientProductionRunTask} or {@link ServerProductionRunTask} instead. + */ +@ApiStatus.Experimental +@UntrackedTask(because = "Always rerun this task.") +public abstract sealed class AbstractProductionRunTask extends AbstractLoomTask permits ClientProductionRunTask, ServerProductionRunTask { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractProductionRunTask.class); + + /** + * A collection of mods that will be used when running the game. The mods must be remapped to run with intermediary names. + * + *

By default this includes the remapped jar. + */ + @Classpath + public abstract ConfigurableFileCollection getMods(); + + /** + * A list of additional JVM arguments to pass to the game. + */ + @Input + public abstract ListProperty getJvmArgs(); + + /** + * A list of additional program arguments to pass to the game. + */ + @Input + public abstract ListProperty getProgramArgs(); + + /** + * The directory to run the game in. + */ + @OutputDirectory + public abstract DirectoryProperty getRunDir(); + + /** + * The {@link JavaLauncher} to use when running the game, this can be used to specify a specific Java version to use. + * + *

See: Java Toolchains + * @return + */ + @Nested + public abstract Property getJavaLauncher(); + + // Internal options + @ApiStatus.Internal + @Classpath + protected abstract ConfigurableFileCollection getClasspath(); + + @ApiStatus.Internal + @Input + protected abstract Property getMainClass(); + + @Inject + protected abstract ExecOperations getExecOperations(); + + @Inject + protected abstract JavaToolchainService getJavaToolchainService(); + + @Inject + public AbstractProductionRunTask() { + JavaToolchainSpec defaultToolchain = getProject().getExtensions().getByType(JavaPluginExtension.class).getToolchain(); + getJavaLauncher().convention(getJavaToolchainService().launcherFor(defaultToolchain)); + getRunDir().convention(getProject().getLayout().getProjectDirectory().dir("run")); + + if (!GradleUtils.getBooleanProperty(getProject(), Constants.Properties.DONT_REMAP)) { + getMods().from(getProject().getTasks().named(RemapTaskConfiguration.REMAP_JAR_TASK_NAME)); + } + + getMods().from(getProject().getConfigurations().named(Constants.Configurations.PRODUCTION_RUNTIME_MODS)); + } + + @TaskAction + public void run() throws IOException { + Files.createDirectories(getRunDir().get().getAsFile().toPath()); + + ExecResult result = getExecOperations().exec(exec -> { + configureCommand(exec); + configureJvmArgs(exec); + configureClasspath(exec); + configureMainClass(exec); + configureProgramArgs(exec); + + exec.setWorkingDir(getRunDir()); + + LOGGER.debug("Running command: {}", exec.getCommandLine()); + }); + result.assertNormalExitValue(); + } + + protected void configureCommand(ExecSpec exec) { + exec.commandLine(getJavaLauncher().get().getExecutablePath()); + } + + protected void configureJvmArgs(ExecSpec exec) { + exec.args(getJvmArgs().get()); + exec.args("-Dfabric.addMods=" + joinFiles(getMods().getFiles().stream())); + } + + protected Stream streamClasspath() { + return getClasspath().getFiles().stream(); + } + + protected void configureClasspath(ExecSpec exec) { + exec.args("-cp"); + exec.args(joinFiles(streamClasspath())); + } + + protected void configureMainClass(ExecSpec exec) { + exec.args(getMainClass().get()); + } + + protected void configureProgramArgs(ExecSpec exec) { + exec.args(getProgramArgs().get()); + } + + @Internal + protected Provider getProjectLoaderVersion() { + return getProject().provider(() -> { + InstallerData installerData = getExtension().getInstallerData(); + + if (installerData == null) { + return null; + } + + return installerData.version(); + }); + } + + protected Provider detachedConfigurationProvider(String mavenNotation, Provider versionProvider) { + return versionProvider.map(version -> { + Dependency serverLauncher = getProject().getDependencies().create(mavenNotation.formatted(version)); + return getProject().getConfigurations().detachedConfiguration(serverLauncher); + }); + } + + private static String joinFiles(Stream stream) { + return stream.map(File::getAbsolutePath) + .collect(Collectors.joining(File.pathSeparator)); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/prod/ClientProductionRunTask.java b/src/main/java/net/fabricmc/loom/task/prod/ClientProductionRunTask.java new file mode 100644 index 00000000..6a8ff514 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/prod/ClientProductionRunTask.java @@ -0,0 +1,157 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task.prod; + +import java.io.File; +import java.io.IOException; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.process.ExecSpec; +import org.jetbrains.annotations.ApiStatus; + +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.Platform; + +/** + * A task that runs the Minecraft client in a similar way to a production launcher. You must manually register a task of this type to use it. + */ +@ApiStatus.Experimental +public abstract non-sealed class ClientProductionRunTask extends AbstractProductionRunTask { + /** + * Whether to use XVFB to run the game, using a virtual framebuffer. This is useful for CI environments that don't have a display server. + * + *

Defaults to true only on Linux and when the "CI" environment variable is set. + * + *

XVFB must be installed, on Debian-based systems you can install it with: apt install -y xvfb + */ + @Input + public abstract Property getUseXVFB(); + + @Nested + @Optional + public abstract Property getTracyCapture(); + + /** + * Configures the tracy profiler to run alongside the game. See @{@link TracyCapture} for more information. + * + * @param action The configuration action. + */ + public void tracy(Action action) { + getTracyCapture().set(getProject().getObjects().newInstance(TracyCapture.class)); + getTracyCapture().finalizeValue(); + action.execute(getTracyCapture().get()); + } + + // Internal options + @Input + protected abstract Property getAssetsIndex(); + + @InputFiles + protected abstract DirectoryProperty getAssetsDir(); + + @Inject + public ClientProductionRunTask() { + getUseXVFB().convention(getProject().getProviders().environmentVariable("CI") + .map(value -> Platform.CURRENT.getOperatingSystem().isLinux()) + .orElse(false) + ); + + getAssetsIndex().set(getExtension().getMinecraftVersion() + .map(minecraftVersion -> getExtension() + .getMinecraftProvider() + .getVersionInfo() + .assetIndex() + .fabricId(minecraftVersion) + ) + ); + getAssetsDir().set(new File(getExtension().getFiles().getUserCache(), "assets")); + getMainClass().convention("net.fabricmc.loader.impl.launch.knot.KnotClient"); + + getClasspath().from(getExtension().getMinecraftProvider().getMinecraftClientJar()); + getClasspath().from(detachedConfigurationProvider("net.fabricmc:fabric-loader:%s", getProjectLoaderVersion())); + getClasspath().from(detachedConfigurationProvider("net.fabricmc:intermediary:%s", getExtension().getMinecraftVersion())); + getClasspath().from(getProject().getConfigurations().named(Constants.Configurations.MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES)); + + dependsOn("downloadAssets"); + } + + @Override + public void run() throws IOException { + if (getTracyCapture().isPresent()) { + getTracyCapture().get().runWithTracy(super::run); + return; + } + + super.run(); + } + + @Override + protected void configureCommand(ExecSpec exec) { + if (getUseXVFB().get()) { + if (!Platform.CURRENT.getOperatingSystem().isLinux()) { + throw new UnsupportedOperationException("XVFB is only supported on Linux"); + } + + exec.commandLine("/usr/bin/xvfb-run"); + exec.args("-a", getJavaLauncher().get().getExecutablePath()); + + return; + } + + super.configureCommand(exec); + } + + @Override + protected void configureJvmArgs(ExecSpec exec) { + super.configureJvmArgs(exec); + + if (Platform.CURRENT.getOperatingSystem().isMacOS()) { + exec.args("-XstartOnFirstThread"); + } + } + + @Override + protected void configureProgramArgs(ExecSpec exec) { + super.configureProgramArgs(exec); + + exec.args( + "--assetIndex", getAssetsIndex().get(), + "--assetsDir", getAssetsDir().get().getAsFile().getAbsolutePath(), + "--gameDir", getRunDir().get().getAsFile().getAbsolutePath() + ); + + if (getTracyCapture().isPresent()) { + exec.args("--tracy"); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/task/prod/ServerProductionRunTask.java b/src/main/java/net/fabricmc/loom/task/prod/ServerProductionRunTask.java new file mode 100644 index 00000000..36a925ca --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/prod/ServerProductionRunTask.java @@ -0,0 +1,108 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task.prod; + +import java.io.File; +import java.io.IOException; +import java.util.stream.Stream; + +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.OutputFile; +import org.jetbrains.annotations.ApiStatus; + +import net.fabricmc.loom.util.LoomVersions; +import net.fabricmc.loom.util.ZipUtils; + +/** + * A task that runs the server using the production server launcher. You must manually register a task of this type to use it. + */ +@ApiStatus.Experimental +public abstract non-sealed class ServerProductionRunTask extends AbstractProductionRunTask { + /** + * The version of Fabric Loader to use. + * + *

Defaults to the version of Fabric Loader that the project is using. + */ + @Input + public abstract Property getLoaderVersion(); + + /** + * The version of Minecraft to use. + * + *

Defaults to the version of Minecraft that the project is using. + */ + @Input + public abstract Property getMinecraftVersion(); + + /** + * The version of the Fabric Installer to use. + * + *

Defaults to a version provided by Loom. + */ + @Input + public abstract Property getInstallerVersion(); + + // Internal options + + @ApiStatus.Internal + @OutputFile + public abstract RegularFileProperty getInstallPropertiesJar(); + + @Inject + public ServerProductionRunTask() { + getLoaderVersion().convention(getProjectLoaderVersion()); + getMinecraftVersion().convention(getExtension().getMinecraftVersion()); + getInstallPropertiesJar().convention(getProject().getLayout().getBuildDirectory().file("server_properties.jar")); + getInstallerVersion().convention(LoomVersions.FABRIC_INSTALLER.version()); + + getMainClass().convention("net.fabricmc.installer.ServerLauncher"); + getClasspath().from(detachedConfigurationProvider("net.fabricmc:fabric-installer:%s:server", getInstallerVersion())); + + getProgramArgs().add("nogui"); + } + + @Override + public void run() throws IOException { + ZipUtils.add( + getInstallPropertiesJar().get().getAsFile().toPath(), + "install.properties", + "fabric-loader-version=%s\ngame-version=%s".formatted(getLoaderVersion().get(), getMinecraftVersion().get()) + ); + + super.run(); + } + + @Override + protected Stream streamClasspath() { + return Stream.concat( + super.streamClasspath(), + Stream.of(getInstallPropertiesJar().get().getAsFile()) + ); + } +} diff --git a/src/main/java/net/fabricmc/loom/task/prod/TracyCapture.java b/src/main/java/net/fabricmc/loom/task/prod/TracyCapture.java new file mode 100644 index 00000000..caff218e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/task/prod/TracyCapture.java @@ -0,0 +1,159 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.task.prod; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.function.Consumer; + +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.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.ExceptionUtil; + +public abstract class TracyCapture { + private static final Logger LOGGER = LoggerFactory.getLogger(TracyCapture.class); + + /** + * The path to the tracy-capture executable. + */ + @InputFile + @Optional + public abstract RegularFileProperty getTracyCapture(); + + /** + * The maximum number of seconds to wait for tracy-capture to stop on its own before killing it. + * + *

Defaults to 10 seconds. + */ + @Input + public abstract Property getMaxShutdownWaitSeconds(); + + /** + * The path to the output file. + */ + @OutputFile + @Optional + public abstract RegularFileProperty getOutput(); + + @Inject + public TracyCapture() { + getMaxShutdownWaitSeconds().convention(10); + } + + void runWithTracy(IORunnable runnable) throws IOException { + TracyCaptureRunner tracyCaptureRunner = createRunner(); + + boolean success = false; + + try { + runnable.run(); + success = true; + } finally { + try { + tracyCaptureRunner.close(); + } catch (Exception e) { + if (success) { + //noinspection ThrowFromFinallyBlock + throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to stop tracy capture", e); + } + } + } + } + + private TracyCaptureRunner createRunner() throws IOException { + File tracyCapture = getTracyCapture().getAsFile().get(); + File output = getOutput().getAsFile().get(); + + ProcessBuilder builder = new ProcessBuilder() + .command(tracyCapture.getAbsolutePath(), "-a", "127.0.0.1", "-f", "-o", output.getAbsolutePath()); + Process process = builder.start(); + + captureLog(process.getInputStream(), LOGGER::info); + captureLog(process.getErrorStream(), LOGGER::error); + + LOGGER.info("Tracy capture started"); + + return new TracyCaptureRunner(process, getMaxShutdownWaitSeconds().get()); + } + + private record TracyCaptureRunner(Process process, int shutdownWait) implements AutoCloseable { + @Override + public void close() throws Exception { + // Wait x seconds for tracy to stop on its own + // This allows time for tracy to save the profile to disk + for (int i = 0; i < shutdownWait; i++) { + if (!process.isAlive()) { + break; + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + // If it's still running, kill it + if (process.isAlive()) { + LOGGER.error("Tracy capture did not stop on its own, killing it"); + process.destroy(); + process.waitFor(); + } + + int exitCode = process.exitValue(); + + if (exitCode != 0) { + throw new RuntimeException("Tracy capture failed with exit code " + exitCode); + } + } + } + + private static void captureLog(InputStream inputStream, Consumer lineConsumer) { + new Thread(() -> { + try { + new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(lineConsumer); + } catch (Exception e) { + // Don't really care, this will happen when the stream is closed + } + }).start(); + } + + @FunctionalInterface + public interface IORunnable { + void run() throws IOException; + } +} diff --git a/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java b/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java index 13b197f7..f860b267 100644 --- a/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java +++ b/src/main/java/net/fabricmc/loom/task/service/SourceRemapperService.java @@ -28,15 +28,19 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicInteger; import org.cadixdev.mercury.Mercury; import org.cadixdev.mercury.remapper.MercuryRemapper; +import org.gradle.api.JavaVersion; +import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.compile.JavaCompile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,7 +73,7 @@ public final class SourceRemapperService extends Service { + Property releaseProperty = javaCompile.getOptions().getRelease(); + + if (!releaseProperty.isPresent()) { + return; + } + + int compileRelease = releaseProperty.get(); + release.set(Math.max(release.get(), compileRelease)); + }); + + final int i = release.get(); + + if (i < 0) { + // Unable to find the release used to compile with, default to the current version + return Integer.parseInt(JavaVersion.current().getMajorVersion()); + } + + return i; + } } diff --git a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java index 1c513e48..ad56653d 100644 --- a/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java +++ b/src/main/java/net/fabricmc/loom/task/service/TinyRemapperService.java @@ -55,6 +55,7 @@ import net.fabricmc.loom.build.IntermediaryNamespaces; import net.fabricmc.loom.extension.RemapperExtensionHolder; import net.fabricmc.loom.task.AbstractRemapJarTask; import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.TinyRemapperLoggerAdapter; import net.fabricmc.loom.util.kotlin.KotlinClasspathService; import net.fabricmc.loom.util.kotlin.KotlinRemapperClassloader; import net.fabricmc.loom.util.service.Service; @@ -131,7 +132,7 @@ public class TinyRemapperService extends Service im } private TinyRemapper createTinyRemapper() { - TinyRemapper.Builder builder = TinyRemapper.newRemapper() + TinyRemapper.Builder builder = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE) .withKnownIndyBsm(Set.copyOf(getOptions().getKnownIndyBsms().get())); for (MappingsService.Options options : getOptions().getMappings().get()) { diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 08e70b69..dd7628c8 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -109,6 +109,14 @@ public class Constants { */ public static final String LOCAL_RUNTIME = "localRuntime"; public static final String NAMED_ELEMENTS = "namedElements"; + /** + * The configuration that contains the Minecraft client and loader runtime libraries, as used by the production run tasks. + */ + public static final String MINECRAFT_TEST_CLIENT_RUNTIME_LIBRARIES = "minecraftTestClientRuntimeLibraries"; + /** + * Mods to be used by {@link net.fabricmc.loom.task.prod.AbstractProductionRunTask} tasks by default. + */ + public static final String PRODUCTION_RUNTIME_MODS = "productionRuntimeMods"; private Configurations() { } diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index 4e7567c1..87b62afc 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -32,17 +32,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.cadixdev.lorenz.MappingSet; import org.cadixdev.mercury.Mercury; import org.cadixdev.mercury.remapper.MercuryRemapper; -import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.internal.project.ProjectInternal; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.internal.logging.progress.ProgressLogger; import org.gradle.internal.logging.progress.ProgressLoggerFactory; import org.slf4j.Logger; @@ -182,7 +178,8 @@ public class SourceRemapper { MappingSet mappings = lorenzMappingService.getMappings(); Mercury mercury = createMercuryWithClassPath(project, MappingsNamespace.of(to) == MappingsNamespace.NAMED); - mercury.setSourceCompatibilityFromRelease(getJavaCompileRelease(project)); + // Always use the latest version + mercury.setSourceCompatibilityFromRelease(Integer.MAX_VALUE); for (File file : extension.getUnmappedModCollection()) { Path path = file.toPath(); @@ -220,30 +217,6 @@ public class SourceRemapper { return this.mercury; } - public static int getJavaCompileRelease(Project project) { - AtomicInteger release = new AtomicInteger(-1); - - project.getTasks().withType(JavaCompile.class, javaCompile -> { - Property releaseProperty = javaCompile.getOptions().getRelease(); - - if (!releaseProperty.isPresent()) { - return; - } - - int compileRelease = releaseProperty.get(); - release.set(Math.max(release.get(), compileRelease)); - }); - - final int i = release.get(); - - if (i < 0) { - // Unable to find the release used to compile with, default to the current version - return Integer.parseInt(JavaVersion.current().getMajorVersion()); - } - - return i; - } - public static void copyNonJavaFiles(Path from, Path to, Logger logger, Path source) throws IOException { Files.walk(from).forEach(path -> { Path targetPath = to.resolve(from.relativize(path).toString()); diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java index a8330cd8..9829df3d 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java @@ -79,7 +79,7 @@ public final class TinyRemapperHelper { int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString()); - TinyRemapper.Builder builder = TinyRemapper.newRemapper() + TinyRemapper.Builder builder = TinyRemapper.newRemapper(TinyRemapperLoggerAdapter.INSTANCE) .ignoreConflicts(extension.isForgeLike()) .threads(Runtime.getRuntime().availableProcessors()) .withMappings(create(mappingTree, fromM, toM, true)) diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperLoggerAdapter.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperLoggerAdapter.java new file mode 100644 index 00000000..8bc9872d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperLoggerAdapter.java @@ -0,0 +1,57 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.tinyremapper.api.TrLogger; + +public final class TinyRemapperLoggerAdapter implements TrLogger { + public static final TinyRemapperLoggerAdapter INSTANCE = new TinyRemapperLoggerAdapter(); + + private static final Logger LOGGER = LoggerFactory.getLogger("TinyRemapper"); + + private TinyRemapperLoggerAdapter() { + } + + @Override + public void log(Level level, String message) { + switch (level) { + case ERROR: + LOGGER.error(message); + break; + case WARN: + LOGGER.warn(message); + break; + case INFO: + LOGGER.info(message); + break; + case DEBUG: + LOGGER.debug(message); + break; + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy index 94e9d293..9f80f5ad 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2023 FabricMC + * Copyright (c) 2023-2025 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 @@ -24,6 +24,7 @@ package net.fabricmc.loom.test.integration +import spock.lang.IgnoreIf import spock.lang.Specification import spock.lang.Unroll @@ -31,15 +32,16 @@ import net.fabricmc.loom.test.util.GradleProjectTestTrait import static net.fabricmc.loom.test.LoomTestConstants.PRE_RELEASE_GRADLE import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS +import static org.gradle.testkit.runner.TaskOutcome.FAILED import static org.gradle.testkit.runner.TaskOutcome.SUCCESS class DataGenerationTest extends Specification implements GradleProjectTestTrait { private static String DEPENDENCIES = """ dependencies { - minecraft "com.mojang:minecraft:1.20.2" - mappings "net.fabricmc:yarn:1.20.2+build.4:v2" - modImplementation "net.fabricmc:fabric-loader:0.14.23" - modImplementation "net.fabricmc.fabric-api:fabric-api:0.90.0+1.20.2" + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.4:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.9" + modImplementation "net.fabricmc.fabric-api:fabric-api:0.114.0+1.21.4" } """ @@ -202,4 +204,52 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait then: result.task(":runDatagen").outcome == SUCCESS } + + @Unroll + def "game tests (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + fabricApi { + configureTests() + } + ''' + DEPENDENCIES + when: + def result = gradle.run(task: "runGameTest", expectFailure: true) + + then: + // We expect this to fail because there is nothing to test + // At least we know that Fabric API is attempting to run the tests + result.task(":runGameTest").outcome == FAILED + result.output.contains("No test functions were given!") + + where: + version << STANDARD_TEST_VERSIONS + } + + @Unroll + @IgnoreIf({ System.getenv("CI") != null }) // This test is disabled on CI because it launches a real client and cannot run headless. + def "client game tests (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + fabricApi { + configureTests { + createSourceSet = true + modId = "example-test" + eula = true + } + } + ''' + DEPENDENCIES + when: + def result = gradle.run(task: "runClientGameTest") + def eula = new File(gradle.projectDir, "build/run/clientGameTest/eula.txt") + + then: + result.task(":runClientGameTest").outcome == SUCCESS + eula.text.contains("eula=true") + + where: + version << STANDARD_TEST_VERSIONS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DownloadTaskTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DownloadTaskTest.groovy new file mode 100644 index 00000000..59e358b9 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DownloadTaskTest.groovy @@ -0,0 +1,130 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 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.Unroll + +import net.fabricmc.loom.test.unit.download.DownloadTest +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 DownloadTaskTest extends DownloadTest implements GradleProjectTestTrait { + @Unroll + def "download (gradle #version)"() { + setup: + server.get("/simpleFile") { + it.result("Hello World") + } + + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << """ + dependencies { + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.8:v2" + } + + tasks.register("download", net.fabricmc.loom.task.DownloadTask) { + url = "${PATH}/simpleFile" + output = file("out.txt") + } + """ + when: + def result = gradle.run(task: "download") + def output = new File(gradle.projectDir, "out.txt") + + then: + result.task(":download").outcome == SUCCESS + output.text == "Hello World" + + where: + version << STANDARD_TEST_VERSIONS + } + + @Unroll + def "download sha1 (gradle #version)"() { + setup: + server.get("/simpleFile") { + it.result("Hello World") + } + + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << """ + dependencies { + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.8:v2" + } + + tasks.register("download", net.fabricmc.loom.task.DownloadTask) { + url = "${PATH}/simpleFile" + sha1 = "0a4d55a8d778e5022fab701977c5d840bbc486d0" + output = file("out.txt") + } + """ + when: + def result = gradle.run(task: "download") + def output = new File(gradle.projectDir, "out.txt") + + then: + result.task(":download").outcome == SUCCESS + output.text == "Hello World" + + where: + version << STANDARD_TEST_VERSIONS + } + + @Unroll + def "download max age (gradle #version)"() { + setup: + server.get("/simpleFile") { + it.result("Hello World") + } + + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << """ + dependencies { + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.8:v2" + } + + tasks.register("download", net.fabricmc.loom.task.DownloadTask) { + url = "${PATH}/simpleFile" + maxAge = Duration.ofDays(1) + output = file("out.txt") + } + """ + when: + def result = gradle.run(task: "download") + def output = new File(gradle.projectDir, "out.txt") + + then: + result.task(":download").outcome == SUCCESS + output.text == "Hello World" + + where: + version << STANDARD_TEST_VERSIONS + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy index c1397f5d..2860e1ba 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/FabricAPITest.groovy @@ -44,7 +44,7 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { setup: def gradle = gradleProject( repo: "https://github.com/FabricMC/fabric.git", - commit: "70277babddfaf52ee30013af94764da19473b3b1", + commit: "d70d2c06bb8fafdb72c6778b29fb050618015ab3", version: version, patch: "fabric_api" ) @@ -63,7 +63,7 @@ class FabricAPITest extends Specification implements GradleProjectTestTrait { .replace('id "fabric-loom" version "1.6.11"', 'id "dev.architectury.loom"') .replace('"fabric-loom"', '"dev.architectury.loom"') + mixinApPatch - def minecraftVersion = "1.21.4-pre3" + def minecraftVersion = "1.21.4" def server = ServerRunner.create(gradle.projectDir, minecraftVersion) .withMod(gradle.getOutputFile("fabric-api-999.0.0.jar")) diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy index 086778ad..4669f362 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/MojangMappingsProjectTest.groovy @@ -120,4 +120,30 @@ class MojangMappingsProjectTest extends Specification implements GradleProjectTe where: version << STANDARD_TEST_VERSIONS } + + @Unroll + def "mojang mappings via lazy provider (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + + gradle.buildGradle << ''' + dependencies { + minecraft "com.mojang:minecraft:1.18-pre5" + mappings project.provider { + loom.layered() { + officialMojangMappings() + } + } + } + ''' + + when: + def result = gradle.run(task: "build") + + then: + result.task(":build").outcome == SUCCESS + + where: + version << STANDARD_TEST_VERSIONS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/RunConfigTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/RunConfigTest.groovy index 0beb66d0..7a8cacb7 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/RunConfigTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/RunConfigTest.groovy @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2018-2023 FabricMC + * Copyright (c) 2018-2025 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 @@ -24,24 +24,32 @@ package net.fabricmc.loom.test.integration +import java.util.concurrent.TimeUnit + +import spock.lang.IgnoreIf import spock.lang.Specification +import spock.lang.Timeout import spock.lang.Unroll import spock.util.environment.RestoreSystemProperties +import net.fabricmc.loom.test.LoomTestConstants import net.fabricmc.loom.test.util.GradleProjectTestTrait +import net.fabricmc.loom.util.download.Download import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS import static org.gradle.testkit.runner.TaskOutcome.SUCCESS // This test runs a mod that exits on mod init class RunConfigTest extends Specification implements GradleProjectTestTrait { - private static List tasks = [ + private static final List tasks = [ "runClient", "runServer", "runTestmodClient", "runTestmodServer", "runAutoTestServer" ] + private static final String TRACY_CAPTURE_LINUX = "https://github.com/modmuss50/tracy-utils/releases/download/0.0.2/linux-x86_64-tracy-capture" + @Unroll def "Run config #task (gradle #version)"() { setup: @@ -130,4 +138,74 @@ class RunConfigTest extends Specification implements GradleProjectTestTrait { where: version << STANDARD_TEST_VERSIONS } + + @Unroll + def "prod server (gradle #version)"() { + setup: + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + dependencies { + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.4:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.9" + } + + tasks.register("prodServer", net.fabricmc.loom.task.prod.ServerProductionRunTask) { + installerVersion = "1.0.1" + } + ''' + when: + def result = gradle.run(task: "prodServer") + + then: + result.task(":prodServer").outcome == SUCCESS + + where: + version << STANDARD_TEST_VERSIONS + } + + @Timeout(value = 10, unit = TimeUnit.MINUTES) + @Unroll + @IgnoreIf({ !os.linux }) // XVFB is installed on the CI for this test + def "prod client (gradle #version)"() { + setup: + def tracyCapture = new File(LoomTestConstants.TEST_DIR, "tracy-capture") + Download.create(TRACY_CAPTURE_LINUX).defaultCache().downloadPath(tracyCapture.toPath()) + + def gradle = gradleProject(project: "minimalBase", version: version) + gradle.buildGradle << ''' + dependencies { + minecraft "com.mojang:minecraft:1.21.4" + mappings "net.fabricmc:yarn:1.21.4+build.4:v2" + modImplementation "net.fabricmc:fabric-loader:0.16.9" + modImplementation "net.fabricmc.fabric-api:fabric-api:0.114.0+1.21.4" + + productionRuntimeMods "net.fabricmc.fabric-api:fabric-api:0.114.0+1.21.4" + } + + tasks.register("prodClient", net.fabricmc.loom.task.prod.ClientProductionRunTask) { + jvmArgs.add("-Dfabric.client.gametest") + + tracy { + tracyCapture = file("tracy-capture") + output = file("profile.tracy") + } + } + ''' + + // Copy tracy into the project + def projectTracyCapture = new File(gradle.projectDir, "tracy-capture") + projectTracyCapture.bytes = tracyCapture.bytes + projectTracyCapture.setExecutable(true) + + when: + def result = gradle.run(task: "prodClient") + + then: + result.task(":prodClient").outcome == SUCCESS + new File(gradle.projectDir, "profile.tracy").exists() + + where: + version << STANDARD_TEST_VERSIONS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy index 860e65eb..0e4b5403 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/FabricApiExtensionTest.groovy @@ -27,13 +27,13 @@ package net.fabricmc.loom.test.unit import org.gradle.api.Project import spock.lang.Specification -import net.fabricmc.loom.configuration.FabricApiExtension +import net.fabricmc.loom.configuration.fabricapi.FabricApiVersions import net.fabricmc.loom.test.util.GradleTestUtil class FabricApiExtensionTest extends Specification { def "get module version"() { when: - def fabricApi = new FabricApiExtension() { + def fabricApi = new FabricApiVersions() { Project project = GradleTestUtil.mockProject() } def version = fabricApi.moduleVersion(moduleName, apiVersion) @@ -51,7 +51,7 @@ class FabricApiExtensionTest extends Specification { def "unknown module"() { when: - def fabricApi = new FabricApiExtension() { + def fabricApi = new FabricApiVersions() { Project project = GradleTestUtil.mockProject() } fabricApi.moduleVersion("fabric-api-unknown", apiVersion) diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/LineNumberRemapperTests.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/LineNumberRemapperTests.groovy index e07bfc70..c9ef451f 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/LineNumberRemapperTests.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/LineNumberRemapperTests.groovy @@ -65,6 +65,31 @@ class LineNumberRemapperTests extends Specification { readLineNumbers(unpacked) == [37, 39, 40] } + def "remapLinenumbersExclude"() { + given: + def className = LineNumberSource.class.name.replace('.', '/') + def input = ZipTestUtils.createZipFromBytes([(className + ".class"): getClassBytes(LineNumberSource.class)]) + + // + 10 to each line number + def entry = new ClassLineNumbers.Entry(className, 30, 40, [ + 27: 37, + 30: 40 + ]) + def lineNumbers = new ClassLineNumbers([(className): entry]) + + def outputJar = Files.createTempDirectory("loom").resolve("output.jar") + + when: + def remapper = new LineNumberRemapper(lineNumbers) + remapper.process(input, outputJar) + + def unpacked = ZipUtils.unpack(outputJar, className + ".class") + + then: + readLineNumbers(getClassBytes(LineNumberSource.class)) == [27, 29, 30] + readLineNumbers(unpacked) == [37, 40] + } + static byte[] getClassBytes(Class clazz) { return clazz.classLoader.getResourceAsStream(clazz.name.replace('.', '/') + ".class").withCloseable { it.bytes diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/ClassEntryTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/ClassEntryTest.groovy new file mode 100644 index 00000000..b766ecef --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/ClassEntryTest.groovy @@ -0,0 +1,63 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2025 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.cache + +import spock.lang.Specification + +import net.fabricmc.loom.decompilers.cache.ClassEntry + +class ClassEntryTest extends Specification { + def "valid class entry"() { + when: + def classEntry = new ClassEntry(name, innerClasses, superClasses) + then: + // Just make sure the constructor doesn't throw an exception + classEntry != null + where: + name | innerClasses | superClasses + "net/fabricmc/Test.class" | [] | [] + "net/fabricmc/Test.class" | [ + "net/fabricmc/Test\$Inner.class" + ] | ["java/lang/List.class"] + } + + def "invalid class entry"() { + when: + new ClassEntry(name, innerClasses, superClasses) + then: + thrown IllegalArgumentException + where: + name | innerClasses | superClasses + "net/fabricmc/Test" | [] | [] + "net/fabricmc/Test.class" | ["net/fabricmc/Test\$Inner"] | ["java/lang/List.class"] + "net/fabricmc/Test.class" | [ + "net/fabricmc/Test\$Inner.class" + ] | ["java/lang/List"] + "net/fabricmc/Test.class" | ["net/Test\$Inner.class"] | ["java/lang/List.class"] + "net/fabricmc/Test.class" | [ + "net/fabricmc/Bar\$Inner.class" + ] | [] + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy index ef6db7ec..edc829fc 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/JarWalkerTest.groovy @@ -123,7 +123,7 @@ class JarWalkerTest extends Specification { classes.size() == 1 classes[0].name() == "net/fabricmc/Example.class" classes[0].innerClasses() == [] - classes[0].superClasses() == ["java/lang/Runnable"] + classes[0].superClasses() == ["java/lang/Runnable.class"] } def "inner classes"() { @@ -146,8 +146,8 @@ class JarWalkerTest extends Specification { "net/fabricmc/other/Test\$Inner.class" ] classes[0].superClasses() == [ - "java/lang/Runnable", - "net/fabricmc/other/Super" + "java/lang/Runnable.class", + "net/fabricmc/other/Super.class" ] } diff --git a/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy b/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy index 805c2094..38d13525 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/ServerRunner.groovy @@ -29,11 +29,12 @@ import java.util.concurrent.TimeUnit import groovy.transform.Immutable import net.fabricmc.loom.test.LoomTestVersions +import net.fabricmc.loom.util.LoomVersions import net.fabricmc.loom.util.download.Download class ServerRunner { static final String LOADER_VERSION = LoomTestVersions.FABRIC_LOADER.version() - static final String INSTALLER_VERSION = LoomTestVersions.FABRIC_INSTALLER.version() + static final String INSTALLER_VERSION = LoomVersions.FABRIC_INSTALLER.version() static final Map FABRIC_API_URLS = [ "1.16.5": "https://github.com/FabricMC/fabric/releases/download/0.37.1%2B1.16/fabric-api-0.37.1+1.16.jar", "1.17.1": "https://github.com/FabricMC/fabric/releases/download/0.37.1%2B1.17/fabric-api-0.37.1+1.17.jar" diff --git a/src/test/resources/patches/fabric_api.patch b/src/test/resources/patches/fabric_api.patch index b2b77775..ae85767c 100644 --- a/src/test/resources/patches/fabric_api.patch +++ b/src/test/resources/patches/fabric_api.patch @@ -1,6 +1,6 @@ diff --git a/build.gradle b/build.gradle ---- a/build.gradle (revision 70277babddfaf52ee30013af94764da19473b3b1) -+++ b/build.gradle (date 1732875235843) +--- a/build.gradle (revision d70d2c06bb8fafdb72c6778b29fb050618015ab3) ++++ b/build.gradle (date 1734958436644) @@ -13,7 +13,7 @@ def ENV = System.getenv() @@ -36,23 +36,17 @@ diff --git a/build.gradle b/build.gradle } def getBranch() { -@@ -247,19 +230,6 @@ - - test { - useJUnitPlatform() -- -- afterEvaluate { -- // See: https://github.com/FabricMC/fabric-loader/pull/585 -- def classPathGroups = loom.mods.stream() -- .map { modSettings -> -- SourceSetHelper.getClasspath(modSettings, getProject()).stream() -- .map(File.&getAbsolutePath) -- .collect(Collectors.joining(File.pathSeparator)) -- } -- .collect(Collectors.joining(File.pathSeparator+File.pathSeparator)) -- -- systemProperty("fabric.classPathGroups", classPathGroups) -- } +@@ -250,10 +233,11 @@ } tasks.withType(ProcessResources).configureEach { +- inputs.property "version", project.version ++ def version = project.version ++ inputs.property "version", version + + filesMatching("fabric.mod.json") { +- expand "version": project.version ++ expand "version": version + } + } + diff --git a/src/test/resources/projects/mavenLibrary/build.gradle b/src/test/resources/projects/mavenLibrary/build.gradle index 6d2d90e4..60a0e20a 100644 --- a/src/test/resources/projects/mavenLibrary/build.gradle +++ b/src/test/resources/projects/mavenLibrary/build.gradle @@ -37,14 +37,14 @@ publishing { from components.java artifact(remapJar) { - classifier "classifier" + classifier = "classifier" } } } repositories { maven { - url "http://localhost:${System.getProperty("loom.test.mavenPort")}/" + url = "http://localhost:${System.getProperty("loom.test.mavenPort")}/" allowInsecureProtocol = true } } diff --git a/src/test/resources/projects/mixinApSimple/build.gradle b/src/test/resources/projects/mixinApSimple/build.gradle index be695713..088d761a 100644 --- a/src/test/resources/projects/mixinApSimple/build.gradle +++ b/src/test/resources/projects/mixinApSimple/build.gradle @@ -73,7 +73,10 @@ loom { mixin { useLegacyMixinAp = true - defaultRefmapName = "default-refmap0000.json" + // After evaluate block only to test for https://github.com/FabricMC/fabric-loom/issues/1249 + afterEvaluate { + defaultRefmapName = "default-refmap0000.json" + } add(sourceSets["main"], "main-refmap0000.json") add(sourceSets["mixin"]) diff --git a/src/test/resources/projects/signed/build.gradle b/src/test/resources/projects/signed/build.gradle index 1c711564..372a4c3d 100644 --- a/src/test/resources/projects/signed/build.gradle +++ b/src/test/resources/projects/signed/build.gradle @@ -36,14 +36,14 @@ publishing { from components.java artifact(remapJar) { builtBy remapJar - classifier "classifier" + classifier = "classifier" } } } repositories { maven { - url "http://localhost:${System.getProperty("loom.test.mavenPort")}/" + url = "http://localhost:${System.getProperty("loom.test.mavenPort")}/" allowInsecureProtocol = true } }