diff --git a/build.gradle b/build.gradle index b7b1e5d..7a66eaf 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,8 @@ plugins { gradleutils.displayName = 'Forge Gradle Utilities' description = "Small collection of utilities for standardizing Forge's buildscripts." group = 'net.minecraftforge' -version = gitversion.tagOffset +//version = gitversion.tagOffset +version = '3.5.0' println "Version: $version" diff --git a/gradleutils-shared/build.gradle b/gradleutils-shared/build.gradle index 1cff468..15b24cf 100644 --- a/gradleutils-shared/build.gradle +++ b/gradleutils-shared/build.gradle @@ -18,7 +18,8 @@ gradleutils.displayName = 'GradleUtils Shared' description = 'The shared base used by all of Forge\'s Gradle plugins.' base.archivesName = 'gradleutils-shared' group = 'net.minecraftforge' -version = gitversion.tagOffset +//version = gitversion.tagOffset +version = '3.5.0' java { toolchain.languageVersion = JavaLanguageVersion.of(17) diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java index bd7d692..2ada177 100644 --- a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java @@ -29,7 +29,7 @@ /// needing to duplicate code across projects. /// /// @param The type of target -public non-sealed abstract class EnhancedPlugin implements Plugin, EnhancedPluginAdditions { +public abstract non-sealed class EnhancedPlugin implements Plugin, EnhancedPluginAdditions { private final String name; private final String displayName; private final @Nullable String toolsExtName; diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPluginAdditions.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPluginAdditions.java index 5a3bb30..4dcfbf9 100644 --- a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPluginAdditions.java +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedPluginAdditions.java @@ -8,7 +8,7 @@ /// This interface defines the additional methods added by [EnhancedPlugin]. They are additionally accessible in tasks /// that implement [EnhancedTask]. -public sealed interface EnhancedPluginAdditions permits EnhancedPlugin, EnhancedTask { +public sealed interface EnhancedPluginAdditions permits EnhancedPlugin, EnhancedTask, EnhancedTaskAdditions { /// Gets a provider to the file for a [Tool] to be used. The tool's state is managed by Gradle through the /// [org.gradle.api.provider.ValueSource] API and will not cause caching issues. /// diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTask.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTask.java index 9292907..e8884df 100644 --- a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTask.java +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTask.java @@ -16,7 +16,7 @@ /// The enhanced task contains a handful of helper methods to make working with the enhanced plugin and caches easier. /// /// @param

The type of enhanced problems -public non-sealed interface EnhancedTask

extends Task, EnhancedPluginAdditions { +public non-sealed interface EnhancedTask

extends Task, EnhancedPluginAdditions, EnhancedTaskAdditions { /// The enhanced plugin type for this task. /// /// @return The plugin type @@ -27,70 +27,14 @@ public non-sealed interface EnhancedTask

extends Tas /// @return The problems type Class

problemsType(); - private EnhancedPlugin getPlugin() { - return this.getProject().getPlugins().getPlugin(this.pluginType()); - } - - @Override - default Tool.Resolved getTool(Tool tool) { - return this.getPlugin().getTool(tool); - } - - @Override - default DirectoryProperty globalCaches() { - return this.getPlugin().globalCaches(); - } - @Override - default DirectoryProperty localCaches() { - return this.getPlugin().localCaches(); - } - - @Override - default DirectoryProperty rootProjectDirectory() { - return this.getPlugin().rootProjectDirectory(); + default EnhancedPlugin plugin() { + return this.getProject().getPlugins().getPlugin(this.pluginType()); } @Override - default DirectoryProperty workingProjectDirectory() { - return this.getPlugin().workingProjectDirectory(); - } - - /// The default output directory to use for this task if it outputs a directory. - /// - /// @return A provider for the directory - default @Internal Provider getDefaultOutputDirectory() { - return this.localCaches().dir(this.getName()).map(this.getPlugin().getProblemsInternal().ensureFileLocation()); - } - - /// The default output file to use for this task if it outputs a file. Uses the `.jar` extension. - /// - /// @return A provider for the file - default @Internal Provider getDefaultOutputFile() { - return this.getDefaultOutputFile("jar"); - } - - /// The default output file to use for this task if it outputs a file. - /// - /// @param ext The extension to use for the file - /// @return A provider for the file - default @Internal Provider getDefaultOutputFile(String ext) { - return this.getOutputFile("output." + ext); - } - - /// The default output log file to use for this task. - /// - /// @return A provider for the file - default @Internal Provider getDefaultLogFile() { - return this.getOutputFile("log.txt"); - } - - /// A file with the specified name in the default output directory. - /// - /// @param fileName The name of the output file - /// @return A provider for the file - default @Internal Provider getOutputFile(String fileName) { - return this.localCaches().file(String.format("%s/%s", this.getName(), fileName)).map(this.getPlugin().getProblemsInternal().ensureFileLocation()); + default String baseName() { + return this.getName(); } default void afterEvaluate(Action action) { diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTaskAdditions.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTaskAdditions.java new file mode 100644 index 0000000..e9611cd --- /dev/null +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/EnhancedTaskAdditions.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.file.Directory; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Internal; + +sealed interface EnhancedTaskAdditions extends EnhancedPluginAdditions permits EnhancedTask, ToolExecSpec { + EnhancedPlugin plugin(); + + String baseName(); + + default Tool.Resolved getTool(Tool tool) { + return this.plugin().getTool(tool); + } + + default DirectoryProperty globalCaches() { + return this.plugin().globalCaches(); + } + + default DirectoryProperty localCaches() { + return this.plugin().localCaches(); + } + + default DirectoryProperty rootProjectDirectory() { + return this.plugin().rootProjectDirectory(); + } + + default DirectoryProperty workingProjectDirectory() { + return this.plugin().workingProjectDirectory(); + } + + /// The default output directory to use for this task if it outputs a directory. + /// + /// @return A provider for the directory + default @Internal Provider getDefaultOutputDirectory() { + return this.localCaches().dir(this.baseName()).map(this.plugin().getProblemsInternal().ensureFileLocation()); + } + + /// The default output file to use for this task if it outputs a file. Uses the `.jar` extension. + /// + /// @return A provider for the file + default @Internal Provider getDefaultOutputFile() { + return this.getDefaultOutputFile("jar"); + } + + /// The default output file to use for this task if it outputs a file. + /// + /// @param ext The extension to use for the file + /// @return A provider for the file + default @Internal Provider getDefaultOutputFile(String ext) { + return this.getOutputFile("output." + ext); + } + + /// The default output log file to use for this task. + /// + /// @return A provider for the file + default @Internal Provider getDefaultLogFile() { + return this.getOutputFile("log.txt"); + } + + /// A file with the specified name in the default output directory. + /// + /// @param fileName The name of the output file + /// @return A provider for the file + default @Internal Provider getOutputFile(String fileName) { + return this.localCaches().file(String.format("%s/%s", this.baseName(), fileName)).map(this.plugin().getProblemsInternal().ensureFileLocation()); + } +} diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollector.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollector.java new file mode 100644 index 0000000..e551a77 --- /dev/null +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollector.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.file.FileSystemLocationProperty; +import org.gradle.api.provider.Provider; +import org.jetbrains.annotations.UnknownNullability; + +import java.io.File; +import java.util.Map; + +public interface ToolExecArgumentsCollector { + void args(Object... args); + + void args(Iterable args); + + void args(String arg, Iterable files); + + void args(String arg, FileSystemLocationProperty fileProvider); + + void args(String arg, @UnknownNullability Provider provider); + + void args(Map args); + + void jvmArgs(Object... args); + + void jvmArgs(Iterable args); + + void environment(String key, String value); + + void systemProperty(String key, String value); +} diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollectorImpl.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollectorImpl.java new file mode 100644 index 0000000..99c942f --- /dev/null +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecArgumentsCollectorImpl.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.file.FileSystemLocationProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderConvertible; +import org.gradle.api.provider.ProviderFactory; +import org.jetbrains.annotations.UnknownNullability; + +import javax.inject.Inject; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +abstract class ToolExecArgumentsCollectorImpl implements ToolExecArgumentsCollector { + final List> args = new ArrayList<>(); + final List> jvmArgs = new ArrayList<>(); + final Map environment = new HashMap<>(); + final Map systemProperties = new HashMap<>(); + + protected abstract @Inject ProviderFactory getProviders(); + + @Inject + public ToolExecArgumentsCollectorImpl() { } + + @Override + public void args(Object... args) { + this.args(Arrays.asList(args)); + } + + @Override + public void args(Iterable args) { + try { + for (var arg : args) { + if (arg instanceof ProviderConvertible providerConvertible) + this.args.add(providerConvertible.asProvider().map(Object::toString)); + if (arg instanceof Provider provider) + this.args.add(provider.map(Object::toString)); + else + this.args.add(this.getProviders().provider(arg::toString)); + } + } catch (NullPointerException e) { + throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); + } + } + + /// Adds each file to the arguments preceded by the given argument. Designed to work well with + /// JOpt Simple. + /// + /// @param arg The flag to use for each file + /// @param files The files to add + @Override + public void args(String arg, Iterable files) { + for (File file : files) + this.args(arg, file); + } + + /// Adds the given argument followed by the given file location to the arguments. + /// + /// @param arg The flag to use + /// @param fileProvider The file to add + @Override + public void args(String arg, FileSystemLocationProperty fileProvider) { + this.args(arg, fileProvider.getLocationOnly()); + } + + /// Adds the given argument followed by the given object (may be a file location) to the arguments. + /// + /// @param arg The flag to use + /// @param provider The object (or file) to add + @Override + public void args(String arg, @UnknownNullability Provider provider) { + if (provider == null || !provider.isPresent()) return; + + // NOTE: We don't use File#getAbsoluteFile because path sensitivity should be handled by tasks. + var value = provider.map(it -> it instanceof FileSystemLocation ? ((FileSystemLocation) it).getAsFile() : it).getOrNull(); + if (value == null) return; + + if (value instanceof Boolean booleanValue) { + if (booleanValue) + this.args(arg); + } else { + this.args(arg, String.valueOf(value)); + } + } + + /// Adds the given map of arguments. + /// + /// [#args(String, Provider)] will be invoked for each entry in the map. If the key and/or value are not of the + /// required types, they will be automatically converted using [Object#toString()] and + /// [org.gradle.api.provider.ProviderFactory#provider(Callable)]. + /// + /// @param args The args to add + /// @deprecated Too ambiguous with [#args(String, Provider)]. Prefer that method instead. + @Override + @Deprecated(forRemoval = true) + public void args(Map args) { + for (Map.Entry entry : args.entrySet()) { + var key = entry.getKey(); + var value = entry.getValue(); + this.args( + key instanceof Provider provider ? provider.map(Object::toString).get() : this.getProviders().provider(key::toString).get(), + value instanceof Provider provider ? (provider instanceof FileSystemLocationProperty file ? file.getLocationOnly() : provider) : this.getProviders().provider(() -> value) + ); + } + } + + @Override + public void jvmArgs(Object... args) { + try { + for (var arg : args) { + this.jvmArgs.add(this.getProviders().provider(arg::toString)); + } + } catch (NullPointerException e) { + throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); + } + } + + @Override + public void jvmArgs(Iterable args) { + try { + for (var arg : args) { + this.jvmArgs.add(this.getProviders().provider(arg::toString)); + } + } catch (NullPointerException e) { + throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); + } + } + + @Override + public void environment(String key, String value) { + try { + this.environment.put(key, value); + } catch (NullPointerException e) { + throw new IllegalStateException("ToolExecBase#environment can only be called inside of #addArguments()", e); + } + } + + @Override + public void systemProperty(String key, String value) { + try { + this.systemProperties.put(key, value); + } catch (NullPointerException e) { + throw new IllegalStateException("ToolExecBase#systemProperty can only be called inside of #addArguments()", e); + } + } +} diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecBase.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecBase.java index 2cbee8c..2fd5fdb 100644 --- a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecBase.java +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecBase.java @@ -80,20 +80,30 @@ public abstract class ToolExecBase

extends DefaultTa return this.additionalArgs; } - //region JavaExec - public abstract @InputFiles @Classpath ConfigurableFileCollection getClasspath(); - - public abstract @Input @Optional Property getMainClass(); + protected final @Nested ToolExecSpec getSpec() { + return this.spec; + } - public abstract @Nested Property getJavaLauncher(); + //region JavaExec + public @Internal ConfigurableFileCollection getClasspath() { + return this.spec.getClasspath(); + } - protected abstract @Nested Property getToolchainLauncher(); + public @Internal Property getMainClass() { + return this.spec.getMainClass(); + } - public abstract @Input @Optional Property getPreferToolchainJvm(); + public @Internal Property getJavaLauncher() { + return this.spec.getJavaLauncher(); + } - public abstract @Internal DirectoryProperty getWorkingDir(); + public @Internal Property getPreferToolchainJvm() { + return this.spec.getPreferToolchainJvm(); + } - protected abstract @Internal MapProperty getForkProperties(); + public @Internal DirectoryProperty getWorkingDir() { + return this.spec.getWorkingDir(); + } //endregion //region Logging @@ -102,22 +112,22 @@ public abstract class ToolExecBase

extends DefaultTa return super.getLogging(); } - protected abstract @Console Property getStandardOutputLogLevel(); + protected @Console Property getStandardOutputLogLevel() { + return this.spec.getStandardOutputLogLevel(); + } - protected abstract @Console Property getStandardErrorLogLevel(); + protected @Console Property getStandardErrorLogLevel() { + return this.spec.getStandardErrorLogLevel(); + } - protected abstract @Internal RegularFileProperty getLogFile(); + protected @Internal RegularFileProperty getLogFile() { + return this.spec.getLogFile(); + } //endregion protected abstract @Inject ObjectFactory getObjects(); - protected abstract @Inject ProviderFactory getProviders(); - - protected abstract @Inject ExecOperations getExecOperations(); - - protected abstract @Inject DependencyFactory getDependencyFactory(); - - protected abstract @Inject JavaToolchainService getJavaToolchains(); + private final ToolExecSpec spec; /// Creates a new task instance using the given types and tool information. /// @@ -128,78 +138,36 @@ public abstract class ToolExecBase

extends DefaultTa /// best practice is to make a single `ToolExec` class for the implementing plugin to use, which other tasks can /// extend off of. protected ToolExecBase(Tool tool) { - var resolved = this.getTool(tool); - SharedUtil.finalizeProperty(this.defaultToolDir.value( - this.globalCaches().dir(tool.getName().toLowerCase(Locale.ENGLISH)).map(this.ensureFileLocationInternal()) - )); - - this.getClasspath().setFrom(resolved.getClasspath()); - - if (resolved.hasMainClass()) - this.getMainClass().set(resolved.getMainClass()); - this.getJavaLauncher().set(resolved.getJavaLauncher()); - - this.getToolchainLauncher().convention(getJavaToolchains().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.current()))); - getProject().getPluginManager().withPlugin("java", javaAppliedPlugin -> - this.getToolchainLauncher().set(getJavaToolchains().launcherFor(getProject().getExtensions().getByType(JavaPluginExtension.class).getToolchain())) - ); - - this.getForkProperties().set(SharedUtil.getForkProperties(getProviders())); - - this.getStandardOutputLogLevel().convention(LogLevel.LIFECYCLE); - this.getStandardErrorLogLevel().convention(LogLevel.ERROR); - - this.getWorkingDir().convention(this.getDefaultOutputDirectory()); - this.getLogFile().convention(this.getDefaultLogFile()); + this.spec = getObjects().newInstance(ToolExecSpec.class, this.plugin(), this.getTool(tool)); } public final void using(CharSequence dependency) { - this.using(getDependencyFactory().create(dependency)); + this.spec.using(dependency); } public final void using(Provider dependency) { - this.getClasspath().setFrom( - getProject().getConfigurations().detachedConfiguration().withDependencies(d -> d.addLater(dependency)) - ); + this.spec.using(dependency); } public final void using(Provider dependency, Action variantSpec) { - this.using(getProject().getDependencies().variantOf(dependency, variantSpec)); + this.spec.using(dependency, variantSpec); } public final void using(ProviderConvertible dependency) { - this.using(dependency.asProvider()); + this.spec.using(dependency); } public final void using(ProviderConvertible dependency, Action variantSpec) { - this.using(getProject().getDependencies().variantOf(dependency, variantSpec)); + this.spec.using(dependency, variantSpec); } public final void using(Dependency dependency) { - this.getClasspath().setFrom( - getProject().getConfigurations().detachedConfiguration(dependency) - ); + this.spec.using(dependency); } @Deprecated public final void usingDirectly(CharSequence downloadUrl) { - var name = getName(); - var url = getProviders().provider(downloadUrl::toString); - this.getClasspath().setFrom(getProviders().of(ToolImpl.Source.class, spec -> spec.parameters(parameters -> { - parameters.getInputFile().set(getProviders().zip(localCaches(), url, (d, s) -> d.file("tools/" + name + '/' + s.substring(s.lastIndexOf('/'))))); - parameters.getDownloadUrl().set(url); - }))); - } - - /// The enhanced problems instance to use for this task. - /// - /// @return The enhanced problems - protected final @Internal P getProblems() { - return this.problems; - } - - private Transformer ensureFileLocationInternal() { - return t -> this.getProblems().ensureFileLocation().transform(t); + this.spec.usingDirectly(downloadUrl); } /// This method should be overridden by subclasses to add arguments to this task via [JavaExec#args]. To preserve @@ -207,119 +175,19 @@ private Transformer ensureFileLocationInter @MustBeInvokedByOverriders protected void addArguments() { } - private transient @Nullable List> args; - private transient @Nullable List> jvmArgs; - private transient @Nullable Map environment; - private transient @Nullable Map systemProperties; - /// @implNote Not invoking this method from an overriding method *will* result in the tool never being executed and /// [#addArguments()] never being run. @TaskAction protected ExecResult exec() throws IOException { - var logger = getLogger(); - - this.args = new ArrayList<>(); - this.jvmArgs = new ArrayList<>(); - this.environment = new HashMap<>(); - this.systemProperties = new HashMap<>(); - - this.addArguments(); - this.args(this.getAdditionalArgs().get()); - - var args = DefaultGroovyMethods.collect(this.args, Closures., String>function(Provider::get)); - var jvmArgs = DefaultGroovyMethods.collect(this.jvmArgs, Closures., String>function(Provider::get)); - - for (var property : this.getForkProperties().get().entrySet()) { - this.systemProperties.putIfAbsent(property.getKey(), property.getValue()); - } - - var stdOutLevel = this.getStandardOutputLogLevel().get(); - var stdErrLevel = this.getStandardErrorLogLevel().get(); - - JavaLauncher javaLauncher; - if (getPreferToolchainJvm().getOrElse(false)) { - var candidateLauncher = getJavaLauncher().get(); - var toolchainLauncher = getToolchainLauncher().get(); - javaLauncher = toolchainLauncher.getMetadata().getLanguageVersion().canCompileOrRun(candidateLauncher.getMetadata().getLanguageVersion()) - ? toolchainLauncher - : candidateLauncher; - } else { - javaLauncher = getJavaLauncher().get(); - } - - var workingDirectory = this.getWorkingDir().map(problems.ensureFileLocation()).get().getAsFile(); - - try (var log = new PrintWriter(new FileWriter(this.getLogFile().getAsFile().get()), true)) { - return getExecOperations().javaexec(spec -> { - spec.setIgnoreExitValue(true); - - spec.setWorkingDir(workingDirectory); - spec.setClasspath(this.getClasspath()); - if (this.getMainClass().isPresent()) - spec.getMainClass().set(this.getMainClass()); - spec.setExecutable(javaLauncher.getExecutablePath().getAsFile().getAbsolutePath()); - spec.setArgs(args); - spec.setJvmArgs(jvmArgs); - spec.setEnvironment(this.environment); - spec.setSystemProperties(this.systemProperties); - - spec.setStandardOutput(SharedUtil.toLog( - line -> { - logger.log(stdOutLevel, line); - log.println(line); - } - )); - spec.setErrorOutput(SharedUtil.toLog( - line -> { - logger.log(stdErrLevel, line); - log.println(line); - } - )); - - log.print("Java Launcher: "); - log.println(spec.getExecutable()); - log.print("Working directory: "); - log.println(spec.getWorkingDir().getAbsolutePath()); - log.print("Main class: "); - log.println(spec.getMainClass().getOrElse("AUTOMATIC")); - log.println("Arguments:"); - for (var s : spec.getArgs()) { - log.print(" "); - log.println(s); - } - log.println("JVM Arguments:"); - for (var s : spec.getAllJvmArgs()) { - log.print(" "); - log.println(s); - } - log.println("Classpath:"); - for (var f : getClasspath()) { - log.print(" "); - log.println(f.getAbsolutePath()); - } - log.println("===================================="); - }); - } + return this.spec.exec(); } protected final void args(Object... args) { - this.args(Arrays.asList(args)); + this.spec.args(args); } - @SuppressWarnings("DataFlowIssue") protected final void args(Iterable args) { - try { - for (var arg : args) { - if (arg instanceof ProviderConvertible providerConvertible) - this.args.add(providerConvertible.asProvider().map(Object::toString)); - if (arg instanceof Provider provider) - this.args.add(provider.map(Object::toString)); - else - this.args.add(this.getProviders().provider(arg::toString)); - } - } catch (NullPointerException e) { - throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); - } + this.spec.args(args); } /// Adds each file to the arguments preceded by the given argument. Designed to work well with @@ -328,8 +196,7 @@ protected final void args(Iterable args) { /// @param arg The flag to use for each file /// @param files The files to add protected final void args(String arg, Iterable files) { - for (File file : files) - this.args(arg, file); + this.spec.args(arg, files); } /// Adds the given argument followed by the given file location to the arguments. @@ -337,7 +204,7 @@ protected final void args(String arg, Iterable files) { /// @param arg The flag to use /// @param fileProvider The file to add protected final void args(String arg, FileSystemLocationProperty fileProvider) { - this.args(arg, fileProvider.getLocationOnly()); + this.spec.args(arg, fileProvider); } /// Adds the given argument followed by the given object (may be a file location) to the arguments. @@ -345,18 +212,7 @@ protected final void args(String arg, FileSystemLocationProperty provider) { - if (provider == null || !provider.isPresent()) return; - - // NOTE: We don't use File#getAbsoluteFile because path sensitivity should be handled by tasks. - var value = provider.map(it -> it instanceof FileSystemLocation ? ((FileSystemLocation) it).getAsFile() : it).getOrNull(); - if (value == null) return; - - if (value instanceof Boolean booleanValue) { - if (booleanValue) - this.args(arg); - } else { - this.args(arg, String.valueOf(value)); - } + this.spec.args(arg, provider); } /// Adds the given map of arguments. @@ -369,53 +225,22 @@ protected final void args(String arg, @UnknownNullability Provider provider) /// @deprecated Too ambiguous with [#args(String, Provider)]. Prefer that method instead. @Deprecated(forRemoval = true) protected final void args(Map args) { - for (Map.Entry entry : args.entrySet()) { - var key = entry.getKey(); - var value = entry.getValue(); - this.args( - key instanceof Provider provider ? provider.map(Object::toString).get() : this.getProviders().provider(key::toString).get(), - value instanceof Provider provider ? (provider instanceof FileSystemLocationProperty file ? file.getLocationOnly() : provider) : this.getProviders().provider(() -> value) - ); - } + this.spec.args(args); } - @SuppressWarnings("DataFlowIssue") protected final void jvmArgs(Object... args) { - try { - for (var arg : args) { - this.jvmArgs.add(this.getProviders().provider(arg::toString)); - } - } catch (NullPointerException e) { - throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); - } + this.spec.jvmArgs(args); } - @SuppressWarnings("DataFlowIssue") protected final void jvmArgs(Iterable args) { - try { - for (var arg : args) { - this.jvmArgs.add(this.getProviders().provider(arg::toString)); - } - } catch (NullPointerException e) { - throw new IllegalStateException("ToolExecBase#jvmArgs can only be called inside of #addArguments()", e); - } + this.spec.jvmArgs(args); } - @SuppressWarnings("DataFlowIssue") protected final void environment(String key, String value) { - try { - this.environment.put(key, value); - } catch (NullPointerException e) { - throw new IllegalStateException("ToolExecBase#environment can only be called inside of #addArguments()", e); - } + this.environment(key, value); } - @SuppressWarnings("DataFlowIssue") protected final void systemProperty(String key, String value) { - try { - this.systemProperties.put(key, value); - } catch (NullPointerException e) { - throw new IllegalStateException("ToolExecBase#systemProperty can only be called inside of #addArguments()", e); - } + this.spec.systemProperty(key, value); } } diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecOperations.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecOperations.java new file mode 100644 index 0000000..7331e56 --- /dev/null +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecOperations.java @@ -0,0 +1,25 @@ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.model.ObjectFactory; +import org.gradle.process.ExecResult; + +import javax.inject.Inject; + +public abstract class ToolExecOperations { + protected abstract @Inject ObjectFactory getObjects(); + + private final Plugin> plugin; + + @Inject + public ToolExecOperations(Plugin> plugin) { + this.plugin = plugin; + } + + public ExecResult toolexec(Tool tool, Action action) { + var spec = getObjects().newInstance(ToolExecSpec.class, plugin, tool); + action.execute(spec); + return spec.exec(); + } +} diff --git a/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecSpec.java b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecSpec.java new file mode 100644 index 0000000..1aff710 --- /dev/null +++ b/gradleutils-shared/src/main/java/net/minecraftforge/gradleutils/shared/ToolExecSpec.java @@ -0,0 +1,452 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils.shared; + +import groovy.lang.Closure; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.Transformer; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.MinimalExternalModuleDependency; +import org.gradle.api.artifacts.dsl.DependencyFactory; +import org.gradle.api.artifacts.dsl.ExternalModuleDependencyVariantSpec; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.file.FileSystemLocationProperty; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.JavaPluginExtension; +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.ProviderConvertible; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Console; +import org.gradle.api.tasks.Exec; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.process.ExecOperations; +import org.gradle.process.ExecResult; +import org.gradle.process.JavaExecSpec; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +public abstract non-sealed class ToolExecSpec implements EnhancedTaskAdditions { + private static final Logger LOGGER = Logging.getLogger(ToolExecSpec.class); + + //region JavaExec + public abstract @InputFiles @Classpath ConfigurableFileCollection getClasspath(); + + public abstract @Input @Optional Property getMainClass(); + + public abstract @Nested Property getJavaLauncher(); + + protected abstract @Nested Property getToolchainLauncher(); + + public abstract @Input @Optional Property getPreferToolchainJvm(); + + public abstract @Internal DirectoryProperty getWorkingDir(); + + protected abstract @Internal MapProperty getForkProperties(); + //endregion + + //region JavaExec Arguments + protected abstract @Input @Optional ListProperty getArgs(); + + protected abstract @Input @Optional ListProperty getJvmArgs(); + + protected abstract @Input @Optional MapProperty getEnvironment(); + + protected abstract @Input @Optional MapProperty getSystemProperties(); + //endregion + + //region Logging + protected abstract @Console Property getStandardOutputLogLevel(); + + protected abstract @Console Property getStandardErrorLogLevel(); + + protected abstract @Internal RegularFileProperty getLogFile(); + //endregion + + @Override + public EnhancedPlugin plugin() { + return this.plugin; + } + + protected abstract @Inject Project getProject(); + + protected abstract @Inject ObjectFactory getObjects(); + + protected abstract @Inject ProviderFactory getProviders(); + + protected abstract @Inject ExecOperations getExecOperations(); + + protected abstract @Inject DependencyFactory getDependencyFactory(); + + protected abstract @Inject JavaToolchainService getJavaToolchains(); + + private final String name; + private final transient EnhancedPlugin plugin; + private final String toolName; + + private Closure javaexecAction = Closures.consumer(it -> { }); + + private transient @Nullable List> overflowArgs; + private transient @Nullable List> overflowJvmArgs; + private transient @Nullable Map overflowEnvironment; + private transient @Nullable Map overflowSystemProperties; + + private void addArgs(Provider provider) { + if (this.overflowArgs != null) { + this.overflowArgs.add(provider); + return; + } + + try { + this.getArgs().add(provider); + } catch (IllegalStateException e) { + this.overflowArgs = new ArrayList<>(); + this.overflowArgs.add(provider); + } + } + + private void addJvmArgs(Provider provider) { + if (this.overflowJvmArgs != null) { + this.overflowJvmArgs.add(provider); + return; + } + + try { + this.getJvmArgs().add(provider); + } catch (IllegalStateException e) { + this.overflowJvmArgs = new ArrayList<>(); + this.overflowJvmArgs.add(provider); + } + } + + private void putEnvironment(String key, String value) { + if (this.overflowEnvironment != null) { + this.overflowEnvironment.put(key, value); + return; + } + + try { + this.getEnvironment().put(key, value); + } catch (IllegalStateException e) { + this.overflowEnvironment = new HashMap<>(); + this.overflowEnvironment.put(key, value); + } + } + + private void putSystemProperties(String key, String value) { + if (this.overflowSystemProperties != null) { + this.overflowSystemProperties.put(key, value); + return; + } + + try { + this.getSystemProperties().put(key, value); + } catch (IllegalStateException e) { + this.overflowSystemProperties = new HashMap<>(); + this.overflowSystemProperties.put(key, value); + } + } + + @Inject + public ToolExecSpec(String name, EnhancedPlugin plugin, Tool.Resolved resolved) { + this.name = name; + this.plugin = plugin; + this.toolName = resolved.getName(); + this.getClasspath().convention(resolved.getClasspath()); + + if (resolved.hasMainClass()) + this.getMainClass().set(resolved.getMainClass()); + this.getJavaLauncher().set(resolved.getJavaLauncher()); + + try { + this.getToolchainLauncher().convention(getJavaToolchains().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.current()))); + getProject().getPluginManager().withPlugin("java", javaAppliedPlugin -> + this.getToolchainLauncher().set(getJavaToolchains().launcherFor(getProject().getExtensions().getByType(JavaPluginExtension.class).getToolchain())) + ); + } catch (Exception ignored) { + // If these fail, we're likely in a Settings environment + // This is fine, we can just try using the Daemon JVM + } + + this.getForkProperties().set(SharedUtil.getForkProperties(getProviders())); + + this.getStandardOutputLogLevel().convention(LogLevel.LIFECYCLE); + this.getStandardErrorLogLevel().convention(LogLevel.ERROR); + + this.getWorkingDir().convention(this.getDefaultOutputDirectory()); + this.getLogFile().convention(this.getDefaultLogFile()); + } + + @Override + public String baseName() { + return this.name; + } + + public final void using(CharSequence dependency) { + this.using(getDependencyFactory().create(dependency)); + } + + public final void using(Provider dependency) { + this.getClasspath().setFrom( + getProject().getConfigurations().detachedConfiguration().withDependencies(d -> d.addLater(dependency)) + ); + } + + public final void using(Provider dependency, Action variantSpec) { + this.using(getProject().getDependencies().variantOf(dependency, variantSpec)); + } + + public final void using(ProviderConvertible dependency) { + this.using(dependency.asProvider()); + } + + public final void using(ProviderConvertible dependency, Action variantSpec) { + this.using(getProject().getDependencies().variantOf(dependency, variantSpec)); + } + + public final void using(Dependency dependency) { + this.getClasspath().setFrom( + getProject().getConfigurations().detachedConfiguration(dependency) + ); + } + + @Deprecated + public final void usingDirectly(CharSequence downloadUrl) { + var url = getProviders().provider(downloadUrl::toString); + this.getClasspath().setFrom(getProviders().of(ToolImpl.Source.class, spec -> spec.parameters(parameters -> { + parameters.getInputFile().set(getProviders().zip(localCaches(), url, (d, s) -> d.file("tools/" + toolName + '/' + s.substring(s.lastIndexOf('/'))))); + parameters.getDownloadUrl().set(url); + }))); + } + + private Transformer ensureFileLocationInternal() { + return t -> this.plugin.getProblemsInternal().ensureFileLocation().transform(t); + } + + public void configure(Action action) { + this.javaexecAction = this.javaexecAction.compose(Closures.unaryOperator(spec -> { + action.execute(spec); + return spec; + })); + } + + public ExecResult exec() { + return this.exec(getExecOperations()); + } + + public ExecResult exec(ExecOperations execOperations) { + var args = this.getArgs().getOrElse(List.of()); + if (this.overflowArgs != null) + args.addAll(DefaultGroovyMethods.collect(this.overflowArgs, Closures., String>function(Provider::get))); + + var jvmArgs = this.getJvmArgs().getOrElse(List.of()); + if (this.overflowJvmArgs != null) + jvmArgs.addAll(DefaultGroovyMethods.collect(this.overflowJvmArgs, Closures., String>function(Provider::get))); + + var environment = new HashMap<>(this.getEnvironment().getOrElse(Map.of())); + if (this.overflowEnvironment != null) + environment.putAll(this.overflowEnvironment); + + var systemProperties = new HashMap<>(this.getSystemProperties().getOrElse(Map.of())); + if (this.overflowSystemProperties != null) + systemProperties.putAll(this.overflowSystemProperties); + for (var property : this.getForkProperties().get().entrySet()) + systemProperties.putIfAbsent(property.getKey(), property.getValue()); + + var stdOutLevel = this.getStandardOutputLogLevel().get(); + var stdErrLevel = this.getStandardErrorLogLevel().get(); + + JavaLauncher javaLauncher; + if (getPreferToolchainJvm().getOrElse(false)) { + var candidateLauncher = getJavaLauncher().get(); + var toolchainLauncher = getToolchainLauncher().get(); + javaLauncher = toolchainLauncher.getMetadata().getLanguageVersion().canCompileOrRun(candidateLauncher.getMetadata().getLanguageVersion()) + ? toolchainLauncher + : candidateLauncher; + } else { + javaLauncher = getJavaLauncher().get(); + } + + var workingDirectory = this.getWorkingDir().map(ensureFileLocationInternal()).get().getAsFile(); + + try (var log = new PrintWriter(new FileWriter(this.getLogFile().getAsFile().get()), true)) { + return execOperations.javaexec(spec -> { + spec.setIgnoreExitValue(true); + + spec.setWorkingDir(workingDirectory); + spec.setClasspath(this.getClasspath()); + if (this.getMainClass().isPresent()) + spec.getMainClass().set(this.getMainClass()); + spec.setExecutable(javaLauncher.getExecutablePath().getAsFile().getAbsolutePath()); + spec.setArgs(args); + spec.setJvmArgs(jvmArgs); + spec.setEnvironment(environment); + spec.setSystemProperties(systemProperties); + + spec.setStandardOutput(SharedUtil.toLog( + line -> { + LOGGER.log(stdOutLevel, line); + log.println(line); + } + )); + spec.setErrorOutput(SharedUtil.toLog( + line -> { + LOGGER.log(stdErrLevel, line); + log.println(line); + } + )); + + javaexecAction.call(spec); + + log.print("Java Launcher: "); + log.println(spec.getExecutable()); + log.print("Working directory: "); + log.println(spec.getWorkingDir().getAbsolutePath()); + log.print("Main class: "); + log.println(spec.getMainClass().getOrElse("AUTOMATIC")); + log.println("Arguments:"); + for (var s : spec.getArgs()) { + log.print(" "); + log.println(s); + } + log.println("JVM Arguments:"); + for (var s : spec.getAllJvmArgs()) { + log.print(" "); + log.println(s); + } + log.println("Classpath:"); + for (var f : getClasspath()) { + log.print(" "); + log.println(f.getAbsolutePath()); + } + log.println("===================================="); + }); + } catch (IOException e) { + throw new RuntimeException("Failed to open log file", e); + } + } + + public void args(Object... args) { + this.args(Arrays.asList(args)); + } + + public void args(Iterable args) { + for (var arg : args) { + if (arg instanceof ProviderConvertible providerConvertible) + this.addArgs(providerConvertible.asProvider().map(Object::toString)); + if (arg instanceof Provider provider) + this.addArgs(provider.map(Object::toString)); + else + this.addArgs(this.getProviders().provider(arg::toString)); + } + } + + /// Adds each file to the arguments preceded by the given argument. Designed to work well with + /// JOpt Simple. + /// + /// @param arg The flag to use for each file + /// @param files The files to add + public void args(String arg, Iterable files) { + for (File file : files) + this.args(arg, file); + } + + /// Adds the given argument followed by the given file location to the arguments. + /// + /// @param arg The flag to use + /// @param fileProvider The file to add + public void args(String arg, FileSystemLocationProperty fileProvider) { + this.args(arg, fileProvider.getLocationOnly()); + } + + /// Adds the given argument followed by the given object (may be a file location) to the arguments. + /// + /// @param arg The flag to use + /// @param provider The object (or file) to add + public void args(String arg, @UnknownNullability Provider provider) { + if (provider == null || !provider.isPresent()) return; + + // NOTE: We don't use File#getAbsoluteFile because path sensitivity should be handled by tasks. + var value = provider.map(it -> it instanceof FileSystemLocation ? ((FileSystemLocation) it).getAsFile() : it).getOrNull(); + if (value == null) return; + + if (value instanceof Boolean booleanValue) { + if (booleanValue) + this.args(arg); + } else { + this.args(arg, String.valueOf(value)); + } + } + + /// Adds the given map of arguments. + /// + /// [#args(String, Provider)] will be invoked for each entry in the map. If the key and/or value are not of the + /// required types, they will be automatically converted using [Object#toString()] and + /// [org.gradle.api.provider.ProviderFactory#provider(Callable)]. + /// + /// @param args The args to add + /// @deprecated Too ambiguous with [#args(String, Provider)]. Prefer that method instead. + @Deprecated(forRemoval = true) + public void args(Map args) { + for (Map.Entry entry : args.entrySet()) { + var key = entry.getKey(); + var value = entry.getValue(); + this.args( + key instanceof Provider provider ? provider.map(Object::toString).get() : this.getProviders().provider(key::toString).get(), + value instanceof Provider provider ? (provider instanceof FileSystemLocationProperty file ? file.getLocationOnly() : provider) : this.getProviders().provider(() -> value) + ); + } + } + + public void jvmArgs(Object... args) { + for (var arg : args) { + this.addJvmArgs(this.getProviders().provider(arg::toString)); + } + } + + public void jvmArgs(Iterable args) { + for (var arg : args) { + this.addJvmArgs(this.getProviders().provider(arg::toString)); + } + } + + public void environment(String key, String value) { + this.putEnvironment(key, value); + } + + public void systemProperty(String key, String value) { + this.putSystemProperties(key, value); + } +}