Custom plugins: how to avoid using afterEvaluate when setting another plugin's extension configuration

I have a custom plugin that supplies several build tasks as well as automating common settings and task configurations for the moowork node plugin. It also might apply different plugins based on its extension properties.

I want consumers of this custom plugin to be able to apply the custom plugin, configure its extension, and then, based on those settings, I want to update the moowork plugin’s configuration accordingly.

So this build script snippet:

apply plugin: 'the-custom-plugin'
customPluginConfig {
   buildEnvironment = 'agent'
   projectKind = 'library'
}

…should be equivalent to:

apply plugin: 'com.moowork.node'
node {
  version = <customPlugin.commonNodeVersion>
  npmVersion = <customPlugin.commonNpmVersion>
  workDir = file(<customPlugin.agentWorkDir>)
  npmWorkDir = file(<customPlugin.agentWorkDir>)
  // various other settings
}
if (<customPlugin.projectKind> == 'library') {
  apply plugin: 'java-library'
} else {
  apply plugin: 'java'
}

I have reviewed the documentation on lazy properties, and gone through responses to similar questions, but all the examples are describing how to use lazy configuration patterns for tasks, and I can’t find any examples or documentation describing how I can defer other plugins’ extension configuration settings after evaluation, before tasks are executed, but without using afterEvaluate.

Any suggestions here would be very welcome

1 Like

The examples for lazy properties do actually show what you need, but they don’t spell out the exact use case. Fundamentally, there is no difference between an extension configuring a task vs. an extension configuring another extension though. You would just configure your extension’s properties as the value of the node extension’s properties.

This is actually your problem in this case. The moowork node plugin was abandoned years ago and was forked to Gradle Node Plugin with a new maintainer when the original maintainer stopped updating and responding. At this point, the moowork node plugin is woefully out of date. The 2.x.x versions should be drop-in replacements, but you would really need to get to 3.0.0+ for the plugin to support what would best handle your use case (all configuration supports lazy properties).

1 Like

I’m trying to sort this out, but not sure what it looks like in a standalone plugin. I’ll update this plugin to work with the newer gradle node plugin, but regarding config setting, would the custom plugin’s apply implementation look something like this?

project.node {
  workDir = extension.buildEnvironment.get() == 'agent' ? file(agentDirPath) : file(devDirPath)
}

…also, how do I avoid doing this?

project.afterEvaluate {
    if (extension.projectKind.get() == 'library') {
        project.apply plugin: 'java-library'
    } else {
        project.apply plugin: 'java'
        project.apply plugin: 'distribution'
       if (extension.hasExternalDistribution.get()) {
           project.distributions {
               external {
                   // distribution config
               }
           }
       }
    }
}

You don’t want to get() the value in your plugin’s apply. Rather, map the value so that the consumer task (node task) only queries the actual value when it needs it. It’d look something like this (untested):

project.node {
    workDir = extension.buildEnvironment.map { e -> e == 'agent' ? file(agentDirPath) : file(devDirPath) }
}

Is it a strict requirement that your plugin applies all the other plugins? Typically, my preference would be to reverse the contract and make your plugin reactive, i.e. configure the distributions if the distribution plugin has been applied to the project or the things specifically needed for a library if java-library has been applied.

1 Like

It will be a mix. For example, the java/java-library plugins would reasonably be expected to be applied in the build script, but there are a few other plugins (some internal, some external) that we’d like to avoid build script authors having to be repeatedly bothered with knowing when they need one plugin or the other, so the snippet I have there is meant more as an example to illustrate the use case than the specific plugins that are involved.

What is the best way to handle conditionally-applied plugins without afterEvaluate?

There are also some extension configuration setups that I’m having a harder time understanding than these simple property assignment cases like with the node plugin.

Taking distributions as an example, how would I conditionally add a custom distribution based on the custom plugin’s extension settings?

The approach below obviously wouldn’t work in the custom plugin’s apply method, but it’s not obvious to me from the lazy property patterns how to properly set up a distribution configuration based on the custom plugin’s extension configuration.

project.withPlugin('distribution') {
  project.distributions {
    if (extension.hasExternalDistribution.get()) {
      external {
         // distribution config
      }
    }
  }
}

Using afterEvaluate is not necessarily bad in this case. You need the project to be evaluated to know what was ultimately configured and act on it. The danger of afterEvaluate is really abuse where you have many things that are trying to execute later and later after each other when order is not guaranteed. As long as you’re handling the configuration of these plugins and eliminating bad coupling, you should be fine.

However, if you just really don’t want an afterEvaluate you could have the extension apply the plugins on the set of your value. The drawback is that if someone sets the value, then later changes it in the build.gradle file or through other means (like afterEvaluate abuse), you’ll have plugins you didn’t want.

1 Like

The APIs are not well designed for this particular case. The expectation is that you might configure a distribution differently based on properties, but not really create a distribution or not based on other properties.

It might work well enough to always register the external distribution, but in the lazy configuration action, throw an Exception if there should not be an external distribution or explicitly set the relevant tasks to enabled = false based on the property. I don’t particularly like these options and there may be cases where the timing doesn’t work quite right, but that’s what I would try if you don’t have any other possibilities that you’re happy with.

1 Like

@jjustinic : thank you so much for all this feedback; I think I’ll be able to get most of the lazy config setup to work where there’s a good fit and find alternatives to relying on afterEvaluate where there’s support for it.

1 Like