Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Boilerplate project configuration in Gradle with Gradle Kotlin DSL

I'm currently trying to improve the way our projects share their configuration. We have lots of different multi-module gradle projects for all of our libraries and microservices (i.e. many git repos).

My main goals are:

  • To not have my Nexus repository config duplicated in every project (also, I can safely assume that the URL won't change)
  • To make my custom Gradle plugins (published to Nexus) available to every project with minimal boilerplate / duplication (they should be available to every project, and the only thing the project cares about is the version it's using)
  • No magic - it should be obvious to developers how everything is configured

My current solution is a custom gradle distribution with an init script that:

  • adds mavenLocal() and our Nexus repository to the project repos (very similar to the Gradle init script documentation example, except it adds repos as well as validating them)
  • configures an extension that allows our gradle plugins to be added to the buildscript classpath (using this workaround). It also adds our Nexus repo as a buildscript repo as that's where the plugins are hosted. We have quite a few plugins (built upon Netflix's excellent nebula plugins) for various boilerplate: standard project setup (kotlin setup, test setup, etc), releasing, publishing, documentation, etc and it means our project build.gradle files are pretty much just for dependencies.

Here is the init script (sanitised):

/**  * Gradle extension applied to all projects to allow automatic configuration of Corporate plugins.  */ class CorporatePlugins {      public static final String NEXUS_URL = "https://example.com/repository/maven-public"     public static final String CORPORATE_PLUGINS = "com.example:corporate-gradle-plugins"      def buildscript      CorporatePlugins(buildscript) {         this.buildscript = buildscript     }      void version(String corporatePluginsVersion) {         buildscript.repositories {             maven {                 url NEXUS_URL             }         }         buildscript.dependencies {             classpath "$CORPORATE_PLUGINS:$corporatePluginsVersion"         }     }  }  allprojects {     extensions.create('corporatePlugins', CorporatePlugins, buildscript) }  apply plugin: CorporateInitPlugin  class CorporateInitPlugin implements Plugin<Gradle> {      void apply(Gradle gradle) {          gradle.allprojects { project ->              project.repositories {                 all { ArtifactRepository repo ->                     if (!(repo instanceof MavenArtifactRepository)) {                         project.logger.warn "Non-maven repository ${repo.name} detected in project ${project.name}. What are you doing???"                     } else if(repo.url.toString() == CorporatePlugins.NEXUS_URL || repo.name == "MavenLocal") {                         // Nexus and local maven are good!                     } else if (repo.name.startsWith("MavenLocal") && repo.url.toString().startsWith("file:")){                         // Duplicate local maven - remove it!                         project.logger.warn("Duplicate mavenLocal() repo detected in project ${project.name} - the corporate gradle distribution has already configured it, so you should remove this!")                         remove repo                     } else {                         project.logger.warn "External repository ${repo.url} detected in project ${project.name}. You should only be using Nexus!"                     }                 }                  mavenLocal()                  // define Nexus repo for downloads                 maven {                     name "CorporateNexus"                     url CorporatePlugins.NEXUS_URL                 }             }         }      }  } 

Then I configure each new project by adding the following to the root build.gradle file:

buildscript {     // makes our plugins (and any others in Nexus) available to all build scripts in the project     allprojects {         corporatePlugins.version "1.2.3"     } }  allprojects  {     // apply plugins relevant to all projects (other plugins are applied where required)     apply plugin: 'corporate.project'      group = 'com.example'      // allows quickly updating the wrapper for our custom distribution     task wrapper(type: Wrapper) {         distributionUrl = 'https://com.example/repository/maven-public/com/example/corporate-gradle/3.5/corporate-gradle-3.5.zip'     } } 

While this approach works, allows reproducible builds (unlike our previous setup which applied a build script from a URL - which at the time wasn't cacheable), and allows working offline, it does make it a little magical and I was wondering if I could do things better.

This was all triggered by reading a comment on Github by Gradle dev Stefan Oehme stating that a build should work without relying on an init script, i.e. init scripts should just be decorative and do things like the documented example - preventing unauthorised repos, etc.

My idea was to write some extension functions that would allow me to add our Nexus repo and plugins to a build in a way that looked like they were built into gradle (similar to the extension functions gradleScriptKotlin() and kotlin-dsl() provided by the Gradle Kotlin DSL.

So I created my extension functions in a kotlin gradle project:

package com.example  import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.dsl.RepositoryHandler import org.gradle.api.artifacts.repositories.MavenArtifactRepository  fun RepositoryHandler.corporateNexus(): MavenArtifactRepository {     return maven {         with(it) {             name = "Nexus"             setUrl("https://example.com/repository/maven-public")         }     } }  fun DependencyHandler.corporatePlugins(version: String) : Any {     return "com.example:corporate-gradle-plugins:$version" } 

With the plan to use them in my project's build.gradle.kts as follows:

import com.example.corporateNexus import com.example.corporatePlugins  buildscript {      repositories {         corporateNexus()     }      dependencies {         classpath(corporatePlugins(version = "1.2.3"))     } } 

However, Gradle was unable to see my functions when used in the buildscript block (unable to compile script). Using them in the normal project repos/dependencies worked fine though (they are visible and work as expected).

If this worked, I was hoping to bundle the jar into my custom distribution , meaning my init script could just do simple validation instead of hiding away the magical plugin and repo configuration. The extension functions wouldn't need to change, so it wouldn't require releasing a new Gradle distribution when plugins change.

What I tried:

  • adding my jar to the test project's buildscript classpath (i.e. buildscript.dependencies) - doesn't work (maybe this doesn't work by design as it doesn't seem right to be adding a dependency to buildscript that's referred to in the same block)
  • putting the functions in buildSrc (which works for normal project deps/repos but not buildscript, but is not a real solution as it just moves the boilerplate)
  • dropping the jar in the lib folder of the distribution

So my question really boils down to:

  • Is what I'm trying to achieve possible (is it possible to make custom classes/functions visible to the buildScript block)?
  • Is there a better approach to configuring a corporate Nexus repo and making custom plugins (published to Nexus) available across lots of separate projects (i.e. totally different codebases) with minimal boilerplate configuration?
like image 241
James Bassett Avatar asked Apr 27 '17 12:04

James Bassett


People also ask

What is DSL in gradle?

Simply, it stands for 'Domain Specific Language'. IMO, in gradle context, DSL gives you a gradle specific way to form your build scripts. More precisely, it's a plugin-based build system that defines a way of setting up your build script using (mainly) building blocks defined in various plugins.

What is the Kotlin gradle DSL?

Gradle's Kotlin DSL provides an alternative syntax to the traditional Groovy DSL with an enhanced editing experience in supported IDEs, with superior content assist, refactoring, documentation, and more.

Does gradle work with Kotlin?

Gradle is a build system that is very commonly used in the Java, Android, and other ecosystems. It is the default choice for Kotlin/Native and Multiplatform when it comes to build systems.

What is Kotlin DSL?

The Kotlin DSL provides built-in support for three destination types: Fragment , Activity , and NavGraph destinations, each of which has its own inline extension function available for building and configuring the destination.


1 Answers

If you want to benefit from all the Gradle Kotlin DSL goodness you should strive to apply all plugins using the plugins {} block. See https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md

You can manage plugin repositories and resolution strategies (e.g. their version) in your settings files. Starting with Gradle 4.4 this file can be written using the Kotlin DSL, aka settings.gradle.kts. See https://docs.gradle.org/4.4-rc-1/release-notes.html.

With this in mind you could then have a centralized Settings script plugin that sets things up and apply it in your builds settings.gradle.kts files:

// corporate-settings.gradle.kts pluginManagement {     repositories {         maven {             name = "Corporate Nexus"             url = uri("https://example.com/repository/maven-public")         }         gradlePluginPortal()     } } 

and:

// settings.gradle.kts apply(from = "https://url.to/corporate-settings.gradle.kts") 

Then in your project build scripts you can simply request plugins from your corporate repository:

// build.gradle.kts plugins {     id("my-corporate-plugin") version "1.2.3" } 

If you want your project build scripts in a multi-project build to not repeat the plugin version you can do so with Gradle 4.3 by declaring versions in your root project. Note that you also could set the versions in settings.gradle.kts using pluginManagement.resolutionStrategy if having all builds use the same plugins version is what you need.

Also note that for all this to work, your plugins must be published with their plugin marker artifact. This is easily done by using the java-gradle-plugin plugin.

like image 187
eskatos Avatar answered Sep 28 '22 22:09

eskatos