Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gradle plugin best practices for tasks that depend on extension objects

Tags:

gradle

groovy

I would like feedback on the best practices for defining plugin tasks that depend on external state (i.e. defined in the build.gradle that referenced the plugin). I'm using extension objects and closures to defer accessing those settings until they're needed and available. I'm also interested in sharing state between tasks, e.g. configuring the outputs of one task to be the inputs of another.

The code uses "project.afterEvaluate" to define the tasks when the required settings have been configured through the extension object. This seems more complex than should be needed. If I move the code out of the "afterEvaluate", it gets compileFlag == null which isn't the external setting. If the code is changed again to use the << or doLast syntax, then it will get the external flag... but then it fails to work with type:Exec and other similarly helpful types.

I feel that I'm fighting Gradle in some ways, which means I don't understand better how to work well with it. The following is a simplified pseudo-code of what I'm using. This works but I'm looking to see if this can be simplified, or indeed what the best practices are. Also, the exception shouldn't be thrown unless the tasks are being executed.

apply plugin: MyPlugin

class MyPluginExtension {
    String compileFlag = null
}

class MyPlugin implements Plugin<Project> {

    void apply(Project project) {

        project.extensions.create("myPluginConfig", MyPluginExtension)

        project.afterEvaluate {

            // Closure delays getting and checking flag until strictly needed
            def compileFlag = {
                if (project.myPluginConfig.compileFlag == null) {
                    throw new InvalidUserDataException(
                            "Must set compileFlag:  myPluginConfig { compileFlag = '-flag' }")
                }
                return project.myPluginConfig.compileFlag
            }

            // Inputs for translateTask
            def javaInputs = {
                project.files(project.fileTree(
                        dir: project.projectDir, includes: ['**/*.java']))
            }

            // This is the output of the first task and input to the second
            def translatedOutputs = {
                project.files(javaInputs().collect { file ->
                    return file.path.replace('src/', 'build/dir/')
                })
            }

            // Translates all java files into 'translatedOutputs'
            project.tasks.create(name: 'translateTask', type:Exec) {
                inputs.files javaInputs()
                outputs.files translatedOutputs()

                executable '/bin/echo'
                inputs.files.each { file ->
                    args file.path
                }
            }

            // Compiles 'translatedOutputs' to binary
            project.tasks.create(name: 'compileTask', type:Exec, dependsOn: 'translateTask') {
                inputs.files translatedOutputs()
                outputs.file project.file(project.buildDir.path + '/compiledBinary')

                executable '/bin/echo'
                args compileFlag()
                translatedOutputs().each { file ->
                    args file.path
                }
            }
        }
    }
}
like image 570
brunobowden Avatar asked Nov 22 '14 07:11

brunobowden


Video Answer


1 Answers

I'd look at this problem another way. It seems like what you want to put in your extension is really owned by each of your tasks. If you had something that was a "global" plugin configuration option, would it be treated as an input necessarily?

Another way of doing this would have been to use your own SourceSets and wire those into your custom tasks. That's not quite easy enough yet, IMO. We're still pulling together the JVM and native representations of sources.

I'd recommend extracting your Exec tasks as custom tasks with a @TaskAction that does the heavy lifting (even if it just calls project.exec {}). You can then annotate your inputs with @Input, @InputFiles, etc and your outputs with @OutputFiles, @OutputDirectory, etc. Those annotations will help auto-wire your dependencies and inputs/outputs (I think that's where some of the fighting is coming from).

Another thing that you're missing is if the compileFlag effects the final output, you'd want to detect changes to it and force a rebuild (but not a re-translate).

I simplified the body of the plugin class by using the Groovy .with method.

I'm not completely happy with this (I think the translatedFiles could be done differently), but I hope it shows you some of the best practices. I made this a working example (as long as you have a src/something.java) by implementing the translate as a copy/rename and the compile as something that just creates an 'executable' file (contents is just the list of the inputs). I've also left your extension class in place to demonstrate the "global" plug-in config. Also take a look at what happens with compileFlag is not set (I wish the error was a little better).

The translateTask isn't going to be incremental (although, I think you could probably figure out a way to do that). So you'd probably need to delete the output directory each time. I wouldn't mix other output into that directory if you want to keep that simple.

HTH

apply plugin: 'base'
apply plugin: MyPlugin

class MyTranslateTask extends DefaultTask {
    @InputFiles FileCollection srcFiles
    @OutputDirectory File translatedDir

    @TaskAction
    public void translate() {
        // println "toolhome is ${project.myPluginConfig.toolHome}"
        // translate java files by renaming them
        project.copy {
            includeEmptyDirs = false
            from(srcFiles)
            into(translatedDir)
            rename '(.+).java', '$1.m'
        }
    }
}

class MyCompileTask extends DefaultTask {
    @Input String compileFlag
    @InputFiles FileCollection translatedFiles
    @OutputDirectory File outputDir

    @TaskAction
    public void compile() {
        // write inputs to the executable file
        project.file("$outputDir/executable") << "${project.myPluginConfig.toolHome} $compileFlag ${translatedFiles.collect { it.path }}"  
    }
}

class MyPluginExtension {
    File toolHome = new File("/some/sane/default")
}

class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.with { 
            extensions.create("myPluginConfig", MyPluginExtension)

            tasks.create(name: 'translateTask', type: MyTranslateTask) {
                description = "Translates all java files into translatedDir"
                srcFiles = fileTree(dir: projectDir, includes: [ '**/*.java' ])
                translatedDir = file("${buildDir}/dir")
            }

            tasks.create(name: 'compileTask', type: MyCompileTask) {
                description = "Compiles translated files into outputDir"                
                translatedFiles = fileTree(tasks.translateTask.outputs.files.singleFile) { 
                   includes [ '**/*.m' ]
                   builtBy tasks.translateTask 
                }
                outputDir = file("${buildDir}/compiledBinary")
            }
        }
    }
}

myPluginConfig {
    toolHome = file("/some/custom/path")
}

compileTask { 
  compileFlag = '-flag'
}
like image 170
bigguy Avatar answered Oct 07 '22 03:10

bigguy