Use an in-tree Gradle plugin instead of an external Gradle Plugin to make version-specific changes easier (#1485)

Co-authored-by: Sebastian Hartte <shartte@users.noreply.github.com>
This commit is contained in:
Bruno Ploumhans 2024-11-22 18:27:57 +01:00 committed by GitHub
parent 9480753414
commit 16b2d4d7cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2841 additions and 402 deletions

View File

@ -41,7 +41,7 @@ jobs:
run: ./gradlew generatePackageInfos
- name: Gen patches
run: ./gradlew :neoforge:unpackSourcePatches
run: ./gradlew :neoforge:genPatches
- name: Run datagen with Gradle
run: ./gradlew :neoforge:runData :tests:runData

View File

@ -4,7 +4,7 @@ import java.util.regex.Pattern
plugins {
id 'net.neoforged.gradleutils' version '3.0.0'
id 'com.diffplug.spotless' version '6.22.0' apply false
id 'net.neoforged.licenser' version '0.7.2'
id 'net.neoforged.licenser' version '0.7.5'
id 'neoforge.formatting-conventions'
id 'neoforge.versioning'
}
@ -23,19 +23,22 @@ System.out.println("NeoForge version ${project.version}")
allprojects {
version rootProject.version
group 'net.neoforged'
repositories {
mavenLocal()
}
}
subprojects {
apply plugin: 'java'
java.toolchain.languageVersion.set(JavaLanguageVersion.of(project.java_version))
}
repositories {
mavenCentral()
// Remove src/ sources from the root project. They are used in the neoforge subproject.
sourceSets {
main {
java {
srcDirs = []
}
resources {
srcDirs = []
}
}
}
// Put licenser here otherwise it tries to license all source sets including decompiled MC sources

81
buildSrc/README.md Normal file
View File

@ -0,0 +1,81 @@
# NeoForge Development Gradle Plugin
## NeoForge Project Structure
Before understanding the `buildSrc` plugin, one should understand the structure of the NeoForge Gradle project it is
applied to.
The project consists of a project tree with the following structure:
| Folder Path | Gradle Project Path | Applied Plugins | Description |
|------------------------------------------------------------------------|----------------------|:------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`/build.gradle`](../build.gradle) | `:` | &mdash; | The root project. Since this project is reused for Kits, the root project name is based on the checkout folder, which actually can lead to issues if it is called `NeoForge`. |
| [`/projects/neoforge/build.gradle`](../projects/neoforge/build.gradle) | `:neoforge` | [NeoDevPlugin](#neodevplugin) | The core NeoForge project, which produces the artifacts that will be published. |
| [`/projects/base/build.gradle`](../projects/base/build.gradle) | `:base` | [NeoDevBasePlugin](#neodevbaseplugin) | A utility project that contains the Minecraft sources without any NeoForge additions. Can be used to quickly compare what NeoForge has changed. |
| [`/tests/build.gradle`](../tests/build.gradle) | `:tests` | [NeoDevExtraPlugin](#neodevextraplugin) | Contains the game and unit tests for NeoForge. |
| [`/testframework/build.gradle`](../testframework/build.gradle) | `:testframework` | [MinecraftDependenciesPlugin](#minecraftdependenciesplugin) | A library providing support classes around writing game tests. |
| [`/coremods/build.gradle`](../coremods/build.gradle) | `:neoforge-coremods` | &mdash; | Java Bytecode transformers that are embedded into NeoForge as a nested Jar file. |
|
## Plugins
### NeoDevBasePlugin
Sources: [NeoDevBasePlugin.java](src/main/java/net/neoforged/neodev/NeoDevBasePlugin.java)
Implicitly applies: [MinecraftDependenciesPlugin](#minecraftdependenciesplugin).
Sets up a `setup` task that reuses code from [NeoDevPlugin](#neodevplugin) to decompile Minecraft and place the
decompiled sources in `projects/base/src/main/java`.
### NeoDevPlugin
Sources: [NeoDevPlugin.java](src/main/java/net/neoforged/neodev/NeoDevPlugin.java)
Implicitly applies: [MinecraftDependenciesPlugin](#minecraftdependenciesplugin).
This is the primary of this repository and is used to configure the `neoforge` subproject.
#### Setup
It creates a `setup` task that performs the following actions via various subtasks:
- Decompile Minecraft using the [NeoForm Runtime](https://github.com/neoforged/neoformruntime) and Minecraft version specific [NeoForm data](https://github.com/neoforged/NeoForm).
- Applies [Access Transformers](../src/main/resources/META-INF/accesstransformer.cfg) to Minecraft sources.
- Applies [NeoForge patches](../patches) to Minecraft sources. Any rejects are saved to the `/rejects` folder in the repository for manual inspection. During updates to new versions, the task can be run with `-Pupdating=true` to apply patches more leniently.
- Unpacks the patched sources to `projects/neoforge/src/main/java`.
#### Config Generation
The plugin creates and configures the tasks to create various configuration files used downstream to develop
mods with this version of NeoForge ([CreateUserDevConfig](src/main/java/net/neoforged/neodev/CreateUserDevConfig.java)), or install it ([CreateInstallerProfile](src/main/java/net/neoforged/neodev/installer/CreateInstallerProfile.java) and [CreateLauncherProfile](src/main/java/net/neoforged/neodev/installer/CreateLauncherProfile.java)).
A separate userdev profile is created for use by other subprojects in this repository.
The only difference is that it uses the FML launch types ending in `dev` rather than `userdev`.
#### Patch Generation
NeoForge injects its hooks into Minecraft by patching the decompiled source code.
Changes are made locally to the decompiled and patched source.
Since that source cannot be published, patches need to be generated before checking in.
The plugin configures the necessary task to do this
([GenerateSourcePatches](src/main/java/net/neoforged/neodev/GenerateSourcePatches.java)).
The source patches are only used during development of NeoForge itself and development of mods that use Gradle plugins implementing the decompile/patch/recompile pipeline.
For use by the installer intended for players as well as Gradle plugins wanting to replicate the production artifacts more closely, binary patches are generated using the ([GenerateBinaryPatches](src/main/java/net/neoforged/neodev/GenerateBinaryPatches.java)) task.
### NeoDevExtraPlugin
Sources: [NeoDevExtraPlugin.java](src/main/java/net/neoforged/neodev/NeoDevExtraPlugin.java)
This plugin can be applied to obtain a dependency on the `neoforge` project to depend on NeoForge including Minecraft
itself. Besides wiring up the dependency, it also creates run configurations based on the run-types defined in the
`neoforge` project.
### MinecraftDependenciesPlugin
This plugin is reused from [ModDevGradle](https://github.com/neoforged/ModDevGradle/).
It sets up repositories and attributes such that
the [libraries that Minecraft itself depends upon](https://github.com/neoforged/GradleMinecraftDependencies) can be
used.

View File

@ -1,3 +1,26 @@
plugins {
id 'java-gradle-plugin'
id 'groovy-gradle-plugin'
}
repositories {
gradlePluginPortal()
mavenCentral()
maven {
name = "NeoForged"
url = "https://maven.neoforged.net/releases"
content {
includeGroup "codechicken"
includeGroup "net.neoforged"
}
}
}
dependencies {
// buildSrc is an includedbuild of the parent directory (gradle.parent)
// ../settings.gradle sets these version properties accordingly
implementation "net.neoforged:moddev-gradle:${gradle.parent.ext.moddevgradle_plugin_version}"
implementation "com.google.code.gson:gson:${gradle.parent.ext.gson_version}"
implementation "io.codechicken:DiffPatch:${gradle.parent.ext.diffpatch_version}"
}

0
buildSrc/settings.gradle Normal file
View File

View File

@ -2,9 +2,14 @@ import java.util.regex.Matcher
project.plugins.apply('com.diffplug.spotless')
final generatePackageInfos = tasks.register('generatePackageInfos', Task) {
doLast {
fileTree('src/main/java').each { javaFile ->
abstract class GeneratePackageInfos extends DefaultTask {
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
abstract ConfigurableFileCollection getFiles();
@TaskAction
void generatePackageInfos() {
getFiles().each { javaFile ->
def packageInfoFile = new File(javaFile.parent, 'package-info.java')
if (!packageInfoFile.exists()) {
def pkgName = javaFile.toString().replaceAll(Matcher.quoteReplacement(File.separator), '/')
@ -27,6 +32,9 @@ final generatePackageInfos = tasks.register('generatePackageInfos', Task) {
}
}
}
final generatePackageInfos = tasks.register('generatePackageInfos', GeneratePackageInfos) {
it.files.from fileTree("src/main/java")
}
spotless {
java {

View File

@ -0,0 +1,72 @@
package net.neoforged.neodev;
import net.neoforged.neodev.utils.FileUtils;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/**
* Runs <a href="https://github.com/neoforged/JavaSourceTransformer">JavaSourceTransformer</a> to apply
* access transformers to the Minecraft source code for extending the access level of existing classes/methods/etc.
* <p>
* Note that at runtime, FML also applies access transformers.
*/
abstract class ApplyAccessTransformer extends JavaExec {
@InputFile
public abstract RegularFileProperty getInputJar();
@InputFile
public abstract RegularFileProperty getAccessTransformer();
@Input
public abstract Property<Boolean> getValidate();
@OutputFile
public abstract RegularFileProperty getOutputJar();
// Used to give JST more information about the classes.
@Classpath
public abstract ConfigurableFileCollection getLibraries();
@Internal
public abstract RegularFileProperty getLibrariesFile();
@Inject
public ApplyAccessTransformer() {}
@Override
@TaskAction
public void exec() {
try {
FileUtils.writeLinesSafe(
getLibrariesFile().getAsFile().get().toPath(),
getLibraries().getFiles().stream().map(File::getAbsolutePath).toList(),
StandardCharsets.UTF_8);
} catch (IOException exception) {
throw new UncheckedIOException("Failed to write libraries for JST.", exception);
}
args(
"--enable-accesstransformers",
"--access-transformer", getAccessTransformer().getAsFile().get().getAbsolutePath(),
"--access-transformer-validation", getValidate().get() ? "error" : "log",
"--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath(),
getInputJar().getAsFile().get().getAbsolutePath(),
getOutputJar().getAsFile().get().getAbsolutePath());
super.exec();
}
}

View File

@ -0,0 +1,79 @@
package net.neoforged.neodev;
import io.codechicken.diffpatch.cli.PatchOperation;
import io.codechicken.diffpatch.util.Input.MultiInput;
import io.codechicken.diffpatch.util.Output.MultiOutput;
import io.codechicken.diffpatch.util.PatchMode;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.gradle.work.DisableCachingByDefault;
import javax.inject.Inject;
import java.io.IOException;
/**
* Applies Java source patches to a source jar and produces a patched source jar as an output.
* It can optionally store rejected hunks into a given folder, which is primarily used for updating
* when the original sources changed and some hunks are expected to fail.
*/
@DisableCachingByDefault(because = "Not worth caching")
abstract class ApplyPatches extends DefaultTask {
@InputFile
public abstract RegularFileProperty getOriginalJar();
@InputDirectory
@PathSensitive(PathSensitivity.NONE)
public abstract DirectoryProperty getPatchesFolder();
@OutputFile
public abstract RegularFileProperty getPatchedJar();
@OutputDirectory
public abstract DirectoryProperty getRejectsFolder();
@Input
protected abstract Property<Boolean> getIsUpdating();
@Inject
public ApplyPatches(Project project) {
getIsUpdating().set(project.getProviders().gradleProperty("updating").map(Boolean::parseBoolean).orElse(false));
}
@TaskAction
public void applyPatches() throws IOException {
var isUpdating = getIsUpdating().get();
var builder = PatchOperation.builder()
.logTo(getLogger()::lifecycle)
.baseInput(MultiInput.detectedArchive(getOriginalJar().get().getAsFile().toPath()))
.patchesInput(MultiInput.folder(getPatchesFolder().get().getAsFile().toPath()))
.patchedOutput(MultiOutput.detectedArchive(getPatchedJar().get().getAsFile().toPath()))
.rejectsOutput(MultiOutput.folder(getRejectsFolder().get().getAsFile().toPath()))
.mode(isUpdating ? PatchMode.FUZZY : PatchMode.ACCESS)
.aPrefix("a/")
.bPrefix("b/")
.level(isUpdating ? io.codechicken.diffpatch.util.LogLevel.ALL : io.codechicken.diffpatch.util.LogLevel.WARN)
.minFuzz(0.9f); // The 0.5 default in DiffPatch is too low.
var result = builder.build().operate();
int exit = result.exit;
if (exit != 0 && exit != 1) {
throw new RuntimeException("DiffPatch failed with exit code: " + exit);
}
if (exit != 0 && !isUpdating) {
throw new RuntimeException("Patches failed to apply.");
}
}
}

View File

@ -0,0 +1,36 @@
package net.neoforged.neodev;
import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.OutputFile;
import javax.inject.Inject;
abstract class CreateCleanArtifacts extends CreateMinecraftArtifacts {
@OutputFile
abstract RegularFileProperty getCleanClientJar();
@OutputFile
abstract RegularFileProperty getRawServerJar();
@OutputFile
abstract RegularFileProperty getCleanServerJar();
@OutputFile
abstract RegularFileProperty getCleanJoinedJar();
@OutputFile
abstract RegularFileProperty getMergedMappings();
@Inject
public CreateCleanArtifacts() {
getAdditionalResults().put("node.stripClient.output.output", getCleanClientJar().getAsFile());
getAdditionalResults().put("node.downloadServer.output.output", getRawServerJar().getAsFile());
getAdditionalResults().put("node.stripServer.output.output", getCleanServerJar().getAsFile());
getAdditionalResults().put("node.rename.output.output", getCleanJoinedJar().getAsFile());
getAdditionalResults().put("node.mergeMappings.output.output", getMergedMappings().getAsFile());
// TODO: does anyone care about this? they should be contained in the client mappings
//"--write-result", "node.downloadServerMappings.output.output:" + getServerMappings().get().getAsFile().getAbsolutePath()
}
}

View File

@ -0,0 +1,203 @@
package net.neoforged.neodev;
import com.google.gson.GsonBuilder;
import net.neoforged.neodev.utils.FileUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Creates the userdev configuration file used by the various Gradle plugins used to develop
* mods for NeoForge, such as <a href="https://github.com/architectury/architectury-loom">Architectury Loom</a>,
* <a href="https://github.com/neoforged/ModDevGradle/">ModDevGradle
* or <a href="https://github.com/neoforged/NeoGradle">NeoGradle</a>.
*/
abstract class CreateUserDevConfig extends DefaultTask {
@Inject
public CreateUserDevConfig() {}
/**
* Toggles the launch type written to the userdev configuration between *dev and *userdev.
*/
@Input
abstract Property<Boolean> getForNeoDev();
@Input
abstract Property<String> getFmlVersion();
@Input
abstract Property<String> getMinecraftVersion();
@Input
abstract Property<String> getNeoForgeVersion();
@Input
abstract Property<String> getRawNeoFormVersion();
@Input
abstract ListProperty<String> getLibraries();
@Input
abstract ListProperty<String> getModules();
@Input
abstract ListProperty<String> getTestLibraries();
@Input
abstract ListProperty<String> getIgnoreList();
@Input
abstract Property<String> getBinpatcherGav();
@OutputFile
abstract RegularFileProperty getUserDevConfig();
@TaskAction
public void writeUserDevConfig() throws IOException {
var config = new UserDevConfig(
2,
"net.neoforged:neoform:%s-%s@zip".formatted(getMinecraftVersion().get(), getRawNeoFormVersion().get()),
"ats/",
"joined.lzma",
new BinpatcherConfig(
getBinpatcherGav().get(),
List.of("--clean", "{clean}", "--output", "{output}", "--apply", "{patch}")),
"patches/",
"net.neoforged:neoforge:%s:sources".formatted(getNeoForgeVersion().get()),
"net.neoforged:neoforge:%s:universal".formatted(getNeoForgeVersion().get()),
getLibraries().get(),
getTestLibraries().get(),
new LinkedHashMap<>(),
getModules().get());
for (var runType : RunType.values()) {
var launchTarget = switch (runType) {
case CLIENT -> "forgeclient";
case DATA -> "forgedata";
case GAME_TEST_SERVER, SERVER -> "forgeserver";
case JUNIT -> "forgejunit";
} + (getForNeoDev().get() ? "dev" : "userdev");
List<String> args = new ArrayList<>();
Collections.addAll(args,
"--launchTarget", launchTarget);
if (runType == RunType.CLIENT || runType == RunType.JUNIT) {
// TODO: this is copied from NG but shouldn't it be the MC version?
Collections.addAll(args,
"--version", getNeoForgeVersion().get());
}
if (runType == RunType.CLIENT || runType == RunType.DATA || runType == RunType.JUNIT) {
Collections.addAll(args,
"--assetIndex", "{asset_index}",
"--assetsDir", "{assets_root}");
}
Collections.addAll(args,
"--gameDir", ".",
"--fml.fmlVersion", getFmlVersion().get(),
"--fml.mcVersion", getMinecraftVersion().get(),
"--fml.neoForgeVersion", getNeoForgeVersion().get(),
"--fml.neoFormVersion", getRawNeoFormVersion().get());
Map<String, String> systemProperties = new LinkedHashMap<>();
systemProperties.put("java.net.preferIPv6Addresses", "system");
systemProperties.put("ignoreList", String.join(",", getIgnoreList().get()));
systemProperties.put("legacyClassPath.file", "{minecraft_classpath_file}");
if (runType == RunType.CLIENT || runType == RunType.GAME_TEST_SERVER) {
systemProperties.put("neoforge.enableGameTest", "true");
if (runType == RunType.GAME_TEST_SERVER) {
systemProperties.put("neoforge.gameTestServer", "true");
}
}
config.runs().put(runType.jsonName, new UserDevRunType(
runType != RunType.JUNIT,
"cpw.mods.bootstraplauncher.BootstrapLauncher",
args,
List.of(
"-p", "{modules}",
"--add-modules", "ALL-MODULE-PATH",
"--add-opens", "java.base/java.util.jar=cpw.mods.securejarhandler",
"--add-opens", "java.base/java.lang.invoke=cpw.mods.securejarhandler",
"--add-exports", "java.base/sun.security.util=cpw.mods.securejarhandler",
"--add-exports", "jdk.naming.dns/com.sun.jndi.dns=java.naming"),
runType == RunType.CLIENT || runType == RunType.JUNIT,
runType == RunType.GAME_TEST_SERVER || runType == RunType.SERVER,
runType == RunType.DATA,
runType == RunType.CLIENT || runType == RunType.GAME_TEST_SERVER,
runType == RunType.JUNIT,
Map.of(
"MOD_CLASSES", "{source_roots}"),
systemProperties
));
}
FileUtils.writeStringSafe(
getUserDevConfig().getAsFile().get().toPath(),
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(config),
// TODO: Not sure what this should be? Most likely the file is ASCII.
StandardCharsets.UTF_8);
}
private enum RunType {
CLIENT("client"),
DATA("data"),
GAME_TEST_SERVER("gameTestServer"),
SERVER("server"),
JUNIT("junit");
private final String jsonName;
RunType(String jsonName) {
this.jsonName = jsonName;
}
}
}
record UserDevConfig(
int spec,
String mcp,
String ats,
String binpatches,
BinpatcherConfig binpatcher,
String patches,
String sources,
String universal,
List<String> libraries,
List<String> testLibraries,
Map<String, UserDevRunType> runs,
List<String> modules) {}
record BinpatcherConfig(
String version,
List<String> args) {}
record UserDevRunType(
boolean singleInstance,
String main,
List<String> args,
List<String> jvmArgs,
boolean client,
boolean server,
boolean dataGenerator,
boolean gameTest,
boolean unitTest,
Map<String, String> env,
Map<String, String> props) {}

View File

@ -0,0 +1,70 @@
package net.neoforged.neodev;
import org.gradle.api.GradleException;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import javax.inject.Inject;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
abstract class GenerateBinaryPatches extends JavaExec {
@Inject
public GenerateBinaryPatches() {}
/**
* The jar file containing classes in the base state.
*/
@InputFile
abstract RegularFileProperty getCleanJar();
/**
* The jar file containing classes in the desired target state.
*/
@InputFile
abstract RegularFileProperty getPatchedJar();
@InputFile
abstract RegularFileProperty getMappings();
/**
* This directory of patch files for the Java sources is used as a hint to only diff class files that
* supposedly have changed. If it is not set, the tool will diff every .class file instead.
*/
@InputDirectory
@Optional
abstract DirectoryProperty getSourcePatchesFolder();
/**
* The location where the LZMA compressed binary patches are written to.
*/
@OutputFile
abstract RegularFileProperty getOutputFile();
@Override
public void exec() {
args("--clean", getCleanJar().get().getAsFile().getAbsolutePath());
args("--dirty", getPatchedJar().get().getAsFile().getAbsolutePath());
args("--srg", getMappings().get().getAsFile().getAbsolutePath());
if (getSourcePatchesFolder().isPresent()) {
args("--patches", getSourcePatchesFolder().get().getAsFile().getAbsolutePath());
}
args("--output", getOutputFile().get().getAsFile().getAbsolutePath());
var logFile = new File(getTemporaryDir(), "console.log");
try (var out = new BufferedOutputStream(new FileOutputStream(logFile))) {
getLogger().info("Logging binpatcher console output to {}", logFile.getAbsolutePath());
setStandardOutput(out);
super.exec();
} catch (IOException e) {
throw new GradleException("Failed to create binary patches.", e);
}
}
}

View File

@ -0,0 +1,55 @@
package net.neoforged.neodev;
import io.codechicken.diffpatch.cli.CliOperation;
import io.codechicken.diffpatch.cli.DiffOperation;
import io.codechicken.diffpatch.util.Input.MultiInput;
import io.codechicken.diffpatch.util.Output.MultiOutput;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputDirectory;
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;
import javax.inject.Inject;
import java.io.IOException;
abstract class GenerateSourcePatches extends DefaultTask {
@InputFile
public abstract RegularFileProperty getOriginalJar();
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getModifiedSources();
@OutputFile
public abstract RegularFileProperty getPatchesJar();
@Inject
public GenerateSourcePatches() {}
@TaskAction
public void generateSourcePatches() throws IOException {
var builder = DiffOperation.builder()
.logTo(getLogger()::lifecycle)
.baseInput(MultiInput.detectedArchive(getOriginalJar().get().getAsFile().toPath()))
.changedInput(MultiInput.folder(getModifiedSources().get().getAsFile().toPath()))
.patchesOutput(MultiOutput.detectedArchive(getPatchesJar().get().getAsFile().toPath()))
.autoHeader(true)
.level(io.codechicken.diffpatch.util.LogLevel.WARN)
.summary(false)
.aPrefix("a/")
.bPrefix("b/")
.lineEnding("\n");
CliOperation.Result<DiffOperation.DiffSummary> result = builder.build().operate();
int exit = result.exit;
if (exit != 0 && exit != 1) {
throw new RuntimeException("DiffPatch failed with exit code: " + exit);
}
}
}

View File

@ -0,0 +1,24 @@
package net.neoforged.neodev;
import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.Sync;
public class NeoDevBasePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// These plugins allow us to declare dependencies on Minecraft libraries needed to compile the official sources
project.getPlugins().apply(MinecraftDependenciesPlugin.class);
var createSources = NeoDevPlugin.configureMinecraftDecompilation(project);
project.getTasks().register("setup", Sync.class, task -> {
task.setGroup(NeoDevPlugin.GROUP);
task.setDescription("Replaces the contents of the base project sources with the unpatched, decompiled Minecraft source code.");
task.from(project.zipTree(createSources.flatMap(CreateMinecraftArtifacts::getSourcesArtifact)));
task.into(project.file("src/main/java/"));
});
}
}

View File

@ -0,0 +1,204 @@
package net.neoforged.neodev;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.attributes.Bundling;
import org.gradle.api.plugins.JavaPlugin;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Helper class to keep track of the many {@link Configuration}s used for the {@code neoforge} project.
*/
class NeoDevConfigurations {
static NeoDevConfigurations createAndSetup(Project project) {
return new NeoDevConfigurations(project);
}
//
// Configurations against which dependencies should be declared ("dependency scopes").
//
/**
* Only the NeoForm data zip and the dependencies to run NeoForm.
* Does not contain the dependencies to run vanilla Minecraft.
*/
final Configuration neoFormData;
/**
* Only the NeoForm dependencies.
* These are the dependencies required to run NeoForm-decompiled Minecraft.
* Does not contain the dependencies to run the NeoForm process itself.
*/
final Configuration neoFormDependencies;
/**
* Libraries used by NeoForge at compilation and runtime.
* These will end up on the MC-BOOTSTRAP module layer.
*/
final Configuration libraries;
/**
* Libraries used by NeoForge at compilation and runtime that need to be placed on the jvm's module path to end up in the boot layer.
* Currently, this only contains the few dependencies that are needed to create the MC-BOOTSTRAP module layer.
* (i.e. BootstrapLauncher and its dependencies).
*/
final Configuration moduleLibraries;
/**
* Libraries that should be accessible in mod development environments at compilation time only.
* Currently, this is only used for MixinExtras, which is already available at runtime via JiJ in the NeoForge universal jar.
*/
final Configuration userdevCompileOnly;
/**
* Libraries that should be accessible at runtime in unit tests.
* Currently, this only contains the fml-junit test fixtures.
*/
final Configuration userdevTestFixtures;
//
// Resolvable configurations.
//
/**
* Resolved {@link #neoFormData}.
* This is used to add NeoForm to the installer libraries.
* Only the zip is used (for the mappings), not the NeoForm tools, so it's not transitive.
*/
final Configuration neoFormDataOnly;
/**
* Resolvable {@link #neoFormDependencies}.
*/
final Configuration neoFormClasspath;
/**
* Resolvable {@link #moduleLibraries}.
*/
final Configuration modulePath;
/**
* Userdev dependencies (written to a json file in the userdev jar).
* This should contain all of NeoForge's additional dependencies for userdev,
* but does not need to include Minecraft or NeoForm's libraries.
*/
final Configuration userdevClasspath;
/**
* Resolvable {@link #userdevCompileOnly}, to add these entries to the ignore list of BootstrapLauncher.
*/
final Configuration userdevCompileOnlyClasspath;
/**
* Resolvable {@link #userdevTestFixtures}, to write it in the userdev jar.
*/
final Configuration userdevTestClasspath;
/**
* Libraries that need to be added to the classpath when launching NeoForge through the launcher.
* This contains all dependencies added by NeoForge, but does not include all of Minecraft's libraries.
* This is also used to produce the legacy classpath file for server installs.
*/
final Configuration launcherProfileClasspath;
//
// The configurations for resolution only are declared in the build.gradle file.
//
/**
* To download each executable tool, we use a resolvable configuration.
* These configurations support both declaration and resolution.
*/
final Map<Tools, Configuration> toolClasspaths;
private static Configuration dependencyScope(ConfigurationContainer configurations, String name) {
return configurations.create(name, configuration -> {
configuration.setCanBeConsumed(false);
configuration.setCanBeResolved(false);
});
}
private static Configuration resolvable(ConfigurationContainer configurations, String name) {
return configurations.create(name, configuration -> {
configuration.setCanBeConsumed(false);
configuration.setCanBeDeclared(false);
});
}
private NeoDevConfigurations(Project project) {
var configurations = project.getConfigurations();
neoFormData = dependencyScope(configurations, "neoFormData");
neoFormDependencies = dependencyScope(configurations, "neoFormDependencies");
libraries = dependencyScope(configurations, "libraries");
moduleLibraries = dependencyScope(configurations, "moduleLibraries");
userdevCompileOnly = dependencyScope(configurations, "userdevCompileOnly");
userdevTestFixtures = dependencyScope(configurations, "userdevTestFixtures");
neoFormDataOnly = resolvable(configurations, "neoFormDataOnly");
neoFormClasspath = resolvable(configurations, "neoFormClasspath");
modulePath = resolvable(configurations, "modulePath");
userdevClasspath = resolvable(configurations, "userdevClasspath");
userdevCompileOnlyClasspath = resolvable(configurations, "userdevCompileOnlyClasspath");
userdevTestClasspath = resolvable(configurations, "userdevTestClasspath");
launcherProfileClasspath = resolvable(configurations, "launcherProfileClasspath");
// Libraries & module libraries & MC dependencies need to be available when compiling in NeoDev,
// and on the runtime classpath too for IDE debugging support.
configurations.getByName("implementation").extendsFrom(libraries, moduleLibraries, neoFormDependencies);
// runtimeClasspath is our reference for all MC dependency versions.
// Make sure that any classpath we resolve is consistent with it.
var runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
neoFormDataOnly.setTransitive(false);
neoFormDataOnly.extendsFrom(neoFormData);
neoFormClasspath.extendsFrom(neoFormDependencies);
modulePath.extendsFrom(moduleLibraries);
modulePath.shouldResolveConsistentlyWith(runtimeClasspath);
userdevClasspath.extendsFrom(libraries, moduleLibraries, userdevCompileOnly);
userdevClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
userdevCompileOnlyClasspath.extendsFrom(userdevCompileOnly);
userdevCompileOnlyClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
userdevTestClasspath.extendsFrom(userdevTestFixtures);
userdevTestClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
launcherProfileClasspath.extendsFrom(libraries, moduleLibraries);
launcherProfileClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
toolClasspaths = createToolClasspaths(project);
}
private static Map<Tools, Configuration> createToolClasspaths(Project project) {
var configurations = project.getConfigurations();
var dependencyFactory = project.getDependencyFactory();
var result = new HashMap<Tools, Configuration>();
for (var tool : Tools.values()) {
var configuration = configurations.create(tool.getGradleConfigurationName(), spec -> {
spec.setDescription("Resolves the executable for tool " + tool.name());
spec.setCanBeConsumed(false);
// Tools are considered to be executable jars.
// Gradle requires the classpath for JavaExec to only contain a single file for these.
if (tool.isRequestFatJar()) {
spec.attributes(attr -> {
attr.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED));
});
}
var gav = tool.asGav(project);
spec.getDependencies().add(dependencyFactory.create(gav));
});
result.put(tool, configuration);
}
return Map.copyOf(result);
}
/**
* Gets a configuration representing the classpath for an executable tool.
* Some tools are assumed to be executable jars, and their configurations only contain a single file.
*/
public Configuration getExecutableTool(Tools tool) {
return Objects.requireNonNull(toolClasspaths.get(tool));
}
}

View File

@ -0,0 +1,35 @@
package net.neoforged.neodev;
import net.neoforged.moddevgradle.dsl.ModModel;
import net.neoforged.moddevgradle.dsl.RunModel;
import org.gradle.api.Action;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Project;
public class NeoDevExtension {
public static final String NAME = "neoDev";
private final NamedDomainObjectContainer<ModModel> mods;
private final NamedDomainObjectContainer<RunModel> runs;
public NeoDevExtension(Project project) {
mods = project.container(ModModel.class);
runs = project.container(RunModel.class, name -> project.getObjects().newInstance(RunModel.class, name, project, mods));
}
public NamedDomainObjectContainer<ModModel> getMods() {
return mods;
}
public void mods(Action<NamedDomainObjectContainer<ModModel>> action) {
action.execute(mods);
}
public NamedDomainObjectContainer<RunModel> getRuns() {
return runs;
}
public void runs(Action<NamedDomainObjectContainer<RunModel>> action) {
action.execute(runs);
}
}

View File

@ -0,0 +1,79 @@
package net.neoforged.neodev;
import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
import net.neoforged.moddevgradle.internal.NeoDevFacade;
import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
import net.neoforged.nfrtgradle.DownloadAssets;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ProjectDependency;
import org.gradle.api.artifacts.dsl.DependencyFactory;
import org.gradle.api.tasks.testing.Test;
import java.util.function.Consumer;
// TODO: the only point of this is to configure runs that depend on neoforge. Maybe this could be done with less code duplication...
// TODO: Gradle says "thou shalt not referenceth otherth projects" yet here we are
// TODO: depend on neoforge configurations that the moddev plugin also uses
public class NeoDevExtraPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(MinecraftDependenciesPlugin.class);
var neoForgeProject = project.getRootProject().getChildProjects().get("neoforge");
var dependencyFactory = project.getDependencyFactory();
var tasks = project.getTasks();
var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
var extension = project.getExtensions().create(NeoDevExtension.NAME, NeoDevExtension.class);
var modulePathDependency = projectDep(dependencyFactory, neoForgeProject, "net.neoforged:neoforge-moddev-module-path");
// TODO: this is temporary
var downloadAssets = neoForgeProject.getTasks().named("downloadAssets", DownloadAssets.class);
var writeNeoDevConfig = neoForgeProject.getTasks().named("writeNeoDevConfig", CreateUserDevConfig.class);
Consumer<Configuration> configureLegacyClasspath = spec -> {
spec.getDependencies().add(projectDep(dependencyFactory, neoForgeProject, "net.neoforged:neoforge-dependencies"));
};
extension.getRuns().configureEach(run -> {
configureLegacyClasspath.accept(run.getAdditionalRuntimeClasspathConfiguration());
});
NeoDevFacade.setupRuns(
project,
neoDevBuildDir,
extension.getRuns(),
writeNeoDevConfig,
modulePath -> modulePath.getDependencies().add(modulePathDependency),
configureLegacyClasspath,
downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
);
var testExtension = project.getExtensions().create(NeoDevTestExtension.NAME, NeoDevTestExtension.class);
var testTask = tasks.register("junitTest", Test.class, test -> test.setGroup("verification"));
tasks.named("check").configure(task -> task.dependsOn(testTask));
NeoDevFacade.setupTestTask(
project,
neoDevBuildDir,
testTask,
writeNeoDevConfig,
testExtension.getLoadedMods(),
testExtension.getTestedMod(),
modulePath -> modulePath.getDependencies().add(modulePathDependency),
configureLegacyClasspath,
downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
);
}
private static ProjectDependency projectDep(DependencyFactory dependencyFactory, Project project, String capabilityNotation) {
var dep = dependencyFactory.create(project);
dep.capabilities(caps -> {
caps.requireCapability(capabilityNotation);
});
return dep;
}
}

View File

@ -0,0 +1,540 @@
package net.neoforged.neodev;
import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
import net.neoforged.moddevgradle.internal.NeoDevFacade;
import net.neoforged.moddevgradle.tasks.JarJar;
import net.neoforged.neodev.installer.CreateArgsFile;
import net.neoforged.neodev.installer.CreateInstallerProfile;
import net.neoforged.neodev.installer.CreateLauncherProfile;
import net.neoforged.neodev.installer.InstallerProcessor;
import net.neoforged.neodev.utils.DependencyUtils;
import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
import net.neoforged.nfrtgradle.DownloadAssets;
import net.neoforged.nfrtgradle.NeoFormRuntimePlugin;
import net.neoforged.nfrtgradle.NeoFormRuntimeTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
import org.gradle.api.file.Directory;
import org.gradle.api.file.RegularFile;
import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.bundling.Zip;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class NeoDevPlugin implements Plugin<Project> {
static final String GROUP = "neoforge development";
static final String INTERNAL_GROUP = "neoforge development/internal";
@Override
public void apply(Project project) {
project.getPlugins().apply(MinecraftDependenciesPlugin.class);
var dependencyFactory = project.getDependencyFactory();
var tasks = project.getTasks();
var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
var rawNeoFormVersion = project.getProviders().gradleProperty("neoform_version");
var fmlVersion = project.getProviders().gradleProperty("fancy_mod_loader_version");
var minecraftVersion = project.getProviders().gradleProperty("minecraft_version");
var neoForgeVersion = project.provider(() -> project.getVersion().toString());
var mcAndNeoFormVersion = minecraftVersion.zip(rawNeoFormVersion, (mc, nf) -> mc + "-" + nf);
var extension = project.getExtensions().create(NeoDevExtension.NAME, NeoDevExtension.class);
var configurations = NeoDevConfigurations.createAndSetup(project);
/*
* MINECRAFT SOURCES SETUP
*/
// 1. Obtain decompiled Minecraft sources jar using NeoForm.
var createSourceArtifacts = configureMinecraftDecompilation(project);
// Task must run on sync to have MC resources available for IDEA nondelegated builds.
NeoDevFacade.runTaskOnProjectSync(project, createSourceArtifacts);
// 2. Apply AT to the source jar from 1.
var atFile = project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg");
var applyAt = configureAccessTransformer(
project,
configurations,
createSourceArtifacts,
neoDevBuildDir,
atFile);
// 3. Apply patches to the source jar from 2.
var patchesFolder = project.getRootProject().file("patches");
var applyPatches = tasks.register("applyPatches", ApplyPatches.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getOriginalJar().set(applyAt.flatMap(ApplyAccessTransformer::getOutputJar));
task.getPatchesFolder().set(patchesFolder);
task.getPatchedJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/patched-sources.jar")));
task.getRejectsFolder().set(project.getRootProject().file("rejects"));
});
// 4. Unpack jar from 3.
var mcSourcesPath = project.file("src/main/java");
tasks.register("setup", Sync.class, task -> {
task.setGroup(GROUP);
task.from(project.zipTree(applyPatches.flatMap(ApplyPatches::getPatchedJar)));
task.into(mcSourcesPath);
});
/*
* RUNS SETUP
*/
// 1. Write configs that contain the runs in a format understood by MDG/NG/etc. Currently one for neodev and one for userdev.
var writeNeoDevConfig = tasks.register("writeNeoDevConfig", CreateUserDevConfig.class, task -> {
task.getForNeoDev().set(true);
task.getUserDevConfig().set(neoDevBuildDir.map(dir -> dir.file("neodev-config.json")));
});
var writeUserDevConfig = tasks.register("writeUserDevConfig", CreateUserDevConfig.class, task -> {
task.getForNeoDev().set(false);
task.getUserDevConfig().set(neoDevBuildDir.map(dir -> dir.file("userdev-config.json")));
});
for (var taskProvider : List.of(writeNeoDevConfig, writeUserDevConfig)) {
taskProvider.configure(task -> {
task.setGroup(INTERNAL_GROUP);
task.getFmlVersion().set(fmlVersion);
task.getMinecraftVersion().set(minecraftVersion);
task.getNeoForgeVersion().set(neoForgeVersion);
task.getRawNeoFormVersion().set(rawNeoFormVersion);
task.getLibraries().addAll(DependencyUtils.configurationToGavList(configurations.userdevClasspath));
task.getModules().addAll(DependencyUtils.configurationToGavList(configurations.modulePath));
task.getTestLibraries().addAll(DependencyUtils.configurationToGavList(configurations.userdevTestClasspath));
task.getTestLibraries().add(neoForgeVersion.map(v -> "net.neoforged:testframework:" + v));
task.getIgnoreList().addAll(configurations.userdevCompileOnlyClasspath.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
return results.stream().map(r -> r.getFile().getName()).toList();
}));
task.getIgnoreList().addAll("client-extra", "neoforge-");
task.getBinpatcherGav().set(Tools.BINPATCHER.asGav(project));
});
}
// 2. Task to download assets.
var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(v -> "net.neoforged:neoform:" + v + "@zip"));
task.getAssetPropertiesFile().set(neoDevBuildDir.map(dir -> dir.file("minecraft_assets.properties")));
});
// FML needs Minecraft resources on the classpath to find it. Add to runtimeOnly so subprojects also get it at runtime.
var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME);
runtimeClasspath.getDependencies().add(
dependencyFactory.create(
project.files(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact))
)
);
// 3. Let MDG do the rest of the setup. :)
NeoDevFacade.setupRuns(
project,
neoDevBuildDir,
extension.getRuns(),
writeNeoDevConfig,
modulePath -> {
modulePath.extendsFrom(configurations.moduleLibraries);
},
legacyClassPath -> {
legacyClassPath.getDependencies().addLater(mcAndNeoFormVersion.map(v -> dependencyFactory.create("net.neoforged:neoform:" + v).capabilities(caps -> {
caps.requireCapability("net.neoforged:neoform-dependencies");
})));
legacyClassPath.extendsFrom(configurations.libraries, configurations.moduleLibraries, configurations.userdevCompileOnly);
},
downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
);
// TODO: Gradle run tasks should be moved to gradle group GROUP
/*
* OTHER TASKS
*/
// Generate source patches into a patch archive.
var genSourcePatches = tasks.register("generateSourcePatches", GenerateSourcePatches.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getOriginalJar().set(applyAt.flatMap(ApplyAccessTransformer::getOutputJar));
task.getModifiedSources().set(project.file("src/main/java"));
task.getPatchesJar().set(neoDevBuildDir.map(dir -> dir.file("source-patches.zip")));
});
// Update the patch/ folder with the current patches.
tasks.register("genPatches", Sync.class, task -> {
task.setGroup(GROUP);
task.from(project.zipTree(genSourcePatches.flatMap(GenerateSourcePatches::getPatchesJar)));
task.into(project.getRootProject().file("patches"));
});
// Universal jar = the jar that contains NeoForge classes
// TODO: signing?
var universalJar = tasks.register("universalJar", Jar.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getArchiveClassifier().set("universal");
task.from(project.zipTree(
tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile)));
task.exclude("net/minecraft/**");
task.exclude("com/**");
task.exclude("mcp/**");
task.manifest(manifest -> {
manifest.attributes(Map.of("FML-System-Mods", "neoforge"));
// These attributes are used from NeoForgeVersion.java to find the NF version without command line arguments.
manifest.attributes(
Map.of(
"Specification-Title", "NeoForge",
"Specification-Vendor", "NeoForge",
"Specification-Version", project.getVersion().toString().substring(0, project.getVersion().toString().lastIndexOf(".")),
"Implementation-Title", project.getGroup(),
"Implementation-Version", project.getVersion(),
"Implementation-Vendor", "NeoForged"),
"net/neoforged/neoforge/internal/versions/neoforge/");
manifest.attributes(
Map.of(
"Specification-Title", "Minecraft",
"Specification-Vendor", "Mojang",
"Specification-Version", minecraftVersion,
"Implementation-Title", "MCP",
"Implementation-Version", mcAndNeoFormVersion,
"Implementation-Vendor", "NeoForged"),
"net/neoforged/neoforge/versions/neoform/");
});
});
var jarJarTask = JarJar.registerWithConfiguration(project, "jarJar");
jarJarTask.configure(task -> task.setGroup(INTERNAL_GROUP));
universalJar.configure(task -> task.from(jarJarTask));
var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
task.setGroup(INTERNAL_GROUP);
var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
});
var binaryPatchOutputs = configureBinaryPatchCreation(
project,
configurations,
createCleanArtifacts,
neoDevBuildDir,
patchesFolder
);
// Launcher profile = the version.json file used by the Minecraft launcher.
var createLauncherProfile = tasks.register("createLauncherProfile", CreateLauncherProfile.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getFmlVersion().set(fmlVersion);
task.getMinecraftVersion().set(minecraftVersion);
task.getNeoForgeVersion().set(neoForgeVersion);
task.getRawNeoFormVersion().set(rawNeoFormVersion);
task.setLibraries(configurations.launcherProfileClasspath);
task.getRepositoryURLs().set(project.provider(() -> {
List<URI> repos = new ArrayList<>();
for (var repo : project.getRepositories().withType(MavenArtifactRepository.class)) {
var uri = repo.getUrl();
if (!uri.toString().endsWith("/")) {
uri = URI.create(uri + "/");
}
repos.add(uri);
}
return repos;
}));
task.getIgnoreList().addAll("client-extra", "neoforge-");
task.setModules(configurations.modulePath);
task.getLauncherProfile().set(neoDevBuildDir.map(dir -> dir.file("launcher-profile.json")));
});
// Installer profile = the .json file used by the NeoForge installer.
var createInstallerProfile = tasks.register("createInstallerProfile", CreateInstallerProfile.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getMinecraftVersion().set(minecraftVersion);
task.getNeoForgeVersion().set(neoForgeVersion);
task.getMcAndNeoFormVersion().set(mcAndNeoFormVersion);
task.getIcon().set(project.getRootProject().file("docs/assets/neoforged.ico"));
// Anything that is on the launcher classpath should be downloaded by the installer.
// (At least on the server side).
task.addLibraries(configurations.launcherProfileClasspath);
// We need the NeoForm zip for the SRG mappings.
task.addLibraries(configurations.neoFormDataOnly);
task.getRepositoryURLs().set(project.provider(() -> {
List<URI> repos = new ArrayList<>();
for (var repo : project.getRepositories().withType(MavenArtifactRepository.class)) {
var uri = repo.getUrl();
if (!uri.toString().endsWith("/")) {
uri = URI.create(uri + "/");
}
repos.add(uri);
}
return repos;
}));
task.getUniversalJar().set(universalJar.flatMap(AbstractArchiveTask::getArchiveFile));
task.getInstallerProfile().set(neoDevBuildDir.map(dir -> dir.file("installer-profile.json")));
// Make all installer processor tools available to the profile
for (var installerProcessor : InstallerProcessor.values()) {
var configuration = configurations.getExecutableTool(installerProcessor.tool);
// Different processors might use different versions of the same library,
// but that is fine because each processor gets its own classpath.
task.addLibraries(configuration);
task.getProcessorClasspaths().put(installerProcessor, DependencyUtils.configurationToGavList(configuration));
task.getProcessorGavs().put(installerProcessor, installerProcessor.tool.asGav(project));
}
});
var createWindowsServerArgsFile = tasks.register("createWindowsServerArgsFile", CreateArgsFile.class, task -> {
task.setLibraries(";", configurations.launcherProfileClasspath, configurations.modulePath);
task.getArgsFile().set(neoDevBuildDir.map(dir -> dir.file("windows-server-args.txt")));
});
var createUnixServerArgsFile = tasks.register("createUnixServerArgsFile", CreateArgsFile.class, task -> {
task.setLibraries(":", configurations.launcherProfileClasspath, configurations.modulePath);
task.getArgsFile().set(neoDevBuildDir.map(dir -> dir.file("unix-server-args.txt")));
});
for (var taskProvider : List.of(createWindowsServerArgsFile, createUnixServerArgsFile)) {
taskProvider.configure(task -> {
task.setGroup(INTERNAL_GROUP);
task.getTemplate().set(project.getRootProject().file("server_files/args.txt"));
task.getFmlVersion().set(fmlVersion);
task.getMinecraftVersion().set(minecraftVersion);
task.getNeoForgeVersion().set(neoForgeVersion);
task.getRawNeoFormVersion().set(rawNeoFormVersion);
// In theory, new BootstrapLauncher shouldn't need the module path in the ignore list anymore.
// However, in server installs libraries are passed as relative paths here.
// Module path detection doesn't currently work with relative paths (BootstrapLauncher #20).
task.getIgnoreList().set(configurations.modulePath.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
return results.stream().map(r -> r.getFile().getName()).toList();
}));
task.getRawServerJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getRawServerJar));
});
}
var installerConfig = configurations.getExecutableTool(Tools.LEGACYINSTALLER);
// TODO: signing?
// We want to inherit the executable JAR manifest from LegacyInstaller.
// - Jar tasks have special manifest handling, so use Zip.
// - The manifest must be the first entry in the jar so LegacyInstaller has to be the first input.
var installerJar = tasks.register("installerJar", Zip.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getArchiveClassifier().set("installer");
task.getArchiveExtension().set("jar");
task.setMetadataCharset("UTF-8");
task.getDestinationDirectory().convention(project.getExtensions().getByType(BasePluginExtension.class).getLibsDirectory());
task.from(project.zipTree(project.provider(installerConfig::getSingleFile)), spec -> {
spec.exclude("big_logo.png");
});
task.from(createLauncherProfile.flatMap(CreateLauncherProfile::getLauncherProfile), spec -> {
spec.rename(s -> "version.json");
});
task.from(createInstallerProfile.flatMap(CreateInstallerProfile::getInstallerProfile), spec -> {
spec.rename(s -> "install_profile.json");
});
task.from(project.getRootProject().file("src/main/resources/url.png"));
task.from(project.getRootProject().file("src/main/resources/neoforged_logo.png"), spec -> {
spec.rename(s -> "big_logo.png");
});
task.from(createUnixServerArgsFile.flatMap(CreateArgsFile::getArgsFile), spec -> {
spec.into("data");
spec.rename(s -> "unix_args.txt");
});
task.from(createWindowsServerArgsFile.flatMap(CreateArgsFile::getArgsFile), spec -> {
spec.into("data");
spec.rename(s -> "win_args.txt");
});
task.from(binaryPatchOutputs.binaryPatchesForClient(), spec -> {
spec.into("data");
spec.rename(s -> "client.lzma");
});
task.from(binaryPatchOutputs.binaryPatchesForServer(), spec -> {
spec.into("data");
spec.rename(s -> "server.lzma");
});
var mavenPath = neoForgeVersion.map(v -> "net/neoforged/neoforge/" + v);
task.getInputs().property("mavenPath", mavenPath);
task.from(project.getRootProject().files("server_files"), spec -> {
spec.into("data");
spec.exclude("args.txt");
spec.filter(s -> {
return s.replaceAll("@MAVEN_PATH@", mavenPath.get());
});
});
// This is true by default (see gradle.properties), and needs to be disabled explicitly when building (see release.yml).
String installerDebugProperty = "neogradle.runtime.platform.installer.debug";
if (project.getProperties().containsKey(installerDebugProperty) && Boolean.parseBoolean(project.getProperties().get(installerDebugProperty).toString())) {
task.from(universalJar.flatMap(AbstractArchiveTask::getArchiveFile), spec -> {
spec.into(String.format("/maven/net/neoforged/neoforge/%s/", neoForgeVersion.get()));
spec.rename(name -> String.format("neoforge-%s-universal.jar", neoForgeVersion.get()));
});
}
});
var userdevJar = tasks.register("userdevJar", Jar.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.getArchiveClassifier().set("userdev");
task.from(writeUserDevConfig.flatMap(CreateUserDevConfig::getUserDevConfig), spec -> {
spec.rename(s -> "config.json");
});
task.from(atFile, spec -> {
spec.into("ats/");
});
task.from(binaryPatchOutputs.binaryPatchesForMerged(), spec -> {
spec.rename(s -> "joined.lzma");
});
task.from(project.zipTree(genSourcePatches.flatMap(GenerateSourcePatches::getPatchesJar)), spec -> {
spec.into("patches/");
});
});
project.getExtensions().getByType(JavaPluginExtension.class).withSourcesJar();
var sourcesJarProvider = project.getTasks().named("sourcesJar", Jar.class);
sourcesJarProvider.configure(task -> {
task.exclude("net/minecraft/**");
task.exclude("com/**");
task.exclude("mcp/**");
});
tasks.named("assemble", task -> {
task.dependsOn(installerJar);
task.dependsOn(universalJar);
task.dependsOn(userdevJar);
task.dependsOn(sourcesJarProvider);
});
}
private static TaskProvider<ApplyAccessTransformer> configureAccessTransformer(
Project project,
NeoDevConfigurations configurations,
TaskProvider<CreateMinecraftArtifacts> createSourceArtifacts,
Provider<Directory> neoDevBuildDir,
File atFile) {
// Pass -PvalidateAccessTransformers to validate ATs.
var validateAts = project.getProviders().gradleProperty("validateAccessTransformers").map(p -> true).orElse(false);
return project.getTasks().register("applyAccessTransformer", ApplyAccessTransformer.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.classpath(configurations.getExecutableTool(Tools.JST));
task.getInputJar().set(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getSourcesArtifact));
task.getAccessTransformer().set(atFile);
task.getValidate().set(validateAts);
task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/access-transformed-sources.jar")));
task.getLibraries().from(configurations.neoFormClasspath);
task.getLibrariesFile().set(neoDevBuildDir.map(dir -> dir.file("minecraft-libraries-for-jst.txt")));
});
}
private static BinaryPatchOutputs configureBinaryPatchCreation(Project project,
NeoDevConfigurations configurations,
TaskProvider<CreateCleanArtifacts> createCleanArtifacts,
Provider<Directory> neoDevBuildDir,
File sourcesPatchesFolder) {
var tasks = project.getTasks();
var artConfig = configurations.getExecutableTool(Tools.AUTO_RENAMING_TOOL);
var remapClientJar = tasks.register("remapClientJar", RemapJar.class, task -> {
task.setDescription("Creates a Minecraft client jar with the official mappings applied. Used as the base for generating binary patches for the client.");
task.getInputJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanClientJar));
task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("remapped-client.jar")));
});
var remapServerJar = tasks.register("remapServerJar", RemapJar.class, task -> {
task.setDescription("Creates a Minecraft dedicated server jar with the official mappings applied. Used as the base for generating binary patches for the client.");
task.getInputJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanServerJar));
task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("remapped-server.jar")));
});
for (var remapTask : List.of(remapClientJar, remapServerJar)) {
remapTask.configure(task -> {
task.setGroup(INTERNAL_GROUP);
task.classpath(artConfig);
task.getMappings().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getMergedMappings));
});
}
var binpatcherConfig = configurations.getExecutableTool(Tools.BINPATCHER);
var generateMergedBinPatches = tasks.register("generateMergedBinPatches", GenerateBinaryPatches.class, task -> {
task.setDescription("Creates binary patch files by diffing a merged client/server jar-file and the compiled Minecraft classes in this project.");
task.getCleanJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanJoinedJar));
task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("merged-binpatches.lzma")));
});
var generateClientBinPatches = tasks.register("generateClientBinPatches", GenerateBinaryPatches.class, task -> {
task.setDescription("Creates binary patch files by diffing a merged client jar-file and the compiled Minecraft classes in this project.");
task.getCleanJar().set(remapClientJar.flatMap(RemapJar::getOutputJar));
task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("client-binpatches.lzma")));
});
var generateServerBinPatches = tasks.register("generateServerBinPatches", GenerateBinaryPatches.class, task -> {
task.setDescription("Creates binary patch files by diffing a merged server jar-file and the compiled Minecraft classes in this project.");
task.getCleanJar().set(remapServerJar.flatMap(RemapJar::getOutputJar));
task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("server-binpatches.lzma")));
});
for (var generateBinPatchesTask : List.of(generateMergedBinPatches, generateClientBinPatches, generateServerBinPatches)) {
generateBinPatchesTask.configure(task -> {
task.setGroup(INTERNAL_GROUP);
task.classpath(binpatcherConfig);
task.getPatchedJar().set(tasks.named("jar", Jar.class).flatMap(Jar::getArchiveFile));
task.getSourcePatchesFolder().set(sourcesPatchesFolder);
task.getMappings().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getMergedMappings));
});
}
return new BinaryPatchOutputs(
generateMergedBinPatches.flatMap(GenerateBinaryPatches::getOutputFile),
generateClientBinPatches.flatMap(GenerateBinaryPatches::getOutputFile),
generateServerBinPatches.flatMap(GenerateBinaryPatches::getOutputFile)
);
}
private record BinaryPatchOutputs(
Provider<RegularFile> binaryPatchesForMerged,
Provider<RegularFile> binaryPatchesForClient,
Provider<RegularFile> binaryPatchesForServer
) {
}
/**
* Sets up NFRT, and creates the sources and resources artifacts.
*/
static TaskProvider<CreateMinecraftArtifacts> configureMinecraftDecompilation(Project project) {
project.getPlugins().apply(NeoFormRuntimePlugin.class);
var configurations = project.getConfigurations();
var dependencyFactory = project.getDependencyFactory();
var tasks = project.getTasks();
var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
var rawNeoFormVersion = project.getProviders().gradleProperty("neoform_version");
var minecraftVersion = project.getProviders().gradleProperty("minecraft_version");
var mcAndNeoFormVersion = minecraftVersion.zip(rawNeoFormVersion, (mc, nf) -> mc + "-" + nf);
// Configuration for all artifacts that should be passed to NFRT to prevent repeated downloads
var neoFormRuntimeArtifactManifestNeoForm = configurations.create("neoFormRuntimeArtifactManifestNeoForm", spec -> {
spec.setCanBeConsumed(false);
spec.setCanBeResolved(true);
spec.getDependencies().addLater(mcAndNeoFormVersion.map(version -> {
return dependencyFactory.create("net.neoforged:neoform:" + version);
}));
});
tasks.withType(NeoFormRuntimeTask.class, task -> {
task.addArtifactsToManifest(neoFormRuntimeArtifactManifestNeoForm);
});
return tasks.register("createSourceArtifacts", CreateMinecraftArtifacts.class, task -> {
var minecraftArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts"));
task.getSourcesArtifact().set(minecraftArtifactsDir.map(dir -> dir.file("base-sources.jar")));
task.getResourcesArtifact().set(minecraftArtifactsDir.map(dir -> dir.file("minecraft-resources.jar")));
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
});
}
}

View File

@ -0,0 +1,30 @@
package net.neoforged.neodev;
import net.neoforged.moddevgradle.dsl.ModModel;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.SetProperty;
import javax.inject.Inject;
public abstract class NeoDevTestExtension {
public static final String NAME = "neoDevTest";
@Inject
public NeoDevTestExtension() {
}
/**
* The mod that will be loaded in JUnit tests.
* The compiled classes from {@code src/test/java} and the resources from {@code src/test/resources}
* will be added to that mod at runtime.
*/
public abstract Property<ModModel> getTestedMod();
/**
* The mods to load when running unit tests. Defaults to all mods registered in the project.
* This must contain {@link #getTestedMod()}.
*
* @see ModModel
*/
public abstract SetProperty<ModModel> getLoadedMods();
}

View File

@ -0,0 +1,53 @@
package net.neoforged.neodev;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.OutputFile;
import javax.inject.Inject;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Produces a remapped jar-file that has almost no other changes applied with the intent of being
* the base against which we {@link GenerateBinaryPatches generate binary patches}.
* <p>
* The installer produces the same Jar file as this task does and then applies the patches against that.
* <p>
* Any changes to the options used here have to be reflected in the {@link net.neoforged.neodev.installer.CreateInstallerProfile installer profile}
* and vice versa, to ensure the patches are generated against the same binary files as they are applied to later.
*/
abstract class RemapJar extends JavaExec {
@Inject
public RemapJar() {}
@InputFile
abstract RegularFileProperty getInputJar();
@InputFile
abstract RegularFileProperty getMappings();
@OutputFile
abstract RegularFileProperty getOutputJar();
@Override
public void exec() {
args("--input", getInputJar().get().getAsFile().getAbsolutePath());
args("--output", getOutputJar().get().getAsFile().getAbsolutePath());
args("--names", getMappings().get().getAsFile().getAbsolutePath());
args("--ann-fix", "--ids-fix", "--src-fix", "--record-fix");
var logFile = new File(getTemporaryDir(), "console.log");
try (var out = new BufferedOutputStream(new FileOutputStream(logFile))) {
getLogger().info("Logging ART console output to {}", logFile.getAbsolutePath());
setStandardOutput(out);
super.exec();
} catch (IOException e) {
throw new GradleException("Failed to remap jar.", e);
}
}
}

View File

@ -0,0 +1,53 @@
package net.neoforged.neodev;
import org.gradle.api.Project;
// If a GAV is changed, make sure to change the corresponding renovate comment in gradle.properties.
public enum Tools {
// Fatjar jst-cli-bundle instead of jst-cli because publication of the latter is currently broken.
JST("net.neoforged.jst:jst-cli-bundle:%s", "jst_version", "toolJstClasspath", true),
// Fatjar because the contents are copy/pasted into the installer jar which must be standalone.
LEGACYINSTALLER("net.neoforged:legacyinstaller:%s:shrunk", "legacyinstaller_version", "toolLegacyinstallerClasspath", true),
// Fatjar because the slim jar currently does not have the main class set in its manifest.
AUTO_RENAMING_TOOL("net.neoforged:AutoRenamingTool:%s:all", "art_version", "toolAutoRenamingToolClasspath", true),
INSTALLERTOOLS("net.neoforged.installertools:installertools:%s", "installertools_version", "toolInstallertoolsClasspath", false),
JARSPLITTER("net.neoforged.installertools:jarsplitter:%s", "installertools_version", "toolJarsplitterClasspath", false),
// Fatjar because it was like that in the userdev json in the past.
// To reconsider, we need to get in touch with 3rd party plugin developers or wait for a BC window.
BINPATCHER("net.neoforged.installertools:binarypatcher:%s:fatjar", "installertools_version", "toolBinpatcherClasspath", true);
private final String gavPattern;
private final String versionProperty;
private final String gradleConfigurationName;
private final boolean requestFatJar;
Tools(String gavPattern, String versionProperty, String gradleConfigurationName, boolean requestFatJar) {
this.gavPattern = gavPattern;
this.versionProperty = versionProperty;
this.gradleConfigurationName = gradleConfigurationName;
this.requestFatJar = requestFatJar;
}
/**
* The name of the Gradle {@link org.gradle.api.artifacts.Configuration} used to resolve this particular tool.
*/
public String getGradleConfigurationName() {
return gradleConfigurationName;
}
/**
* Some tools may be incorrectly packaged and declare transitive dependencies even for their "fatjar" variants.
* Gradle will not run these, so we ignore them.
*/
public boolean isRequestFatJar() {
return requestFatJar;
}
public String asGav(Project project) {
var version = project.property(versionProperty);
if (version == null) {
throw new IllegalStateException("Could not find property " + versionProperty);
}
return gavPattern.formatted(version);
}
}

View File

@ -0,0 +1,126 @@
package net.neoforged.neodev.installer;
import net.neoforged.neodev.utils.DependencyUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.ArchiveOperations;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
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.TaskAction;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Creates the JVM/program argument files used by the dedicated server launcher.
*/
public abstract class CreateArgsFile extends DefaultTask {
@Inject
public CreateArgsFile() {}
@InputFile
public abstract RegularFileProperty getTemplate();
@Input
public abstract Property<String> getFmlVersion();
@Input
public abstract Property<String> getMinecraftVersion();
@Input
public abstract Property<String> getNeoForgeVersion();
@Input
public abstract Property<String> getRawNeoFormVersion();
@Input
protected abstract Property<String> getPathSeparator();
@Input
protected abstract Property<String> getModules();
@Input
public abstract ListProperty<String> getIgnoreList();
@Input
protected abstract Property<String> getClasspath();
public void setLibraries(String separator, Configuration classpath, Configuration modulePath) {
getPathSeparator().set(separator);
getClasspath().set(DependencyUtils.configurationToClasspath(classpath, "libraries/", separator));
getModules().set(DependencyUtils.configurationToClasspath(modulePath, "libraries/", separator));
}
@InputFile
public abstract RegularFileProperty getRawServerJar();
@OutputFile
public abstract RegularFileProperty getArgsFile();
@Inject
protected abstract ArchiveOperations getArchiveOperations();
private String resolveClasspath() throws IOException {
var ourClasspath = getClasspath().get() + getPathSeparator().get()
+ "libraries/net/minecraft/server/%s/server-%s-extra.jar".formatted(
getRawNeoFormVersion().get(), getRawNeoFormVersion().get());
// The raw server jar also contains its own classpath.
// We want to make sure that our versions of the libraries are used when there is a conflict.
var ourClasspathEntries = Stream.of(ourClasspath.split(getPathSeparator().get()))
.map(CreateArgsFile::stripVersionSuffix)
.collect(Collectors.toSet());
var serverClasspath = getArchiveOperations().zipTree(getRawServerJar())
.filter(spec -> spec.getPath().endsWith("META-INF" + File.separator + "classpath-joined"))
.getSingleFile();
var filteredServerClasspath = Stream.of(Files.readString(serverClasspath.toPath()).split(";"))
.filter(path -> !ourClasspathEntries.contains(stripVersionSuffix(path)))
// Exclude the actual MC server jar, which is under versions/
.filter(path -> path.startsWith("libraries/"))
.collect(Collectors.joining(getPathSeparator().get()));
return ourClasspath + getPathSeparator().get() + filteredServerClasspath;
}
// Example:
// Convert "libraries/com/github/oshi/oshi-core/6.4.10/oshi-core-6.4.10.jar"
// to "libraries/com/github/oshi/oshi-core".
private static String stripVersionSuffix(String classpathEntry) {
var parts = classpathEntry.split("/");
return String.join("/", List.of(parts).subList(0, parts.length - 2));
}
@TaskAction
public void createArgsFile() throws IOException {
var replacements = new HashMap<String, String>();
replacements.put("@MODULE_PATH@", getModules().get());
replacements.put("@MODULES@", "ALL-MODULE-PATH");
replacements.put("@IGNORE_LIST@", String.join(",", getIgnoreList().get()));
replacements.put("@PLUGIN_LAYER_LIBRARIES@", "");
replacements.put("@GAME_LAYER_LIBRARIES@", "");
replacements.put("@CLASS_PATH@", resolveClasspath());
replacements.put("@TASK@", "forgeserver");
replacements.put("@FORGE_VERSION@", getNeoForgeVersion().get());
replacements.put("@FML_VERSION@", getFmlVersion().get());
replacements.put("@MC_VERSION@", getMinecraftVersion().get());
replacements.put("@MCP_VERSION@", getRawNeoFormVersion().get());
var contents = Files.readString(getTemplate().get().getAsFile().toPath());
for (var entry : replacements.entrySet()) {
contents = contents.replaceAll(entry.getKey(), entry.getValue());
}
Files.writeString(getArgsFile().get().getAsFile().toPath(), contents);
}
}

View File

@ -0,0 +1,230 @@
package net.neoforged.neodev.installer;
import com.google.gson.GsonBuilder;
import net.neoforged.neodev.utils.FileUtils;
import net.neoforged.neodev.utils.MavenIdentifier;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.RegularFileProperty;
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.InputFile;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
/**
* Creates the JSON profile used by legacyinstaller for installing the client into the vanilla launcher,
* or installing a dedicated server.
*/
public abstract class CreateInstallerProfile extends DefaultTask {
@Inject
public CreateInstallerProfile() {}
@Input
public abstract Property<String> getMinecraftVersion();
@Input
public abstract Property<String> getNeoForgeVersion();
@Input
public abstract Property<String> getMcAndNeoFormVersion();
@InputFile
public abstract RegularFileProperty getIcon();
@Nested
protected abstract ListProperty<IdentifiedFile> getLibraryFiles();
public void addLibraries(Configuration libraries) {
getLibraryFiles().addAll(IdentifiedFile.listFromConfiguration(getProject(), libraries));
}
@Input
public abstract ListProperty<URI> getRepositoryURLs();
@Input
public abstract MapProperty<InstallerProcessor, List<String>> getProcessorClasspaths();
@Input
public abstract MapProperty<InstallerProcessor, String> getProcessorGavs();
@InputFile
public abstract RegularFileProperty getUniversalJar();
@OutputFile
public abstract RegularFileProperty getInstallerProfile();
private void addProcessor(List<ProcessorEntry> processors, @Nullable List<String> sides, InstallerProcessor processor, List<String> args) {
var classpath = getProcessorClasspaths().get().get(processor);
var mainJar = getProcessorGavs().get().get(processor);
if (!classpath.contains(mainJar)) {
throw new IllegalStateException("Processor %s is not included in its own classpath %s".formatted(mainJar, classpath));
}
processors.add(new ProcessorEntry(sides, mainJar, classpath, args));
}
@TaskAction
public void createInstallerProfile() throws IOException {
var icon = "data:image/png;base64," + Base64.getEncoder().encodeToString(Files.readAllBytes(getIcon().getAsFile().get().toPath()));
var data = new LinkedHashMap<String, LauncherDataEntry>();
var neoFormVersion = getMcAndNeoFormVersion().get();
data.put("MAPPINGS", new LauncherDataEntry(String.format("[net.neoforged:neoform:%s:mappings@txt]", neoFormVersion), String.format("[net.neoforged:neoform:%s:mappings@txt]", neoFormVersion)));
data.put("MOJMAPS", new LauncherDataEntry(String.format("[net.minecraft:client:%s:mappings@txt]", neoFormVersion), String.format("[net.minecraft:server:%s:mappings@txt]", neoFormVersion)));
data.put("MERGED_MAPPINGS", new LauncherDataEntry(String.format("[net.neoforged:neoform:%s:mappings-merged@txt]", neoFormVersion), String.format("[net.neoforged:neoform:%s:mappings-merged@txt]", neoFormVersion)));
data.put("BINPATCH", new LauncherDataEntry("/data/client.lzma", "/data/server.lzma"));
data.put("MC_UNPACKED", new LauncherDataEntry(String.format("[net.minecraft:client:%s:unpacked]", neoFormVersion), String.format("[net.minecraft:server:%s:unpacked]", neoFormVersion)));
data.put("MC_SLIM", new LauncherDataEntry(String.format("[net.minecraft:client:%s:slim]", neoFormVersion), String.format("[net.minecraft:server:%s:slim]", neoFormVersion)));
data.put("MC_EXTRA", new LauncherDataEntry(String.format("[net.minecraft:client:%s:extra]", neoFormVersion), String.format("[net.minecraft:server:%s:extra]", neoFormVersion)));
data.put("MC_SRG", new LauncherDataEntry(String.format("[net.minecraft:client:%s:srg]", neoFormVersion), String.format("[net.minecraft:server:%s:srg]", neoFormVersion)));
data.put("PATCHED", new LauncherDataEntry(String.format("[%s:%s:%s:client]", "net.neoforged", "neoforge", getNeoForgeVersion().get()), String.format("[%s:%s:%s:server]", "net.neoforged", "neoforge", getNeoForgeVersion().get())));
data.put("MCP_VERSION", new LauncherDataEntry(String.format("'%s'", neoFormVersion), String.format("'%s'", neoFormVersion)));
var processors = new ArrayList<ProcessorEntry>();
BiConsumer<InstallerProcessor, List<String>> commonProcessor = (processor, args) -> addProcessor(processors, null, processor, args);
BiConsumer<InstallerProcessor, List<String>> clientProcessor = (processor, args) -> addProcessor(processors, List.of("client"), processor, args);
BiConsumer<InstallerProcessor, List<String>> serverProcessor = (processor, args) -> addProcessor(processors, List.of("server"), processor, args);
serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "EXTRACT_FILES", "--archive", "{INSTALLER}",
"--from", "data/run.sh", "--to", "{ROOT}/run.sh", "--exec", "{ROOT}/run.sh",
"--from", "data/run.bat", "--to", "{ROOT}/run.bat",
"--from", "data/user_jvm_args.txt", "--to", "{ROOT}/user_jvm_args.txt", "--optional", "{ROOT}/user_jvm_args.txt",
"--from", "data/win_args.txt", "--to", "{ROOT}/libraries/net/neoforged/neoforge/%s/win_args.txt".formatted(getNeoForgeVersion().get()),
"--from", "data/unix_args.txt", "--to", "{ROOT}/libraries/net/neoforged/neoforge/%s/unix_args.txt".formatted(getNeoForgeVersion().get()))
);
serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "BUNDLER_EXTRACT", "--input", "{MINECRAFT_JAR}", "--output", "{ROOT}/libraries/", "--libraries")
);
serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "BUNDLER_EXTRACT", "--input", "{MINECRAFT_JAR}", "--output", "{MC_UNPACKED}", "--jar-only")
);
var neoformDependency = "net.neoforged:neoform:" + getMcAndNeoFormVersion().get() + "@zip";;
commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "MCP_DATA", "--input", String.format("[%s]", neoformDependency), "--output", "{MAPPINGS}", "--key", "mappings")
);
commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "DOWNLOAD_MOJMAPS", "--version", getMinecraftVersion().get(), "--side", "{SIDE}", "--output", "{MOJMAPS}")
);
commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
List.of("--task", "MERGE_MAPPING", "--left", "{MAPPINGS}", "--right", "{MOJMAPS}", "--output", "{MERGED_MAPPINGS}", "--classes", "--fields", "--methods", "--reverse-right")
);
clientProcessor.accept(InstallerProcessor.JARSPLITTER,
List.of("--input", "{MINECRAFT_JAR}", "--slim", "{MC_SLIM}", "--extra", "{MC_EXTRA}", "--srg", "{MERGED_MAPPINGS}")
);
serverProcessor.accept(InstallerProcessor.JARSPLITTER,
List.of("--input", "{MC_UNPACKED}", "--slim", "{MC_SLIM}", "--extra", "{MC_EXTRA}", "--srg", "{MERGED_MAPPINGS}")
);
// Note that the options supplied here have to match the ones used in the RemapJar task used to generate the binary patches
commonProcessor.accept(InstallerProcessor.FART,
List.of("--input", "{MC_SLIM}", "--output", "{MC_SRG}", "--names", "{MERGED_MAPPINGS}", "--ann-fix", "--ids-fix", "--src-fix", "--record-fix")
);
commonProcessor.accept(InstallerProcessor.BINPATCHER,
List.of("--clean", "{MC_SRG}", "--output", "{PATCHED}", "--apply", "{BINPATCH}")
);
getLogger().info("Collecting libraries for Installer Profile");
// Remove potential duplicates.
var libraryFilesToResolve = new LinkedHashMap<MavenIdentifier, IdentifiedFile>(getLibraryFiles().get().size());
for (var libraryFile : getLibraryFiles().get()) {
var existingFile = libraryFilesToResolve.putIfAbsent(libraryFile.getIdentifier().get(), libraryFile);
if (existingFile != null) {
var existing = existingFile.getFile().getAsFile().get();
var duplicate = libraryFile.getFile().getAsFile().get();
if (!existing.equals(duplicate)) {
throw new IllegalArgumentException("Cannot resolve installer profile! Library %s has different files: %s and %s.".formatted(
libraryFile.getIdentifier().get(),
existing,
duplicate));
}
}
}
var libraries = new ArrayList<>(
LibraryCollector.resolveLibraries(getRepositoryURLs().get(), libraryFilesToResolve.values()));
var universalJar = getUniversalJar().getAsFile().get().toPath();
libraries.add(new Library(
"net.neoforged:neoforge:%s:universal".formatted(getNeoForgeVersion().get()),
new LibraryDownload(new LibraryArtifact(
LibraryCollector.sha1Hash(universalJar),
Files.size(universalJar),
"https://maven.neoforged.net/releases/net/neoforged/neoforge/%s/neoforge-%s-universal.jar".formatted(
getNeoForgeVersion().get(),
getNeoForgeVersion().get()),
"net/neoforged/neoforge/%s/neoforge-%s-universal.jar".formatted(
getNeoForgeVersion().get(),
getNeoForgeVersion().get())
))));
var profile = new InstallerProfile(
"1",
"NeoForge",
"neoforge-%s".formatted(getNeoForgeVersion().get()),
icon,
getMinecraftVersion().get(),
"/version.json",
"/big_logo.png",
"Welcome to the simple NeoForge installer",
"https://mirrors.neoforged.net",
true,
data,
processors,
libraries,
"{LIBRARY_DIR}/net/minecraft/server/{MINECRAFT_VERSION}/server-{MINECRAFT_VERSION}.jar"
);
FileUtils.writeStringSafe(
getInstallerProfile().getAsFile().get().toPath(),
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(profile),
StandardCharsets.UTF_8
);
}
}
record InstallerProfile(
String spec,
String profile,
String version,
String icon,
String minecraft,
String json,
String logo,
String welcome,
String mirrorList,
boolean hideExtract,
Map<String, LauncherDataEntry> data,
List<ProcessorEntry> processors,
List<Library> libraries,
String serverJarPath) {}
record LauncherDataEntry(
String client,
String server) {}
record ProcessorEntry(
@Nullable
List<String> sides,
String jar,
List<String> classpath,
List<String> args) {}

View File

@ -0,0 +1,130 @@
package net.neoforged.neodev.installer;
import com.google.gson.GsonBuilder;
import net.neoforged.neodev.utils.DependencyUtils;
import net.neoforged.neodev.utils.FileUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import javax.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Creates the JSON file for running NeoForge via the Vanilla launcher.
*/
public abstract class CreateLauncherProfile extends DefaultTask {
@Inject
public CreateLauncherProfile() {}
@Input
public abstract Property<String> getFmlVersion();
@Input
public abstract Property<String> getMinecraftVersion();
@Input
public abstract Property<String> getNeoForgeVersion();
@Input
public abstract Property<String> getRawNeoFormVersion();
@Nested
protected abstract ListProperty<IdentifiedFile> getLibraryFiles();
public void setLibraries(Configuration libraries) {
getLibraryFiles().set(IdentifiedFile.listFromConfiguration(getProject(), libraries));
}
@Input
public abstract ListProperty<URI> getRepositoryURLs();
@Input
public abstract ListProperty<String> getIgnoreList();
@Input
protected abstract Property<String> getModulePath();
public void setModules(Configuration modules) {
getModulePath().set(DependencyUtils.configurationToClasspath(modules, "${library_directory}/", "${classpath_separator}"));
}
@OutputFile
public abstract RegularFileProperty getLauncherProfile();
@TaskAction
public void createLauncherProfile() throws IOException {
var time = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
getLogger().info("Collecting libraries for Launcher Profile");
var libraries = LibraryCollector.resolveLibraries(getRepositoryURLs().get(), getLibraryFiles().get());
var gameArguments = new ArrayList<>(List.of(
"--fml.neoForgeVersion", getNeoForgeVersion().get(),
"--fml.fmlVersion", getFmlVersion().get(),
"--fml.mcVersion", getMinecraftVersion().get(),
"--fml.neoFormVersion", getRawNeoFormVersion().get(),
"--launchTarget", "forgeclient"));
var jvmArguments = new ArrayList<>(List.of(
"-Djava.net.preferIPv6Addresses=system",
"-DignoreList=" + String.join(",", getIgnoreList().get()),
"-DlibraryDirectory=${library_directory}"));
jvmArguments.add("-p");
jvmArguments.add(getModulePath().get());
jvmArguments.addAll(List.of(
"--add-modules", "ALL-MODULE-PATH",
"--add-opens", "java.base/java.util.jar=cpw.mods.securejarhandler",
"--add-opens", "java.base/java.lang.invoke=cpw.mods.securejarhandler",
"--add-exports", "java.base/sun.security.util=cpw.mods.securejarhandler",
"--add-exports", "jdk.naming.dns/com.sun.jndi.dns=java.naming"));
var arguments = new LinkedHashMap<String, List<String>>();
arguments.put("game", gameArguments);
arguments.put("jvm", jvmArguments);
var profile = new LauncherProfile(
"neoforge-%s".formatted(getNeoForgeVersion().get()),
time,
time,
"release",
"cpw.mods.bootstraplauncher.BootstrapLauncher",
getMinecraftVersion().get(),
arguments,
libraries
);
FileUtils.writeStringSafe(
getLauncherProfile().getAsFile().get().toPath(),
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(profile),
StandardCharsets.UTF_8
);
}
}
record LauncherProfile(
String id,
String time,
String releaseTime,
String type,
String mainClass,
String inheritsFrom,
Map<String, List<String>> arguments,
List<Library> libraries) {}

View File

@ -0,0 +1,48 @@
package net.neoforged.neodev.installer;
import net.neoforged.neodev.utils.DependencyUtils;
import net.neoforged.neodev.utils.MavenIdentifier;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import javax.inject.Inject;
import java.io.File;
import java.util.List;
/**
* Combines a {@link File} and its {@link MavenIdentifier maven identifier},
* for usage as task inputs that will be passed to {@link LibraryCollector}.
*/
abstract class IdentifiedFile {
static Provider<List<IdentifiedFile>> listFromConfiguration(Project project, Configuration configuration) {
return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(
artifacts -> artifacts.stream()
.map(artifact -> IdentifiedFile.of(project, artifact))
.toList());
}
private static IdentifiedFile of(Project project, ResolvedArtifactResult resolvedArtifact) {
var identifiedFile = project.getObjects().newInstance(IdentifiedFile.class);
identifiedFile.getFile().set(resolvedArtifact.getFile());
identifiedFile.getIdentifier().set(DependencyUtils.guessMavenIdentifier(resolvedArtifact));
return identifiedFile;
}
@Inject
public IdentifiedFile() {}
@InputFile
@PathSensitive(PathSensitivity.NONE)
protected abstract RegularFileProperty getFile();
@Input
protected abstract Property<MavenIdentifier> getIdentifier();
}

View File

@ -0,0 +1,19 @@
package net.neoforged.neodev.installer;
import net.neoforged.neodev.Tools;
/**
* Identifies the tools used by the {@link InstallerProfile} to install NeoForge.
*/
public enum InstallerProcessor {
BINPATCHER(Tools.BINPATCHER),
FART(Tools.AUTO_RENAMING_TOOL),
INSTALLERTOOLS(Tools.INSTALLERTOOLS),
JARSPLITTER(Tools.JARSPLITTER);
public final Tools tool;
InstallerProcessor(Tools tool) {
this.tool = tool;
}
}

View File

@ -0,0 +1,3 @@
package net.neoforged.neodev.installer;
record Library(String name, LibraryDownload downloads) {}

View File

@ -0,0 +1,7 @@
package net.neoforged.neodev.installer;
record LibraryArtifact(
String sha1,
long size,
String url,
String path) {}

View File

@ -0,0 +1,174 @@
package net.neoforged.neodev.installer;
import net.neoforged.neodev.utils.MavenIdentifier;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.Function;
/**
* For each file in a collection, finds the repository that the file came from.
*/
class LibraryCollector {
public static List<Library> resolveLibraries(List<URI> repositoryUrls, Collection<IdentifiedFile> libraries) throws IOException {
var collector = new LibraryCollector(repositoryUrls);
for (var library : libraries) {
collector.addLibrary(library.getFile().getAsFile().get(), library.getIdentifier().get());
}
var result = collector.libraries.stream().map(future -> {
try {
return future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).toList();
LOGGER.info("Collected %d libraries".formatted(result.size()));
return result;
}
private static final Logger LOGGER = Logging.getLogger(LibraryCollector.class);
/**
* Hosts from which we allow the installer to download.
* We whitelist here to avoid redirecting player download traffic to anyone not affiliated with Mojang or us.
*/
private static final List<String> HOST_WHITELIST = List.of(
"minecraft.net",
"neoforged.net",
"mojang.com"
);
private static final URI MOJANG_MAVEN = URI.create("https://libraries.minecraft.net");
private static final URI NEOFORGED_MAVEN = URI.create("https://maven.neoforged.net/releases");
private final List<URI> repositoryUrls;
private final List<Future<Library>> libraries = new ArrayList<>();
private final HttpClient httpClient = HttpClient.newBuilder().build();
private LibraryCollector(List<URI> repoUrl) {
this.repositoryUrls = new ArrayList<>(repoUrl);
// Only remote repositories make sense (no maven local)
repositoryUrls.removeIf(it -> {
var lowercaseScheme = it.getScheme().toLowerCase(Locale.ROOT);
return !lowercaseScheme.equals("https") && !lowercaseScheme.equals("http");
});
// Allow only URLs from whitelisted hosts
repositoryUrls.removeIf(uri -> {
var lowercaseHost = uri.getHost().toLowerCase(Locale.ROOT);
return HOST_WHITELIST.stream().noneMatch(it -> lowercaseHost.equals(it) || lowercaseHost.endsWith("." + it));
});
// Always try Mojang Maven first, then our installer Maven
repositoryUrls.removeIf(it -> it.getHost().equals(MOJANG_MAVEN.getHost()));
repositoryUrls.removeIf(it -> it.getHost().equals(NEOFORGED_MAVEN.getHost()) && it.getPath().startsWith(NEOFORGED_MAVEN.getPath()));
repositoryUrls.add(0, NEOFORGED_MAVEN);
repositoryUrls.add(0, MOJANG_MAVEN);
LOGGER.info("Collecting libraries from:");
for (var repo : repositoryUrls) {
LOGGER.info(" - " + repo);
}
}
private void addLibrary(File file, MavenIdentifier identifier) throws IOException {
final String name = identifier.artifactNotation();
final String path = identifier.repositoryPath();
var sha1 = sha1Hash(file.toPath());
var fileSize = Files.size(file.toPath());
// Try each configured repository in-order to find the file
CompletableFuture<Library> libraryFuture = null;
for (var repositoryUrl : repositoryUrls) {
var artifactUri = joinUris(repositoryUrl, path);
var request = HttpRequest.newBuilder(artifactUri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
Function<String, CompletableFuture<Library>> makeRequest = (String previousError) -> {
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding())
.thenApply(response -> {
if (response.statusCode() != 200) {
LOGGER.info(" Got %d for %s".formatted(response.statusCode(), artifactUri));
String message = "Could not find %s: %d".formatted(artifactUri, response.statusCode());
// Prepend error message from previous repo if they all fail
if (previousError != null) {
message = previousError + "\n" + message;
}
throw new RuntimeException(message);
}
LOGGER.info(" Found %s -> %s".formatted(name, artifactUri));
return new Library(
name,
new LibraryDownload(new LibraryArtifact(
sha1,
fileSize,
artifactUri.toString(),
path)));
});
};
if (libraryFuture == null) {
libraryFuture = makeRequest.apply(null);
} else {
libraryFuture = libraryFuture.exceptionallyCompose(error -> {
return makeRequest.apply(error.getMessage());
});
}
}
libraries.add(libraryFuture);
}
static String sha1Hash(Path path) throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try (var in = Files.newInputStream(path);
var din = new DigestInputStream(in, digest)) {
byte[] buffer = new byte[8192];
while (din.read(buffer) != -1) {
}
}
return HexFormat.of().formatHex(digest.digest());
}
private static URI joinUris(URI repositoryUrl, String path) {
var baseUrl = repositoryUrl.toString();
if (baseUrl.endsWith("/") && path.startsWith("/")) {
while (path.startsWith("/")) {
path = path.substring(1);
}
return URI.create(baseUrl + path);
} else if (!baseUrl.endsWith("/") && !path.startsWith("/")) {
return URI.create(baseUrl + "/" + path);
} else {
return URI.create(baseUrl + path);
}
}
}

View File

@ -0,0 +1,4 @@
package net.neoforged.neodev.installer;
record LibraryDownload(
LibraryArtifact artifact) {}

View File

@ -0,0 +1,84 @@
package net.neoforged.neodev.utils;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.provider.Provider;
import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class DependencyUtils {
private DependencyUtils() {
}
/**
* Given a resolved artifact, try to guess which Maven GAV it was resolved from.
*/
public static MavenIdentifier guessMavenIdentifier(ResolvedArtifactResult result) {
String group;
String artifact;
String version;
String ext = "";
String classifier = "";
var filename = result.getFile().getName();
var startOfExt = filename.lastIndexOf('.');
if (startOfExt != -1) {
ext = filename.substring(startOfExt + 1);
filename = filename.substring(0, startOfExt);
}
if (result.getId() instanceof ModuleComponentArtifactIdentifier moduleId) {
group = moduleId.getComponentIdentifier().getGroup();
artifact = moduleId.getComponentIdentifier().getModule();
version = moduleId.getComponentIdentifier().getVersion();
var expectedBasename = artifact + "-" + version;
if (filename.startsWith(expectedBasename + "-")) {
classifier = filename.substring((expectedBasename + "-").length());
}
} else {
// When we encounter a project reference, the component identifier does not expose the group or module name.
// But we can access the list of capabilities associated with the published variant the artifact originates from.
// If the capability was not overridden, this will be the project GAV. If it is *not* the project GAV,
// it will be at least in valid GAV format, not crashing NFRT when it parses the manifest. It will just be ignored.
var capabilities = result.getVariant().getCapabilities();
if (capabilities.size() == 1) {
var capability = capabilities.get(0);
group = capability.getGroup();
artifact = capability.getName();
version = capability.getVersion();
} else {
throw new IllegalArgumentException("Don't know how to break " + result.getId().getComponentIdentifier() + " into Maven components.");
}
}
return new MavenIdentifier(group, artifact, version, classifier, ext);
}
/**
* Turns a configuration into a list of GAV entries.
*/
public static Provider<List<String>> configurationToGavList(Configuration configuration) {
return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
// Using .toList() fails with the configuration cache - looks like Gradle can't deserialize the resulting list?
return results.stream().map(artifact -> guessMavenIdentifier(artifact).artifactNotation()).collect(Collectors.toCollection(ArrayList::new));
});
}
/**
* Turns a configuration into a classpath string,
* assuming that the contents of the configuration are installed following the Maven directory layout.
*
* @param prefix string to add in front of each classpath entry
* @param separator separator to add between each classpath entry
*/
public static Provider<String> configurationToClasspath(Configuration configuration, String prefix, String separator) {
return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(
results -> results.stream()
.map(artifact -> prefix + guessMavenIdentifier(artifact).repositoryPath())
.collect(Collectors.joining(separator))
);
}
}

View File

@ -0,0 +1,72 @@
package net.neoforged.neodev.utils;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
public final class FileUtils {
private FileUtils() {
}
public static void writeStringSafe(Path destination, String content, Charset charset) throws IOException {
if (!charset.newEncoder().canEncode(content)) {
throw new IllegalArgumentException("The given character set " + charset
+ " cannot represent this string: " + content);
}
try (var out = newSafeFileOutputStream(destination)) {
var encodedContent = content.getBytes(charset);
out.write(encodedContent);
}
}
public static void writeLinesSafe(Path destination, List<String> lines, Charset charset) throws IOException {
writeStringSafe(destination, String.join("\n", lines), charset);
}
public static OutputStream newSafeFileOutputStream(Path destination) throws IOException {
var uniqueId = ProcessHandle.current().pid() + "." + Thread.currentThread().getId();
var tempFile = destination.resolveSibling(destination.getFileName().toString() + "." + uniqueId + ".tmp");
var closed = new boolean[1];
return new FilterOutputStream(Files.newOutputStream(tempFile)) {
@Override
public void close() throws IOException {
try {
super.close();
if (!closed[0]) {
atomicMoveIfPossible(tempFile, destination);
}
} finally {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
closed[0] = true;
}
}
};
}
/**
* Atomically moves the given source file to the given destination file.
* If the atomic move is not supported, the file will be moved normally.
*
* @param source The source file
* @param destination The destination file
* @throws IOException If an I/O error occurs
*/
private static void atomicMoveIfPossible(final Path source, final Path destination) throws IOException {
try {
Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException ex) {
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}

View File

@ -0,0 +1,13 @@
package net.neoforged.neodev.utils;
import java.io.Serializable;
public record MavenIdentifier(String group, String artifact, String version, String classifier, String extension) implements Serializable {
public String artifactNotation() {
return group + ":" + artifact + ":" + version + (classifier.isEmpty() ? "" : ":" + classifier) + ("jar".equals(extension) ? "" : "@" + extension);
}
public String repositoryPath() {
return group.replace(".", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + (classifier.isEmpty() ? "" : "-" + classifier) + "." + extension;
}
}

View File

@ -5,15 +5,6 @@ plugins {
id 'neoforge.formatting-conventions'
}
repositories {
maven { url = 'https://maven.neoforged.net/releases' }
maven {
name 'Mojang'
url 'https://libraries.minecraft.net'
}
mavenCentral()
}
jar {
manifest {
attributes(

8
coremods/settings.gradle Normal file
View File

@ -0,0 +1,8 @@
repositories {
maven { url = 'https://maven.neoforged.net/releases' }
maven {
name 'Mojang'
url 'https://libraries.minecraft.net'
}
mavenCentral()
}

View File

@ -4,8 +4,14 @@ org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=false
org.gradle.configuration-cache=true
org.gradle.debug=false
#org.gradle.warning.mode=fail
# renovate: net.neoforged:moddev-gradle
moddevgradle_plugin_version=2.0.46-beta
# renovate: io.codechicken:DiffPatch
diffpatch_version=2.0.0.35
java_version=21
@ -14,6 +20,14 @@ neoform_version=20241023.131943
# on snapshot versions, used to prefix the version
neoforge_snapshot_next_stable=21.4
# renovate: net.neoforged.jst:jst-cli-bundle
jst_version=1.0.45
legacyinstaller_version=3.0.+
# renovate: net.neoforged:AutoRenamingTool
art_version=2.0.3
# renovate: net.neoforged.installertools:installertools
installertools_version=2.1.2
mergetool_version=2.0.0
accesstransformers_version=11.0.1
coremods_version=6.0.4
@ -22,7 +36,6 @@ modlauncher_version=11.0.4
securejarhandler_version=3.0.8
bootstraplauncher_version=2.0.2
asm_version=9.7
installer_version=2.1.+
mixin_version=0.15.2+mixin.0.8.7
terminalconsoleappender_version=1.3.0
nightconfig_version=3.8.0
@ -43,12 +56,8 @@ nashorn_core_version=15.3
lwjgl_glfw_version=3.3.2
mixin_extras_version=0.4.1
jupiter_api_version=5.7.0
jupiter_api_version=5.10.2
vintage_engine_version=5.+
assertj_core=3.25.1
neogradle.runtime.platform.installer.debug=true
# We want to be able to have a junit run disconnected from the test and main sourcesets
neogradle.subsystems.conventions.sourcesets.automatic-inclusion=false
neogradle.subsystems.conventions.enabled=false
neogradle.subsystems.tools.jst=net.neoforged.jst:jst-cli-bundle:1.0.45

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,3 +1,15 @@
dynamicProject {
neoform("${project.minecraft_version}-${project.neoform_version}")
plugins {
id 'java-library'
}
apply plugin: net.neoforged.neodev.NeoDevBasePlugin
dependencies {
implementation("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform-dependencies'
}
endorseStrictVersions()
}
}

View File

@ -10,6 +10,11 @@ plugins {
id 'neoforge.versioning'
}
apply plugin : net.neoforged.neodev.NeoDevPlugin
// Because of the source set reference.
evaluationDependsOn(":neoforge-coremods")
gradleutils.setupSigning(project: project, signAllPublications: true)
changelog {
@ -17,55 +22,6 @@ changelog {
disableAutomaticPublicationRegistration()
}
dynamicProject {
runtime("${project.minecraft_version}-${project.neoform_version}",
rootProject.layout.projectDirectory.dir('patches'),
rootProject.layout.projectDirectory.dir('rejects'))
}
final checkVersion = JCCPlugin.providePreviousVersion(
project.providers, project.providers.provider({['https://maven.neoforged.net/releases']}), project.providers.provider({'net.neoforged:neoforge'}),
project.provider { project.version }.map { ver -> CompatibilityTask.VersionComponentTest.MINOR.predicate(ver) }
)
final createCompatJar = tasks.register('createCompatibilityCheckJar', ProvideNeoForgeJarTask) {
// Use the same jar that the patches were generated against
cleanJar.set(tasks.generateClientBinaryPatches.clean)
maven.set('https://maven.neoforged.net/releases')
artifact.set('net.neoforged:neoforge')
version.set(checkVersion)
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(java_version)
}
}
checkJarCompatibility {
isAPI = true
baseJar = createCompatJar.flatMap { it.output }
}
installerProfile {
profile = 'NeoForge'
}
minecraft {
// FML looks for this mod id to find the minecraft classes
modIdentifier 'minecraft'
accessTransformers {
file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
}
}
tasks.configureEach { tsk ->
if (tsk.name == 'neoFormApplyUserAccessTransformer' && project.hasProperty('validateAccessTransformers')) {
tsk.inputs.property('validation', 'error')
tsk.logLevel('ERROR')
tsk.doFirst {
tsk.getRuntimeProgramArguments().addAll(tsk.getRuntimeProgramArguments().get())
tsk.getRuntimeProgramArguments().add('--access-transformer-validation=error')
}
}
}
sourceSets {
main {
java {
@ -77,12 +33,56 @@ sourceSets {
}
}
dependencies {
runtimeOnly "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
final checkVersion = JCCPlugin.providePreviousVersion(
project.providers, project.providers.provider({['https://maven.neoforged.net/releases']}), project.providers.provider({'net.neoforged:neoforge'}),
project.provider { project.version }.map { ver -> CompatibilityTask.VersionComponentTest.MINOR.predicate(ver) }
)
final createCompatJar = tasks.register('createCompatibilityCheckJar', ProvideNeoForgeJarTask) {
// Use the same jar that the patches were generated against
cleanJar.set(tasks.generateClientBinPatches.cleanJar)
maven.set('https://maven.neoforged.net/releases')
artifact.set('net.neoforged:neoforge')
version.set(checkVersion)
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(java_version)
}
}
checkJarCompatibility {
isAPI = true
baseJar = createCompatJar.flatMap { it.output }
}
moduleOnly "cpw.mods:securejarhandler:${project.securejarhandler_version}"
neoDev {
mods {
minecraft {
sourceSet sourceSets.main
}
"neoforge-coremods" {
sourceSet project(":neoforge-coremods").sourceSets.main
}
}
}
dependencies {
// For an overview of what the nonstandard configurations do,
// have a look at NeoDevConfigurations.java in the buildSrc folder.
neoFormData("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform'
}
endorseStrictVersions()
}
neoFormDependencies("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform-dependencies'
}
endorseStrictVersions()
}
moduleLibraries "cpw.mods:securejarhandler:${project.securejarhandler_version}"
for (var asmModule : ["org.ow2.asm:asm", "org.ow2.asm:asm-commons", "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.ow2.asm:asm-analysis"]) {
moduleOnly(asmModule) {
moduleLibraries(asmModule) {
// Vanilla ships with ASM 9.3 transitively (via their OpenID connect library dependency), we require
// ASM in a more recent version and have to strictly require this to override the strict Minecraft version.
version {
@ -90,166 +90,72 @@ dependencies {
}
}
}
moduleOnly "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
moduleOnly "net.neoforged:JarJarFileSystems:${project.jarjar_version}"
moduleLibraries "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
moduleLibraries "net.neoforged:JarJarFileSystems:${project.jarjar_version}"
installer ("net.neoforged.fancymodloader:loader:${project.fancy_mod_loader_version}") {
libraries ("net.neoforged.fancymodloader:loader:${project.fancy_mod_loader_version}") {
exclude group: 'org.slf4j'
exclude group: 'net.fabricmc'
}
installer ("net.neoforged.fancymodloader:earlydisplay:${project.fancy_mod_loader_version}") {
libraries ("net.neoforged.fancymodloader:earlydisplay:${project.fancy_mod_loader_version}") {
exclude group: 'org.lwjgl'
exclude group: 'org.slf4j'
exclude group: 'net.fabricmc'
}
installer "cpw.mods:securejarhandler:${project.securejarhandler_version}"
installer "org.ow2.asm:asm:${project.asm_version}"
installer "org.ow2.asm:asm-commons:${project.asm_version}"
installer "org.ow2.asm:asm-tree:${project.asm_version}"
installer "org.ow2.asm:asm-util:${project.asm_version}"
installer "org.ow2.asm:asm-analysis:${project.asm_version}"
installer "net.neoforged:accesstransformers:${project.accesstransformers_version}"
installer "net.neoforged:bus:${project.eventbus_version}"
installer "net.neoforged:coremods:${project.coremods_version}"
installer "cpw.mods:modlauncher:${project.modlauncher_version}"
installer "net.neoforged:mergetool:${project.mergetool_version}:api"
installer "com.electronwill.night-config:core:${project.nightconfig_version}"
installer "com.electronwill.night-config:toml:${project.nightconfig_version}"
installer "org.apache.maven:maven-artifact:${project.apache_maven_artifact_version}"
installer "net.jodah:typetools:${project.typetools_version}"
installer "net.minecrell:terminalconsoleappender:${project.terminalconsoleappender_version}"
installer("net.fabricmc:sponge-mixin:${project.mixin_version}") { transitive = false }
installer "org.openjdk.nashorn:nashorn-core:${project.nashorn_core_version}"
installer ("net.neoforged:JarJarSelector:${project.jarjar_version}") {
libraries "net.neoforged:accesstransformers:${project.accesstransformers_version}"
libraries "net.neoforged:bus:${project.eventbus_version}"
libraries "net.neoforged:coremods:${project.coremods_version}"
libraries "cpw.mods:modlauncher:${project.modlauncher_version}"
libraries "net.neoforged:mergetool:${project.mergetool_version}:api"
libraries "com.electronwill.night-config:core:${project.nightconfig_version}"
libraries "com.electronwill.night-config:toml:${project.nightconfig_version}"
libraries "org.apache.maven:maven-artifact:${project.apache_maven_artifact_version}"
libraries "net.jodah:typetools:${project.typetools_version}"
libraries "net.minecrell:terminalconsoleappender:${project.terminalconsoleappender_version}"
libraries("net.fabricmc:sponge-mixin:${project.mixin_version}") { transitive = false }
libraries "org.openjdk.nashorn:nashorn-core:${project.nashorn_core_version}"
libraries ("net.neoforged:JarJarSelector:${project.jarjar_version}") {
exclude group: 'org.slf4j'
}
// We depend on apache commons directly as there is a difference between the version the server uses and the one the client does
installer "org.apache.commons:commons-lang3:${project.apache_commons_lang3_version}"
installer ("net.neoforged:JarJarMetadata:${project.jarjar_version}") {
libraries "org.apache.commons:commons-lang3:${project.apache_commons_lang3_version}"
libraries ("net.neoforged:JarJarMetadata:${project.jarjar_version}") {
exclude group: 'org.slf4j'
}
// Manually override log4j since the version coming from other `installer` dependencies is outdated
installer "org.apache.logging.log4j:log4j-api:${project.log4j_version}"
installer "org.apache.logging.log4j:log4j-core:${project.log4j_version}"
compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}"
userdevCompileOnly jarJar("io.github.llamalad7:mixinextras-neoforge:${project.mixin_extras_version}"), {
jarJar.ranged(it, "[${project.mixin_extras_version},)")
userdevCompileOnly jarJar("io.github.llamalad7:mixinextras-neoforge:${project.mixin_extras_version}")
userdevTestFixtures("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}") {
endorseStrictVersions()
}
userdevTestImplementation("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}")
compileOnly(jarJar(project(":neoforge-coremods")))
// Must be implementation instead of compileOnly so that running dependent projects such as tests will trigger (re)compilation of coremods.
// (Only needed when compiling through IntelliJ non-delegated builds - otherwise `compileOnly` would work).
implementation(jarJar(project(":neoforge-coremods")))
}
runTypes {
client {
singleInstance false
client true
arguments.addAll '--fml.neoForgeVersion', project.version
arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
arguments.addAll '--fml.mcVersion', project.minecraft_version
arguments.addAll '--fml.neoFormVersion', project.neoform_version
}
server {
server true
arguments.addAll '--fml.neoForgeVersion', project.version
arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
arguments.addAll '--fml.mcVersion', project.minecraft_version
arguments.addAll '--fml.neoFormVersion', project.neoform_version
}
gameTestServer {
from project.runTypes.server
gameTest true
}
gameTestClient {
from project.runTypes.client
gameTest true
}
data {
dataGenerator true
// Don't set modid here so we can reuse this runType for test datagen
arguments.addAll '--fml.neoForgeVersion', project.version
arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
arguments.addAll '--fml.mcVersion', project.minecraft_version
arguments.addAll '--fml.neoFormVersion', project.neoform_version
}
junit {
junit true
arguments.addAll '--fml.neoForgeVersion', project.version
arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
arguments.addAll '--fml.mcVersion', project.minecraft_version
arguments.addAll '--fml.neoFormVersion', project.neoform_version
}
}
runs {
client { }
server { }
gameTestServer { }
gameTestClient { }
data {
arguments.addAll '--mod', 'neoforge'
modSources.add project.sourceSets.main
idea {
primarySourceSet project.sourceSets.main
neoDev {
runs {
client {
client()
}
server {
server()
}
gameTestServer {
type = "gameTestServer"
}
data {
data()
programArguments.addAll '--mod', 'neoforge', '--flat', '--all', '--validate',
'--existing', rootProject.file("src/main/resources").absolutePath,
'--output', rootProject.file("src/generated/resources").absolutePath
}
}
}
runs.configureEach { it ->
modSources.add project(":neoforge-coremods").sourceSets.main
final File gameDir = project.file("run/${it.name}") as File
gameDir.mkdirs();
it.workingDirectory.set gameDir
it.arguments.addAll '--gameDir', gameDir.absolutePath
}
tasks.register("genPatches") {
dependsOn tasks.unpackSourcePatches
}
launcherProfile {
arguments {
game '--fml.neoForgeVersion'
game project.version
game '--fml.fmlVersion'
game project.fancy_mod_loader_version
game '--fml.mcVersion'
game project.minecraft_version
game '--fml.neoFormVersion'
game project.neoform_version
}
}
userdevProfile {
runTypes.configureEach {
argument '--fml.neoForgeVersion'
argument project.version
argument '--fml.fmlVersion'
argument project.fancy_mod_loader_version
argument '--fml.mcVersion'
argument project.minecraft_version
argument '--fml.neoFormVersion'
argument project.neoform_version
}
additionalTestDependencyArtifactCoordinate "net.neoforged:testframework:${project.version}"
}
tasks.withType(Javadoc.class).configureEach {
options.tags = [
'apiNote:a:<em>API Note:</em>',
@ -259,27 +165,6 @@ tasks.withType(Javadoc.class).configureEach {
options.addStringOption('Xdoclint:all,-missing', '-public')
}
configurations {
forValidation {
canBeConsumed = true
canBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
}
extendsFrom api, runtimeOnly
}
}
artifacts {
forValidation(jar.archiveFile) {
builtBy(jar)
}
}
AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.components.findByName("java")
// Ensure the two default variants are not published, since they
// contain Minecraft classes
@ -290,10 +175,12 @@ javaComponent.withVariantsFromConfiguration(configurations.runtimeElements) {
it.skip()
}
// Resolvable configurations only
configurations {
modDevBundle {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
extendsFrom neoFormData
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "data"))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -304,8 +191,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {} // Publish it
}
modDevConfig {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "data"))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -316,8 +203,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {} // Publish it
}
installerJar {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
@ -332,8 +219,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
universalJar {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
@ -345,8 +232,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
changelog {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, "changelog"))
@ -357,11 +244,9 @@ configurations {
}
}
modDevApiElements {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
afterEvaluate {
extendsFrom userdevCompileOnly, installerLibraries, moduleOnly
}
extendsFrom libraries, moduleLibraries, userdevCompileOnly, neoFormDependencies
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -373,11 +258,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevRuntimeElements {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
afterEvaluate {
extendsFrom installerLibraries, moduleOnly
}
extendsFrom libraries, moduleLibraries, neoFormDependencies
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -389,11 +272,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevModulePath {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
afterEvaluate {
extendsFrom moduleOnly
}
extendsFrom moduleLibraries
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -404,8 +285,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevTestFixtures {
canBeDeclared = false
canBeResolved = false
canBeConsumed = true
extendsFrom userdevTestFixtures
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@ -418,63 +300,35 @@ configurations {
}
}
dependencies {
modDevBundle("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform'
}
endorseStrictVersions()
}
modDevApiElements("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform-dependencies'
}
endorseStrictVersions()
}
modDevRuntimeElements("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
capabilities {
requireCapability 'net.neoforged:neoform-dependencies'
}
endorseStrictVersions()
}
modDevTestFixtures("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}") {
endorseStrictVersions()
}
}
processResources {
inputs.property("version", project.version)
final version = project.version
filesMatching("META-INF/neoforge.mods.toml") {
expand([
"global": [
"neoForgeVersion": project.version
"neoForgeVersion": version
]
])
}
}
afterEvaluate {
artifacts {
modDevBundle(userdevJar) {
setClassifier("userdev") // Legacy
}
modDevConfig(createUserdevJson.output) {
builtBy(createUserdevJson)
setClassifier("moddev-config")
}
universalJar(signUniversalJar.output) {
builtBy(signUniversalJar)
setClassifier("universal")
}
installerJar(signInstallerJar.output) {
builtBy(signInstallerJar)
setClassifier("installer")
}
changelog(createChangelog.outputFile) {
builtBy(createChangelog)
setClassifier("changelog")
setExtension("txt")
}
artifacts {
modDevBundle(userdevJar) {
setClassifier("userdev") // Legacy
}
modDevConfig(writeUserDevConfig.userDevConfig) {
setClassifier("moddev-config")
}
universalJar(universalJar) {
setClassifier("universal")
}
installerJar(installerJar) {
setClassifier("installer")
}
changelog(createChangelog.outputFile) {
builtBy(createChangelog)
setClassifier("changelog")
setExtension("txt")
}
}

View File

@ -1,13 +1,28 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenLocal()
maven { url = 'https://maven.neoforged.net/releases' }
mavenLocal()
}
}
plugins {
id 'net.neoforged.gradle.platform' version '7.0.171'
id 'net.neoforged.moddev.repositories' version "${moddevgradle_plugin_version}"
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
// This makes the version available to buildSrc
gradle.ext.moddevgradle_plugin_version = moddevgradle_plugin_version
gradle.ext.gson_version = gson_version
gradle.ext.diffpatch_version = diffpatch_version
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
rulesMode = RulesMode.FAIL_ON_PROJECT_RULES
repositories {
mavenCentral()
mavenLocal()
}
}
if (rootProject.name.toLowerCase() == "neoforge") {
@ -15,13 +30,10 @@ if (rootProject.name.toLowerCase() == "neoforge") {
rootProject.name = "NeoForge-Root"
}
dynamicProjects {
include ':base'
include ':neoforge'
project(":base").projectDir = file("projects/base")
project(":neoforge").projectDir = file("projects/neoforge")
}
include ':base'
project(':base').projectDir = file('projects/base')
include ':neoforge'
project(':neoforge').projectDir = file('projects/neoforge')
include ':tests'
project(":tests").projectDir = file("tests")

View File

@ -3,25 +3,19 @@ plugins {
id 'maven-publish'
id 'com.diffplug.spotless'
id 'net.neoforged.licenser'
id 'net.neoforged.gradle.platform'
id 'neoforge.formatting-conventions'
}
java.withSourcesJar()
repositories {
maven {
name 'Mojang'
url 'https://libraries.minecraft.net'
}
maven {
name 'NeoForged'
url 'https://maven.neoforged.net/releases'
}
}
apply plugin : net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin
dependencies {
implementation project(path: ':neoforge', configuration: 'runtimeElements')
// TODO: is this leaking in the POM? (most likely yes)
// TODO: does this need to be changed back to runtimeDependencies?
// TODO: should use attributes to resolve the right variant instead of hardcoding
compileOnly project(path: ':neoforge', configuration: 'apiElements')
runtimeOnly project(path: ':neoforge', configuration: 'runtimeElements')
compileOnly(platform("org.junit:junit-bom:${project.jupiter_api_version}"))
compileOnly "org.junit.jupiter:junit-jupiter-params"
@ -30,6 +24,14 @@ dependencies {
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
}
sourceSets {
main {
// TODO: cursed
compileClasspath += project(':neoforge').sourceSets.main.compileClasspath
runtimeClasspath += project(':neoforge').sourceSets.main.runtimeClasspath
}
}
license {
header = rootProject.file('codeformat/HEADER.txt')
include '**/*.java'
@ -39,6 +41,7 @@ tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
def version = project.version
tasks.withType(ProcessResources).configureEach {
inputs.properties version: version

View File

@ -1,27 +1,18 @@
plugins {
id 'java'
id 'net.neoforged.gradle.platform'
id 'com.diffplug.spotless'
id 'net.neoforged.licenser'
id 'neoforge.formatting-conventions'
}
apply plugin : net.neoforged.neodev.NeoDevExtraPlugin
evaluationDependsOn(":neoforge")
def neoforgeProject = project(':neoforge')
def testframeworkProject = project(':testframework')
def coremodsProject = project(':neoforge-coremods')
repositories {
mavenLocal()
maven {
name 'Mojang'
url 'https://libraries.minecraft.net'
}
maven {
name 'NeoForged'
url 'https://maven.neoforged.net/releases'
}
}
sourceSets {
main {
resources {
@ -31,10 +22,6 @@ sourceSets {
junit {}
}
configurations {
junitImplementation.extendsFrom(implementation)
}
dependencies {
implementation project(path: ':neoforge', configuration: 'runtimeElements')
implementation(testframeworkProject)
@ -45,74 +32,79 @@ dependencies {
junitImplementation("org.assertj:assertj-core:${project.assertj_core}")
junitImplementation "net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}"
junitImplementation project(path: ':neoforge', configuration: 'runtimeElements')
junitImplementation(testframeworkProject)
compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}"
}
runs {
client {
configure neoforgeProject.runTypes.client
}
junit {
configure neoforgeProject.runTypes.junit
unitTestSource sourceSets.junit
}
server {
configure neoforgeProject.runTypes.server
}
gameTestServer {
configure neoforgeProject.runTypes.gameTestServer
}
gameTestClient {
configure neoforgeProject.runTypes.gameTestClient
}
data {
configure neoforgeProject.runTypes.data
junitTest {
useJUnitPlatform()
classpath = sourceSets.junit.output + sourceSets.junit.runtimeClasspath
testClassesDirs = sourceSets.junit.output.classesDirs
outputs.upToDateWhen { false }
}
arguments.addAll '--flat', '--all', '--validate',
'--mod', 'data_gen_test',
'--mod', 'global_loot_test',
'--mod', 'scaffolding_test',
'--mod', 'custom_tag_types_test',
'--mod', 'new_model_loader_test',
'--mod', 'remove_tag_datagen_test',
'--mod', 'tag_based_tool_types',
'--mod', 'custom_transformtype_test',
'--mod', 'data_pack_registries_test',
'--mod', 'biome_modifiers_test',
'--mod', 'structure_modifiers_test',
'--mod', 'custom_preset_editor_test',
'--mod', 'custom_predicate_test',
'--mod', 'neotests',
'--existing-mod', 'testframework',
'--existing', sourceSets.main.resources.srcDirs[0].absolutePath
neoDev {
mods {
neotests {
sourceSet sourceSets.main
}
testframework {
sourceSet project(":testframework").sourceSets.main
}
junit {
sourceSet sourceSets.junit
}
coremods {
sourceSet coremodsProject.sourceSets.main
}
}
final File gameDir = project.file("runs/${name}") as File
gameDir.mkdirs();
runs {
client {
client()
}
server {
server()
}
gameTestServer {
type = "gameTestServer"
}
data {
data()
workingDirectory.set gameDir
arguments.addAll '--gameDir', gameDir.absolutePath
programArguments.addAll '--flat', '--all', '--validate',
'--mod', 'data_gen_test',
'--mod', 'global_loot_test',
'--mod', 'scaffolding_test',
'--mod', 'custom_tag_types_test',
'--mod', 'new_model_loader_test',
'--mod', 'remove_tag_datagen_test',
'--mod', 'tag_based_tool_types',
'--mod', 'custom_transformtype_test',
'--mod', 'data_pack_registries_test',
'--mod', 'biome_modifiers_test',
'--mod', 'structure_modifiers_test',
'--mod', 'custom_preset_editor_test',
'--mod', 'custom_predicate_test',
'--mod', 'neotests',
'--existing-mod', 'testframework',
'--existing', project.file("src/main/resources").absolutePath,
'--output', project.file("src/generated/resources").absolutePath
}
}
runs.configureEach {
// Add NeoForge and Minecraft (both under the "minecraft" mod), and exclude junit.
loadedMods = [neoforgeProject.neoDev.mods.minecraft, mods.neotests, mods.testframework, mods.coremods]
gameDirectory.set project.file("runs/${it.name}") as File
}
}
//We need the assets and natives tasks from the forge project.
runs.configureEach {
dependsOn.add(neoforgeProject.runtime.assets)
dependsOn.add(neoforgeProject.runtime.natives)
modSource neoforgeProject.sourceSets.main
modSource coremodsProject.sourceSets.main
modSource testframeworkProject.sourceSets.main
}
afterEvaluate {
runs.data {
// Override --output that forge already has
def args = new ArrayList<String>(arguments.get());
def outputIndex = args.indexOf('--output');
args.set(outputIndex+1, file('src/generated/resources/').absolutePath);
arguments.set(args);
}
runs.junit.modSources.all().get().values().remove(sourceSets.main)
neoDevTest {
loadedMods = [ project(":neoforge").neoDev.mods.minecraft, neoDev.mods.testframework, neoDev.mods.coremods, neoDev.mods.junit ]
}
license {