Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,30 @@ neoForge {
}
```

### Launch Metadata

If you need to launch a configured run from another Gradle task or an external tool, you can use the launch metadata
file generated for each run. This avoids depending on ModDevGradle's internal argument-file names.

For each run, ModDevGradle registers a `create<RunName>LaunchMetadata` task of type
`CreateLaunchMetadata`, which exposes the metadata file as an output:

```groovy
import net.neoforged.moddevgradle.tasks.CreateLaunchMetadata

def serverLaunchMetadata = tasks.named("createServerLaunchMetadata", CreateLaunchMetadata).flatMap {
it.metadataFile
}
```

The file is written to `build/moddev/<runName>LaunchMetadata.properties` and contains the Java executable, working
directory, DevLaunch main class, argument-file paths, mod-folder argument, and `environment.<name>` entries for run
environment variables.

Launchers should use the `workingDirectory` from the metadata. If a launcher needs disposable or test-specific state,
configure the run's `gameDirectory` to point at that directory instead of replacing the metadata working directory at
launch time.

### Jar-in-Jar

To embed external Jar-files into your mod file, you can use the `jarJar` configuration added by the plugin.
Expand Down
2 changes: 2 additions & 0 deletions gradle/gradle-daemon-jvm.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=17
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import net.neoforged.moddevgradle.dsl.RunModel;
import net.neoforged.moddevgradle.internal.utils.ExtensionUtils;
import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal;
import net.neoforged.moddevgradle.tasks.CreateLaunchMetadata;
import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
import net.neoforged.nfrtgradle.DownloadAssets;
import org.gradle.api.DomainObjectCollection;
Expand Down Expand Up @@ -266,6 +267,10 @@ public static void setupRuns(
task.setGroup(branding.publicTaskGroup());
task.setDescription("Creates batch files/shell scripts to launch the game from outside of Gradle (i.e. Renderdoc, NVidia Nsight, etc.)");
});
var createLaunchMetadataTask = project.getTasks().register("createLaunchMetadata", Task.class, task -> {
task.setGroup(branding.publicTaskGroup());
task.setDescription("Creates metadata files to launch the game from outside of Gradle or your IDE.");
});

Map<RunModel, TaskProvider<PrepareRun>> prepareRunTasks = new IdentityHashMap<>();
runs.all(run -> {
Expand All @@ -285,7 +290,8 @@ public static void setupRuns(
assetPropertiesFile,
devLaunchConfig,
versionCapabilities,
createLaunchScriptsTask);
createLaunchScriptsTask,
createLaunchMetadataTask);
prepareRunTasks.put(run, prepareRunTask);
});
ideIntegration.configureRuns(prepareRunTasks, runs);
Expand All @@ -308,7 +314,8 @@ private static TaskProvider<PrepareRun> setupRunInGradle(
Provider<RegularFile> assetPropertiesFile,
Configuration devLaunchConfig,
VersionCapabilitiesInternal versionCapabilities,
TaskProvider<Task> createLaunchScriptsTask) {
TaskProvider<Task> createLaunchScriptsTask,
TaskProvider<Task> createLaunchMetadataTask) {
var ideIntegration = IdeIntegration.of(project, branding);
var configurations = project.getConfigurations();
var javaExtension = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class);
Expand Down Expand Up @@ -392,6 +399,8 @@ private static TaskProvider<PrepareRun> setupRunInGradle(
});
ideIntegration.runTaskOnProjectSync(prepareRunTask);

var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null);

var launchScriptTask = tasks.register(InternalModelHelper.nameOfRun(run, "create", "launchScript"), CreateLaunchScriptTask.class, task -> {
task.setGroup(branding.internalTaskGroup());
task.setDescription("Creates a bash/shell-script to launch the " + run.getName() + " Minecraft run from outside Gradle or your IDE.");
Expand All @@ -405,10 +414,29 @@ private static TaskProvider<PrepareRun> setupRunInGradle(
task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath()));
task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath()));
task.getEnvironment().set(run.getEnvironment());
task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null));
task.getModFolders().set(modFoldersProvider);
});
createLaunchScriptsTask.configure(task -> task.dependsOn(launchScriptTask));

var launchMetadataTask = tasks.register(InternalModelHelper.nameOfRun(run, "create", "launchMetadata"), CreateLaunchMetadata.class, task -> {
task.setGroup(branding.internalTaskGroup());
task.setDescription("Creates launch metadata for the " + run.getName() + " Minecraft run.");

task.getWorkingDirectory().set(run.getGameDirectory().map(d -> d.getAsFile().getAbsolutePath()));
task.getJavaExecutable().set(launchScriptTask.flatMap(CreateLaunchScriptTask::getJavaExecutable));
task.getClasspathArgsFile().set(launchScriptTask.flatMap(CreateLaunchScriptTask::getClasspathArgsFile));
task.getVmArgsFile().set(prepareRunTask.flatMap(PrepareRun::getVmArgsFile));
task.getProgramArgsFile().set(prepareRunTask.flatMap(PrepareRun::getProgramArgsFile));
task.getMainClass().set(RunUtils.DEV_LAUNCH_MAIN_CLASS);
task.getModFoldersArgument().set(modFoldersProvider
.getClassesArgument()
.map(RunUtils::getModFoldersArgument));
task.getEnvironment().set(run.getEnvironment());
task.getMetadataFile().set(RunUtils.getLaunchMetadata(argFileDir, run));
task.dependsOn(launchScriptTask);
});
createLaunchMetadataTask.configure(task -> task.dependsOn(launchMetadataTask));

tasks.register(InternalModelHelper.nameOfRun(run, "run", ""), RunGameTask.class, task -> {
task.setGroup(branding.publicTaskGroup());
task.setDescription("Runs the " + run.getName() + " Minecraft run configuration.");
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public static Provider<RegularFile> getLaunchScript(Provider<Directory> modDevFo
return modDevFolder.map(dir -> dir.file(InternalModelHelper.nameOfRun(run, "run", scriptExtension)));
}

public static Provider<RegularFile> getLaunchMetadata(Provider<Directory> modDevFolder, RunModel run) {
return modDevFolder.map(dir -> dir.file(InternalModelHelper.nameOfRun(run, "", "launchMetadata.properties")));
}

public enum RunArgFile {
VMARGS("runVmArgs.txt"),
PROGRAMARGS("runProgramArgs.txt"),
Expand All @@ -177,6 +181,10 @@ public static ModFoldersProvider getGradleModFoldersProvider(Project project, Pr
return modFoldersProvider;
}

static String getModFoldersArgument(String classesArgument) {
return "-Dfml.modFolders=%s".formatted(classesArgument);
}

public static Project findSourceSetProject(Project someProject, SourceSet sourceSet) {
for (var s : ExtensionUtils.getSourceSets(someProject)) {
if (s == sourceSet) {
Expand Down Expand Up @@ -289,7 +297,7 @@ public ModFoldersProvider() {

@Internal
public String getArgument() {
return "-Dfml.modFolders=%s".formatted(getClassesArgument().get());
return RunUtils.getModFoldersArgument(getClassesArgument().get());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package net.neoforged.moddevgradle.tasks;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import net.neoforged.moddevgradle.internal.utils.FileUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;

/**
* Writes stable, machine-readable metadata for launching a configured Minecraft run.
*/
public abstract class CreateLaunchMetadata extends DefaultTask {
@Input
public abstract Property<String> getWorkingDirectory();

@Input
public abstract Property<String> getJavaExecutable();

@InputFile
@PathSensitive(PathSensitivity.ABSOLUTE)
public abstract RegularFileProperty getClasspathArgsFile();

@InputFile
@PathSensitive(PathSensitivity.ABSOLUTE)
public abstract RegularFileProperty getVmArgsFile();

@InputFile
@PathSensitive(PathSensitivity.ABSOLUTE)
public abstract RegularFileProperty getProgramArgsFile();

@Input
public abstract Property<String> getMainClass();

@Input
public abstract Property<String> getModFoldersArgument();

@Input
public abstract MapProperty<String, String> getEnvironment();

@OutputFile
public abstract RegularFileProperty getMetadataFile();

@TaskAction
public void createMetadata() throws IOException {
var metadata = new TreeMap<String, String>();
metadata.put("workingDirectory", getWorkingDirectory().get());
metadata.put("javaExecutable", getJavaExecutable().get());
metadata.put("classpathArgsFile", getClasspathArgsFile().get().getAsFile().getAbsolutePath());
metadata.put("vmArgsFile", getVmArgsFile().get().getAsFile().getAbsolutePath());
metadata.put("programArgsFile", getProgramArgsFile().get().getAsFile().getAbsolutePath());
metadata.put("mainClass", getMainClass().get());
metadata.put("modFolders", getModFoldersArgument().get());

for (var entry : getEnvironment().get().entrySet()) {
metadata.put("environment." + entry.getKey(), entry.getValue());
}

var lines = new ArrayList<String>(metadata.size());
for (Map.Entry<String, String> entry : metadata.entrySet()) {
lines.add(formatProperty(entry));
}

var destination = getMetadataFile().get().getAsFile().toPath();
Files.createDirectories(destination.getParent());
FileUtils.writeLinesSafe(destination, lines, StandardCharsets.ISO_8859_1);
}

private static String formatProperty(Map.Entry<String, String> entry) {
var properties = new Properties();
properties.setProperty(entry.getKey(), entry.getValue());

var output = new ByteArrayOutputStream();
try {
properties.store(output, null);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

return output.toString(StandardCharsets.ISO_8859_1)
.lines()
.filter(line -> !line.startsWith("#"))
.findFirst()
.orElseThrow();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package net.neoforged.moddevgradle.functional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Test;

public class LaunchMetadataFunctionalTest extends AbstractFunctionalTest {
@Test
public void testCreateServerLaunchMetadata() throws IOException {
writeFile(settingsFile, "rootProject.name = 'launch-metadata'");
writeGroovyBuildScript("""
plugins {
id "net.neoforged.moddev"
}

neoForge {
version = "{DEFAULT_NEOFORGE_VERSION}"
runs {
server {
server()
environment("MDG_TEST_ENV", "present")
}
}
}
""");

var result = GradleRunner.create()
.withPluginClasspath()
.withProjectDir(testProjectDir)
.withArguments("createServerLaunchMetadata", "--stacktrace")
.build();

assertEquals(TaskOutcome.SUCCESS, result.task(":createServerLaunchMetadata").getOutcome());
assertEquals(TaskOutcome.SUCCESS, result.task(":createServerLaunchScript").getOutcome());

var metadataFile = testProjectDir.toPath().resolve("build/moddev/serverLaunchMetadata.properties");
assertThat(metadataFile).exists();

var properties = loadProperties(metadataFile);

assertThat(properties.getProperty("workingDirectory")).endsWith("run");
assertThat(properties.getProperty("javaExecutable")).isNotBlank();
assertThat(properties.getProperty("mainClass")).isEqualTo("net.neoforged.devlaunch.Main");
assertThat(properties.getProperty("classpathArgsFile")).endsWith("serverRunClasspath.txt");
assertThat(properties.getProperty("vmArgsFile")).endsWith("serverRunVmArgs.txt");
assertThat(properties.getProperty("programArgsFile")).endsWith("serverRunProgramArgs.txt");
assertThat(properties.getProperty("modFolders")).startsWith("-Dfml.modFolders=");
assertThat(properties.getProperty("environment.MDG_TEST_ENV")).isEqualTo("present");

assertThat(Path.of(properties.getProperty("classpathArgsFile"))).exists();
assertThat(Path.of(properties.getProperty("vmArgsFile"))).exists();
assertThat(Path.of(properties.getProperty("programArgsFile"))).exists();
}

@Test
public void testCreateLaunchMetadataAggregate() throws IOException {
writeFile(settingsFile, "rootProject.name = 'launch-metadata-aggregate'");
writeGroovyBuildScript("""
plugins {
id "net.neoforged.moddev"
}

neoForge {
version = "{DEFAULT_NEOFORGE_VERSION}"
runs {
client {
client()
}
server {
server()
}
}
}
""");

var result = GradleRunner.create()
.withPluginClasspath()
.withProjectDir(testProjectDir)
.withArguments("createLaunchMetadata", "--stacktrace")
.build();

assertEquals(TaskOutcome.SUCCESS, result.task(":createLaunchMetadata").getOutcome());
assertThat(testProjectDir.toPath().resolve("build/moddev/clientLaunchMetadata.properties")).exists();
assertThat(testProjectDir.toPath().resolve("build/moddev/serverLaunchMetadata.properties")).exists();
}

private static Properties loadProperties(Path path) throws IOException {
var properties = new Properties();
try (var input = Files.newInputStream(path)) {
properties.load(input);
}
return properties;
}
}
Loading
Loading