idiomatic gradle plugin writing - gradlesummit 2016

74
#GradleSummit Gradle Summit 2016 IDIOMATIC GRADLE PLUGIN WRITING Schalk W. Cronjé

Upload: schalk-cronje

Post on 08-Jan-2017

519 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Idiomatic Gradle Plugin Writing - GradleSummit 2016

#GradleSummit Gradle Summit 2016

IDIOMATIC GRADLE

PLUGIN WRITINGSchalk W. Cronjé

Page 2: Idiomatic Gradle Plugin Writing - GradleSummit 2016

1

ABOUT ME

Email:

Twitter / Ello : @ysb33r

[email protected]

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

Page 3: Idiomatic Gradle Plugin Writing - GradleSummit 2016

2

 

Page 4: Idiomatic Gradle Plugin Writing - GradleSummit 2016

3

ABOUT THIS PRESENTATIONWritten in Asciidoctor (1.5.3.2)

Styled by asciidoctor-revealjs extension

Built using:

Gradle

gradle-asciidoctor-plugin

gradle-vfs-plugin

Page 5: Idiomatic Gradle Plugin Writing - GradleSummit 2016

4

GET YOUR DAILY GRADLE DOSE

@DailyGradle

#gradleTip

Page 6: Idiomatic Gradle Plugin Writing - GradleSummit 2016

5

6

THE PROBLEMThere is no consistency in the way plugin authors craft extensions

to the Gradle DSL today

Page 7: Idiomatic Gradle Plugin Writing - GradleSummit 2016

QUALITY ATTRIBUTES OF DSL

Readability

Consistency

Flexibility

Expressiveness

Page 8: Idiomatic Gradle Plugin Writing - GradleSummit 2016

7

PROJECT LAYOUT

Figure 1. Plugin project �le layout

Page 9: Idiomatic Gradle Plugin Writing - GradleSummit 2016

8

BUILD SCRIPTrepositories { jcenter() }

apply plugin : 'groovy'

dependencies { compile localGroovy() compile gradleApi() testCompile ("org.spockframework:spock-core:1.0-groovy-2.3") { exclude module : 'groovy-all' } }

Page 10: Idiomatic Gradle Plugin Writing - GradleSummit 2016

9 . 1

9 . 2

TRICK : SPOCK VERSIONext { spockGrVer = GroovySystem.version.replaceAll(/\.\d+$/,'') }

dependencies { testCompile ("org.spockframework:spock-core:1.0-${spockGrVer}") { exclude module : 'groovy-all' } }

Page 11: Idiomatic Gradle Plugin Writing - GradleSummit 2016

CREATE PLUGIN CLASSpackage idiomatic.gradle.authoring

import org.gradle.api.Plugin import org.gradle.api.Project

class MyExamplePlugin implements Plugin<Project> {

void apply(Project project) { } }

Page 12: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 13: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 14: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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.

Page 15: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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)

Page 16: Idiomatic Gradle Plugin Writing - GradleSummit 2016

12 . 2

12 . 3

GRADLE BUILD VERSION

gradle/wrapper/gradle-wrapper.properties

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

Page 17: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 18: Idiomatic Gradle Plugin Writing - GradleSummit 2016

13 . 1

CREATE TASK CLASSpackage idiomatic.gradle.authoring

import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction

class MyExampleTasks extends DefaultTask {

@TaskAction void exec() { } }

Page 19: Idiomatic Gradle Plugin Writing - GradleSummit 2016

13 . 2

14 . 1

HONOUR OFFLINEgradle --offline

The build should operate without accessingnetwork resources.

Page 20: Idiomatic Gradle Plugin Writing - GradleSummit 2016

14 . 2

HONOUR OFFLINEUnset the enabled property, if build is of�ine

task VfsCopy extends DefaultTask { VfsCopy() {

enabled = !project.gradle.startParameter.isOffline()

}

}

Page 21: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 22: Idiomatic Gradle Plugin Writing - GradleSummit 2016

15

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 23: Idiomatic Gradle Plugin Writing - GradleSummit 2016

16 . 1

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 24: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 = [] }

Page 25: Idiomatic Gradle Plugin Writing - GradleSummit 2016

16 . 3

KNOW YOUR TASK ANNOTATIONS

@Input

@InputFile

@InputFiles

@InputDirectory

@Nested

@OutputFile

@OutputFiles

@OutputDirectory

@OutputDirectories

@Optional

Page 26: Idiomatic Gradle Plugin Writing - GradleSummit 2016

17

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 27: Idiomatic Gradle Plugin Writing - GradleSummit 2016

18

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 28: Idiomatic Gradle Plugin Writing - GradleSummit 2016

19 . 1

19 . 2

MAPStask myTask( type: MyTask ) {

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

myOptions prop3 : 'add/another'

// Explicit reset myOptions = [:]

}

Page 29: Idiomatic Gradle Plugin Writing - GradleSummit 2016

MAPS@Input

Map getMyOptions() {

this.attrs

}

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

}

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

}

private Map attrs = [:]

Page 30: Idiomatic Gradle Plugin Writing - GradleSummit 2016

19 . 3

20 . 1

COMPATIBILITY TESTING

How can a plugin author test a plugin against multiple Gradle

versions?

Page 31: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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?

Page 32: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 33: Idiomatic Gradle Plugin Writing - GradleSummit 2016

20 . 3

COMPATIBILITY TESTING

Create src/gradleTest/NameOfTest folder.

Add build.gradle

Add task runGradleTest

Add project structure

Page 34: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 35: Idiomatic Gradle Plugin Writing - GradleSummit 2016

20 . 5

TRICK : SAFE FILENAMESAbility to create safe �lenames 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 36: Idiomatic Gradle Plugin Writing - GradleSummit 2016

21

22 . 1

CONVERTING EXTENSION TO NEW MODEL

Quickly convert an existing extension to be useable withinmodel

Easy migration path for existing users of a plugin

Little rewrite of code

Page 37: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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)

Page 38: Idiomatic Gradle Plugin Writing - GradleSummit 2016

22 . 2

22 . 3

LINKING EXTENSION TO NEW MODEL

Old build script style

externalTool { executable 'gmake' execArgs '-s','-B' }

Page 39: Idiomatic Gradle Plugin Writing - GradleSummit 2016

LINKING EXTENSION TO NEW MODEL

New model style

model { externalTool { executable 'gmake' executable = 'gmake' execArgs = ['-i'] execArgs '-s','-B' } }

Page 40: Idiomatic Gradle Plugin Writing - GradleSummit 2016

22 . 4

22 . 5

LINKING EXTENSION TO NEW MODEL

Create model rule

class ExtensionContainerRules extends RuleSource { @Model ExternalToolExtension externalTool(ExtensionContainer ext) { ext.getByType(ExternalToolExtension) } }

Page 41: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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.

Page 42: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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.

Page 43: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 } }

Page 44: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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) } }

Page 45: Idiomatic Gradle Plugin Writing - GradleSummit 2016

23 . 4

MIGRATING EXTENSION TO UNMANAGED MODEL

Disadvantages

Gradle can never guarantee the con�guration to beimmutable.

Gradle will not decorate the extension with any othermethods.

Page 46: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 47: Idiomatic Gradle Plugin Writing - GradleSummit 2016

24

GET THE BOOKS

https://leanpub.com/b/idiomaticgradle

Page 48: Idiomatic Gradle Plugin Writing - GradleSummit 2016

25

THANK YOUKeep your DSL extensions beautiful

Don’t spring surprising behaviour on the user

Email:

Twitter / Ello : @ysb33r

#idiomaticgradle

[email protected]

Page 49: Idiomatic Gradle Plugin Writing - GradleSummit 2016

26

ADVANCED CONCEPTS

User override library version

Extend (decorate) existing task

Add generated JVM source sets

Operating system

Page 50: Idiomatic Gradle Plugin Writing - GradleSummit 2016

27

USER OVERRIDE LIBRARY VERSION

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

Allow user �exibility to try a different version of suchlibrary

Dynamically load library when needed

Still use power of Gradle’s dependency resolution

Page 51: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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'}

Page 52: Idiomatic Gradle Plugin Writing - GradleSummit 2016

28 . 2

28 . 3

USER OVERRIDE LIBRARY VERSION

1. Create Extension

2. Add extension object in plugin apply

3. Create custom classloader

Page 53: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 54: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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}" } } } }

Page 55: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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}") ) } } }

Page 56: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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')

Page 57: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 58: Idiomatic Gradle Plugin Writing - GradleSummit 2016

28 . 8

STYLE : PROJECT EXTENSIONS

Treat project extensions as you would for any kind of globalcon�guration.

With care!

Do not make the extension con�guration block a taskcon�guration.

Task instantiation may read defaults from extension.

Do not force extension values onto tasks

Page 59: Idiomatic Gradle Plugin Writing - GradleSummit 2016

28 . 9

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 �les rather than force user to usestandard Copy syntax

Page 60: Idiomatic Gradle Plugin Writing - GradleSummit 2016

29 . 1

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 61: Idiomatic Gradle Plugin Writing - GradleSummit 2016

29 . 2

29 . 3

EXTEND EXISTING TASK1. Create extension class

2. Add extension to task

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

Page 62: Idiomatic Gradle Plugin Writing - GradleSummit 2016

EXTEND EXISTING TASKCreate extension class

class MyExtension { String initScript

MyExtension( Task t ) {

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

}

Page 63: Idiomatic Gradle Plugin Writing - GradleSummit 2016

29 . 4

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 64: Idiomatic Gradle Plugin Writing - GradleSummit 2016

29 . 5

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 65: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 abetter experience.

Page 66: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 67: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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

Page 68: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 sourcesetthat Gradle supports

Page 69: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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 */ } } }

Page 70: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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') }

}

Page 71: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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'

}

}

}

}

Page 72: Idiomatic Gradle Plugin Writing - GradleSummit 2016

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()}

Page 73: Idiomatic Gradle Plugin Writing - GradleSummit 2016

31

GET THE BOOKS

https://leanpub.com/b/idiomaticgradle

Page 74: Idiomatic Gradle Plugin Writing - GradleSummit 2016

32

THANK YOUKeep your DSL extensions beautiful

Don’t spring surprising behaviour on the user

Email:

Twitter / Ello : @ysb33r

#idiomaticgradle

[email protected]