From f2c0c688974589eaa11bec6be5f9938ee846d416 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 18:17:45 -0800 Subject: [PATCH 1/7] Rewrite installer tasks --- .../forgedev/ForgeDevExtension.java | 24 ++ .../net/minecraftforge/forgedev/Tools.java | 1 + .../net/minecraftforge/forgedev/Util.java | 19 -- .../tasks/CheckForgeJarCompatibility.java | 11 + .../legacy/tasks/DownloadDependency.groovy | 22 +- .../forgedev/legacy/tasks/InstallerJar.groovy | 1 + .../forgedev/legacy/tasks/Util.groovy | 35 ++- .../forgedev/legacy/values/LibraryInfo.java | 88 ++++--- .../forgedev/legacy/values/MavenInfo.java | 26 ++ .../values/MinimalResolvedArtifact.java | 72 +++--- .../forgedev/tasks/SingleFileOutput.java | 11 + .../forgedev/tasks/installer/Installer.java | 227 ++++++++++++++++++ .../tasks/installer/InstallerJar.java | 91 +++++++ .../tasks/installer/InstallerJson.java | 196 +++++++++++++++ .../tasks/installer/LauncherJson.java | 113 +++++++++ .../forgedev/tasks/installer/Tool.java | 37 +++ .../tasks/installer/steps/Extract.java | 45 ++++ .../tasks/installer/steps/ExtractBundle.java | 41 ++++ .../forgedev/tasks/installer/steps/Step.java | 56 +++++ .../patching/binary/BinaryPatcherExec.groovy | 4 +- 20 files changed, 1027 insertions(+), 93 deletions(-) create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/SingleFileOutput.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Tool.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Step.java diff --git a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java index 217072d..43388ad 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java +++ b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java @@ -4,10 +4,12 @@ */ package net.minecraftforge.forgedev; +import net.minecraftforge.forgedev.legacy.values.CIRuntime; import net.minecraftforge.forgedev.tasks.compat.LegacyExtractZip; import net.minecraftforge.forgedev.tasks.compat.LegacyMergeFilesTask; import net.minecraftforge.forgedev.tasks.filtering.LegacyFilterNewJar; import net.minecraftforge.forgedev.tasks.generation.GeneratePatcherConfigV2; +import net.minecraftforge.forgedev.tasks.installer.Installer; import net.minecraftforge.forgedev.tasks.installertools.DownloadMappings; import net.minecraftforge.forgedev.tasks.launcher.SlimeLauncherExec; import net.minecraftforge.forgedev.tasks.mappings.LegacyApplyMappings; @@ -47,6 +49,7 @@ import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Zip; import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.internal.Actions; import org.gradle.language.base.plugins.LifecycleBasePlugin; import org.gradle.plugins.ide.eclipse.model.EclipseModel; import org.jetbrains.annotations.VisibleForTesting; @@ -77,9 +80,14 @@ public abstract class ForgeDevExtension { protected abstract @Inject ProjectLayout getProjectLayout(); + private final Project project; + private final Provider isCi; + @Inject public ForgeDevExtension(ForgeDevPlugin plugin, Project project) { this.mavenizerRepo.set(plugin.globalCaches().dir("repo").map(this.problems.ensureFileLocation())); + this.project = project; + this.isCi = getProviders().of(CIRuntime.class, it -> {}); this.setup(plugin, project); } @@ -101,6 +109,22 @@ public Configuration getMinecraftConfiguration() { return this.minecraftDepsConfiguration; } + public Installer installer() { + return installer(Installer.DEFAULT_NAME); + } + public Installer installer(String name) { + return installer(name, i -> {}); + } + public Installer installer(String name, Action action) { + var ret = Installer.register(this.project, this, name); + action.execute(ret); + return ret; + } + + public boolean isCi() { + return this.isCi.get(); + } + private void setup(ForgeDevPlugin plugin, Project project) { var tasks = project.getTasks(); diff --git a/src/main/groovy/net/minecraftforge/forgedev/Tools.java b/src/main/groovy/net/minecraftforge/forgedev/Tools.java index 6ef98d5..c30ecb0 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/Tools.java +++ b/src/main/groovy/net/minecraftforge/forgedev/Tools.java @@ -14,6 +14,7 @@ private Tools() { } public static final Tool DIFFPATCH = Tool.of("diffpatch", "io.codechicken:DiffPatch:2.1.0.42:all", Constants.MAVEN_CENTRAL, 8); public static final Tool BINPATCH = Tool.ofForge("binpatcher", "net.minecraftforge:binarypatcher:1.2.2:fatjar", 8); public static final Tool INSTALLERTOOLS = Tool.ofForge("installertools", "net.minecraftforge:installertools:1.4.4:fatjar", 8); + public static final Tool INSTALLER = Tool.ofForge("installer", "net.minecraftforge:installer:2.2.9:fatjar", 8); public static final Tool JARCOMPATIBILITYCHECKER = Tool.ofForge("jarcompatibilitychecker", "net.minecraftforge:JarCompatibilityChecker:0.1.28:all", 8); public static final Tool RENAMER = Tool.ofForge("renamer", "net.minecraftforge:ForgeAutoRenamingTool:1.1.1:all", 8); public static final Tool SRG2SRC = Tool.ofForge("srg2source", "net.minecraftforge:Srg2Source:8.1.1:fatjar", 17); diff --git a/src/main/groovy/net/minecraftforge/forgedev/Util.java b/src/main/groovy/net/minecraftforge/forgedev/Util.java index 76545aa..41d4d50 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/Util.java +++ b/src/main/groovy/net/minecraftforge/forgedev/Util.java @@ -5,26 +5,7 @@ package net.minecraftforge.forgedev; import net.minecraftforge.gradleutils.shared.SharedUtil; -import org.gradle.TaskExecutionRequest; -import org.gradle.api.Action; -import org.gradle.api.Project; -import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.provider.Provider; import org.gradle.api.specs.Spec; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.jvm.toolchain.JavaLanguageVersion; -import org.gradle.jvm.toolchain.JavaLauncher; -import org.gradle.jvm.toolchain.JavaToolchainService; -import org.gradle.jvm.toolchain.JavaToolchainSpec; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.Callable; public final class Util extends SharedUtil { private Util() { } diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/CheckForgeJarCompatibility.java b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/CheckForgeJarCompatibility.java index f84f3a9..1c14358 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/CheckForgeJarCompatibility.java +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/CheckForgeJarCompatibility.java @@ -104,6 +104,17 @@ public static TaskProvider register(Project project, Stri task.getInputJar().set(reobfJar.flatMap(LegacyReobfuscateJar::getOutput)); }); checkJarCompatibility.configure(action); + + var providers = project.getProviders(); + var hasMaven = providers.environmentVariable("MAVEN_USER").isPresent() && providers.environmentVariable("MAVEN_PASSWORD").isPresent(); + var checkCompatibility = providers.gradleProperty("net.minecraftforge.forge.build.check.compatibility").map(Boolean::parseBoolean).getOrElse(false); + + if (!hasMaven && checkCompatibility) { + project.getTasks().named("check", task -> { + task.dependsOn(checkJarCompatibility); + }); + } + return checkJarCompatibility; } diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/DownloadDependency.groovy b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/DownloadDependency.groovy index 5ab6527..a841f05 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/DownloadDependency.groovy +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/DownloadDependency.groovy @@ -23,6 +23,12 @@ import javax.inject.Inject @CompileStatic abstract class DownloadDependency extends DefaultTask { static TaskProvider register(Project project, String name, Object dependency) { + return project.tasks.register(name, DownloadDependency) { + it.artifact = dependency; + } + } + + public void setArtifact(Object dependency) { final def unpacked if (dependency instanceof ProviderConvertible) unpacked = dependency.asProvider().get() @@ -42,15 +48,13 @@ abstract class DownloadDependency extends DefaultTask { } ) - project.tasks.register(name, DownloadDependency) { - it.output.fileProvider(it.providers.provider { - try { - configuration.singleFile - } catch (IllegalStateException e) { - throw new IllegalArgumentException('Downloaded dependency variant is not a single file', e) - } - }) - } + output.fileProvider(providers.provider { + try { + configuration.singleFile + } catch (IllegalStateException e) { + throw new IllegalArgumentException('Downloaded dependency variant is not a single file', e) + } + }) } abstract @OutputFile RegularFileProperty getOutput() diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy index d6df54b..68eccba 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy @@ -22,6 +22,7 @@ abstract class InstallerJar extends Zip { @Input @Optional abstract Property getFat() @Input @Optional abstract Property getOffline() + protected abstract @Inject ProjectLayout getLayout() protected abstract @Inject ProviderFactory getProviders() diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/Util.groovy b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/Util.groovy index 7cc6df0..d7962fc 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/Util.groovy +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/Util.groovy @@ -12,10 +12,13 @@ import groovy.transform.stc.ClosureParams import groovy.transform.stc.SimpleType import net.minecraftforge.forgedev.legacy.values.LibraryInfo import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact +import net.minecraftforge.gradleutils.shared.SharedUtil import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.provider.HasConfigurableValue import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty @@ -181,11 +184,11 @@ final class Util { } @CompileDynamic - private static Provider> artifactTree(Project project, String artifact, boolean transitive = true) { + static Provider> artifactTree(Project project, String artifact, boolean transitive = true) { return MinimalResolvedArtifact.from(project, project.configurations.detachedConfiguration( project.dependencies.create(artifact) ).tap { it.transitive = transitive }).map { list -> - var map = new HashMap(list.size()) + var map = new LinkedHashMap(list.size()) for (var minimal in list) { map.put(minimal.info().key(), minimal) } @@ -239,4 +242,32 @@ final class Util { } } } + + @CompileDynamic + static String asArtifactString(Object artifact) { + def value = SharedUtil.unpack(artifact) + if (!(value instanceof Dependency)) + throw new IllegalArgumentException("Cannot get non-dependency as artifact string! Found: $value.class") + + def classifier = value.hasProperty('classifier') ? ":$value.classifier" : '' + def extension = value.hasProperty('artifactType') ? "@$value.artifactType" : value.hasProperty('extension') ? "@$value.extension" : '' + classifier = classifier != ':null' ? classifier : '' + extension = extension != '@null' ? extension : '' + + "$value.group:$value.name:$value.version$classifier$extension".toString() + } + + static R finalize(Project project, R ret) { + ret.disallowChanges(); + ret.finalizeValueOnRead(); + return ret; + } + + static String capitalize(String s) { + return s.capitalize(); + } + + static String kebab(String s) { + s.replaceAll('([A-Z])', '-$1').toLowerCase() + } } diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/LibraryInfo.java b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/LibraryInfo.java index d47012e..c01c7b7 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/LibraryInfo.java +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/LibraryInfo.java @@ -5,22 +5,39 @@ package net.minecraftforge.forgedev.legacy.values; import net.minecraftforge.forgedev.legacy.tasks.Util; +import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.Transformer; import org.gradle.api.artifacts.Configuration; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.plugins.ide.eclipse.model.Library; import java.io.File; import java.io.Serializable; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; public record LibraryInfo(String name, Downloads downloads) implements Serializable { public record Downloads(ArtifactInfo artifact) implements Serializable { - public record ArtifactInfo(String path, String url, String sha1, long size) implements Serializable { + public static class ArtifactInfo implements Serializable { + public String path; + public String url; + public String sha1; + public long size; + + public ArtifactInfo(String path, String url, String sha1, long size) { + this.path = path; + this.url = url; + this.sha1 = sha1; + this.size = size; + } + public ArtifactInfo validateUrl(boolean offline) { if (offline || !url.startsWith("https://libraries.minecraft.net/")) return this; @@ -57,41 +74,48 @@ public static Map from(Collection return from(dependencies, Boolean.valueOf(validateUrl)); } + public static LibraryInfo from(MinimalResolvedArtifact dependency) { + var info = dependency.info(); + var url = "https://libraries.minecraft.net/" + info.path(); + if (!Util.checkExists(url)) + url = "https://maven.minecraftforge.net/" + info.path(); + + var file = dependency.file(); + var sha1 = Util.sha1(dependency.file()); + + return new LibraryInfo( + info.name(), + info.path(), + url, + sha1, + file.length() + ); + } + private static Map from(Collection dependencies, Boolean offline) { - var ret = new HashMap(dependencies.size()); + var ret = new LinkedHashMap(dependencies.size()); var semaphore = new Semaphore(1, true); dependencies.parallelStream().forEachOrdered(dependency -> { - var info = dependency.info(); - var url = "https://libraries.minecraft.net/" + info.path(); - if (!Util.checkExists(url)) - url = "https://maven.minecraftforge.net/" + info.path(); - - var file = dependency.file(); - var sha1 = Util.sha1(dependency.file()); + var library = from(dependency); + if (offline != null) + library = library.validateUrl(offline); try { semaphore.acquire(); + ret.put(dependency.info().key(), library); + semaphore.release(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted while trying to get library info for " + dependency.info(), e); } - - var library = new LibraryInfo( - info.name(), - info.path(), - url, - sha1, - file.length() - ); - if (offline != null) - library = library.validateUrl(offline); - ret.put(info.key(), library); - - semaphore.release(); }); return ret; } + public static Provider from(Project project, TaskProvider task) { + return MinimalResolvedArtifact.from(project, task).map(LibraryInfo::from); + } + @SafeVarargs public static Provider> from(Project project, TaskProvider... tasks) { var dependencies = project.getObjects().listProperty(MinimalResolvedArtifact.class); @@ -100,17 +124,21 @@ public static Provider> from(Project project, TaskProvi } var ret = project.getObjects().mapProperty(String.class, LibraryInfo.class).value(dependencies.map(LibraryInfo::from)); - - ret.disallowChanges(); - ret.finalizeValueOnRead(); - if (project.getState().getExecuted()) { - ret.finalizeValue(); - } - - return ret; + return Util.finalize(project, ret); } public static Provider> from(Project project, Configuration configuration) { return MinimalResolvedArtifact.from(project, configuration).map(LibraryInfo::from); } + + public static List toList(List list) { + return list.stream().map(LibraryInfo::from).toList(); + } + + public static Transformer apply(Action action) { + return info -> { + action.execute(info); + return info; + }; + } } diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MavenInfo.java b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MavenInfo.java index 94863f2..53a2374 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MavenInfo.java +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MavenInfo.java @@ -27,6 +27,32 @@ public int compareTo(MavenInfo that) { return this.key.compareTo(that.key); } + public static MavenInfo from(String gav) { + var parts = gav.split(":"); + var group = parts[0]; + var name = parts[1]; + var version = parts[2]; + + String classifier = null; + String extension = null; + + if (parts.length > 3) { + classifier = parts[3]; + var idx = classifier.indexOf('@'); + if (idx != -1) { + classifier = classifier.substring(0, idx); + extension = classifier.substring(idx + 1); + } + } else { + var idx = version.indexOf('@'); + if (idx != -1) { + version = version.substring(0, idx); + extension = version.substring(idx + 1); + } + } + return from(group, name, version, classifier, extension); + } + public static MavenInfo from(String artGroup, String artName, String artVersion, @Nullable String artClassifier, @Nullable String artExtension) { if (artExtension == null) artExtension = "jar"; diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java index 5f1d8d0..fe4830e 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java @@ -4,11 +4,13 @@ */ package net.minecraftforge.forgedev.legacy.values; +import net.minecraftforge.forgedev.legacy.tasks.Util; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFile; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; @@ -19,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; public record MinimalResolvedArtifact(MavenInfo info, File file) implements Serializable { private static Provider from(Project project, ProjectDependency projectDependency, FileCollection files) { @@ -26,28 +29,17 @@ private static Provider from(Project project, ProjectDe var ret = project.getObjects().property(MinimalResolvedArtifact.class).value(project.provider(files::getSingleFile).map(file -> new MinimalResolvedArtifact(info, file) )); - - ret.disallowChanges(); - ret.finalizeValueOnRead(); - if (project.getState().getExecuted()) { - ret.finalizeValue(); - } - - return ret; + return Util.finalize(project, ret); } public static Provider from(Project project, TaskProvider task) { - var ret = project.getObjects().property(MinimalResolvedArtifact.class).value(project.getProviders().zip(MavenInfo.from(project, task), task.flatMap(AbstractArchiveTask::getArchiveFile), (info, regularFile) -> - new MinimalResolvedArtifact(info, regularFile.getAsFile()) - )); - - ret.disallowChanges(); - ret.finalizeValueOnRead(); - if (project.getState().getExecuted()) { - ret.finalizeValue(); - } - - return ret; + var ret = project.getObjects().property(MinimalResolvedArtifact.class).value( + MavenInfo.from(project, task).zip( + task.flatMap(AbstractArchiveTask::getArchiveFile).map(RegularFile::getAsFile), + MinimalResolvedArtifact::new + ) + ); + return Util.finalize(project, ret); } public static MinimalResolvedArtifact from(Project project, ResolvedArtifactResult artifact) { @@ -61,7 +53,7 @@ public static Provider> from(Project project, Conf var configurations = project.getConfigurations(); // Find any artifacts from the 'installer' config - // This config specifies the runtime files we intend for the interaller to have. + // This config specifies the runtime files we intend for the installer to have. // And are typically what we would be developing and testing alongside Forge. // So we may have local modified versions for (var dependency : configuration.getDependencies()) { @@ -69,23 +61,39 @@ public static Provider> from(Project project, Conf from(project, projectDependency, ret); } else { var c = configurations.detachedConfiguration(dependency); - ret.addAll(c.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> { - var artifacts = new ArrayList(results.size()); - for (var artifact : results) { - artifacts.add(MinimalResolvedArtifact.from(null, artifact)); - } - return artifacts; - })); + ret.addAll(c.getIncoming().getArtifacts().getResolvedArtifacts().map(MinimalResolvedArtifact::transform)); } } - ret.disallowChanges(); - ret.finalizeValueOnRead(); - if (project.getState().getExecuted()) { - ret.finalizeValue(); + return Util.finalize(project, ret); + } + + private static List transform(Set results) { + var artifacts = new ArrayList(results.size()); + for (var artifact : results) { + artifacts.add(MinimalResolvedArtifact.from(null, artifact)); } + return artifacts; + } + + public static Provider single(Project project, String artifact) { + return from(project, artifact, false).map(l -> l.get(0)); + } + public static Provider from(MavenInfo info, Provider file) { + return file.map(f -> new MinimalResolvedArtifact(info, f.getAsFile())); + } + public static Provider> from(Project project, String artifact) { + return from(project, artifact, true); + } + public static Provider> from(Project project, String artifact, boolean transitive) { + var ret = project.getObjects().listProperty(MinimalResolvedArtifact.class); - return ret; + var c = project.getConfigurations().detachedConfiguration( + project.getDependencies().create(artifact) + ); + c.setTransitive(transitive); + ret.set(c.getIncoming().getArtifacts().getResolvedArtifacts().map(MinimalResolvedArtifact::transform)); + return Util.finalize(project, ret); } private static void from(Project project, ProjectDependency projectDependency, ListProperty ret) { diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/SingleFileOutput.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/SingleFileOutput.java new file mode 100644 index 0000000..533fe94 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/SingleFileOutput.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks; + +import org.gradle.api.file.RegularFileProperty; + +public interface SingleFileOutput { + RegularFileProperty getOutput(); +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java new file mode 100644 index 0000000..cb4d3a8 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import net.minecraftforge.forgedev.ForgeDevExtension; +import net.minecraftforge.forgedev.Tools; +import net.minecraftforge.forgedev.legacy.tasks.DownloadDependency; +import net.minecraftforge.forgedev.legacy.tasks.Util; +import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MavenInfo; +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import net.minecraftforge.forgedev.tasks.SingleFileOutput; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; +import java.util.List; + +public abstract class Installer { + static final List JSON_COMMENT = List.of( + "Please do not automate the download and installation of Forge.", + "Our efforts are supported by ads from the download page.", + "If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/" + ); + private static final Action DO_NOTHING = input -> {}; + @SuppressWarnings("unchecked") + private static Action noop() { + return (Action)DO_NOTHING; + } + + private final Project project; + private final String name; + // Public facing tasks + private final TaskProvider jar; + private final TaskProvider json; + private final TaskProvider launcherJson; + + // Implemntation details + private final TaskProvider base; + + @Input + public abstract Property getCi(); + @Input + public abstract Property getOffline(); + + @Inject + public Installer(Project project, String name, TaskProvider jar, TaskProvider json, TaskProvider launcherJson, TaskProvider base) { + this.project = project; + this.name = name; + this.jar = jar; + this.json = json; + this.launcherJson = launcherJson; + this.base = base; + this.getCi().convention(false); + this.getOffline().convention(false); + } + public String getName() { return this.name; } + + public TaskProvider getJar() { + return this.jar; + } + public void jar(Action action) { + getJar().configure(action); + } + + public TaskProvider<@NotNull InstallerJson> getJson() { + return this.json; + } + public void json(Action action) { + getJson().configure(action); + } + + public TaskProvider getLauncherJson() { + return this.launcherJson; + } + public void launcherJson(Action action) { + getLauncherJson().configure(action); + } + + public Tool tool(Object dependency) { + return tool(dependency, true); + } + public Tool tool(Object dependency, boolean transitive) { + var gav = Util.asArtifactString(dependency); + var tree = MinimalResolvedArtifact.from(project, gav, transitive).get(); + this.jar.configure(task -> { + for (var artifact : tree) + task.library(project.provider(() -> artifact), noop()); + }); + return new Tool(gav, tree); + } + + public void pack(TaskProvider task) { + pack(task, noop()); + } + public void pack(TaskProvider task, Action action) { + pack(MinimalResolvedArtifact.from(project, task), action); + } + public void pack(Provider info) { + pack(info, noop()); + } + public void pack(Provider info, Action action) { + this.getJson().configure(task -> task.library(info, action)); + this.getJar().configure(task -> task.pack(info)); + } + + public void library(String artifact) { + library(artifact, noop()); + } + public void library(String artifact, Action action) { + library(MinimalResolvedArtifact.single(project, artifact), action); + } + public void library(TaskProvider task) { + library(task, noop()); + } + public void library(TaskProvider task, Action action) { + library(MinimalResolvedArtifact.from(project, task), action); + } + public void library(Provider info) { + library(info, noop()); + } + public void library(Provider info, Action action) { + this.getJson().configure(task -> task.library(info, action)); + this.getJar().configure(task -> task.library(info, action)); + } + + public void launcherLibraries(Configuration configuration) { + this.getLauncherJson().configure(task -> task.libraries(configuration)); + this.getJar().configure(task -> task.libraries(MinimalResolvedArtifact.from(project, configuration))); + } + public void launcherLibrary(String artifact) { + launcherLibrary(artifact, noop()); + } + public void launcherLibrary(String artifact, Action action) { + launcherLibrary(MinimalResolvedArtifact.single(project, artifact), action); + } + public void launcherLibrary(TaskProvider task) { + launcherLibrary(task, noop()); + } + public void launcherLibrary(TaskProvider task, Action action) { + launcherLibrary(MinimalResolvedArtifact.from(project, task), action); + } + public void launcherLibrary(TaskProvider task, String classifier) { + launcherLibrary(task, classifier, noop()); + } + public void launcherLibrary(TaskProvider task, String classifier, Action action) { + launcherLibrary(MinimalResolvedArtifact.from(MavenInfo.from(project, classifier), task.flatMap(SingleFileOutput::getOutput)), action); + } + public void launcherLibrary(Provider info) { + launcherLibrary(info, noop()); + } + public void launcherLibrary(Provider info, Action action) { + this.getLauncherJson().configure(task -> task.library(info, action)); + this.getJar().configure(task -> task.library(info, action)); + } + + + /// Helper to set the base installer to Forge's this is meant to make it easy to do something like: + /// installer { + /// baseVersion = "1.0" + /// } + public void setBaseVersion(String version) { + this.setBase("net.minecraftforge:installer:" + version + ":fatjar"); + } + public void setBase(String artifact) { + base.configure(task -> task.setArtifact(artifact) ); + } + + @ApiStatus.Internal + public static final String DEFAULT_NAME = "installer"; + + @ApiStatus.Internal + public static Installer register(Project project, ForgeDevExtension ext, String name) { + var tasks = project.getTasks(); + + // This is annoying, but this allows me to pass in a reference to Installer to the tasks so they can see each other + class Holder { + Installer value; + } + var holder = new Holder(); + var self = project.getProviders().provider(() -> holder.value); + + var base = DownloadDependency.register(project, name + "DownloadBase", Tools.INSTALLER.getModule().toString()); + var jar = tasks.register(name + "Jar", InstallerJar.class, self); + var json = tasks.register(name + "Json", InstallerJson.class); + var launcherJson = tasks.register(name + "LauncherJson", LauncherJson.class); + + var baseDir = project.getLayout().getBuildDirectory().dir(name).get(); + var ret = project.getObjects().newInstance(Installer.class, project, name, jar, json, launcherJson, base); + holder.value = ret; + ret.getCi().convention(ext.isCi()); + + jar.configure(task -> { + task.getArchiveClassifier().set(Util.kebab(name)); + + task.from( + json, + launcherJson + ); + + task.from(project.zipTree(base.map(DownloadDependency::getOutput)), cfg -> { + cfg.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + }); + }); + + json.configure(task -> { + task.getOutput().set(baseDir.file("installer_profile.json")); + }); + + launcherJson.configure(task -> { + task.getOutput().set(baseDir.file("version.json")); + }); + + return ret; + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java new file mode 100644 index 0000000..08d89c9 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import net.minecraftforge.util.download.DownloadUtils; +import org.gradle.api.Action; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.bundling.Zip; +import org.jetbrains.annotations.ApiStatus; + +import javax.inject.Inject; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; + +public abstract class InstallerJar extends Zip { + private final Provider installer; + + @Inject + public InstallerJar(Provider installer) { + this.installer = installer; + // We have to `set` here because the default plugin forces the conventions after the task is created + // But since configuration is done in order, callers can override in their actions + this.getArchiveClassifier().set("installer"); + this.getArchiveExtension().set("jar"); // We use Zip task so it doesn't overwrite the Manifest + // Technically this should pull from BasePluginExtension, but there is a to-do in gradle to break that so I don't care. + // https://github.com/gradle/gradle/blob/fb1720293a5c90a642f636843e6793555761e64e/platforms/jvm/plugins-java-base/src/main/java/org/gradle/api/plugins/JavaBasePlugin.java#L354 + this.getDestinationDirectory().set(getProject().getLayout().getBuildDirectory().dir("libs")); + } + + @ApiStatus.Internal + public void pack(Provider info) { + this.from(info.map(MinimalResolvedArtifact::file), spec -> { + spec.rename( name -> { + var path = info.get().info().path(); + getProject().getLogger().lifecycle("Adding: " + path); + return "maven/" + path; + }); + spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + }); + } + + @ApiStatus.Internal + public void libraries(Provider> libraries) { + // TODO: [ForgeDev][LazyConfig] See if we can trick CopySpec into allowing an empty list + for (var artifact : libraries.get()) { + library(this.getProject().provider(() -> artifact), lib -> {}); + } + } + + @ApiStatus.Internal + public void library(Provider provider, Action action) { + // TODO: [ForgeDev][LazyConfig] See if we can trick CopySpec into allowing an empty list + var info = provider.map(LibraryInfo::from).map(LibraryInfo.apply(action)).get(); + var artifact = info.downloads().artifact(); + var offline = installer.get().getOffline().get(); + + // If we are not making an offline installer, and we're on the CI don't check remote, assume we're gunna publish everything + if (!offline && installer.get().getCi().get()) { + getProject().getLogger().lifecycle("Skipping: " + artifact.path); + return; + } + + // If it's an offline jar, always pack + var pack = offline || artifact.url.isEmpty(); + + // If it's not, Check if the remote + if (!pack) { + try { + // See if the remote hash is the same as ours + var remote = DownloadUtils.downloadString(artifact.url + ".sha1"); + pack = !artifact.sha1.equals(remote); + } catch (FileNotFoundException e) { + // The file doesn't exist, Mojang's maven doesn't include them, so assume it exists if it's on there. + pack = !artifact.url.startsWith("https://libraries.minecraft.net/"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + if (pack) + pack(provider); + else + getProject().getLogger().lifecycle("Skipping: " + artifact.path); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java new file mode 100644 index 0000000..b6c3953 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import groovy.json.JsonBuilder; +import net.minecraftforge.forgedev.legacy.tasks.Util; +import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import net.minecraftforge.forgedev.tasks.installer.steps.Extract; +import net.minecraftforge.forgedev.tasks.installer.steps.ExtractBundle; +import net.minecraftforge.forgedev.tasks.installer.steps.Step; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.*; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public abstract class InstallerJson extends DefaultTask { + protected abstract @Inject ProviderFactory getProviders(); + protected abstract @Inject ObjectFactory getObjects(); + + @OutputFile abstract RegularFileProperty getOutput(); + + @InputFiles abstract ConfigurableFileCollection getInput(); + @InputFile @Optional abstract RegularFileProperty getIcon(); + @Input abstract Property getLauncherJsonName(); + @Input abstract Property getLogo(); + @Input abstract Property getMirrors(); + @Input abstract Property getWelcome(); + @Input abstract Property getProfileName(); + @Input abstract Property getProfileVersion(); + @Input abstract Property getExecutablePath(); + @Input abstract Property getMinecraft(); + @Input abstract Property getMinecraftServerPath(); + + @Input abstract Property getHideExtract(); + @Input abstract Property getHideClient(); + @Input abstract Property getHideServer(); + + @Input @Optional abstract MapProperty getData(); + @Input @Optional abstract ListProperty getSteps(); + + private final Set> extraLibraries = new LinkedHashSet<>(); + + @Inject + public InstallerJson() { + getOutput().convention(getProject().getLayout().getBuildDirectory().file("libs/install_profile.json")); + + getLauncherJsonName().convention("/version.json"); + getLogo().convention("/big_logo.png"); + getMirrors().convention("https://files.minecraftforge.net/mirrors-2.0.json"); + getWelcome().convention("Welcome to the " + Util.capitalize(getProject().getName()) + " installer."); + getMinecraftServerPath().convention("{LIBRARY_DIR}/net/minecraft/server/{MINECRAFT_VERSION}/server-{MINECRAFT_VERSION}-bundled.jar"); + getProfileName().convention(getProject().getName()); + + getHideExtract().convention(true); + getHideClient().convention(false); + getHideServer().convention(false); + } + + public void executable(TaskProvider task) { + var info = MinimalResolvedArtifact.from(this.getProject(), task); + this.getExecutablePath().set(info.map(i -> i.info().name())); + } + + public void library(Provider info, Action action) { + this.getInput().from(info.map(MinimalResolvedArtifact::file)); + this.extraLibraries.add(info.map(LibraryInfo::from).map(LibraryInfo.apply(action))); + } + + private R step(Class cls, Tool tool, Action action) { + var ret = this.getObjects().newInstance(cls, tool); + this.getInput().from(tool.getArtifacts().stream().map(MinimalResolvedArtifact::file).toList()); + action.execute(ret); + return ret; + } + + public Step step(Tool tool, Action action) { + return step(Step.class, tool, action); + } + public Extract extract(Tool tool, Action action) { + return step(Extract.class, tool, action); + } + public ExtractBundle extractBundle(Tool tool, Action action) { + return step(ExtractBundle.class, tool, action); + } + + public record Data(String client, String server) implements Serializable {} + + public void data(String key, String client, String server) { + this.getData().put(key, new Data(client, server)); + } + + public void data(String key, RegularFileProperty client, RegularFileProperty server) { + this.getData().put(key, client.zip(server, (l, r) -> new Data( + "'" + Util.sha1(client.get().getAsFile()) + "'", + "'" + Util.sha1(server.get().getAsFile()) + "'" + ))); + } + + @TaskAction + protected void exec() throws IOException { + var libraries = new ArrayList(); + var json = new LinkedHashMap(); + json.put("_comment", Installer.JSON_COMMENT); + json.put("spec", 1); + if (getHideExtract().getOrElse(false)) + json.put("hideExtract", true); + if (getHideClient().getOrElse(false)) + json.put("hideClient", true); + if (getHideServer().getOrElse(false)) + json.put("hideServer", true); + json.put("profile", getProfileName().get()); + json.put("version", getProfileVersion().get()); + json.put("path", getExecutablePath().get()); + json.put("minecraft", getMinecraft().get()); + json.put("serverJarPath", getMinecraftServerPath().get()); + + if (getData().isPresent() && !getData().get().isEmpty()) + json.put("data", getData().get()); + + if (getSteps().isPresent() && !getSteps().get().isEmpty()) { + var processors = new ArrayList>(); + json.put("processors", processors); + for (var step : getSteps().get()) { + var data = new LinkedHashMap(); + if (!step.sides.isEmpty()) + data.put("sides", step.sides); + + var tool = step.getTool(); + data.put("jar", tool.getJar()); + if (!tool.getArtifacts().isEmpty()) { + var classpath = new ArrayList(); + for (var info : LibraryInfo.from(tool.getArtifacts()).values()) { + libraries.add(info); + if (!tool.getJar().equals(info.name())) + classpath.add(info.name()); + } + data.put("classpath", classpath); + } + data.put("args", step.getArgs()); + if (!step.getOutputs().isEmpty()) + data.put("outputs", step.getOutputs()); + processors.add(data); + } + } + + for (var library : this.extraLibraries) { + var info = library.get(); + libraries.add(info); + } + + var seen = new HashSet(); + libraries.sort(Comparator.comparing(LibraryInfo::name)); + libraries.removeIf(l -> !seen.add(l.name())); + json.put("libraries", libraries); + + var icon = getIcon().getOrNull(); + if (icon != null) { + var data = Files.readAllBytes(icon.getAsFile().toPath()); + var base64 = Base64.getEncoder().encodeToString(data); + json.put("icon", "data:image/png;base64," + base64); + } + json.put("json", getLauncherJsonName().get()); + json.put("logo", getLogo().get()); + if (!getMirrors().get().isEmpty()) + json.put("mirrorList", getMirrors().get()); + json.put("welcome", getWelcome().get()); + + var output = getOutput().get().getAsFile().toPath(); + var jsonData = new JsonBuilder(json).toPrettyString(); + Files.writeString(output, jsonData); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java new file mode 100644 index 0000000..8301403 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import groovy.json.JsonBuilder; +import net.minecraftforge.forgedev.legacy.tasks.Util; +import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.jetbrains.annotations.ApiStatus; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public abstract class LauncherJson extends DefaultTask { + protected abstract @Inject ProviderFactory getProviders(); + protected abstract @Inject ObjectFactory getObjects(); + + @OutputFile abstract RegularFileProperty getOutput(); + + @InputFiles abstract ConfigurableFileCollection getInput(); + @Input abstract Property getTimestamp(); + @Input abstract Property getReleaseTime(); + @Input abstract Property getId(); + @Input @Optional abstract Property getInheritsFrom(); + @Input abstract Property getType(); + @Input @Optional abstract Property getMainClass(); + @Input @Optional abstract ListProperty getGameArgs(); + @Input @Optional abstract ListProperty getJvmArgs(); + @Input abstract ListProperty getLibraries(); + + @Inject + public LauncherJson() { + getOutput().convention(getProject().getLayout().getBuildDirectory().file("libs/version.json")); + + // TODO: [ForgeDev][Reproduceable] Add helper to get timestamp from latest git commit + var timestamp = Util.iso8601Now(); + getTimestamp().convention(timestamp); + getReleaseTime().convention(timestamp); + getType().convention("release"); + getMainClass().convention("main"); + + } + + @ApiStatus.Internal + public void libraries(Configuration config) { + this.getInput().from(config); + this.getLibraries().addAll(MinimalResolvedArtifact.from(getProject(), config).map(LibraryInfo::toList)); + } + @ApiStatus.Internal + public void library(Provider info, Action action) { + this.getInput().from(info.map(MinimalResolvedArtifact::file)); + this.getLibraries().add(info.map(LibraryInfo::from).map(LibraryInfo.apply(action))); + } + + @TaskAction + protected void exec() throws IOException { + var json = new LinkedHashMap(); + json.put("_comment", Installer.JSON_COMMENT); + json.put("id", getId().get()); + json.put("time", getTimestamp().get()); + json.put("releaseTime", getReleaseTime().get()); + if (getInheritsFrom().isPresent()) + json.put("inheritsFrom", getInheritsFrom().get()); + json.put("type", getType().get()); + json.put("logging", Map.of()); // Hardcoded for now, if we ever wanna move to remotely hosted log configs we could + json.put("mainClass", getMainClass().orElse("")); + + // I could model out this and add support for rules and shit.. but I dont want to + var args = new LinkedHashMap>(); + if (getGameArgs().isPresent()) + args.put("game", getGameArgs().get()); + if (getJvmArgs().isPresent()) + args.put("jvm", getJvmArgs().get()); + if (!args.isEmpty()) + json.put("arguments", args); + + var libraries = new ArrayList<>(this.getLibraries().get()); + var seen = new HashSet(); + //libraries.sort(Comparator.comparing(LibraryInfo::name)); + libraries.removeIf(l -> !seen.add(l.name())); + json.put("libraries", libraries); + + getLogger().lifecycle("Launcher Json " + json.toString()); + var output = getOutput().get().getAsFile().toPath(); + getLogger().lifecycle("Launcher File " + output.toString()); + var jsonData = new JsonBuilder(json).toPrettyString(); + getLogger().lifecycle("Launcher Written"); + Files.writeString(output, jsonData); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Tool.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Tool.java new file mode 100644 index 0000000..a9c5520 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Tool.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import org.jetbrains.annotations.ApiStatus; + +import java.io.Serializable; +import java.util.List; + +public class Tool implements Serializable { + private final String jar; + private final List classpath; + + public Tool(String jar, List classpath) { + this.jar = jar; + this.classpath = classpath; + } + + public String getJar() { + return this.jar; + } + + @ApiStatus.Internal + List getArtifacts() { + return this.classpath; + } + + public List getClasspath() { + return classpath.stream() + .map(art -> art.info().name()) + .filter(name -> !name.equals(this.jar)) + .toList(); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java new file mode 100644 index 0000000..cc1edfb --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java @@ -0,0 +1,45 @@ +package net.minecraftforge.forgedev.tasks.installer.steps; + +import net.minecraftforge.forgedev.tasks.installer.Tool; + +import javax.inject.Inject; +import java.io.Serial; +import java.util.List; + +public class Extract extends Step { + @Serial + private static final long serialVersionUID = 1L; + + @Inject + public Extract(Tool tool) { + super(tool); + args.addAll(List.of( + "--task", "EXTRACT_FILES" + )); + } + + public void archive(String archive) { + args.add("--archive"); + args.add(archive); + } + + public void extract(String from, String to) { + args.addAll(List.of( + "--from", from, + "--to", to + )); + } + + public void optional(String from, String to) { + args.addAll(List.of( + "--from", from, + "--to", to, + "--optional", to + )); + } + + public void executable(String executable) { + args.add("--exec"); + args.add(executable); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java new file mode 100644 index 0000000..f726e56 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java @@ -0,0 +1,41 @@ +package net.minecraftforge.forgedev.tasks.installer.steps; + +import net.minecraftforge.forgedev.tasks.installer.Tool; + +import javax.inject.Inject; +import java.io.Serial; +import java.util.List; + +public class ExtractBundle extends Step { + @Serial + private static final long serialVersionUID = 1L; + + @Inject + public ExtractBundle(Tool tool) { + super(tool); + args.addAll(List.of( + "--task", "BUNDLER_EXTRACT" + )); + } + + public void extract(String from, String to) { + args.addAll(List.of( + "--input", from, + "--output", to + )); + } + + public void libraries() { + libraries("{MINECRAFT_JAR}", "{ROOT}/libraries/"); + } + + public void libraries(String from, String to) { + extract("{MINECRAFT_JAR}", "{ROOT}/libraries/"); + args.add("--libraries"); + } + + public void jar(String from, String to) { + extract(from, to); + args.add("--jar-only"); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Step.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Step.java new file mode 100644 index 0000000..aa56007 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Step.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer.steps; + +import net.minecraftforge.forgedev.tasks.installer.Tool; + +import javax.inject.Inject; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Step implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private final Tool tool; + public List sides = List.of(); + + protected List args = new ArrayList<>(); + protected Map outputs = new LinkedHashMap<>(); + + @Inject + public Step(Tool tool) { + this.tool = tool; + } + + public Tool getTool() { + return this.tool; + } + public List getSides() { + return sides; + } + public List getArgs() { + return args; + } + public Map getOutputs() { + return outputs; + } + + public void setSides(List sides) { + this.sides = sides; + } + + public void setSide(String side) { + this.sides = List.of(side); + } + + public void cache(String key, String value) { + outputs.put(key, value); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/patching/binary/BinaryPatcherExec.groovy b/src/main/groovy/net/minecraftforge/forgedev/tasks/patching/binary/BinaryPatcherExec.groovy index 5075f50..f18662b 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/patching/binary/BinaryPatcherExec.groovy +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/patching/binary/BinaryPatcherExec.groovy @@ -8,6 +8,7 @@ import groovy.transform.CompileStatic import groovy.transform.PackageScope import net.minecraftforge.forgedev.Tools import net.minecraftforge.forgedev.Util +import net.minecraftforge.forgedev.tasks.SingleFileOutput import net.minecraftforge.forgedev.tasks.ToolExec import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFileProperty @@ -23,9 +24,10 @@ import org.gradle.api.tasks.OutputFile import javax.inject.Inject @CompileStatic -@PackageScope abstract class BinaryPatcherExec extends ToolExec { +@PackageScope abstract class BinaryPatcherExec extends ToolExec implements SingleFileOutput { // Shared abstract @InputFiles ConfigurableFileCollection getClean() + @Override abstract @OutputFile RegularFileProperty getOutput() abstract @Input @Optional ListProperty getPrefix() abstract @Input Property getPack200() From c3fd78c78652f50cb7766b6c5984837c893c59f1 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 18:25:01 -0800 Subject: [PATCH 2/7] Use GSOn isntead of JsonBuilder --- .../forgedev/tasks/installer/InstallerJson.java | 7 +++++-- .../forgedev/tasks/installer/LauncherJson.java | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java index b6c3953..6301822 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java @@ -4,7 +4,8 @@ */ package net.minecraftforge.forgedev.tasks.installer; -import groovy.json.JsonBuilder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import net.minecraftforge.forgedev.legacy.tasks.Util; import net.minecraftforge.forgedev.legacy.values.LibraryInfo; import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; @@ -38,6 +39,8 @@ import java.util.Set; public abstract class InstallerJson extends DefaultTask { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + protected abstract @Inject ProviderFactory getProviders(); protected abstract @Inject ObjectFactory getObjects(); @@ -190,7 +193,7 @@ protected void exec() throws IOException { json.put("welcome", getWelcome().get()); var output = getOutput().get().getAsFile().toPath(); - var jsonData = new JsonBuilder(json).toPrettyString(); + var jsonData = GSON.toJson(json); Files.writeString(output, jsonData); } } diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java index 8301403..ae8dde8 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -4,7 +4,8 @@ */ package net.minecraftforge.forgedev.tasks.installer; -import groovy.json.JsonBuilder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import net.minecraftforge.forgedev.legacy.tasks.Util; import net.minecraftforge.forgedev.legacy.values.LibraryInfo; import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; @@ -35,6 +36,8 @@ import java.util.Map; public abstract class LauncherJson extends DefaultTask { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + protected abstract @Inject ProviderFactory getProviders(); protected abstract @Inject ObjectFactory getObjects(); @@ -106,7 +109,7 @@ protected void exec() throws IOException { getLogger().lifecycle("Launcher Json " + json.toString()); var output = getOutput().get().getAsFile().toPath(); getLogger().lifecycle("Launcher File " + output.toString()); - var jsonData = new JsonBuilder(json).toPrettyString(); + var jsonData = GSON.toJson(json); getLogger().lifecycle("Launcher Written"); Files.writeString(output, jsonData); } From b90806d4e80fddb462af34ab16baa8037fadbcf5 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 19:35:02 -0800 Subject: [PATCH 3/7] Don't re-resolve libraries for MiminalResolvedArtifact.from(Configurations) Fixes sorting issue, also fixes duplicate/transitives being added incorrectly. Finish Launcher Json. --- .../values/MinimalResolvedArtifact.java | 34 ++++------ .../tasks/installer/InstallerJson.java | 5 +- .../tasks/installer/LauncherJson.java | 63 ++++++++++++++----- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java index fe4830e..a7343d5 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/values/MinimalResolvedArtifact.java @@ -6,6 +6,7 @@ import net.minecraftforge.forgedev.legacy.tasks.Util; import org.gradle.api.Project; +import org.gradle.api.Transformer; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.result.ResolvedArtifactResult; @@ -49,31 +50,18 @@ public static MinimalResolvedArtifact from(Project project, ResolvedArtifactResu public static Provider> from(Project project, Configuration configuration) { var ret = project.getObjects().listProperty(MinimalResolvedArtifact.class); - - var configurations = project.getConfigurations(); - - // Find any artifacts from the 'installer' config - // This config specifies the runtime files we intend for the installer to have. - // And are typically what we would be developing and testing alongside Forge. - // So we may have local modified versions - for (var dependency : configuration.getDependencies()) { - if (dependency instanceof ProjectDependency projectDependency) { - from(project, projectDependency, ret); - } else { - var c = configurations.detachedConfiguration(dependency); - ret.addAll(c.getIncoming().getArtifacts().getResolvedArtifacts().map(MinimalResolvedArtifact::transform)); - } - } - + ret.addAll(configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(transform(project))); return Util.finalize(project, ret); } - private static List transform(Set results) { - var artifacts = new ArrayList(results.size()); - for (var artifact : results) { - artifacts.add(MinimalResolvedArtifact.from(null, artifact)); - } - return artifacts; + private static Transformer, Set> transform(Project project) { + return results -> { + var artifacts = new ArrayList(results.size()); + for (var artifact : results) { + artifacts.add(MinimalResolvedArtifact.from(project, artifact)); + } + return artifacts; + }; } public static Provider single(Project project, String artifact) { @@ -92,7 +80,7 @@ public static Provider> from(Project project, Stri project.getDependencies().create(artifact) ); c.setTransitive(transitive); - ret.set(c.getIncoming().getArtifacts().getResolvedArtifacts().map(MinimalResolvedArtifact::transform)); + ret.set(c.getIncoming().getArtifacts().getResolvedArtifacts().map(transform(project))); return Util.finalize(project, ret); } diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java index 6301822..6a6c938 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java @@ -39,7 +39,10 @@ import java.util.Set; public abstract class InstallerJson extends DefaultTask { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Gson GSON = new GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create(); protected abstract @Inject ProviderFactory getProviders(); protected abstract @Inject ObjectFactory getObjects(); diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java index ae8dde8..b0bcae2 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -30,13 +30,17 @@ import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; public abstract class LauncherJson extends DefaultTask { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Gson GSON = new GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create(); protected abstract @Inject ProviderFactory getProviders(); protected abstract @Inject ObjectFactory getObjects(); @@ -53,6 +57,9 @@ public abstract class LauncherJson extends DefaultTask { @Input @Optional abstract ListProperty getGameArgs(); @Input @Optional abstract ListProperty getJvmArgs(); @Input abstract ListProperty getLibraries(); + @Input abstract Property getSortLibraries(); + @Input abstract Property getLibrariesLast(); + @Input abstract Property getDuplicateLibraries(); @Inject public LauncherJson() { @@ -64,7 +71,9 @@ public LauncherJson() { getReleaseTime().convention(timestamp); getType().convention("release"); getMainClass().convention("main"); - + getSortLibraries().convention(true); + getLibrariesLast().convention(true); + getDuplicateLibraries().convention(false); } @ApiStatus.Internal @@ -74,6 +83,7 @@ public void libraries(Configuration config) { } @ApiStatus.Internal public void library(Provider info, Action action) { + getLogger().lifecycle("Library: " + info); this.getInput().from(info.map(MinimalResolvedArtifact::file)); this.getLibraries().add(info.map(LibraryInfo::from).map(LibraryInfo.apply(action))); } @@ -89,8 +99,42 @@ protected void exec() throws IOException { json.put("inheritsFrom", getInheritsFrom().get()); json.put("type", getType().get()); json.put("logging", Map.of()); // Hardcoded for now, if we ever wanna move to remotely hosted log configs we could - json.put("mainClass", getMainClass().orElse("")); + json.put("mainClass", getMainClass().getOrElse("")); + + if (!getLibrariesLast().get()) + addLibraries(json); + + addArgs(json); + + if (getLibrariesLast().get()) + addLibraries(json); + + var output = getOutput().get().getAsFile().toPath(); + var jsonData = GSON.toJson(json); + Files.writeString(output, jsonData); + } + + private void addLibraries(Map json) { + var libraries = new ArrayList<>(this.getLibraries().get()); + + // Older versions didn't de-duplicate, so our 'bootstrap' config got added twice. + if (!getDuplicateLibraries().get()) { + var seen = new HashSet(); + libraries.removeIf(l -> !seen.add(l.name())); + } + // Classpath order doesn't matter in anything that works in the JPMS. + // Anything 1.13+ And Gradle's resolution of dependencies is not the same order as their old api. + // It makes this hard to diff against + // So lets sort by name and call it good + if (getSortLibraries().get()) + libraries.sort(Comparator.comparing(LibraryInfo::name)); + + json.put("libraries", libraries); + } + + private void addArgs(Map json) { + // This is below libraries for legacy diffing, It doesn't matter // I could model out this and add support for rules and shit.. but I dont want to var args = new LinkedHashMap>(); if (getGameArgs().isPresent()) @@ -99,18 +143,5 @@ protected void exec() throws IOException { args.put("jvm", getJvmArgs().get()); if (!args.isEmpty()) json.put("arguments", args); - - var libraries = new ArrayList<>(this.getLibraries().get()); - var seen = new HashSet(); - //libraries.sort(Comparator.comparing(LibraryInfo::name)); - libraries.removeIf(l -> !seen.add(l.name())); - json.put("libraries", libraries); - - getLogger().lifecycle("Launcher Json " + json.toString()); - var output = getOutput().get().getAsFile().toPath(); - getLogger().lifecycle("Launcher File " + output.toString()); - var jsonData = GSON.toJson(json); - getLogger().lifecycle("Launcher Written"); - Files.writeString(output, jsonData); } } From cda2c6809ff237731426f6d1c600dde81aa8bec6 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 20:40:17 -0800 Subject: [PATCH 4/7] Move InstallerJar configuration to a seperate task, its now functional --- .../forgedev/tasks/installer/Installer.java | 24 ++-- .../tasks/installer/InstallerJar.java | 55 +------- .../tasks/installer/InstallerJarConfig.java | 121 ++++++++++++++++++ .../tasks/installer/LauncherJson.java | 1 - 4 files changed, 136 insertions(+), 65 deletions(-) create mode 100644 src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java index cb4d3a8..f4c23b4 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java @@ -43,6 +43,7 @@ private static Action noop() { private final String name; // Public facing tasks private final TaskProvider jar; + private final TaskProvider jarConfig; private final TaskProvider json; private final TaskProvider launcherJson; @@ -50,19 +51,20 @@ private static Action noop() { private final TaskProvider base; @Input - public abstract Property getCi(); + public abstract Property getDev(); @Input public abstract Property getOffline(); @Inject - public Installer(Project project, String name, TaskProvider jar, TaskProvider json, TaskProvider launcherJson, TaskProvider base) { + public Installer(Project project, String name, TaskProvider jar, TaskProvider jarConfig, TaskProvider json, TaskProvider launcherJson, TaskProvider base) { this.project = project; this.name = name; this.jar = jar; + this.jarConfig = jarConfig; this.json = json; this.launcherJson = launcherJson; this.base = base; - this.getCi().convention(false); + this.getDev().convention(false); this.getOffline().convention(false); } public String getName() { return this.name; } @@ -94,7 +96,7 @@ public Tool tool(Object dependency) { public Tool tool(Object dependency, boolean transitive) { var gav = Util.asArtifactString(dependency); var tree = MinimalResolvedArtifact.from(project, gav, transitive).get(); - this.jar.configure(task -> { + this.jarConfig.configure(task -> { for (var artifact : tree) task.library(project.provider(() -> artifact), noop()); }); @@ -132,12 +134,12 @@ public void library(Provider info) { } public void library(Provider info, Action action) { this.getJson().configure(task -> task.library(info, action)); - this.getJar().configure(task -> task.library(info, action)); + this.jarConfig.configure(task -> task.library(info, action)); } public void launcherLibraries(Configuration configuration) { this.getLauncherJson().configure(task -> task.libraries(configuration)); - this.getJar().configure(task -> task.libraries(MinimalResolvedArtifact.from(project, configuration))); + this.jarConfig.configure(task -> task.libraries(configuration)); } public void launcherLibrary(String artifact) { launcherLibrary(artifact, noop()); @@ -162,7 +164,7 @@ public void launcherLibrary(Provider info) { } public void launcherLibrary(Provider info, Action action) { this.getLauncherJson().configure(task -> task.library(info, action)); - this.getJar().configure(task -> task.library(info, action)); + this.jarConfig.configure(task -> task.library(info, action)); } @@ -192,17 +194,19 @@ class Holder { var self = project.getProviders().provider(() -> holder.value); var base = DownloadDependency.register(project, name + "DownloadBase", Tools.INSTALLER.getModule().toString()); - var jar = tasks.register(name + "Jar", InstallerJar.class, self); + var jarConfig = tasks.register(name + "JarConfig", InstallerJarConfig.class, self); + var jar = tasks.register(name + "Jar", InstallerJar.class); var json = tasks.register(name + "Json", InstallerJson.class); var launcherJson = tasks.register(name + "LauncherJson", LauncherJson.class); var baseDir = project.getLayout().getBuildDirectory().dir(name).get(); - var ret = project.getObjects().newInstance(Installer.class, project, name, jar, json, launcherJson, base); + var ret = project.getObjects().newInstance(Installer.class, project, name, jar, jarConfig, json, launcherJson, base); holder.value = ret; - ret.getCi().convention(ext.isCi()); + ret.getDev().convention(!ext.isCi()); jar.configure(task -> { task.getArchiveClassifier().set(Util.kebab(name)); + task.dependsOn(jarConfig); task.from( json, diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java index 08d89c9..4a9aa8b 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java @@ -4,26 +4,17 @@ */ package net.minecraftforge.forgedev.tasks.installer; -import net.minecraftforge.forgedev.legacy.values.LibraryInfo; import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; -import net.minecraftforge.util.download.DownloadUtils; -import org.gradle.api.Action; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.bundling.Zip; import org.jetbrains.annotations.ApiStatus; import javax.inject.Inject; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; public abstract class InstallerJar extends Zip { - private final Provider installer; - @Inject - public InstallerJar(Provider installer) { - this.installer = installer; + public InstallerJar() { // We have to `set` here because the default plugin forces the conventions after the task is created // But since configuration is done in order, callers can override in their actions this.getArchiveClassifier().set("installer"); @@ -44,48 +35,4 @@ public void pack(Provider info) { spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); }); } - - @ApiStatus.Internal - public void libraries(Provider> libraries) { - // TODO: [ForgeDev][LazyConfig] See if we can trick CopySpec into allowing an empty list - for (var artifact : libraries.get()) { - library(this.getProject().provider(() -> artifact), lib -> {}); - } - } - - @ApiStatus.Internal - public void library(Provider provider, Action action) { - // TODO: [ForgeDev][LazyConfig] See if we can trick CopySpec into allowing an empty list - var info = provider.map(LibraryInfo::from).map(LibraryInfo.apply(action)).get(); - var artifact = info.downloads().artifact(); - var offline = installer.get().getOffline().get(); - - // If we are not making an offline installer, and we're on the CI don't check remote, assume we're gunna publish everything - if (!offline && installer.get().getCi().get()) { - getProject().getLogger().lifecycle("Skipping: " + artifact.path); - return; - } - - // If it's an offline jar, always pack - var pack = offline || artifact.url.isEmpty(); - - // If it's not, Check if the remote - if (!pack) { - try { - // See if the remote hash is the same as ours - var remote = DownloadUtils.downloadString(artifact.url + ".sha1"); - pack = !artifact.sha1.equals(remote); - } catch (FileNotFoundException e) { - // The file doesn't exist, Mojang's maven doesn't include them, so assume it exists if it's on there. - pack = !artifact.url.startsWith("https://libraries.minecraft.net/"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - if (pack) - pack(provider); - else - getProject().getLogger().lifecycle("Skipping: " + artifact.path); - } } diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java new file mode 100644 index 0000000..ae31858 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import net.minecraftforge.util.download.DownloadUtils; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.TaskAction; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import javax.inject.Inject; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; + +@ApiStatus.Internal +public abstract class InstallerJarConfig extends DefaultTask { + private final Provider installer; + + @InputFiles abstract ConfigurableFileCollection getInput(); + @Input abstract Property getDev(); + @Input abstract Property getOffline(); + @Input abstract ListProperty getLibraries(); + private final Map, Action> actions = new IdentityHashMap<>(); + + @Inject + public InstallerJarConfig(Provider installer) { + this.installer = installer; + this.getDev().convention(installer.flatMap(Installer::getDev)); + this.getOffline().convention(installer.flatMap(Installer::getOffline)); + } + + @TaskAction + protected void exec() { + var actions = new HashMap>(); + for (var entry : this.actions.entrySet()) { + actions.put(entry.getKey().get(), entry.getValue()); + } + + for (var artifact : getLibraries().get()) { + var action = actions.get(artifact.info().name()); + pack(artifact, action); + } + } + + @ApiStatus.Internal + public void libraries(Configuration config) { + this.getInput().from(config); + this.getLibraries().addAll(MinimalResolvedArtifact.from(getProject(), config)); + } + + @ApiStatus.Internal + public void library(Provider info, Action action) { + this.getInput().from(info.map(MinimalResolvedArtifact::file)); + this.getLibraries().add(info); + actions.put(info.map(artifact -> artifact.info().name()), action); + } + + private void pack(MinimalResolvedArtifact resolved, @Nullable Action action) { + var info = LibraryInfo.from(resolved); + if (action != null) + action.execute(info); + + var artifact = info.downloads().artifact(); + var offline = getOffline().get(); + + // If we are not making an offline installer, and we're on the CI don't check remote, assume we're gunna publish everything + if (!offline && !getDev().get()) { + //getProject().getLogger().lifecycle("Skipping: " + artifact.path); + return; + } + + // If it's an offline jar, always pack + var pack = offline || artifact.url.isEmpty(); + + // If it's not, Check if the remote + if (!pack) { + try { + // See if the remote hash is the same as ours + var remote = DownloadUtils.downloadString(artifact.url + ".sha1"); + pack = !artifact.sha1.equals(remote); + } catch (FileNotFoundException e) { + // The file doesn't exist, Mojang's maven doesn't include them, so assume it exists if it's on there. + pack = !artifact.url.startsWith("https://libraries.minecraft.net/"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + if (!pack) { + //getProject().getLogger().lifecycle("Skipping: " + artifact.path); + return; + } + + this.installer.get().getJar().configure(task -> { + task.from(resolved.file(), spec -> { + spec.rename(name -> { + var path = resolved.info().path(); + getProject().getLogger().lifecycle("Adding: " + path); + return "maven/" + path; + }); + spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + }); + }); + } +} diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java index b0bcae2..739355d 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -83,7 +83,6 @@ public void libraries(Configuration config) { } @ApiStatus.Internal public void library(Provider info, Action action) { - getLogger().lifecycle("Library: " + info); this.getInput().from(info.map(MinimalResolvedArtifact::file)); this.getLibraries().add(info.map(LibraryInfo::from).map(LibraryInfo.apply(action))); } From 43489f2df55c502581bb1ad5338ef5f1806d78c2 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 21:18:30 -0800 Subject: [PATCH 5/7] Make Jar Config faster by filtering duplicates Move copying the base to Jar Config so its after user config --- .../forgedev/ForgeDevExtension.java | 5 +++ .../forgedev/legacy/tasks/InstallerJar.groovy | 1 - .../forgedev/tasks/installer/Installer.java | 6 +-- .../tasks/installer/InstallerJarConfig.java | 38 +++++++++++++------ .../tasks/installer/steps/Extract.java | 4 ++ .../tasks/installer/steps/ExtractBundle.java | 4 ++ 6 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java index 43388ad..7d810a5 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java +++ b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java @@ -4,6 +4,7 @@ */ package net.minecraftforge.forgedev; +import net.minecraftforge.forgedev.legacy.tasks.DownloadDependency; import net.minecraftforge.forgedev.legacy.values.CIRuntime; import net.minecraftforge.forgedev.tasks.compat.LegacyExtractZip; import net.minecraftforge.forgedev.tasks.compat.LegacyMergeFilesTask; @@ -37,6 +38,7 @@ import org.gradle.api.attributes.Attribute; import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.ProjectLayout; import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlugin; @@ -112,6 +114,9 @@ public Configuration getMinecraftConfiguration() { public Installer installer() { return installer(Installer.DEFAULT_NAME); } + public Installer installer(Action action) { + return installer(Installer.DEFAULT_NAME, action); + } public Installer installer(String name) { return installer(name, i -> {}); } diff --git a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy index 68eccba..d6df54b 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy +++ b/src/main/groovy/net/minecraftforge/forgedev/legacy/tasks/InstallerJar.groovy @@ -22,7 +22,6 @@ abstract class InstallerJar extends Zip { @Input @Optional abstract Property getFat() @Input @Optional abstract Property getOffline() - protected abstract @Inject ProjectLayout getLayout() protected abstract @Inject ProviderFactory getProviders() diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java index f4c23b4..00ba411 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java @@ -194,7 +194,7 @@ class Holder { var self = project.getProviders().provider(() -> holder.value); var base = DownloadDependency.register(project, name + "DownloadBase", Tools.INSTALLER.getModule().toString()); - var jarConfig = tasks.register(name + "JarConfig", InstallerJarConfig.class, self); + var jarConfig = tasks.register(name + "JarConfig", InstallerJarConfig.class, self, base); var jar = tasks.register(name + "Jar", InstallerJar.class); var json = tasks.register(name + "Json", InstallerJson.class); var launcherJson = tasks.register(name + "LauncherJson", LauncherJson.class); @@ -212,10 +212,6 @@ class Holder { json, launcherJson ); - - task.from(project.zipTree(base.map(DownloadDependency::getOutput)), cfg -> { - cfg.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); - }); }); json.configure(task -> { diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java index ae31858..a6675a1 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java @@ -4,6 +4,7 @@ */ package net.minecraftforge.forgedev.tasks.installer; +import net.minecraftforge.forgedev.legacy.tasks.DownloadDependency; import net.minecraftforge.forgedev.legacy.values.LibraryInfo; import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; import net.minecraftforge.util.download.DownloadUtils; @@ -18,6 +19,7 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; @@ -25,12 +27,14 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; @ApiStatus.Internal public abstract class InstallerJarConfig extends DefaultTask { private final Provider installer; + private final TaskProvider base; @InputFiles abstract ConfigurableFileCollection getInput(); @Input abstract Property getDev(); @@ -39,22 +43,39 @@ public abstract class InstallerJarConfig extends DefaultTask { private final Map, Action> actions = new IdentityHashMap<>(); @Inject - public InstallerJarConfig(Provider installer) { + public InstallerJarConfig(Provider installer, TaskProvider base) { this.installer = installer; + this.base = base; this.getDev().convention(installer.flatMap(Installer::getDev)); this.getOffline().convention(installer.flatMap(Installer::getOffline)); } @TaskAction protected void exec() { + // Add the base here, so that the buildscript configuration runs first + installer.get().jar( task -> { + task.from(getProject().zipTree(base.map(DownloadDependency::getOutput)), cfg -> { + cfg.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + }); + }); + var actions = new HashMap>(); for (var entry : this.actions.entrySet()) { actions.put(entry.getKey().get(), entry.getValue()); } - for (var artifact : getLibraries().get()) { + var seen = new HashSet(); + for (var artifact : getLibraries().get()) { + var info = LibraryInfo.from(artifact); var action = actions.get(artifact.info().name()); - pack(artifact, action); + if (action != null) + action.execute(info); + + // Check for duplicates without pinging remote servers + if (!seen.add(info.downloads().artifact().path)) + continue; + + pack(artifact, info); } } @@ -71,11 +92,7 @@ public void library(Provider info, Action actions.put(info.map(artifact -> artifact.info().name()), action); } - private void pack(MinimalResolvedArtifact resolved, @Nullable Action action) { - var info = LibraryInfo.from(resolved); - if (action != null) - action.execute(info); - + private void pack(MinimalResolvedArtifact resolved, LibraryInfo info) { var artifact = info.downloads().artifact(); var offline = getOffline().get(); @@ -110,9 +127,8 @@ private void pack(MinimalResolvedArtifact resolved, @Nullable Action { task.from(resolved.file(), spec -> { spec.rename(name -> { - var path = resolved.info().path(); - getProject().getLogger().lifecycle("Adding: " + path); - return "maven/" + path; + getProject().getLogger().lifecycle("Adding: " + artifact.path); + return "maven/" + artifact.path; }); spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); }); diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java index cc1edfb..b3236b9 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ package net.minecraftforge.forgedev.tasks.installer.steps; import net.minecraftforge.forgedev.tasks.installer.Tool; diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java index f726e56..8d55380 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ package net.minecraftforge.forgedev.tasks.installer.steps; import net.minecraftforge.forgedev.tasks.installer.Tool; From 9083b3e0cf3114d121a93233fc749029aaeee192 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 21:50:18 -0800 Subject: [PATCH 6/7] Fix typo and add ability to have launcher json only files --- .../forgedev/tasks/installer/Installer.java | 2 +- .../forgedev/tasks/installer/LauncherJson.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java index 00ba411..4f129a8 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/Installer.java @@ -215,7 +215,7 @@ class Holder { }); json.configure(task -> { - task.getOutput().set(baseDir.file("installer_profile.json")); + task.getOutput().set(baseDir.file("install_profile.json")); }); launcherJson.configure(task -> { diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java index 739355d..23e7144 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -8,7 +8,9 @@ import com.google.gson.GsonBuilder; import net.minecraftforge.forgedev.legacy.tasks.Util; import net.minecraftforge.forgedev.legacy.values.LibraryInfo; +import net.minecraftforge.forgedev.legacy.values.MavenInfo; import net.minecraftforge.forgedev.legacy.values.MinimalResolvedArtifact; +import net.minecraftforge.forgedev.tasks.SingleFileOutput; import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.artifacts.Configuration; @@ -24,6 +26,7 @@ import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; import org.jetbrains.annotations.ApiStatus; import javax.inject.Inject; @@ -87,6 +90,16 @@ public void library(Provider info, Action this.getLibraries().add(info.map(LibraryInfo::from).map(LibraryInfo.apply(action))); } + public void generated(TaskProvider task, String classifier) { + generated(task, classifier, t -> {}); + } + public void generated(TaskProvider task, String classifier, Action action) { + library(MinimalResolvedArtifact.from(MavenInfo.from(getProject(), classifier), task.flatMap(SingleFileOutput::getOutput)), info -> { + action.execute(info); + info.downloads().artifact().url = ""; + }); + } + @TaskAction protected void exec() throws IOException { var json = new LinkedHashMap(); From abf9358fdd4103429597c2a1a50fa76553f592c8 Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 19 Feb 2026 22:10:33 -0800 Subject: [PATCH 7/7] Use ArchiveOperations --- .../tasks/installer/InstallerJarConfig.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java index a6675a1..10c779c 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java @@ -11,6 +11,7 @@ import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.ArchiveOperations; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.provider.ListProperty; @@ -21,7 +22,6 @@ import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskProvider; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; import javax.inject.Inject; import java.io.FileNotFoundException; @@ -42,6 +42,8 @@ public abstract class InstallerJarConfig extends DefaultTask { @Input abstract ListProperty getLibraries(); private final Map, Action> actions = new IdentityHashMap<>(); + @Inject protected abstract ArchiveOperations getArchiveOperations(); + @Inject public InstallerJarConfig(Provider installer, TaskProvider base) { this.installer = installer; @@ -54,17 +56,24 @@ public InstallerJarConfig(Provider installer, TaskProvider { - task.from(getProject().zipTree(base.map(DownloadDependency::getOutput)), cfg -> { + task.from(getArchiveOperations().zipTree(base.map(DownloadDependency::getOutput)), cfg -> { cfg.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); }); }); + // If we are not making an offline installer, and we're on the CI don't check remote, assume we're gunna publish everything + if (!getOffline().get() && !getDev().get()) { + //getLogger().lifecycle("Skipping: " + artifact.path); + return; + } + var actions = new HashMap>(); for (var entry : this.actions.entrySet()) { actions.put(entry.getKey().get(), entry.getValue()); } var seen = new HashSet(); + // TODO: [ForgeDev][Optimization] Thread this, as it takes 30s/build because of all the network requests for (var artifact : getLibraries().get()) { var info = LibraryInfo.from(artifact); var action = actions.get(artifact.info().name()); @@ -94,16 +103,9 @@ public void library(Provider info, Action private void pack(MinimalResolvedArtifact resolved, LibraryInfo info) { var artifact = info.downloads().artifact(); - var offline = getOffline().get(); - - // If we are not making an offline installer, and we're on the CI don't check remote, assume we're gunna publish everything - if (!offline && !getDev().get()) { - //getProject().getLogger().lifecycle("Skipping: " + artifact.path); - return; - } - // If it's an offline jar, always pack - var pack = offline || artifact.url.isEmpty(); + // If it's an offline jar, or generated artifact always pack + var pack = getOffline().get() || artifact.url.isEmpty(); // If it's not, Check if the remote if (!pack) { @@ -120,14 +122,14 @@ private void pack(MinimalResolvedArtifact resolved, LibraryInfo info) { } if (!pack) { - //getProject().getLogger().lifecycle("Skipping: " + artifact.path); + //getLogger().lifecycle("Skipping: " + artifact.path); return; } this.installer.get().getJar().configure(task -> { task.from(resolved.file(), spec -> { spec.rename(name -> { - getProject().getLogger().lifecycle("Adding: " + artifact.path); + getLogger().lifecycle("Adding: " + artifact.path); return "maven/" + artifact.path; }); spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE);