methodMissing no longer being called on extension after upgrading to Gradle 2.2 - gradle

Our build uses a custom plugin extension in gradle that has dynamic methods. This worked fine in gradle 2.1, but methodMissing is no longer called in 2.2 and I get the following exception (here's the caused by part):
Caused by: org.gradle.api.internal.MissingMethodException: Could not find method common() for arguments [api] on org.gradle.api.internal.artifacts.dsl.DefaultComponentModuleMetadataHandler_Decorated#1bef1304.
at org.gradle.api.internal.AbstractDynamicObject.methodMissingException(AbstractDynamicObject.java:68)
at org.gradle.api.internal.AbstractDynamicObject.invokeMethod(AbstractDynamicObject.java:56)
at org.gradle.api.internal.CompositeDynamicObject.invokeMethod(CompositeDynamicObject.java:172)
at org.gradle.api.internal.artifacts.dsl.DefaultComponentModuleMetadataHandler_Decorated.invokeMethod(Unknown Source)
...
How do I get dynamic functions working in our build system with gradle 2.2?
The Background:
These dynamic methods are used for several things, but one is to simplify how projects depend on other projects (it is a very large system with over 80 subprojects that each may have multiple named APIs (public, internal, add-on, etc)).
This is my Plugin's apply:
void apply(Project project) {
project.subprojects.each { subproject ->
subproject.extensions.create("modules", ModuleExtension.class ) }
}
ModuleExtension has no variables or functions other than methodMissing:
def methodMissing(String name, args)
{
//return project dependency based on name/args. This no longer gets called in 2.2!
}
Sample usage in a gradle file:
dependencies {
compile module.nameOfModule( "name of api" )
}
I've also overrode the following in ModuleExtension just to see if they are getting called, but they are not:
def invokeMethod(String name, args)
def propertyMissing(String name)
def propertyMissing(String name, value)

I'm actually unable to reproduce this issue in Gradle 2.2. However, this is a somewhat misuse of Gradle extensions. If you simply want a globally available object I would simply add it as a project extra property. This has the added benefit of not having to be created for every subproject, since projects inherit properties from their parent.
ext {
modules = new ModuleExtension()
}
Edit: This is due to the new support for module replacements introduced in Gradle 2.2. The symbol modules within a dependencies block now delegates to a ComponentModuleMetadataHandler rather than your extension. You'll either have to rename your extension something other than modules or qualify the call by using project.modules.nameOfModule.

Related

Can I create a Gradle plugin that adds a dependency or another plugin based on a Gradle extension value?

Can I create a Gradle plugin that adds a dependency based on an extension value?
I have a convention plugin that I use for libraries various projects, which brings in various dependencies, takes care of boilerplate configuration, configures other plugins etc etc. I want to add an extension to the plugin that can tell the plugin whether or not to add a certain dependency, in this case it happens to be Spock, as not every library module needs the Spock dependency.
So far, my plugin looks like this
interface BasePluginExtension {
Property<Boolean> getUseSpock()
}
class BasePlugin implements Plugin<Project> {
#Override
void apply(Project project) {
BasePluginExtension basePluginExtension = project.extensions.create('basePluginConfig', BasePluginExtension)
// If a value was supplied, use it, otherwise assume we want Spock
if (basePluginExtension?.useSpock?.get() ?: true) {
// Printing for debugging purposes
println "I'm using spock! ${basePluginExtension.useSpock.get()}"
// Currently apply a plugin that applies Spock but could also just add a dependency
project.plugins.apply("test-config")
}
}
}
Then in the build.gradle file that I want to pull my plugin into, I have
plugins {
id 'base-plugin'
}
basePluginConfig {
useSpock = true
}
I'm following the docs on configuring an extension but I am getting the following error:
Cannot query the value of extension 'basePluginConfig' property 'useSpock' because it has no value available.
I've also tried the method of making an abstract class for the extension but I want the ability to have multiple configurable parameters in the future.
Is adding a dependency after plugin extension values have been configured not allowed/out of order for how Gradle works? Or am I possibly missing something obvious?

How can I access the dependencies of an application from within the build file of a dependency embedded in the application?

I have a Gradle-based library that is imported as a dependency into consuming applications. In other words, an application that consumes my library will have a build.gradle file with a list of dependencies that includes both my library as well as any other dependencies they wish to import.
From within my library's build.gradle file, I need to write a Gradle task that can access the full set of dependencies declared by the consuming application. In theory, this should be pretty straightforward, but hours of searching has not yielded a working solution yet.
The closest I've come is to follow this example and define an additional task in the library's build.gradle file that runs after the library is built:
build {
doLast {
project.getConfigurations().getByName('runtime')
.resolvedConfiguration
.firstLevelModuleDependencies
.each { println(it.name) }
}
}
I keep getting an error message that the 'runtime' configuration (passed into getByName and referenced in the Gradle forum post I linked) cannot be found. I have tried other common Gradle configurations that I can think of, but I never get any dependencies back from this code.
So: what is the best way to access the full set of dependencies declared by a consuming application from within the build file of one of those dependencies?
Okay, I mostly figured it out. The code snippet is essentially correct, but the configuration I should have been accessing was 'compileClasspath' or 'runtimeClasspath', not 'runtime'. This page helped me understand the configuration I was looking for.
The final build task in the library looks roughly like this:
build {
doLast {
// ...
def deps = project.getConfigurations().getByName('compileClasspath')
.resolvedConfiguration
.firstLevelModuleDependencies
.each {
// it.name will give you the dependency in the standard Gradle format (e.g."org.springframework.boot:spring-boot:1.5.22.RELEASE")
}
}
}

Where is documentation for Gradle's `test` block from Java plugin located?

As can be seen from the top of documentation of class org.gradle.api.tasks.testing.Test, test tasks can be configured using the following piece of code:
test {
// configuration here. For example:
useJUnitPlatform()
}
From usage of method useJUnitPlatform we can assume that method test is called with a Closure which has an instance of aforementioned class Test as delegate.
In Gradle, there are other similar methods which take a Closure. For example, afterEvaluate. Documentation of method afterEvaluate is readily available in documentation of class Project. This is also mentioned in the user guide:
This example uses method Project.afterEvaluate() to add a closure which is executed after the project is evaluated.
Where is the documentation of method test? I could not find it. Or maybe this isn't a method in a class, but inserted via reflection into class Project at runtime or some similar DSL magic?
test in this context is not a method per se, but rather a task named test. To figure out what exactly is going on here requires diving into the internals of Gradle itself which is not part of any public documentation because well, it's not part of the public API.
The only way to figure exactly out what is going on is to debug Gradle during its execution. The easiest way to do that is to generate a plugin project via gradle init. Write a simple Gradle build file such as (build.gradle; I am assuming you are using the Groovy DSL):
plugins {
id("java")
}
test {
useJUnitPlatform()
}
Then write a basic functional test and start debugging. I was curious myself what is going and did just that.
In the following screenshot, you can see the stack trace in the bottom left corner. As you can see, there is a lot of methods called.
There is a mixture of Groovy specific methods and Gradle specific methods. Digging further in, you will come to:
You can see here bottom right that the list of objects is:
Project (root project)
Extra properties
Extensions
Tasks
This aligns with what I mentioned earlier: Gradle will go out of its way to match to what is being asked for. This is also explained in the "A Groovy Build Script Primer" in official documentation (starting from "If the name is unqualified [...]").
Eventually, you will land in some of the public API methods:
getByName is part of NamedDomainObjectContainer which is documented. However, what actually implements that interface is not as you can see from the Javadoc here. The implementation, from debugging, is DefaultTaskContainer.
The rest I will leave to you as an exercise. Hopefully this gives you an idea as to what is going on behind the scenes.
Indeed, test { ... } in this case is not calling a method with name test. This block is a feature of the Gradle API called "Groovy dynamic task configuration block". Per Gradle documentation version 6.1:
// Configure task using Groovy dynamic task configuration block
myCopy {
from 'resources'
into 'target'
}
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')
This works for any task. Task access is just a shortcut for the tasks.named() (Kotlin) or tasks.getByName() (Groovy) method. It is important to note that blocks used here are for configuring the task and are not evaluated when the task executes.
As such, per this shortcut convention, test { ... } block is used for configuring a task registered in the project – task with name test in this case.
Although nowadays I'd recommend using Gradle's Configuration Avoidance API to configure a task lazily instead of eagerly:
project.tasks.named('test', Test).configure {
it.useJunitPlatform()
}
See getByName replacement in the table "Existing vs New API overview".

How to access the project version from within Gradle Kotlin DSL expressions?

I have a Gradle 5.3 build script using Kotlin DSL, similar to this:
plugins {
`kotlin-dsl`
`java-library`
}
group = "my.company"
version = "1.2.3"
Here, version= resolves to org.gradle.api.Project.setVersion.
Now, farther down, I'd like to do this (porting from a Groovy DSL build file):
tasks.named<Jar>("jar") {
manifest {
attributes(
"Product-Version" to version
)
}
}
Here, version resolves to AbstractArchiveTask.getVersion -- not what I want (and also deprecated)!
Figuring I could use Kotlin's qualified this, I tried to use
"${this#Project.version}"
instead (NB: the extra string wrapping gets rid of an additional type error), but I get Unresolved reference: #Project now.
How can I access the project version from within a Kotlin DSL expression?
The Gradle script doesn't seem to be nested inside Project but instead delegates accessors to its relevant properties. In fact, the top-level this is of type Build_gradle.
When those accessors are shadowed, variable project can be used; that is,
project.version
solves the issue at hand. As an alternative,
this#Build_gradle.version
is also valid but less readable.

Gradle - common part of DSL in separate (in other) git repository

We use our cusrom plugin and define the script in this way (This is an approximate pseudocode):
//It is common part for every script (1)
environments {
"env1" {
server mySettings("host1", "port1", "etc")
}
"env2" {
server mySettings("host2", "port2", "etc")
}
... //another common scopes
}
and
def defaultSettings(def envHost, def envPort = "15555" ...) {
return {
// Specific settings for the current script (package names, versions etc)
}
}
So in all my scripts (which are separate projects and are in separate git repositories) the common part (1) is repeated.
Is there any correct way to define the common part as a specific project (this can not be part of the plugin - the common part also changes periodically)?
I want to refer to this part when creating a new project and describe only the project-specific settings.
It looks like gradle multi-project builds, but common part should be in other git repository/Nexus.
Important clarification - the common part can also be in the Nexus, have a version ( to have POM descriptor).
It's quite common to have an "opinionated" plugin and a "base" plugin. Gradle uses this concept quite often.
One example is the java plugin automatically applies the java-base plugin. So the java-base plugin contains all of the tasks (logic) but doesn't actually do anything. The java plugin adds the tasks and configures them (eg it adds the src/main/java and src/test/java conventions). So the java-base plugin is not opinionated, the java plugin is opinionated.
So, you could do the same, have a base plugin and a opinionated plugin which
Applies the base plugin
Configures the environments specific for your use case
Note also that you can move logic from build.gradle to a plugin if you put the logic within a project.with { ... } closure. Eg:
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.with {
subprojects { ... }
configurations { ... }
dependencies { ... }
task foo(type: Bar) { ... }
}
}
}
There is another solution to your problem. The approach may be less clean than using an opinionated plugin, but it allows you to manage simple Gradle scripts independently from your projects:
The apply from: term to include Gradle scripts is not limited to file paths, but can also handle URLs. This way, you can simply manage your scripts in a standalone repository and provide the newest version via a web server.
To test this way of script distribution and access, you can even use the raw file view feature provided by various repository platforms like GitHub or Bitbucket:
apply from: 'https://raw.githubusercontent.com/<user>/<repo>/<branch>/<file>'
The biggest disadvantage of this approach is the fact, that you need to have access to the local or even global web server for each build, if you need to ensure company-external or offline builds, you should stick to #LanceJavas solution and use a custom plugin.

Resources