Configuring a plugin programmatically using extension variables

Hi,

I’ve been trying to get my head around custom Gradle plugins throughout the week and I’ve run into a bit of a wall. My goal is basically to consolidate a lot of common configuration into a custom plugin. One thing this plugin should include is the ‘maven-publish’ configuration. This configuration is identicle across projects except for the artifact details (group ID, artifact ID, version). So, my idea is to define an extension which will allow configuration of only these changing properties, with the actual configuration taking place within my plugin. This would allow me to reduce a lot of repeated script code in all of my projects (I intend on adding other common stuff to the plugin too if I can get this to work).

Here’s my current plugin code (Groovy):

class ConfigPlugin implements Plugin<Project>
{
    @Override
    void apply( Project project )
    {
        applyPlugins( project )
        createExtensions( project )
        configureRepoPublishing( project )        
    }

    void applyPlugins( Project project )
    {
        project.apply plugin: 'maven-publish'
    }

    void createExtensions( Project project )
    {
        project.extensions.create( 'commonConfig', CommonConfigExtension )
        project.commonConfig.extensions.create( 'repoConfig', RepositoryExtension )
    }

    void configureRepoPublishing( Project project )
    {
        println 'Configuring repo publishing.'
        println "Version: ${project.commonConfig.projectVersion}"

        project.publishing {
            publications {
                aar( MavenPublication ) {
                    groupId project.commonConfig.repoConfig.groupId
                    version project.commonConfig.projectVersion
                    artifactId project.commonConfig.repoConfig.artifactId
                    artifact( project.commonConfig.repoConfig.artifactPath )
                }
            }

            repositories {
                maven {
                    url {
                        REPO_URL
                    }
                    credentials {
                        username REPO_USERNAME
                        password REPO_PW
                    }
                }
            }
        }
    }
}

The project build script configuration for the extensions defined above looks like this:

commonConfig {

    projectVersion PROJECT_VERSION
	
    repoConfig {
        groupId REPO_GROUP_ID
        artifactId REPO_ARTIFACT_ID
        artifactPath REPO_ARTIFACT_PATH
    }
}

The problem is that the commonConfig and repoConfig extension variables being access in the publishing configuration are all null. I gather this is due to the plugin code being executed before the extension configuration takes place in the project build script, and so these values have not been set at this stage. So to get around this I tried surrounding the configuration code in a project.afterEvaluate {} closure. This unfortunately results in the following error:

“Cannot configure the ‘publishing’ extension after it has been accessed.”

I’m not sure if this is a ‘maven-publish’ specific issue or if my approach is just wrong, so was hoping to get some clarification here. There doesn’t seem to be a lot of information out there (that I could find) on how to do this sort of thing so any help would be appreciated.

EDIT:
I’ll just add that If i add the afterEvaluate closure and remove the publishing configuration block from the plugin, I can confirm that I do have access to the commonConfig and repoConfig extension variables. So at least the extensions appear to be working.

1 Like

You are not going to get very far with this solution. You should study the new rules based plugins and rewrite all this logic in the new scheme. The afterEvaluate is a hack that will not give you reliable results and eventually will stop working completely even if you manage to get it to work in the first place. The maven-publish plugin is a rules based plugin which would best to combine with another rules based plugin solution that you create.

As a suggestion what you are trying to do seems like a new @Mutate configuration rule supplying your desired configuration as values for the PublicationContainer model object. The PublicationContainer would be the subject of your rule and there you could use code like this to create and configure your publications:

@Mutate
void createMyPublications(PublicationContainer publications) {
    publications.create('aar', MavenPublication) {
        // Configure the maven publication here
    }
}

And to complete this answer, I would probably migrate your commonConfig and repoConfig to ‘@Managed’ elements, and add them to the new model using '@Model
These new model elements would then be used as inputs in the @Mutate rule suggested above by @Alex_Vol

I suggest reading the chapter on the new model

Your use case is a nice and easy one to get yourself up to date with the new model.

1 Like

Thanks guys, I suspected there might be a different approach to this that I hadn’t come across yet. Looks like I’ve got some more reading to do!

Ok, so I’ve had a crack at adopting the new rule based configuration model and I’m a bit stuck again. Here’s my plugin code so far:

class ConfigPlugin extends RuleSource
{
    @Managed
    interface CommonConfig
    {
        String getProjectVersion()
        void setProjectVersion( String projectVersion )

        String getGroupId()
        void setGroupId( String groupId )

        String getArtifactId()
        void setArtifactId( String artifactId )

        String getArtifactPath()
        void setArtifactPath( String artifactPath )
    }

    @Model
    void commonConfig( CommonConfig commonConfig ) {}

    @Mutate
    void createPublications( PublicationContainer publications, CommonConfig commonConfig )
    {
        publications.create( 'aar', MavenPublication ) {
            version commonConfig.projectVersion
            groupId commonConfig.groupId
            artifactId commonConfig.artifactId

            artifact( commonConfig.artifactPath )
        }
    }
}

And the relevent project build.gradle configuration code:

apply plugin: 'maven-publish'
apply plugin: ConfigPlugin

model {
    commonConfig {
        projectVersion PROJECT_VERSION
        groupId REPO_GROUP_ID
        artifactId REPO_ARTIFACT_ID
        artifactPath REPO_ARTIFACT_PATH
    }
}

The problem is the PublicationContainer model path seems to be unknown. I get the following error when attempting a build:

Error:A problem occurred configuring project ‘:testproject’.
> The following model rules could not be applied due to unbound inputs and/or subjects:
ConfigPlugin#createPublications
subject:
- PublicationContainer (parameter 1) []
inputs:
- commonConfig ConfigPlugin.CommonConfig (parameter 2)
[
] - indicates that a model item could not be found for the path or type.

Is there some way I should be making the ‘maven-publish’ plugin model paths visible to my own config plugin other than applying it in the project build script or plugin?

You can use the gradle ‘model’ task to see what’s in the new model space.
You’ll see PublishingExtension
This should be the subject of your rule. From PublishingExtension you can access the publications container. See here

1 Like

Ah, yes that did the trick. It’s all working now, and the end result is a very simple plugin which is nice to see. For reference here’s the final working plugin code (with sensitve credential info omitted)

class ConfigPlugin extends RuleSource
{
    static final String repositoryUrl = #####
    static final String repoUsername = #####
    static final String repoPassword = #####

    @Managed
    interface CommonConfig
    {
        String getProjectVersion()
        void setProjectVersion( String projectVersion )

        String getGroupId()
        void setGroupId( String groupId )

        String getArtifactId()
        void setArtifactId( String artifactId )

        String getArtifactPath()
        void setArtifactPath( String artifactPath )
    }

    @Model
    void commonConfig( CommonConfig commonConfig ) {}

    @Mutate
    void createPublications( PublishingExtension publishing, CommonConfig commonConfig )
    {
        publishing.publications.create( 'aar', MavenPublication ) {
            version commonConfig.projectVersion
            groupId commonConfig.groupId
            artifactId commonConfig.artifactId

            artifact( commonConfig.artifactPath )
        }

        publishing.repositories.maven {
            url {
                repositoryUrl
            }
            credentials {
                username repoUsername
                password repoPassword
            }
        }
    }
}

This is compiled as a *.jar archive and published to my Maven repository. One of my projects can then utilise this plugin in its build.gradle script file like so (once the appropriate dependency and repository is added):

apply plugin: 'maven-publish'
apply plugin: 'common.android.config'

model {
    commonConfig {
        projectVersion = PROJECT_VERSION
        groupId = repoPackageName
        artifactId = repoArtifactBaseId
        artifactPath = "$buildDir/outputs/aar/${project.getName()}-debug.aar"
    }
}

Note that this is part of a larger build script for an Android app. Publishing this project with gradle publish works as expected.

A couple of follow up questions while I’m at it:

1.When I use the gradle model task for my project which is using my plugin, the output seems to only show the tasks model, but none of my own plugin models or the publishing extension model. Is this a bug or am I missing something here? Here’s the actual output:

>gradlew model
:model                                                                                       
                      
------------------------------------------------------------
Root project          
------------------------------------------------------------
                      
+ tasks               
      | Type:           org.gradle.model.ModelMap<org.gradle.api.Task>
      | Creator:        Project.<init>.tasks()
    + clean           
          | Type:       org.gradle.api.tasks.Delete
          | Value:      task ':clean'
          | Creator:    Project.<init>.tasks.clean()
          | Rules:    
             ? copyToTaskContainer
    + components      
          | Type:       org.gradle.api.reporting.components.ComponentReport
          | Value:      task ':components'
          | Creator:    tasks.addPlaceholderAction(components)
          | Rules:    
             ? copyToTaskContainer
    + dependencies    
          | Type:       org.gradle.api.tasks.diagnostics.DependencyReportTask
          | Value:      task ':dependencies'
          | Creator:    tasks.addPlaceholderAction(dependencies)
          | Rules:    
             ? copyToTaskContainer
    + dependencyInsight
          | Type:       org.gradle.api.tasks.diagnostics.DependencyInsightReportTask
          | Value:      task ':dependencyInsight'
          | Creator:    tasks.addPlaceholderAction(dependencyInsight)
          | Rules:    
             ? HelpTasksPlugin.Rules#addDefaultDependenciesReportConfiguration
             ? copyToTaskContainer
    + help            
          | Type:       org.gradle.configuration.Help
          | Value:      task ':help'
          | Creator:    tasks.addPlaceholderAction(help)
          | Rules:    
             ? copyToTaskContainer
    + init            
          | Type:       org.gradle.buildinit.tasks.InitBuild
          | Value:      task ':init'
          | Creator:    tasks.addPlaceholderAction(init)
          | Rules:    
             ? copyToTaskContainer
    + model           
          | Type:       org.gradle.api.reporting.model.ModelReport
          | Value:      task ':model'
          | Creator:    tasks.addPlaceholderAction(model)
          | Rules:    
             ? copyToTaskContainer
    + projects        
          | Type:       org.gradle.api.tasks.diagnostics.ProjectReportTask
          | Value:      task ':projects'
          | Creator:    tasks.addPlaceholderAction(projects)
          | Rules:    
             ? copyToTaskContainer
    + properties      
          | Type:       org.gradle.api.tasks.diagnostics.PropertyReportTask
          | Value:      task ':properties'
          | Creator:    tasks.addPlaceholderAction(properties)
          | Rules:    
             ? copyToTaskContainer
    + tasks           
          | Type:       org.gradle.api.tasks.diagnostics.TaskReportTask
          | Value:      task ':tasks'
          | Creator:    tasks.addPlaceholderAction(tasks)
          | Rules:    
             ? copyToTaskContainer
    + wrapper         
          | Type:       org.gradle.api.tasks.wrapper.Wrapper
          | Value:      task ':wrapper'
          | Creator:    tasks.addPlaceholderAction(wrapper)
          | Rules:    
             ? copyToTaskContainer

2.Currently I have applied the ‘maven-publish’ plugin in my project build.gradle script file, whereas ideally this would just be applied within my config plugin. If my plugin consists purely of a RuleSource subclass, is it possible to apply another plugin within this plugin? Or do I need to do this in the apply(..) method of a Plugin<Project> implementation, with the RuleSource sublass included as an inner class?

Thanks again for all the help.

Taking the Gradle source code for new rules based core plugins as my guideline I would answer that creating a hybrid plugin with the RuleSource class an the inner class and the apply method applying the maven-plugin is the way to go.

Not sure what to say about the model task output. I use Gradle 2.9 and the model output is way more than you show here. What version of Gradle are you running this on?

Thanks Alex, I ended up going the hybrid route and it’s working well.

Regarding the model task output, this occurs using Gradle 2.9. Both Gradle and the wrapper behave the same way. I’ve tried using a Gradle 2.8 wrapper but again I get the same behaviour. This is on a 64-bit Windows 10 machine for my Android projects.

My iOS build scripts which use the same plugins, including my custom plugin of this topic, produce much more output with all of the expected models. This is on an OSX machine (10.10.5 Yosemite) using Gradle 2.8.

I’m at a bit of a loss as to what could be causing this issue. I’ll give Gradle 2.10 a shot this week as well to see if anything changes.