Gradle plugin to read build command parameters

I am new to Gradle plugin development and hoping that someone can tell me whether this is even possible. The idea is to write a standalone plugin that can be called by a multi project and at build time, extract the build command parameters. For example, running this command,

./gradlew :subprojA:assemble :subprojB:assemble

the plugin should be able to determine that subprojA and subprojB are to be assembled. AFAIK, a plugin is task centric and I am hoping there is a way to tap into the calling project build lifecycle and gets executed automatically so that info can be extracted.

Thanks in advance for any insights.

If you want to know which projects are going to built as part of any given Gradle execution then you simply need inspect the ‘TaskExecutionGraph’. Not only are the CLI arguments not publicly available from your build script, they are not necessarily inclusive of all projects being built. For example, if ‘subprojB’ depends on ‘subprojA’ then executing ‘$ gradle :subprojB:assemble’ will actually result in both ‘subprojB’ and ‘subprojA’ being built, despite not explicitly specifying any task of ‘subprojA’ in the command line.

A much more robust solution is to determine the projects being built by aggregating a list from all executing tasks.

gradle.taskGraph.whenReady { graph ->

println graph.allTasks.unique { it.project }.collect { it.project }

}

Hi Mark, great, that produces a list of unique projects being built, question is can I put that n a plugin somehow and not in the calling project? Doesn’t seem possible but thought I would ask. You also raised an interesting point about dependency, so let’s say ‘subprojA’ is at ‘1.0.1’ and is dependent on ‘subprojB’ which is at ‘0.0.5’. Only ‘subprojA’ has changed and needs to be built and deployed. ‘subprojB 0.0.5’ artifact is already deployed and doesn’t need to be rebuilt, what does the build actually do? Does it download ‘subprojB 0.0.5’ artifact or rebuild it locally?

Yes, you can put that code in a plugin just the same. You can get at the ‘Gradle’ instance via ‘Project.getGradle()’.

If a project (actually, task to be more accurate) has already been built the individual tasks will still be part of the task graph, however they will be up-to-date and therefore not actually perform any work. The result of querying the task graph will be the same regardless.

I probably didn’t understand you correctly but I tried this in the plugin ‘build.gradle’ file

Project.getGradle().taskGraph.whenReady { graph ->
    println graph.allTasks.unique { it.project }.collect { it.project }
}

Gave me this error during build

A problem occurred evaluating root project 'gradle-sample-plugin'.
> No signature of method: static org.gradle.api.Project.getGradle() is applicable for argument types: () values: []
  Possible solutions: getClass()

As to the other part of the question, if the task is still part of the task graph even though it’s current, that means it’ll be listed which is not what I want unless there is a way to query if it is current.

The ‘getGradle()’ method isn’t static, so you have to call it on an instance of ‘Project’. When you say “plugin build.gradle” are you talking about a script plugin or binary plugin? A script plugin would look exactly like my original code sample. In the case of a binary plugin, it would be something like this.

public class MyPlugin implements Plugin {

void apply(Project project) {

project.gradle.taskGraph.whenReady {

}

}

}

As for you second question, if you only want to include tasks that actually executed you can look at ‘Task.getDidWork()’, but that would have to be done after the task was executed. You could use a ‘TaskExecutionListener’ for that. This all seems to be getting a bit convoluted though. Could you provide some context as to what exactly you are trying to accomplish with this plugin?

“plugin build.gradle” is referring to a standalone binary plugin. Ok your code now makes more sense :slight_smile: I think getting the task it was executed is fine. The premise for what I am doing is simple, I am trying to create a binary plugin that can be called by projects with subprojects to determine at build time which subproject got built and from that, I want to generate an appropriate git tag based on the subproject(s) and associated versions. Do you know of an easier way to do this?

Can you give an example of what that git tag might look like? How would it be different depending on what subset of subprojects actually changed?

Sure, an example would be ‘subprojA-1.0.1/subprojB-0.0.5’ in the case that both subprojects need to be built, if only ‘subprojA’ needs to be built, it should just be ‘subprojA-1.0.1’, does that make sense?

Excuse my questioning but what is the purpose of git tags with such a pattern? Why is it that you are independently versioning subsets of a single git repository? If these projects are truly independent and versioned differently, should the be in separate repositories?

Fair question, it was what I suggested to the developer in the first place and that all subprojects should get the same version number when a version is bumped, note that each subproject generates a separate artifact. However, he felt strongly that certain subprojects should be grouped together because they logically belong together plus it cuts down on the number of repos and build.gradle files. Is there a standard per se for this sort of project structure?

Regardless, if I need to do this, is what you suggested the easiest way to accomplish it? Thanks for all your help btw, you’ve been very helpful.

That is definitely an atypical setup and not particularly recommended. Like I said, if a subproject is truly independent and version separately it should be in a separate repository with its own build. In fact, I’m not sure Gradle supports multi-project builds well when subprojects have differing versions.

That being said, why not just generate a git tag with every modules version number in it, every time? I assume changes to version numbers themselves will continue to be a manual process. For example, if project A is at version 1.0.0 and B is at 0.5.0 then I’ll have a tag of ‘A-1.0.0/B-0.5.0’. If I update B to 0.5.1 then the result is a tag of ‘B-0.5.1’ which is somewhat misleading, since the repository also still contains version 1.0.0 of project A. Wouldn’t it be more correct to then tag your repo with ‘A-1.0.0/B-0.5.1’?

Yeah I agree. You raised an interesting point about whether Gradle supports multi-project builds, it seems to work fine so far, assemble on a subproject only generates that artifact, so do you know of other concerns?

Yeah we thought about generating a git tag with every modules version number for every build but the tag is really meant to indicate which subproject / module got build and deployed for that particular build run.

In the spirit of Gradle being a tool that is flexible to support many different use cases, here is my solution :slight_smile:

task buildManifest {

Set projects = []

gradle.taskGraph.afterTask { task ->

if (task.didWork) projects += task.project

}

doLast {

println projects

}

mustRunAfter subprojects.collectNested { it.tasks }

}

subprojects {

apply plugin: ‘java’

assemble.finalizedBy buildManifest

}

Basically this adds a ‘buildManifest’ task which will run whenever a project’s ‘assemble’ task is run, and additionally after all other tasks have run. This is to ensure it will always be the last task. It then prints a list of any projects, which have tasks that executed as part of this current build and performed work (i.e… not ‘up-to-date’).

That looks awesome! I’ll try it out tomorrow and let you know how it goes. Thanks for all your help and oh, great attitude BTW :slight_smile:

Looks like your code is for inline script in the multi-projects app ‘build.gradle’ and not the plugin, is that correct? I am trying to get this code into the plugin, so I’ll see if I can tweak it. This is my attempt and I am sure I got something wrong, ignore the GreetingPlugin naming, I am modifying a sample plugin code

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('hello', type: GreetingTask)
        project.tasks.create('buildManifest', BuildManifestTask)
        project.subprojects.assemble.finalizeBy buildManifest
    }
  }
  class BuildManifestTask extends DefaultTask {
    @TaskAction
    def buildManifest() {
        Set projects = []
        gradle.taskGraph.afterTask { task ->
            if (task.didWork) projects += task.project
        }
          doLast {
            println projects
        }
          mustRunAfter subprojects.collectNested { it.tasks }
    }
}

Got this error on the multiproject build using the plugin, ‘No such property: buildManifest for class: org.gradle.GreetingPlugin’

You can still use DSL even in a plugin. Change the final line of the ‘apply()’ method like so.

project.subprojects {

assemble.finalizedBy ‘:buildManifest’

}

Edit You may have to reference the task by path string since that task actually exists on the root project and won’t be in closure scope like in my script example.

Yep, that works, thanks. So I added it as a dependency to a multiproject app and ran ‘assemble’ but it didn’t seem to trigger the ‘buildManifest’ task, it seems like it should, I also tried to ‘apply plugin: ‘GreetingPlugin’’ but that caused the error ‘Plugin with id ‘GreetingPlugin’ not found’. Here’s the plugin id as defined in the properties file

src/main/resources/META-INF/gradle-plugins/org.samples.greeting.properties
implementation-class=org.gradle.GreetingPlugin

Correct, adding the plugin simply as dependency is not sufficient, you have to apply the plugin to your project. You’ll have to refer to it by its fully qualified name, ie. the name of your .properties file.

apply plugin: ‘org.sample.greeting’

Additionally, ensure you are adding your plugin dependency to your buildscript, not your project. Take a look at the Gradle documentation for details.

Works again, I added it to the top level ‘build.gradle’, thanks! The syntax is befuddling to a newbie I must say. However, the build is now not finding the ‘assemble’ task for any of the subprojects

A problem occurred evaluating root project 'parentproj'.
> Failed to apply plugin [id 'org.samples.greeting']
   > Could not find property 'assemble' on project ':subprojA'.

Without applying the plugin, ‘assemble’ works just fine.