Establishing task dependencies in a plugin

I’m beginning to sense a pattern here. Gradle makes hard things easy and makes easy things hard. :wink:

Especially with plugins. All the documentation is geared to the use case of writing build scripts. My goal is to write plugins that encapsulate default behavior so that the writing of build scripts that use these plugins, in many instances, can be as simple as applying the plugin with little or no build script configuration needed. I find myself defeated at every turn.

Okay, so here’s what I want to do.

I have several plugins each of which applies the os.nebula plugin, each creating rpms that follow a template set in the plugin. I have one more plugin whose job is simply to tar up all of these rpms into a .tar file. It has a ‘bundle’ task derived from the Tar task. Each of these plugins is applied in a project that is a subproject of the parent project. This includes the bundler plugin.

I want to define a dependency order that the Bundler task is invoked last.
To do that I must make it depend on all of the Rpm subprojects’ Rpm tasks.

One way to do it, perhaps quite easily, would be to explicitly declare these in the build script for the Bundler task.
But I want to do it in the Bundler plugin in keeping with the above design philosophy.

This means there must be some logic that scans the subprojects, finding all the rpm tasks, and making the bundler task dependOn them. In other words, I’m trying to set my execution-time dependencies at configure time. But this means I need to set configure time dependencies. But there seems to be no way to do the latter.

The default configuration order is alphabetical. I suppose I could hack this by naming by bundler project “zzzBundler”. But there’s gotta be a better way. I want to say “evaluate and execute this project after all others”. But to do this in a plugin means (I think) that I have to do it in the evaluation phase, and we are back to a chicken/egg problem. I have looked at evaluationDependsOnChildren() how that would help since the bundler is itself a child, and evaluationDependsOn (which again seems to demand an iteration through subprojects, some of which may not yet be configured).

I’m not sure where to turn next. I may try invoking the bundler from the parent build.gradle, but so far I’ve been using that only for property configurations and the like.

Am I missing something?

When you use bundlerProject.evaluationDependsOn(rpmProjects) and then put your bundler configuration into bundlerProject.afterEvaluate{}, all the rpmProjects will be configured at that point.

I would prefer an explicit mechanism where the build author decides which projects he wants to bundle. Otherwise you would hard-code your specific decision (all sibbling projects of the bundlr project) into the plugin, reducing it’s potential for reuse.

I’m sure I don’t grok everything here, but if these plugins have reasonable coupling with each other, why don’t you make each of the “Rpm subproject’s Rpm tasks” know that they have to set their own dependency relationship with the “Bundler” task? In other words, instead of doing a top-down search for all the tasks, just make the plugin that defines the “subproject” tasks locate the Bundler task and make it depend on the task being defined in the plugin.

where would this logic go:

bundlerProject.evaluationDependsOn(rpmProjects)
and how would rpmProjects be defined?

In your bundler plugin.

That’s up to you to decide. As far as I understood you wanted something like project.parent.children - project.

Docs say

Declares that this project has an evaluation dependency on the project with the given path.

which sounds to me singular, not plural. So are you talking about multiple evaluationDependsOn statements or some syntactic sugar I am not aware of?

Yes, everything I write here is usually pseudo code to point you in the right direction.

rpmProjects.each { builderProject.evaluationDependsOn(it) }

should do the trick.

I’ve been at this all day and gotten nowhere.
If this is expected Gradle functionality then gradle is broken. It simply cannot be right that there is no way to programatically set the dependency order in a plugin. I tried everything you suggested and there was always a problem. The alphabetical order thing kept cropping up, even with afterEvaluate. Some tries led to circular dependencies.
This should be easy, not hard.

I officially give up at this point on trying to do this in a plugin. I will just have to do it all in settings.xml, I think.
This is much too hard. In maven you order the modules by typing them in a list. There are other problems with Maven but I like that.

I think someone needs a nap. :slightly_smiling:

Actually, in Maven the order of modules in the “modules” list does not specify the dependency order. That’s just the list of modules to work on. It does actually look at the dependencies of those modules and determines an actual correct order.

Despite the fact that I don’t know the exact solution, I’m fairly certain things along these lines are doable.

I would ask again to the people who know exactly how this works, is it reasonable to consider what I suggested earlier, such that instead of doing a search for subprojects, simply have the plugin application in each subproject, if it can know it’s a subproject, reference the parent project and set the task dependency?

I’m sorry you’re frustrated Steve, but without seeing any of your code, it’s hard to tell you where the mistake is. It can’t be a big thing, as I have done similar things before, e.g. for aggregating IntelliJ Plugin Repositories.

So without seeing your code I can just guess: Be sure to put evaluationDependsOn() calls outside of afterEvaluate() blocks. Otherwise the evaluation dependency is registred too late.

Also, if you just want to get all tasks of a certain type, no matter if they exist now or are added later, use tasks.withType(RpmTask). You won’t even need any of the methods mentioned above. Those are only necessary if you want to look at the actual configuration values on the tasks.

Thanks, David and Stefan. I tried all of your suggestions and always there was some issue.

If I didn’t do some sort of evaluationDependsOn() then the iron law of alphabetical order always raised its ugly head.
If I did try to set up evaluationDependsOn() then it would at some point be necessary to iterate the subprojects, and there was always an issue there. Circular dependencies. Other error messages.

That is just too hard. We’re swimming up stream. And thinking about it, it should not be the responsibility of the bundler plugin to do this. Perhaps I would need yet another overarching kind of plugin to orchestrate the whole thing. But, too much effort.

And so, I gave up and stopped fighting the framework and just do it from the root build.gradle, which now looks like

// all base build.gradles must supply this.
// it should not be overridden.
ext.baseName ='gvp008'
version '1.0.0'

subprojects { proj ->
    
    proj.version = version
    task hello << { task -> println "I'm $proj.name" }
}

afterEvaluate {
    project(':bundle').evaluationDependsOn(':audio')
    project(':bundle').evaluationDependsOn(':content')
    project(':bundle').evaluationDependsOn(':database')
    project(':bundle').evaluationDependsOn(':grammars')
}

That’s not too bad, really, a small compromise I’m willing to live with.

Now on to the next problem!

No matter what I do, the bundle:bundle task is always UP-TO-DATE. I’ve tried every trick I could find documented (primarily setting outputs.upToDateWhen {false}) at every event I could think of but the closure is never called, The UP-TO-DATE decision has evidently been made “up above, somewhere” and gradle never feels the need to check.

To be discussed.

Hi Steve,

There should be no cyclic dependency. Bundler depends on the bundled projects, not the other way round. Make sure you excluded the Bundler project itself when iterating over the child projects. I think once you fix that, it will work as you desired.

That’s what I was getting at in my first answer. The Bundler plugin should have an explicit API to register the projects to be bundled.

I won’t comment much on the snippet you posted, because I think it is going completely in the wrong direction. But there is one thing I have to point out:

afterEvaluate {
    project(':bundle').evaluationDependsOn(':audio')
    ...
}

I don’t know what this is supposed to do, but it won’t work. Calling evaluationDependsOn in an afterEvaluate block is too late - as the name implies. Evaluation dependencies need to be set as early as possible.

You say this won’t work, but it’s been working ever since I put it in and it won’t work without it. What I thought it was doing and what it seems to be doing is that after the top level build.gradle is evaluated it makes sure that the bundle subproject evaluation depends on all of the other subprojects’ evaluations. This was the very thing I couldn’t accomplish in code… In other words, it orders the subproject evaluations in the order I want them to be done.

As far as your suggestion of a registration API in the bundler goes, would this not require that the other subprojects would need to be able to find the bundler to call this api, and I may be wrong but I think evaluation order would bite me there too.

It works only because the parent project is evaluated first by default. I just wanted to point out to you that you should not be putting them in an afterEvaluate block. It will work just fine if you put them in the buildscript at the top level.

The build author would decide how he uses that API. I would expect either an explicit project list in the Bundler project or some iteration over all projects, using some matching criteria. The point is it would be up to the build author to decide what needs to be bundled, not up to the plugin.

Again: Without seeing your code I cannot tell you where the error is. What you tried (iterate over subprojects, call evaluationDependsOn, add them to the bundle inputs) should work. Just quoting myself here:

I think our philosophies are not in synch here. What I want to implement is a rule that says this:
The bundler subproject’s evaluation depends on the evaluation of every project other than the bundler. The build author of any project collection using this system can and should expect that to be the case. He should only need to list the subprojects, not worry about evaluation time dependencies.

The point here is that there are large numbers of project collections which will fall under this pattern, and I want to minimize the work involved in porting them over. These plugins are not general purpose, for use outside this organization, in which case I might agree with you that it should be up to the build author to make the decision.

This could be easily be attained by having available some convention other than alphabetical order determining which subprojects are evaluated in which order. For example, an option under which subprojects are evaluated in the order given in settings.gradle.

I already showed you how to do that:

and how to depend on the evaluation of those projects:

putting it together is easy:

(project.parent.children - project).each {sibbling ->
  project.evaluationDependsOn(sibbling)
  //make bundle task depend on the output of the rpm task
}

Hey Steve,

I briefly followed this thread here. I wonder why you can’t do the following in your bundler plugin

void apply(final Project project) {
    Task bundleTask = ...
    project.subprojects {
        plugins.withId("rpm-plugin") {
            bundleTask.dependsOn tasks.withType(Rpm)
        }
    }
}

The logic within the plugins.withId closure is executed after the “rpm-plugin” is applied to a subproject and the tasks.withType(Rpm) is lazily evaluated. strictly speaking you should also be able to simplify this by doing:

void apply(final Project project) {
    Task bundleTask = ...
    project.subprojects {
        bundleTask.dependsOn tasks.withType(Rpm)
    }
}

This simplified snippet relies on the fact that the “bundle” task depends on all the tasks of type Rpm in your subprojects. Does that fit your usecase?

cheers,
René

Do your ellipses indicate that a constructor will be called here? If not, then I’m afraid the task object won’t exist at the time apply() is called. I believe the constructor is called by the task declaration in the build script which is:

repositories {
    mavenCentral name: "nexus", artifactUrls: ["http://mavencentral.it.att.com:8084/nexus/content/groups/att-public-group"]
    mavenLocal()
}
apply plugin: 'vt.bundler'

defaultTasks 'bundle'
task bundle(type: Bundler) {
    
}

But maybe there’s a better for me to write the build script? Can the plugin “own” the task?

I’ll confirm that you are correct in this. I moved them out of the afterEvaluate() and it works just as well.

Yes the plugin should own the task. It can be created within the plugin:

void apply(final Project project) {
    Task bundleTask = project.tasks.create("bundle", Bundler.class)
    project.subprojects {
        bundleTask.dependsOn tasks.withType(Rpm)
    }
}