Idiomatic Gradle Plugin Writing

Schalk Cronjé
Schalk CronjéDirector / Consultant at Ntaba UK Ltd
IDIOMATIC GRADLE PLUGIN WRITING
Schalk W. Cronjé
ABOUT ME
Email:
Twitter / Ello : @ysb33r
ysb33r@gmail.com
Gradle plugins authored/contributed to: VFS, Asciidoctor,
JRuby family (base, jar, war etc.), GnuMake, Doxygen
ABOUT THIS PRESENTATION
Written in Asciidoctor
Styled by asciidoctor-revealjs extension
Built using:
Gradle
gradle-asciidoctor-plugin
gradle-vfs-plugin
THE PROBLEM
There is no consistency in the way plugin authors craft
extensions to the Gradle DSL today
QUALITY ATTRIBUTES OF DSL
Readability
Consistency
Flexibility
Expressiveness
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
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
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. Can
result 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'
}
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
HOWNOT2 : COLLECTION OF FILES
Typical 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') ]
}
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
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 = []
}
STYLE : TASKS
Provide a default instantiation of your new task class
Keep in mind that user would want to create additional
tasks of same type
Make it easy for them!!
KNOW YOUR ANNOTATIONS
@Input
@InputFile
@InputFiles
@InputDirectory
@OutputFile
@OutputFiles
@OutputDirectory
@OutputDirectories
@Optional
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 = []
HOWNOT2 : MAPS
Typical 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' ]
}
MAPS
task myTask( type: MyTask ) {
myOptions prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt'
myOptions prop3 : 'add/another'
// Explicit reset
myOptions = [:]
}
MAPS
@Input
Map getMyOptions() {
this.attrs
}
void setMyOptions(Map m) {
this.attrs=m
}
void myOptions(Map m) {
this.attrs+=m
}
private Map attrs = [:]
USER OVERRIDE LIBRARY VERSION
Ship with prefered (and tested) version of dependent
library set as default
Allow user flexibility to try a different version of such
library
Dynamically load library when needed
Still use power of Gradle’s dependency resolution
USER OVERRIDE LIBRARY VERSION
Example DSL from Asciidoctor
asciidoctorj {
version = '1.5.3-SNAPSHOT'
}
Example DSL from JRuby Base
jruby {
execVersion = '1.7.12'
}
USER OVERRIDE LIBRARY VERSION
1. Create Extension
2. Add extension object in plugin apply
3. Create custom classloader
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
}
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( 'asciidoctorj' )
// Add dependency at the end of configuration phase
project.afterEvaluate {
project.dependencies {
asciidoctorj "org.asciidoctor:asciidoctorj" +
"${project.asciidoctorj.version}"
}
}
}
}
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.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')
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
STYLE : PROJECT EXTENSIONS
Treat project extensions as you would for any kind of
global configuration.
With care!
Do not make the extension configuration block a task
configuration.
Task instantiation may read defaults from extension.
Do not force extension values onto tasks
GENERATED CODE
May need to generate code from template and add to
current sourceset(s)
Example: jruby-jar-plugin adds a custom class
file to JAR
Useful for separation of concerns in certain generative
programming environments
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
GENERATED CODE
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
sourceset that Gradle supports
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 */ }
}
}
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')
}
}
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'
}
}
}
}
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
EXTEND EXISTING TASK
Task type extension by inheritance is not always best
solution
Adding behaviour to existing task type better in certain
contexts
Example: jruby-jar-plugin wants to semantically
describe bootstrap files rather than force user to use
standard Copy syntax
EXTEND EXISTING TASK
jruby-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'
}
}
EXTEND EXISTING TASK
1. Create extension class
2. Add extension to task
3. Link extension attributes to task attributes (for caching)
EXTEND EXISTING TASK
Create extension class
class MyExtension {
String initScript
MyExtension( Task t ) {
// TODO: Add Gradle caching support
// (See later slide)
}
}
EXTEND EXISTING TASK
Add 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
)
}
EXTEND EXISTING TASK
Add 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 }
}
}
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 a
better experience.
TRICK : SELF-REFERENCING PLUGIN
New 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')
TRICK : SAFE FILENAMES
Ability to create safe filenames on all platforms from input
data
Example: Asciidoctor output directories based upon
backend 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))
}
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 API
import org.gradle.nativeplatform.platform.OperatingSystem
// But to get an instance the internal API is needed instead
import org.gradle.internal.os.OperatingSystem
println "Are we on Windows? ${OperatingSystem.current().isWindows()}
CONCLUSION
Keep your DSL extensions beautiful
Don’t spring surprising behaviour on the user
Email:
Twitter / Ello : @ysb33r
ysb33r@gmail.com
1 of 44

More Related Content

What's hot(20)

Gradle IntroductionGradle Introduction
Gradle Introduction
Dmitry Buzdin7.8K views
The world of gradle  - an introduction for developersThe world of gradle  - an introduction for developers
The world of gradle - an introduction for developers
Tricode (part of Dept)2.7K views
SBT Crash CourseSBT Crash Course
SBT Crash Course
Michal Bigos5.9K views
In the Brain of Hans Dockter: GradleIn the Brain of Hans Dockter: Gradle
In the Brain of Hans Dockter: Gradle
Skills Matter1.5K views
Simple Build ToolSimple Build Tool
Simple Build Tool
David Galichet5.3K views
Getting started with sbtGetting started with sbt
Getting started with sbt
ikenna4u463 views
Continous delivery with sbtContinous delivery with sbt
Continous delivery with sbt
Wojciech Pituła2.3K views
Scala and Play with GradleScala and Play with Gradle
Scala and Play with Gradle
Wei Chen2.1K views
Gradle talk, Javarsovia 2010Gradle talk, Javarsovia 2010
Gradle talk, Javarsovia 2010
Tomek Kaczanowski7K views
Gradle - time for another buildGradle - time for another build
Gradle - time for another build
Igor Khotin2.4K views
SBT Concepts, part 2SBT Concepts, part 2
SBT Concepts, part 2
Renat Bekbolatov1.1K views

Similar to Idiomatic Gradle Plugin Writing(20)

GradleFXGradleFX
GradleFX
Christophe Herreman3K views
Making the Most of Your Gradle BuildMaking the Most of Your Gradle Build
Making the Most of Your Gradle Build
Andres Almiray1.5K views
Anatomy of a Gradle pluginAnatomy of a Gradle plugin
Anatomy of a Gradle plugin
Dmytro Zaitsev574 views
Making the Most of Your Gradle BuildMaking the Most of Your Gradle Build
Making the Most of Your Gradle Build
Andres Almiray2.2K views
Improving your Gradle buildsImproving your Gradle builds
Improving your Gradle builds
Peter Ledbrook1.1K views
Dart WorkshopDart Workshop
Dart Workshop
Dmitry Buzdin8.2K views
Gradle - small introductionGradle - small introduction
Gradle - small introduction
Igor Popov844 views
Ejb3 Struts Tutorial EnEjb3 Struts Tutorial En
Ejb3 Struts Tutorial En
Ankur Dongre357 views
Ejb3 Struts Tutorial EnEjb3 Struts Tutorial En
Ejb3 Struts Tutorial En
Ankur Dongre703 views
Dependency injection in scalaDependency injection in scala
Dependency injection in scala
Michal Bigos9.8K views
GradleGradle
Gradle
Return on Intelligence6K views
Multilingualism makes better programmersMultilingualism makes better programmers
Multilingualism makes better programmers
Alexander Varwijk152 views
10 Cool Facts about Gradle10 Cool Facts about Gradle
10 Cool Facts about Gradle
Evgeny Goldin6.8K views
Introduction to Apache AntIntroduction to Apache Ant
Introduction to Apache Ant
Shih-Hsiang Lin1.5K views
Doctrine and NoSQLDoctrine and NoSQL
Doctrine and NoSQL
Benjamin Eberlei5.1K views

More from Schalk Cronjé(20)

DocuOps & Asciidoctor in a JVM WorldDocuOps & Asciidoctor in a JVM World
DocuOps & Asciidoctor in a JVM World
Schalk Cronjé592 views
DocuOps & AsciidoctorDocuOps & Asciidoctor
DocuOps & Asciidoctor
Schalk Cronjé1.5K views
What's new in AsciidoctorWhat's new in Asciidoctor
What's new in Asciidoctor
Schalk Cronjé425 views
Probability ManagementProbability Management
Probability Management
Schalk Cronjé112 views
Gradle in a Polyglot WorldGradle in a Polyglot World
Gradle in a Polyglot World
Schalk Cronjé1.6K views
Asciidoctor in 15minAsciidoctor in 15min
Asciidoctor in 15min
Schalk Cronjé1.1K views
Beyond estimates - Overview at Agile:MKBeyond estimates - Overview at Agile:MK
Beyond estimates - Overview at Agile:MK
Schalk Cronjé548 views
Groovy VFS (with 1.0 news)Groovy VFS (with 1.0 news)
Groovy VFS (with 1.0 news)
Schalk Cronjé970 views
RfC2822 for Mere MortalsRfC2822 for Mere Mortals
RfC2822 for Mere Mortals
Schalk Cronjé794 views
Groovy VFSGroovy VFS
Groovy VFS
Schalk Cronjé3K views
Real World TDDReal World TDD
Real World TDD
Schalk Cronjé953 views

Recently uploaded(20)

Neo4j y GenAI Neo4j y GenAI
Neo4j y GenAI
Neo4j27 views
Unleash The MonkeysUnleash The Monkeys
Unleash The Monkeys
Jacob Duijzer7 views
WebAssemblyWebAssembly
WebAssembly
Jens Siebert32 views
LAVADORA ROLO.docxLAVADORA ROLO.docx
LAVADORA ROLO.docx
SamuelRamirez835247 views
Advanced API Mocking TechniquesAdvanced API Mocking Techniques
Advanced API Mocking Techniques
Dimpy Adhikary17 views

Idiomatic Gradle Plugin Writing

  • 1. IDIOMATIC GRADLE PLUGIN WRITING Schalk W. Cronjé
  • 2. ABOUT ME Email: Twitter / Ello : @ysb33r ysb33r@gmail.com Gradle plugins authored/contributed to: VFS, Asciidoctor, JRuby family (base, jar, war etc.), GnuMake, Doxygen
  • 3. ABOUT THIS PRESENTATION Written in Asciidoctor Styled by asciidoctor-revealjs extension Built using: Gradle gradle-asciidoctor-plugin gradle-vfs-plugin
  • 4. THE PROBLEM There is no consistency in the way plugin authors craft extensions to the Gradle DSL today
  • 5. QUALITY ATTRIBUTES OF DSL Readability Consistency Flexibility Expressiveness
  • 6. 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
  • 7. 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
  • 8. 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. Can result 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' }
  • 9. 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
  • 10. HOWNOT2 : COLLECTION OF FILES Typical 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') ] }
  • 11. 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
  • 12. 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 = [] }
  • 13. STYLE : TASKS Provide a default instantiation of your new task class Keep in mind that user would want to create additional tasks of same type Make it easy for them!!
  • 15. 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 = []
  • 16. HOWNOT2 : MAPS Typical 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' ] }
  • 17. MAPS task myTask( type: MyTask ) { myOptions prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt' myOptions prop3 : 'add/another' // Explicit reset myOptions = [:] }
  • 18. MAPS @Input Map getMyOptions() { this.attrs } void setMyOptions(Map m) { this.attrs=m } void myOptions(Map m) { this.attrs+=m } private Map attrs = [:]
  • 19. USER OVERRIDE LIBRARY VERSION Ship with prefered (and tested) version of dependent library set as default Allow user flexibility to try a different version of such library Dynamically load library when needed Still use power of Gradle’s dependency resolution
  • 20. USER OVERRIDE LIBRARY VERSION Example DSL from Asciidoctor asciidoctorj { version = '1.5.3-SNAPSHOT' } Example DSL from JRuby Base jruby { execVersion = '1.7.12' }
  • 21. USER OVERRIDE LIBRARY VERSION 1. Create Extension 2. Add extension object in plugin apply 3. Create custom classloader
  • 22. 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 }
  • 23. 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( 'asciidoctorj' ) // Add dependency at the end of configuration phase project.afterEvaluate { project.dependencies { asciidoctorj "org.asciidoctor:asciidoctorj" + "${project.asciidoctorj.version}" } } } }
  • 24. 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.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')
  • 25. 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
  • 26. STYLE : PROJECT EXTENSIONS Treat project extensions as you would for any kind of global configuration. With care! Do not make the extension configuration block a task configuration. Task instantiation may read defaults from extension. Do not force extension values onto tasks
  • 27. GENERATED CODE May need to generate code from template and add to current sourceset(s) Example: jruby-jar-plugin adds a custom class file to JAR Useful for separation of concerns in certain generative programming environments
  • 28. 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
  • 29. GENERATED CODE 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 sourceset that Gradle supports
  • 30. 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 */ } } }
  • 31. 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') } }
  • 32. 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' } } } }
  • 33. 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
  • 34. EXTEND EXISTING TASK Task type extension by inheritance is not always best solution Adding behaviour to existing task type better in certain contexts Example: jruby-jar-plugin wants to semantically describe bootstrap files rather than force user to use standard Copy syntax
  • 35. EXTEND EXISTING TASK jruby-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' } }
  • 36. EXTEND EXISTING TASK 1. Create extension class 2. Add extension to task 3. Link extension attributes to task attributes (for caching)
  • 37. EXTEND EXISTING TASK Create extension class class MyExtension { String initScript MyExtension( Task t ) { // TODO: Add Gradle caching support // (See later slide) } }
  • 38. EXTEND EXISTING TASK Add 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 ) }
  • 39. EXTEND EXISTING TASK Add 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 } } }
  • 40. 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 a better experience.
  • 41. TRICK : SELF-REFERENCING PLUGIN New 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')
  • 42. TRICK : SAFE FILENAMES Ability to create safe filenames on all platforms from input data Example: Asciidoctor output directories based upon backend 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)) }
  • 43. 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 API import org.gradle.nativeplatform.platform.OperatingSystem // But to get an instance the internal API is needed instead import org.gradle.internal.os.OperatingSystem println "Are we on Windows? ${OperatingSystem.current().isWindows()}
  • 44. CONCLUSION Keep your DSL extensions beautiful Don’t spring surprising behaviour on the user Email: Twitter / Ello : @ysb33r ysb33r@gmail.com