Slide 1

Slide 1 text

Gradle plugin best practices by example Benjamin Muschko, Gradle Inc.

Slide 2

Slide 2 text

The dark past of build logic Unstructured  spaghetti  code Copy  &  paste  of  code  snippets The  build  tool  tells  you  how  to  structure  code Build  can  only  be  understood  by  build  guru™ Testing  through  manual  execution

Slide 3

Slide 3 text

Build logic “-ilities” Typical  non-­‐functional  software  requirements   also  apply  to  build  code…   • Reusability   • Testability   • Maintainability   • Extensibility   • Configurability

Slide 4

Slide 4 text

Reusability Avoid  copy/paste!  Allow  code  to  be  shared   among  independent  projects.

Slide 5

Slide 5 text

Testability Build  code  is  no  different  from  application   code.  Test  it  on  multiple  levels!

Slide 6

Slide 6 text

Maintainability Avoid  the  big  ball  of  mud!  Cohesion  and   separation  of  concerns  are  important.

Slide 7

Slide 7 text

Extensibility Extend  Gradle's  build  language  by  your  own   declarative  &  expressive  language  constructs.

Slide 8

Slide 8 text

Configurability Don't  box  in  your  users!  Implement   convention  over  configuration  with  ease.

Slide 9

Slide 9 text

The end goal Maintainable,  reusable  &  tested  code Consumers  only  configure  the  code Complex  implement.  details  are  hidden

Slide 10

Slide 10 text

How to get there? Concepts  that  help  implement  these   requirements…   • Good  software  engineering  practices   • Custom  tasks   • Plugins   • Gradle's  extension  mechanisms   • Testing  capabilities

Slide 11

Slide 11 text

Techniques covered in talk • Declarative  vs.  imperative  logic   • Convention  over  configuration   • Capabilities  vs.  conventions   • Gradle's  extension  mechanisms   • Testing  plugin  logic   • Publishing  the  plugin  artifacts   • Writing  and  generating  documentation   • Setting  up  Continuous  Integration/Delivery

Slide 12

Slide 12 text

Case study: Gradle Docker plugin                      Serves  as  showcase  plugin  project.   • Plugin  for  managing  Docker  images  and   containers  through  Docker  remote  API   • API  communication  via  Docker  Java  library   • Written  in  Groovy   • Source  code  available  on  GitHub  (https:/ / github.com/bmuschko/gradle-­‐docker-­‐plugin)

Slide 13

Slide 13 text

Always check in the Wrapper Project  becomes  instantly  buildable  for  every   developer  and  on  the  CI  machine. Check  in  Wrapper  files   into  VCS  repository.

Slide 14

Slide 14 text

Hiding complex, imperative logic Concept  applies  to  tasks  and  plugins.   • Reusable  and  configurable   • Easy  to  structure,  refactor  and  test   • Avoid  global  properties  and  methods

Slide 15

Slide 15 text

Task type > ad-hoc task Prefer  implementing  task  types  to   implementing  ad-­‐hoc  tasks. 
 task createDockerfile(type: Dockerfile) {
 destFile = file("$buildDir/mydockerfile/Dockerfile")
 from 'ubuntu:12.04'
 maintainer 'Benjamin Muschko "[email protected]"'
 }
 
 task buildImage(type: DockerBuildImage) {
 dependsOn createDockerfile
 inputDir = createDockerfile.destFile.parentFile
 tag = 'bmuschko/myimage'
 }

Slide 16

Slide 16 text

Binary plugin > script plugin Use  script  plugins  to  organize  build  logic   based  on  functional  boundaries  in  project. 
 apply from: "$rootDir/gradle/dependencies.gradle"
 apply from: "$rootDir/gradle/test-setup.gradle"
 apply from: "$rootDir/gradle/integration-test.gradle"
 apply from: "$rootDir/gradle/functional-test.gradle"
 apply from: "$rootDir/gradle/additional-artifacts.gradle"
 apply from: "$rootDir/gradle/publishing.gradle"
 apply from: "$rootDir/gradle/documentation.gradle"
 apply from: "$rootDir/gradle/release.gradle"

Slide 17

Slide 17 text

Convention over configuration Provide  sensible  defaults  and  standards.  Expose   a  way  to  re-­‐configure  them  to  user’s  needs.   Examples:   • src/main/java  for  Java-­‐based  applications   • Project  name  derived  from  directory  name   • Output  directory  is  build

Slide 18

Slide 18 text

Convention Plugin  consumers  will  be  most  comfortable   with  using  convention  plugins.  Pick  sensible   defaults  for  the  user.   Good  indicator:   The  less  a  consumer  has  to  re-­‐configure   defaults  the  better.

Slide 19

Slide 19 text

Configuration Real-­‐world  project  might  need  to  re-­‐ configure  the  defaults.  Make  it  convenient  to   change  defaults.   Reason:   Their  view  of  the  world  might  look  different   than  yours.

Slide 20

Slide 20 text

Users only declare the “what” Expose  a  custom  DSL  from  your  binary  plugin  to   configure  runtime  behavior.   The  “how”  is  the  responsibility  of  the  plugin  impl. 
 apply plugin: 'com.bmuschko.docker-remote-api'
 
 docker {
 url = 'https://192.168.59.103:2376'
 certPath = new File(System.properties['user.home'], '.boot2docker/certs/boot2docker-vm')
 }

Slide 21

Slide 21 text

Example: Providing default dependencies Plugins  often  rely  on  external  libraries.   Automatically  resolve  default  version,  but   make  it  configurable.

Slide 22

Slide 22 text

Customizing Docker Java library Introduce  custom  configura]on  in  plugin. 
 project.configurations.create('docker')
 .setVisible(false)
 .setTransitive(true)
 .setDescription('The Docker Java libraries to be used.') Setting  dependency  from  consuming  build. 
 dependencies {
 docker 'com.github.docker-java:docker-java:2.0.0'
 docker 'org.slf4j:slf4j-simple:1.7.5'
 }

Slide 23

Slide 23 text

Providing default (but customizable) dependency Only  use  defaults,  if  no  dependencies  are   assigned  to  custom  configuration. 
 Configuration config = project.configurations.create('docker')
 
 project.tasks.withType(AbstractDockerRemoteApiTask) {
 config.defaultDependencies { deps ->
 deps.add(project.dependencies .create('com.github.docker-java:docker-java:2.1.0'))
 deps.add(project.dependencies .create('org.slf4j:slf4j-simple:1.7.5'))
 }
 
 conventionMapping.with {
 classpath = { config } 
 }
 } Gradle  2.5

Slide 24

Slide 24 text

Capabilities vs. conventions Separating  general-­‐purpose  functionality   from  pre-­‐configured,  opinionated   functionality.   Finding  the  right  balance  between  both   aspects  is  key.

Slide 25

Slide 25 text

Capabilities • Un-­‐opinionated  functionality   • Provide  general-­‐purpose  concepts   Examples:   • Custom  task  types  without  creating  an  actual   task  instance   • Add  new  concepts  without  configuring  them   (e.g.  source  sets,  build  types  and  flavors)

Slide 26

Slide 26 text

Conventions • Opinionated  functionality   • Instantiate  and/or  pre-­‐configure  concepts   Examples:   • Standardized  directory  layouts   • Enhanced  tasks  created  by  a  plugin   • Adding  default  values  to  extensions   • Declaring  task  dependencies  to  form  a  lifecycle

Slide 27

Slide 27 text

Plugin composition Plugins  can  build  upon  other  plugins.  This  is  a   common  pattern.   A  base  plugin  provides     generic  capabilities   Another  plugin  builds  on  the  base,     adding  opinionated  conventions

Slide 28

Slide 28 text

Example: Docker plugins Allows  users  to  pick  functionality  they  need   in  their  projects.   Plugin  that  adds  task  types  for     interacting  with  Docker  remote  API   Plugin  for  creating  &  pushing     Docker  image  for  Java  applications

Slide 29

Slide 29 text

Example: Docker Java application plugin Applying  plugin  by  identifier  or  type.   
 import org.gradle.api.Plugin
 import org.gradle.api.Project
 
 class DockerJavaApplicationPlugin implements Plugin {
 @Override
 void apply(Project project) {
 project.apply(plugin: DockerRemoteApiPlugin)
 }
 }

Slide 30

Slide 30 text

Build for further extension • Anticipate  more  specific  extensions  to  your   plugin   • Implemented  through  plugin  composition   • Base  plugin  act  as  enablers  for  custom   convention  plugins   • Enterprise  build  convention  plugins  re-­‐ configure  your  plugins

Slide 31

Slide 31 text

Put yourself in the shoes of consumer Do  not  automatically  apply  plugins  in  your   plugin!   Reason:     Makes  your  plugin  highly  opinionated.

Slide 32

Slide 32 text

Reacting to plugins React  to  plugins  that  might  be  applied  in   consuming  build  script.  Only  then  logic  is   executed.   The  same  concept  applies  to  tasks.

Slide 33

Slide 33 text

Using configuration rules Reacting  to  plugins 
 project.plugins.withType(IdeaPlugin) {
 // configure Idea plugin in the context of your plugin
 }
 Reacting  to  task  types 
 project.tasks.withType(War) {
 // configure all War tasks in the context of your plugin
 }

Slide 34

Slide 34 text

Testing build logic Build  logic  needs  to  be  testable  on  multiple   levels.

Slide 35

Slide 35 text

Implementing test types with Gradle Unit  testing:     No  Gradle  tooling  needed   Integration  testing:     Use  ProjectBuilder  to  create  pseudo  Project   instance   Functional  testing:   Nebula  Test,  Gradle  TestKit

Slide 36

Slide 36 text

Example: Writing unit test with Spock 
 package com.bmuschko.gradle.docker.tasks.image
 
 import spock.lang.Specification
 
 import static com.bmuschko.gradle.docker.tasks.image.Dockerfile.*
 
 class DockerfileTest extends Specification {
 def "Instruction String representation is built correctly"() {
 expect:
 instructionInstance.keyword == keyword
 instructionInstance.build() == builtInstruction 
 where:
 instructionInstance | keyword | builtInstruction
 new FromInstruction('ubuntu:14.04') | 'FROM' | 'FROM ubuntu:14.04'
 new FromInstruction({ 'ubuntu:14.04' }) | 'FROM' | 'FROM ubuntu:14.04'
 ...
 }
 }

Slide 37

Slide 37 text

Separating test type source code    Default  unit  test  directory Custom  integration  test  directory Custom  functional  test  directory

Slide 38

Slide 38 text

Example: Integration test source set and task 
 sourceSets {
 integrationTest {
 groovy.srcDir file('src/integTest/groovy')
 resources.srcDir file('src/integTest/resources')
 compileClasspath += sourceSets.main.output + configurations.testRuntime
 runtimeClasspath += output + compileClasspath
 }
 }
 
 task integrationTest(type: Test) {
 description = 'Runs the integration tests.'
 group = 'verification'
 testClassesDir = sourceSets.integrationTest.output.classesDir
 classpath = sourceSets.integrationTest.runtimeClasspath
 mustRunAfter test
 }
 
 check.dependsOn integrationTest

Slide 39

Slide 39 text

Example: Writing integration test with Spock 
 import org.gradle.api.Project
 import org.gradle.testfixtures.ProjectBuilder class DockerJavaApplicationPluginIntegrationTest extends Specification {
 @Rule TemporaryFolder temporaryFolder = new TemporaryFolder()
 Project project
 
 def setup() {
 project = ProjectBuilder.builder().withProjectDir(temporaryFolder.root).build()
 }
 
 def "Creates tasks out-of-the-box when application plugin is applied"() {
 when:
 project.apply(plugin: DockerJavaApplicationPlugin)
 project.apply(plugin: 'application')
 
 then: project.tasks.findByName(DockerJavaApplicationPlugin .COPY_DIST_RESOURCES_TASK_NAME)
 project.tasks.findByName(DockerJavaApplicationPlugin.DOCKERFILE_TASK_NAME)
 project.tasks.findByName(DockerJavaApplicationPlugin.BUILD_IMAGE_TASK_NAME)
 project.tasks.findByName(DockerJavaApplicationPlugin.PUSH_IMAGE_TASK_NAME)
 }
 }

Slide 40

Slide 40 text

Functional testing Testing  the  build  logic  from  the  end  user’s   perspective.   • Exercising  build  logic  as  part  of  a   programmatically  executed  build.   • Make  assertions  about  build  outcome.  

Slide 41

Slide 41 text

Using the Gradle TestKit Functional  testing  support  in  Gradle  core.   • Uses  Tooling  API  as  test  execution  harness.   • Agnostic  of  test  framework.   • Assertions  made  based  on  build  output,   build  logging  or  test  of  tasks  +  their  result.   Gradle  2.6

Slide 42

Slide 42 text

TestKit usage Declaring  TestKit  dependency 
 dependencies {
 testCompile gradleTestKit()
 }
 Declaring  the  Spock  dependency 
 dependencies {
 testCompile 'org.spockframework:spock-core:1.0-groovy-2.3'
 }

Slide 43

Slide 43 text

Example: Writing functional tests with TestKit 
 def "can successfully create Dockerfile"() {
 given:
 buildFile << """
 import com.bmuschko.gradle.docker.tasks.image.Dockerfile
 
 task dockerfile(type: Dockerfile) {
 from 'ubuntu:14.04'
 maintainer 'Benjamin Muschko "[email protected]"'
 } """
 
 when:
 def result = GradleRunner.create()
 .withProjectDir(testProjectDir.root)
 .withArguments('dockerfile')
 .build()
 
 then:
 result.task(":dockerfile").outcome == SUCCESS testProjectDir.file('Dockerfile').exists()
 }

Slide 44

Slide 44 text

DEMO Functional  testing  with  TestKit

Slide 45

Slide 45 text

Roadmap for TestKit Gradle  2.6   Mechanics  for  executing  tests   Functional  tests  can  query  build  result   Gradle  2.7   Isolation  of  test  environment,  dedicated  daemons   Gradle  2.8   Convenient  injection  of  code  under  test

Slide 46

Slide 46 text

Cross-version compatibility tests Forward  and  backward  compatibility   independent  of  Gradle  version  used  to  build   plugin  artifact. 2.4 2.5 2.6 2.3 2.2  Version  used   to  build  plugin

Slide 47

Slide 47 text

Implementing compatibility tests No  built-­‐in  support  in  Gradle  yet.  On  the   roadmap  for  TestKit.   Intermediate  options:   • Custom  implementation  using  Tooling  API.   • Community  plugins  like  https:/ /github.com/ ysb33r/gradleTest

Slide 48

Slide 48 text

Importance of documentation Plugins  are  not  self-­‐documenting.   Thorough  documentation  is  crucial  for  plugin   consumers.  

Slide 49

Slide 49 text

Answer questions for consumers How  can  I  use  the  plugin  and  configure  it?   What  tasks  &  extensions  are  provided?   What  impact  does  the  plugin  have  on  my   project?

Slide 50

Slide 50 text

Impact on consumers De-­‐mystify  added  functionality.   Give  consumers  the  feeling  that  they  are   under  control.

Slide 51

Slide 51 text

What should be documented? • Purpose  of  plugin,  the  repository  that  hosts  the  plugin   • Plugins:  Identifier,  type  and  description  bundled  in  a  JAR   • Enhanced  tasks:  their  name,  type  and  dependencies   • Custom  task  types:  Javadocs,  Groovydocs   • Conventions,  custom  extensions  (e.g.  DSLs)   • Extension  properties/methods   • Usage  examples  in  textual  form   • A  good  set  of  functional  tests  that  demonstrate  the  use   of  the  plugin

Slide 52

Slide 52 text

Example: Plugin description & usage

Slide 53

Slide 53 text

Example: Extension usage and properties

Slide 54

Slide 54 text

Example: Textual usage & functional tests

Slide 55

Slide 55 text

Example: Linking custom tasks to API docs

Slide 56

Slide 56 text

Example: Publishing API docs to GitHub pages Javadocs/Groovydocs  are  essential  to  allow  users  discover   API  classes  exposed  by  plugin.   • Create  new  branch  gh-pages   • Remove  all  files  from  working  tree  and  index   • Push  new  branch   Available  under   http(s)://.github.io/

Slide 57

Slide 57 text

Example: Publishing Javadocs to GitHub pages Use  gradle-­‐git  plugin  to  push  to  automatically   publish  Javadocs.   
 apply plugin: 'org.ajoberstar.github-pages'
 
 githubPages {
 repoUri = '[email protected]:bmuschko/gradle-docker-plugin.git'
 
 pages {
 from(javadoc.outputs.files) {
 into 'docs/javadoc'
 }
 }
 }

Slide 58

Slide 58 text

DEMO Publishing  changed  API  documentation

Slide 59

Slide 59 text

Publishing the plugin binaries Make  artifact(s)  available  to  consumers  by   uploading  them  to  one  or  many  binary   repositories.

Slide 60

Slide 60 text

Provide appropriate metadata 
 apply plugin: 'maven-publish'
 
 publishing {
 publications {
 mavenJava(MavenPublication) {
 pom.withXml {
 def root = asNode()
 root.appendNode('name', 'Gradle Docker plugin')
 root.appendNode('description', 'Gradle plugin for managing Docker images and containers.')
 root.appendNode('url', 'https://github.com/bmuschko/gradle-docker-plugin')
 root.appendNode('inceptionYear', '2014')
 
 def scm = root.appendNode('scm')
 scm.appendNode('url', 'https://github.com/bmuschko/gradle-docker-plugin')
 scm.appendNode('connection', 'scm:https://[email protected]/bmuschko/gradle-docker-plugin.git')
 scm.appendNode('developerConnection', 'scm:git://github.com/bmuschko/gradle-docker-plugin.git')
 
 def license = root.appendNode('licenses').appendNode('license')
 license.appendNode('name', 'The Apache Software License, Version 2.0')
 license.appendNode('url', 'http://www.apache.org/licenses/LICENSE-2.0.txt')
 license.appendNode('distribution', 'repo')
 
 def developers = root.appendNode('developers')
 def developer = developers.appendNode('developer')
 developer.appendNode('id', 'bmuschko')
 developer.appendNode('name', 'Benjamin Muschko')
 developer.appendNode('email', '[email protected]')
 }
 }
 }
 }

Slide 61

Slide 61 text

Publishing doesn’t stop with plugin JAR 
 task sourcesJar(type: Jar) {
 classifier 'sources'
 from sourceSets.main.allSource
 }
 
 task groovydocJar(type: Jar, dependsOn: groovydoc) {
 classifier 'groovydoc'
 from groovydoc.destinationDir
 }
 
 task javadocJar(type: Jar, dependsOn: javadoc) {
 classifier 'javadoc'
 from javadoc.destinationDir
 }
 
 artifacts {
 archives sourcesJar
 archives groovydocJar
 archives javadocJar
 }

Slide 62

Slide 62 text

Share Open Source plugins on the portal https:/ /plugins.gradle.org/

Slide 63

Slide 63 text

Publishing to the Gradle plugin portal Requires  use  of  the  “Plugin  Publishing  Plugin” 
 buildscript {
 repositories {
 maven {
 url 'https://plugins.gradle.org/m2/'
 }
 } 
 dependencies {
 classpath 'com.gradle.publish:plugin-publish-plugin:0.9.1'
 }
 }
 
 apply plugin: 'com.gradle.plugin-publish'

Slide 64

Slide 64 text

Declaring required portal plugin metadata 
 pluginBundle {
 website = 'https://github.com/bmuschko/gradle-docker-plugin'
 vcsUrl = 'https://github.com/bmuschko/gradle-docker-plugin.git'
 description = 'Gradle plugin for managing Docker images and containers.'
 tags = ['gradle', 'docker', 'container', 'image', 'lightweight', 'vm', 'linux']
 
 plugins {
 dockerRemoteApi {
 id = 'com.bmuschko.docker-remote-api'
 displayName = 'Provides custom tasks for interacting with Docker via its remote API.'
 }
 
 dockerJavaApplication {
 id = 'com.bmuschko.docker-java-application'
 displayName = 'Creates and pushes a Docker image for a Java application.'
 }
 }
 }

Slide 65

Slide 65 text

Continuous Integration A  plugin  might  start  small  but  will  grow  in   complexity  over  time.  Avoid  integration  hell!   • Fast  feedback  with  every  code  integration   • Always  execute  (an  assorted  set  of)  tests   • Use  CI  cloud-­‐based  product  or  host  in-­‐ house

Slide 66

Slide 66 text

Adding a CI badge Give  consumers  and  contributor  a  chance  to   see  the  build  status.

Slide 67

Slide 67 text

Working towards Continuous Delivery Make  sure  your  plugin  is  always  production   ready.   • End  goal:  avoid  manual  steps   • Build  and  visualize  suitable  delivery  pipeline   • Go  the  extra  mile  -­‐  model  push-­‐button   release  capabilities

Slide 68

Slide 68 text

Example: Potential release steps  Publi Assemble   artifact(s)    Tag  VCS   repository    Publish   artifact(s) Plugin  JAR   Sources  JAR   Docs  JAR Create  tag   Push  tag Create  metadata   Upload  artifact(s)   Update  API  docs

Slide 69

Slide 69 text

DEMO Implementing  a  plugin  release  process

Slide 70

Slide 70 text

Example: Build pipeline with Snap CI                      Build,  test  and  deploy  in  the  cloud.   • Free  of  charge  for  public  Github  repos   • Model  build  pipelines  with  automatic  and/ or  manual  execution  steps   • Execute  build  for  pull  requests

Slide 71

Slide 71 text

Example: Snap CI pipeline for Docker plugin https:/ /snap-­‐ci.com/bmuschko/gradle-­‐docker-­‐plugin/branch/master Compilation      Unit  tests Integration            tests Functional            tests Release

Slide 72

Slide 72 text

DEMO Showcasing  build  pipeline  on  Snap  CI

Slide 73

Slide 73 text

Other aspects • Be  mindful  of  the  performance  impact  your   plugin  might  have  on  consuming  build   • Avoid  using  internal  Gradle  APIs  as  much  as   possible

Slide 74

Slide 74 text

Thank  You! Please  ask  questions… https:/ /www.github.com/bmuschko @bmuschko