basic gradle plugin writing
Post on 08-Jan-2017
831 Views
Preview:
TRANSCRIPT
Gr8Day Warsaw 2016
IDIOMATIC GRADLE
PLUGIN WRITINGSchalk 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, Bintray
ABOUT THIS PRESENTATIONWritten in Asciidoctor (1.5.3.2)
Styled by asciidoctor-revealjs extension
Built using:
Gradle
gradle-asciidoctor-plugin
gradle-vfs-plugin
GET YOUR DAILY GRADLE DOSE
@DailyGradle
#gradleTip
THE PROBLEMThere is no consistency in the way plugin authors craft extensions
to the Gradle DSL today
QUALITY ATTRIBUTES OF DSL
Readability
Consistency
Flexibility
Expressiveness
PROJECT LAYOUT
Figure 1. Plugin project file layout
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' } }
TRICK : SPOCK VERSIONext { spockGrVer = GroovySystem.version.replaceAll(/\.\d+$/,'') }
dependencies { testCompile ("org.spockframework:spock-core:1.0-${spockGrVer}") { exclude module : 'groovy-all' } }
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) { } }
CREATE PROPERTIES FILE
META-INF/gradle-plugins/idiomatic.authored.example.properties
implementation-class=idiomatic.gradle.authoring.MyExamplePlugin
Name of file must match plugin identifier
CREATE TASK CLASSpackage idiomatic.gradle.authoring
import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction
class MyExampleTasks extends DefaultTask {
@TaskAction void exec() { } }
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.10 (for new model)
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 }
GRADLE BUILD VERSION// gradle/wrapper/gradle-wrapper.properties distributionUrl=https\://..../distributions/gradle-2.0-all.zip
HONOUR OFFLINEgradle --offline
The build should operate without accessingnetwork resources.
HONOUR OFFLINEUnset the enabled property, if build is offline
task VfsCopy extends DefaultTask { VfsCopy() {
enabled = !project.gradle.startParameter.isOffline()
}
}
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
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') ]
}
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
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 : 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!!
KNOW YOUR ANNOTATIONS
@Input
@InputFile
@InputFiles
@InputDirectory
@OutputFile
@OutputFiles
@OutputDirectory
@OutputDirectories
@Optional
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 = []
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' ]
}
MAPStask 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 = [:]
COMPATIBILITY TESTING
How can a plugin author test a plugin against multiple Gradle
versions?
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?
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
COMPATIBILITY TESTING
Create src/gradleTest/NameOfTest folder.
Add build.gradle
Add task runGradleTest
Add project structure
COMPATIBILITY TESTING
Add versions to main build.gradle
gradleTest { versions '2.0', '2.2', '2.4', '2.5', '2.9' }
Run it!
./gradlew gradleTest
THANK YOU
Keep your DSLextensions beautiful
Don’t spring surprisingbehaviour on the user
Email:
Twitter / Ello : @ysb33r
#idiomaticgradle
ysb33r@gmail.com
http://bit.ly/1iJmdiP
ADVANCED CONCEPTS
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' }
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
USER OVERRIDE LIBRARY VERSION
Example DSL from Asciidoctor
asciidoctorj { version = '1.6.0-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( 'int_asciidoctorj' )
// Add dependency at the end of configuration phase project.afterEvaluate { project.dependencies { int_asciidoctorj "org.asciidoctor:asciidoctorj" + "${project.asciidoctorj.version}" } } } }
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}") ) } } }
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')
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
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
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
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
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' } }
EXTEND EXISTING TASK1. Create extension class
2. Add extension to task
3. Link extension attributes to task attributes (for caching)
EXTEND EXISTING TASKCreate extension class
class MyExtension { String initScript
MyExtension( Task t ) {
// TODO: Add Gradle caching support // (See later slide) }
}
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 ) }
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 } } }
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.
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
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
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
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 */ } } }
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') }
}
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'
}
}
}
}
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)) }
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')
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()}
THANK YOU
Keep your DSLextensions beautiful
Don’t spring surprisingbehaviour on the user
Email:
Twitter / Ello : @ysb33r
#idiomaticgradle
ysb33r@gmail.com
http://bit.ly/1iJmdiP
top related