Structure your Project with Gradle by using Kotlin Everywhere

KOTLIN/Everywhere Hamburg 2019

Who is Gradle?

gradle staff map

What is Gradle?

gradle structure

Model Your Project with Gradle and Kotlin: Your Project as Software Components

Your Project as a Software Component

components0

Don’t be afraid of Gradle - it’s just Kotlin

build.gradle.kts

// empty file

settings.gradle.kts

rootProject.name = "kotlin-everywhere"

Don’t be afraid of Gradle - it’s just Kotlin

build.gradle.kts

tasks.register("t1") {
    inputs.dir("...")
    ouptuts.file("...")
    doLast {
        ...
    }
}

settings.gradle.kts

rootProject.name = "kotlin-everywhere"

Don’t be afraid of Gradle - it’s just Kotlin

build.gradle.kts

tasks.register("t1") {
    inputs.dir("...")
    ouptuts.file("...")
    doLast {
        // do some work
    }
}

settings.gradle.kts

rootProject.name = "kotlin-everywhere"

Don’t be afraid of Gradle - it’s just Kotlin

buildSrc/src/main/kotlin/MyProject.kt

fun org.gradle.api.Project.configureMyProject() {
    tasks.register("t1") {
        inputs.dir("...")
        ouptuts.file("...")
        doLast {
            // do some work
        }
    }
}

build.gradle.kts

configureMyProject()

You define a model of your software project through a Java/Kotlin API

Entry point: org.gradle.api.Project

Your Project as a Software Component

components1

You define a model of your software project through a Java/Kotlin API

Entry point: org.gradle.api.Project

Your Project as a Software Component

fun org.gradle.api.Project.configureMyProject() {
    println("Tasks: ${tasks.size}")
    plugins.apply("java")
    println("Tasks: ${tasks.size} (Java plugin added)")
    plugins.apply("org.jetbrains.kotlin.jvm")
    println("Tasks: ${tasks.size} (Kotlin plugin added)")
    plugins.apply("com.android.application")
    println("Tasks: ${tasks.size} (Android plugin added)")
}
> Configure project :
Tasks: 16
Tasks: 31 (Java plugin added)
Tasks: 36 (Kotlin plugin added)
Tasks: 49 (Android plugin added)

You define a model of your software project through a Java/Kotlin API

Entry point: org.gradle.api.Project

Your Project as a Software Component

components2

You define a model of your software project through a Java/Kotlin API

Entry point: org.gradle.api.Project

Your Project as multiple Software Components

components3

settings.gradle.kts

include("user-data", "account-data", "services", "desktop-app", "android-app")

Your Project as multiple Software Components

components5
project(":user-data").plugins.apply("java")
project(":account-data").plugins.apply("java")
project(":services").plugins.apply("org.jetbrains.kotlin.jvm")
project(":desktop-app").plugins.apply("org.jetbrains.kotlin.jvm")
project(":android-app").plugins.apply("org.jetbrains.kotlin.android")
project(":android-app").plugins.apply("com.android.application")

Your Project as multiple Software Components

components4

settings.gradle.kts

include("user-data", "account-data", "services", "desktop-app", "android-app")
rootProject.children.forEach {
    val base = when(it.name) {
        "user-data"    -> "data"
        "account-data" -> "data"
        "services"     -> "services"
        else           -> "apps"
    }
    it.projectDir = file("$base/${it.name}")
}

Model Your Project with Gradle and Kotlin: Variant-Aware Dependency Management

Dependency Management

components2 1

Variant-Aware Dependency Management

components2 1

Entry point: org.gradle.api.artifacts.Configuration (API for 3 concepts)

  • Provide: define sets of artifacts I produce for other projects

  • Consume: define sets of dependencies to projects and modules

  • Resolve: tool to collect artifacts from dependencies (e.g. all jars)

Variant-Aware Dependency Management

components2 2 1

Provide: define sets of artifacts I produce for other projects (:services)

val everythingYouNeedToRunMe by configurations.creating {
    isCanBeResolved = false; isCanBeConsumed = true // this is a variant!
}

Variant-Aware Dependency Management

components2 2 2

Provide: define sets of artifacts I produce for other projects (:services)

val everythingYouNeedToRunMe by configurations.creating {
    isCanBeResolved = false; isCanBeConsumed = true // this is a variant!
    extendsFrom(otherComponentsINeedToRun)
}

Consume: define sets of dependencies to projects and modules (:desktop-app)

val otherComponentsINeedToRun by configurations.creating {
    isCanBeResolved = false; isCanBeConsumed = false // this is a bucket!
}
dependencies {
    otherComponentsINeedToRun(project(":services"))
}

Variant-Aware Dependency Management

components2 2 3

Resolve: tool to collect artifacts from dependencies (e.g. all jars)

val pathToAllFiles by configurations.creating {
    isCanBeResolved = true; isCanBeConsumed = false // this is a tool!
    extendsFrom(otherComponentsINeedToRun)
}
tasks.register("allFiles") {
    doLast {
        println(configurations["pathToAllFiles"].incoming
            .artifacts.artifactFiles.map { it.name })
    }
}

gradlew desktop-app:allFiles

> Task :desktop-app:allFiles
[services.jar, services-sources.jar]

Consume other Projects and Modules

components2 4

desktop-app/build.gradle.kts

dependencies {
    implementation(project(":services"))
}

services/build.gradle.kts

dependencies {
    api(project(":user-data"))
    implementation("com.conversantmedia:disruptor:1.2.15")
}

Resolve with Variant Selection

components2 5

gradlew desktop-app:dependencies --configuration compileClasspath

compileClasspath
\--- project :services
     \--- project :user-data

Resolve with Variant Selection

components2 6

gradlew desktop-app:dependencies --configuration compileClasspath

compileClasspath
\--- project :services
     \--- project :user-data

gradlew desktop-app:dependencies --configuration runtimeClasspath

runtimeClasspath
\--- project :services
     +--- project :user-data
     \--- com.conversantmedia:disruptor:1.2.15
          \--- org.slf4j:slf4j-api:1.7.13

Resolve with Variant Selection

components2 7

gradlew services:outgoingVariants

Variant api-variant
--------------------------------------------------
Attributes
- org.gradle.jvm.version         = 11
- org.gradle.libraryelements     = classes
- org.gradle.usage               = java-api

desktop-app/build.gradle.kts

configurations["compileClasspath"].attributes {
    attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}

Resolve with Variant Selection

components2 8

desktop-app/build.gradle.kts

configurations["compileClasspath"].attributes {
    attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}

gradlew desktop-app:dependencies --configuration compileClasspath

compileClasspath
\--- project :services
     +--- project :user-data
     \--- com.conversantmedia:disruptor:1.2.15
          \--- org.slf4j:slf4j-api:1.7.13

Resolve with Variant Selection

components2 9

desktop-app/build.gradle.kts

configurations["runtimeClasspath"].attributes {

}

gradlew desktop-app:allFiles

> Task :desktop-app:allFiles
[services.jar, user-data.jar, disruptor-1.2.15.jar,
 slf4j-api-1.7.13.jar]

Resolve with Variant Selection

components2 10

desktop-app/build.gradle.kts

configurations["runtimeClasspath"].attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}

gradlew desktop-app:allFiles

> Task :desktop-app:allFiles
[services-jdk8.jar, user-data-jdk8.jar,
 disruptor-1.2.15.jar, slf4j-api-1.7.13.jar]

What about Published Modules?

components2 12
  • For projects: Gradle has the full model in memory

  • For modules: Gradle needs to build the model from metadata

POM Module Metadata

components2 13

com/conversantmedia/disruptor/1.2.15/disruptor-1.2.15.pom

<groupId>com.conversantmedia</groupId>
<artifactId>disruptor</artifactId>
<packaging>jar</packaging>
<version>1.2.15</version>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.13</version>
    <scope>compile</scope>
</dependency>

POM Module Metadata

components2 14

com/conversantmedia/disruptor/1.2.15/*.jar

disruptor-1.2.15-jdk10.jar      2018-12-20 14:41    134319
disruptor-1.2.15-jdk8.jar       2018-12-20 14:41    134482
disruptor-1.2.15.jar            2018-12-20 14:41    134482

Gradle Module Metadata (GMM)

components2 15

disruptor-1.2.15.module

"component": { "group": "com.conversantmedia", "module": "disruptor", "version": "1.2.15" },
"variants": [
  { "name": "runtimeElements",
    "attributes": { "org.gradle.jvm.version": 11, "org.gradle.usage": "java-runtime" },
    "dependencies": [{ "group": "org.slf4j", "module": "slf4j-api", "version": { "requires": "1.7.13" }}],
    "files": [{ "name": "conversantmedia-1.2.15.jar", "url": "conversantmedia-1.2.15.jar" }] },
  { "name": "jdk8RuntimeElements",
    "attributes": { "org.gradle.jvm.version": 8, "org.gradle.usage": "java-runtime" },
    "dependencies": [{ "group": "org.slf4j", "module": "slf4j-api", "version": { "requires": "1.7.13" }}],
    "files": [{ "name": "conversantmedia-1.2.15-jdk8.jar", "url": "conversantmedia-1.2.15-jdk8.jar" }]
  },

Variant Selection on Projects and Modules

components2 15

desktop-app/build.gradle.kts

configurations["runtimeClasspath"].attributes {
    attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8)
}

gradlew desktop-app:allFiles

> Task :desktop-app:allFiles
[services-jdk8.jar, user-data-jdk8.jar,
 disruptor-1.2.15-jdk8.jar, slf4j-api-1.7.13.jar]

Model Your Project with Gradle and Kotlin: Features building on Variant-Aware Dependency Management

Handle conflicting Implementations

components3 2 1
dependencies {
    implementation("velocity:velocity:1.5")
    implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}
  • Capabilities: Provided by variants, each can be provided only once

  • Attributes: Define which implementation of a capability fits best

Handle conflicting Implementations

components3 2 2
dependencies {
    implementation("velocity:velocity:1.5")
    implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}
  • Capabilities: Provided by variants, each can be provided only once

  • Attributes: Define which implementation of a capability fits best

Handle conflicting Implementations

components3 2 3
dependencies {
    implementation("velocity:velocity:1.5")
    implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}
Cannot select module with conflict on capability 'log4j:log4j:1.2.12'
also provided by [org.slf4j:log4j-over-slf4j:1.7.13(api-variant)]

Handle conflicting Implementations

class LoggingCapability : ComponentMetadataRule {
  val loggingModules = setOf("log4j", "log4j-over-slf4j")
  override fun execute(ctx: ComponentMetadataContext) = ctx.details.run {
    if (loggingModules.contains(id.name)) {
      allVariants {
        withCapabilities { addCapability("log4j", "log4j", id.version) }
      }
    }
  }
}
  • Component Metadata Rules: Add missing metadata

configurations.all {
  resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
    select(candidates.find { it.module == "log4j-over-slf4j" })
    because("use slf4j in place of log4j")
  }
}
  • Capability Resolution Strategy: Decide what fits in your context

Test Fixtures

user-data/src/main/java/com/acme/User.kt

data class User(val firstName: String, val lastName: String)

user-data/src/testFixtures/java/com/acme/Simpsons.kt

object Simpsons {
    val HOMER  = User("Homer", "Simpson")
    val MARGE  = User("Majorie", "Simpson")
    val BART   = User("Bartholomew", "Simpson")
    val LISA   = User("Elisabeth Marie", "Simpson")
    val MAGGIE = User("Margaret Eve", "Simpson")
    val FAMILY = setOf(HOMER, MARGE, BART, LISA, MAGGIE)
}

user-data/build.gradle.kts

plugins {
    `java-library`
    `java-test-fixtures` // new plugin since Gradle 5.6
    `maven-publish`      // you can publish test fixtures with GMM!
}

Test Fixtures

components3 1
dependencies {
    api(project(":user-data"))
    implementation("com.conversantmedia:disruptor:1.2.15")
    testImplementation(testFixtures(project(":user-data")))
    testImplementation(testFixtures("com.conversantmedia:disruptor:1.2.15"))
}

Versions and Version Conflicts

components3 3
  • For projects: Gradle always uses/builds the working copy

  • For modules: Gradle needs to choose a version

    • Gradle considers all version constraints in the dependency graph

dependencies {
    implementation("com.conversantmedia:disruptor:1.2.15")
    api("org.slf4j:slf4j-api") {
        version { requires("1.6.6") }
    }
}

Versions and Version Conflicts

components3 4
dependencies {
    implementation("com.conversantmedia:disruptor:1.2.15")
    constraints {
        api("org.slf4j:slf4j-api") {
            version {
                requires("1.6.6")
            }
        }
    }
}

Versions and Version Conflicts

components3 5
dependencies {
    implementation("com.conversantmedia:disruptor:1.2.15")
    constraints {
        api("org.slf4j:slf4j-api") {
            version {
                requires("1.6.6")
                forSubgraph()
            }
        }
    }
}

Platforms for Managing Dependency Versions

components3 6
plugins { `java-platform` }
dependencies {
    constraints {
        api("com.conversantmedia:disruptor:1.2.15")
        api("org.slf4j:slf4j-api:1.6.6") { version { forSubgraph() } }
    }
}

Platforms for Managing Dependency Versions

components3 7
dependencies {
    api(platform(project(":my-platform")))
    implementation("com.conversantmedia:disruptor")
}

Maybe try a Build Scan with Gradle Enterprise

scan httpclient
scan httpclient comparison

Remember…​

  • With Gradle you model your project through a Java/Kotlin API

    • org.gradle.api.Project (basic building blocks, apply plugins here)

    • org.gradle.api.Task (not covered today, let plugins add them)

    • org.gradle.api.artifacts.Configuration (good stuff is hidden here)

  • Use variants as interface between projects and modules

    • Variant Attributes and Capabilities 😳

    • Gradle 6.0 will go full steam on Gradle Module Metadata (GMM) 🚀

    • All new features shown are compatible with GMM 🥳

    • Configuration is the entry point (better APIs will follow) 🙈

Thank you

🕊️ @jeoj