diff --git a/build.gradle b/build.gradle index 2bf7179..ef550b0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,50 +1,66 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.attributes.plugin.GradlePluginApiVersion plugins { - id 'dev.gradleplugins.groovy-gradle-plugin' + id 'java-gradle-plugin' + id 'groovy' id 'idea' id 'eclipse' id 'maven-publish' alias libs.plugins.licenser alias libs.plugins.gradleutils + alias libs.plugins.javadoc.links alias libs.plugins.plugin.publish alias libs.plugins.shadow } final projectDisplayName = 'Forge Gradle Utilities' +final projectArtifactId = base.archivesName = 'gradleutils' description = 'Small collection of utilities for standardizing MinecraftForge gradle scripts' group = 'net.minecraftforge' version = gitversion.tagOffset println "Version: $version" -// Git Version requires Java 17 -java.toolchain.languageVersion = JavaLanguageVersion.of 17 +java { + toolchain.languageVersion = JavaLanguageVersion.of(17) + withSourcesJar() + withJavadocJar() +} configurations { named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME) { // Fixes a conflict between Git Version's shadowed SLF4J from JGit and Gradle's own loggers exclude group: 'org.slf4j', module: 'slf4j-api' } -} -repositories { - maven { url = 'https://maven.minecraftforge.net' } - mavenCentral() + // Applies the "Gradle Plugin API Version" attribute to configuration + // This was added in Gradle 7, gives consumers useful errors if they are on an old version + def applyGradleVersionAttribute = { Configuration configuration -> + configuration.attributes { + attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, objects.named(GradlePluginApiVersion, libs.versions.gradle.get())) + } + } + + // TODO [GradleUtils] Re-enable this after first publish of GradleUtils 3.0.0 +// named('apiElements', applyGradleVersionAttribute) +// named('runtimeElements', applyGradleVersionAttribute) +// named('shadowRuntimeElements', applyGradleVersionAttribute) } dependencies { - // Static Analysis + // Gradle API + compileOnly libs.gradle compileOnly libs.nulls + // JavaDoc Links Plugin + compileOnly libs.gradle.javadoc.links + // GitHub Actions Workflows implementation libs.yaml - // Git Version - implementation libs.gitver - - // Backwards compatibility - implementation libs.jgit + // Tools + implementation libs.bundles.utils } // Removes local Gradle API from compileOnly. This is a workaround for bugged plugins. @@ -58,7 +74,7 @@ afterEvaluate { project -> } license { - header = rootProject.file 'LICENSE-header.txt' + header = rootProject.file('LICENSE-header.txt') newLine = false exclude '**/*.properties' } @@ -68,7 +84,7 @@ tasks.named('jar', Jar) { } tasks.named('shadowJar', ShadowJar) { - enableRelocation = true + enableAutoRelocation = true archiveClassifier = null relocationPrefix = 'net.minecraftforge.gradleutils.shadow' } @@ -77,64 +93,58 @@ tasks.withType(GroovyCompile).configureEach { groovyOptions.optimizationOptions.indy = true } +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { languageVersion = JavaLanguageVersion.of(24) } + + options { StandardJavadocDocletOptions options -> + options.windowTitle = projectDisplayName + project.version + options.tags 'apiNote:a:API Note:', 'implNote:a:Implementation Note:', 'implSpec:a:Implementation Requirements:' + } +} + changelog { fromBase() publishAll = false } gradlePlugin { - website.set gitversion.url - vcsUrl.set gitversion.url + '.git' - - compatibility { - minimumGradleVersion = libs.versions.gradle.get() - } - - groovy { - withSourcesJar() - withGroovydocJar() + website = gitversion.url + vcsUrl = gitversion.url + '.git' + + plugins.register('gradleutils') { + id = 'net.minecraftforge.gradleutils' + implementationClass = 'net.minecraftforge.gradleutils.GradleUtilsPlugin' + displayName = projectDisplayName + description = project.description + tags.set(['minecraftforge']) } +} - plugins { - register('gradleutils') { - id = 'net.minecraftforge.gradleutils' - implementationClass = 'net.minecraftforge.gradleutils.GradleUtilsPlugin' - displayName = projectDisplayName - description = project.description - tags = ['minecraftforge'] - } - register('changelog') { - id = 'net.minecraftforge.changelog' - implementationClass = 'net.minecraftforge.gradleutils.changelog.ChangelogPlugin' - displayName = 'Git Changelog' - description = 'Creates a changelog text file based on git history using Git Version' - tags = ['git', 'changelog'] - } - } +// Allows the thin jar to be published, but won't be considered as the java-runtime variant in the module +// This forces Gradle to use the fat jar when applying the plugin +(components.java as AdhocComponentWithVariants).withVariantsFromConfiguration(configurations.runtimeElements) { + skip() } publishing { publications.register('pluginMaven', MavenPublication) { - artifactId = project.name - + artifactId = projectArtifactId changelog.publish it pom { pom -> name = projectDisplayName description = project.description - gradleutils.pom.setGitHubDetails pom + gradleutils.pom.setGitHubDetails(pom) licenses { license gradleutils.pom.licenses.LGPLv2_1 } - // TODO [GradleUtils][GU3.0] Re-evaluate active developers in GU 3.0 developers { - developer gradleutils.pom.developers.LexManos - developer gradleutils.pom.developers.SizableShrimp - developer gradleutils.pom.developers.Paint_Ninja developer gradleutils.pom.developers.Jonathing + developer gradleutils.pom.developers.Paint_Ninja + developer gradleutils.pom.developers.LexManos } } } diff --git a/gradle.properties b/gradle.properties index 871a237..8d5be5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,9 +2,6 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.configureondemand=true -# TODO [GradleUtils][Gradle9] Re-enable config cache in Gradle 9 -# Configuration Cache causes issues with plugin publishing. -# Do continue to make our Gradle plugins (GU, FG7, etc.) support it though. #org.gradle.configuration-cache=true #org.gradle.configuration-cache.parallel=true diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties deleted file mode 100644 index ae79801..0000000 --- a/gradle/gradle-daemon-jvm.properties +++ /dev/null @@ -1,12 +0,0 @@ -#This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/65aaef917b9f394804f058f1861225c9/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/c728c5388b044fbdbbc44b0c6acee0df/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/65aaef917b9f394804f058f1861225c9/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/c728c5388b044fbdbbc44b0c6acee0df/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/dc463b4a8183dbcaa1b32544189c7f03/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/cb7dc109dd590ebca2d703734d23c9d3/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/65aaef917b9f394804f058f1861225c9/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/c728c5388b044fbdbbc44b0c6acee0df/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/43ee83889b87bacad5d3071ae7bbd349/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/2d57bdd1e17a18f83ff073919daa35ba/redirect -toolchainVersion=17 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca025c8..2a84e18 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index fb91e5e..21f60f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,37 +1,44 @@ -import org.gradle.api.initialization.resolve.RepositoriesMode - plugins { - id 'dev.gradleplugins.gradle-plugin-development' version '1.9.0' id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' } rootProject.name = 'gradleutils' dependencyResolutionManagement { - // Repositories are located in build.gradle for this project - // dev.gradleplugins.groovy-gradle-plugin is bugged and force adds repositories on the project - // so, we can't declare the repositories in here - repositoriesMode = RepositoriesMode.PREFER_PROJECT + repositories { + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://maven.moddinglegacy.com' } // Gradle API + maven { url = 'https://repo.eclipse.org/content/groups/releases/' } + mavenCentral() + //mavenLocal() + } versionCatalogs.register('libs') { - plugin 'licenser', 'net.minecraftforge.licenser' version '1.2.0' - plugin 'gradleutils', 'net.minecraftforge.gradleutils' version '2.5.1' - plugin 'plugin-publish', 'com.gradle.plugin-publish' version '1.3.1' - plugin 'shadow', 'com.gradleup.shadow' version '9.0.0-beta13' + version 'gradle-javadoc-links', '8.14' + + plugin 'licenser', 'net.minecraftforge.licenser' version '1.2.0' // https://plugins.gradle.org/plugin/net.minecraftforge.licenser + plugin 'gradleutils', 'net.minecraftforge.gradleutils' version '2.6.0' // https://plugins.gradle.org/plugin/net.minecraftforge.gradleutils + plugin 'javadoc-links', 'io.freefair.javadoc-links' versionRef 'gradle-javadoc-links' // https://plugins.gradle.org/plugin/io.freefair.javadoc-links + plugin 'plugin-publish', 'com.gradle.plugin-publish' version '1.3.1' // https://plugins.gradle.org/plugin/com.gradle.plugin-publish + plugin 'shadow', 'com.gradleup.shadow' version '9.0.0-rc3' // https://plugins.gradle.org/plugin/com.gradleup.shadow // Gradle API - version 'gradle', '7.3' + // TODO [ForgeGradle][FG7][Gradle Api] REMOVE once Gradle publish their own API artifacts + // Original: https://github.com/remal-gradle-api/packages/packages/760197?version=9.0.0 + // Mirror: https://repos.moddinglegacy.com/#/modding-legacy/name/remal/gradle-api/gradle-api/9.0.0 + version 'gradle', '9.0.0' + library 'gradle', 'name.remal.gradle-api', 'gradle-api' versionRef 'gradle' + library 'nulls', 'org.jetbrains', 'annotations' version '26.0.2' - // Static Analysis - library 'nulls', 'org.jetbrains', 'annotations' version '26.0.2' + // JavaDoc Links Plugin + library 'gradle-javadoc-links', 'io.freefair.javadoc-links', 'io.freefair.javadoc-links.gradle.plugin' versionRef 'gradle-javadoc-links' // GitHub Actions Workflows library 'yaml', 'org.yaml', 'snakeyaml' version '2.4' - // Git Version - library 'gitver', 'net.minecraftforge', 'gitversion' version '0.5.2' - - // Backwards compatibility - library 'jgit', 'org.eclipse.jgit', 'org.eclipse.jgit' version '7.2.0.202503040940-r' + // Tools + library 'utils-hash', 'net.minecraftforge', 'hash-utils' version '0.1.9' // https://files.minecraftforge.net/net/minecraftforge/hash-utils/index.html + library 'utils-download', 'net.minecraftforge', 'download-utils' version '0.3.1' // https://files.minecraftforge.net/net/minecraftforge/download-utils/index.html + bundle 'utils', ['utils-hash', 'utils-download'] } } diff --git a/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.groovy b/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.groovy deleted file mode 100644 index 83ff14d..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.groovy +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileStatic -import org.gradle.api.DefaultTask -import org.gradle.api.provider.Property -import org.gradle.api.provider.ProviderFactory -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.TaskAction - -import javax.inject.Inject - -/** - * This task prints the marker lines into the log which configure the pipeline. - * - * @deprecated Will be removed once Forge moves off of TeamCity. - */ -@CompileStatic -@Deprecated(forRemoval = true) -@SuppressWarnings('GrDeprecatedAPIUsage') -abstract class ConfigureTeamCity extends DefaultTask { - public static final String NAME = 'configureTeamCity' - - @Inject - ConfigureTeamCity(ProviderFactory providers) { - this.description = 'Prints the marker lines into the log which configure the pipeline. [deprecated]' - this.onlyIf/*('Only runs on TeamCity, so the TEAMCITY_VERSION environment variable must be set.')*/ { - providers.environmentVariable('TEAMCITY_VERSION').present - } - - this.buildNumber.convention providers.provider { this.project.version?.toString() } - } - - /** The build number to print, usually the project version. */ - abstract @Input Property getBuildNumber() - - @TaskAction - void exec() { - this.logger.warn 'WARNING: Usage of TeamCity is deprecated within Minecraft Forge Minecraft Forge has been gradually moving off of TeamCity and into GitHub Actions. When the migration is fully complete, this task along with its automatic setup will be removed.' - - final buildNumber = this.buildNumber.get() - - this.logger.lifecycle 'Setting project variables and parameters.' - println "##teamcity[buildNumber '$buildNumber']" - println "##teamcity[setParameter name='env.PUBLISHED_JAVA_ARTIFACT_VERSION' value='$buildNumber']" - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.java b/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.java new file mode 100644 index 0000000..cb8d82e --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/ConfigureTeamCity.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils; + +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; + +// TODO [GradleUtils][TeamCity] Delete this when off of TeamCity +@Deprecated(forRemoval = true) +abstract class ConfigureTeamCity extends DefaultTask implements HasPublicType { + static final String NAME = "configureTeamCity"; + + @Override + public TypeOf getPublicType() { + // We don't want this task to be configurable, so tell Gradle it's just a default task + return TypeOf.typeOf(DefaultTask.class); + } + + @Inject + public ConfigureTeamCity(ProviderFactory providers) { + this.setGroup("Build Setup"); + this.setDescription("Prints the marker lines into the log which configure the pipeline. [deprecated]"); + this.onlyIf("Only runs on TeamCity, so the TEAMCITY_VERSION environment variable must be set.", task -> providers.environmentVariable("TEAMCITY_VERSION").isPresent()); + + this.getBuildNumber().convention(providers.provider(() -> this.getProject().getVersion()).map(Object::toString)); + } + + /** The build number to print, usually the project version. */ + protected abstract @Input Property getBuildNumber(); + + @TaskAction + public void exec() { + this.getLogger().warn("WARNING: Usage of TeamCity is deprecated within Minecraft Forge Minecraft Forge has been gradually moving off of TeamCity and into GitHub Actions. When the migration is fully complete, this task along with its automatic setup will be removed."); + + final var buildNumber = this.getBuildNumber().get(); + this.getLogger().lifecycle("Setting project variables and parameters."); + System.out.printf("##teamcity[buildNumber '%s']%n", buildNumber); + System.out.printf("##teamcity[setParameter name='env.PUBLISHED_JAVA_ARTIFACT_VERSION' value='%s']%n", buildNumber); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/Constants.java b/src/main/groovy/net/minecraftforge/gradleutils/Constants.java new file mode 100644 index 0000000..47691f7 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/Constants.java @@ -0,0 +1,10 @@ +package net.minecraftforge.gradleutils; + +final class Constants { + static final String FORGE_MAVEN = "https://maven.minecraftforge.net/"; + static final String FORGE_MAVEN_RELEASE = FORGE_MAVEN + "releases"; + static final String MC_LIBS_MAVEN = "https://libraries.minecraft.net/"; + + static final String FORGE_ORG_NAME = "Forge Development LLC"; + static final String FORGE_ORG_URL = "https://minecraftforge.net"; +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.groovy deleted file mode 100644 index 8b856e1..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.groovy +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileDynamic -import groovy.transform.CompileStatic -import net.minecraftforge.gradleutils.gitversion.GitVersionExtension -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import org.gradle.api.provider.ProviderFactory -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.yaml.snakeyaml.DumperOptions -import org.yaml.snakeyaml.Yaml - -import javax.inject.Inject -import java.nio.charset.StandardCharsets -import java.nio.file.Files - -/** - * This task generates the GitHub Actions workflow file for the project, respecting declared subprojects in Git Version. - *

This can be very useful when creating new projects or subprojects.

- */ -@CompileStatic -abstract class GenerateActionsWorkflow extends DefaultTask { - public static final String NAME = 'generateActionsWorkflow' - - @Inject - GenerateActionsWorkflow(ProviderFactory providers) { - this.description = 'Generates the GitHub Actions workflow file for the project, respecting declared subprojects in Git Version.' - - this.projectName.convention providers.provider { this.project.name } - this.branch.convention providers.provider { this.project.extensions.getByType(GitVersionExtension).info.branch } - this.localPath.convention this.project.extensions.getByType(GitVersionExtension).projectPath - this.paths.convention providers.provider { this.project.extensions.getByType(GitVersionExtension).subprojectPaths.get().collect { "!${it}/**".toString() } } - this.gradleJavaVersion.convention 21 - this.sharedActionsBranch.convention 'v0' - - this.outputFile.convention this.project.rootProject.layout.projectDirectory.file(providers.provider { "build_${this.project.name}.yaml" }) - } - - abstract @OutputFile RegularFileProperty getOutputFile() - - abstract @Input Property getProjectName() - abstract @Input @Optional Property getBranch() - abstract @Input @Optional Property getLocalPath() - abstract @Input @Optional ListProperty getPaths() - abstract @Input Property getGradleJavaVersion() - abstract @Input Property getSharedActionsBranch() - - @CompileDynamic - private List getPathsResolved() { - this.paths.getOrElse(List.of()) - } - - @TaskAction - void exec() throws IOException { - var localPath = this.localPath.orNull - var paths = this.pathsResolved - - var push = [ - 'branches': this.branch.getOrElse('master'), - 'paths' : new ArrayList().tap { - if (localPath) add(localPath + '/**') - - add('!.github/workflows/**') - add('!settings.gradle') - addAll(paths) - } - ] as Map - - var taskPrefix = localPath ? ":${localPath}:" : '' - var with = [ - 'java' : this.gradleJavaVersion.get(), - 'gradle_tasks': "${taskPrefix}check ${taskPrefix}publish".toString() - ] as Map - if (localPath) with.put('subproject', localPath) - - Map yaml = [ - 'name' : "Build ${this.projectName.get()}", - 'on' : ['push': push], - 'permissions': ['contents': 'read'], - 'jobs' : [ - 'build': [ - 'uses' : "MinecraftForge/SharedActions/.github/workflows/gradle.yml@${this.sharedActionsBranch.get()}".toString(), - 'with' : with, - 'secrets': [ - 'DISCORD_WEBHOOK': '${{ secrets.DISCORD_WEBHOOK }}' - ] - ] - ] - ] - var workflow = new Yaml( - new DumperOptions().tap { - explicitStart = false - defaultFlowStyle = DumperOptions.FlowStyle.BLOCK - prettyFlow = true - } - ).dump(yaml).replace("'on':", 'on:') - - var file = outputFile.asFile.get() - if (!file.parentFile.exists() && !file.parentFile.mkdirs()) - throw new IllegalStateException('Failed to create directories for output!') - - Files.writeString( - file.toPath(), - workflow, - StandardCharsets.UTF_8 - ) - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.java b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.java new file mode 100644 index 0000000..0aaa0e2 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflow.java @@ -0,0 +1,76 @@ +package net.minecraftforge.gradleutils; + +import org.gradle.api.Task; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; + +/// This task generates the GitHub Actions workflow file for the project. +/// +/// If the project is also using the Git Version plugin (currently auto-applied by GradleUtils), it will respect any +/// declared subprojects. +/// +/// @implNote See [GenerateActionsWorkflowImpl] +public sealed interface GenerateActionsWorkflow extends Task permits GenerateActionsWorkflowInternal { + /// The name for this task. + /// + /// Each [project][org.gradle.api.Project] should only have one of this type of task and it must be named this. + String NAME = "generateActionsWorkflow"; + + //@formatter:off -- newline breaks the formatting for the default location in JavaDoc + /** + * The output file for this task. + *

Default: + * {@link org.gradle.api.Project#getRootProject() rootProject}{@code /.github/workflows/publish_}{@link org.gradle.api.Project#getName() name}{@code .yaml}

+ * + * @return The property for the output file + */ + @OutputFile RegularFileProperty getOutputFile(); + //@formatter:on + + /// The project name to use in the workflow file. + /// + /// Default: [org.gradle.api.Project#getName()] + /// + /// @return The property for the project name + @Input Property getProjectName(); + + /// The branch name to use in the workflow file. + /// + /// Default: Automatically detected by Git Version, otherwise `master` + /// + /// @return The property for the branch name + /// @implNote See [GenerateActionsWorkflowImpl#DEFAULT_BRANCH] + @Optional @Input Property getBranch(); + + /// The local path from the [root project][org.gradle.api.Project#getRootProject()] to the current project to use in + /// the workflow file. This local path is not used to invoke Gradle but rather for Git Version. + /// + /// @return The property for the local path + @Optional @Input Property getLocalPath(); + + /// The paths to pass into the workflow file. Prepend a path with `!` to ignore it instead of include it. The Git + /// Version plugin will automatically add declared subproject paths to exclude. + /// + /// @return The property for the paths + @Optional @Input ListProperty getPaths(); + + /// The Java version to invoke Gradle with. + /// + /// Default: The project's toolchain version, or `17` if it is lower than that. + /// + /// @return The property for the Gradle Java version + /// @implNote See [GenerateActionsWorkflowImpl#DEFAULT_GRADLE_JAVA] + @Input Property getGradleJavaVersion(); + + /// The Shared Actions branch to use with this workflow. + /// + /// Default: `v0` + /// + /// @return The property for the Shared Actions branch + /// @implNote See [GenerateActionsWorkflowImpl#DEFAULT_SHARED_ACTIONS_BRANCH] + @Input Property getSharedActionsBranch(); +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowImpl.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowImpl.groovy new file mode 100644 index 0000000..4333802 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowImpl.groovy @@ -0,0 +1,142 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.DefaultTask +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml + +import javax.inject.Inject +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path + +@CompileStatic +@PackageScope abstract class GenerateActionsWorkflowImpl extends DefaultTask implements GenerateActionsWorkflowInternal { + private static final String DEFAULT_BRANCH = 'master' + private static final int DEFAULT_GRADLE_JAVA = 17 + private static final String DEFAULT_SHARED_ACTIONS_BRANCH = 'v0' + + private final GradleUtilsProblems problems + + protected abstract @Inject ObjectFactory getObjects() + protected abstract @Inject ProviderFactory getProviders() + protected abstract @Inject ProjectLayout getProjectLayout() + + GenerateActionsWorkflowImpl() { + this.problems = this.objects.newInstance(GradleUtilsProblems) + + this.group = 'Build Setup' + this.description = 'Generates the GitHub Actions workflow file for the project, respecting declared subprojects in Git Version.' + + final rootDirectory = this.project.rootProject.layout.projectDirectory + + this.projectName.convention(this.providers.provider { this.project.name }) + this.branch.convention(DEFAULT_BRANCH) + this.localPath.convention(this.providers.provider { getRelativePath(rootDirectory, this.projectLayout.projectDirectory) }) + this.gradleJavaVersion.convention(this.project.extensions.getByType(JavaPluginExtension).toolchain.languageVersion.map { it.canCompileOrRun(DEFAULT_GRADLE_JAVA) ? it.asInt() : DEFAULT_GRADLE_JAVA }) + this.sharedActionsBranch.convention(DEFAULT_SHARED_ACTIONS_BRANCH) + + this.outputFile.convention(rootDirectory.dir('.github/workflows').file(this.projectName.map { "publish_${it}.yaml" })) + this.gitVersionPresent.convention(providers.provider { this.project.pluginManager.hasPlugin('net.minecraftforge.gitversion') }) + } + + private static String getRelativePath(FileSystemLocation root, FileSystemLocation file) { + getRelativePath(root.asFile, file.asFile) + } + + private static String getRelativePath(File root, File file) { + return root == file ? '' : getRelativePath(root.toPath(), file.toPath()) + } + + private static String getRelativePath(Path root, Path path) { + return root.relativize(path).toString().replace(root.fileSystem.separator, '/') + } + + @Override abstract @OutputFile RegularFileProperty getOutputFile() + protected abstract @Input Property getGitVersionPresent() + + @Override abstract @Input Property getProjectName() + @Override abstract @Optional @Input Property getBranch() + @Override abstract @Optional @Input Property getLocalPath() + @Override abstract @Optional @Input ListProperty getPaths() + @Override abstract @Input Property getGradleJavaVersion() + @Override abstract @Input Property getSharedActionsBranch() + + @TaskAction + void exec() throws IOException { + if (!this.gitVersionPresent.getOrElse(false)) { + this.logger.warn('WARNING: {} output file will be missing key data. See Problems report for details.', this.name) + this.problems.ghWorkflowGitVersionMissing(this.name) + } + + var localPath = this.localPath.orNull + var paths = this.paths.getOrElse(List.of()) + + var push = [ + 'branches': this.branch.get(), + 'paths' : new ArrayList().tap { + if (localPath) add(localPath + '/**') + + add('!.github/workflows/**') + add('!settings.gradle') + addAll(paths) + } + ] as Map + + var taskPrefix = localPath ? ":${localPath}:" : '' + var with = [ + 'java' : this.gradleJavaVersion.get(), + 'gradle_tasks': "${taskPrefix}check ${taskPrefix}publish".toString() + ] as Map + if (localPath) with.put('subproject', localPath) + + Map yaml = [ + 'name' : "Build ${this.projectName.get()}", + 'on' : ['push': push], + 'permissions': ['contents': 'read'], + 'jobs' : [ + 'build': [ + 'uses' : "MinecraftForge/SharedActions/.github/workflows/gradle.yml@${this.sharedActionsBranch.get()}".toString(), + 'with' : with, + 'secrets': [ + 'DISCORD_WEBHOOK': '${{ secrets.DISCORD_WEBHOOK }}' + ] + ] + ] + ] + var workflow = new Yaml( + new DumperOptions().tap { + explicitStart = false + defaultFlowStyle = DumperOptions.FlowStyle.BLOCK + prettyFlow = true + } + ).dump(yaml).replace("'on':", 'on:') + + var file = outputFile.asFile.get() + if (!file.parentFile.exists() && !file.parentFile.mkdirs()) + throw new IllegalStateException() + + Files.writeString( + file.toPath(), + workflow, + StandardCharsets.UTF_8 + ) + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowInternal.java b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowInternal.java new file mode 100644 index 0000000..ad8895a --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GenerateActionsWorkflowInternal.java @@ -0,0 +1,11 @@ +package net.minecraftforge.gradleutils; + +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; + +non-sealed interface GenerateActionsWorkflowInternal extends GenerateActionsWorkflow, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(GenerateActionsWorkflow.class); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtils.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtils.groovy deleted file mode 100644 index 84bc3d4..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtils.groovy +++ /dev/null @@ -1,425 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileDynamic -import groovy.transform.CompileStatic -import net.minecraftforge.gitver.api.GitVersion -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.api.errors.GitAPIException -import org.eclipse.jgit.lib.Config -import org.eclipse.jgit.storage.file.FileBasedConfig -import org.eclipse.jgit.transport.RemoteConfig -import org.eclipse.jgit.util.FS -import org.eclipse.jgit.util.SystemReader -import org.gradle.api.Action -import org.gradle.api.Project -import org.gradle.api.artifacts.repositories.MavenArtifactRepository -import org.gradle.authentication.http.BasicAuthentication -import org.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.Nullable - -/** - * Utility methods, usually for GradleUtils itself and is often delegated to from the extension. - * - * @see GradleUtilsExtension - */ -@CompileStatic -@Deprecated(forRemoval = true, since = '2.5') -@ApiStatus.ScheduledForRemoval(inVersion = '3.0') -class GradleUtils { - static void ensureAfterEvaluate(Project project, Action action) { - if (project.state.executed) - action.execute project - else - project.afterEvaluate action - } - - //@formatter:off - @CompileDynamic - private static void initDynamic() { String.metaClass.rsplit = GradleUtils.&rsplit } - static { initDynamic() } - //@formatter:on - - private static boolean rsplitDeprecationLogged - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static @Nullable List rsplit(@Nullable String input, String del, int limit = -1) { - if (!rsplitDeprecationLogged) { - println 'WARNING: Usage of GradleUtils/String.rsplit is DEPRECATED and will be removed in GradleUtils 3.0!' - rsplitDeprecationLogged = true - } - - rsplitInternal input, del, limit - } - - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - private static @Nullable List rsplitInternal(@Nullable String input, String del, int limit = -1) { - if (input === null) return null - List lst = [] - int x = 0 - int idx - String tmp = input - while ((idx = tmp.lastIndexOf(del)) !== -1 && (limit === -1 || x++ < limit)) { - lst.add(0, tmp.substring(idx + del.length(), tmp.length())) - tmp = tmp.substring(0, idx) - } - lst.add(0, tmp) - return lst - } - - /** @deprecated Use {@link GitVersion#disableSystemConfig() */ - @Deprecated(forRemoval = true, since = '2.4') - @CompileStatic - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static class DisableSystemConfig extends SystemReader.Delegate { - final SystemReader parent - - DisableSystemConfig(SystemReader parent) { - super(parent) - this.parent = parent - - println 'WARNING: Usage of GradleUtils.DisableSystemConfig is DEPRECATED and will be removed in GradleUtils 3.0! Consider using GitVersion.disableSystemConfig() instead.' - } - - @Override - FileBasedConfig openSystemConfig(Config parent, FS fs) { - new FileBasedConfig(parent, null, fs) { - @Override void load() {} - - @Override boolean isOutdated() { false } - } - } - } - - private static boolean gitInfoDeprecationLogged - /** @deprecated Use {@link GitVersion#getInfo()} via {@link net.minecraftforge.gradleutils.gitversion.GitVersionExtension#getVersion() GitVersionExtension.getVersion()} */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static Map gitInfo(File dir, String... globFilters) { - if (!gitInfoDeprecationLogged) { - println 'WARNING: Usage of GradleUtils.gitInfo(File, String...) is DEPRECATED and will be removed in GradleUtils 3.0! Consider using GitVersion.disableSystemConfig() instead.' - gitInfoDeprecationLogged = true - } - - try (var version = GitVersion.builder().project(dir).strict(false).build()) { - [ - dir : version.gitDir.absolutePath, - tag : version.info.tag, - offset : version.info.offset, - hash : version.info.hash, - branch : version.info.branch, - commit : version.info.commit, - abbreviatedId: version.info.abbreviatedId, - url : version.url - ].tap { it.removeAll { it.value == null } } - } - } - - /** - * Get a configuring action to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * publishing block. - * - * Important: The following environment variables must be set for this to work: - *
    - *
  • {@code MAVEN_USER}: Containing the username to use for authentication
  • - *
  • {@code MAVEN_PASSWORD}: Containing the password to use for authentication
  • - *
  • {@code MAVEN_URL_RELEASE}: Containing the URL to use for the release repository
  • - *
  • {@code MAVEN_URL_SNAPSHOT}: Containing the URL to use for the snapshot repository
  • - *
- * - * @param project The project to setup publishing for - * @param defaultFolder The default folder if the required maven information is not set - * @return The action - */ - static Action getPublishingForgeMaven(Project project, File defaultFolder = project.rootProject.file('repo')) { - setupSnapshotCompatiblePublishing(project, 'https://maven.minecraftforge.net/releases', defaultFolder) - } - - /** - * Get a configuring action to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * publishing block. This action respects the current project's version, with regards to publishing to a release or - * snapshot repository. - *

- * Important: The following environment variables must be set for this to work: - *

    - *
  • {@code MAVEN_USER}: Containing the username to use for authentication
  • - *
  • {@code MAVEN_PASSWORD}: Containing the password to use for authentication
  • - *
- *

- * The following environment variables are optional: - *

    - *
  • {@code MAVEN_URL_RELEASE}: Containing the URL to use for the release repository
  • - *
  • {@code MAVEN_URL_SNAPSHOT}: Containing the URL to use for the snapshot repository
  • - *
- *

- * If the {@code MAVEN_URL_RELEASE} variable is not set, the passed in fallback URL will be used for the release - * repository (by default, this is {@code https://maven.minecraftforge.net/}). This is done to preserve backwards - * compatibility with the old {@link #getPublishingForgeMaven(Project, File)} method. - * - * @param project The project to setup publishing for - * @param defaultFolder The default folder if the required maven information is not set - * @return The action - */ - static Action setupSnapshotCompatiblePublishing(Project project, String fallbackPublishingEndpoint = 'https://maven.minecraftforge.net/releases', File defaultFolder = project.rootProject.file('repo'), File defaultSnapshotFolder = project.rootProject.file('snapshots')) { - // make properties of what we use so gradle's cache is aware - final snapshot = project.objects.property(Boolean).value project.providers.provider { - project.version?.toString()?.endsWith('-SNAPSHOT') - } - - // collecting all of our environment variables here so gradle's cache is aware - final mavenUser = project.providers.environmentVariable 'MAVEN_USER' - final mavenPassword = project.providers.environmentVariable 'MAVEN_PASSWORD' - final mavenUrlRelease = project.providers.environmentVariable 'MAVEN_URL_RELEASE' - final mavenUrlSnapshots = project.providers.environmentVariable 'MAVEN_URL_SNAPSHOTS' - - return { MavenArtifactRepository repo -> - repo.name = 'forge' - - if (mavenUser.present && mavenPassword.present) { - var publishingEndpoint = mavenUrlRelease.present ? mavenUrlRelease.get() : fallbackPublishingEndpoint - - repo.url = snapshot.getOrElse(false) && mavenUrlSnapshots.present - ? mavenUrlSnapshots.get() - : publishingEndpoint - - repo.authentication { authentication -> - authentication.create('basic', BasicAuthentication) - } - - repo.credentials { credentials -> - credentials.username = mavenUser.get() - credentials.password = mavenPassword.get() - } - } else { - repo.url = snapshot.getOrElse(false) - ? defaultSnapshotFolder.absoluteFile.toURI() - : defaultFolder.absoluteFile.toURI() - } - } as Action - } - - /** - * Get a configuring action for the Forge maven to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * repositories block. - * - * @return The action - */ - static Closure getForgeMaven() { - { MavenArtifactRepository repo -> - repo.name = 'MinecraftForge' - repo.url = 'https://maven.minecraftforge.net/' - } - } - - /** - * Get a configuring action for the Forge releases maven to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * repositories block. - * - * @return The action - */ - static Closure getForgeReleaseMaven() { - { MavenArtifactRepository repo -> - repo.name = 'MinecraftForge releases' - repo.url = 'https://maven.minecraftforge.net/releases' - } - } - - /** - * Get a configuring action for the Forge snapshots maven to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * repositories block. - * - * @return The action - */ - static Closure getForgeSnapshotMaven() { - { MavenArtifactRepository repo -> - repo.name = 'MinecraftForge snapshots' - repo.url = 'https://maven.minecraftforge.net/snapshots' - } - } - - /** - * Get a configuring action for the Minecraft libraries maven to be passed into - * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Action) RepositoryHandler.maven(Action)} in a - * repositories block. - * - * @return The action - */ - static Closure getMinecraftLibsMaven() { - { MavenArtifactRepository repo -> - repo.name = 'Minecraft libraries' - repo.url = 'https://libraries.minecraft.net/' - } - } - - /** - * Returns a version in the form {@code $tag.$offset}, e.g. 1.0.5 - * - * @param info A git info object generated from {@code #gitInfo} - * @return a version in the form {@code $tag.$offset}, e.g. 1.0.5 - * @deprecated Use {@link GitVersion#getTagOffset()} instead - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getTagOffsetVersion(Map info) { - "${info.tag}.${info.offset}" - } - - /** @deprecated Filters can no longer be defined at configuration. Use the Git Version config. */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getFilteredTagOffsetVersion(Map info, boolean prefix = false, String filter) { - getTagOffsetVersion info - } - - /** - * Returns a version in the form {@code $tag.$offset}, optionally with the branch appended if it is not in the - * defined list of allowed branches - * - * @param info A git info object generated from {@link #gitInfo(File, String ...)} - * @param allowedBranches A list of allowed branches; the current branch is appended if not in this list - * @return a version in the form {@code $tag.$offset} or {@code $tag.$offset-$branch} - * @deprecated Use {@link GitVersion#getTagOffsetBranch(String ...)} via {@link net.minecraftforge.gradleutils.gitversion.GitVersionExtension#getVersion() GitVersionExtension.getVersion()} - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getTagOffsetBranchVersion(Map info, String... allowedBranches) { - if (!allowedBranches || allowedBranches.length === 0) - allowedBranches = [null, 'master', 'main', 'HEAD'] - final version = getTagOffsetVersion(info) - String branch = info.branch - if (branch?.startsWith('pulls/')) - branch = 'pr' + rsplitInternal(branch, '/', 1)[1] - branch = branch?.replaceAll(/[\\\/]/, '-') - return branch in allowedBranches ? version : "$version-${branch}" - } - - /** @deprecated Filters can no longer be defined at configuration. Use the Git Version config. */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getFilteredTagOffsetBranchVersion(Map info, boolean prefix = false, String filter, String... allowedBranches) { - getTagOffsetBranchVersion info, allowedBranches - } - - /** - * Returns a version in the form {@code $mcVersion-$tag.$offset}, optionally with - * the branch appended if it is not in the defined list of allowed branches - * - * @param info A git info object generated from {@code #gitInfo} - * @param mcVersion The current minecraft version - * @param allowedBranches A list of allowed branches; the current branch is appended if not in this list - * @return a version in the form {@code $mcVersion-$tag.$offset} or {@code $mcVersion-$tag.$offset-$branch} - * @deprecated Use {@link GitVersion#getMCTagOffsetBranch(String, String ...)} instead - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getMCTagOffsetBranchVersion(Map info, String mcVersion, String... allowedBranches) { - if (!allowedBranches || allowedBranches.length === 0) - allowedBranches = [null, 'master', 'main', 'HEAD', mcVersion, mcVersion + '.0', mcVersion + '.x', rsplitInternal(mcVersion, '.', 1)[0] + '.x'] - - "$mcVersion-${getTagOffsetBranchVersion(info, allowedBranches)}" - } - - /** @deprecated Filters for GitVersion should be set early, using one of the methods in {@link GradleUtilsExtension} */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String getFilteredMCTagOffsetBranchVersion(Map info, boolean prefix = false, String filter, String mcVersion, String... allowedBranches) { - getMCTagOffsetBranchVersion info, mcVersion, allowedBranches - } - - /** @see net.minecraftforge.gitver.internal.GitUtils#buildProjectUrl(String) GitUtils.buildProjectUrl(String) */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String buildProjectUrl(String project) { - buildProjectUrl "MinecraftForge", project - } - - /** @see net.minecraftforge.gitver.internal.GitUtils#buildProjectUrl(String, String) GitUtils.buildProjectUrl(String, String) */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String buildProjectUrl(String organization, String project) { - buildProjectUrlLogDeprecation() - "https://github.com/${organization}/${project}" - } - - /** - * Identical to - * {@link net.minecraftforge.gitver.internal.GitUtils#buildProjectUrl(Git) GitUtils.buildProjectUrl(Git)}. The only - * difference is that this does not return {@code null} to preserve GradleUtils 2.x behavior. - * - * @deprecated Replaced by GitVersion, use {@link GitVersion#getUrl()} via {@link net.minecraftforge.gradleutils.gitversion.GitVersionExtension#getVersion() GitVersionExtension.getVersion()} - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - static String buildProjectUrl(Git git) { - buildProjectUrlLogDeprecation() - - List remotes - try { - remotes = git.remoteList().call() - if (remotes.isEmpty()) return '' - } catch (GitAPIException ignored) { - return '' - } - - //Get the origin remote. - var originRemote = - remotes.find { // First try finding the remote that has MinecraftForge - remote -> remote.URIs.find { it.toString().contains('MinecraftForge/') } - } ?: remotes.find { // Ok, just get the origin then - remote -> remote.name == 'origin' - } ?: remotes.first() // No origin? Get whatever we can get our hands on - - var originUrls = originRemote.getURIs() - if (originUrls.empty) return '' - - //Grab its string representation and process. - var originUrlString = originUrls.first().toString() - //Determine the protocol - if (originUrlString.lastIndexOf(':') > 'https://'.length()) { - //If ssh then check for authentication data. - if (originUrlString.contains('@')) { - //We have authentication data: Strip it. - return 'https://' + originUrlString.substring(originUrlString.indexOf('@') + 1).replace('.git', '').replace(':', '/') - } else { - //No authentication data: Switch to https. - return 'https://' + originUrlString.replace('ssh://', '').replace('.git', '').replace(':', '/') - } - } else if (originUrlString.startsWith('http')) { - //Standard http protocol: Strip the '.git' ending only. - return originUrlString.replace('.git', '') - } - - //What other case exists? Just to be sure lets return this. - return originUrlString - } - - private static boolean buildProjectUrlDeprecationLogged - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - private static void buildProjectUrlLogDeprecation() { - if (!buildProjectUrlDeprecationLogged) { - println 'WARNING: Usage of GradleUtils.buildProjectUrl is DEPRECATED and will be removed in GradleUtils 3.0! Use gitversion.url instead.' - buildProjectUrlDeprecationLogged = true - } - } - - /** - * Configures CI related tasks for TeamCity. - * - * @param project The project to configure TeamCity tasks for - * @deprecated Once Forge has completely moved off of TeamCity, this will be deleted. New tasks added to GradleUtils should handle registration themselves. - */ - @Deprecated(forRemoval = true) - static void setupCITasks(Project project) { - project.tasks.register ConfigureTeamCity.NAME, ConfigureTeamCity - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.groovy deleted file mode 100644 index 969ec48..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.groovy +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileStatic -import net.minecraftforge.gradleutils.gitversion.GitVersionExtension -import org.gradle.api.Action -import org.gradle.api.Project -import org.gradle.api.artifacts.repositories.MavenArtifactRepository -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ProviderFactory -import org.jetbrains.annotations.ApiStatus - -import javax.inject.Inject - -/** - * The heart of the GradleUtils library. This class can be directly accessed from buildscripts that have the - * {@linkplain GradleUtilsPlugin plugin} applied using {@code gradleutils}. - */ -@CompileStatic -class GradleUtilsExtension { - public static final String NAME = 'gradleutils' - - private final Project project - private final ObjectFactory objects - private final ProviderFactory providers - - private final GitVersionExtension gitversion - - /** Holds a project-aware Pom utilities class, useful for configuring repositories and publishing. */ - public final PomUtils pom - - /** @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getRoot() GitVersion.getRoot()} via {@link GitVersionExtension#getVersion()} instead. */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - @Lazy DirectoryProperty gitRoot = { - this.project.logger.warn "WARNING: This project is still using 'gradleutils.gitRoot'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'gitversion.rootDir' instead." - - this.gitversion.rootDir - }() - /** @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getInfo() GitVersion.getInfo()} via {@link GitVersionExtension#getVersion()} instead. */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - @Lazy Map gitInfo = { - this.project.logger.warn "WARNING: This project is still using 'gradleutils.gitInfo'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'gitversion.info' instead." - - var gitversion = this.project.extensions.getByType(GitVersionExtension) - var info = gitversion.info - [ - dir : gitversion.gitDir.get().asFile.absolutePath, - tag : info.tag, - offset : info.offset, - hash : info.hash, - branch : info.branch, - commit : info.commit, - abbreviatedId: info.abbreviatedId, - url : gitversion.url - ].tap { it.removeAll { it.value == null } } - }() - - /** @deprecated This constructor will be made package-private in GradleUtils 3.0 */ - @Inject - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - GradleUtilsExtension(Project project, ObjectFactory objects, ProviderFactory providers) { - this.project = project - this.objects = objects - this.providers = providers - - // Git Version - this.gitversion = project.extensions.getByType(GitVersionExtension) - - // Pom Utils - this.pom = new PomUtils(this.gitversion) - - // Tasks - this.project.tasks.register GenerateActionsWorkflow.NAME, GenerateActionsWorkflow - GradleUtils.setupCITasks this.project - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *


-     *     // Before:
-     *     version = gradleutils.tagOffsetVersion
-     *
-     *     // After:
-     *     version = gitversion.tagOffset
-     * 
- * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getTagOffset() GitVersion.tagOffset} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getTagOffsetVersion() { - this.logDeprecation('tagOffsetVersion', 'tagOffsetVersion') - this.project.extensions.getByType(GitVersionExtension).tagOffset - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *

-     *     // Before:
-     *     version = gradleutils.tagOffsetVersion
-     *
-     *     // After:
-     *     version = gitversion.tagOffset
-     * 
- * You must declare your filters in the Git Version config file!. - * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getTagOffset() GitVersion.tagOffset} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getFilteredTagOffsetVersion(boolean prefix = false, String filter) { - this.updateInfo(prefix, filter) - this.tagOffsetVersion - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *

-     *     // Before:
-     *     version = gradleutils.getTagOffsetBranchVersion()
-     *
-     *     // After:
-     *     version = gitversion.tagOffsetBranch
-     * 
- * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getTagOffsetBranch(String ...) GitVersion.getTagOffsetBranch(String...)} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getTagOffsetBranchVersion(String... allowedBranches) { - this.logDeprecation('tagOffsetBranchVersion', 'getTagOffsetBranchVersion(String...)') - var gitversion = this.project.extensions.getByType(GitVersionExtension) - allowedBranches ? gitversion.getTagOffsetBranch(allowedBranches) : gitversion.tagOffsetBranch - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *

-     *     // Before:
-     *     version = gradleutils.tagOffsetBranchVersion
-     *
-     *     // After:
-     *     version = gitversion.tagOffsetBranch
-     * 
- *

- * You must declare your filters in the Git Version config file!. - * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getTagOffset() GitVersion.tagOffset} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getFilteredTagOffsetBranchVersion(boolean prefix = false, String filter, String... allowedBranches) { - this.updateInfo(prefix, filter) - this.getTagOffsetBranchVersion(allowedBranches) - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *


-     *     // Before:
-     *     version = gradleutils.getMCTagOffsetBranchVersion('1.21.4')
-     *
-     *     // After:
-     *     version = gitversion.getMCTagOffsetBranch('1.21.4')
-     * 
- * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getMCTagOffsetBranch(String, String ...) GitVersion.getMCTagOffsetBranch(String, String...)} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getMCTagOffsetBranchVersion(String mcVersion, String... allowedBranches) { - this.logDeprecation('MCTagOffsetBranchVersion', 'getMCTagOffsetBranchVersion(String, String...)') - var gitVersion = this.project.extensions.getByType(GitVersionExtension) - allowedBranches ? gitVersion.getMCTagOffsetBranch(mcVersion, allowedBranches) : gitVersion.getMCTagOffsetBranch(mcVersion) - } - - /** - * This method has been deprecated in favor of usage of GitVersion. - *

-     *     // Before:
-     *     version = gradleutils.getMCTagOffsetBranchVersion('1.21.4')
-     *
-     *     // After:
-     *     version = gitversion.getMCTagOffsetBranch('1.21.4')
-     * 
- *

- * You must declare your filters in the Git Version config file!. - * - * @deprecated Use {@link net.minecraftforge.gitver.api.GitVersion#getMCTagOffsetBranch(String, String ...) GitVersion.getMCTagOffsetBranch(String, String...)} instead. - */ - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - String getFilteredMCTagOffsetBranchVersion(boolean prefix = false, String filter, String mcVersion, String... allowedBranches) { - this.updateInfo(prefix, filter) - this.getMCTagOffsetBranchVersion(mcVersion, allowedBranches) - } - - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - private void updateInfo(boolean prefix, String filter) { - this.gitversion.versionInternal.tap { - if (prefix) it.tagPrefix = filter - else it.filters = new String[] {filter} - } - } - - private void logDeprecation(String name, String fullName) { - this.project.logger.warn "WARNING: This project is still using 'gradleutils.$name'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'gitversion.$fullName' instead." - } - - /** @see GradleUtils#getPublishingForgeMaven(Project, File) */ - Action getPublishingForgeMaven(File defaultFolder = this.project.rootProject.file('repo')) { - GradleUtils.getPublishingForgeMaven(this.project, defaultFolder) - } - - /** @see GradleUtils#getForgeMaven() */ - static Closure getForgeMaven() { - GradleUtils.forgeMaven - } - - /** @see GradleUtils#getForgeReleaseMaven() */ - static Closure getForgeReleaseMaven() { - GradleUtils.forgeReleaseMaven - } - - /** @see GradleUtils#getForgeSnapshotMaven() */ - static Closure getForgeSnapshotMaven() { - GradleUtils.forgeSnapshotMaven - } - - /** @see GradleUtils#getMinecraftLibsMaven() */ - static Closure getMinecraftLibsMaven() { - GradleUtils.minecraftLibsMaven - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.java b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.java new file mode 100644 index 0000000..fe1a2be --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtension.java @@ -0,0 +1,170 @@ +package net.minecraftforge.gradleutils; + +import groovy.lang.Closure; + +import java.io.File; + +/// Contains various utilities for working with Gradle scripts. +/// +/// [Projects][org.gradle.api.Project] that apply GradleUtils are given [GradleUtilsExtension.ForProject]. +@SuppressWarnings("rawtypes") // public-facing closures +public sealed interface GradleUtilsExtension permits GradleUtilsExtensionInternal, GradleUtilsExtension.ForProject { + /// The name for this extension. + String NAME = "gradleutils"; + + /** + * A closure for the Forge maven to be passed into + * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)}. + *


+     * repositories {
+     *     maven fg.forgeMaven
+     * }
+     * 
+ */ + Closure forgeMaven = GradleUtilsExtensionInternal.forgeMaven; + + /** + * A closure for the Forge releases maven to be passed into + * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)}. + *

+     * repositories {
+     *     maven fg.forgeReleaseMaven
+     * }
+     * 
+ * + * @see #forgeMaven + */ + Closure forgeReleaseMaven = GradleUtilsExtensionInternal.forgeReleaseMaven; + + /** + * A closure for the Minecraft libraries maven to be passed into + * {@link org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)}. + *

+     * repositories {
+     *     maven fg.minecraftLibsMaven
+     * }
+     * 
+ */ + Closure minecraftLibsMaven = GradleUtilsExtensionInternal.minecraftLibsMaven; + + /// The GradleUtils extension for {@linkplain org.gradle.api.Project projects}, which include additional utilities + /// that are only available for them. + /// + /// When applied, GradleUtils will + /// - Create a referenceable [PomUtils] instance in [#getPom()]. + /// - Register the `generateActionsWorkflow` task to the project for generating a default template GitHub Actions + /// workflow. + /// - Register the `configureTeamCity` task to the project for working with TeamCity CI pipelines. + /// + /// @see GradleUtilsExtension + sealed interface ForProject extends GradleUtilsExtension permits GradleUtilsExtensionInternal.ForProject { + /// Utilities for working with a [org.gradle.api.publish.maven.MavenPom] for publishing artifacts. + /// + /// @return The POM utilities + /// @see PomUtils + PomUtils getPom(); + + /// Get a configuring closure to be passed into [org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)] + /// in a publishing block. + /// + /// This closure respects the current project's version in regard to publishing to a release or snapshot + /// repository. + /// + /// **Important:** The following environment variables must be set for this to work: + /// - `MAVEN_USER`: Containing the username to use for authentication + /// - `MAVEN_PASSWORD`: Containing the password to use for authentication + /// + /// The following environment variables are optional: + /// - `MAVEN_URL_RELEASE`: Containing the URL to use for the release repository + /// - `MAVEN_URL_SNAPSHOT`: Containing the URL to use for the snapshot repository + /// + /// If the required environment variables are not present, the output Maven will be a local folder named `repo` + /// on the root of the [project directory][org.gradle.api.file.ProjectLayout#getProjectDirectory()]. + /// + /// If the `MAVEN_URL_RELEASE` variable is not set, the Forge releases repository will be used + /// (`https://maven.minecraftforge.net/releases`). + /// + /// @return The closure + default Closure getPublishingForgeMaven() { + return getPublishingForgeMaven("https://maven.minecraftforge.net/releases"); + } + + /// Get a configuring closure to be passed into [org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)] + /// in a publishing block. + /// + /// This closure respects the current project's version in regard to publishing to a release or snapshot + /// repository. + /// + /// **Important:** The following environment variables must be set for this to work: + /// - `MAVEN_USER`: Containing the username to use for authentication + /// - `MAVEN_PASSWORD`: Containing the password to use for authentication + /// + /// The following environment variables are optional: + /// - `MAVEN_URL_RELEASE`: Containing the URL to use for the release repository + /// - `MAVEN_URL_SNAPSHOT`: Containing the URL to use for the snapshot repository + /// + /// If the required environment variables are not present, the output Maven will be a local folder named `repo` + /// on the root of the [project directory][org.gradle.api.file.ProjectLayout#getProjectDirectory()]. + /// + /// If the `MAVEN_URL_RELEASE` variable is not set, the passed in fallback URL will be used for the release + /// repository. + /// + /// @param fallbackPublishingEndpoint The fallback URL for the release repository + /// @return The closure + Closure getPublishingForgeMaven(String fallbackPublishingEndpoint); + + /// Get a configuring closure to be passed into [org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)] + /// in a publishing block. + /// + /// This closure respects the current project's version in regard to publishing to a release or snapshot + /// repository. + /// + /// **Important:** The following environment variables must be set for this to work: + /// - `MAVEN_USER`: Containing the username to use for authentication + /// - `MAVEN_PASSWORD`: Containing the password to use for authentication + /// + /// The following environment variables are optional: + /// - `MAVEN_URL_RELEASE`: Containing the URL to use for the release repository + /// - `MAVEN_URL_SNAPSHOT`: Containing the URL to use for the snapshot repository + /// + /// If the required environment variables are not present, the output Maven will be set to the given default + /// folder. + /// + /// If the `MAVEN_URL_RELEASE` variable is not set, the passed in fallback URL will be used for the release + /// repository. + /// + /// @param fallbackPublishingEndpoint The fallback URL for the release repository + /// @param defaultFolder The default folder if the required maven information is not set + /// @return The closure + default Closure getPublishingForgeMaven(String fallbackPublishingEndpoint, File defaultFolder) { + return getPublishingForgeMaven(fallbackPublishingEndpoint, defaultFolder, new File(defaultFolder.getAbsoluteFile().getParentFile(), "snapshots")); + } + + /// Get a configuring closure to be passed into [org.gradle.api.artifacts.dsl.RepositoryHandler#maven(Closure)] + /// in a publishing block. + /// + /// This closure respects the current project's version in regard to publishing to a release or snapshot + /// repository. + /// + /// **Important:** The following environment variables must be set for this to work: + /// - `MAVEN_USER`: Containing the username to use for authentication + /// - `MAVEN_PASSWORD`: Containing the password to use for authentication + /// + /// The following environment variables are optional: + /// - `MAVEN_URL_RELEASE`: Containing the URL to use for the release repository + /// - `MAVEN_URL_SNAPSHOT`: Containing the URL to use for the snapshot repository + /// + /// If the required environment variables are not present, the output Maven will be set to the given default + /// folder. + /// + /// If the `MAVEN_URL_RELEASE` variable is not set, the passed in fallback URL will be used for the release + /// repository. + /// + /// @param fallbackPublishingEndpoint The fallback URL for the release repository + /// @param defaultFolder The default folder if the required maven information is not set + /// @param defaultSnapshotFolder The default folder for the snapshot repository if the required maven + /// information is not set + /// @return The closure + Closure getPublishingForgeMaven(String fallbackPublishingEndpoint, File defaultFolder, File defaultSnapshotFolder); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionImpl.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionImpl.groovy new file mode 100644 index 0000000..a5fcf49 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionImpl.groovy @@ -0,0 +1,87 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import org.gradle.api.Project +import org.gradle.api.artifacts.repositories.MavenArtifactRepository +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ProviderFactory +import org.gradle.authentication.http.BasicAuthentication + +import javax.inject.Inject + +import static net.minecraftforge.gradleutils.GradleUtilsPlugin.LOGGER + +@CompileStatic +@PackageScope abstract class GradleUtilsExtensionImpl implements GradleUtilsExtensionInternal { + protected abstract @Inject ObjectFactory getObjects() + protected abstract @Inject ProviderFactory getProviders() + + @Inject + GradleUtilsExtensionImpl() { } + + @CompileStatic + @PackageScope static abstract class ForProjectImpl extends GradleUtilsExtensionImpl implements GradleUtilsExtensionInternal.ForProject { + private final Project project + + final PomUtils pom + + private ForProjectImpl(Project project) { + this.project = project + + this.pom = this.objects.newInstance(PomUtilsImpl, project) + + project.tasks.register(GenerateActionsWorkflow.NAME, GenerateActionsWorkflowImpl) + project.tasks.register(ConfigureTeamCity.NAME, ConfigureTeamCity) + } + + @Override + Closure getPublishingForgeMaven(String fallbackPublishingEndpoint) { + this.getPublishingForgeMaven(fallbackPublishingEndpoint, this.project.rootProject.file('repo')) + } + + @Override + Closure getPublishingForgeMaven(String fallbackPublishingEndpoint, File defaultFolder, File defaultSnapshotFolder) { + // make properties of what we use so gradle's cache is aware + final snapshot = this.objects.property(Boolean).value(this.providers.provider { + this.project.version?.toString()?.endsWith('-SNAPSHOT') + }) + + // collecting all of our environment variables here so gradle's cache is aware + final mavenUser = this.providers.environmentVariable('MAVEN_USER') + final mavenPassword = this.providers.environmentVariable('MAVEN_PASSWORD') + final mavenUrlRelease = this.providers.environmentVariable('MAVEN_URL').orElse(this.providers.environmentVariable('MAVEN_URL_RELEASE')) + final mavenUrlSnapshots = this.providers.environmentVariable('MAVEN_URL_SNAPSHOTS') + + return { MavenArtifactRepository repo -> + repo.name = 'forge' + + if (mavenUser.present && mavenPassword.present) { + var publishingEndpoint = mavenUrlRelease.present ? mavenUrlRelease.get() : fallbackPublishingEndpoint + + repo.url = snapshot.getOrElse(false) && mavenUrlSnapshots.present + ? mavenUrlSnapshots.get() + : publishingEndpoint + + repo.authentication { authentication -> + authentication.create('basic', BasicAuthentication) + } + + repo.credentials { credentials -> + credentials.username = mavenUser.get() + credentials.password = mavenPassword.get() + } + } else { + LOGGER.info('Forge publishing credentials not found, using local folder') + repo.url = snapshot.getOrElse(false) + ? defaultSnapshotFolder.absoluteFile.toURI() + : defaultFolder.absoluteFile.toURI() + } + } + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionInternal.java b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionInternal.java new file mode 100644 index 0000000..7dbaffd --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsExtensionInternal.java @@ -0,0 +1,37 @@ +package net.minecraftforge.gradleutils; + +import groovy.lang.Closure; +import net.minecraftforge.gradleutils.shared.Closures; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; + +@SuppressWarnings("rawtypes") // public-facing closures +non-sealed interface GradleUtilsExtensionInternal extends GradleUtilsExtension, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(GradleUtilsExtension.class); + } + + Closure forgeMaven = Closures.consumer(repo -> { + repo.setName("MinecraftForge"); + repo.setUrl(Constants.FORGE_MAVEN); + }); + + Closure forgeReleaseMaven = Closures.consumer(repo -> { + repo.setName("MinecraftForge releases"); + repo.setUrl(Constants.FORGE_MAVEN_RELEASE); + }); + + Closure minecraftLibsMaven = Closures.consumer(repo -> { + repo.setName("Minecraft libraries"); + repo.setUrl(Constants.MC_LIBS_MAVEN); + }); + + non-sealed interface ForProject extends GradleUtilsExtensionInternal, GradleUtilsExtension.ForProject, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(GradleUtilsExtension.ForProject.class); + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.groovy b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.groovy deleted file mode 100644 index e597694..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileStatic -import net.minecraftforge.gradleutils.changelog.ChangelogPlugin -import net.minecraftforge.gradleutils.gitversion.GitVersionPlugin -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ProviderFactory - -import javax.inject.Inject - -/** The entry point for the Gradle Utils plugin. Exists to create the {@linkplain GradleUtilsExtension extension}. */ -@CompileStatic -abstract class GradleUtilsPlugin implements Plugin { - protected abstract @Inject ObjectFactory getObjects() - protected abstract @Inject ProviderFactory getProviders() - - @Override - void apply(Project project) { - project.plugins.apply GitVersionPlugin - project.plugins.apply ChangelogPlugin - // TODO [GradleUtils][GU3.0] Use direct constructor - project.extensions.create GradleUtilsExtension.NAME, GradleUtilsExtension, project, this.objects, this.providers - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.java b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.java new file mode 100644 index 0000000..e364b59 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsPlugin.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils; + +import net.minecraftforge.gradleutils.shared.EnhancedPlugin; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.plugins.ExtensionAware; + +import javax.inject.Inject; + +abstract class GradleUtilsPlugin extends EnhancedPlugin { + static final String NAME = "gradleutils"; + static final String DISPLAY_NAME = "MinecraftForge Gradle Utilities"; + + static final Logger LOGGER = Logging.getLogger(GradleUtilsPlugin.class); + + @Inject + public GradleUtilsPlugin() { + super(NAME, DISPLAY_NAME); + } + + @Override + public void setup(ExtensionAware target) { + if (target instanceof Project project) + project.getExtensions().create(GradleUtilsExtension.NAME, GradleUtilsExtensionImpl.ForProjectImpl.class, project); + else + target.getExtensions().create(GradleUtilsExtension.NAME, GradleUtilsExtensionImpl.class); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsProblems.java b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsProblems.java new file mode 100644 index 0000000..b263dbb --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/GradleUtilsProblems.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils; + +import net.minecraftforge.gradleutils.shared.EnhancedProblems; +import org.gradle.api.problems.Severity; + +import javax.inject.Inject; + +abstract class GradleUtilsProblems extends EnhancedProblems { + @Inject + public GradleUtilsProblems() { + super(GradleUtilsPlugin.NAME, GradleUtilsPlugin.DISPLAY_NAME); + } + + //region GitHub Workflow Generation + void ghWorkflowGitVersionMissing(String taskName) { + this.getReporter().report(id("gh-workflow-gitversion-missing", "GitHub Actions workflow is missing critical Git Version details"), spec -> spec + .details(""" + Task %s is generating a GitHub Actions workflow without critical data from Git Version. + The workflow file will likely be incomplete or be missing details that may cause it to fail.""".formatted(taskName)) + .severity(Severity.WARNING) + .stackLocation() + .solution("Apply the Git Version Plugin (net.minecraftforge.gitversion) to your project.") + .solution("If the Git Version plugin is applied, double check the Git Version Gradle plugin implementation.") + .solution("Manually add in the necessary details to the generated workflow file.") + .solution(HELP_MESSAGE)); + } + //endregion + + //region PomUtils + RuntimeException pomUtilsGitVersionMissing() { + return this.getReporter().throwing(new UnsupportedOperationException(), id("pomutils-missing-url", "Cannot add POM remote details without URL"), spec -> spec + .details(""" + Cannot add POM remote details using `gradleutils.pom.addRemoteDetails` without the URL. + If the Git Version plugin has not been applied, the URL must be manually specified as the second parameter.""") + .severity(Severity.ERROR) + .stackLocation() + .solution("Apply the Git Version Plugin (net.minecraftforge.gitversion) to your project.") + .solution("Manually add the remote URL in `addRemoteDetails`.") + .solution(HELP_MESSAGE)); + } + + void reportPomUtilsForgeProjWithoutForgeOrg() { + this.getReporter().report(id("pomutils-forge-proj-missing-forge-org", "Detected Forge project is missing Forge organization details"), spec -> spec + .details(""" + This project was autodetected as a MinecraftForge project, but `gradleutils.pom.addForgeDetails` was not used.""") + .severity(Severity.ADVICE) + .stackLocation() + .solution("Consider using `gradleutils.pom.addForgeDetails`")); + } + //endregion +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.groovy b/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.groovy deleted file mode 100644 index 8c6547f..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.groovy +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import net.minecraftforge.gradleutils.gitversion.GitVersionExtension -import org.gradle.api.Action -import org.gradle.api.publish.maven.MavenPom -import org.gradle.api.publish.maven.MavenPomDeveloper -import org.gradle.api.publish.maven.MavenPomLicense -import org.jetbrains.annotations.ApiStatus - -/** - * Utilities for making configuring a {@code MavenPom} more ergonomic. - * - * @see MavenPom - */ -@CompileStatic -@SuppressWarnings('unused') -final class PomUtils { - private final GitVersionExtension gitversion - - @PackageScope PomUtils(GitVersionExtension gitversion) { - this.gitversion = gitversion - } - - /** Allows accessing licenses from buildscripts using {@code gradleutils.pom.licenses}. */ - public static final Licenses licenses = new Licenses() - @CompileStatic - static final class Licenses { - public static final Closure Apache2_0 = makeLicense('Apache-2.0', 'https://www.apache.org/licenses/LICENSE-2.0') - public static final Closure LGPLv2_1 = makeLicense('LGPL-2.1-only', 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html') - public static final Closure LGPLv3 = makeLicense('LGPL-3.0-only', 'https://www.gnu.org/licenses/lgpl-3.0-standalone.html') - public static final Closure MIT = makeLicense('MIT', 'https://opensource.org/license/mit/') - - private static Closure makeLicense(String name, String url) { - return { MavenPomLicense license -> - license.name.set name - license.url.set url - license.distribution.set 'repo' - } - } - - private Licenses() {} - } - - // the IDE complains that the keys aren't actually Action if we make a map normally with Groovy - // so, using Map.<>of() to make it explicit with the type arguments - // HashMap so that withDefault() works - //@formatter:off - /** Common developers in the Minecraft Forge organization. */ - public static final Map> developers = [ - LexManos : makeDev('LexManos', 'Lex Manos'), - Paint_Ninja : makeDev('Paint_Ninja'), - SizableShrimp: makeDev('SizableShrimp'), - cpw : makeDev('cpw'), - Jonathing : makeDev('Jonathing', 'me@jonathing.me', 'https://jonathing.me', 'America/New_York') // i'm overkill - ].>withDefault(this.&makeDev) - //@formatter:on - - /** - * @deprecated Casing changed. - * @see #developers - */ - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - public static final Map> Developers = developers - - private static Action makeDev(String id, String name = id) { - return { MavenPomDeveloper developer -> - developer.id.set id - developer.name.set name - } as Action - } - - private static Action makeDev(String id, String name = id, String email, String url, String timezone) { - return { MavenPomDeveloper developer -> - developer.id.set id - developer.name.set name - developer.email.set email - developer.url.set url - developer.timezone.set timezone - } as Action - } - - /** - * Reduces boilerplate when setting up GitHub details in a {@link MavenPom}. - * - * @param pom The pom to configure - */ - void setGitHubDetails(MavenPom pom) { - setGitHubDetails(pom, this.gitversion) - } - - /** - * Reduces boilerplate when setting up GitHub details in a {@link MavenPom}. The organization is assumed to be - * {@literal 'MinecraftForge'}. - * - * @param pom The pom to configure - * @param repo The name of the repository on GitHub - */ - static void setGitHubDetails(MavenPom pom, String repo) { - this.setGitHubDetails(pom, 'MinecraftForge', repo) - } - - /** - * Reduces boilerplate when setting up GitHub details in a {@link MavenPom}. - * - * @param pom The pom to configure - * @param organization The organization or username the GitHub project is under - * @param repo The name of the repository on GitHub - */ - static void setGitHubDetails(MavenPom pom, String organization, String repo) { - setGitHubDetailsInternal pom, "github.com/${organization}/${repo}".toString() - } - - static void setGitHubDetails(MavenPom pom, GitVersionExtension gitversion) { - setGitHubDetailsInternal pom, stripProtocol(gitversion.url) - } - - private static void setGitHubDetailsInternal(MavenPom pom, String url) { - var fullURL = "https://${url}".toString() - pom.url.set fullURL - pom.scm { scm -> - scm.url.set fullURL - scm.connection.set "scm:git:git://${url}.git".toString() - scm.developerConnection.set "scm:git:git@${url}.git".toString() - } - - if (url.contains('github.com')) { - pom.issueManagement { issues -> - issues.system.set url.split('\\.', 2)[0] - issues.url.set "https://${url}/issues".toString() - } - pom.ciManagement { ci -> - ci.system.set 'github' - ci.url.set "https://${url}/actions".toString() - } - } - - if (url.contains('MinecraftForge/')) { - pom.organization { org -> - org.name.set 'Forge Development LLC' - org.url.set 'https://minecraftforge.net' - } - } - } - - private static String stripProtocol(String url) { - if (!url) return url - - final s = '://' - int index = url.indexOf(s) - return index == -1 ? url : url.substring(index + s.length()) - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.java b/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.java new file mode 100644 index 0000000..5ad2b7d --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/PomUtils.java @@ -0,0 +1,70 @@ +package net.minecraftforge.gradleutils; + +import org.gradle.api.Action; +import org.gradle.api.publish.maven.MavenPom; +import org.gradle.api.publish.maven.MavenPomDeveloper; +import org.gradle.api.publish.maven.MavenPomLicense; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Map; + +/** + * Contains utilities to make working with {@link MavenPom POMs} more ergonomic. + *

This can be accessed by {@linkplain org.gradle.api.Project projects} using the + * {@link GradleUtilsExtension.ForProject gradleutils} extension.

+ */ +public sealed interface PomUtils permits PomUtilsInternal { + /// Allows accessing [licenses][Licenses] from buildscripts using `gradleutils.pom.licenses`. + /// + /// @return A reference to the licenses + /// @see Licenses + Licenses getLicenses(); + + /// Contains several licenses used by MinecraftForge to reduce needing to manually write them out in each project + /// that uses one. + /// + /// @see #getLicenses() + sealed interface Licenses permits PomUtilsInternal.Licenses { + /// @see Apache License 2.0 on SPDX + Action Apache2_0 = PomUtilsInternal.makeLicense("Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0"); + /// @see GNU Lesser General Public License v2.1 only on SPDX + Action LGPLv2_1 = PomUtilsInternal.makeLicense("LGPL-2.1-only", "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html"); + /// @see GNU Lesser General Public License v3.0 only on SPDX + Action LGPLv3 = PomUtilsInternal.makeLicense("LGPL-3.0-only", "https://www.gnu.org/licenses/lgpl-3.0-standalone.html"); + /// @see MIT License on SPDX + Action MIT = PomUtilsInternal.makeLicense("MIT", "https://opensource.org/license/mit/"); + } + + /// Contains several developers within the MinecraftForge organization to reduce needing to manually write them out + /// in each project they contribute to. + /// + /// If a queried developer does not exist, it is automatically created with the input which is set to the + /// {@linkplain MavenPomDeveloper#getId() ID} and {@linkplain MavenPomDeveloper#getName() name}. + Map> developers = PomUtilsInternal.makeDevelopers(); + + /// Adds MinecraftForge-specific details to the given POM. + /// + /// @param pom The POM to add details to + @ApiStatus.Internal + static void addForgeDetails(MavenPom pom) { + PomUtilsInternal.addForgeDetails(pom); + } + + /// Adds details from the project's remote URL to the given POM. + /// + /// @param pom The POM to add details to + /// @apiNote If the project does not have the `net.minecraftforge.gitversion` plugin applied, this method will fail. + /// If you are not using Git Version, manually specify your project's URL using + /// [#addRemoteDetails(MavenPom,String)]. + void addRemoteDetails(MavenPom pom); + + /** + * Adds details from the given remote URL to the given POM. + * + * @param pom The pom to add details to + * @param url The URL of the repository + * @apiNote If you are using the {@code net.minecraftforge.gitversion} plugin, you can use + * {@link #addRemoteDetails(MavenPom)} to use the URL discovered by Git Version instead of specifying it manually. + */ + void addRemoteDetails(MavenPom pom, String url); +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsImpl.groovy b/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsImpl.groovy new file mode 100644 index 0000000..356d90e --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsImpl.groovy @@ -0,0 +1,58 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils + +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.PackageScope +import net.minecraftforge.gradleutils.shared.SharedUtil +import org.gradle.api.Project +import org.gradle.api.model.ObjectFactory +import org.gradle.api.publish.maven.MavenPom + +import javax.inject.Inject + +@CompileStatic +@PackageScope abstract class PomUtilsImpl implements PomUtilsInternal { + private final Project project + private final GradleUtilsProblems problems + + protected abstract @Inject ObjectFactory getObjects() + + @Inject + PomUtilsImpl(Project project) { + this.project = project + this.problems = this.objects.newInstance(GradleUtilsProblems) + } + + final Licenses licenses = this.objects.newInstance(Licenses) + + @CompileStatic + @PackageScope static abstract class Licenses implements PomUtilsInternal.Licenses { + @Inject + Licenses() { } + } + + @Override + @CompileDynamic + void addRemoteDetails(MavenPom pom) { + // Overridden by Git Version Plugin + throw this.problems.pomUtilsGitVersionMissing() + } + + @Override + void addRemoteDetails(MavenPom pom, String url) { + super.addRemoteDetails(pom, url) + + if (url.contains('github.com/MinecraftForge/')) { + SharedUtil.ensureAfterEvaluate(this.project) { + pom.organization { organization -> + if (organization.name.orNull != Constants.FORGE_ORG_NAME) + this.problems.reportPomUtilsForgeProjWithoutForgeOrg() + } + } + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsInternal.java b/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsInternal.java new file mode 100644 index 0000000..2e8898e --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/PomUtilsInternal.java @@ -0,0 +1,125 @@ +package net.minecraftforge.gradleutils; + +import org.gradle.api.Action; +import org.gradle.api.publish.maven.MavenPom; +import org.gradle.api.publish.maven.MavenPomDeveloper; +import org.gradle.api.publish.maven.MavenPomLicense; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; +import org.jetbrains.annotations.MustBeInvokedByOverriders; + +import java.util.HashMap; +import java.util.Map; + +non-sealed interface PomUtilsInternal extends PomUtils, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(PomUtils.class); + } + + non-sealed interface Licenses extends PomUtils.Licenses, HasPublicType { + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(PomUtils.Licenses.class); + } + } + + static Action makeLicense(String name, String url) { + return license -> { + license.getName().set(name); + license.getUrl().set(url); + license.getDistribution().set("repo"); + }; + } + + static Map> makeDevelopers() { + return makeDevelopers(Map.of( + "LexManos", makeDev("LexManos", "Lex Manos"), + "Paint_Ninja", makeDev("Paint_Ninja"), + "SizableShrimp", makeDev("SizableShrimp"), + "cpw", makeDev("cpw"), + "Jonathing", makeDev("Jonathing", "me@jonathing.me", "https://jonathing.me", "America/New_York") + )); + } + + private static Map> makeDevelopers(Map> defaults) { + return new HashMap<>(defaults) { + @Override + public Action get(Object key) { + this.ensure((String) key); + return super.get(key); + } + + private void ensure(String key) { + if (!this.containsKey(key)) + this.put(key, makeDev(key)); + } + }; + } + + private static Action makeDev(String id) { + return makeDev(id, id); + } + + private static Action makeDev(String id, String name) { + return developer -> { + developer.getId().set(id); + developer.getName().set(name); + }; + } + + private static Action makeDev(String id, String email, String url, String timezone) { + return makeDev(id, id, email, url, timezone); + } + + private static Action makeDev(String id, String name, String email, String url, String timezone) { + return developer -> { + developer.getId().set(id); + developer.getName().set(name); + developer.getEmail().set(email); + developer.getUrl().set(url); + developer.getTimezone().set(timezone); + }; + } + + static void addForgeDetails(MavenPom pom) { + pom.organization(organization -> { + organization.getName().set(Constants.FORGE_ORG_NAME); + organization.getUrl().set(Constants.FORGE_ORG_URL); + }); + } + + @Override + @MustBeInvokedByOverriders + default void addRemoteDetails(MavenPom pom, String url) { + if (url == null || url.isBlank()) + throw new IllegalArgumentException(); + + var strippedUrl = stripProtocol(url); + var fullURL = "https://" + url; + pom.getUrl().set(fullURL); + pom.scm(scm -> { + scm.getUrl().set(fullURL); + scm.getConnection().set("scm:git:git://%s.git".formatted(strippedUrl)); + scm.getDeveloperConnection().set("scm:git:git@%s.git".formatted(strippedUrl)); + }); + + // the rest is GitHub-exclusive information + if (!strippedUrl.contains("github.com")) return; + + pom.issueManagement(issues -> { + issues.getSystem().set(url.split("\\.", 2)[0]); + issues.getUrl().set(fullURL + "/issues"); + }); + pom.ciManagement(ci -> { + ci.getSystem().set("github"); + ci.getUrl().set(fullURL + "/actions"); + }); + } + + private static String stripProtocol(String url) { + int protocolIdx = url.indexOf("://"); + var ret = protocolIdx == -1 ? url : url.substring(protocolIdx + "://".length()); + return ret.endsWith("/") ? ret.substring(0, ret.length() - 1) : ret; + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogExtension.groovy b/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogExtension.groovy deleted file mode 100644 index 7eada5e..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogExtension.groovy +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.changelog - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import org.gradle.api.Project -import org.gradle.api.file.Directory -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.TaskProvider -import org.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.Nullable - -import javax.inject.Inject - -/** The heart of the Changelog plugin. This extension is used to enable and partially configure the changelog generation task. */ -@CompileStatic -class ChangelogExtension { - public static final String NAME = 'changelog' - - @PackageScope final Project project - - /** @deprecated The Git root is automatically discovered by Git Version on Changelog generation. */ - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - @Nullable Directory gitRoot - - boolean publishAll = true - @PackageScope boolean isGenerating - private @Lazy TaskProvider task = { - this.isGenerating = true - - ChangelogUtils.setupChangelogTask(this.project) { TaskProvider task -> - this.project.afterEvaluate { Project project -> - if (this.gitRoot) { - task.configure { - it.gitDirectory.set gitRoot - } - } - - if (this.publishAll) - ChangelogUtils.setupChangelogGenerationOnAllPublishTasks project - } - } - }() - - /** @deprecated This constructor will be made package-private in GradleUtils 3.0 */ - @Inject - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - ChangelogExtension(Project project) { - this.project = project - } - - void fromBase() { - from null - } - - void from(String marker) { - this.task.configure { it.start.set marker } - } - - private static boolean fromTagDeprecationWarning - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - void fromTag(String tag) { - if (!fromTagDeprecationWarning) { - this.project.logger.warn "WARNING: This project is still using 'changelog.fromTag'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'changelog.from' instead." - fromTagDeprecationWarning = true - } - - this.from tag - } - - private static boolean fromCommitDeprecationWarning - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - void fromCommit(String commit) { - if (!fromCommitDeprecationWarning) { - this.project.logger.warn "WARNING: This project is still using 'changelog.fromCommit'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'changelog.from' instead." - fromCommitDeprecationWarning = true - } - - this.from commit - } - - private static boolean disableAutomaticPublicationRegistrationDeprecationWarning - @Deprecated(forRemoval = true, since = '2.4') - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - void disableAutomaticPublicationRegistration() { - if (!disableAutomaticPublicationRegistrationDeprecationWarning) { - this.project.logger.warn "WARNING: This project is still using 'changelog.disableAutomaticPublicationRegistration'. It has been deprecated and will be removed in GradleUtils 3.0. Consider using 'changelog.publishAll = false' instead." - disableAutomaticPublicationRegistrationDeprecationWarning = true - } - - this.publishAll = false - } - - void publish(MavenPublication publication) { - ChangelogUtils.setupChangelogGenerationForPublishing this.project, publication - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogPlugin.groovy b/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogPlugin.groovy deleted file mode 100644 index 2d7e386..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogPlugin.groovy +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.changelog - -import groovy.transform.CompileStatic -import org.gradle.api.Plugin -import org.gradle.api.Project - -/** The entry point for the Changelog plugin. Exists to create the {@linkplain ChangelogExtension extension}. */ -@CompileStatic -abstract class ChangelogPlugin implements Plugin { - @Override - void apply(Project project) { - // TODO [GradleUtils][3.0][Changelog] Use direct constructor - project.extensions.create ChangelogExtension.NAME, ChangelogExtension, project - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogUtils.groovy b/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogUtils.groovy deleted file mode 100644 index 1bbc1b8..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/changelog/ChangelogUtils.groovy +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.changelog - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import groovy.transform.PackageScopeTarget -import net.minecraftforge.gradleutils.GradleUtils -import org.gradle.api.Action -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.artifacts.Configuration -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenArtifact -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.tasks.TaskProvider -import org.gradle.language.base.plugins.LifecycleBasePlugin - -/** Utility methods for configuring and working with the changelog tasks. */ -@CompileStatic -@PackageScope([PackageScopeTarget.CLASS, PackageScopeTarget.METHODS]) -class ChangelogUtils { - /** - * Adds the createChangelog task to the target project. Also exposes it as a artifact of the 'createChangelog' - * configuration. - *

- * This is the - * recommended way - * to share task outputs between multiple projects. - * - * @param project Project to add the task to - * @return The task responsible for generating the changelog - */ - static TaskProvider setupChangelogTask(Project project, Action> action) { - project.tasks.register(GenerateChangelog.NAME, GenerateChangelog).tap { task -> - project.configurations.register(GenerateChangelog.NAME) { Configuration c -> c.canBeResolved = false } - project.artifacts.add(GenerateChangelog.NAME, task) - action.execute task - - project.afterEvaluate { - //noinspection ConfigurationAvoidance -- we are in afterEvaluate - project.tasks.findByName(LifecycleBasePlugin.ASSEMBLE_TASK_NAME)?.configure { Task assemble -> assemble.dependsOn(task) } - } - } - } - - /** - * Sets up the changelog generation on all maven publications in the project. - *

- * It also sets up publishing for all subprojects as long as that subproject does not have another changelog plugin - * overriding the propagation. - * - * @param project The project to add changelog generation publishing to - */ - static void setupChangelogGenerationOnAllPublishTasks(Project project) { - setupChangelogGenerationForAllPublications(project) - project.subprojects.forEach { sub -> - sub.afterEvaluate { - // attempt to get the current subproject's changelog extension - var ext = sub.extensions.findByType(ChangelogExtension) - - // find the changelog extension for the highest project that has it, if the subproject doesn't - for (var parent = project; ext == null && parent != null; parent = parent.parent == parent ? null : parent.parent) { - ext = parent.extensions.findByType(ChangelogExtension) - } - - // if the project with changelog is publishing all changelogs, set up changelogs for the subproject - if (ext != null && ext.publishAll) - setupChangelogGenerationForAllPublications(sub) - } - } - } - - private static void setupChangelogGenerationForAllPublications(Project project) { - var ext = project.extensions.findByName(PublishingExtension.NAME) as PublishingExtension - if (ext == null) return - - // Get each extension and add the publishing task as a publishing artifact - ext.publications.withType(MavenPublication).configureEach { MavenPublication publication -> - setupChangelogGenerationForPublishing project, publication - } - } - - private static ChangelogExtension findParent(Project project) { - var ext = project.extensions.findByType(ChangelogExtension) - if (ext?.isGenerating) return ext - - Project parent = project.parent == project ? null : project.parent - return parent == null ? null : findParent(parent) - } - - /** - * The recommended way to share task outputs across projects is to export them as dependencies - *

- * So for any project that doesn't generate the changelog directly, we must create a - * {@linkplain CopyChangelog copy task} and new configuration - */ - private static TaskProvider findChangelogTask(Project project) { - // See if we've already made the task - if (project.tasks.names.contains(GenerateChangelog.NAME)) - return project.tasks.named(GenerateChangelog.NAME) - - if (project.tasks.names.contains(CopyChangelog.NAME)) - return project.tasks.named(CopyChangelog.NAME) - - // See if there is any parent with a changelog configured - var parent = findParent(project) - if (parent == null) return null - - project.tasks.register(CopyChangelog.NAME, CopyChangelog) { task -> - var dependency = project.dependencies.project('path': parent.project.path, 'configuration': GenerateChangelog.NAME) - task.configuration.set project.configurations.detachedConfiguration(dependency).tap { it.canBeConsumed = false } - } - } - - /** - * Sets up the changelog generation on the given maven publication. - * - * @param project The project in question - * @param publication The publication in question - */ - static void setupChangelogGenerationForPublishing(Project project, MavenPublication publication) { - GradleUtils.ensureAfterEvaluate(project) { - setupChangelogGenerationForPublishingAfterEvaluation(it, publication) - } - } - - private static void setupChangelogGenerationForPublishingAfterEvaluation(Project project, MavenPublication publication) { - boolean existing = !publication.artifacts.findAll { MavenArtifact it -> it.classifier == 'changelog' && it.extension == 'txt' }.isEmpty() - if (existing) return - - // Grab the task - var task = findChangelogTask(project) - - // Add a new changelog artifact and publish it - publication.artifact(task.get().outputs.files.singleFile) { - it.builtBy(task) - it.classifier = 'changelog' - it.extension = 'txt' - } - } - - private ChangelogUtils() {} -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/changelog/CopyChangelog.groovy b/src/main/groovy/net/minecraftforge/gradleutils/changelog/CopyChangelog.groovy deleted file mode 100644 index 9b66c6f..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/changelog/CopyChangelog.groovy +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.changelog - -import groovy.transform.CompileStatic -import org.gradle.api.DefaultTask -import org.gradle.api.file.FileCollection -import org.gradle.api.file.ProjectLayout -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction - -import javax.inject.Inject - -/** This task copies a changelog file to this project's build directory.*/ -@CompileStatic -abstract class CopyChangelog extends DefaultTask { - public static final String NAME = 'copyChangelog' - - @Inject - CopyChangelog(ProjectLayout layout) { - this.description = "Copies a changelog file to this project's build directory." - - this.outputFile.convention layout.buildDirectory.file('changelog.txt') - } - - /** The output file for the copied changelog. */ - abstract @OutputFile RegularFileProperty getOutputFile() - /** The configuration (or file collection) containing the changelog to copy. It must be a single file. */ - abstract @InputFiles Property getConfiguration() - - @TaskAction - void exec() { - var input = this.configuration.get().singleFile - var output = this.outputFile.get().asFile - if (!output.parentFile.exists()) - output.parentFile.mkdirs() - output.bytes = input.bytes - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/changelog/GenerateChangelog.groovy b/src/main/groovy/net/minecraftforge/gradleutils/changelog/GenerateChangelog.groovy deleted file mode 100644 index 9cebec7..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/changelog/GenerateChangelog.groovy +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.changelog - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import net.minecraftforge.gitver.api.GitVersion -import net.minecraftforge.gitver.api.GitVersionException -import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.ProjectLayout -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.Property -import org.gradle.api.provider.ProviderFactory -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -import javax.inject.Inject - -/** This task generates a changelog for the project based on the Git history using Git Version. */ -@CompileStatic -abstract class GenerateChangelog extends DefaultTask { - @PackageScope static final String NAME = 'createChangelog' - - @Inject - GenerateChangelog(ObjectFactory objects, ProjectLayout layout, ProviderFactory providers) { - this.description = 'Generates a changelog for the project based on the Git history using Git Version.' - - //Setup defaults: Using merge-base based text changelog generation of the local project into build/changelog.txt - this.outputFile.convention layout.buildDirectory.file('changelog/changelog.txt') - - this.gitDirectory.convention objects.directoryProperty().fileProvider(providers.provider { GitVersion.findGitRoot(layout.projectDirectory.asFile) }).dir('.git') - this.projectPath.convention providers.provider { GitVersion.findRelativePath(layout.projectDirectory.asFile) } - this.buildMarkdown.convention false - } - - /** The output file for the changelog. */ - abstract @OutputFile RegularFileProperty getOutputFile() - /** The {@code .git} directory to base the Git Version off of. */ - abstract @InputDirectory @PathSensitive(PathSensitivity.NONE) DirectoryProperty getGitDirectory() - /** The path string of the project from the root. Used to configure Git Version without needing to specify the directory itself. */ - abstract @Input Property getProjectPath() - /** The tag (or object ID) to start the changelog from. */ - abstract @Input @Optional Property getStart() - /** The project URL to use in the changelog. Will attempt to find a URL from Git Version if unspecified. */ - abstract @Input @Optional Property getProjectUrl() - /** Whether to build the changelog in markdown format. */ - abstract @Input Property getBuildMarkdown() - - @TaskAction - void exec() throws IOException { - GitVersion.disableSystemConfig() - - var gitDir = this.gitDirectory.asFile.get() - try (var version = GitVersion.builder().gitDir(gitDir).project(new File(gitDir.absoluteFile.parentFile, this.projectPath.get())).build()) { - var changelog = version.generateChangelog(this.start.orNull, this.projectUrl.orNull, !this.buildMarkdown.get()) - - var file = outputFile.asFile.get() - if (!file.parentFile.exists()) - file.parentFile.mkdirs() - - file.setText(changelog, 'UTF8') - } catch (GitVersionException e) { - this.logger.error 'ERROR: Failed to generate the changelog for this project, likely due to a misconfiguration. GitVersion has caught the exception, the details of which are attached to this error. Check that the correct tags are being used, or updating the tag prefix accordingly.' - throw e - } catch (IOException e) { - this.logger.error 'ERROR: Changelog was generated successfully, but could not be written to the disk. Ensure that you have write permissions to the output directory.' - throw new RuntimeException(e) - } - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionExtension.groovy b/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionExtension.groovy deleted file mode 100644 index 9d74d42..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionExtension.groovy +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.gitversion - -import groovy.transform.CompileStatic -import groovy.transform.PackageScope -import net.minecraftforge.gitver.api.GitVersion -import net.minecraftforge.gitver.api.GitVersionException -import org.gradle.api.Project -import org.gradle.api.file.Directory -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.FileSystemLocation -import org.gradle.api.file.ProjectLayout -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.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.Nullable - -import javax.inject.Inject - -/** - * The heart of the Git Version Gradle plugin. This extension is responsible for creating the GitVersion object and - * allowing access to it from Gradle buildscripts. - *

When using Gradle's Configuration Cache, the system Git config is disabled.

- */ -@CompileStatic -@SuppressWarnings('GrDeprecatedAPIUsage') -class GitVersionExtension { - public static final String NAME = 'gitversion' - - private final Project project - private final ObjectFactory objects - private final ProjectLayout layout - private final ProviderFactory providers - - /** @deprecated This constructor will be made package-private in GradleUtils 3.0 */ - @Inject - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - GitVersionExtension(Project project, ObjectFactory objects, ProjectLayout layout, ProviderFactory providers) { - this.project = project - this.objects = objects - this.layout = layout - this.providers = providers - } - - - /* GIT VERSION */ - - @Deprecated(forRemoval = true) - @ApiStatus.ScheduledForRemoval(inVersion = '3.0') - @PackageScope @Lazy GitVersion versionInternal = { - GitVersion.disableSystemConfig() - - var projectDir = this.layout.projectDirectory.asFile - var builder = GitVersion.builder().project(projectDir) - var gitversion = this.tryBuild(builder) - return gitversion ?: tryBuild(builder.project(GitVersion.findGitRoot(projectDir))) - - /* - try { - return builder.build().tap { it.info } - } catch (GitVersionException ignored) { - this.project.logger.warn 'WARNING: Git Version failed to get version numbers! Attempting to use default version 0.0.0. Check your GitVersion config file and make sure the correct tag prefix and filters are in use. Ensure that the tags you are attempting to use exist in the repository.' - return builder.strict(false).build() - } catch (IllegalArgumentException e) { - this.project.logger.error 'ERROR: Git Version is misconfigured and cannot be used, likely due to incorrect paths being set. This is an unrecoverable problem and needs to be addressed in the config file. Ensure that the correct subprojects and paths are declared in the config file' - throw e - } - */ - }() - - // TODO [GitVersion][Gradle] Handle this better in GU3.0 - private static @Nullable GitVersion tryBuild(GitVersion.Builder builder) { - try { - builder.build().tap { info } - } catch (GitVersionException ignored) { - builder.strict(false).build() - } catch (IllegalArgumentException ignored) { - null - } - } - - // TODO [GradleUtils][3.0] Make private - private static boolean deprecationWarning - @Deprecated(forRemoval = true) - GitVersion getVersion() { - if (!deprecationWarning) { - this.project.logger.warn "WARNING: The usage of 'gitversion.version' has been deprecated and will be removed in GradleUtils 3.0. Please remove the 'version' call (i.e. 'gitversion.version.tagOffset' -> 'gitversion.tagOffset')." - deprecationWarning = true - } - - this.versionInternal - } - - - /* VERSION NUMBER */ - - String getTagOffset() { - this.versionInternal.tagOffset - } - - String getTagOffsetBranch() { - this.versionInternal.tagOffsetBranch - } - - String getTagOffsetBranch(String... allowedBranches) { - this.versionInternal.getTagOffsetBranch allowedBranches - } - - String getTagOffsetBranch(Collection allowedBranches) { - this.versionInternal.getTagOffsetBranch allowedBranches - } - - String getMCTagOffsetBranch(String mcVersion) { - this.versionInternal.getMCTagOffsetBranch mcVersion - } - - String getMCTagOffsetBranch(String mcVersion, String... allowedBranches) { - this.versionInternal.getMCTagOffsetBranch mcVersion, allowedBranches - } - - String getMCTagOffsetBranch(String mcVersion, Collection allowedBranches) { - this.versionInternal.getMCTagOffsetBranch mcVersion, allowedBranches - } - - - /* INFO */ - - GitVersion.Info getInfo() { - this.versionInternal.info - } - - @Nullable String getUrl() { - this.versionInternal.url - } - - - /* FILE SYSTEM */ - - @Lazy DirectoryProperty gitDir = { - this.objects.directoryProperty().fileProvider(this.providers.provider { - this.versionInternal.gitDir - }) - }() - - @Lazy DirectoryProperty rootDir = { - this.objects.directoryProperty().fileProvider(this.providers.provider { - this.versionInternal.root - }) - }() - - @Lazy DirectoryProperty projectDir = { - this.objects.directoryProperty().fileProvider(this.providers.provider { - this.versionInternal.project - }) - }() - - @Lazy Property projectPath = { - this.objects.property(String).value(this.providers.provider { - this.versionInternal.projectPath - }) - }() - - Provider getRelativePath(FileSystemLocation file) { - this.getRelativePath this.providers.provider { file } - } - - Provider getRelativePath(Provider file) { - this.providers.provider { - this.versionInternal.getRelativePath file.get().asFile - } - } - - - /* SUBPROJECTS */ - - @Lazy ListProperty subprojects = { - this.objects.listProperty(Directory).value(this.providers.provider { - this.versionInternal.subprojects.collect { - dir -> this.layout.dir(this.providers.provider { dir }).get() - } - }) - }() - - private @Lazy ListProperty subprojectPathsFromRoot = { - this.objects.listProperty(String).value(this.providers.provider { - this.versionInternal.getSubprojectPaths true - }) - }() - - @Lazy ListProperty subprojectPaths = { - this.objects.listProperty(String).value(this.providers.provider { - this.versionInternal.subprojectPaths - }) - }() - - ListProperty getSubprojectPaths(boolean fromRoot) { - fromRoot ? this.subprojectPathsFromRoot : this.subprojectPaths - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionPlugin.groovy b/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionPlugin.groovy deleted file mode 100644 index 5ce7b08..0000000 --- a/src/main/groovy/net/minecraftforge/gradleutils/gitversion/GitVersionPlugin.groovy +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.gradleutils.gitversion - -import groovy.transform.CompileStatic -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.file.ProjectLayout -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ProviderFactory - -import javax.inject.Inject - -/** The entry point for the Git Version Gradle plugin. Exists to create the {@linkplain GitVersionExtension extension}. */ -@CompileStatic -abstract class GitVersionPlugin implements Plugin { - /** @see ObjectFactory Service Injection */ - @Inject abstract ObjectFactory getObjects() - /** @see ProjectLayout Service Injection */ - @Inject abstract ProjectLayout getLayout() - /** @see ProviderFactory Service Injection */ - @Inject abstract ProviderFactory getProviders() - - @Override - void apply(Project project) { - // TODO [GradleUtils][3.0][GitVersion] Use direct constructor - project.extensions.create(GitVersionExtension.NAME, GitVersionExtension, project, this.objects, this.layout, this.providers) - } -} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/package-info.java b/src/main/groovy/net/minecraftforge/gradleutils/package-info.java new file mode 100644 index 0000000..6387c29 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/package-info.java @@ -0,0 +1,9 @@ +/// GradleUtils is a convention plugin that is used by almost all of Forge's projects. It mostly consists of helper +/// classes and methods such as [net.minecraftforge.gradleutils.GradleUtilsExtension] and +/// [net.minecraftforge.gradleutils.PomUtils] that help eliminate duplicate code within buildscripts and allow us to +/// change any default values, such as Maven URLs, in case things change unexpectedly. +/// +/// If you are a non-Forge consumer, it is recommended not to use any Forge-specific APIs, including methods such as +/// [net.minecraftforge.gradleutils.PomUtils#addForgeDetails(org.gradle.api.publish.maven.MavenPom)] and the +/// [net.minecraftforge.gradleutils.GenerateActionsWorkflow] task. +package net.minecraftforge.gradleutils; diff --git a/src/main/groovy/net/minecraftforge/gradleutils/services/GradleAPIJavadocLinkProvider.java b/src/main/groovy/net/minecraftforge/gradleutils/services/GradleAPIJavadocLinkProvider.java new file mode 100644 index 0000000..2577660 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/services/GradleAPIJavadocLinkProvider.java @@ -0,0 +1,20 @@ +package net.minecraftforge.gradleutils.services; + +import io.freefair.gradle.plugins.maven.javadoc.JavadocLinkProvider; +import io.freefair.gradle.plugins.maven.javadoc.JavadocLinkUtil; +import org.gradle.util.GradleVersion; +import org.jetbrains.annotations.Nullable; + +/// Service to allow Nokee and Remal's redistributions of the Gradle API to link to Gradle's JavaDocs website. +public class GradleAPIJavadocLinkProvider implements JavadocLinkProvider { + /// Invoked by the [java.util.ServiceLoader]. + public GradleAPIJavadocLinkProvider() { } + + @Override + public @Nullable String getJavadocLink(String group, String artifact, String version) { + if (!"gradle-api".equals(artifact)) return null; + if (!"dev.gradleplugins".equals(group) && !"name.remal.gradle-api".equals(group)) return null; + + return JavadocLinkUtil.getGradleApiLink(GradleVersion.version(version)); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/services/package-info.java b/src/main/groovy/net/minecraftforge/gradleutils/services/package-info.java new file mode 100644 index 0000000..6308cac --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/services/package-info.java @@ -0,0 +1,4 @@ +/// This package contains services that are loaded into the Gradle classpath along with this plugin. It contains some +/// helpers that silently enhance the effects of other plugins, such as the +/// [net.minecraftforge.gradleutils.services.GradleAPIJavadocLinkProvider]. +package net.minecraftforge.gradleutils.services; diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/Closures.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/Closures.java new file mode 100644 index 0000000..005024f --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/Closures.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.gradleutils.shared; + +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import org.codehaus.groovy.reflection.ReflectionUtils; +import org.codehaus.groovy.runtime.InvokerInvocationException; +import org.gradle.api.Action; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/// This class contains helper methods for creating closures in Java code. +/// +/// @apiNote Always use [#invoke] instead of [Closure#call] to avoid issues with classloader exceptions +/// and closure parameter delegations. +public final class Closures { + /// Invokes a given closure with the given object as the delegate type and parameter. + /// + /// This is used to work around a Groovy DSL implementation detail that involves dynamic objects within + /// buildscripts. By default, Gradle will attempt to locate the dynamic object that is being referenced within a + /// closure and use handlers within the buildscript's class loader to work with it. This is unfortunately very + /// unfriendly to trying to use closures on traditional objects. The solution is to both manually set the closure's + /// [delegate][Closure#setDelegate(Object)], [resolve strategy][Closure#setResolveStrategy(int)], and temporarily + /// swap out the [current thread's context class loader][Thread#setContextClassLoader(ClassLoader)] with that of the + /// closure in order to force resolution of the groovy metaclass to the delegate object. + /// + /// I'm very sorry. + /// + /// @param The return type + /// @param closure The closure to invoke + /// @param object The parameter(s) to pass into the closure + /// @return The result of the closure execution casted to the generic type parameter + /// @see org.gradle.api.internal.AbstractTask.ClosureTaskAction#doExecute(org.gradle.api.Task) + @SuppressWarnings({"rawtypes", "JavadocReference"}) + public static @UnknownNullability T invoke(@DelegatesTo(strategy = Closure.DELEGATE_FIRST) Closure closure, Object... object) { + closure.setDelegate(object[0]); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + return invokeInternal(closure, object); + } + + private static final Object[] EMPTY_ARGS = {}; + + /// Invokes a given closure with no parameters. + /// + /// @param The return type + /// @param closure The closure to invoke + /// @return The result of the closure execution casted to the generic type parameter + /// @see #invoke(Closure, Object...) + @SuppressWarnings("rawtypes") + public static @UnknownNullability T invoke(Closure closure) { + return invokeInternal(closure, EMPTY_ARGS); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static @UnknownNullability T invokeInternal(Closure closure, Object... object) { + var original = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(closure.getClass().getClassLoader()); + try { + var ret = closure.getMaximumNumberOfParameters() == 0 ? closure.call() : closure.call(object); + return ret != null ? (T) ret : null; + } catch (InvokerInvocationException e) { + throw e.getCause() instanceof RuntimeException rte ? rte : e; + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } + + /// Creates a closure backed by the given unary operator. + /// + /// @param function The function to apply + /// @param The return type of the function + /// @return The closure + /// @apiNote For static methods only. + public static Closure unaryOperator(UnaryOperator function) { + return unaryOperator(ReflectionUtils.getCallingClass(), function); + } + + /// Creates a closure backed by the given unary operator. + /// + /// @param owner The owner of the closure + /// @param function The function to apply + /// @param The return type of the function + /// @return The closure + /// @apiNote For instance methods only. + public static Closure unaryOperator(Object owner, UnaryOperator function) { + return function(owner, function); + } + + /// Creates a closure backed by the given function. + /// + /// @param function The function to apply + /// @param The parameter type of the function + /// @param The return type of the function + /// @return The closure + /// @apiNote For static methods only. + public static Closure function(Function function) { + return function(ReflectionUtils.getCallingClass(), function); + } + + /// Creates a closure backed by the given function. + /// + /// @param owner The owner of the closure + /// @param function The function to apply + /// @param The parameter type of the function + /// @param The return type of the function + /// @return The closure + /// @apiNote For instance methods only. + public static Closure function(Object owner, Function function) { + return new Functional(owner, function); + } + + /// Creates a closure backed by the given supplier. + /// + /// @param supplier The supplier to use + /// @param The return type of the supplier + /// @return The closure + /// @apiNote For static methods only. + public static Closure supplier(Supplier supplier) { + return supplier(ReflectionUtils.getCallingClass(), supplier); + } + + /// Creates a closure backed by the given supplier. + /// + /// @param owner The owner of the closure + /// @param supplier The supplier to use + /// @param The return type of the supplier + /// @return The closure + /// @apiNote For instance methods only. + public static Closure supplier(Object owner, Supplier supplier) { + return callable(owner, supplier::get); + } + + /// Creates a closure backed by the given callable. + /// + /// @param callable The callable to use + /// @param The return type of the callable + /// @return The closure + /// @apiNote For static methods only. + public static Closure callable(Callable callable) { + return callable(ReflectionUtils.getCallingClass(), callable); + } + + /// Creates a closure backed by the given callable. + /// + /// @param owner The owner of the closure + /// @param callable The callable to use + /// @param The return type of the callable + /// @return The closure + /// @apiNote For instance methods only. + public static Closure callable(Object owner, Callable callable) { + return new Supplying<>(owner, callable); + } + + /// Creates a closure backed by the given action. + /// + /// @param action The action to execute + /// @param The type of the action + /// @return The closure + /// @apiNote For static methods only. + public static Closure action(Action action) { + return action(ReflectionUtils.getCallingClass(), action); + } + + /// Creates a closure backed by the given action. + /// + /// @param owner The owner of the closure + /// @param action The action to execute + /// @param The type of the action + /// @return The closure + /// @apiNote For instance methods only. + public static Closure action(Object owner, Action action) { + return consumer(owner, action::execute); + } + + /// Creates an action backed by the given closure. + /// + /// @param closure The closure to call + /// @param The type of the action + /// @return The action + public static Action toAction(Closure closure) { + return it -> Closures.invoke(closure, it); + } + + /// Creates a closure backed by the given consumer. + /// + /// @param consumer The consumer to execute + /// @param The type of the action + /// @return The closure + /// @apiNote For static methods only. + public static Closure consumer(Consumer consumer) { + return consumer(ReflectionUtils.getCallingClass(), consumer); + } + + /// Creates a closure backed by the given consumer. + /// + /// @param owner The owner of the closure + /// @param consumer The consumer to execute + /// @param The type of the action + /// @return The closure + /// @apiNote For instance methods only. + public static Closure consumer(Object owner, Consumer consumer) { + return new Consuming<>(owner, consumer); + } + + /// Creates a closure backed by the given runnable. + /// + /// @param runnable The runnable to execute + /// @return The closure + /// @apiNote For static methods only. + public static Closure runnable(Runnable runnable) { + return runnable(ReflectionUtils.getCallingClass(), runnable); + } + + /// Creates a closure backed by the given runnable. + /// + /// @param owner The owner of the closure + /// @param runnable The runnable to execute + /// @return The closure + /// @apiNote For instance methods only. + public static Closure runnable(Object owner, Runnable runnable) { + return new Running(owner, runnable); + } + + /// Creates an empty closure. + /// + /// @return The empty closure + /// @apiNote For static methods only. + public static Closure empty() { + return empty(ReflectionUtils.getCallingClass()); + } + + /// Creates an empty closure. + /// + /// @param owner The owner of the closure + /// @return The empty closure + /// @apiNote For instance methods only. + public static Closure empty(Object owner) { + return new Empty(owner); + } + + private static final class Functional extends Closure { + private final Function function; + + private Functional(Object owner, Function function) { + super(owner, owner); + this.function = function; + } + + @SuppressWarnings("unused") // invoked by Groovy + public R doCall(T object) { + return this.function.apply(object); + } + } + + private static final class Supplying extends Closure { + private final Callable supplier; + + private Supplying(Object owner, Callable supplier) { + super(owner, owner); + this.supplier = supplier; + } + + @SuppressWarnings("unused") // invoked by Groovy + public R doCall() throws Exception { + return this.supplier.call(); + } + } + + private static final class Consuming extends Closure { + private final Consumer consumer; + + private Consuming(Object owner, Consumer consumer) { + super(owner, owner); + this.consumer = consumer; + } + + @SuppressWarnings("unused") // invoked by Groovy + public Void doCall(T object) { + this.consumer.accept(object); + return null; + } + } + + private static final class Running extends Empty { + private final Runnable runnable; + + private Running(Object owner, Runnable runnable) { + super(owner); + this.runnable = runnable; + } + + @Override + public Void doCall() { + this.runnable.run(); + return super.doCall(); + } + } + + private static sealed class Empty extends Closure permits Running { + public Empty(Object owner) { + super(owner, owner); + } + + @SuppressWarnings("unused") // invoked by Groovy + public Void doCall() { + return null; + } + } + + private Closures() { } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java new file mode 100644 index 0000000..10a6551 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedPlugin.java @@ -0,0 +1,165 @@ +package net.minecraftforge.gradleutils.shared; + +import org.codehaus.groovy.runtime.InvokerHelper; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.invocation.Gradle; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.jetbrains.annotations.ApiStatus; + +import javax.inject.Inject; +import java.io.File; +import java.util.Objects; + +/// The enhanced plugin contains several helper members to assist in making Gradle plugins as clean as possible without +/// needing to duplicate code across projects. +/// +/// @param The type of target +@ApiStatus.OverrideOnly +public abstract class EnhancedPlugin implements Plugin { + private final String name; + private final String displayName; + + private T target; + private final EnhancedProblems problemsInternal; + + /** + * @see ObjectFactory + * Service Injection + */ + protected abstract @Inject ObjectFactory getObjects(); + + /** + * @see ProviderFactory + * Service Injection + */ + protected abstract @Inject ProviderFactory getProviders(); + + /// This constructor must be called by all subclasses using a public constructor annotated with [Inject]. The name + /// and display name passed in are used in a minimal instance of [EnhancedProblems], which is used to set up the + /// plugin's [global][#getGlobalCaches()] and [local][#getLocalCaches()] caches. Additionally, the name is used to + /// create the cache folders (`minecraftforge/name`). + /// + /// @param name The name for this plugin (must be machine-friendly) + /// @param displayName The display name for this plugin + protected EnhancedPlugin(String name, String displayName) { + this.name = name; + this.displayName = displayName; + + this.problemsInternal = this.getObjects().newInstance(EnhancedProblems.Minimal.class, name, displayName); + } + + /// This method is used by Gradle to apply this plugin. You should instead override [#setup(T)] to do plugin setup. + /// + /// @param target The target for this plugin + @Override + public final void apply(T target) { + this.setup(this.target = target); + } + + /// Called when this plugin is applied to do setup work. + /// + /// @param target The target for this plugin (can also get after setup with [#getTarget()]) + public abstract void setup(T target); + + /// Gets the target for this plugin. This will throw an exception if this is called before application (i.e. through + /// early usage of [#getGlobalCaches()]). + /// + /// @return The plugin target + /// @throws RuntimeException If this plugin is not yet applied + protected final T getTarget() { + try { + return Objects.requireNonNull(this.target); + } catch (Exception e) { + throw this.problemsInternal.pluginNotYetApplied(e); + } + } + + final EnhancedProblems getProblemsInternal() { + return this.problemsInternal; + } + + + /* TOOLS */ + + /// 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. + /// + /// @param tool The tool to get + /// @return A provider for the tool file + @SuppressWarnings("deprecation") // deprecation intentional, please use this method + public Provider getTool(Tool tool) { + return tool.get(this.getGlobalCaches(), this.getProviders()); + } + + + /* CACHES */ + + private final Lazy globalCaches = Lazy.simple(this::makeGlobalCaches); + + /// Gets the global caches to be used for this plugin. These caches persist between projects and should be used to + /// eliminate excess work done by projects that request the same data. + /// + /// It is stored in `~/.gradle/caches/minecraftforge/plugin`. + /// + /// @return The global caches + /// @throws RuntimeException If this plugin cannot access global caches (i.e. the target is not [Project] or + /// [org.gradle.api.initialization.Settings]) + public final DirectoryProperty getGlobalCaches() { + return this.globalCaches.get(); + } + + private DirectoryProperty makeGlobalCaches() { + try { + var startParameter = ((Gradle) InvokerHelper.getPropertySafe(this.target, "gradle")).getStartParameter(); + var gradleUserHomeDir = this.getObjects().directoryProperty().fileValue(startParameter.getGradleUserHomeDir()); + + return this.getObjects().directoryProperty().convention( + gradleUserHomeDir.dir("caches/minecraftforge/" + this.name).map(this.problemsInternal.ensureFileLocation()) + ); + } catch (Exception e) { + throw this.problemsInternal.illegalPluginTarget( + new IllegalArgumentException("Failed to get %s global caches directory for target: %s".formatted(this.displayName, this.target), e), + "types with access to Gradle (#getGradle()Lorg/gradle/api/invocation/Gradle), such as projects or settings." + ); + } + } + + private final Lazy localCaches = Lazy.simple(this::makeLocalCaches); + + /// Gets the local caches to be used for this plugin. Data done by tasks that should not be shared between projects + /// should be stored here. + /// + /// It is located in `project/build/minecraftforge/plugin`. + /// + /// @return The global caches + /// @throws RuntimeException If this plugin cannot access global caches (i.e. the target is not [Project] or + /// [org.gradle.api.initialization.Settings]) + public final DirectoryProperty getLocalCaches() { + return this.localCaches.get(); + } + + private DirectoryProperty makeLocalCaches() { + try { + DirectoryProperty workingProjectBuildDir; + if (this.target instanceof Project project) { + workingProjectBuildDir = project.getLayout().getBuildDirectory(); + } else { + var startParameter = ((Gradle) InvokerHelper.getPropertySafe(this.target, "gradle")).getStartParameter(); + workingProjectBuildDir = this.getObjects().directoryProperty().fileValue(new File(Objects.requireNonNullElseGet(startParameter.getProjectDir(), startParameter::getCurrentDir), "build")); + } + + return this.getObjects().directoryProperty().convention( + workingProjectBuildDir.dir("minecraftforge/" + this.name).map(this.problemsInternal.ensureFileLocation()) + ); + } catch (Exception e) { + throw this.problemsInternal.illegalPluginTarget( + new IllegalArgumentException("Failed to get %s local caches directory for target: %s".formatted(this.displayName, this.getTarget()), e), + "projects or types with access to Gradle (#getGradle()Lorg/gradle/api/invocation/Gradle), such as settings." + ); + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedProblems.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedProblems.java new file mode 100644 index 0000000..1ccf7d3 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedProblems.java @@ -0,0 +1,325 @@ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.Action; +import org.gradle.api.Task; +import org.gradle.api.Transformer; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.problems.Problem; +import org.gradle.api.problems.ProblemGroup; +import org.gradle.api.problems.ProblemId; +import org.gradle.api.problems.ProblemReporter; +import org.gradle.api.problems.ProblemSpec; +import org.gradle.api.problems.Problems; +import org.gradle.api.problems.Severity; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.reflect.HasPublicType; +import org.gradle.api.reflect.TypeOf; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collection; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/// The enhanced problems contain several base helper members to help reduce duplicate code between Gradle plugins. +@ApiStatus.OverrideOnly +public abstract class EnhancedProblems implements Problems { + /// The common message to send in [ProblemSpec#solution(String)] when reporting problems. + protected static final String HELP_MESSAGE = "Consult the documentation or ask for help on the Forge Forums, GitHub, or Discord server."; + + private final String displayName; + private final ProblemGroup problemGroup; + + private final Problems delegate; + private final Predicate properties; + + @Override + public ProblemReporter getReporter() { + return this.delegate.getReporter(); + } + + /// Gets the problem group used by this problems instance. It is unique for the plugin. + /// + /// @return The problem group + public final ProblemGroup getProblemGroup() { + return this.problemGroup; + } + + /// Creates a new enhanced problems instance using the given name and display name. These names are passed into a + /// problem group that will be used by this instance. + /// + /// @param name The name for this enhanced problems instance + /// @param displayName The display name for this enhanced problems instance + /// @implSpec The implementing subclass must make their constructor public, annotated with + /// [Inject], and have no parameters, passing in static strings to this base constructor. + protected EnhancedProblems(String name, String displayName) { + this.problemGroup = ProblemGroup.create(name, this.displayName = displayName); + + this.delegate = this.unwrapProblems(); + this.properties = this.unwrapProperties(); + } + + /// Creates a problem ID to be used when reporting problems. The name must be unique so as to not potentially + /// override other reported problems in the report. + /// + /// @param name The name for this problem + /// @param displayName The display name for this problem + /// @return The problem ID + protected final ProblemId id(String name, String displayName) { + return ProblemId.create(name, displayName, this.getProblemGroup()); + } + + /// Checks if the given property exists and equals `true`. This checks both [Gradle][ProviderFactory#gradleProperty] + /// and [System][ProviderFactory#systemProperty] properties, giving the former higher priority. If for some reason a + /// provider factory is not available in the current environment, [Boolean#getBoolean(String)] will be used + /// instead. + /// + /// @param property The property to test + /// @return If the property exists and is `true` + protected final boolean hasProperty(String property) { + return this.properties.test(property); + } + + + /* DEFAULT PROBLEMS */ + + //region Enhanced Plugin + + /// Reports an illegal plugin target. + /// + /// @param e The exception that was caught, will be re-thrown (or wrapped with a + /// [RuntimeException]) + /// @param firstAllowedTarget The first allowed target for the plugin + /// @param allowedTargets The remaining allowed targets for the plugin + /// @return The exception to throw + public final RuntimeException illegalPluginTarget(Exception e, Class firstAllowedTarget, Class... allowedTargets) { + return this.getReporter().throwing(e, id("invalid-plugin-target", "Invalid plugin target"), spec -> spec + .details(""" + Attempted to apply the %s plugin to an invalid target. + This plugin can only be applied on the following types: + %s""".formatted(this.displayName, Stream.concat(Stream.of(firstAllowedTarget), Stream.of(allowedTargets)).map(Class::getName).collect(Collectors.joining(", ", "[", "]")))) + .severity(Severity.ERROR) + .stackLocation() + .solution("Use a valid plugin target.") + .solution(HELP_MESSAGE)); + } + + /// Reports an illegal plugin target. + /// + /// @param e The exception that was caught, will be re-thrown (or wrapped with a [RuntimeException]) + /// @param allowedTargets A string stating the allowed targets for the plugin + /// @return The exception to throw + public final RuntimeException illegalPluginTarget(Exception e, String allowedTargets) { + return this.getReporter().throwing(e, id("invalid-plugin-target", "Invalid plugin target"), spec -> spec + .details(""" + Attempted to apply the %s plugin to an invalid target. + This plugin can only be applied on %s""".formatted(this.displayName, allowedTargets)) + .severity(Severity.ERROR) + .stackLocation() + .solution("Use a valid plugin target.") + .solution(HELP_MESSAGE)); + } + + /// Reports an illegal access of a plugin before it has been applied. + /// + /// @param e The exception that was caught, will be re-thrown (or wrapped with a [RuntimeException]) + /// @return The exception to throw + public final RuntimeException pluginNotYetApplied(Exception e) { + return this.getReporter().throwing(e, id("plugin-not-yet-applied", "%s is not applied".formatted(this.displayName)), spec -> spec + .details(""" + Attempted to get details from the %s plugin, but it has not yet been applied to the target.""".formatted(this.displayName)) + .severity(Severity.ERROR) + .stackLocation() + .solution("Apply the plugin before attempting to use it from the target's plugin manager.") + .solution("Apply the plugin before attempting to register any of its tasks, especially those that require in-house caching or tools.") + .solution(HELP_MESSAGE) + ); + } + //endregion + + //region ToolExecBase + + /// Reports an implementation of [ToolExecBase] that is not enhanced with [EnhancedTask]. + /// + /// @param task The affected task + public final void reportToolExecNotEnhanced(Task task) { + this.getReporter().report(id("tool-exec-not-enhanced", "ToolExec subclass doesn't implement EnhancedTask"), spec -> spec + .details(""" + Implementing subclass of ToolExecBase should also implement (a subclass of) EnhancedTask. + Not doing so will result in global caches being ignored. Please check your implementations. + Affected task: %s (%s)""".formatted(task, task.getClass())) + .severity(Severity.WARNING) + .stackLocation() + .solution("Double check your task implementation.")); + } + + /// Reports an implementation of [ToolExecBase] that adds arguments without using [ToolExecBase#addArguments()] + /// + /// @param task The affected task + public final void reportToolExecEagerArgs(Task task) { + this.getReporter().report(id("tool-exec-eager-args", "ToolExecBase implementation adds arguments without using addArguments()"), spec -> spec + .details(""" + A ToolExecBase task is eagerly adding arguments using JavaExec#args without using ToolExecBase#addArguments. + This may cause implementations or superclasses to have their arguments ignored or missing. + Affected task: %s (%s)""".formatted(task, task.getClass())) + .severity(Severity.WARNING) + .stackLocation() + .solution("Use ToolExecBase#addArguments")); + } + //endregion + + //region Utilities + + /// A utility method to ensure that a [FileSystemLocation] [Provider] has (its parent) directory created. If the + /// directory cannot be created, an exception will be thrown when the provider that consumes this is resolved. + /// + /// @param The type of file system location (i.e. [org.gradle.api.file.RegularFile] or [Directory]) + /// @return The transformer to apply onto a provider + public final Transformer ensureFileLocation() { + return file -> { + var dir = file instanceof Directory ? file.getAsFile() : file.getAsFile().getParentFile(); + try { + Files.createDirectories(dir.toPath()); + } catch (IOException e) { + throw this.getReporter().throwing(e, id("cannot-ensure-directory", "Failed to create directory"), spec -> spec + .details(""" + Failed to create a directory required for %s to function. + Directory: %s""" + .formatted(this.displayName, dir.getAbsolutePath())) + .severity(Severity.ERROR) + .stackLocation() + .solution("Ensure that the you have write access to the directory that needs to be created.") + .solution(HELP_MESSAGE)); + } + + return file; + }; + } + //endregion + + + /* EMPTY */ + + private interface EmptyReporter extends ProblemReporter, HasPublicType { + EmptyReporter INSTANCE = new EmptyReporter() { }; + Problems AS_PROBLEMS = () -> INSTANCE; + + @Override + default TypeOf getPublicType() { + return TypeOf.typeOf(ProblemReporter.class); + } + + @Override + default Problem create(ProblemId problemId, Action action) { + return new Problem() { }; + } + + @Override + default void report(ProblemId problemId, Action spec) { } + + @Override + default void report(Problem problem) { } + + @Override + default void report(Collection problems) { } + + @Override + default RuntimeException throwing(Throwable exception, ProblemId problemId, Action spec) { + return this.toRTE(exception); + } + + @Override + default RuntimeException throwing(Throwable exception, Problem problem) { + return this.toRTE(exception); + } + + @Override + default RuntimeException throwing(Throwable exception, Collection problems) { + return this.toRTE(exception); + } + + private RuntimeException toRTE(Throwable exception) { + return exception instanceof RuntimeException rte ? rte : new RuntimeException(exception); + } + } + + + /* MINIMAL */ + + static abstract class Minimal extends EnhancedProblems implements HasPublicType { + @Inject + public Minimal(String name, String displayName) { + super(name, displayName); + } + + @Override + public TypeOf getPublicType() { + return TypeOf.typeOf(EnhancedProblems.class); + } + } + + + /* IMPL INSTANTIATION */ + + /** @see Reporting Problems */ + protected @Inject Problems getProblems() { + throw new IllegalStateException(); + } + + /** + * @see ProviderFactory + * Service Injection + */ + protected @Inject ProviderFactory getProviders() { + throw new IllegalStateException(); + } + + private Problems unwrapProblems() { + try { + return this.getProblems(); + } catch (Exception e) { + return EmptyReporter.AS_PROBLEMS; + } + } + + private Predicate unwrapProperties() { + try { + var providers = Objects.requireNonNull(this.getProviders()); + return property -> isTrue(providers, property); + } catch (Exception e) { + return Boolean::getBoolean; + } + } + + + /* IMPL UTILS */ + + private static @Nullable Boolean getBoolean(Provider provider) { + if (Boolean.TRUE.equals(provider.map("true"::equalsIgnoreCase).getOrNull())) return true; + if (Boolean.FALSE.equals(provider.map("false"::equalsIgnoreCase).getOrNull())) return false; + return null; + } + + private static boolean isTrue(ProviderFactory providers, String property) { + return isTrue(providers.gradleProperty(property)) || isTrue(providers.systemProperty(property)); + } + + private static boolean isTrue(Provider provider) { + return Boolean.TRUE.equals(getBoolean(provider)); + } + + private static boolean isFalse(ProviderFactory providers, String property) { + return isFalse(providers.gradleProperty(property)) || isFalse(providers.systemProperty(property)); + } + + private static boolean isFalse(Provider provider) { + return Boolean.FALSE.equals(getBoolean(provider)); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedTask.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedTask.java new file mode 100644 index 0000000..dcb253c --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/EnhancedTask.java @@ -0,0 +1,52 @@ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Internal; + +/// The enhanced task contains a handful of helper methods to make working with the enhanced plugin and caches easier. +/// +/// @param The type of enhanced plugin +public interface EnhancedTask> extends Task { + /// The enhanced plugin type for this task. + /// + /// @return The plugin type + @Internal + Class getPluginType(); + + /// The enhanced plugin associated with this task. + /// + /// @return The plugin + @Internal + default T getPlugin() { + return this.getProject().getPlugins().getPlugin(this.getPluginType()); + } + + /// The default output directory to use for this task if it outputs a directory. + /// + /// @return A provider for the directory + @Internal + default Provider getDefaultOutputDirectory() { + return this.getPlugin().getLocalCaches().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 + @Internal + default 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 + @Internal + default Provider getDefaultOutputFile(String ext) { + return this.getPlugin().getLocalCaches().file("%s/output.%s".formatted(this.getName(), ext)).map(this.getPlugin().getProblemsInternal().ensureFileLocation()); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/Lazy.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/Lazy.java new file mode 100644 index 0000000..b3a608c --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/Lazy.java @@ -0,0 +1,155 @@ +package net.minecraftforge.gradleutils.shared; + +import groovy.lang.Closure; +import org.gradle.api.Action; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +/// This is a simple implementation of a [groovy.lang.Lazy] value, primarily aimed for use in Java code. +/// +/// @param The type of result +/// @apiNote This lazy implementation uses Groovy's [closures][Closure] instead of typical [suppliers][Supplier] or +/// [callables][Callable], as the closure API allows chaining via [Closure#compose(Closure)] and +/// [Closure#andThen(Closure)]. They can still be created using callables. +/// @see Lazy.Actionable +public sealed class Lazy implements Supplier, Callable permits Lazy.Actionable { + /// Creates a simple lazy of the given callable. + /// + /// @param The return type of the callable + /// @param callable The callable to use + /// @return The lazy value + public static Lazy simple(Callable callable) { + return simple(Closures.callable(callable)); + } + + /// Creates a simple lazy of the given closure. + /// + /// @param The return type of the closure + /// @param closure The callable to use + /// @return The lazy value + public static Lazy simple(Closure closure) { + return new Lazy<>(closure); + } + + /// Creates an actionable lazy of the given callable. + /// + /// @param The return type of the callable + /// @param callable The callable to use + /// @return The lazy value + public static Actionable actionable(Callable callable) { + return actionable(Closures.callable(callable)); + } + + /// Creates an actionable lazy of the given closure. + /// + /// @param The return type of the closure + /// @param closure The callable to use + /// @return The lazy value + public static Actionable actionable(Closure closure) { + return new Actionable<>(closure); + } + + /// The closure that will provide the value for this lazy. + protected final Closure closure; + /// The value of this lazy, will be `null` if it has not yet been computed with [#get()]. + protected @Nullable T value; + + private Lazy(Closure closure) { + this.closure = closure; + } + + /// Checks if this lazy value is present. It is not a requirement that the value has to be resolved. + /// + /// For simple lazies, it is simply a check if the value has been resolved. See [Actionable#isPresent()] for + /// actionable lazies. + /// + /// @return If this lazy is present + public boolean isPresent() { + return this.value != null; + } + + /// Runs the given action on this lazy value if it is present. + /// + /// @param action The action to run + /// @see #isPresent() + public final void ifPresent(Action action) { + if (this.isPresent()) + action.execute(this.get()); + } + + /// Gets (and resolves if absent) this lazy value. + /// + /// @return The value + @Override + public T get() { + return this.value == null ? this.value = Closures.invoke(this.closure) : this.value; + } + + @Override + public T call() { + return this.get(); + } + + /// Represents a lazily computed value with the ability to optionally work with it using [#ifPresent(Action)] and + /// safely mutate it using [#map(Action)]. + /// + /// @param The type of result + public static final class Actionable extends Lazy { + private boolean present = false; + + private Closure modifications = Closures.unaryOperator(o -> { + this.present = true; + return o; + }); + + private Actionable(Closure closure) { + super(closure); + } + + /// Queues the given action to run on the value once it has been computed. If the value has already been + /// resolved, the action will be executed instantly. + /// + /// This marks this lazy as present, meaning that any usage of [#ifPresent(Action)] will resolve the value. + /// + /// @param action The action to run + public void map(Action action) { + this.present = true; + if (this.value != null) { + action.execute(this.value); + } else { + this.modifications = this.modifications.andThen(Closures.unaryOperator(value -> { + action.execute(value); + return value; + })); + } + } + + /// Checks if this actionable lazy is present. Presence can either mean that the value has already been computed + /// or has been mutated by [#map(Action)]. + /// + /// @return If this actionable lazy is present + @Override + public boolean isPresent() { + return this.present; + } + + /// Copies this actionable lazy. This can be useful if you need to split off execution paths and have the same + /// object with different mutations at a specific time. + /// + /// @return The a new actionable lazy copied from this one + public Actionable copy() { + var ret = new Actionable<>(this.closure); + ret.value = this.value; + ret.present = this.present; + ret.modifications = this.modifications; + return ret; + } + + @Override + public T get() { + return this.value == null ? this.value = Closures.invoke(this.closure.andThen(this.modifications)) : this.value; + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/SharedUtil.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/SharedUtil.java new file mode 100644 index 0000000..12144f8 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/SharedUtil.java @@ -0,0 +1,213 @@ +package net.minecraftforge.gradleutils.shared; + +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import groovy.transform.stc.ClosureParams; +import groovy.transform.stc.FirstParam; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.FileCollectionDependency; +import org.gradle.api.artifacts.ModuleVersionSelector; +import org.gradle.api.plugins.ExtensionAware; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +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 java.io.File; +import java.io.OutputStream; + +/// Shared utilities for Gradle plugins. +public abstract class SharedUtil { + //region Java Launcher + + /// Gets the Java launcher that [can compile or run][JavaLanguageVersion#canCompileOrRun(JavaLanguageVersion)] the + /// given version. + /// + /// If the currently running Java toolchain is able to compile and run the given version, it will be used instead. + /// + /// @param java The Java plugin extension of the currently-used toolchain + /// @param javaToolchains The Java toolchain service to get the Java launcher from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherFor(JavaPluginExtension java, JavaToolchainService javaToolchains, int version) { + return launcherFor(java, javaToolchains, JavaLanguageVersion.of(version)); + } + + /// Gets the Java launcher that [can compile or run][JavaLanguageVersion#canCompileOrRun(JavaLanguageVersion)] the + /// given version. + /// + /// If the currently running Java toolchain is able to compile and run the given version, it will be used instead. + /// + /// @param java The Java plugin extension of the currently-used toolchain + /// @param javaToolchains The Java toolchain service to get the Java launcher from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherFor(JavaPluginExtension java, JavaToolchainService javaToolchains, JavaLanguageVersion version) { + JavaToolchainSpec currentToolchain = java.getToolchain(); + return currentToolchain.getLanguageVersion().getOrElse(JavaLanguageVersion.current()).canCompileOrRun(version) + ? javaToolchains.launcherFor(currentToolchain) + : launcherForStrictly(javaToolchains, version); + } + + /// Gets the Java launcher that [can compile or run][JavaLanguageVersion#canCompileOrRun(JavaLanguageVersion)] the + /// given version. + /// + /// If the currently running Java toolchain is able to compile and run the given version, it will be used instead. + /// + /// @param object The extension-aware object to get the Java extensions from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherFor(ExtensionAware object, int version) { + var extensions = object.getExtensions(); + return launcherFor(extensions.getByType(JavaPluginExtension.class), extensions.getByType(JavaToolchainService.class), version); + } + + /// Gets the Java launcher that [can compile or run][JavaLanguageVersion#canCompileOrRun(JavaLanguageVersion)] the + /// given version. + /// + /// If the currently running Java toolchain is able to compile and run the given version, it will be used instead. + /// + /// @param object The extension-aware object to get the Java extensions from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherFor(ExtensionAware object, JavaLanguageVersion version) { + var extensions = object.getExtensions(); + return launcherFor(extensions.getByType(JavaPluginExtension.class), extensions.getByType(JavaToolchainService.class), version); + } + + /// Gets the Java launcher strictly for the given version, even if the currently running Java toolchain is higher + /// than it. + /// + /// @param javaToolchains The Java toolchain service to get the Java launcher from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherForStrictly(JavaToolchainService javaToolchains, int version) { + return launcherForStrictly(javaToolchains, JavaLanguageVersion.of(version)); + } + + /// Gets the Java launcher strictly for the given version, even if the currently running Java toolchain is higher + /// than it. + /// + /// @param javaToolchains The Java toolchain service to get the Java launcher from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherForStrictly(JavaToolchainService javaToolchains, JavaLanguageVersion version) { + return javaToolchains.launcherFor(spec -> spec.getLanguageVersion().set(version)); + } + + /// Gets the Java launcher strictly for the given version, even if the currently running Java toolchain is higher + /// than it. + /// + /// @param object The extension-aware object to get the Java extensions from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherForStrictly(ExtensionAware object, int version) { + return launcherForStrictly(object.getExtensions().getByType(JavaToolchainService.class), version); + } + + /// Gets the Java launcher strictly for the given version, even if the currently running Java toolchain is higher + /// than it. + /// + /// @param object The extension-aware object to get the Java extensions from + /// @param version The version of Java required + /// @return A provider for the Java launcher + public static Provider launcherForStrictly(ExtensionAware object, JavaLanguageVersion version) { + return launcherForStrictly(object.getExtensions().getByType(JavaToolchainService.class), version); + } + //endregion + + //region Project Eval + + /// Runs the given closure using [Project#afterEvaluate(Action)]. If the project is already executed, the closure + /// will be called instantly. + /// + /// @param project The project to run the closure on + /// @param closure The closure to execute + public static void ensureAfterEvaluate( + Project project, + @DelegatesTo(value = Project.class, strategy = Closure.DELEGATE_FIRST) + @ClosureParams(value = FirstParam.class) + Closure closure + ) { + ensureAfterEvaluate(project, Closures.toAction(closure)); + } + + /// Runs the given action using [Project#afterEvaluate(Action)]. If the project is already executed, the action will + /// be executed instantly. + /// + /// @param project The project to run the action on + /// @param action The action to execute + public static void ensureAfterEvaluate(Project project, Action action) { + if (project.getState().getExecuted()) + action.execute(project); + else + project.afterEvaluate(action); + } + //endregion + + //region Action Logging + + /// Creates an output stream that logs to the given action. + /// + /// @param logger The logger to log to + /// @return The output stream + public static OutputStream toLog(Action logger) { + return new OutputStream() { + private StringBuffer buffer = new StringBuffer(512); + + @Override + public void write(int b) { + if (b == '\r' || b == '\n') { + if (!this.buffer.isEmpty()) { + logger.execute(this.buffer.toString()); + this.buffer = new StringBuffer(512); + } + } else { + this.buffer.append(b); + } + } + }; + } + //endregion + + //region toString() + + /// Converts a given module to string. Use this instead of [Object#toString()]. + /// + /// @param module The module + /// @return The string representation + public static String toString(ModuleVersionSelector module) { + var version = module.getVersion(); + return "%s:%s%s".formatted( + module.getGroup(), + module.getName(), + version != null ? ':' + version : "" + ); + } + + /// Converts a given dependency to string. Use this instead of [Object#toString()]. + /// + /// @param dependency The dependency + /// @return The string representation + public static String toString(Dependency dependency) { + var group = dependency.getGroup(); + var version = dependency.getVersion(); + var reason = dependency.getReason(); + return "(%s) %s%s%s%s%s".formatted( + dependency.getClass().getName(), + group != null ? group + ':' : "", + dependency.getName(), + version != null ? ':' + version : "", + reason != null ? " (" + reason + ')' : "", + dependency instanceof FileCollectionDependency files ? " [%s]".formatted(String.join(", ", files.getFiles().getFiles().stream().map(File::getAbsolutePath).map(CharSequence.class::cast)::iterator)) : "" + ); + } + //endregion + + /// Empty constructor. This class should only be extended to make referencing these static methods easier. + protected SharedUtil() { } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/Tool.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/Tool.java new file mode 100644 index 0000000..dfc3df4 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/Tool.java @@ -0,0 +1,72 @@ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.file.Directory; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +/// Tools are definitions of Java libraries (may or may not be executable) that are managed by Gradle using a +/// [org.gradle.api.provider.ValueSource]. This means that while the downloading and local caching of this file are done +/// in house, the Gradle-specific caching and file tracking are done by Gradle. This enables the usage of downloading +/// external files quickly without breaking caches. +public sealed interface Tool permits ToolImpl { + /// Creates a new tool with the given information. + /// + /// @param name The name for this tool (will be used in the file name) + /// @param version The version for this tool (will be used in the file name) + /// @param downloadUrl The download URL for this tool + /// @param javaVersion The Java version this tool was built with, or should run on + /// @param mainClass The main class to use when executing this tool + /// @return The tool + static Tool of(String name, String version, String downloadUrl, int javaVersion, String mainClass) { + return new ToolImpl(name, version, downloadUrl, javaVersion, mainClass); + } + + /// Creates a new tool with the given information. + /// + /// @param name The name for this tool (will be used in the file name) + /// @param version The version for this tool (will be used in the file name) + /// @param downloadUrl The download URL for this tool + /// @param javaVersion The Java version this tool was built with, or should run on + /// @return The tool + static Tool of(String name, String version, String downloadUrl, int javaVersion) { + return new ToolImpl(name, version, downloadUrl, javaVersion, null); + } + + /// The name for this tool. Primarily used by [ToolExecBase] to create a default tool directory. + /// + /// @return The name of this tool + String getName(); + + /// The Java version this tool was built with. Primarily used by [ToolExecBase] to determine the + /// [org.gradle.jvm.toolchain.JavaLauncher]. + /// + /// @return The Java version + int getJavaVersion(); + + /// The main class to use when executing this tool. Can be `null`, but does not necessarily mean that the tool is + /// not executable. + /// + /// @return The main class, or `null` if unspecified + @Nullable String getMainClass(); + + /// Gets this tool and returns a provider for the downloaded/cached file. + /// + /// @param cachesDir The caches directory to store the downloaded tool in + /// @param providers The provider factory for creating the provider + /// @return The provider to the tool file + /// @deprecated Use [EnhancedPlugin#getTool(Tool)] + Provider get(Provider cachesDir, ProviderFactory providers); + + /// Gets this tool and returns a provider for the downloaded/cached file. + /// + /// @param cachesDir The caches directory to store the downloaded tool in + /// @param providers The provider factory for creating the provider + /// @return The provider to the tool file + /// @deprecated Use [EnhancedPlugin#getTool(Tool)] + default Provider get(Directory cachesDir, ProviderFactory providers) { + return this.get(providers.provider(() -> cachesDir), providers); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolExecBase.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolExecBase.java new file mode 100644 index 0000000..656988d --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolExecBase.java @@ -0,0 +1,144 @@ +package net.minecraftforge.gradleutils.shared; + +import org.gradle.api.Transformer; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.file.FileSystemLocationProperty; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.provider.Provider; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.JavaExec; +import org.jetbrains.annotations.MustBeInvokedByOverriders; + +import javax.inject.Inject; +import java.io.File; +import java.util.Locale; +import java.util.Objects; + +/// This tool execution task is a template on top of [JavaExec] to make executing [tools][Tool] much easier and more +/// consistent between plugins. +/// +/// @param

The type of enhanced problems, used for common problems reporting with illegal task arguments +/// @see JavaExec +/// @see Tool +public abstract class ToolExecBase

extends JavaExec { + private final P problems; + /// The default tool directory (usage is not required). + protected final @Internal DirectoryProperty defaultToolDir; + + /** + * @see ProjectLayout + * Service Injection + */ + protected abstract @Inject ProjectLayout getProjectLayout(); + + /// Creates a new task instance using the given types and tool information. + /// + /// @param problemsType The type of problems to use for this task (accessible via [#getProblems()]) + /// @param tool The tool to use for this task + /// @implSpec The implementing subclass must make their constructor public, annotated with + /// [Inject], and have only a single parameter for [Tool], passing in static plugin and problems types to this base + /// constructor. The types must also be manually specified in the class declaration when overriding this class. The + /// best practice is to make a single `ToolExec` class for the implementing plugin to use, which other tasks can + /// extend off of. + protected ToolExecBase(Class

problemsType, Tool tool) { + this.problems = this.getObjectFactory().newInstance(problemsType); + + if (this instanceof EnhancedTask enhancedTask) { + this.defaultToolDir = this.getObjectFactory().directoryProperty().value( + enhancedTask.getPlugin().getGlobalCaches().dir(tool.getName().toLowerCase(Locale.ENGLISH)).map(this.ensureFileLocationInternal()) + ); + this.setClasspath(this.getObjectFactory().fileCollection().from(enhancedTask.getPlugin().getTool(tool))); + } else { + this.getProject().afterEvaluate(project -> this.problems.reportToolExecNotEnhanced(this)); + + this.defaultToolDir = this.getObjectFactory().directoryProperty().value( + this.getProjectLayout().getBuildDirectory().dir("minecraftforge/tools/%s/workDir".formatted(tool.getName().toLowerCase(Locale.ENGLISH))).map(this.ensureFileLocationInternal()) + ); + this.setClasspath(this.getObjectFactory().fileCollection().from(tool.get( + this.getProjectLayout().getBuildDirectory().dir("minecraftforge/tools/" + tool.getName().toLowerCase(Locale.ENGLISH)).map(this.ensureFileLocationInternal()), + this.getProviderFactory() + ))); + } + + this.defaultToolDir.disallowChanges(); + this.defaultToolDir.finalizeValueOnRead(); + + this.getMainClass().convention(Objects.requireNonNull(tool.getMainClass(), "Tool must have a main class")); + this.getJavaLauncher().convention(SharedUtil.launcherForStrictly(this.getJavaToolchainService(), tool.getJavaVersion())); + } + + /// The enhanced problems instance to use for this task. + /// + /// @return The enhanced problems + protected final P getProblems() { + return this.problems; + } + + private Transformer ensureFileLocationInternal() { + return t -> this.problems.ensureFileLocation().transform(t); + } + + /// This method should be overridden by subclasses to add arguments to this task via [JavaExec#args]. To preserve + /// arguments added by superclasses, this method [must be invoked by overriders][MustBeInvokedByOverriders]. + @MustBeInvokedByOverriders + protected void addArguments() { } + + @Override + public void exec() { + if (this.getArgs().isEmpty()) + this.addArguments(); + else + this.problems.reportToolExecEagerArgs(this); + + this.getLogger().info("{} {}", this.getClasspath().getAsPath(), String.join(" ", this.getArgs())); + + super.exec(); + } + + /// 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 + protected final void args(String arg, Iterable files) { + for (var 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 + protected final 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 + protected final void args(String arg, Provider provider) { + var value = provider.map(it -> it instanceof FileSystemLocation f ? f.getAsFile() : it).get(); + + this.args(arg, String.valueOf(value)); + } + + /// Adds the given argument if and only if the given boolean property is [present][Provider#isPresent()] and true. + /// + /// @param arg The argument to add + /// @param onlyIf The provider to test + protected final void argOnlyIf(String arg, Provider onlyIf) { + this.argOnlyIf(arg, task -> onlyIf.isPresent() && onlyIf.getOrElse(false)); + } + + /// Adds the given argument if and only if the given spec, using this task, is satisfied. + /// + /// @param arg The argument to add + /// @param onlyIf The spec to test + protected final void argOnlyIf(String arg, Spec> onlyIf) { + if (onlyIf.isSatisfiedBy(this)) + this.args(arg); + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolImpl.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolImpl.java new file mode 100644 index 0000000..1bdf004 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/ToolImpl.java @@ -0,0 +1,75 @@ +package net.minecraftforge.gradleutils.shared; + +import net.minecraftforge.util.download.DownloadUtils; +import net.minecraftforge.util.hash.HashStore; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.api.provider.ValueSource; +import org.gradle.api.provider.ValueSourceParameters; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; + +record ToolImpl(String getName, String version, String fileName, String downloadUrl, int getJavaVersion, @Nullable String getMainClass) implements Tool { + private static final Logger LOGGER = Logging.getLogger(Tool.class); + + ToolImpl(String name, String version, String downloadUrl, int javaVersion, @Nullable String mainClass) { + this(name, version, "%s-%s.jar".formatted(name, version), downloadUrl, javaVersion, mainClass); + } + + @Override + public Provider get(Provider cachesDir, ProviderFactory providers) { + return providers.of(Source.class, spec -> spec.parameters(parameters -> { + parameters.getInputFile().set(cachesDir.map(d -> d.file("tools/" + this.fileName))); + parameters.getDownloadUrl().set(this.downloadUrl); + })); + } + + static abstract class Source implements ValueSource { + interface Parameters extends ValueSourceParameters { + RegularFileProperty getInputFile(); + + Property getDownloadUrl(); + } + + @Inject + public Source() { } + + @Override + public File obtain() { + var parameters = this.getParameters(); + + // inputs + var downloadUrl = parameters.getDownloadUrl().get(); + + // outputs + var outFile = parameters.getInputFile().get().getAsFile(); + var name = outFile.getName(); + + // in-house caching + var cache = HashStore.fromFile(outFile).add("url", downloadUrl); + + if (outFile.exists() && cache.isSame()) { + LOGGER.info("Default tool already downloaded: {}", name); + } else { + LOGGER.info("Downloading default tool: {}", name); + try { + DownloadUtils.downloadFile(outFile, downloadUrl); + } catch (IOException e) { + throw new RuntimeException("Failed to download default tool: " + name, e); + } + + cache.save(); + } + + return outFile; + } + } +} diff --git a/src/main/groovy/net/minecraftforge/gradleutils/shared/package-info.java b/src/main/groovy/net/minecraftforge/gradleutils/shared/package-info.java new file mode 100644 index 0000000..bbac1f7 --- /dev/null +++ b/src/main/groovy/net/minecraftforge/gradleutils/shared/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains common code that is shared between MinecraftForge's Gradle plugins. The purpose is to reduce + * duplicate code and keep the majority of complex implementation details here, instead of in the implementing plugins. + *

The majority of these implementations consist of "enhanced" types of existing Gradle types, which are extensions + * that include Forge-specific helper methods.

+ **/ +@ApiStatus.Internal +package net.minecraftforge.gradleutils.shared; + +import org.jetbrains.annotations.ApiStatus; diff --git a/src/main/resources/META-INF/services/io.freefair.gradle.plugins.maven.javadoc.JavadocLinkProvider b/src/main/resources/META-INF/services/io.freefair.gradle.plugins.maven.javadoc.JavadocLinkProvider new file mode 100644 index 0000000..68e96ad --- /dev/null +++ b/src/main/resources/META-INF/services/io.freefair.gradle.plugins.maven.javadoc.JavadocLinkProvider @@ -0,0 +1 @@ +net.minecraftforge.gradleutils.services.GradleAPIJavadocLinkProvider \ No newline at end of file