Add nestJars API for nesting locally built mod jars (#1427)

* Add nestJars API for FileCollection support

* Add missin new-line

* Fix imports

* Rework constructor to avoid ugliness

* Update java docs

* Update java docs

* Implement asked improvements

* Fix checkstyle
This commit is contained in:
Finn Rades
2025-11-07 22:06:07 +01:00
committed by GitHub
parent 6877f704a0
commit eca987a2d3
10 changed files with 166 additions and 28 deletions

View File

@@ -40,6 +40,8 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.publish.maven.MavenPublication;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import org.jetbrains.annotations.ApiStatus;
import net.fabricmc.loom.api.decompilers.DecompilerOptions;
@@ -272,4 +274,26 @@ public interface LoomGradleExtensionAPI {
* @return A lazily evaluated {@link FileCollection} containing the named minecraft jars.
*/
FileCollection getNamedMinecraftJars();
/**
* Nest mod jars from a {@link FileCollection} into the specified jar task.
* This is useful for including locally built mod jars or jars that don't come from Maven.
*
* <p>Important: The jars must already be valid mod jars (containing a fabric.mod.json file).
* Non-mod jars will be rejected.
*
* <p>Example usage:
* {@snippet lang=groovy :
* loom {
* nestJars(tasks.jar, files('local-mod.jar'))
* nestJars(tasks.remapJar, tasks.named('buildOtherMod'))
* }
* }
*
* @param jarTask the jar task to nest jars into (can be jar or remapJar)
* @param jars the file collection containing mod jars to nest
* @since 1.14
*/
@ApiStatus.Experimental
void nestJars(TaskProvider<? extends Jar> jarTask, FileCollection jars);
}

View File

@@ -45,6 +45,8 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.publish.maven.MavenPublication;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.InterfaceInjectionExtensionAPI;
@@ -543,5 +545,10 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA
public MixinExtension getMixin() {
throw new RuntimeException("Yeah... something is really wrong");
}
@Override
public void nestJars(TaskProvider<? extends Jar> jarTask, FileCollection jars) {
throw new RuntimeException("Yeah... something is really wrong");
}
}
}

View File

@@ -40,6 +40,8 @@ import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.jvm.tasks.Jar;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.LoomNoRemapGradlePlugin;
@@ -57,6 +59,8 @@ import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessorManager;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider;
import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider;
import net.fabricmc.loom.task.NestJarsAction;
import net.fabricmc.loom.task.RemapJarTask;
import net.fabricmc.loom.util.Constants;
import net.fabricmc.loom.util.download.Download;
import net.fabricmc.loom.util.download.DownloadBuilder;
@@ -339,4 +343,17 @@ public abstract class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl
public boolean disableObfuscation() {
return disableObfuscation.get();
}
@Override
public void nestJars(TaskProvider<? extends Jar> jarTask, FileCollection jars) {
jarTask.configure(task -> {
if (task instanceof RemapJarTask remapJarTask) {
// For RemapJarTask, add to the nestedJars property
remapJarTask.getNestedJars().from(jars);
} else {
// For regular Jar tasks (non-remap mode), add a NestJarsAction with the FileCollection
task.doLast(new NestJarsAction(jars));
}
});
}
}

View File

@@ -26,12 +26,12 @@ package net.fabricmc.loom.task;
import java.io.File;
import java.io.Serializable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Action;
import org.gradle.api.Task;
import org.gradle.api.file.Directory;
import org.gradle.api.provider.Provider;
import org.gradle.api.file.FileCollection;
import org.gradle.jvm.tasks.Jar;
import org.jetbrains.annotations.NotNull;
@@ -39,38 +39,26 @@ import net.fabricmc.loom.build.nesting.JarNester;
/**
* Configuration-cache-compatible action for nesting jars.
* Uses a provider to avoid capturing task references at configuration time.
* Uses a FileCollection to avoid capturing task references at configuration time.
* Do NOT turn me into a record!
*/
class NestJarsAction implements Action<Task>, Serializable {
private final Provider<Directory> nestedJarsDir;
public class NestJarsAction implements Action<Task>, Serializable {
private final FileCollection jars;
NestJarsAction(Provider<Directory> nestedJarsDir) {
this.nestedJarsDir = nestedJarsDir;
public NestJarsAction(FileCollection jars) {
this.jars = jars;
}
@Override
public void execute(@NotNull Task t) {
final Jar jarTask = (Jar) t;
final File jarFile = jarTask.getArchiveFile().get().getAsFile();
final List<File> allJars = new ArrayList<>(jars.getFiles());
if (!nestedJarsDir.isPresent()) {
return;
}
final File outputDir = nestedJarsDir.get().getAsFile();
if (outputDir.exists() && outputDir.isDirectory()) {
final File[] jars = outputDir.listFiles((dir, name) -> name.endsWith(".jar"));
if (jars != null && jars.length > 0) {
JarNester.nestJars(
Arrays.asList(jars),
jarFile,
jarTask.getLogger()
);
jarTask.getLogger().lifecycle("Nested {} jar(s) into {}", jars.length, jarFile.getName());
}
// Nest all collected jars
if (!allJars.isEmpty()) {
JarNester.nestJars(allJars, jarFile, jarTask.getLogger());
jarTask.getLogger().lifecycle("Nested {} jar(s) into {}", allJars.size(), jarFile.getName());
}
}
}
}

View File

@@ -52,10 +52,11 @@ public class NonRemappedJarTaskConfiguration {
project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class).configure(task -> {
task.dependsOn(processIncludeJarsTask);
// Use JarNester to properly add jars and update fabric.mod.json
task.doLast(new NestJarsAction(processIncludeJarsTask.flatMap(NestableJarGenerationTask::getOutputDirectory)));
task.doLast(new NestJarsAction(project.fileTree(processIncludeJarsTask.flatMap(NestableJarGenerationTask::getOutputDirectory))
.matching(pattern -> pattern.include("*.jar"))));
});
// Add jar task to unmapped collection
extension.getUnmappedModCollection().from(project.getTasks().getByName(JavaPlugin.JAR_TASK_NAME));
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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 NestJarsApiTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "nest jars using loom.nestJars() API (gradle #version)"() {
setup:
def gradle = gradleProject(project: "nestJarsApi", version: version)
when:
def result = gradle.run(tasks: ["remapJar"])
then:
result.task(":remapJar").outcome == SUCCESS
result.task(":createNestedMod").outcome == SUCCESS
// Assert the locally built mod jar is nested using the new API
gradle.hasOutputZipEntry("nestJarsApi.jar", "META-INF/jars/nested-mod.jar")
where:
version << STANDARD_TEST_VERSIONS
}
}

View File

@@ -0,0 +1,30 @@
plugins {
id 'fabric-loom'
}
repositories {
mavenCentral()
}
dependencies {
minecraft 'com.mojang:minecraft:1.20.1'
mappings loom.officialMojangMappings()
}
// Create a simple mod jar file to nest
task createNestedMod(type: Jar) {
archiveBaseName = 'nested-mod'
destinationDirectory = layout.buildDirectory.dir('nested-mods')
from('nested-mod-content') {
include 'fabric.mod.json'
}
}
// Use the new nestJars API to nest the locally built jar
loom {
nestJars(tasks.named('remapJar'), files(createNestedMod.outputs.files))
}
// Make sure remapJar depends on the nested mod being created
tasks.remapJar.dependsOn(createNestedMod)

View File

@@ -0,0 +1,8 @@
{
"schemaVersion": 1,
"id": "nestedmod",
"version": "1.0.0",
"name": "Nested Mod",
"description": "This mod will be nested using the nestJars API",
"environment": "*"
}

View File

@@ -0,0 +1 @@
rootProject.name = 'nestJarsApi'

View File

@@ -0,0 +1,8 @@
{
"schemaVersion": 1,
"id": "mainmod",
"version": "1.0.0",
"name": "Main Mod",
"description": "Main mod that nests another mod using nestJars API",
"environment": "*"
}