gradle plugins, take it to the next level
TRANSCRIPT
Gradle PluginsTake It To The Next Level
Eyal LEZMY
eyal.fr
SLIDES bit.ly/gradle-plugin-next-level
Agenda
01
Basics
02
Gradly DSL
03
Android Gradle Plugin
04
Test it
05
Publish it
{ }
BASICS01
Build scriptsYour build.gradle file
Script pluginsThe customization you start writing
Binary pluginsThe code I want you to write
BASICS
Gradle Plugins Types
Is a piece of work for a buildCompiling a class, generating javadoc, ...
Can be manipulateddoFirst, doLast
Can inherits from anothertype
Can depend on another taskdependsOn, finalizedBy
BASICS
The Gradle Task
Is a piece of work for a buildCompiling a class, generating javadoc, ...
Can be manipulateddoFirst, doLast
Can inherits from anothertype
Can depend on another taskdependsOn, finalizedBy
BASICS
The Gradle Task
A build = A task graph
Is a Gradle projectBasically, a Groovy project
It containsA build.gradleA plugin classA descriptorOne or several tasksAn extension
ExamplesJava, Groovy, Maven, Android plugin
BASICS
The Binary Plugin
BASICS
InitializationChoose project(s) to build
ConfigurationExecute build.gradleBuild task graph
ExecutionExecute task chain
Gradle build
lifecycle
BASICS
Project evaluationbeforeEvaluateafterEvaluate
Task GraphwhenTaskAddedwhenReadybeforeTaskafterTask
The lifecycleevents
GRADLY DSL02
EXTEND IT
ReadableThe user can easily understand
FlexibleExpress complex situations, on a simple way
IntuitiveThe user can easily configure
TalkativeHelp the user solve his problems
What makes a good
DSL
Use nested extensions
Make it readable
READABLE
//create the extensionproject.extensions.create(“myExtension”,MyExtension, arg1, arg2)
Plugin class
class MyExtension {
String myInfo List<String> myList
MyExtension(def arg1, def arg2) {
myInfo = “Default String” myList = [“default”, “list”]
}
}
READABLE
Extension class
READABLE
apply: “myPlugin”
...
myExtension {
myInfo “New String” myList [“new”, “list”]
}
build.gradle
READABLE
genymotion {
//configure genymotion configLicenseServer true configLicenseServerAddress “192.168.1.33” configSdkPath “/home/me/Android/sdk” configUseCustomSdk true
//launch devices device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”)}
build.gradle
READABLE
genymotion {
config { licenseServer true licenseServerAddress “192.168.1.33” sdkPath “/home/me/Android/sdk” useCustomSdk true }
//launch devices device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”)}
build.gradle
//create the extensionproject.extensions.create(“genymotion”,GenymotionExtension)//create the nested extensionproject.genymotion.extensions.create(“config”,GenymotionConfig)
READABLE
Plugin class
Use Containers
Make it flexible
FLEXIBLE
genymotion { //launch devices device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”)}
build.gradle
FLEXIBLE
genymotion {
device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”, 1920, 1080, “xxhdpi”, ...) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, 1280, 800, “xhdpi”, ...))}
build.gradle
FLEXIBLE
genymotion {
device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”, 1920, 1080, “xxhdpi”, [“path/to/apk”, “path/to/apk2”], [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/database.db”:”/tmp/], true)
device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, 1280, 800, “xhdpi”, “path/to/apk”, [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”], true)}
build.gradle
FLEXIBLE
build.gradlegenymotion {
device(name: “Nexus5”, template: “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”, width: 1920, height: 1080, density: “xxhdpi”, install: [“path/to/apk”, “path/to/apk2”], pullAfter: [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”], stopWhenFinished: true)
device(name: “Nexus4”, template: “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, ...)}
FLEXIBLE
genymotion {
devices { Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } }}
build.gradle
FLEXIBLE
genymotion {
devices { Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } }}
build.gradle
project.genymotion.devices
FLEXIBLE
genymotion {
devices { Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } }}
build.gradle
project.genymotion.devices(Closure c)
FLEXIBLE
genymotion {
devices { Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } }}
build.gradle
project.genymotion.devices(Closure c)
Add ‘Nexus4’Add ‘Nexus5’
FLEXIBLE
genymotion {
devices { Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } }}
build.gradle
project.genymotion.devices(Closure c)
Add ‘Nexus4’Add ‘Nexus5’
Container
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions.create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions.create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions.create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
Create a container for DeviceLaunch
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions. create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
Create the extension
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions.create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
Add the DeviceLaunch container
FLEXIBLE
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch,
new DeviceLaunchFactory(instantiator))
project.extensions.create(“genymotion”,GenymotionExtension, project, deviceLaunches)
project.afterEvaluate { project.genymotion.injectTasks() }
}}
The Plugin class
FLEXIBLE
The Extension classclass GenymotionExtension {
NamedDomainObjectContainer<DeviceLaunch> deviceLaunches
GenymotionExtension(Project project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches }
def devices(Closure closure) { deviceLaunches.configure(closure) }
...
FLEXIBLE
The Extension classclass GenymotionExtension {
NamedDomainObjectContainer<DeviceLaunch> deviceLaunches
GenymotionExtension(Project project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches }
def devices(Closure closure) { deviceLaunches.configure(closure) }
...
DeviceLaunch container
FLEXIBLE
The Extension classclass GenymotionExtension {
NamedDomainObjectContainer<DeviceLaunch> deviceLaunches
GenymotionExtension(Project project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches }
def devices(Closure closure) { deviceLaunches.configure(closure) }
...
We get it from plugin apply()
FLEXIBLE
The Extension classclass GenymotionExtension {
NamedDomainObjectContainer<DeviceLaunch> deviceLaunches
GenymotionExtension(Project project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches }
def devices(Closure closure) { deviceLaunches.configure(closure) }
... Create the syntax genymotion.devices{ }
FLEXIBLE
The Extension classclass GenymotionExtension {
NamedDomainObjectContainer<DeviceLaunch> deviceLaunches
GenymotionExtension(Project project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches }
def devices(Closure closure) { deviceLaunches.configure(closure) }
... Let Gradle add all the declared items
FLEXIBLE
The Extension classclass DeviceLaunchFactory implements NamedDomainObjectFactory<DeviceLaunch> {
final Instantiator instantiator
public DeviceLaunchFactory(Instantiator instantiator) { this.instantiator = instantiator }
@Override DeviceLaunch create(String name) { return instantiator.newInstance(DeviceLaunch.class, name) }}
FLEXIBLE
class DeviceLaunchFactory implements NamedDomainObjectFactory<DeviceLaunch> {
final Instantiator instantiator
public DeviceLaunchFactory(Instantiator instantiator) { this.instantiator = instantiator }
@Override DeviceLaunch create(String name) { return instantiator.newInstance(DeviceLaunch.class, name) }}
The Extension class
INTUITIVE
The modelclass DeviceLaunch {
String name
DeviceLaunch(String name) { this.name = name }
...
}
methods > properties
Make it intuitive
INTUITIVE
genymotion { devices {
Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true }
}}
build.gradle
INTUITIVE
genymotion { devices {
Nexus5 { template “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true }
}}
build.gradle
INTUITIVE
build.gradle
install [“path/to/apk”, “path/to/apk2”]pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”]
INTUITIVE
install “path/to/apk”, “path/to/apk2”, ...
pullAfter from:“/sdcard/prop.txt”, to:”/tmp/”pullAfter from:“/sdcard/data.db”, to:”/tmp/”
build.gradle
install [“path/to/apk”, “path/to/apk2”]pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”]
INTUITIVE
The Extension class (1/2)class GenymotionExtension {
private List<String> install = []
def install(String... paths) { install.addAll(paths) }
def setInstall(String... paths) { install.clear() install.addAll(paths) }
...
INTUITIVE
The Extension class (2/2)
...
private def pullAfter = [:]
def pullAfter(String from, String to) { pullAfter.put(from, to) }
...
}
Counterbalance the lack of autocompletion
Make it talkative
TALKATIVE
No suggestion in IDEs
No integrated documentation
No Discoverability
TALKATIVE
Log is your voiceBut respect the Gradle conventioned levels
Errors are part of documentationAnticipate the mistakes and deliver the appropriate explicit message
Be Talkative
DANCE WITH THE ANDROID GRADLE PLUGIN
03
ANDROID GRADLE PLUGIN
android.applicationVariantsOnly for the app plugin
android.libraryVariantsOnly for the library plugin
android.testVariantsFor both plugins
The entry points
android.applicationVariants.all { variant -> ....
}
ANDROID GRADLE PLUGIN
android.applicationVariants.all { variant -> ....
}
ANDROID GRADLE PLUGIN
Call it after the evaluation
ANDROID GRADLE PLUGIN
tools.android.com/tech-docs/new-build-system/user-guide
“Manipulating tasks” sectionDetails variant attributes
The documentation
ANDROID GRADLE PLUGIN
Not up-to-date~30% wrong information
But a good entry point
The documentation
ANDROID GRADLE PLUGIN
The source code100% accurateThe real
documentation
$ git clone https://android.googlesource.com/platform/tools/base
$ git checkout tags/gradle_1.3.1
ANDROID GRADLE PLUGIN
ANDROID GRADLE PLUGIN
Your debuggerBrowsing through the project on-the-fly
Integration tests...... are highly recommended
The real documentation
part 2
ANDROID GRADLE PLUGIN
Avoid using explicit values“connectedAndroidTest”, ...
Use dedicated properties
Internals are changing a lot
android.testVariants.all { variant -> Task testTask = variant.connectedAndroidTest
... }
ANDROID GRADLE PLUGIN
android.testVariants.all { variant -> Task testTask = variant.connectedAndroidTest
... }
ANDROID GRADLE PLUGIN
$ gradle test --stacktrace
android.testVariants.all { variant -> Task testTask = variant.connectedAndroidTest
... }
ANDROID GRADLE PLUGIN
$ gradle test --stacktrace
groovy.lang.MissingPropertyException: Could not find property 'connectedAndroidTest'
...BUILD FAILED
variant.variantData.connectedTestTask = "task ':connectedDebugAndroidTest'"
ANDROID GRADLE PLUGIN
Using the debugger
variant.variantData.connectedTestTask = "task ':connectedDebugAndroidTest'"
ANDROID GRADLE PLUGIN
@Overridepublic DefaultTask getConnectedInstrumentTest() { return variantData.connectedTestTask;}
Using the debugger
TestVariantImpl.java
variant.variantData.connectedTestTask = "task ':connectedDebugAndroidTest'"
ANDROID GRADLE PLUGIN
@Overridepublic DefaultTask getConnectedInstrumentTest() { return variantData.connectedTestTask;}
Using the debugger
TestVariantImpl.java
Task testTask = variant.connectedInstrumentTest
The good API
ANDROID GRADLE PLUGIN
connectedAndroidTest1.0.0 05/12/14
ANDROID GRADLE PLUGIN
connectedAndroidTest
connectedAndroidTestDebug
1.0.0
1.2.0
05/12/14
23/04/15
5 months
ANDROID GRADLE PLUGIN
connectedAndroidTest
connectedAndroidTestDebug
connectedDebugAndroidTest
1.0.0
1.2.0
1.3.0
05/12/14
23/04/15
01/07/15
5 months
3 months
ANDROID GRADLE PLUGIN
Do not depend on a specific release
Integration tests...... are highly recommended
Internals are changing a lot
TEST IT04
Very simpleAs simple as Groovy is
Groovy is your best friendVery easy to mock
Junit & coAs anybody knows
TEST IT
Gradle project testing
ProjectBuilderTo create a project stub
EvaluateTo execute your build script
TEST IT
A few specificities
TEST IT
Our buid.gradle
...
repositories { mavenCentral()}
dependencies { testCompile 'junit:junit:4.11'}
...
TEST IT
Our buid.gradle
...
repositories { mavenCentral()}
dependencies { testCompile 'junit:junit:4.11'}
...
Adding maven central repository
TEST IT
Our buid.gradle
...
repositories { mavenCentral()}
dependencies { testCompile 'junit:junit:4.11'}
...
Adding junit as testing dependency
Now, test the extension
TEST IT
Your first test!
class GenymotionPluginTest {
@Test public void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.genymotion instanceof GenymotionExtension }}
TEST IT
Your first test!
class GenymotionPluginTest {
@Test public void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.genymotion instanceof GenymotionExtension }}
Stub a Gradle project
TEST IT
Your first test!
class GenymotionPluginTest {
@Test public void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.genymotion instanceof GenymotionExtension }}
Apply our plugin
TEST IT
Your first test!
class GenymotionPluginTest {
@Test public void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.genymotion instanceof GenymotionExtension }}
Test our extension exists
Now, test the task
TEST IT
Your second test!
class GenymotionPluginTest {
@Test public void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.tasks.genymotionTask instanceof GenymotionTask }}
We initialize our project
TEST IT
Your second test!
class GenymotionPluginTest {
@Test public void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion'
assert project.tasks.genymotionTask instanceof GenymotionTask }}
We test the task
TEST IT
Run your second test$ gradle test --stacktrace --debug
TEST IT
Run your second test$ gradle test --stacktrace --debug
...
com.genymotion.GenymotionPluginTest > canAddGenymotionTask FAILEDMissingPropertyException:Could not find property 'genymotionTask' on task set
...
BUILD FAILED
TEST IT
Run your second test$ gradle test --stacktrace --debug
...
com.genymotion.GenymotionPluginTest > canAddGenymotionTask FAILEDMissingPropertyException: Could not find property 'genymotionTask' on task set
...
BUILD FAILED
Our task is not created
class GenymotionPlugin implements Plugin<Project> {
void apply(Project project) {
//create extensions ...
project.afterEvaluate { //create the tasks ... } }}
TEST IT
The Plugin class
Tasks are created after project.evaluate()
So, evaluate.
TEST IT
Your first test!
class GenymotionPluginTest {
@Test public void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' project.evaluate()
assert project.tasks.genymotionTask instanceof GenymotionTask }} We launch evaluate() on the project
TEST IT
Run your second test$ gradle test --stacktrace --debug
TEST IT
Run your second test$ gradle test --stacktrace --debug
...
BUILD SUCCESSFUL
TEST IT
Run your second test$ gradle test --stacktrace --debug
...
BUILD SUCCESSFUL Yay!
TEST IT
LUKE DALEYGradleware Principal Engineer
GRADLE FORUM
You don't see this in the API docs for Project because it is an internal method and is therefore potentially subject to change in future releases.
There will be a supported mechanism for doing this kind of thing in the near future.
”
“
TEST IT
LUKE DALEYGradleware Principal Engineer
GRADLE FORUM
You don't see this in the API docs for Project because it is an internal method and is therefore potentially subject to change in future releases.
There will be a supported mechanism for doing this kind of thing in the near future.
June 2011
”
“
What about Android?
TEST IT
build.gradlerepositories { jcenter()}
dependencies { testCompile 'junit:junit:4.11' testCompile "com.android.tools.build:gradle:1.3.1"}
TEST IT
Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build();
project.apply plugin: 'com.android.application'project.apply plugin: 'genymotion'
project.android { compileSdkVersion 21 buildToolsVersion "21.1.2"}
Test class
We create a project from a folder
TEST IT
Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build();
project.apply plugin: 'com.android.application'project.apply plugin: 'genymotion'
project.android { compileSdkVersion 21 buildToolsVersion "21.1.2"}
Test class
We add the Android Gradle plugin
TEST IT
Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build();
project.apply plugin: 'com.android.application'project.apply plugin: 'genymotion'
project.android { compileSdkVersion 21 buildToolsVersion "21.1.2"}
Test class
We declare the mandatory values
TEST IT
res/test/android-app
TEST IT
sdk.dir=/path/to/android/sdk
local.properties
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.genymotion.sample">
</manifest>
AndroidManifest.xml
TEST IT
@Test@Category(Android)public void canInjectToVariants() {
project = getAndroidProject()
project.android.productFlavors { flavor1 flavor2 } project.evaluate()
...
Test class (1/2) We annotate Android related tests
TEST IT
@Test@Category(Android)public void canInjectToVariants() {
project = getAndroidProject()
project.android.productFlavors { flavor1 flavor2 } project.evaluate()
...
Test class (1/2)
We add flavors to the project
TEST IT
...
project.android.testVariants.all { variant ->
Task connectedTask = variant.connectedInstrumentTest assert connectedTask.getTaskDependencies().getDependencies() .contains(genymotionTask) }}
Test class (2/2)
We test the dependency is done
Test with several Android plugin versions
Control Android plugin versionfrom outside the project
Use Gradle properties
TEST IT
Ensure compatibility
TEST IT
def androidVersion = "+"if (hasProperty("androidPluginVersion")) { androidVersion = androidPluginVersion}
dependencies { testCompile 'junit:junit:4.11' testCompile "com.android.tools.build:gradle: $androidVersion"}
build.gradle
./gradlew test -PandroidPluginVersion=1.3.1
cmd
Run Android integration tests daily On your CI
Test with the beta releasesUse jcenter()Set the default plugin version to “+”
TEST IT
Ensure compatibility
PUBLISH IT05
PUBLISH IT
Sharing with peopleBeing public
Easy embbedingOn the build.gradle
Why publishing
your plugin?
PUBLISH IT
Host code on githubOpen Source
Host binary on bintraybintray.com
Referenced on JCenterjcenter()
How to?The quick way, for free
PUBLISH IT
Gradle Plugin Devhttps://github.com/etiennestuder/gradle-plugindev-plugin
Publish automatically to Bintray
The good tool
Thank you!
eyal.fr
SLIDES bit.ly/gradle-plugin-next-level