Merge 1.10

This commit is contained in:
Juuz
2025-03-04 18:32:25 +02:00
78 changed files with 2513 additions and 775 deletions

View File

@@ -1,7 +0,0 @@
# Ignore everything
/*
!/src
!/build.gradle
!/.gitignore
!/test-project

View File

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

View File

@@ -1,7 +0,0 @@
package net.fabricmc.loom.bootstrap;
import org.gradle.api.plugins.PluginAware;
public interface BootstrappedPlugin {
void apply(PluginAware project);
}

View File

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

View File

@@ -1,8 +0,0 @@
plugins {
id 'dev.architectury.loom' version '0.13.local'
}
dependencies {
minecraft "com.mojang:minecraft:1.16.5"
mappings loom.officialMojangMappings()
}

View File

@@ -1,10 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
mavenLocal()
}
}

View File

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

View File

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

View File

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

View File

@@ -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" }
fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" }

View File

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

3
gradlew vendored
View File

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

View File

@@ -9,6 +9,4 @@ dependencyResolutionManagement {
from(files("gradle/runtime.libs.versions.toml"))
}
}
}
include "bootstrap"
}

View File

@@ -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<PluginAware> {
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<String> 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<? extends Runnable> jobClass : SETUP_JOBS) {
project.getObjects().newInstance(jobClass).run();

View File

@@ -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<Boolean> getCreateRunConfiguration();
/**
* Contains a boolean property indicating whether a new source set should be created for the data generation process.
*/
Property<Boolean> getCreateSourceSet();
/**
* Contains a string property representing the mod ID associated with the data generation process.
*
* <p>This must be set when {@link #getCreateRunConfiguration()} is set.
*/
Property<String> getModId();
/**
* Contains a boolean property indicating whether strict validation is enabled.
*/
Property<Boolean> getStrictValidation();
/**
* Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset.
*/
Property<Boolean> getAddToResources();
/**
* Contains a boolean property indicating whether data generation will be compiled and ran with the client.
*/
Property<Boolean> getClient();
}

View File

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

View File

@@ -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.
*
* <p>Default: false
*/
Property<Boolean> getCreateSourceSet();
/**
* Contains a string property representing the mod ID associated with the tests.
*
* <p>This must be set when {@link #getCreateSourceSet()} is set.
*/
@Optional
Property<String> getModId();
/**
* Contains a boolean property indicating whether a run configuration will be created for the server side game tests, using Vanilla Game Test framework.
*
* <p>Default: true
*/
Property<Boolean> 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.
*
* <p>Default: true
*/
Property<Boolean> getEnableClientGameTests();
/**
* Contains a boolean property indicating whether the eula has been accepted. By enabling this you agree to the Minecraft EULA located at <a href="https://aka.ms/MinecraftEULA">https://aka.ms/MinecraftEULA</a>.
*
* <p>This only works when {@link #getEnableClientGameTests()} is enabled.
*
* <p>Default: false
*/
Property<Boolean> getEula();
/**
* Contains a boolean property indicating whether the run directories should be cleared before running the tests.
*
* <p>This only works when {@link #getEnableClientGameTests()} is enabled.
*
* <p>Default: true
*/
Property<Boolean> getClearRunDirectory();
/**
* Contains a string property representing the username to use for the client side game tests.
*
* <p>This only works when {@link #getEnableClientGameTests()} is enabled.
*
* <p>Default: Player0
*/
@Optional
Property<String> getUsername();
}

View File

@@ -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<T extends Task> {
protected final Project project;
private final LoomGradleExtension loomExtension;
protected final MixinExtension mixinExtension;
protected final Map<SourceSet, T> invokerTasks;
protected final Map<SourceSet, TaskProvider<T>> invokerTasks;
private final String name;
private final Collection<Configuration> apConfigurations;
protected AnnotationProcessorInvoker(Project project,
Collection<Configuration> apConfigurations,
Map<SourceSet, T> invokerTasks, String name) {
Map<SourceSet, TaskProvider<T>> invokerTasks, String name) {
this.project = project;
this.loomExtension = LoomGradleExtension.get(project);
this.mixinExtension = loomExtension.getMixin();
@@ -149,8 +150,8 @@ public abstract class AnnotationProcessorInvoker<T extends Task> {
}
}
for (Map.Entry<SourceSet, T> entry : invokerTasks.entrySet()) {
passMixinArguments(entry.getValue(), entry.getKey());
for (Map.Entry<SourceSet, TaskProvider<T>> entry : invokerTasks.entrySet()) {
entry.getValue().configure(t -> passMixinArguments(t, entry.getKey()));
}
}

View File

@@ -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<GroovyCompile> {
AnnotationProcessorInvoker.GROOVY);
}
private static Map<SourceSet, GroovyCompile> getInvokerTasks(Project project) {
private static Map<SourceSet, TaskProvider<GroovyCompile>> 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

View File

@@ -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<JavaCompile> {
AnnotationProcessorInvoker.JAVA);
}
private static Map<SourceSet, JavaCompile> getInvokerTasks(Project project) {
private static Map<SourceSet, TaskProvider<JavaCompile>> 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

View File

@@ -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<JavaCompile> {
kaptExtension.setIncludeCompileClasspath(false);
}
private static Map<SourceSet, JavaCompile> getInvokerTasks(Project project) {
private static Map<SourceSet, TaskProvider<JavaCompile>> 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<SourceSet, JavaCompile> entry : invokerTasks.entrySet()) {
for (Map.Entry<SourceSet, TaskProvider<JavaCompile>> 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);
}
});
});
}
}

View File

@@ -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<ScalaCompile> {
AnnotationProcessorInvoker.SCALA);
}
private static Map<SourceSet, ScalaCompile> getInvokerTasks(Project project) {
private static Map<SourceSet, TaskProvider<ScalaCompile>> 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

View File

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

View File

@@ -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<String, Map<String, String>> moduleVersionCache = new HashMap<>();
private static final HashMap<String, Map<String, String>> 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<DataGenerationSettings> 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<File> 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<Boolean> getCreateRunConfiguration();
/**
* Contains a boolean property indicating whether a new source set should be created for the data generation process.
*/
Property<Boolean> getCreateSourceSet();
/**
* Contains a string property representing the mod ID associated with the data generation process.
*
* <p>This must be set when {@link #getCreateRunConfiguration()} is set.
*/
Property<String> getModId();
/**
* Contains a boolean property indicating whether strict validation is enabled.
*/
Property<Boolean> getStrictValidation();
/**
* Contains a boolean property indicating whether the generated resources will be automatically added to the main sourceset.
*/
Property<Boolean> getAddToResources();
/**
* Contains a boolean property indicating whether data generation will be compiled and ran with the client.
*/
Property<Boolean> getClient();
}
private String getDependencyNotation(String moduleName, String fabricApiVersion) {
return String.format("net.fabricmc.fabric-api:%s:%s", moduleName, moduleVersion(moduleName, fabricApiVersion));
}
private Map<String, String> getApiModuleVersions(String fabricApiVersion) {
try {
return populateModuleVersionMap(getApiMavenPom(fabricApiVersion));
} catch (PomNotFoundException e) {
throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion);
}
}
private Map<String, String> 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<String, String> populateModuleVersionMap(File pomFile) {
try {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document pom = docBuilder.parse(pomFile);
Map<String, String> 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());
}
}

View File

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

View File

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

View File

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

View File

@@ -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<DataGenerationSettings> action) {
dataGeneration.configureDataGeneration(action);
}
@Override
public void configureTests() {
configureTests(gameTestSettings -> { });
}
@Override
public void configureTests(Action<GameTestSettings> action) {
testing.configureTests(action);
}
}

View File

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

View File

@@ -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<String, Map<String, String>> moduleVersionCache = new HashMap<>();
private final HashMap<String, Map<String, String>> 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<String, String> getApiModuleVersions(String fabricApiVersion) {
try {
return populateModuleVersionMap(getApiMavenPom(fabricApiVersion));
} catch (PomNotFoundException e) {
throw new RuntimeException("Could not find fabric-api version: " + fabricApiVersion);
}
}
private Map<String, String> 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<String, String> populateModuleVersionMap(File pomFile) {
try {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document pom = docBuilder.parse(pomFile);
Map<String, String> 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);
}
}
}

View File

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

View File

@@ -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<ArtifactRef> resolveArtifacts(Project project, Configuration configuration) {
final List<ArtifactRef> artifacts = new ArrayList<>();
for (ResolvedArtifact artifact : configuration.getResolvedConfiguration().getResolvedArtifacts()) {
final Set<ResolvedArtifact> 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<ResolvedArtifact> resolvedArtifacts) {
if (isCIBuild()) {
return;
}
final DependencyHandler dependencies = project.getDependencies();
List<ComponentIdentifier> 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()) {

View File

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

View File

@@ -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<FabricModJson> modDependencies, List<FabricMo
// Returns a list of jar mods that are found on the compile and runtime remapping configurations
private static Stream<FabricModJson> getCompileRuntimeModsFromRemapConfigs(Project project, Map<String, List<FabricModJson>> fmjCache) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
final List<Path> runtimeEntries = extension.getRuntimeRemapConfigurations().stream()
final Set<String> 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<FabricModJson> 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<Path, @Nullable FabricModJson> modFromZip(Map<String, List<FabricModJson>> fmjCache) {
return zipPath -> {
final List<FabricModJson> 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<RemapConfigurationSettings, Stream<Path>> resolveArtifacts(Project project, boolean runtime) {
final Usage usage = project.getObjects().named(Usage.class, runtime ? Usage.JAVA_RUNTIME : Usage.JAVA_API);

View File

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

View File

@@ -109,12 +109,7 @@ public class MinecraftLibraryProvider {
private void provideServerLibraries() {
final BundleMetadata serverBundleMetadata = minecraftProvider.getServerBundleMetadata();
if (serverBundleMetadata == null) {
return;
}
final List<Library> libraries = MinecraftLibraryHelper.getServerLibraries(serverBundleMetadata);
final List<Library> libraries = serverBundleMetadata != null ? MinecraftLibraryHelper.getServerLibraries(serverBundleMetadata) : Collections.emptyList();
final List<Library> processLibraries = processLibraries(libraries);
processLibraries.forEach(this::applyServerLibrary);
}

View File

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

View File

@@ -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<M extends MinecraftProvider> 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<M extends MinecraftProvide
public abstract MappingsNamespace getTargetNamespace();
/**
* @return A list of jars that should be remapped
*/
public abstract List<RemappedJars> getRemappedJars();
/**
* @return A list of output jars that this provider generates
*/
public List<? extends OutputJar> getOutputJars() {
return getRemappedJars();
}
// Returns a list of MinecraftJar.Type's that this provider exports to be used as a dependency
public List<MinecraftJar.Type> getDependencyTypes() {
return Collections.emptyList();
@@ -94,7 +108,7 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
throw new IllegalStateException("No remapped jars provided");
}
if (!areOutputsValid(remappedJars) || context.refreshOutputs() || !hasBackupJars(minecraftJars)) {
if (shouldRefreshOutputs(context)) {
try {
remapInputs(remappedJars, context.configContext());
createBackupJars(minecraftJars);
@@ -125,16 +139,6 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
return outputJarPath.resolveSibling(outputJarPath.getFileName() + ".backup");
}
protected boolean hasBackupJars(List<MinecraftJar> minecraftJars) {
for (MinecraftJar minecraftJar : minecraftJars) {
if (!Files.exists(getBackupJarPath(minecraftJar))) {
return false;
}
}
return true;
}
protected void createBackupJars(List<MinecraftJar> minecraftJars) throws IOException {
for (MinecraftJar minecraftJar : minecraftJars) {
Files.copy(minecraftJar.getPath(), getBackupJarPath(minecraftJar), StandardCopyOption.REPLACE_EXISTING);
@@ -202,11 +206,16 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
return "net.minecraft:%s:%s".formatted(getName(type), getVersion());
}
private boolean areOutputsValid(List<RemappedJars> 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<? extends OutputJar> 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<M extends MinecraftProvide
return false;
}
return true;
for (OutputJar outputJar : outputJars) {
if (!getMavenHelper(outputJar.type()).exists(null)) {
LOGGER.info("Refreshing outputs for mapped jar, as {} does not exist", outputJar.outputJar());
return true;
}
}
for (OutputJar outputJar : outputJars) {
if (!Files.exists(getBackupJarPath(outputJar.outputJar()))) {
LOGGER.info("Refreshing outputs for mapped jar, as backup jar does not exist for {}", outputJar.outputJar());
return true;
}
}
LOGGER.debug("All outputs are up to date");
return false;
}
private void remapInputs(List<RemappedJars> remappedJars, ConfigContext configContext) throws IOException {
@@ -306,7 +330,15 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
return minecraftProvider;
}
public record RemappedJars(Path inputJar, MinecraftJar outputJar, MappingsNamespace sourceNamespace, Path... remapClasspath) {
public sealed interface OutputJar permits RemappedJars, SimpleOutputJar {
MinecraftJar outputJar();
default MinecraftJar.Type type() {
return outputJar().getType();
}
}
public record RemappedJars(Path inputJar, MinecraftJar outputJar, MappingsNamespace sourceNamespace, Path... remapClasspath) implements OutputJar {
public Path outputJarPath() {
return outputJar().getPath();
}
@@ -314,9 +346,8 @@ public abstract class AbstractMappedMinecraftProvider<M extends MinecraftProvide
public String name() {
return outputJar().getName();
}
}
public MinecraftJar.Type type() {
return outputJar().getType();
}
public record SimpleOutputJar(MinecraftJar outputJar) implements OutputJar {
}
}

View File

@@ -78,18 +78,30 @@ public abstract sealed class IntermediaryMinecraftProvider<M extends MinecraftPr
@Override
public List<MinecraftJar> provide(ProvideContext context) throws Exception {
final List<MinecraftJar> 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<M extends MinecraftPr
throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars");
}
@Override
public List<? extends OutputJar> getOutputJars() {
return List.of(
new SimpleOutputJar(getMergedJar())
);
}
@Override
public List<MinecraftJar.Type> getDependencyTypes() {
return List.of(MinecraftJar.Type.MERGED);

View File

@@ -85,17 +85,28 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
@Override
public List<MinecraftJar> provide(ProvideContext context) throws Exception {
final ProvideContext childContext = context.withApplyDependencies(false);
final List<MinecraftJar> 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<M extends MinecraftProvider> extend
);
}
return List.of(getMergedJar());
return minecraftJars;
}
@Override
@@ -115,6 +126,13 @@ public abstract class NamedMinecraftProvider<M extends MinecraftProvider> extend
throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars");
}
@Override
public List<? extends OutputJar> getOutputJars() {
return List.of(
new SimpleOutputJar(getMergedJar())
);
}
@Override
public List<MinecraftJar.Type> getDependencyTypes() {
return List.of(MinecraftJar.Type.MERGED);

View File

@@ -65,7 +65,7 @@ public abstract class ProcessedNamedMinecraftProvider<M extends MinecraftProvide
parentMinecraftProvider.provide(context.withApplyDependencies(false));
boolean requiresProcessing = context.refreshOutputs() || !hasBackupJars(minecraftJars) || parentMinecraftJars.stream()
boolean requiresProcessing = shouldRefreshOutputs(context) || parentMinecraftJars.stream()
.map(this::getProcessedPath)
.anyMatch(jarProcessorManager::requiresProcessingJar);
@@ -81,6 +81,14 @@ public abstract class ProcessedNamedMinecraftProvider<M extends MinecraftProvide
return List.copyOf(minecraftJarOutputMap.values());
}
@Override
public List<? extends OutputJar> getOutputJars() {
return parentMinecraftProvider.getMinecraftJars().stream()
.map(this::getProcessedJar)
.map(SimpleOutputJar::new)
.toList();
}
@Override
public MavenScope getMavenScope() {
return MavenScope.LOCAL;

View File

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

View File

@@ -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<String> innerClasses, List<String> 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<String> innerClasses, List<String> 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<String> innerClasses, List<String> 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);

View File

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

View File

@@ -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<Configuration> getApConfigurationsStream(Function<SourceSet, String> getApConfigNameFunc);
@NotNull
Stream<Map.Entry<SourceSet, Task>> getInvokerTasksStream(String compileTaskLanguage);
<T extends Task> Stream<Map.Entry<SourceSet, TaskProvider<T>>> getInvokerTasksStream(String compileTaskLanguage, Class<T> taskType);
@NotNull
@Input

View File

@@ -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<Map.Entry<SourceSet, Task>> getInvokerTasksStream(String compileTaskLanguage) {
public <T extends Task> Stream<Map.Entry<SourceSet, TaskProvider<T>>> getInvokerTasksStream(String compileTaskLanguage, Class<T> taskType) {
return getMixinSourceSetsStream()
.flatMap(sourceSet -> {
try {
Task task = project.getTasks().getByName(sourceSet.getCompileTaskName(compileTaskLanguage));
TaskProvider<T> 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 -> { });
}
});
}

View File

@@ -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<String> getUrl();
/**
* The expected SHA-1 hash of the downloaded file.
*/
@Optional
@Input
public abstract Property<String> 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<Duration> getMaxAge();
/**
* The file to download to.
*/
@OutputFile
public abstract RegularFileProperty getOutput();
// Internal stuff:
@ApiStatus.Internal
@Input
protected abstract Property<Boolean> 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<String> getUrl();
Property<String> getSha1();
Property<Duration> getMaxAge();
RegularFileProperty getOutputFile();
Property<Boolean> getIsOffline();
}
public abstract static class DownloadAction implements WorkAction<DownloadWorkParameters> {
@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);
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -159,7 +159,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask {
getInjectAccessWidener().convention(false);
TaskProvider<NestableJarGenerationTask> 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());

View File

@@ -84,7 +84,7 @@ public abstract class RemapTaskConfiguration implements Runnable {
});
Action<RemapJarTask> remapJarTaskAction = task -> {
final AbstractArchiveTask jarTask = getTasks().named(JavaPlugin.JAR_TASK_NAME, AbstractArchiveTask.class).get();
final TaskProvider<AbstractArchiveTask> 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));
};

View File

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

View File

@@ -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.
*
* <p>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.
*
* <p>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<String> getJvmArgs();
/**
* A list of additional program arguments to pass to the game.
*/
@Input
public abstract ListProperty<String> 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.
*
* <p>See: <a href="https://docs.gradle.org/current/userguide/toolchains.html#sec:plugins_toolchains">Java Toolchains</a>
* @return
*/
@Nested
public abstract Property<JavaLauncher> getJavaLauncher();
// Internal options
@ApiStatus.Internal
@Classpath
protected abstract ConfigurableFileCollection getClasspath();
@ApiStatus.Internal
@Input
protected abstract Property<String> 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<File> 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<String> getProjectLoaderVersion() {
return getProject().provider(() -> {
InstallerData installerData = getExtension().getInstallerData();
if (installerData == null) {
return null;
}
return installerData.version();
});
}
protected Provider<Configuration> detachedConfigurationProvider(String mavenNotation, Provider<String> versionProvider) {
return versionProvider.map(version -> {
Dependency serverLauncher = getProject().getDependencies().create(mavenNotation.formatted(version));
return getProject().getConfigurations().detachedConfiguration(serverLauncher);
});
}
private static String joinFiles(Stream<File> stream) {
return stream.map(File::getAbsolutePath)
.collect(Collectors.joining(File.pathSeparator));
}
}

View File

@@ -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.
*
* <p>Defaults to true only on Linux and when the "CI" environment variable is set.
*
* <p>XVFB must be installed, on Debian-based systems you can install it with: <code>apt install -y xvfb</code>
*/
@Input
public abstract Property<Boolean> getUseXVFB();
@Nested
@Optional
public abstract Property<TracyCapture> 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<? super TracyCapture> action) {
getTracyCapture().set(getProject().getObjects().newInstance(TracyCapture.class));
getTracyCapture().finalizeValue();
action.execute(getTracyCapture().get());
}
// Internal options
@Input
protected abstract Property<String> 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");
}
}
}

View File

@@ -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.
*
* <p>Defaults to the version of Fabric Loader that the project is using.
*/
@Input
public abstract Property<String> getLoaderVersion();
/**
* The version of Minecraft to use.
*
* <p>Defaults to the version of Minecraft that the project is using.
*/
@Input
public abstract Property<String> getMinecraftVersion();
/**
* The version of the Fabric Installer to use.
*
* <p>Defaults to a version provided by Loom.
*/
@Input
public abstract Property<String> 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<File> streamClasspath() {
return Stream.concat(
super.streamClasspath(),
Stream.of(getInstallPropertiesJar().get().getAsFile())
);
}
}

View File

@@ -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.
*
* <p>Defaults to 10 seconds.
*/
@Input
public abstract Property<Integer> 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<String> 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;
}
}

View File

@@ -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<SourceRemapperService.O
task.getSourceNamespace(),
task.getTargetNamespace()
));
o.getJavaCompileRelease().set(SourceRemapper.getJavaCompileRelease(task.getProject()));
o.getJavaCompileRelease().set(getJavaCompileRelease(task.getProject()));
o.getClasspath().from(task.getClasspath());
});
}
@@ -135,4 +139,28 @@ public final class SourceRemapperService extends Service<SourceRemapperService.O
return mercury;
}
public static int getJavaCompileRelease(Project project) {
AtomicInteger release = new AtomicInteger(-1);
project.getTasks().withType(JavaCompile.class, javaCompile -> {
Property<Integer> 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;
}
}

View File

@@ -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<TinyRemapperService.Options> 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()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String> tasks = [
private static final List<String> 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
}
}

View File

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

View File

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

View File

@@ -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"
] | []
}
}

View File

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

View File

@@ -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<String, String> 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"

View File

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

View File

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

View File

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

View File

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