#GradleSummit Gradle Summit 2016
IDIOMATIC GRADLE
PLUGIN WRITING
Schalk W. Cronjé
1
ABOUT ME
Email:
Twitter / Ello : @ysb33r
ysb33r@gmail.com
Gradle plugins authored/contributed to: VFS, Asciidoctor,
JRuby family (base, jar, war etc.), GnuMake, Doxygen, Bintray
2
 
3
ABOUT THIS PRESENTATION
Written in Asciidoctor (1.5.3.2)
Styled by asciidoctor-revealjs extension
Built using:
Gradle
gradle-asciidoctor-plugin
gradle-vfs-plugin
4
GET YOUR DAILY GRADLE DOSE
@DailyGradle
#gradleTip
5
6
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
7
PROJECT LAYOUT
Figure 1. Plugin project le layout
8
BUILD SCRIPT
repositories {
jcenter()
}
apply plugin : 'groovy'
dependencies {
compile localGroovy()
compile gradleApi()
testCompile ("org.spockframework:spock-core:1.0-groovy-2.3") {
exclude module : 'groovy-all'
}
}
9 . 1
9 . 2
TRICK : SPOCK VERSION
ext {
spockGrVer = GroovySystem.version.replaceAll(/.d+$/,'')
}
dependencies {
testCompile ("org.spockframework:spock-core:1.0-${spockGrVer}") {
exclude module : 'groovy-all'
}
}
CREATE PLUGIN CLASS
package idiomatic.gradle.authoring
import org.gradle.api.Plugin
import org.gradle.api.Project
class MyExamplePlugin implements Plugin<Project> {
void apply(Project project) {
}
}
10 . 1
10 . 2
CREATE PROPERTIES FILE
META-INF/gradle-
plugins/idiomatic.authored.example.properties
implementation-class=idiomatic.gradle.authoring.MyExamplePlugin
Name of le must match plugin identi er
11
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
FOR BEST COMPATIBILITY
Support same JDK range as Gradle
Gradle 1.x - mininum JDK5
Gradle 2.x - minimum JDK6
Build against Gradle 2.0
… unless proper compatibility testing is in place
Suggested baseline at Gradle 2.12 (for new model)
Only use later versions if speci c new functionality is
required.
12 . 1
JDK 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
}
(Fixed in 2.14)
12 . 2
12 . 3
GRADLE BUILD VERSION
gradle/wrapper/gradle-wrapper.properties
distributionUrl=https://..../distributions/gradle-2.0-all.zip
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!!
13 . 1
CREATE TASK CLASS
package idiomatic.gradle.authoring
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
class MyExampleTasks extends DefaultTask {
@TaskAction
void exec() {
}
}
13 . 2
14 . 1
HONOUR OFFLINE
gradle --offline
The build should operate without accessing
network resources.
14 . 2
HONOUR OFFLINE
Unset the enabled property, if build is of ine
task VfsCopy extends DefaultTask {
VfsCopy() {
enabled = !project.gradle.startParameter.isOffline()
}
}
PREFER METHODS OVER PROPERTIES
( IOW To assign or not to assign )
Methods provide more exibility
Tend to provide better readability
Assignment is better suited towards
One-shot attribute setting
Overriding default attributes
Non-lazy evaluation
15
HOW NOT 2 : 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') ]
}
16 . 1
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
16 . 2
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 = []
}
16 . 3
KNOW YOUR TASK ANNOTATIONS
@Input
@InputFile
@InputFiles
@InputDirectory
@Nested
@OutputFile
@OutputFiles
@OutputDirectory
@OutputDirectories
@Optional
17
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 = []
18
HOW NOT 2 : 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' ]
}
19 . 1
19 . 2
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 = [:]
19 . 3
20 . 1
COMPATIBILITY TESTING
How can a plugin author test a plugin against multiple Gradle
versions?
COMPATIBILITY TESTING
Gradle 2.7 added TestKit
2.9 added multi-distribution testing
Really became useful in 2.12/2.13
What to do for Gradle 2.0 - 2.8?
20 . 2
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
20 . 3
COMPATIBILITY TESTING
Create src/gradleTest/NameOfTest folder.
Add build.gradle
Add task runGradleTest
Add project structure
20 . 4
COMPATIBILITY TESTING
Add versions to main build.gradle
gradleTest {
versions '2.0', '2.2', '2.4', '2.5', '2.9'
}
Run it!
./gradlew gradleTest
20 . 5
TRICK : SAFE FILENAMES
Ability to create safe lenames 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))
}
21
22 . 1
CONVERTING EXTENSION TO NEW MODEL
Quickly convert an existing extension to be useable within
model
Easy migration path for existing users of a plugin
Little rewrite of code
CONVERTING EXTENSION TO NEW MODEL
Existing extension code
class ExternalToolExtension {
String executable = 'make'
List<String> execArgs = []
void execArgs(String... args) {
this.execArgs.addAll(args as List)
}
}
In plugin apply
project.extensions.create('externalTool',ExternalToolExtension)
22 . 2
22 . 3
LINKING EXTENSION TO NEW MODEL
Old build script style
externalTool {
executable 'gmake'
execArgs '-s','-B'
}
LINKING EXTENSION TO NEW MODEL
New model style
model {
externalTool {
executable 'gmake'
executable = 'gmake'
execArgs = ['-i']
execArgs '-s','-B'
}
}
22 . 4
22 . 5
LINKING EXTENSION TO NEW MODEL
Create model rule
class ExtensionContainerRules extends RuleSource {
@Model
ExternalToolExtension externalTool(ExtensionContainer ext) {
ext.getByType(ExternalToolExtension)
}
}
LINKING EXTENSION TO NEW MODEL
Disadvantages
Changes made in the extension automatically re ects new
model.
Order of new model evaluation and
project.afterEvaluate execution not guaranteed.
Gradle can never guarantee the con guration to be
immutable.
22 . 6
MIGRATING EXTENSION TO UNMANAGED MODEL
Eliminate some of the issues of linking.
Similar minimal code changes as for linking.
Need to take care of decoration yourself.
Remove creation of extension when plugin is applied.
23 . 1
MIGRATING EXTENSION TO UNMANAGED MODEL
Modify extension class
class ExternalToolExtension {
String executable = 'make'
List<String> execArgs = []
void execArgs(String... args) {
this.execArgs.addAll(args as List)
}
void executable(String exe) { // <-- Add this
this.executable = exe
}
}
23 . 2
23 . 3
MIGRATING EXTENSION TO UNMANAGED MODEL
Model rule remains unchanged
class ExtensionContainerRules extends RuleSource {
@Model
ExternalToolExtension externalTool(ExtensionContainer ext) {
ext.getByType(ExternalToolExtension)
}
}
23 . 4
MIGRATING EXTENSION TO UNMANAGED MODEL
Disadvantages
Gradle can never guarantee the con guration to be
immutable.
Gradle will not decorate the extension with any other
methods.
TRICK : SELF-REFERENCING PLUGIN
New 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')
24
GET THE BOOKS
https://leanpub.com/b/idiomaticgradle
25
THANK YOU
Keep your DSL extensions beautiful
Don’t spring surprising behaviour on the user
Email:
Twitter / Ello : @ysb33r
#idiomaticgradle
ysb33r@gmail.com
26
ADVANCED CONCEPTS
User override library version
Extend (decorate) existing task
Add generated JVM source sets
Operating system
27
USER OVERRIDE LIBRARY VERSION
Ship with prefered (and tested) version of dependent
library set as default
Allow user exibility to try a different version of such
library
Dynamically load library when needed
Still use power of Gradle’s dependency resolution
28 . 1
USER OVERRIDE LIBRARY VERSION
Example DSL from Asciidoctor
asciidoctorj {
version = '1.6.0-SNAPSHOT'
}
Example DSL from JRuby Base
jruby {
execVersion = '1.7.12'
}
28 . 2
28 . 3
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
}
28 . 4
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}"
}
}
}
}
28 . 5
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}")
)
}
}
}
28 . 6
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')
28 . 7
NEED 2 KNOW : 'AFTEREVALUATE'
afterEvaluate adds to a list of closures to be executed
at end of con guration phase
Execution order is FIFO
Plugin author has no control over the order
28 . 8
STYLE : PROJECT EXTENSIONS
Treat project extensions as you would for any kind of global
con guration.
With care!
Do not make the extension con guration block a task
con guration.
Task instantiation may read defaults from extension.
Do not force extension values onto tasks
28 . 9
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 les rather than force user to use
standard Copy syntax
29 . 1
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'
}
}
29 . 2
29 . 3
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)
}
}
29 . 4
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
)
}
29 . 5
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 'jrubyInitScript' , { -> this.initScript }
}
}
29 . 6
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 a
better experience.
29 . 7
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 le to JAR
Useful for separation of concerns in certain generative
programming environments
30 . 1
ADD GENERATED JVM SOURCE SETS
1. Create generator task using Copy task as transformer
2. Con gure generator task
3. Update SourceSet
4. Add dependency between generation and compilation
30 . 2
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 sourceset
that Gradle supports
30 . 3
ADD GENERATED JVM SOURCE SETS
Step 2 : Con gure 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 */ }
}
}
30 . 4
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')
}
}
30 . 5
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'
}
}
}
}
30 . 6
TRICK : OPERATING SYSTEM
Sometimes customised work has to be done on a speci c
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()}
31
GET THE BOOKS
https://leanpub.com/b/idiomaticgradle
32
THANK YOU
Keep your DSL extensions beautiful
Don’t spring surprising behaviour on the user
Email:
Twitter / Ello : @ysb33r
#idiomaticgradle
ysb33r@gmail.com

Idiomatic Gradle Plugin Writing - GradleSummit 2016

  • 1.
    #GradleSummit Gradle Summit2016 IDIOMATIC GRADLE PLUGIN WRITING Schalk W. Cronjé
  • 2.
    1 ABOUT ME Email: Twitter /Ello : @ysb33r ysb33r@gmail.com Gradle plugins authored/contributed to: VFS, Asciidoctor, JRuby family (base, jar, war etc.), GnuMake, Doxygen, Bintray
  • 3.
  • 4.
    3 ABOUT THIS PRESENTATION Writtenin Asciidoctor (1.5.3.2) Styled by asciidoctor-revealjs extension Built using: Gradle gradle-asciidoctor-plugin gradle-vfs-plugin
  • 5.
    4 GET YOUR DAILYGRADLE DOSE @DailyGradle #gradleTip
  • 6.
    5 6 THE PROBLEM There isno consistency in the way plugin authors craft extensions to the Gradle DSL today
  • 7.
    QUALITY ATTRIBUTES OFDSL Readability Consistency Flexibility Expressiveness
  • 8.
    7 PROJECT LAYOUT Figure 1.Plugin project le layout
  • 9.
    8 BUILD SCRIPT repositories { jcenter() } applyplugin : 'groovy' dependencies { compile localGroovy() compile gradleApi() testCompile ("org.spockframework:spock-core:1.0-groovy-2.3") { exclude module : 'groovy-all' } }
  • 10.
    9 . 1 9. 2 TRICK : SPOCK VERSION ext { spockGrVer = GroovySystem.version.replaceAll(/.d+$/,'') } dependencies { testCompile ("org.spockframework:spock-core:1.0-${spockGrVer}") { exclude module : 'groovy-all' } }
  • 11.
    CREATE PLUGIN CLASS packageidiomatic.gradle.authoring import org.gradle.api.Plugin import org.gradle.api.Project class MyExamplePlugin implements Plugin<Project> { void apply(Project project) { } }
  • 12.
    10 . 1 10. 2 CREATE PROPERTIES FILE META-INF/gradle- plugins/idiomatic.authored.example.properties implementation-class=idiomatic.gradle.authoring.MyExamplePlugin Name of le must match plugin identi er
  • 13.
    11 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
  • 14.
    FOR BEST COMPATIBILITY Supportsame JDK range as Gradle Gradle 1.x - mininum JDK5 Gradle 2.x - minimum JDK6 Build against Gradle 2.0 … unless proper compatibility testing is in place Suggested baseline at Gradle 2.12 (for new model) Only use later versions if speci c new functionality is required.
  • 15.
    12 . 1 JDKCOMPATIBILITY // 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 } (Fixed in 2.14)
  • 16.
    12 . 2 12. 3 GRADLE BUILD VERSION gradle/wrapper/gradle-wrapper.properties distributionUrl=https://..../distributions/gradle-2.0-all.zip
  • 17.
    STYLE : TASKS Providea 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!!
  • 18.
    13 . 1 CREATETASK CLASS package idiomatic.gradle.authoring import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction class MyExampleTasks extends DefaultTask { @TaskAction void exec() { } }
  • 19.
    13 . 2 14. 1 HONOUR OFFLINE gradle --offline The build should operate without accessing network resources.
  • 20.
    14 . 2 HONOUROFFLINE Unset the enabled property, if build is of ine task VfsCopy extends DefaultTask { VfsCopy() { enabled = !project.gradle.startParameter.isOffline() } }
  • 21.
    PREFER METHODS OVERPROPERTIES ( IOW To assign or not to assign ) Methods provide more exibility Tend to provide better readability Assignment is better suited towards One-shot attribute setting Overriding default attributes Non-lazy evaluation
  • 22.
    15 HOW NOT 2: 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') ] }
  • 23.
    16 . 1 COLLECTIONOF 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
  • 24.
    16 . 2 COLLECTIONOF 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 = [] }
  • 25.
    16 . 3 KNOWYOUR TASK ANNOTATIONS @Input @InputFile @InputFiles @InputDirectory @Nested @OutputFile @OutputFiles @OutputDirectory @OutputDirectories @Optional
  • 26.
    17 COLLECTION OF STRINGS importorg.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 = []
  • 27.
    18 HOW NOT 2: 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' ] }
  • 28.
    19 . 1 19. 2 MAPS task myTask( type: MyTask ) { myOptions prop1 : 'foo/bar.txt', prop2 : 'bar/foo.txt' myOptions prop3 : 'add/another' // Explicit reset myOptions = [:] }
  • 29.
    MAPS @Input Map getMyOptions() { this.attrs } voidsetMyOptions(Map m) { this.attrs=m } void myOptions(Map m) { this.attrs+=m } private Map attrs = [:]
  • 30.
    19 . 3 20. 1 COMPATIBILITY TESTING How can a plugin author test a plugin against multiple Gradle versions?
  • 31.
    COMPATIBILITY TESTING Gradle 2.7added TestKit 2.9 added multi-distribution testing Really became useful in 2.12/2.13 What to do for Gradle 2.0 - 2.8?
  • 32.
    20 . 2 COMPATIBILITYTESTING GradleTest plugin to the rescue buildscript { dependencies { classpath "org.ysb33r.gradle:gradletest:0.5.4" } } apply plugin : 'org.ysb33r.gradletest' http://bit.ly/1LfUUU4
  • 33.
    20 . 3 COMPATIBILITYTESTING Create src/gradleTest/NameOfTest folder. Add build.gradle Add task runGradleTest Add project structure
  • 34.
    20 . 4 COMPATIBILITYTESTING Add versions to main build.gradle gradleTest { versions '2.0', '2.2', '2.4', '2.5', '2.9' } Run it! ./gradlew gradleTest
  • 35.
    20 . 5 TRICK: SAFE FILENAMES Ability to create safe lenames 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)) }
  • 36.
    21 22 . 1 CONVERTINGEXTENSION TO NEW MODEL Quickly convert an existing extension to be useable within model Easy migration path for existing users of a plugin Little rewrite of code
  • 37.
    CONVERTING EXTENSION TONEW MODEL Existing extension code class ExternalToolExtension { String executable = 'make' List<String> execArgs = [] void execArgs(String... args) { this.execArgs.addAll(args as List) } } In plugin apply project.extensions.create('externalTool',ExternalToolExtension)
  • 38.
    22 . 2 22. 3 LINKING EXTENSION TO NEW MODEL Old build script style externalTool { executable 'gmake' execArgs '-s','-B' }
  • 39.
    LINKING EXTENSION TONEW MODEL New model style model { externalTool { executable 'gmake' executable = 'gmake' execArgs = ['-i'] execArgs '-s','-B' } }
  • 40.
    22 . 4 22. 5 LINKING EXTENSION TO NEW MODEL Create model rule class ExtensionContainerRules extends RuleSource { @Model ExternalToolExtension externalTool(ExtensionContainer ext) { ext.getByType(ExternalToolExtension) } }
  • 41.
    LINKING EXTENSION TONEW MODEL Disadvantages Changes made in the extension automatically re ects new model. Order of new model evaluation and project.afterEvaluate execution not guaranteed. Gradle can never guarantee the con guration to be immutable.
  • 42.
    22 . 6 MIGRATINGEXTENSION TO UNMANAGED MODEL Eliminate some of the issues of linking. Similar minimal code changes as for linking. Need to take care of decoration yourself. Remove creation of extension when plugin is applied.
  • 43.
    23 . 1 MIGRATINGEXTENSION TO UNMANAGED MODEL Modify extension class class ExternalToolExtension { String executable = 'make' List<String> execArgs = [] void execArgs(String... args) { this.execArgs.addAll(args as List) } void executable(String exe) { // <-- Add this this.executable = exe } }
  • 44.
    23 . 2 23. 3 MIGRATING EXTENSION TO UNMANAGED MODEL Model rule remains unchanged class ExtensionContainerRules extends RuleSource { @Model ExternalToolExtension externalTool(ExtensionContainer ext) { ext.getByType(ExternalToolExtension) } }
  • 45.
    23 . 4 MIGRATINGEXTENSION TO UNMANAGED MODEL Disadvantages Gradle can never guarantee the con guration to be immutable. Gradle will not decorate the extension with any other methods.
  • 46.
    TRICK : SELF-REFERENCINGPLUGIN New 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')
  • 47.
  • 48.
    25 THANK YOU Keep yourDSL extensions beautiful Don’t spring surprising behaviour on the user Email: Twitter / Ello : @ysb33r #idiomaticgradle ysb33r@gmail.com
  • 49.
    26 ADVANCED CONCEPTS User overridelibrary version Extend (decorate) existing task Add generated JVM source sets Operating system
  • 50.
    27 USER OVERRIDE LIBRARYVERSION Ship with prefered (and tested) version of dependent library set as default Allow user exibility to try a different version of such library Dynamically load library when needed Still use power of Gradle’s dependency resolution
  • 51.
    28 . 1 USEROVERRIDE LIBRARY VERSION Example DSL from Asciidoctor asciidoctorj { version = '1.6.0-SNAPSHOT' } Example DSL from JRuby Base jruby { execVersion = '1.7.12' }
  • 52.
    28 . 2 28. 3 USER OVERRIDE LIBRARY VERSION 1. Create Extension 2. Add extension object in plugin apply 3. Create custom classloader
  • 53.
    USER OVERRIDE LIBRARYVERSION 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 }
  • 54.
    28 . 4 USEROVERRIDE 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}" } } } }
  • 55.
    28 . 5 USEROVERRIDE 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}") ) } } }
  • 56.
    28 . 6 USEROVERRIDE 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')
  • 57.
    28 . 7 NEED2 KNOW : 'AFTEREVALUATE' afterEvaluate adds to a list of closures to be executed at end of con guration phase Execution order is FIFO Plugin author has no control over the order
  • 58.
    28 . 8 STYLE: PROJECT EXTENSIONS Treat project extensions as you would for any kind of global con guration. With care! Do not make the extension con guration block a task con guration. Task instantiation may read defaults from extension. Do not force extension values onto tasks
  • 59.
    28 . 9 EXTENDEXISTING 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 les rather than force user to use standard Copy syntax
  • 60.
    29 . 1 EXTENDEXISTING 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' } }
  • 61.
    29 . 2 29. 3 EXTEND EXISTING TASK 1. Create extension class 2. Add extension to task 3. Link extension attributes to task attributes (for caching)
  • 62.
    EXTEND EXISTING TASK Createextension class class MyExtension { String initScript MyExtension( Task t ) { // TODO: Add Gradle caching support // (See later slide) } }
  • 63.
    29 . 4 EXTENDEXISTING 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 ) }
  • 64.
    29 . 5 EXTENDEXISTING TASK Add 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 } } }
  • 65.
    29 . 6 NEED2 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 a better experience.
  • 66.
    29 . 7 ADDGENERATED 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 le to JAR Useful for separation of concerns in certain generative programming environments
  • 67.
    30 . 1 ADDGENERATED JVM SOURCE SETS 1. Create generator task using Copy task as transformer 2. Con gure generator task 3. Update SourceSet 4. Add dependency between generation and compilation
  • 68.
    30 . 2 ADDGENERATED 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 sourceset that Gradle supports
  • 69.
    30 . 3 ADDGENERATED JVM SOURCE SETS Step 2 : Con gure 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 */ } } }
  • 70.
    30 . 4 ADDGENERATED 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') } }
  • 71.
    30 . 5 ADDGENERATED 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' } } } }
  • 72.
    30 . 6 TRICK: OPERATING SYSTEM Sometimes customised work has to be done on a speci c 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()}
  • 73.
  • 74.
    32 THANK YOU Keep yourDSL extensions beautiful Don’t spring surprising behaviour on the user Email: Twitter / Ello : @ysb33r #idiomaticgradle ysb33r@gmail.com