I’m in the process of creating a Gradle plugin using the Java language. To allow users of the plugin to specify variable values from external sources, I’ve utilized extensions. However, when trying to use this extension in build.gradle, I encounter an error message saying ‘Could not find method ~’. What could be the reason? Below are my code and the error message.
buid.gradle
ssv {
isDateInBuildArtifactDirPath = true;
}
extension class
package io.github.mainmethod0126.gradle.simple.versioning.extension;
import org.gradle.api.Project;
import io.github.mainmethod0126.gradle.simple.versioning.utils.DateUtils;
/**
*
* I wanted to create it as a singleton object, but it seemed like we would
* always have to pass the 'project' object as an argument when calling
* {@code getInstance()} from outside. Therefore, I created the object to be
* conveniently used after calling the initial {@code initExtension()} function.
*
* warn! : This class was not designed with consideration for multi-threading
* and is not thread-safe. Please be cautious when using it in a multi-threaded
* environment.
*/
public class SimpleSemanticVersionPluginExtension {
/**
* This is not a singleton object, but rather a global variable used for the
* convenience of users.
*/
private static SimpleSemanticVersionPluginExtension extension;
public static void init(Project project) {
if (extension == null) {
extension = project.getExtensions().create("ssv",
SimpleSemanticVersionPluginExtension.class);
}
}
public static SimpleSemanticVersionPluginExtension getExtension() {
return extension;
}
public String buildDate = DateUtils.getCurrentDate(DateUtils.DateUnit.DAY);
public boolean isDateInBuildArtifactDirPath = false;
public String applicationVersion = "0.0.0";
public boolean isDateInBuildArtifactDirPath() {
return isDateInBuildArtifactDirPath;
}
public void setDateInBuildPath(boolean isDateInBuildArtifactDirPath) {
this.isDateInBuildArtifactDirPath = isDateInBuildArtifactDirPath;
}
public String getBuildDate() {
return buildDate;
}
public void setBuildDate(String buildDate) {
this.buildDate = buildDate;
}
public String getApplicationVersion() {
return applicationVersion;
}
public void setApplicationVersion(String applicationVersion) {
this.applicationVersion = applicationVersion;
}
}
1: Task failed with an exception.
-----------
* Where:
Build file 'D:\Project\simple-semantic-version\app\build.gradle' line: 51
* What went wrong:
A problem occurred evaluating project ':app'.
> Could not find method ssv() for arguments [build_iu1hap49qtfsj2q98v84jkvq$_run_closure3$_closure10@5f4a36b0] on extension 'gradlePlugin' of type org.gradle.plugin.devel.GradlePluginDevelopmentExtension.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
The reason is, that you are not adding an extension named ssv but try to configure one named ssv.
Some general advices from a very cursory look at your code:
Consider using Kotlin DSL instead of Groovy DSL for build scripts, it is the default, you immediately get type-safe build scripts, actually helpful error messages if you mess up syntax, and amazingly better IDE support if you use a proper IDE like IntelliJ IDEA.
If you use Groovy DSL (or also Kotlin DSL), don’t use semicolons at line end, that is just visual clutter in Groovy and Kotlin
Never use static state, you actually also just found out why, because that is what bit you in the butt. As you only add the extension to the project if the static variable is null, you add it for the first build where the configuration would also work, but on the 2nd and any further build within the same Gradle daemon and with unchanged classpath, the static variable is already set, so you don’t add the extension, so there is nothing to be configured and that is what the error effectively tells you, that there is no extension with that name. So in most JVM programs it is not the best idea to use static state except for exceptional case, as the state is then exactly that, static. A similar problem had the Spotbugs plugin in the past, as Spotbugs was not designed to be used in-process multiple times and heavily uses static state. So running different builds in the same Gradle daemon with the same Spotbugs version then caused these builds to influence each other, even across totally different projects that were built. This is fixed now by not running Spotbugs in-process but in a separate process. So while it is generally a good advice to use static state very sparesely, in the context of a Gradle plugin it is even more important to not do that, except for exceptional cases.
Typically, the plugin class adds the extension, registers tasks, configures default values and wires extension properties and task properties together
You should not use simple types for extension properties and task properties, but use Property and related types, that makes lazy configuration of your extension and tasks possible and can also save you much boilerplate code. Your extension can then for example as simple as
Don’t use Gradle#addBuildListener, that is not compatible with Configuration Cache which can save much build time unless someone applies a plugin that violates its restrictions. In your case you want to use a dataflow action instead.
Thank you for your answer. I have a follow-up question. In the case where SimpleSemanticVersionPluginExtension is changed to an interface, as in the example code you sent, how do I set the default value? I want to set the default value for the case where the extension is not specified in build.gradle.
As I said, the idiomatic way is to have as little opinion as possible in tasks or extensions and add the opinion like default values in the plugin that creates those extensions and / or tasks. This increases reusability.
If you really want to define the default values within the extension, you could of course make it an abstract class instead and assign the default values in the constructor.
Thank you very much for your response. I have changed the SimpleSemanticVersionPluginExtension class based on your feedback, but I am still getting the same error.
This time, I have included a GitHub link to the longer source code and the original source code in case you are interested in reviewing them.
Additionally, as Kotlin syntax is unfamiliar to me at the moment, I will consider applying it in the future.
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
* User Manual available at https://docs.gradle.org/7.3/userguide/building_java_projects.html
*/
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
}
}
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
id 'application'
id 'com.gradle.plugin-publish' version '1.1.0'
id 'com.github.johnrengelman.shadow' version '7.1.2'
id 'java-gradle-plugin'
}
apply plugin: 'com.github.johnrengelman.shadow'
group = 'io.github.mainmethod0126'
version = "0.1.2"
sourceCompatibility = 11
targetCompatibility = 11
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
}
ssv {
}
compileJava {
options.encoding = 'UTF-8'
}
gradlePlugin {
website = 'https://github.com/mainmethod0126/simple-semantic-version'
vcsUrl = 'https://github.com/mainmethod0126/simple-semantic-version'
version = version
plugins {
greetingsPlugin {
id = 'io.github.mainmethod0126.simple-semantic-version'
displayName = 'This is a plug-in for convenient semantic versioning'
description = 'This is a plug-in for convenient semantic versioning'
tags.set(['java', 'versioning', 'semantic', 'plugins'])
implementationClass = 'io.github.mainmethod0126.gradle.simple.versioning.SemanticVersionManager'
}
}
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
gradlePluginPortal()
google()
}
shadowJar {
baseName = "gradle-semantic-versioning-manager-plugin"
version = version
classifier = null
}
tasks.withType(Test).configureEach {
it.jvmArgs = ["--add-opens=java.base/java.lang=ALL-UNNAMED"]
}
dependencies {
implementation gradleApi()
// https://mvnrepository.com/artifact/org.json/json
implementation 'org.json:json:20220924'
// Use JUnit Jupiter for testing.
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
// This dependency is used by the application.
implementation 'com.google.guava:guava:30.1.1-jre'
// https://mvnrepository.com/artifact/org.assertj/assertj-core
testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2'
}
application {
// Define the main class for the application.
mainClass = 'io.github.mainmethod0126.gradle.simple.versioning.App'
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}
C:\Projects\simple-semantic-version>gradlew build
> Configure project :app
Shadow plugin detected. Will automatically select a fat jar as the main plugin artifact.
FAILURE: Build completed with 2 failures.
1: Task failed with an exception.
-----------
* Where:
Build file 'C:\Projects\simple-semantic-version\app\build.gradle' line: 42
* What went wrong:
A problem occurred evaluating project ':app'.
> Could not find method ssv() for arguments [build_aqhed3g9mdfd31pp53xf8xpvj$_run_closure2@4c2cd9cb] on project ':app' of type org.gradle.api.Project.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
==============================================================================
2: Task failed with an exception.
-----------
* What went wrong:
A problem occurred configuring project ':app'.
> Please configure the `shadowJar` task to not add a classifier to the jar it produces
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
==============================================================================
* Get more help at https://help.gradle.org
Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
See https://docs.gradle.org/7.6/userguide/command_line_interface.html#sec:command_line_warnings
BUILD FAILED in 2s