idiomatic gradle plugin writing
TRANSCRIPT
![Page 1: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/1.jpg)
#GGX Groovy Grails Exchange 2015
IDIOMATIC GRADLE
PLUGIN WRITINGSchalk W. Cronjé
![Page 2: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/2.jpg)
ABOUT ME
Email:
Twitter / Ello : @ysb33r
Gradle plugins authored/contributed to: VFS, Asciidoctor,JRuby family (base, jar, war etc.), GnuMake, Doxygen
![Page 3: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/3.jpg)
ABOUT THIS PRESENTATIONWritten in Asciidoctor (1.5.3.2)
Styled by asciidoctor-revealjs extension
Built using:
Gradle
gradle-asciidoctor-plugin
gradle-vfs-plugin
![Page 4: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/4.jpg)
THE PROBLEMThere is no consistency in the way plugin authors craft extensions
to the Gradle DSL today
![Page 5: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/5.jpg)
QUALITY ATTRIBUTES OF DSL
Readability
Consistency
Flexibility
Expressiveness
![Page 6: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/6.jpg)
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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/7.jpg)
FOR BEST COMPATIBILITY// build.gradle targetCompatibility = 1.6 sourceCompatibility = 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.properties distributionUrl=https\://..../distributions/gradle-2.0-all.zip
![Page 8: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/8.jpg)
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
@Input String aProperty = 'stdValue'
@Input void aValue(String s) { ... }
myTask { aProperty = 'newValue'
aValue 'newValue' }
![Page 9: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/9.jpg)
PREFER METHODS OVER PROPERTIES( 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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/10.jpg)
HOW NOT 2 : COLLECTION OF FILESTypical implementation …
class MyTask extends DefaultTask {
@InputFiles List<File> mySources
}
leads to ugly DSL
task myTask( type: MyTask ) {
myTask = [ file('foo/bar.txt'), new File( 'bar/foo.txt') ]
}
![Page 11: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/11.jpg)
COLLECTION OF FILESmyTask { 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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/12.jpg)
COLLECTION OF FILES
Ignore Groovy shortcut; use three methods
class 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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/13.jpg)
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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/14.jpg)
KNOW YOUR ANNOTATIONS
@Input
@InputFile
@InputFiles
@InputDirectory
@OutputFile
@OutputFiles
@OutputDirectory
@OutputDirectories
@Optional
![Page 15: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/15.jpg)
COLLECTION OF STRINGSimport 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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/16.jpg)
HOW NOT 2 : MAPSTypical implementation …
class MyTask extends DefaultTask {
@Input
Map myOptions
}
leads to ugly DSL
task myTask( type: MyTask ) {
myOptions = [ prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt' ]
}
![Page 17: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/17.jpg)
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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/18.jpg)
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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/19.jpg)
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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/20.jpg)
USER OVERRIDE LIBRARY VERSION
Example DSL from Asciidoctor
asciidoctorj { version = '1.6.0-SNAPSHOT' }
Example DSL from JRuby Base
jruby { execVersion = '1.7.12'}
![Page 21: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/21.jpg)
USER OVERRIDE LIBRARY VERSION
1. Create Extension
2. Add extension object in plugin apply
3. Create custom classloader
![Page 22: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/22.jpg)
USER OVERRIDE LIBRARY VERSION
Step 1: Create project extension
class 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](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/23.jpg)
USER OVERRIDE LIBRARY VERSION
Step 2: Add extension object in plugin apply
class MyPlugin implements Plugin<Project> { void apply(Project project) {
// Create the extension & configuration project.extensions.create('asciidoctorj',MyExtension,project) project.configuration.maybeCreate( 'int_asciidoctorj' )
// Add dependency at the end of configuration phase project.afterEvaluate { project.dependencies { int_asciidoctorj "org.asciidoctor:asciidoctorj" + "${project.asciidoctorj.version}" } } } }
![Page 24: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/24.jpg)
USER OVERRIDE LIBRARY VERSION (2.5+)
Step 2: Add extension object Gradle 2.5+
class MyPlugin implements Plugin<Project> { void apply(Project project) {
// Create the extension & configuration project.extensions.create('asciidoctorj',MyExtension,project) def conf = configurations.maybeCreate( 'int_asciidoctorj' )
conf.defaultDependencies { deps -> deps.add( project.dependencies.create( "org.asciidoctor:asciidoctorj:${asciidoctorj.version}") ) } } }
![Page 25: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/25.jpg)
USER OVERRIDE LIBRARY VERSION
Step 3: Custom classloader (usually loaded from task action)
// Get all of the files in the `asciidoctorj` configuration def urls = project.configurations.int_asciidoctorj.files.collect { it.toURI().toURL() }
// Create the classloader for all those files def classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader)
// Load one or more classes as required def instance = classLoader.loadClass( 'org.asciidoctor.Asciidoctor$Factory')
![Page 26: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/26.jpg)
NEED 2 KNOW : '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 27: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/27.jpg)
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 28: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/28.jpg)
NEED 2 KNOW : 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 29: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/29.jpg)
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 30: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/30.jpg)
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 extension
jrubyJavaBootstrap { // Expressing intent & context. jruby { initScript = 'bin/asciidoctor' } }
![Page 31: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/31.jpg)
EXTEND EXISTING TASK1. Create extension class
2. Add extension to task
3. Link extension attributes to task attributes (for caching)
![Page 32: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/32.jpg)
EXTEND EXISTING TASKCreate extension class
class MyExtension { String initScript
MyExtension( Task t ) {
// TODO: Add Gradle caching support // (See later slide) }
}
![Page 33: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/33.jpg)
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 34: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/34.jpg)
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 'jrubyInitScript' , { -> this.initScript } } }
![Page 35: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/35.jpg)
NEED 2 KNOW : 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 36: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/36.jpg)
HONOUR OFFLINEgradle --offline
The build should operate without accessingnetwork resources.
![Page 37: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/37.jpg)
HONOUR OFFLINEUnset the enabled property, if build is offline
task VfsCopy extends DefaultTask { VfsCopy() {
enabled = !project.gradle.startParameter.isOffline()
}
}
![Page 38: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/38.jpg)
ADD GENERATED JVM SOURCE SETS
May need to generate code from template and add to
current sourceset(s)
Example: Older versions of jruby-jar-plugin added
a custom class file to JAR
Useful for separation of concerns in certain generative
programming environments
![Page 39: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/39.jpg)
ADD GENERATED JVM SOURCE SETS
1. Create generator task using Copy task as transformer
2. Configure generator task
3. Update SourceSet
4. Add dependency between generation and compilation
![Page 40: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/40.jpg)
ADD GENERATED JVM SOURCE SETS
Step1 : Add generator task
class 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 of sourcesetthat Gradle supports
![Page 41: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/41.jpg)
ADD GENERATED JVM SOURCE SETS
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 42: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/42.jpg)
ADD GENERATED JVM SOURCE SETS
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 43: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/43.jpg)
ADD GENERATED JVM SOURCE SETS
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 44: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/44.jpg)
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 API import 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 45: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/45.jpg)
TRICK : SELF-REFERENCING PLUGINNew plugin depends on functionality in the plugin
Apply plugin direct in build.gradle
apply plugin: new GroovyScriptEngine( ['src/main/groovy','src/main/resources']. collect{ file(it).absolutePath } .toArray(new String[2]), project.class.classLoader ).loadScriptByName('book/SelfReferencingPlugin.groovy')
![Page 46: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/46.jpg)
COMPATIBILITY TESTING
How can a plugin author test a plugin against multiple Gradle
versions?
![Page 47: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/47.jpg)
COMPATIBILITY TESTING
Gradle 2.7 added TestKit
Gradle 2.9 added multi-distribution testing
TestKit still falls short in ease-of-use
(Hopefully to be corrected over future releases)
What to do for Gradle 2.0 - 2.8?
![Page 48: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/48.jpg)
COMPATIBILITY TESTING
GradleTest plugin to the rescue
buildscript { dependencies { classpath "org.ysb33r.gradle:gradletest:0.5.4" } }
apply plugin : 'org.ysb33r.gradletest'
http://bit.ly/1LfUUU4
![Page 49: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/49.jpg)
COMPATIBILITY TESTING
Create src/gradleTest/NameOfTest folder.
Add build.gradle
Add task runGradleTest
Add project structure
![Page 50: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/50.jpg)
COMPATIBILITY TESTING
Add versions to main build.gradle
gradleTest { versions '2.0', '2.2', '2.4', '2.5', '2.9' }
Run it!
./gradlew gradleTest
![Page 51: Idiomatic Gradle Plugin Writing](https://reader031.vdocument.in/reader031/viewer/2022021919/58720e291a28ab176b8b7f35/html5/thumbnails/51.jpg)
THANK YOU
Keep your DSLextensions beautiful
Don’t spring surprisingbehaviour on the user
Email:
Twitter / Ello : @ysb33r
#idiomaticgradle
http://bit.ly/1iJmdiP