Idiomatic Gradle Plugin Writing

44
IDIOMATIC GRADLE PLUGIN WRITING Schalk W. Cronjé

Transcript of Idiomatic Gradle Plugin Writing

Page 1: Idiomatic Gradle Plugin Writing

IDIOMATIC GRADLE PLUGIN WRITING

Schalk W. Cronjé

Page 2: Idiomatic Gradle Plugin Writing

ABOUT ME

Email:

Twitter / Ello : @ysb33r

[email protected]

Gradle plugins authored/contributed to: VFS, Asciidoctor,JRuby family (base, jar, war etc.), GnuMake, Doxygen

Page 3: Idiomatic Gradle Plugin Writing

ABOUT THIS PRESENTATIONWritten in Asciidoctor

Styled by asciidoctor-revealjs extension

Built using:

Gradle

gradle-asciidoctor-plugin

gradle-vfs-plugin

Page 4: Idiomatic Gradle Plugin Writing

THE PROBLEMThere is no consistency in the way plugin authors craft

extensions to the Gradle DSL today

Page 5: Idiomatic Gradle Plugin Writing

QUALITY ATTRIBUTES OF DSL

Readability

Consistency

Flexibility

Expressiveness

Page 6: Idiomatic Gradle Plugin Writing

FOR BEST COMPATIBILITY

Support same JDK range as Gradle

Gradle 1.x - mininum JDK5

Gradle 2.x - minimum JDK6

Build against Gradle 2.0

Only use later versions if specific new functionality is

required.

Suggested baseline at Gradle 2.6

Page 7: Idiomatic Gradle Plugin Writing

FOR BEST COMPATIBILITY

// build.gradletargetCompatibility = 1.6sourceCompatibility = 1.6

project.tasks.withType(JavaCompile) { task -> task.sourceCompatibility = project.sourceCompatibility task.targetCompatibility = project.targetCompatibility}

project.tasks.withType(GroovyCompile) { task -> task.sourceCompatibility = project.sourceCompatibility task.targetCompatibility = project.targetCompatibility}

// gradle/wrapper/gradle-wrapper.propertiesdistributionUrl=https\://..../distributions/gradle-2.0-all.zip

Page 8: Idiomatic Gradle Plugin Writing

NOMENCLATURE

Property: A public data member (A Groovy property)

Method: A standard Java/Groovy method

Attribute: A value, set or accessed via the Gradle DSL. Canresult in a public method call or property access.

User: Person authoring or executing a Gradle build script

@InputString aProperty = 'stdValue'

@Inputvoid aValue(String s) { ... }

myTask { aProperty = 'newValue'

aValue 'newValue'}

Page 9: Idiomatic Gradle Plugin Writing

PREFER METHODS OVERPROPERTIES

( IOW To assign or not to assign )

Methods provide more flexibility

Tend to provide better readability

Assignment is better suited towards

One-shot attribute setting

Overriding default attributes

Non-lazy evaluation

Page 10: Idiomatic Gradle Plugin Writing

HOWNOT2 : COLLECTION OF FILESTypical implementation …

class MyTask extends DefaultTask {

@InputFiles List<File> mySources

}

leads to ugly DSLtask myTask( type: MyTask ) {

myTask = [ file('foo/bar.txt'), new File( 'bar/foo.txt') ]

}

Page 11: Idiomatic Gradle Plugin Writing

COLLECTION OF FILES

myTask { mySources file( 'path/foobar' ) mySources new File( 'path2/foobar' ) mySources 'file3', 'file4' mySources { "lazy evaluate file name later on" }}

Allow ability to:

Use strings and other objects convertible to File

Append lists

Evaluate as late as possible

Reset default values

Page 12: Idiomatic Gradle Plugin Writing

COLLECTION OF FILES

Ignore Groovy shortcut; use three methodsclass MyTask extends DefaultTask { @InputFiles

FileCollection getDocuments() {

project.files(this.documents) // magic API method }

void setDocuments(Object... docs) { this.documents.clear()

this.documents.addAll(docs as List) }

void documents(Object... docs) { this.documents.addAll(docs as List) }

private List<Object> documents = []}

Page 13: Idiomatic Gradle Plugin Writing

STYLE : TASKSProvide a default instantiation of your new task class

Keep in mind that user would want to create additionaltasks of same type

Make it easy for them!!

Page 14: Idiomatic Gradle Plugin Writing

KNOW YOUR ANNOTATIONS

@Input

@InputFile

@InputFiles

@InputDirectory

@OutputFile

@OutputFiles

@OutputDirectory

@OutputDirectories

@Optional

Page 15: Idiomatic Gradle Plugin Writing

COLLECTION OF STRINGS

import org.gradle.util.CollectionUtils

Ignore Groovy shortcut; use three methods @Input

List<String> getScriptArgs() {

// stringize() is your next magic API method CollectionUtils.stringize(this.scriptArgs)

}

void setScriptArgs(Object... args) { this.scriptArgs.clear()

this.scriptArgs.addAll(args as List) }

void scriptArgs(Object... args) { this.scriptArgs.addAll(args as List) }

private List<Object> scriptArgs = []

Page 16: Idiomatic Gradle Plugin Writing

HOWNOT2 : MAPSTypical implementation …

class MyTask extends DefaultTask {

@Input

Map myOptions

}

leads to ugly DSLtask myTask( type: MyTask ) {

myOptions = [ prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt' ]

}

Page 17: Idiomatic Gradle Plugin Writing

MAPStask myTask( type: MyTask ) {

myOptions prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt'

myOptions prop3 : 'add/another'

// Explicit reset myOptions = [:]

}

Page 18: Idiomatic Gradle Plugin Writing

MAPS@Input

Map getMyOptions() {

this.attrs

}

void setMyOptions(Map m) { this.attrs=m

}

void myOptions(Map m) { this.attrs+=m

}

private Map attrs = [:]

Page 19: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

Ship with prefered (and tested) version of dependentlibrary set as default

Allow user flexibility to try a different version of suchlibrary

Dynamically load library when needed

Still use power of Gradle’s dependency resolution

Page 20: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

Example DSL from Asciidoctorasciidoctorj { version = '1.5.3-SNAPSHOT'}

Example DSL from JRuby Basejruby { execVersion = '1.7.12'}

Page 21: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

1. Create Extension

2. Add extension object in plugin apply

3. Create custom classloader

Page 22: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

Step 1: Create project extensionclass MyExtension {

// Set the default dependent library version String version = '1.5.0'

MyExtension(Project proj) { project= proj }

@PackageScope Project project}

Page 23: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

Step 2: Add extension object in plugin applyclass MyPlugin implements Plugin<Project> { void apply(Project project) {

// Create the extension & configuration project.extensions.create('asciidoctorj',MyExtension,project) project.configuration.maybeCreate( 'asciidoctorj' )

// Add dependency at the end of configuration phase project.afterEvaluate { project.dependencies { asciidoctorj "org.asciidoctor:asciidoctorj" + "${project.asciidoctorj.version}" } } }}

Page 24: Idiomatic Gradle Plugin Writing

USER OVERRIDE LIBRARY VERSION

Step 3: Custom classloader (usually loaded from task action)// Get all of the files in the `asciidoctorj` configurationdef urls = project.configurations.asciidoctorj.files.collect { it.toURI().toURL()}

// Create the classloader for all those filesdef classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader)

// Load one or more classes as requireddef instance = classLoader.loadClass( 'org.asciidoctor.Asciidoctor$Factory')

Page 25: Idiomatic Gradle Plugin Writing

NEED2KNOW : 'AFTEREVALUATE'

afterEvaluate adds to a list of closures to be

executed at end of configuration phase

Execution order is FIFO

Plugin author has no control over the order

Page 26: Idiomatic Gradle Plugin Writing

STYLE : PROJECT EXTENSIONS

Treat project extensions as you would for any kind ofglobal configuration.

With care!

Do not make the extension configuration block a taskconfiguration.

Task instantiation may read defaults from extension.

Do not force extension values onto tasks

Page 27: Idiomatic Gradle Plugin Writing

GENERATED CODE

May need to generate code from template and add tocurrent sourceset(s)

Example: jruby-jar-plugin adds a custom classfile to JAR

Useful for separation of concerns in certain generativeprogramming environments

Page 28: Idiomatic Gradle Plugin Writing

GENERATED CODE

1. Create generator task using Copy task as transformer

2. Configure generator task

3. Update SourceSet

4. Add dependency between generation and compilation

Page 29: Idiomatic Gradle Plugin Writing

GENERATED CODE

Step1 : Add generator taskclass MyPlugin implements Plugin<Project> { void apply(Project project) { Task stubTask = project.tasks.create ( name : 'myGenerator', type : Copy )

configureGenerator(stubTask) addGeneratedToSource(project) addTaskDependencies(project) }

void configureGenerator(Task t) { /* TODO: <-- See next slides */ } void addGeneratedToSource(Project p) { /* TODO: <-- See next slides */ } void addTaskDependencies(Project p) { /* TODO: <-- See next slides */ }}

This example uses Java, but can apply to any kind ofsourceset that Gradle supports

Page 30: Idiomatic Gradle Plugin Writing

GENERATED CODE

Step 2 : Configure generator task/* DONE: <-- See previous slide for apply() */

void configureGenerator(Task stubTask) { project.configure(stubTask) { group "Add to correct group" description 'Generates a JRuby Java bootstrap class'

from('src/template/java') { include '*.java.template' } into new File(project.buildDir,'generated/java')

rename '(.+)\\.java\\.template','$1.java' filter { String line -> /* Do something in here to transform the code */ } }}

Page 31: Idiomatic Gradle Plugin Writing

GENERATED CODE

Step 3 : Add generated code to SourceSet/* DONE: <-- See earlier slide for apply() */

void addGeneratedToSource(Project project) {

project.sourceSets.matching { it.name == "main" } .all { it.java.srcDir new File(project.buildDir,'generated/java') }

}

Page 32: Idiomatic Gradle Plugin Writing

GENERATED CODE

Step 4 : Add task dependencies/* DONE: <-- See earlier slide for apply() */

void addTaskDependencies(Project project) { try { Task t = project.tasks.getByName('compileJava')

if( t instanceof JavaCompile) { t.dependsOn 'myGenerator'

}

} catch(UnknownTaskException) { project.tasks.whenTaskAdded { Task t ->

if (t.name == 'compileJava' && t instanceof JavaCompile) { t.dependsOn 'myGenerator'

}

}

}

}

Page 33: Idiomatic Gradle Plugin Writing

NEED2KNOW : PLUGINS

Plugin author has no control over order in which plugins

will be applied

Handle both cases of related plugin applied before or

after yours

Page 34: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASKTask type extension by inheritance is not always bestsolution

Adding behaviour to existing task type better in certaincontexts

Example: jruby-jar-plugin wants to semanticallydescribe bootstrap files rather than force user to usestandard Copy syntax

Page 35: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASKjruby-jar-plugin without extension

jrubyJavaBootstrap { // User gets exposed (unnecessarily) to the underlying task type // Has to craft too much glue code from( { // @#$$!!-ugly code goes here } )}

jruby-jar-plugin with extensionjrubyJavaBootstrap { // Expressing intent & context. jruby { initScript = 'bin/asciidoctor' }}

Page 36: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASK1. Create extension class

2. Add extension to task

3. Link extension attributes to task attributes (for caching)

Page 37: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASKCreate extension class

class MyExtension { String initScript

MyExtension( Task t ) {

// TODO: Add Gradle caching support // (See later slide) }

}

Page 38: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASKAdd extension class to task

class MyPlugin implements Plugin<Project> { void apply(Project project) { Task stubTask = project.tasks.create ( name : 'jrubyJavaBootstrap', type : Copy )

stubTask.extensions.create( 'jruby', MyExtension, stubTask )}

Page 39: Idiomatic Gradle Plugin Writing

EXTEND EXISTING TASKAdd Gradle caching support

class MyExtension { String initScript

MyExtension( Task t ) {

// Tell the task the initScript is also a property t.inputs.property 'jrubyInitScipt' , { -> this.initScript } }}

Page 40: Idiomatic Gradle Plugin Writing

NEED2KNOW : TASK EXTENSIONS

Good way extend existing tasks in composable way

Attributes on extensions are not cached

Changes will not cause a rebuild of the task

Do the extra work to cache and provide the user with abetter experience.

Page 41: Idiomatic Gradle Plugin Writing

TRICK : SELF-REFERENCING PLUGINNew plugin depends on functionality in the plugin

Apply plugin direct in build.gradle

apply plugin: new GroovyScriptEngine(

[file('src/main/groovy').absolutePath, file('src/main/resources').absolutePath]. toArray(new String[2]), this.class.classLoader

).loadScriptByName('src/main/groovy/spath/to/MyPlugin.groovy')

Page 42: Idiomatic Gradle Plugin Writing

TRICK : SAFE FILENAMESAbility to create safe filenames on all platforms from inputdata

Example: Asciidoctor output directories based uponbackend names

// WARNING: Using a very useful internal APIimport org.gradle.internal.FileUtils

File outputBackendDir(final File outputDir, final String backend) { // FileUtils.toSafeFileName is your magic method new File(outputDir, FileUtils.toSafeFileName(backend))}

Page 43: Idiomatic Gradle Plugin Writing

TRICK : OPERATING SYSTEM

Sometimes customised work has to be done on a specific

O/S

Example: jruby-gradle-plugin needs to set TMP in

environment on Windows

// This is the public interface APIimport org.gradle.nativeplatform.platform.OperatingSystem

// But to get an instance the internal API is needed insteadimport org.gradle.internal.os.OperatingSystem

println "Are we on Windows? ${OperatingSystem.current().isWindows()}

Page 44: Idiomatic Gradle Plugin Writing

CONCLUSION

Keep your DSL extensions beautiful

Don’t spring surprising behaviour on the user

Email:

Twitter / Ello : @ysb33r

[email protected]