fabric.mod.json generation task (#1345)

* fabric.mod.json DSL.

* A start on generating FMJs

* More work

* The rest

* Task works

* Javadoc and cleanup

* Fixes
This commit is contained in:
modmuss
2025-09-02 09:27:55 +01:00
committed by GitHub
parent 2617ed1f49
commit 4a34c4bde5
8 changed files with 2029 additions and 0 deletions

View File

@@ -161,6 +161,7 @@ dependencies {
testImplementation testLibs.bcprov
testImplementation testLibs.bcutil
testImplementation testLibs.bcpkix
testImplementation testLibs.fabric.loader
compileOnly runtimeLibs.jetbrains.annotations
testCompileOnly runtimeLibs.jetbrains.annotations

View File

@@ -0,0 +1,785 @@
/*
* 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.fmj;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.jetbrains.annotations.ApiStatus;
/**
* Represents the Fabric mod JSON v1 specification.
*
* <p>This class defines properties of a Fabric mod JSON file via a type-safe DSL.
*/
public abstract class FabricModJsonV1Spec {
/**
* The ID of the mod.
* @return A {@link Property} containing a {@link String} representing the mod ID
*/
@Input
public abstract Property<String> getModId();
/**
* The version of the mod.
* @return A {@link Property} containing a {@link String} representing the mod version
*/
@Input
public abstract Property<String> getVersion();
/**
* The display name of the mod.
* @return A {@link Property} containing a {@link String} representing the mod name
*/
@Input
@Optional
public abstract Property<String> getName();
/**
* The description of the mod.
* @return A {@link Property} containing a {@link String} representing the mod description
*/
@Input
@Optional
public abstract Property<String> getDescription();
/**
* A list of other mod IDs that this mod uses as aliases.
* @return A {@link ListProperty} containing a list of {@link String} representing the mod aliases
*/
@Input
@Optional
public abstract ListProperty<String> getProvides();
/**
* The environment the mod runs in.
*
* <p>One of `client`, `server`, or `*`.
*
* @return A {@link Property} containing a {@link String} representing the mod environment
*/
@Input
@Optional
public abstract Property<String> getEnvironment();
/**
* Sets the environment to 'client', indicating the mod only runs on the client side.
*/
public void client() {
getEnvironment().set("client");
}
/**
* Sets the environment to 'server', indicating the mod only runs on the server side.
*/
public void server() {
getEnvironment().set("server");
}
/**
* A list of entrypoints for the mod.
* @return A {@link ListProperty} containing a list of {@link Entrypoint} representing the mod entrypoints
*/
@Input
@Optional
public abstract ListProperty<Entrypoint> getEntrypoints();
/**
* Add a new entrypoint with the given name and value.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param value The value of the entrypoint, typically a fully qualified class name
*/
public void entrypoint(String entrypoint, String value) {
entrypoint(entrypoint, value, metadata -> { });
}
/**
* Add a new entrypoint with the given name and value, and configure it with the given action.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param value The value of the entrypoint, typically a fully qualified class name
* @param action An action to configure the entrypoint further
*/
public void entrypoint(String entrypoint, String value, Action<Entrypoint> action) {
entrypoint(entrypoint, metadata -> {
metadata.getValue().set(value);
action.execute(metadata);
});
}
/**
* Add a new entrypoint with the given name, and configure it with the given action.
*
* @param entrypoint The name of the entrypoint, such as "main" or "client"
* @param action An action to configure the entrypoint
*/
public void entrypoint(String entrypoint, Action<Entrypoint> action) {
create(Entrypoint.class, getEntrypoints(), e -> {
e.getEntrypoint().set(entrypoint);
action.execute(e);
});
}
/**
* A list of additional JARs to load with the mod.
*
* <p>A path relative to the root of the mod JAR.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the additional JARs
*/
@Input
@Optional
public abstract ListProperty<String> getJars();
/**
* A list of Mixin configurations for the mod.
*
* @return A {@link ListProperty} containing a list of {@link Mixin} representing the mod Mixins
*/
@Input
@Optional
public abstract ListProperty<Mixin> getMixins();
/**
* Add a new Mixin configuration with the given value.
*
* @param value The value of the Mixin configuration, typically a path to a JSON file
*/
public void mixin(String value) {
mixin(value, mixin -> { });
}
/**
* Add a new Mixin configuration with the given value, and configure it with the given action.
*
* @param value The value of the Mixin configuration, typically a path to a JSON file
* @param action An action to configure the Mixin further
*/
public void mixin(String value, Action<Mixin> action) {
mixin(mixin -> {
mixin.getValue().set(value);
action.execute(mixin);
});
}
/**
* Add a new Mixin configuration, and configure it with the given action.
*
* @param action An action to configure the Mixin
*/
public void mixin(Action<Mixin> action) {
create(Mixin.class, getMixins(), action);
}
/**
* The path to the access widener file for the mod.
*
* <p>A path relative to the root of the mod JAR.
*
* @return A {@link Property} containing a {@link String} representing the access widener path
*/
@Input
@Optional
public abstract Property<String> getAccessWidener();
/**
* A list of depedencies that this mod depends on (required).
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getDepends();
/**
* Add a required dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the dependency
* @param versionRequirements A collection of version requirement strings
*/
public void depends(String modId, Iterable<String> versionRequirements) {
depends(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a required dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the dependency
* @param versionRequirement A version requirement string
*/
public void depends(String modId, String versionRequirement) {
depends(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a required dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the dependency
* @param action An action to configure the dependency further
*/
public void depends(String modId, Action<Dependency> action) {
depends(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a required dependency, and configure it with the given action.
*
* @param action An action to configure the dependency
*/
public void depends(Action<Dependency> action) {
create(Dependency.class, getDepends(), action);
}
/**
* A list of recommended dependencies.
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod recommended dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getRecommends();
/**
* Add a recommended dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the recommended dependency
* @param versionRequirements A collection of version requirement strings
*/
public void recommends(String modId, Iterable<String> versionRequirements) {
recommends(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a recommended dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the recommended dependency
* @param versionRequirement A version requirement string
*/
public void recommends(String modId, String versionRequirement) {
recommends(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a recommended dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the recommended dependency
* @param action An action to configure the recommended dependency further
*/
public void recommends(String modId, Action<Dependency> action) {
recommends(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a recommended dependency, and configure it with the given action.
*
* @param action An action to configure the recommended dependency
*/
public void recommends(Action<Dependency> action) {
create(Dependency.class, getRecommends(), action);
}
/**
* A list of suggested dependencies.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod suggested dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getSuggests();
/**
* Add a suggested dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the suggested dependency
* @param versionRequirements A collection of version requirement strings
*/
public void suggests(String modId, Iterable<String> versionRequirements) {
suggests(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a suggested dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the suggested dependency
* @param versionRequirement A version requirement string
*/
public void suggests(String modId, String versionRequirement) {
suggests(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a suggested dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the suggested dependency
* @param action An action to configure the suggested dependency further
*/
public void suggests(String modId, Action<Dependency> action) {
suggests(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a suggested dependency, and configure it with the given action.
*
* @param action An action to configure the suggested dependency
*/
public void suggests(Action<Dependency> action) {
create(Dependency.class, getSuggests(), action);
}
/**
* A list of conflicting dependencies.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod conflicting dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getConflicts();
/**
* Add a conflicting dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the conflicting dependency
* @param versionRequirements A collection of version requirement strings
*/
public void conflicts(String modId, Iterable<String> versionRequirements) {
conflicts(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a conflicting dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the conflicting dependency
* @param versionRequirement A version requirement string
*/
public void conflicts(String modId, String versionRequirement) {
conflicts(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a conflicting dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the conflicting dependency
* @param action An action to configure the conflicting dependency further
*/
public void conflicts(String modId, Action<Dependency> action) {
conflicts(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a conflicting dependency, and configure it with the given action.
*
* @param action An action to configure the conflicting dependency
*/
public void conflicts(Action<Dependency> action) {
create(Dependency.class, getConflicts(), action);
}
/**
* A list of dependencies that this mod breaks.
*
* @return A {@link ListProperty} containing a list of {@link Dependency} representing the mod broken dependencies
*/
@Input
@Optional
public abstract ListProperty<Dependency> getBreaks();
/**
* Add a broken dependency on another mod with the given mod ID and version requirements.
*
* @param modId The mod ID of the broken dependency
* @param versionRequirements A collection of version requirement strings
*/
public void breaks(String modId, Iterable<String> versionRequirements) {
breaks(modId, dependency -> {
dependency.getVersionRequirements().addAll(versionRequirements);
});
}
/**
* Add a broken dependency on another mod with the given mod ID and a single version requirement.
*
* @param modId The mod ID of the broken dependency
* @param versionRequirement A version requirement string
*/
public void breaks(String modId, String versionRequirement) {
breaks(modId, dependency -> {
dependency.getVersionRequirements().add(versionRequirement);
});
}
/**
* Add a broken dependency on another mod with the given mod ID, and configure it with the given action.
*
* @param modId The mod ID of the broken dependency
* @param action An action to configure the broken dependency further
*/
public void breaks(String modId, Action<Dependency> action) {
breaks(dependency -> {
dependency.getModId().set(modId);
action.execute(dependency);
});
}
/**
* Add a broken dependency, and configure it with the given action.
*
* @param action An action to configure the broken dependency
*/
public void breaks(Action<Dependency> action) {
create(Dependency.class, getBreaks(), action);
}
/**
* A list of licenses for the mod.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the mod licenses
*/
@Input
@Optional
public abstract ListProperty<String> getLicenses();
/**
* A list of authors of the mod.
*
* @return A {@link ListProperty} containing a list of {@link Person} representing the mod authors
*/
@Input
@Optional
public abstract ListProperty<Person> getAuthors();
/**
* Add a new author with the given name.
*
* @param name The name of the author
*/
public void author(String name) {
author(name, person -> { });
}
/**
* Add a new author with the given name, and configure it with the given action.
*
* @param name The name of the author
* @param action An action to configure the author further
*/
public void author(String name, Action<Person> action) {
author(person -> {
person.getName().set(name);
action.execute(person);
});
}
/**
* Add a new author, and configure it with the given action.
*
* @param action An action to configure the author
*/
public void author(Action<Person> action) {
create(Person.class, getAuthors(), action);
}
/**
* A list of contributors to the mod.
*
* @return A {@link ListProperty} containing a list of {@link Person} representing the mod contributors
*/
@Input
@Optional
public abstract ListProperty<Person> getContributors();
/**
* Add a new contributor with the given name.
*
* @param name The name of the contributor
*/
public void contributor(String name) {
contributor(name, person -> { });
}
/**
* Add a new contributor with the given name, and configure it with the given action.
*
* @param name The name of the contributor
* @param action An action to configure the contributor further
*/
public void contributor(String name, Action<Person> action) {
contributor(person -> {
person.getName().set(name);
action.execute(person);
});
}
/**
* Add a new contributor, and configure it with the given action.
*
* @param action An action to configure the contributor
*/
public void contributor(Action<Person> action) {
create(Person.class, getContributors(), action);
}
/**
* A map of contact information for the mod.
*
* <p>The key is the platform (e.g. "email", "github", "discord") and the value is the contact detail for that platform.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the mod contact information
*/
@Input
@Optional
public abstract MapProperty<String, String> getContactInformation();
/**
* A list of icons for the mod.
*
* @return A {@link ListProperty} containing a list of {@link Icon} representing the mod icons
*/
@Input
@Optional
public abstract ListProperty<Icon> getIcons();
/**
* Add a new icon with the given path.
*
* <p>Note: Only 1 unsized icon is allowed. If you need to specify multiple icons or sizes, use {@link #icon(int, String)}
*
* @param path The path to the icon file, relative to the root of the mod JAR
*/
public void icon(String path) {
icon(path, icon -> { });
}
/**
* Add a new icon with the given size and path.
*
* @param size The size of the icon in pixels (e.g. 16, 32, 64)
* @param path The path to the icon file, relative to the root of the mod JAR
*/
public void icon(int size, String path) {
icon(path, icon -> icon.getSize().set(size));
}
/**
* Add a new icon with the given path, and configure it with the given action.
*
* @param path The path to the icon file, relative to the root of the mod JAR
* @param action An action to configure the icon further
*/
public void icon(String path, Action<Icon> action) {
icon(icon -> {
icon.getPath().set(path);
action.execute(icon);
});
}
/**
* Add a new icon, and configure it with the given action.
*
* @param action An action to configure the icon
*/
public void icon(Action<Icon> action) {
create(Icon.class, getIcons(), action);
}
/**
* A map of language adapters for the mod.
*
* <p>The key is the adapter name and the value is the fully qualified class name of the adapter.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the mod language adapters
*/
@Input
@Optional
public abstract MapProperty<String, String> getLanguageAdapters();
/**
* A map of custom data for the mod.
*
* <p>This can be used by other tools to store additional information about the mod.
*
* <p>The object is encoded to JSON using Gson, so it can be any type that Gson supports.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link Object} values representing the mod custom data
*/
@Input
@Optional
public abstract MapProperty<String, Object> getCustomData();
public abstract static class Entrypoint {
/**
* The name of the entrypoint, such as "main" or "client".
*
* @return A {@link Property} containing a {@link String} representing the entrypoint name
*/
@Input
public abstract Property<String> getEntrypoint();
/**
* The value of the entrypoint, typically a fully qualified class name.
*
* @return A {@link Property} containing a {@link String} representing the entrypoint value
*/
@Input
public abstract Property<String> getValue();
/**
* The language adapter to use for this entrypoint, if any.
*
* @return A {@link Property} containing a {@link String} representing the entrypoint language adapter
*/
@Input
@Optional
public abstract Property<String> getAdapter();
}
public abstract static class Mixin {
/**
* The value of the Mixin configuration, typically a path to a JSON file.
*
* @return A {@link Property} containing a {@link String} representing the Mixin configuration value
*/
@Input
public abstract Property<String> getValue();
/**
* The environment the Mixin configuration applies to.
*
* <p>One of `client`, `server`, or `*`.
*
* @return A {@link Property} containing a {@link String} representing the Mixin configuration environment
*/
@Input
@Optional
public abstract Property<String> getEnvironment();
}
public abstract static class Dependency {
/**
* The mod ID of the dependency.
*
* @return A {@link Property} containing a {@link String} representing the dependency mod ID
*/
@Input
public abstract Property<String> getModId();
/**
* A list of version requirements for the dependency.
*
* <p>Each version requirement is a string that specifies a version or range of versions.
*
* @return A {@link ListProperty} containing a list of {@link String} representing the dependency version requirements
*/
@Input
@Optional
public abstract ListProperty<String> getVersionRequirements();
}
public abstract static class Person {
/**
* The name of the person.
*
* @return A {@link Property} containing a {@link String} representing the person's name
*/
@Input
public abstract Property<String> getName();
/**
* A map of contact information for the person.
*
* <p>The key is the platform (e.g. "email", "github", "discord") and the value is the contact detail for that platform.
*
* @return A {@link MapProperty} containing a map of {@link String} keys and {@link String} values representing the person's contact information
*/
@Input
@Optional
public abstract MapProperty<String, String> getContactInformation();
}
public abstract static class Icon {
/**
* The path to the icon file, relative to the root of the mod JAR.
*
* @return A {@link Property} containing a {@link String} representing the icon path
*/
@Input
public abstract Property<String> getPath();
/**
* The size of the icon in pixels (e.g. 16, 32, 64).
*
* <p>If not specified, the icon is considered to be "unsized". Only one unsized icon is allowed.
*
* @return A {@link Property} containing an {@link Integer} representing the icon size
*/
@Input
@Optional // Icon is required if there is more than 1 icon specified
public abstract Property<Integer> getSize();
}
// Internal stuff:
@Inject
@ApiStatus.Internal
protected abstract ObjectFactory getObjectFactory();
private <T> void create(Class<T> type, ListProperty<T> list, Action<T> action) {
T item = getObjectFactory().newInstance(type);
action.execute(item);
list.add(item);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Nested;
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 net.fabricmc.loom.api.fmj.FabricModJsonV1Spec;
import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator;
/**
* A task that generates a {@code fabric.mod.json} file using the configured {@link FabricModJsonV1Spec} specification.
*/
public abstract class FabricModJsonV1Task extends AbstractLoomTask {
/**
* The fabric.mod.json spec.
*/
@Nested
public abstract Property<FabricModJsonV1Spec> getJson();
/**
* The output file to write the generated fabric.mod.json to.
*/
@OutputFile
public abstract RegularFileProperty getOutputFile();
@Inject
protected abstract WorkerExecutor getWorkerExecutor();
@Inject
protected abstract ObjectFactory getObjectFactory();
public FabricModJsonV1Task() {
getJson().set(getObjectFactory().newInstance(FabricModJsonV1Spec.class));
}
/**
* Configure the fabric.mod.json spec.
*
* @param action A {@link Action} that configures the spec.
*/
public void json(Action<FabricModJsonV1Spec> action) {
action.execute(getJson().get());
}
@TaskAction
public void run() {
final WorkQueue workQueue = getWorkerExecutor().noIsolation();
workQueue.submit(FabricModJsonV1WorkAction.class, params -> {
params.getSpec().set(getJson());
params.getOutputFile().set(getOutputFile());
});
}
public interface FabricModJsonV1WorkParameters extends WorkParameters {
Property<FabricModJsonV1Spec> getSpec();
RegularFileProperty getOutputFile();
}
public abstract static class FabricModJsonV1WorkAction implements WorkAction<FabricModJsonV1WorkParameters> {
@Override
public void execute() {
FabricModJsonV1Spec spec = getParameters().getSpec().get();
Path outputPath = getParameters().getOutputFile().get().getAsFile().toPath();
String json = FabricModJsonV1Generator.INSTANCE.generate(spec);
try {
Files.writeString(outputPath, json);
} catch (IOException e) {
throw new UncheckedIOException("Failed to write fabric.mod.json", e);
}
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.fmj.gen;
public interface FabricModJsonGenerator<Spec> {
String generate(Spec spec);
}

View File

@@ -0,0 +1,200 @@
/*
* 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.fmj.gen;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.add;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addArray;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addRequired;
import static net.fabricmc.loom.util.fmj.gen.GeneratorUtils.addStringOrArray;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.fabricmc.loom.LoomGradlePlugin;
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec;
// Opposite of https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java
public final class FabricModJsonV1Generator implements FabricModJsonGenerator<FabricModJsonV1Spec> {
private static final int VERSION = 1;
public static final FabricModJsonV1Generator INSTANCE = new FabricModJsonV1Generator();
private FabricModJsonV1Generator() {
}
public String generate(FabricModJsonV1Spec spec) {
Objects.requireNonNull(spec);
JsonObject fmj = new JsonObject();
fmj.addProperty("schemaVersion", VERSION);
// Required
addRequired(fmj, "id", spec.getModId());
addRequired(fmj, "version", spec.getVersion());
// All other fields are optional
// Match the order as specified in V1ModMetadataParser to make it easier to compare
addArray(fmj, "provides", spec.getProvides(), JsonPrimitive::new);
add(fmj, "environment", spec.getEnvironment());
add(fmj, "entrypoints", spec.getEntrypoints(), this::generateEntrypoints);
addArray(fmj, "jars", spec.getJars(), this::generateJar);
addArray(fmj, "mixins", spec.getMixins(), this::generateMixins);
add(fmj, "accessWidener", spec.getAccessWidener());
add(fmj, "depends", spec.getDepends(), this::generateDependencies);
add(fmj, "recommends", spec.getRecommends(), this::generateDependencies);
add(fmj, "suggests", spec.getSuggests(), this::generateDependencies);
add(fmj, "conflicts", spec.getConflicts(), this::generateDependencies);
add(fmj, "breaks", spec.getBreaks(), this::generateDependencies);
add(fmj, "name", spec.getName());
add(fmj, "description", spec.getDescription());
addArray(fmj, "authors", spec.getAuthors(), this::generatePerson);
addArray(fmj, "contributors", spec.getContributors(), this::generatePerson);
add(fmj, "contact", spec.getContactInformation());
addStringOrArray(fmj, "license", spec.getLicenses());
add(fmj, "icon", spec.getIcons(), this::generateIcon);
add(fmj, "languageAdapters", spec.getLanguageAdapters());
add(fmj, "custom", spec.getCustomData(), this::generateCustomData);
return LoomGradlePlugin.GSON.toJson(fmj);
}
private JsonElement generatePerson(FabricModJsonV1Spec.Person person) {
if (person.getContactInformation().get().isEmpty()) {
return new JsonPrimitive(person.getName().get());
}
JsonObject json = new JsonObject();
addRequired(json, "name", person.getName());
add(json, "contact", person.getContactInformation());
return json;
}
private JsonObject generateEntrypoints(List<FabricModJsonV1Spec.Entrypoint> entrypoints) {
Map<String, List<FabricModJsonV1Spec.Entrypoint>> entrypointsMap = entrypoints.stream()
.collect(Collectors.groupingBy(entrypoint -> entrypoint.getEntrypoint().get()));
JsonObject json = new JsonObject();
entrypointsMap.forEach((entrypoint, entries) -> json.add(entrypoint, generateEntrypoint(entries)));
return json;
}
private JsonArray generateEntrypoint(List<FabricModJsonV1Spec.Entrypoint> entries) {
JsonArray json = new JsonArray();
for (FabricModJsonV1Spec.Entrypoint entry : entries) {
json.add(generateEntrypointEntry(entry));
}
return json;
}
private JsonElement generateEntrypointEntry(FabricModJsonV1Spec.Entrypoint entrypoint) {
if (!entrypoint.getAdapter().isPresent()) {
return new JsonPrimitive(entrypoint.getValue().get());
}
JsonObject json = new JsonObject();
addRequired(json, "value", entrypoint.getValue());
addRequired(json, "adapter", entrypoint.getAdapter());
return json;
}
private JsonObject generateJar(String jar) {
JsonObject json = new JsonObject();
json.addProperty("file", jar);
return json;
}
private JsonElement generateMixins(FabricModJsonV1Spec.Mixin mixin) {
if (!mixin.getEnvironment().isPresent()) {
return new JsonPrimitive(mixin.getValue().get());
}
JsonObject json = new JsonObject();
addRequired(json, "config", mixin.getValue());
addRequired(json, "environment", mixin.getEnvironment());
return json;
}
private JsonObject generateDependencies(List<FabricModJsonV1Spec.Dependency> dependencies) {
JsonObject json = new JsonObject();
for (FabricModJsonV1Spec.Dependency dependency : dependencies) {
json.add(dependency.getModId().get(), generateDependency(dependency));
}
return json;
}
private JsonElement generateDependency(FabricModJsonV1Spec.Dependency dependency) {
List<String> requirements = dependency.getVersionRequirements().get();
if (requirements.isEmpty()) {
throw new IllegalStateException("Dependency " + dependency.getModId().get() + " must have at least one version requirement");
}
if (requirements.size() == 1) {
return new JsonPrimitive(dependency.getModId().get());
}
JsonArray json = new JsonArray();
for (String s : requirements) {
json.add(s);
}
return json;
}
private JsonElement generateIcon(List<FabricModJsonV1Spec.Icon> icons) {
if (icons.size() == 1 && !icons.getFirst().getSize().isPresent()) {
return new JsonPrimitive(icons.getFirst().getPath().get());
}
JsonObject json = new JsonObject();
for (FabricModJsonV1Spec.Icon icon : icons) {
String size = String.valueOf(icon.getSize().get());
json.addProperty(size, icon.getPath().get());
}
return json;
}
private JsonObject generateCustomData(Map<String, Object> customData) {
JsonObject json = new JsonObject();
customData.forEach((name, o) -> json.add(name, LoomGradlePlugin.GSON.toJsonTree(o)));
return json;
}
}

View File

@@ -0,0 +1,143 @@
/*
* 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.fmj.gen;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
public final class GeneratorUtils {
private GeneratorUtils() {
}
public static void add(JsonObject json, String key, Property<String> property) {
add(json, key, property, JsonPrimitive::new);
}
public static void addRequired(JsonObject json, String key, Property<String> property) {
addRequired(json, key, property, JsonPrimitive::new);
}
public static void addStringOrArray(JsonObject json, String key, ListProperty<String> property) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, GeneratorUtils::stringOrArray);
}
public static <V> void addSingleOrArray(JsonObject json, String key, ListProperty<V> property, Function<V, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, entries -> singleOrArray(entries, converter));
}
public static <V> void addArray(JsonObject json, String key, ListProperty<V> property, Function<V, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, entries -> array(entries, converter));
}
public static <V, P extends Property<V>> void add(JsonObject json, String key, P property, Function<V, JsonElement> converter) {
if (!property.isPresent()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static <V, P extends Property<V>> void addRequired(JsonObject json, String key, P property, Function<V, JsonElement> converter) {
property.get(); // Ensure it's present
add(json, key, property, converter);
}
public static <V> void add(JsonObject json, String key, ListProperty<V> property, Function<List<V>, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static <K, V> void add(JsonObject json, String key, MapProperty<K, V> property, Function<Map<K, V>, JsonElement> converter) {
if (property.get().isEmpty()) {
return;
}
json.add(key, converter.apply(property.get()));
}
public static void add(JsonObject json, String key, MapProperty<String, String> property) {
if (property.get().isEmpty()) {
return;
}
add(json, key, property, GeneratorUtils::map);
}
public static JsonElement stringOrArray(List<String> strings) {
return singleOrArray(strings, JsonPrimitive::new);
}
public static <V> JsonElement singleOrArray(List<V> entries, Function<V, JsonElement> converter) {
if (entries.size() == 1) {
return converter.apply(entries.getFirst());
}
return array(entries, converter);
}
public static <V> JsonElement array(List<V> entries, Function<V, JsonElement> converter) {
JsonArray array = new JsonArray();
for (V entry : entries) {
array.add(converter.apply(entry));
}
return array;
}
public static JsonObject map(Map<String, String> map) {
JsonObject obj = new JsonObject();
for (Map.Entry<String, String> entry : map.entrySet()) {
obj.addProperty(entry.getKey(), entry.getValue());
}
return obj;
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.Specification
import spock.lang.Unroll
import net.fabricmc.loom.test.util.GradleProjectTestTrait
import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS
class FabricModJsonTask extends Specification implements GradleProjectTestTrait {
@Unroll
def "Generate FMJ"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: version)
gradle.buildGradle << '''
dependencies {
minecraft "com.mojang:minecraft:1.21.8"
mappings "net.fabricmc:yarn:1.21.8+build.1:v2"
}
tasks.register("generateModJson", net.fabricmc.loom.task.FabricModJsonV1Task) {
outputFile = file("fabric.mod.json")
json {
modId = "examplemod"
version = "1.0.0"
}
}
'''
when:
// Run the task twice to ensure its up to date
def result = gradle.run(task: "generateModJson")
then:
result.task(":generateModJson").outcome == SUCCESS
new File(gradle.projectDir, "fabric.mod.json").text == """
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0"
}
""".stripIndent().trim()
where:
version << STANDARD_TEST_VERSIONS
}
}

View File

@@ -0,0 +1,682 @@
/*
* 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.fmj
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.intellij.lang.annotations.Language
import spock.lang.Specification
import net.fabricmc.loader.impl.metadata.ModMetadataParser
import net.fabricmc.loom.api.fmj.FabricModJsonV1Spec
import net.fabricmc.loom.test.util.GradleTestUtil
import net.fabricmc.loom.util.fmj.gen.FabricModJsonV1Generator
class FabricModJsonV1GeneratorTest extends Specification {
static Project project = GradleTestUtil.mockProject()
static ObjectFactory objectFactory = project.getObjects()
def "minimal"() {
given:
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0"
}
""")
tryParse(json) == 1
}
def "single license"() {
given:
def spec = baseSpec()
spec.licenses.add("MIT")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"license": "MIT"
}
""")
tryParse(json) == 1
}
def "multiple licenses"() {
given:
def spec = baseSpec()
spec.licenses.addAll("MIT", "Apache-2.0")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"license": [
"MIT",
"Apache-2.0"
]
}
""")
tryParse(json) == 1
}
def "named author"() {
given:
def spec = baseSpec()
spec.author("Epic Modder")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"authors": [
"Epic Modder"
]
}
""")
tryParse(json) == 1
}
def "author with contact info"() {
given:
def spec = baseSpec()
spec.author("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"authors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
]
}
""")
tryParse(json) == 1
}
def "named contributor"() {
given:
def spec = baseSpec()
spec.contributor("Epic Modder")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contributors": [
"Epic Modder"
]
}
""")
tryParse(json) == 1
}
def "contributor with contact info"() {
given:
def spec = baseSpec()
spec.contributor("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contributors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
]
}
""")
tryParse(json) == 1
}
def "contact info"() {
given:
def spec = baseSpec()
spec.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
""")
tryParse(json) == 1
}
def "provides"() {
given:
def spec = baseSpec()
spec.provides.set(['oldid', 'veryoldid'])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"provides": [
"oldid",
"veryoldid"
]
}
""")
tryParse(json) == 1
}
def "environment"() {
given:
def spec = baseSpec()
spec.environment.set("client")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"environment": "client"
}
""")
tryParse(json) == 1
}
def "jars"() {
given:
def spec = baseSpec()
spec.jars.set(["libs/some-lib.jar"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"jars": [
{
"file": "libs/some-lib.jar"
}
]
}
""")
tryParse(json) == 1
}
def "entrypoints"() {
given:
def spec = baseSpec()
spec.entrypoint("main", "com.example.Main")
spec.entrypoint("main", "com.example.Blocks")
spec.entrypoint("client", "com.example.KotlinClient::init") {
it.adapter.set("kotlin")
}
spec.entrypoint("client") {
it.value.set("com.example.Client")
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"entrypoints": {
"client": [
{
"value": "com.example.KotlinClient::init",
"adapter": "kotlin"
},
"com.example.Client"
],
"main": [
"com.example.Main",
"com.example.Blocks"
]
}
}
""")
tryParse(json) == 1
}
def "mixins"() {
given:
def spec = baseSpec()
spec.mixin("mymod.mixins.json")
spec.mixin("mymod.client.mixins.json") {
it.environment.set("client")
}
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"mixins": [
"mymod.mixins.json",
{
"config": "mymod.client.mixins.json",
"environment": "client"
}
]
}
""")
tryParse(json) == 1
}
def "access widener"() {
given:
def spec = baseSpec()
spec.accessWidener.set("mymod.accesswidener")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"accessWidener": "mymod.accesswidener"
}
""")
tryParse(json) == 1
}
def "depends"() {
given:
def spec = baseSpec()
spec.depends("fabricloader", ">=0.14.0")
spec.depends("fabric-api", [">=0.14.0", "<0.15.0"])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"depends": {
"fabricloader": "fabricloader",
"fabric-api": [
"\\u003e\\u003d0.14.0",
"\\u003c0.15.0"
]
}
}
""")
tryParse(json) == 1
}
def "single icon"() {
given:
def spec = baseSpec()
spec.icon("icon.png")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"icon": "icon.png"
}
""")
tryParse(json) == 1
}
def "multiple icons"() {
given:
def spec = baseSpec()
spec.icon(64, "icon_64.png")
spec.icon(128, "icon_128.png")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"icon": {
"64": "icon_64.png",
"128": "icon_128.png"
}
}
""")
tryParse(json) == 1
}
def "language adapters"() {
given:
def spec = baseSpec()
spec.languageAdapters.put("kotlin", "net.fabricmc.loader.api.language.KotlinAdapter")
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"languageAdapters": {
"kotlin": "net.fabricmc.loader.api.language.KotlinAdapter"
}
}
""")
tryParse(json) == 1
}
def "custom data"() {
given:
def spec = baseSpec()
spec.customData.put("examplemap", ["custom": "data"])
spec.customData.put("examplelist", [1, 2, 3])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"custom": {
"examplemap": {
"custom": "data"
},
"examplelist": [
1,
2,
3
]
}
}
""")
tryParse(json) == 1
}
def "complete"() {
given:
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
spec.name.set("Example Mod")
spec.description.set("This is an example mod.")
spec.licenses.addAll("MIT", "Apache-2.0")
spec.author("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
spec.contributor("Epic Modder") {
it.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
}
spec.contactInformation.set(["discord": "epicmodder#1234", "email": "epicmodder@example.com"])
spec.provides.set(['oldid', 'veryoldid'])
spec.environment.set("client")
spec.jars.set(["libs/some-lib.jar"])
spec.entrypoint("main", "com.example.Main")
spec.entrypoint("main", "com.example.Blocks")
spec.entrypoint("client", "com.example.KotlinClient::init") {
it.adapter.set("kotlin")
}
spec.entrypoint("client") {
it.value.set("com.example.Client")
}
spec.mixin("mymod.mixins.json")
spec.mixin("mymod.client.mixins.json") {
it.environment.set("client")
}
spec.accessWidener.set("mymod.accesswidener")
spec.depends("fabricloader", ">=0.14.0")
spec.depends("fabric-api", [">=0.14.0", "<0.15.0"])
spec.recommends("recommended-mod", ">=1.0.0")
spec.suggests("suggested-mod", ">=1.0.0")
spec.conflicts("conflicting-mod", "<1.0.0")
spec.breaks("broken-mod", "<1.0.0")
spec.icon(64, "icon_64.png")
spec.icon(128, "icon_128.png")
spec.languageAdapters.put("kotlin", "net.fabricmc.loader.api.language.KotlinAdapter")
spec.customData.put("examplemap", ["custom": "data"])
spec.customData.put("examplelist", [1, 2, 3])
when:
def json = FabricModJsonV1Generator.INSTANCE.generate(spec)
then:
json == j("""
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"provides": [
"oldid",
"veryoldid"
],
"environment": "client",
"entrypoints": {
"client": [
{
"value": "com.example.KotlinClient::init",
"adapter": "kotlin"
},
"com.example.Client"
],
"main": [
"com.example.Main",
"com.example.Blocks"
]
},
"jars": [
{
"file": "libs/some-lib.jar"
}
],
"mixins": [
"mymod.mixins.json",
{
"config": "mymod.client.mixins.json",
"environment": "client"
}
],
"accessWidener": "mymod.accesswidener",
"depends": {
"fabricloader": "fabricloader",
"fabric-api": [
"\\u003e\\u003d0.14.0",
"\\u003c0.15.0"
]
},
"recommends": {
"recommended-mod": "recommended-mod"
},
"suggests": {
"suggested-mod": "suggested-mod"
},
"conflicts": {
"conflicting-mod": "conflicting-mod"
},
"breaks": {
"broken-mod": "broken-mod"
},
"name": "Example Mod",
"description": "This is an example mod.",
"authors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
],
"contributors": [
{
"name": "Epic Modder",
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
}
}
],
"contact": {
"discord": "epicmodder#1234",
"email": "epicmodder@example.com"
},
"license": [
"MIT",
"Apache-2.0"
],
"icon": {
"64": "icon_64.png",
"128": "icon_128.png"
},
"languageAdapters": {
"kotlin": "net.fabricmc.loader.api.language.KotlinAdapter"
},
"custom": {
"examplemap": {
"custom": "data"
},
"examplelist": [
1,
2,
3
]
}
}
""")
tryParse(json) == 1
}
// Ensure that Fabric loader can actually parse the generated JSON.
private static int tryParse(String json) {
def meta = new ByteArrayInputStream(json.bytes).withCloseable {
//noinspection GroovyAccessibility
ModMetadataParser.readModMetadata(it, false)
}
return meta.getSchemaVersion()
}
private static FabricModJsonV1Spec baseSpec() {
def spec = objectFactory.newInstance(FabricModJsonV1Spec.class)
spec.modId.set("examplemod")
spec.version.set("1.0.0")
return spec
}
private static String j(@Language("JSON") String json) {
return json.stripIndent().trim()
}
}