Home

Awesome

Gradle Java-lib plugin

License CI Appveyor build status codecov

About

Plugin do all boilerplate of maven publication configuration (using maven-publish) for java (or groovy) library or gradle plugin. Simplifies POM configuration and dependencies management (BOM). Also, changes some defaults common for java projects (like UTF-8 usage).

Makes gradle more "maven" (in sense of simplicity, some behaviours and for multi-module projects).

Features:

If you need multiple publications from the same project, then you will have to perform additional configuration or, maybe (depends on case), use only pom plugin.

Confusion point: plugin named almost the same as gradle's own java-library plugin, but plugins do different things (gradle plugin only provides api and implementation configurations) and plugins could be used together.

Summary

Setup

Maven Central Gradle Plugin Portal

buildscript {
    repositories {
      gradlePluginPortal()
    }
    dependencies {
        classpath 'ru.vyarus:gradle-java-lib-plugin:3.0.0'
    }
}
apply plugin: 'ru.vyarus.java-lib'

OR

plugins {
    id 'ru.vyarus.java-lib' version '3.0.0'
}

Compatibility

Plugin compiled for java 8, compatible with java 17

GradleVersion
73.0.0
5.12.4.0
4.61.1.2
older1.0.5

NOTE: plugin-publish 1.x would work properly only with gradle 7.6 or above

Snapshots

<details> <summary>Snapshots may be used through JitPack</summary> </details>

Usage

Plugin activate features based on registered plugins. Plugin support several usage scenarios.

In case of multi-module projects, plugin activate features only in applied module, ignoring submodules or root module (so to apply it in all submodules use allprojects or subprojects section)

Example projects:

Java module

plugins {
  id 'java' // groovy or java-library
  // id 'signing'
  // id 'project-report'
  id 'ru.vyarus.java-lib'
}

group = 'your.group'                    
version = '1.0.0'                       
description = 'My project description'

// configure target pom
maven.pom {
  name = 'Project Name'
  description = 'My awesome project'
  ...
}

repositories { mavenLocal(); mavenCentral() }
dependencies {
  ...
}

javaLib {
  // withoutJavadoc()
  // withoutSources()
  withoutGradleMetadata()
  
  // autoModuleName = 'project-module-name'
  
  pom {
    // removeDependencyManagement()
    // forceVersions()
    // disableScopesCorrection()
    // disableBomsReorder()
  }
}

Activates with java, groovy or java-library plugin.
Typical usage: single-module gradle project which must be published to maven central (or any other maven repo)

BOM module

plugins {
  id 'java-platform' 
  // id 'signing'
  // id 'project-report'
  id 'ru.vyarus.java-lib'
}

group = 'your.group'                    
version = '1.0.0'                       
description = 'My project description'

maven.pom {
  ...
}

repositories { mavenLocal(); mavenCentral() }
dependencies {
  api platform('ru.vyarus.guicey:guicey-bom:5.2.0-1')
  constraints {
    api 'org.webjars:webjars-locator:0.40'
  }
  // add subprojects to published BOM
  project.subprojects.each { api it }
}

javaLib {
  bom {
    // change artifact from project name, if required
    artifactId = 'something-bom'
    description = 'Different from project description'
  }
  withoutGradleMetadata()
}

Activates with java-platform plugin.
Typical usage: BOM module (might be root project) in multi-module project

Root project reports aggregation

plugins {
  id 'base' 
  id 'jacoco'
  //id 'project-report'
  id 'ru.vyarus.java-lib'
}

javaLib {
  aggregateReports()
}

// sub modules - simple java projects
subprojects {
  apply plugin: 'java'
  
  ...
}

Activates with base plugin.
Used to aggregate test and coverage reports from java submodules.

By default, will only register openDependencyReport task added if project-report plugin enabled.
Reports aggregation must be explicitly triggered:

In short: it adds absolutely the same tasks as in java modules and generates reports exactly into the same locations so there would be no difference in paths when configuring external services (e.g. coveralls).

NOTE: aggregation will work with java-platform plugin too if it used in the root module (see complete multi-module example).

Options

javaLib {

  /**
   * Do not publish javadoc (groovydoc) with `maven` publication. 
   */
  withoutJavadoc()

  /**
   * Do not publish sources with `maven` publication. 
   */
  withoutSources()

  /**
   * Do not publish gradle metadata artifact. 
   * Affects all publications (not just registered by plugin).
   */
  withoutGradleMetadata()

  /**
   * Disable all publications. Might be used to disable configured BOM publication or any sub-module publication.
   */
  withoutPublication()

  /**
   * Shortcut for Auto-Module-Name meta-inf header declaration
   */
  autoModuleName = 'project-module-name'

  /**
   * Used ONLY with java-platform plugin if published artifact must differ from
   * project name (for example, when declared in the root project).
   */
  bom {
    // when not declared, project.name used
    artifactId = 'name'
    // when not declared, project.description used
    description = 'desc'
  }

  /**
   * Used in the root project (project with child projects) to aggregate
   * test, coverage (jacoco) and dependency (project-report) reports. 
   * Requires at least `base` plugin. Will work java-platform plugin
   * (will not work with java plugin because such module can't aggregate).
   */
  aggregateReports()
}

POM

You need to specify general project info:

group = 'your.group'                    // maven group
version = '1.0.0'                       // project version
description = 'My project description'  // optional (affects jar manifest) 

Note: maven artifactId will be the same as project name, and the default for project name is current directory name. If you need to change name, add in settings.gradle:

rootProject.name = 'the-name-you-want'

For maven-central publication you need to fill all required pom sections:

maven.pom {
    // name and desciption set automatically from project, but you can override them here
    //name 'Project Name'
    //description 'My awesome project'
    licenses {
        license {
            name = "The MIT License"
            url = "http://www.opensource.org/licenses/MIT"
            distribution = 'repo'
        }
    }
    scm {
        url = 'https://github.com/me/my-repo'
        connection = 'scm:git@github.com:me/my-repo.git'
        developerConnection = 'scm:git@github.com:me/my-repo.git'
    }
    developers {
        developer {
            id = "dev1"
            name = "Dev1 Name"
            email = "dev1@email.com"
        }
    }
}

Read more about pom configuration in the pom plugin's docs.

If your project hosted on github you may use github-info plugin, which fills most github-related pom sections for you automatically.

Use the following configurations to get correct scopes in the resulted pom:

Maven scopeGradle configuration
compileimplementation, api
runtimeruntimeOnly
providedprovided (not compileOnly!)
optionaloptional, feature variants

See pom plugin doc for more details about dependencies scopes in the generated pom

Using BOMs

When you use BOMs (for dependencies versions management) with spring plugin or gradle platform you'll have dependencyManagement section generated in the target pom. Often it is not desired:to use only resolved versions and avoid dependencyManagent use:

maven.pom.removeDependencyManagement()

Read more in the pom plugin's docs

BOM declaration

The simplest way to declare BOM is using java-platform

plugins {
  id 'java-platform'
  id 'ru.vyarus.java-lib'
}

repositories { mavenLocal(); mavenCentral() }
dependencies {
  api platform('ru.vyarus.guicey:guicey-bom:5.2.0-1')
  constraints {
    api 'org.webjars:webjars-locator:0.40'
  }
  // add subprojects to published BOM
  project.subprojects.each { api it }
}

Java-lib plugin would automatically activate dependencies declaration (constraints block).

I propose to mix dependencies and modules into single BOM declaration, but you can always split dependencies management and modules BOM by declaring two platforms in two different modules.

If you use java-platform in the root project, then you might want to change name of published artifact (by default it would be root project name in this case). To change it use:

javaLib {
  bom {
    artifactId = 'some-bom'
    description = 'overridden description'
  }
}

Publication

maven-publish plugin used for publication.

By default, plugin configures maven publication with javadoc or (and) groovydoc and sources jars for java (groovy or java-library) plugins and bom publication for java-platform plugin.

Use install task to deploy everything into local maven repository.

$ gradlew install

If you don't want to publish everything (jar, sources, javadoc) then you can:

javaLib {
  withtouSources()
  withoutJavadoc()
}

OR override list of publishing artifacts:

publishing.publications.maven.artifacts = [jar, javadocJar]

NOTE that for maven central publication sources and javadocs are required

To ADD artifacts for publication, configure them directly for publication:

publishing {
    publications.maven {
        artifact buildDelivery { archiveClassifier.set('zip') }
    }
}

Here the result of buildDelivery task (of type Zip) added to maven publication with zip classifier.

Gradle metadata

Since gradle 6, gradle would always publish its metadata:

Gradle Module Metadata is a unique format aimed at improving dependency resolution by making it multi-platform and variant-aware.

Essentially, it's an additional .module file containing json representation of dependencies. This is really necessary only when advanced gradle features used (constraints (not in platform), variants).

But this would mean that gradle and maven projects would use different dependencies after publication: maven use pom, gradle would load .module file with additional dependencies info.

It would be more honest to publish only pom (especially for public projects) and disable metadata publishing:

javaLib {
  withoutMavenMetadata()
}

Also note, that maven central could complain about metadata file (if published).

Publish to repository

You must configure repository for actual publication repository must be configured:

publishing {
    repositories {
        maven {
            // change to point to your repo, e.g. http://my.org/repo
            url "$buildDir/repo"
        }
    }
}

Then publish task may be used to perform publish.

Publish to maven-central

For maven-central publication use nexus publish plugin which automates full maven central release cycle.

plugins {
  ...
  id 'io.github.gradle-nexus.publish-plugin' version '1.1.0'
}

nexusPublishing {
  repositories {
    sonatype {
      username = findProperty('sonatypeUser')
      password = findProperty('sonatypePassword')
    }
  }
}

For release, you would need to call two tasks: publishToSonatype, closeAndReleaseSonatypeStagingRepository

You'll need to configure sonatypeUser and sonatypePassword properties in global gradle file: ~/.gradle/gradle.properties

IMPORTANT artifacts must be signed!

Gradle plugin

Gradle plugin project will have java-gradle-plugin, which declares its own maven publication pluginMaven (with main jar as artifact). Also, plugin creates one more publication per declared plugin to publish plugin marker artifact (required by gradle plugins dsl).

Java-lib plugin will still create separate publication maven and you should use it for publishing with bintray (same way as for usual library)

Publishing to gradle plugin repository

For publishing in gradle plugin repository you will use com.gradle.plugin-publish plugin.

IMPORTANT: plugin-publish 1.x is supported for gradle 7.6 and above, for lower gradle use 0.x

Use maven publication for publishing into maven central or other repo (optional). Plugin-publish will use it's plugin-maven publication for plugins portal publication. Both publications would contain the same artifacts.

Example for publishing in maven central and plugin portal (gradle 7.6 or above):

plugins {
  id 'com.gradle.plugin-publish' version '1.2.1'
  id 'java-gradle-plugin'
  id 'ru.vyarus.java-lib' version '3.0.0'
}

repositories { mavenLocal(); mavenCentral(); gradlePluginPortal() }

group = 'com.foo'
description = 'Short description'

gradlePlugin {
  plugins {
    myPlugin {
      id = 'com.foo.plugin'
      displayName = project.description
      description = 'Long description'
      tags.set(['something'])
      implementationClass = 'com.foo.MyPlugin'
    }
  }
}

Here publishMavenPublicationToMavenRepository would publish to repository and publishPlugins publish into plugins portal.

Assuming custom maven (name!) repository is configured:

publishing {
  repositories { maven { url "http://some.repo/"} }
}
Publishing only to custom repo

This is in-house plugin case, when plugin is published only into corporate repository.

The simplest solution is to disable pluginMaven publication tasks (but marker artifact publications should remain!) and publish only remaining maven publication:

tasks.withType(AbstractPublishToMaven) { Task task ->
    if (task.name.startsWith("publishPluginMaven")) {
        task.enabled(false)
    }
}

This will disable: publishPluginMavenPublicationToMavenLocal and publishPluginMavenPublicationToMavenRepository

And you can simply use publish task to trigger all required publications without duplicates.

The same way, install will install all required artifacts locally (including markers) and so it is possible to use plugins from local maven repository too (with plugin syntax):

add to settings.gradle:

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
    }
}

Encodings

UTF-8 applied to:

Note that groovydoc task does not have encoding configuration, but it should use UTF-8 by defautl.

For tests, encoding is important (especially on windows) because test forked process will not inherit root gradle encoding configuration.

Tasks

NOTE: for gradle 7.6 and above native javadoc and sources registration used

install task added to simplify publication to local maven repository: this is simply shortcut for gradle's publishToMavenLocal task (simply shorter to type and more common name after maven).

Main Jar

Plugin applies default manifest properties:

'Implementation-Title': project.description ?: project.name,
'Implementation-Version': project.version,
'Built-By': System.getProperty('user.name'),
'Built-Date': new Date(),
'Built-JDK': System.getProperty('java.version'),
'Built-Gradle': gradle.gradleVersion,
'Target-JDK': project.targetCompatibility

You can override it:

jar {
    manifest {
        attributes 'Implementation-Title': 'My Custom value',
            'Built-By': 'Me'
    }
}

For all not specified properties default values will be used.

Plugin will include additional files inside jar (like maven do) into META-INF/maven/group/artifact/

pom.properties contains:

Signing

Plugin will configure signing automatically for configured publications: maven and bom (note that in case of gradle plugin, gradle use its own publication for portal publication and it would not be signed (no need)).

You only need to apply signing plugin:

plugins {
  id 'java'
  id 'signing'
  id 'ru.vyarus.java-lib'
}

No additional configuration required, except properties in the global gradle config ~/.gradle/gradle.properties:

signing.keyId = 78065050
signing.password =
signing.secretKeyRingFile = /path/to/certs.gpg

IMPORTANT: password property (empty) required even if no password used!

Note that project build will not complain while building snapshot versions (version ending with -SNAPSHOT) - signing task would be simply ignored. But, on release gradle would fail if signing not configured properly.

Signing certificate

Certificate generation described in many articles around the web, for example, sonatype gpg guide.

I will just show required commands for generation and obtaining keyring file:

Certificate generation:

gpg --gen-key

(if you want, you can leave passphrase blank - just hit enter several times)

Alternatively, gpg --full-gen-key may be used to set exact algorithm and expiration (by default generated key would expire in few years)

List keys:

gpg --list-keys
gpg --list-secret-keys

You can always edit key if required (for example change expiration):

gpg --edit-key (key id)
gpg> key 1
gpg> expire
(follow prompts)
gpg> save

Create keyring file:

gpg --export-secret-keys (key id) > cert.gpg

Put cert.gpg somewhere and set full path to it in signing.secretKeyRingFile

You also need short key id:

gpg --list-secret-keys --keyid-format SHORT

Example output:
  sec   rsa3072/78065050 2021-06-06 [SC]

Here 78065050 is your keyid which should be set as signing.keyId

If you set passphrase, set it in signing.password, otherwise leave it blank

IMPORTANT: for maven central, you'll need to register your public key with

gpg --keyserver keyserver.ubuntu.com --send-keys (short key id)

That's all.

Dependency report

When project-report plugin active, openDependencyReport task created.

This is pure utility task: it calls htmlDependencyReport and opens it directly in the browser (directly on page with dependencies, instead of index).

This simply faster: manual htmlDependencyReport requires several clicks to open required report.

Maven-like multi-module project

Here is an example of how plugin could be used in multi-module project to apply maven configuration style: root project manage all dependency versions.

plugins {
    id 'jacoco'
    id 'java-platform'
    id 'ru.vyarus.java-lib'
}

description = 'Maven-like project'

// dependency versions management
dependencies {
    api platform("ru.vyarus:dropwizard-guicey:$guicey")
    constraints {
        api 'com.h2database:h2:1.4.200'

        // add subprojects to BOM
        project.subprojects.each { api it }
    }
}

javaLib {
    aggregateReports()
    // publish root BOM as custom artifact
    bom {
        artifactId = 'sample-bom'
        description = 'Sample project BOM'
    }
  
    // OR disable BOM publication
    // withoutPublication()
}

// maven publication related configuration applied to all projects
allprojects {
    //apply plugin: 'project-report'
    //apply plugin: 'signing'

    repositories { mavenCentral(); mavenLocal() }

    group = 'com.test'
  
    // such delay is required because java-lib (and java) plugin would be applied only
    // in the subprojects section and so this would configure root project configuration
    // without delay
    plugins.withId('java') {
        maven.pom {
            licenses {
                license {
                    name = "The MIT License"
                    url = "http://www.opensource.org/licenses/MIT"
                    distribution = 'repo'
                }
            }
            scm {
                url = 'https://github.com/me/my-repo.git'
                connection = 'scm:git@github.com:me/my-repo.git'
                developerConnection = 'scm:git@github.com:me/my-repo.git'
            }
            //...
        }
    }  

    javaLib.withoutGradleMetadata()
}

// all sub-modules are normal java modules, using root BOM (like maven)
subprojects {
    apply plugin: 'groovy'
    apply plugin: 'jacoco'
    apply plugin: 'ru.vyarus.java-lib'

    sourceCompatibility = 1.8

    // common dependencies for all modules
    dependencies {
        implementation platform(project(':'))

        compileOnly 'com.github.spotbugs:spotbugs-annotations:4.2.3'
        implementation 'ru.vyarus:dropwizard-guicey'

        testImplementation 'org.spockframework:spock-core'
        testImplementation 'io.dropwizard:dropwizard-testing'
    }

    javaLib {
        // java 9 auto module name
        autoModuleName = "com.sample.module"
    }
  
    maven {
        // use only direct dependencies in the generated pom, removing BOM
        removeDependencyManagement()
    }
}

Here required dependency versions declared in the root project using gradle platform. Platform published as BOM with custom artifact name (dual BOM: both project modules and dependencies).

Sub-projects are java modules which use platform declared in the root project for dependency management. maven.removeDependencyManagement() prevents "leaking" platform into module poms (generated poms would contain just required dependencies with resolved versions)

groovy plugin used just as an example (used for spock tests, main sources might be java-only): it could be java or java-library plugin.

The complete multi-module project example could be generated with java-library generator.

APPENDIX: boilerplate plugin removes

Section briefly shows what plugin configures so if plugin defaults didn't fit your needs, you can easily reproduce parts of it in your custom build.

Java module boilerplate

plugins { id 'java' }

apply plugin: 'ru.vyarus.pom'

jar {    
    manifest {
        attributes 'Implementation-Title': project.description ?: project.name,
                'Implementation-Version': project.version,
                'Built-By': System.getProperty('user.name'),
                'Built-Date': new Date(),
                'Built-JDK': System.getProperty('java.version'),
                'Built-Gradle': gradle.gradleVersion,
                'Target-JDK': project.targetCompatibility
    }
}

java {
    withJavadocJar()
    withSourcesJar()
}        

task generatePomPropertiesFile {
    inputs.properties ([
            'version': "${ -> project.version }",
            'groupId': "${ -> project.group }",
            'artifactId': "${ -> project.name }"
    ])
    outputs.file "$project.buildDir/generatePomPropertiesFile/pom.properties"
    doLast {
        File file = outputs.files.singleFile
        file.parentFile.mkdirs()
        file << inputs.properties.collect{ key, value -> "$key: $value" }.join('\n')
    }
}

model {
    tasks.jar {
        into("META-INF/maven/$project.group/$project.name") {
            from generatePomFileForMavenPublication
            rename ".*.xml", "pom.xml"
            from generatePomPropertiesFile
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    it.options.encoding = StandardCharsets.UTF_8
}

tasks.withType(GroovyCompile).configureEach {
    it.options.encoding = StandardCharsets.UTF_8
}

tasks.withType(Test).configureEach {
   it.systemProperty JvmOptions.FILE_ENCODING_KEY, StandardCharsets.UTF_8
}

tasks.withType(Javadoc).configureEach {
  it.with {
    options.encoding = StandardCharsets.UTF_8
    // StandardJavadocDocletOptions
    options.charSet = StandardCharsets.UTF_8
    options.docEncoding = StandardCharsets.UTF_8
  }
}

jar.manifest {
  attributes 'Automatic-Module-Name': 'module-name'
}  

publishing.publications {
    maven(MavenPublication) {
        from components.java
    }
}

task.jacocoTestReport.xml.required.set(true)

tasks.register('install') {
    dependsOn: publishToMavenLocal 
    group: 'publishing'
    doLast {
        logger.warn "INSTALLED $project.group:$project.name:$project.version"
    }
}

Java platform boilerplate

plugins { id 'java-platform' }

apply plugin: 'ru.vyarus.pom'

javaPlatform.allowDependencies()

maven.pom {
  name = 'custom-name'                // if differs from project name
  description = 'custom description'
}

publishing.publications {
  bom(MavenPublication) {
    from components.javaPlatform
    artifactId = 'custom-name'      // if differs from project name
  }
}

jacocoTestReport.reports.xml.required.set(true)

tasks.register('install') {
  dependsOn: publishToMavenLocal
  group: 'publishing'
  doLast {
    logger.warn "INSTALLED $project.group:custom-name:$project.version"
  }
}

Reports aggregation boilerplate

task test (type: TestReport, description: 'Generates aggregated test report') {
    group = 'verification'
    destinationDir = project.file("${project.buildDir}/reports/tests/test")
    reportOn project.subprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.test
}

def projectsWithCoverage = project.subprojects.findAll { it.plugins.hasPlugin(JacocoPlugin) }

task jacocoTestReport (type: JacocoReport, description: 'Generates aggregated jacoco coverage report') {
    dependsOn 'test'
    group = 'verification'
    executionData project.files(projectsWithCoverage
            .collect { it.file("${it.buildDir}/jacoco/test.exec") })
            .filter { it.exists() }
    sourceDirectories.from = project.files(projectsWithCoverage.sourceSets.main.allSource.srcDirs)
    classDirectories.from = project.files(projectsWithCoverage.sourceSets.main.output)
    reports.xml.destination = project.file("$project.buildDir/reports/jacoco/test/jacocoTestReport.xml")
    reports.xml.required.set(true)
    reports.html.destination = project.file("$project.buildDir/reports/jacoco/test/html/")
}

htmlDependencyReport.projects = project.allprojects

Utility boilerplate

Signing:

signing {
  sign publishing.publications.maven    // or bom
  required = { !project.version.toString().endsWith('SNAPSHOT') }
}

Gradle metadata disabling

tasks.withType(GenerateModuleMetadata).configureEach {
    enabled = false
}

Open report:

task openDependencyReport(description: 'Opens gradle htmlDependencyReport in browser', group: 'help') {
  dependsOn 'htmlDependencyReport'
  doLast {
      java.awt.Desktop.desktop.open(file("build/reports/project/dependencies/root.${project.name}.html))
  }
}

Might also like


gradle plugin generator