diff --git a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java index 217072d..7d810a5 100644 --- a/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java +++ b/src/main/groovy/net/minecraftforge/forgedev/ForgeDevExtension.java @@ -4,10 +4,13 @@ */ 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; 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; @@ -35,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; @@ -47,6 +51,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 +82,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 +111,25 @@ public Configuration getMinecraftConfiguration() { return this.minecraftDepsConfiguration; } + 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 -> {}); + } + 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/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..a7343d5 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,14 @@ */ package net.minecraftforge.forgedev.legacy.values; +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; 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 +22,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 +30,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) { @@ -57,35 +50,38 @@ public static MinimalResolvedArtifact from(Project project, ResolvedArtifactResu public static Provider> from(Project project, Configuration configuration) { var ret = project.getObjects().listProperty(MinimalResolvedArtifact.class); + ret.addAll(configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(transform(project))); + return Util.finalize(project, ret); + } - var configurations = project.getConfigurations(); - - // Find any artifacts from the 'installer' config - // This config specifies the runtime files we intend for the interaller 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(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; + }; + } - ret.disallowChanges(); - ret.finalizeValueOnRead(); - if (project.getState().getExecuted()) { - ret.finalizeValue(); - } + 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(transform(project))); + 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..4f129a8 --- /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 jarConfig; + private final TaskProvider json; + private final TaskProvider launcherJson; + + // Implemntation details + private final TaskProvider base; + + @Input + public abstract Property getDev(); + @Input + public abstract Property getOffline(); + + @Inject + 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.getDev().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.jarConfig.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.jarConfig.configure(task -> task.library(info, action)); + } + + public void launcherLibraries(Configuration configuration) { + this.getLauncherJson().configure(task -> task.libraries(configuration)); + this.jarConfig.configure(task -> task.libraries(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.jarConfig.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 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); + + var baseDir = project.getLayout().getBuildDirectory().dir(name).get(); + var ret = project.getObjects().newInstance(Installer.class, project, name, jar, jarConfig, json, launcherJson, base); + holder.value = ret; + ret.getDev().convention(!ext.isCi()); + + jar.configure(task -> { + task.getArchiveClassifier().set(Util.kebab(name)); + task.dependsOn(jarConfig); + + task.from( + json, + launcherJson + ); + }); + + json.configure(task -> { + task.getOutput().set(baseDir.file("install_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..4a9aa8b --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJar.java @@ -0,0 +1,38 @@ +/* + * 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.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; + +public abstract class InstallerJar extends Zip { + @Inject + 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"); + 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); + }); + } +} 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..10c779c --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJarConfig.java @@ -0,0 +1,139 @@ +/* + * 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.tasks.DownloadDependency; +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.ArchiveOperations; +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.gradle.api.tasks.TaskProvider; +import org.jetbrains.annotations.ApiStatus; + +import javax.inject.Inject; +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(); + @Input abstract Property getOffline(); + @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; + 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(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()); + if (action != null) + action.execute(info); + + // Check for duplicates without pinging remote servers + if (!seen.add(info.downloads().artifact().path)) + continue; + + pack(artifact, info); + } + } + + @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, LibraryInfo info) { + var artifact = info.downloads().artifact(); + + // 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) { + 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) { + //getLogger().lifecycle("Skipping: " + artifact.path); + return; + } + + this.installer.get().getJar().configure(task -> { + task.from(resolved.file(), spec -> { + spec.rename(name -> { + getLogger().lifecycle("Adding: " + artifact.path); + return "maven/" + artifact.path; + }); + spec.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); + }); + }); + } +} 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..6a6c938 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/InstallerJson.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +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; +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 { + private static final Gson GSON = new GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create(); + + 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 = 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 new file mode 100644 index 0000000..23e7144 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/LauncherJson.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.forgedev.tasks.installer; + +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.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; +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.gradle.api.tasks.TaskProvider; +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.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() + .disableHtmlEscaping() + .setPrettyPrinting() + .create(); + + 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(); + @Input abstract Property getSortLibraries(); + @Input abstract Property getLibrariesLast(); + @Input abstract Property getDuplicateLibraries(); + + @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"); + getSortLibraries().convention(true); + getLibrariesLast().convention(true); + getDuplicateLibraries().convention(false); + } + + @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))); + } + + 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(); + 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().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()) + args.put("game", getGameArgs().get()); + if (getJvmArgs().isPresent()) + args.put("jvm", getJvmArgs().get()); + if (!args.isEmpty()) + json.put("arguments", args); + } +} 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..b3236b9 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/Extract.java @@ -0,0 +1,49 @@ +/* + * 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.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..8d55380 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/forgedev/tasks/installer/steps/ExtractBundle.java @@ -0,0 +1,45 @@ +/* + * 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.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()