diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java
index 8c7ae953c..4da936ba4 100644
--- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java
+++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java
@@ -66,6 +66,22 @@ public interface LoomGradleExtensionAPI {
RegularFileProperty getAccessWidenerPath();
+ /**
+ * Specifies the {@code fabric.mod.json} file location used in injected interface processing.
+ *
+ *
+ * By default, either {@code src/main/resources/fabric.mod.json}
+ * or {@code src/client/resources/fabric.mod.json} in the project directory is used.
+ *
+ *
+ *
+ * Providing a path to a different location allows using a shared or preprocessed
+ * file. However, at the end it must be put into the root of the processed
+ * resources directory (usually {@code build/resources/main}).
+ *
+ */
+ RegularFileProperty getFabricModJsonPath();
+
NamedDomainObjectContainer getDecompilerOptions();
void decompilers(Action> action);
diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java
index 4b86632c4..0b47ba3ce 100644
--- a/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java
+++ b/src/main/java/net/fabricmc/loom/api/fabricapi/DataGenerationSettings.java
@@ -24,9 +24,13 @@
package net.fabricmc.loom.api.fabricapi;
+import java.io.File;
+
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
+import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
+
/**
* Represents the settings for data generation.
*/
@@ -67,4 +71,11 @@ public interface DataGenerationSettings {
* Contains a boolean property indicating whether data generation will be compiled and ran with the client.
*/
Property getClient();
+
+ /**
+ * Sets {@link #getModId()} property based on the {@code id} field defined in the provided file.
+ */
+ default void modId(File fabricModJsonFile) {
+ getModId().set(FabricModJsonFactory.createFromFile(fabricModJsonFile).getId());
+ }
}
diff --git a/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java
index 1e5561918..de6a7251e 100644
--- a/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java
+++ b/src/main/java/net/fabricmc/loom/api/fabricapi/GameTestSettings.java
@@ -24,10 +24,14 @@
package net.fabricmc.loom.api.fabricapi;
+import java.io.File;
+
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Optional;
import org.jetbrains.annotations.ApiStatus;
+import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
+
/**
* Represents the settings for game and/or client tests.
*/
@@ -89,4 +93,11 @@ public interface GameTestSettings {
*/
@Optional
Property getUsername();
+
+ /**
+ * Sets {@link #getModId()} property based on the {@code id} field defined in the provided file.
+ */
+ default void modId(File fabricModJsonFile) {
+ getModId().set(FabricModJsonFactory.createFromFile(fabricModJsonFile).getId());
+ }
}
diff --git a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java
index 22bec5966..9be2291d2 100644
--- a/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java
+++ b/src/main/java/net/fabricmc/loom/configuration/fabricapi/FabricApiAbstractSourceSet.java
@@ -24,8 +24,6 @@
package net.fabricmc.loom.configuration.fabricapi;
-import java.io.IOException;
-
import javax.inject.Inject;
import org.gradle.api.Project;
@@ -64,17 +62,13 @@ protected SourceSet configureSourceSet(Property modId, boolean isClient)
});
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()");
- }
+ final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), sourceSet);
- return fabricModJson.getId();
- } catch (IOException e) {
- throw new org.gradle.api.UncheckedIOException("Failed to read mod id from the datagen source set.", e);
+ 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();
}));
extension.getMods().create(modId.get(), mod -> {
diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java
index e547ac61b..b7ccb43fa 100644
--- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java
+++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java
@@ -25,8 +25,8 @@
package net.fabricmc.loom.extension;
import java.io.File;
-import java.io.IOException;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -34,7 +34,6 @@
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.NamedDomainObjectList;
import org.gradle.api.Project;
-import org.gradle.api.UncheckedIOException;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
@@ -74,7 +73,7 @@
import net.fabricmc.loom.util.DeprecationHelper;
import net.fabricmc.loom.util.MirrorUtil;
import net.fabricmc.loom.util.fmj.FabricModJson;
-import net.fabricmc.loom.util.fmj.FabricModJsonFactory;
+import net.fabricmc.loom.util.fmj.FabricModJsonHelpers;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
/**
@@ -86,6 +85,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
protected final ListProperty jarProcessors;
protected final ConfigurableFileCollection log4jConfigs;
protected final RegularFileProperty accessWidener;
+ protected final RegularFileProperty fabricModJsonPath;
protected final ManifestLocations versionsManifests;
protected final Property customMetadata;
protected final SetProperty knownIndyBsms;
@@ -118,6 +118,7 @@ protected LoomGradleExtensionApiImpl(Project project, LoomFiles directories) {
.empty();
this.log4jConfigs = project.files(directories.getDefaultLog4jConfigFile());
this.accessWidener = project.getObjects().fileProperty();
+ this.fabricModJsonPath = project.getObjects().fileProperty();
this.versionsManifests = new ManifestLocations();
this.versionsManifests.add("mojang", MirrorUtil.getVersionManifests(project), -2);
this.versionsManifests.add("fabric_experimental", MirrorUtil.getExperimentalVersions(project), -1);
@@ -205,6 +206,11 @@ public RegularFileProperty getAccessWidenerPath() {
return accessWidener;
}
+ @Override
+ public RegularFileProperty getFabricModJsonPath() {
+ return fabricModJsonPath;
+ }
+
@Override
public NamedDomainObjectContainer getDecompilerOptions() {
return decompilers;
@@ -293,17 +299,13 @@ public SetProperty getKnownIndyBsms() {
@Override
public String getModVersion() {
- try {
- final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(getProject(), SourceSetHelper.getMainSourceSet(getProject()));
+ List fabricModJsons = FabricModJsonHelpers.getModsInProject(getProject());
- if (fabricModJson == null) {
- throw new RuntimeException("Could not find a fabric.mod.json file in the main sourceset");
- }
-
- return fabricModJson.getModVersion();
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to read mod version from main sourceset.", e);
+ if (fabricModJsons.isEmpty()) {
+ throw new RuntimeException("Could not find a fabric.mod.json file in the main sourceset");
}
+
+ return fabricModJsons.getFirst().getModVersion();
}
@Override
diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java
index 0cfec9c6f..bb0d2a31a 100644
--- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java
+++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java
@@ -107,27 +107,38 @@ public static Optional createFromZipOptional(Path zipPath) {
return Optional.ofNullable(createFromZipNullable(zipPath));
}
+ public static FabricModJson createFromFile(File file) {
+ JsonObject modJson = readFmjJsonObject(file);
+ return create(modJson, new FabricModJsonSource.DirectorySource(file.toPath().getParent()));
+ }
+
@Nullable
- public static FabricModJson createFromSourceSetsNullable(Project project, SourceSet... sourceSets) throws IOException {
- final File file = SourceSetHelper.findFirstFileInResource(FABRIC_MOD_JSON, project, sourceSets);
+ public static FabricModJson createFromSourceSetsNullable(Project project, SourceSet... sourceSets) {
+ File file = SourceSetHelper.findFirstFileInResource(FABRIC_MOD_JSON, project, sourceSets);
if (file == null) {
return null;
}
+ try {
+ JsonObject modJson = readFmjJsonObject(file);
+ return create(modJson, new FabricModJsonSource.SourceSetSource(project, sourceSets));
+ } catch (JsonSyntaxException e) {
+ LOGGER.warn("Failed to parse fabric.mod.json: {}", file.getAbsolutePath());
+ return null;
+ }
+ }
+
+ private static JsonObject readFmjJsonObject(File file) {
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
final JsonObject modJson = LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class);
if (modJson == null) {
// fromJson returns null if the file is empty
LOGGER.warn("Failed to parse empty fabric.mod.json: {}", file.getAbsolutePath());
- return null;
}
- return create(modJson, new FabricModJsonSource.SourceSetSource(project, sourceSets));
- } catch (JsonSyntaxException e) {
- LOGGER.warn("Failed to parse fabric.mod.json: {}", file.getAbsolutePath());
- return null;
+ return modJson;
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + file.getAbsolutePath(), e);
}
diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonHelpers.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonHelpers.java
index 43e0e4745..4481283af 100644
--- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonHelpers.java
+++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonHelpers.java
@@ -24,22 +24,32 @@
package net.fabricmc.loom.util.fmj;
-import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.gradle.api.Project;
+import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.SourceSet;
-import net.fabricmc.loom.LoomGradleExtension;
+import net.fabricmc.loom.api.LoomGradleExtensionAPI;
import net.fabricmc.loom.util.gradle.SourceSetHelper;
+import net.fabricmc.loom.LoomGradleExtension;
public class FabricModJsonHelpers {
- // Returns a list of Mods found in the provided project's main or client sourcesets
+ /**
+ * Returns the list of mods provided by either {@link LoomGradleExtensionAPI#getFabricModJsonPath()}
+ * or {@code fabric.mod.json} in main or client resources.
+ */
public static List getModsInProject(Project project) {
final LoomGradleExtension extension = LoomGradleExtension.get(project);
+ Provider overrideFile = extension.getFabricModJsonPath().getAsFile();
+
+ if (overrideFile.isPresent()) {
+ return List.of(FabricModJsonFactory.createFromFile(overrideFile.get()));
+ }
+
var sourceSets = new ArrayList();
sourceSets.add(SourceSetHelper.getMainSourceSet(project));
@@ -47,14 +57,10 @@ public static List getModsInProject(Project project) {
sourceSets.add(SourceSetHelper.getSourceSetByName("client", project));
}
- try {
- final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(project, sourceSets.toArray(SourceSet[]::new));
+ final FabricModJson fabricModJson = FabricModJsonFactory.createFromSourceSetsNullable(project, sourceSets.toArray(SourceSet[]::new));
- if (fabricModJson != null) {
- return List.of(fabricModJson);
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ if (fabricModJson != null) {
+ return List.of(fabricModJson);
}
return Collections.emptyList();
diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/InterfaceInjectionTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/InterfaceInjectionTest.groovy
index 947bb8267..52946286d 100644
--- a/src/test/groovy/net/fabricmc/loom/test/integration/InterfaceInjectionTest.groovy
+++ b/src/test/groovy/net/fabricmc/loom/test/integration/InterfaceInjectionTest.groovy
@@ -24,6 +24,7 @@
package net.fabricmc.loom.test.integration
+import org.gradle.testkit.runner.BuildResult
import spock.lang.Specification
import spock.lang.Unroll
@@ -49,4 +50,34 @@ class InterfaceInjectionTest extends Specification implements GradleProjectTestT
where:
version << STANDARD_TEST_VERSIONS
}
+
+ @Unroll
+ def "Resolve custom FMJ"() {
+ setup:
+ GradleProject gradle = gradleProject(project: "fmjPathConfig", version: version)
+
+ when:
+ BuildResult result = gradle.run(task: "build", args: ["-PoverrideFMJ=true"])
+
+ then:
+ result.task(":build").outcome == SUCCESS
+
+ where:
+ version << STANDARD_TEST_VERSIONS
+ }
+
+ @Unroll
+ def "Fail to find FMJ"() {
+ setup:
+ GradleProject gradle = gradleProject(project: "fmjPathConfig", version: version)
+
+ when:
+ BuildResult result = gradle.run(task: "build", expectFailure: true)
+
+ then:
+ result.task(":build") == null
+
+ where:
+ version << STANDARD_TEST_VERSIONS
+ }
}
diff --git a/src/test/resources/projects/fmjPathConfig/build.gradle b/src/test/resources/projects/fmjPathConfig/build.gradle
new file mode 100644
index 000000000..287825cf1
--- /dev/null
+++ b/src/test/resources/projects/fmjPathConfig/build.gradle
@@ -0,0 +1,37 @@
+// This is used by a range of tests that append to this file before running the gradle tasks.
+// Can be used for tests that require minimal custom setup
+plugins {
+ id 'fabric-loom'
+ id 'maven-publish'
+}
+
+version = "1.0.0"
+group = "com.example"
+
+// In multi-version setup this would be a separate project,
+// but a source set will suffice for a test.
+sourceSets {
+ custom {
+ }
+ main {
+ compileClasspath += sourceSets.custom.compileClasspath
+ runtimeClasspath += sourceSets.custom.runtimeClasspath
+ }
+}
+
+
+dependencies {
+ minecraft "com.mojang:minecraft:1.17.1"
+ mappings "net.fabricmc:yarn:1.17.1+build.59:v2"
+ modImplementation "net.fabricmc:fabric-loader:0.11.6"
+}
+
+base {
+ archivesName = "fabric-example-mod"
+}
+
+if (project.hasProperty("overrideFMJ")) {
+ loom {
+ fabricModJsonPath = file("src/custom/resources/fabric.mod.json")
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/projects/fmjPathConfig/src/custom/resources/fabric.mod.json b/src/test/resources/projects/fmjPathConfig/src/custom/resources/fabric.mod.json
new file mode 100644
index 000000000..64b2e239a
--- /dev/null
+++ b/src/test/resources/projects/fmjPathConfig/src/custom/resources/fabric.mod.json
@@ -0,0 +1,11 @@
+{
+ "schemaVersion": 1,
+ "id": "testmod",
+ "version": "1",
+ "name": "Test Mod",
+ "custom": {
+ "loom:injected_interfaces": {
+ "net/minecraft/class_310": ["InjectedInterface"]
+ }
+ }
+}
diff --git a/src/test/resources/projects/fmjPathConfig/src/main/java/ExampleMod.java b/src/test/resources/projects/fmjPathConfig/src/main/java/ExampleMod.java
new file mode 100644
index 000000000..cd72e67d4
--- /dev/null
+++ b/src/test/resources/projects/fmjPathConfig/src/main/java/ExampleMod.java
@@ -0,0 +1,10 @@
+import net.minecraft.client.MinecraftClient;
+
+import net.fabricmc.api.ModInitializer;
+
+public class ExampleMod implements ModInitializer {
+ @Override
+ public void onInitialize() {
+ MinecraftClient.getInstance().newMethodThatDidNotExist();
+ }
+}
diff --git a/src/test/resources/projects/fmjPathConfig/src/main/java/InjectedInterface.java b/src/test/resources/projects/fmjPathConfig/src/main/java/InjectedInterface.java
new file mode 100644
index 000000000..a8225d0c3
--- /dev/null
+++ b/src/test/resources/projects/fmjPathConfig/src/main/java/InjectedInterface.java
@@ -0,0 +1,4 @@
+public interface InjectedInterface {
+ default void newMethodThatDidNotExist() {
+ }
+}