Search code examples
javagradlegroovybuild.gradlegradle-plugin

How to write Gradle plugin to cut down build.gradle file


A bit of Context first: I am working on migrating my companies projects to be built by Gradle. One thing, that this results in, is having redundancy in my build.gradle files, as I am configuring the same Skeleton over and over again. This includes:

  • Setting the java-,maven-publish- and org.sonarcube-plugin
  • Configuring the repositories to be mavenCentral and our private Artifactory Repo
  • Configuring the publishing block, that is all the same, except for the artifactId
  • Building a Manifest inside the Jar block (using helper Methods, to correctly build the Manifests classpath)
  • Helper Methods
  • two Tasks
  • two dependsOn statements

build.gradle file as of now:

plugins {
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id "org.sonarqube" version "3.2.0"
    id 'maven-publish'
    id 'java' 
}

group = 'group'
version = 'version'
sourceCompatibility = '11'
ext.artifactName = 'ProjectName'

// Where to look for dependencies:
repositories {
    mavenCentral()
    maven{
        credentials{
            username = "${artifactory_user}"
            password = "${artifactory_password}"
        }
        url "${artifactory_contextUrl}"
        allowInsecureProtocol = true
    }
}
// Where to publish what Artifacts to:
publishing {
    publications {
        maven(MavenPublication) {
            groupId = 'group'
            artifactId = 'ProjectName'
            String buildEnvVar = System.env.BUILDENVIRONMENT
            if(buildEnvVar == null){
                version = 'LOCAL BUILD'
            }else{
                version = 'version'
            }
            from components.java
        }
    }
    repositories {
        maven {
            // change to point to your repo, e.g. http://my.org/repo
            name = "gradle-dev"
            url = "${artifactory_contextUrl}"
            allowInsecureProtocol = true
            credentials{
                username = "${artifactory_user}"
                password = "${artifactory_password}"
            }
        }
    }
}

dependencies {...}

jar { 
    // configuration of variables
    String dateString = new Date().format("yyyy-MM-dd HH:mm:ss ")
    String localBuild = "LOCAL BUILD by " + System.getProperty("user.name") + " on " + dateString
    String buildEnvVar = System.env.BUILDENVIRONMENT
    String buildEnvironment
    String classpath = createCP()
    if(buildEnvVar == null){
        buildEnvironment = localBuild 
        archiveName = "ProjectName"
    }else{
        buildEnvironment = buildEnvVar
        archiveFileName= "ProjectName_" + version + ".jar"
        
        delete fileTree("build/libs") {
            include('*')
        }
    }
    manifest {
        attributes (
            "Main-Class": "org.example.foo",
            "Specification-Title" : "ProjectName",
            "Specification-Vendor" : "blab",
            "Specification-Version" : "Spec-version", 
            "Implementation-Title" : "$System.env.JOB_NAME",
            "Implementation-Version" : "Impl-version",
            "Implementation-Vendor" : "blub",
            "Implementation-Vendor-Id" : "blob",
            "Implementation-Url" : "bleb",
            "Build-By" : buildEnvironment,
            'Class-Path': classpath
        )
    }
}

String createCP () {
    // super secret can't share
}

// will suffix the jars with release or debug, depending on it being compiled with or without debug-information
project.gradle.taskGraph.whenReady{
    boolean isDebug = project.gradle.taskGraph.getAllTasks().join(' ').contains('debugJar')
    compileJava.options.debug = isDebug
    String suffix = isDebug? "debug" : "release"
    String fullJarName = "$artifactName-$suffix" + ".jar"
    
    jar.setProperty('archiveName', fullJarName)
}

tasks.named('test') {
    useJUnitPlatform()
}

task debugJar() {}
debugJar.dependsOn(jar)

//Downloads all Jars the project depends on, and saves them in buildDirectory/output/libs if the gradle build command is executed.

 task copyToLib(type: Copy) {
    into "${buildDir}/output/libs"
    from configurations.runtimeClasspath
}

build.dependsOn(copyToLib)

what I want to achive:

plugins {
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id "org.sonarqube" version "3.2.0"
    id 'maven-publish'
    id 'java'
    id 'mySuperPlugin'
}

// Configure mySuperPlugin
mySuperPlugin {
    artifactId = 'xyz'
    mainClass = 'org.example.foo'
    version = 'version'
    stuffFromOtherTasks = ...
}

// Where to look for dependencies:
repositories {
    mavenCentral()
    maven{
        credentials{
            username = "${artifactory_user}"
            password = "${artifactory_password}"
        }
        url "${artifactory_contextUrl}"
        allowInsecureProtocol = true
    }
}

dependencies {...}

Most of the values are the same. The ones that aren't are passed in via Environment-Variables (Jenkins-JobName,...), or get determined through helper Methods. I reckon, that i will most likely not end up with a buildfile like the one above, but atleast some of the buildfile must be outsourceable.

I know as of now, that i can create seperate Tasks in a plugin, like comparing two files, that have been passed. What I didn't find a solution to yet:

  • Can I modify the Jar Task of the project applying the plugin, inside the plugin?
  • How do I pass Outputs from other Tasks into my plugins tasks?
  • How do I access the applying projects data (i.e. the runtimeClasspath)
  • Is a plugin even what i want to do, or is there another way of cutting down the build.gradle file?

I am relatively unexperienced with gradle. I have read through quite a bit of the docs and other postings, but chances are i just overlooked some best-practice way of doing certain things. Therefore, feel free to criticize my buildfile aswell as my approach!


Solution

  • You can do this in a couple of ways. And this comes down to if your project is composed of multiple sub-projects or if they are stand alone projects. For stand alone projects you can do the following in your settings.gradle file:

    includeBuild("../common-project/build.gradle")
    

    The common project would just house the common build.gradle file, and you'd declare all of the items you want to share in there. It would look like a normal build.gradle file.

    That would require that each project share the common configuration from another project, but wouldn't require any additional projects be checked out. Just the common project and the project itself. For more details see:

    https://docs.gradle.org/current/userguide/composite_builds.html

    If you have more of a multi-project build like say many micro-services or subprojects that make up a larger project then using multi-project setup and declare the common pieces in the allprojects closure in the root build.gradle:

    allprojects {
        plugin: java
        plugin: web
    
        repositories {
          ....
        }
    
        dependencies {
           ....
        }
    }
    

    In the multi-project case you'd have to check out the top level project and all subprojects. Your folder layout might look like this:

    - my-project
      - service-1
          src
          build.gradle
      - service-2
          src
          build.gradle
      - service-3
          src
          build.gradle
        build.gradle
        settings.gradle
    

    In the multi-project setup service1, service2, and service3 would be declared in the settings.gradle file using include like so:

    rootProject.name = 'my-project'
    include `service1`
    include `service2`
    include `service3`
    

    In a multi-project setup you'd typically house that in a single source repository as oppose to using includeBuild where each project would belong to a separate source code repo. The former way forces you to checkout the appropriate number of projects in the multi-project case. But, in the includeBuild case the developer would have to know to check out minimum of 2 projects.