Gradle plugins, take it to the next level

Post on 17-Jan-2017

567 views 0 download

Transcript of Gradle plugins, take it to the next level

Gradle PluginsTake It To The 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