Gradle convention plugins used by Hiero projects.
Any Gradle-based Java project that uses the Java Module System (JPMS) may use these convention plugins. That includes projects that are not part of the Hiero organisation. The conventions follow latest Gradle best practices and ensure compatibility with performance features such as the Remote Build Cache and the Configuration Cache. The convention plugins pull in a curated set of third-party Gradle plugins that also support these features and are compatible with the latest Gradle version.
- Using the Convention Plugins (for project maintainers)
- List of Convention Plugins (for project maintainers)
- Building a project that uses these plugins (for developers)
- Defining modules and dependencies in a project that uses these plugins (for developers)
Apply the entry point plugin org.hiero.gradle.build
in the settings.gradle.kts
file. Additionally, define where
Modules (subprojects) are located in the directory hierarchy by using the javaModules { ... }
notation
(which is provided by the org.gradlex.java-module-dependencies plugin).
// settings.gradle.kts
plugins {
id("org.hiero.gradle.build") version "0.3.1"
}
// Define location of Modules (subprojects)
javaModules {
directory("product-a") { // searches for 'module-info.java' in subfolders of 'product-a'
group = "org.example.product-a"
}
}
In each Module (subproject), apply one of the org.hiero.gradle.module.*
plugins and, if desired, additional
org.hiero.gradle.feature.*
plugins.
For example, to define a Library Module that also provides test fixtures and has JMH benchmarks, the plugins block should look like this:
plugins {
id("org.hiero.gradle.module.library")
id("org.hiero.gradle.feature.test-fixtures")
id("org.hiero.gradle.feature.benchmark")
}
There is a minimal example setup.
├── settings.gradle.kts // Entriy point (see above)
├── gradle.properties // Turn on Gradle caches
├── gradle/aggregation/build.gradle.kts // List of all product/service modules for consistent resolution
├── gradle/toolchain-versions.properties // JDK version (and other tools if applicable)
├── gradle/wrapper/gradle-wrapper.properties // Gradle version (defined through Gradle wrapper)
├── hiero-dependency-versions/build.gradle.kts // Versions of 3rd-party modules
├── product-a // Folder containing all modules of 'product-a'
│ ├── module-app // Example of a Application module
│ │ ├── build.gradle.kts // Select which build features to use in 'plugins {}' (see above)
│ │ └── src/main/java/module-info.java // Define dependencies to other modules
│ ├── module-lib // Example of a Library module
│ │ ├── build.gradle.kts // Select which build features to use in 'plugins {}' (see above)
│ │ └── src/main/java/module-info.java // Define dependencies to other modules
│ └── description.txt // Description of the product (for published metadata),
│ // or set 'description' for individual module in build.gradle.kts
└── version.txt // Version of all modules/products
The plugins are written in Gradle's Kotlin DSL and are found in src/main/kotlin. Each plugin has a short description located in src/main/descriptions.
Each plugin configures a certain build aspect, following this naming pattern:
org.hiero.gradle.base.*
Base plugins need to be used in all Modules to establish a certain foundation for the setup. For example, the same dependency management configuration should be applied everywhere to consistently use the same 3rd party libraries everywhere.org.hiero.gradle.feature.*
Each feature plugin configures one aspect of building the software – like compiling code or testing code.org.hiero.gradle.check.*
Check plugins help with keeping the software maintainable over time. They check things like the dependency setup or code formatting.org.hiero.gradle.report.*
Report plugins configure the export of information into reports that can be picked up by other systems - e.g. for code coverage reporting.org.hiero.gradle.module.*
Module plugins combine plugins from all categories above to define Module Types that are then used in thebuild.gradle.kts
files of the individual Modules of our software.
Run ./gradlw
to get the list of tasks that are useful to check and test local changes:
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
qualityGate - Apply spotless rules and run all quality checks.
test - Runs the test suite.
In addition, the following build parameters may be useful:
Task | Parameter | Description | Remarks |
---|---|---|---|
test |
-PactiveProcessorCount=<proc-number> |
not run tests in parallel and reduce number of processors used | |
jmh |
-PjmhTests=<includes> |
select benchmarks to run - e.g. com.example.jmh.Benchmark1 |
only projects with feature.benchmark |
Open the root folder of the project in IntelliJ. It is automatically recognized as Gradle project and imported.
Before you can use all features reliably, make sure that Gradle is started with the JDK used in the project.
The JDK version of the project is defined in gradle/toolchain-versions.properties
.
You can use IntelliJ to download the JDK if you do not have it installed.
After you changed something in the project setup you should press the Reload All Gradle project in IntelliJ. Changes to the project setup include:
- Changing build setup/plugins in
build.gradle.kts
files - Changing dependencies in
src/main/java/module-info.java
files - Changing dependency versions in
hiero-dependency-versions/build.gradle.kts
You can run all tasks (see command line) from the Gradle tool window. Usually, you only require the tasks listed in the build group.
To run only a single test, you can use the run test options offered by IntelliJ when you open a test file. IntelliJ then automatically constructs the required Gradle call to run the test through Gradle.
GitHub action pipelines should use the official setup-gradle action with the following tasks and parameters.
In a CI pipeline for PR validation with multiple steps use the following.
(The available test sets are determined by the additional feature.test-*
plugins used in the project.)
Task and Parameters | Description |
---|---|
./gradlew assemble |
Build all artifacts (populates remote build cache) |
./gradlew qualityCheck |
Run all checks except tests |
./gradlew test |
Run all unit tests |
./gradlew <test-set> |
Run all tests in test-set (possibly on different agents in parallel) |
Alternatively, if you are fine to do more in one pipeline step, you can use:
Task and Parameters | Description |
---|---|
./gradlew build |
assemble + qualiyCheck + test |
./gradlew <test-set> |
Run all tests in test-set |
The following environment variables should be populated from secrets to ensure a performant build.
Env Variable | Description |
---|---|
GRADLE_CACHE_USERNAME |
Gradle remote build cache username |
GRADLE_CACHE_PASSWORD |
Gradle remote build cache password |
Running test
produces code coverage data. The following creates a single XML file with all coverage data for upload
to coverage analysis services like Codecov.
Task and Parameters | Description |
---|---|
./gradlew testCodeCoverageReport |
Generate a single XML with all coverage data |
Report location: gradle/aggregation/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml
Before doing the publishing, you may need to update the version (version.txt file) in a preceding step.
Task and Parameters | Description |
---|---|
./gradlew versionAsSpecified -PnewVersion=<version> |
Define version to store in version.txt |
./gradlew versionAsSnapshot |
Add -SNAPSHOT suffix to version.txt |
./gradlew versionAsPrefixedCommit -PcommitPrefix=<prefix> |
Set version based on current commit hash |
To perform the actual publishing use one of the following.
(If multiple products with different groups should be published, the releaseMavenCentral
task needs to run
multiple times with different values for the publishingPackageGroup
parameter.)
Task and Parameters | Description |
---|---|
./gradlew releaseMavenCentral -PpublishingPackageGroup=<group> --no-configuration-cache |
Publish artifacts to Maven central |
./gradlew publishPlugins --no-configuration-cache |
Publish plugin to Gradle plugin portal |
The following parameters may be used to tune or test the publishing (default is false
for all parameters).
Task and Parameters | Description |
---|---|
-PpublishSigningEnabled=<true|false> |
Set to true for actual publishing |
-Ps01SonatypeHost=<true|false> |
Use the s01.oss.sonatype.org host if required |
-PpublishTestRelease=<true|false> |
false - auto-release from staging when successful |
The following environment variables should be populated from secrets to ensure a fully functional build.
Env Variable | Description |
---|---|
NEXUS_USERNAME |
Maven Central publish username |
NEXUS_PASSWORD |
Maven Central publish password |
GRADLE_PUBLISH_KEY |
Gradle plugin portal publish username |
GRADLE_PUBLISH_SECRET |
Gradle plugin portal publish password |
If feature.rust
and feature.test-multios
is used, you can configure a matrix pipeline to run test
on multiple
agents with different operating systems. In this case, you can use the following parameter to skip the rust installation
on the test-only agents where compiled code is retrieved from the remote build cache.
Parameters | Description |
---|---|
-PskipInstallRustToolchains=<true|false> |
Skip installRustToolchains task if all cargoBuild* tasks are FROM-CACHE |
The project structure endorsed by this setup uses the
Java Module System (JPMS)
as the primary system for defining modules and their dependencies. For a smooth integration of JPMS and Gradle's
dependency management, the org.gradlex.java-module-dependencies
plugin, and the additional notations it provides, are
utilised. Therefore, the means to define dependencies differ from traditional Gradle-base Java projects.
For more background information please refer to:
- This video series on Modularity in Java (with Gradle)
- The documentation of the
org.gradlex.java-module-dependencies
plugin
A module is defined by adding a build.gradle.kts and module-info.java in a module folder inside the folder that represents the product the module belongs to:
└── product-a
└── module-lib
├── build.gradle.kts
└── src/main/java/module-info.java
For Gradle to discover the module, the product directory needs to be registered in
settings.gradle.kts in the root of the repository by using the
notation provided by the org.gradlex.java-module-dependencies
plugin.
If you add a module to an existing product, this is already done. If you are starting a new product, you have
to add the entry and define the group for all modules of the product. The group is used in publishing modules
to Maven Central.
javaModules {
directory("product") { group = "org.example.product" }
}
In the build.gradle.kts file, you define the type of the module by using a Module convention plugin.
plugins { id("org.hiero.gradle.module.library") }
There are currently three types:
- Library Module You most likely need this: a reusable Java library that can be published
- Application Module An application that can be executed (for example a test application)
- Gradle Plugin Module A Gradle plugin
If you need to support additional features for the developing of the module, you may add additional Feature convention plugins.
With the Java Module System (JPMS), dependencies between modules are
defined in the src/main/java/module-info.java file
that each module contains. A dependency to another module is defined by a requires
statement and the other module
is identified by its Module Name there. For example, a dependency to the module-a
module is expressed by
requires org.example.module.a
. A dependency to the 3rd party library com.fasterxml.jackson.databind
is expressed by
requires com.fasterxml.jackson.databind
.
Each dependency definition contains a scope
– e.g. requires
or requires transitive
. If you are unsure about a scope, use requires
when adding a dependency.
Then execute ./gradlew qualityGate
which runs a dependency scope check that analysis the code to determine which
Java types are visible (and should be visible) to which modules. If the check fails, it will advise you how to change
the scope.
In addition to the production code of a module (located in src/main/java),
your module will most likely also contain test code (located in src/test/java).
From the JPMS perspective, the test code is a separate module, with its own src/test/java/module-info.java
file.
If possible, you can add this file and use it to define dependencies for the test code.
However, it is not possible to treat tests as separate module if they break the encapsulation of the main module. This is the case if the tests need access to internals (like protected methods) and are therefore placed in the same Java package as the main code. This is also referred to as whitebox testing.
For such a setup you may omit the additional module-info.java
. The tests are then executed as traditional
Java tests without enforcing module encapsulation at test runtime. To still keep the dependency notations consistent,
you define requires
of the test code in the build.gradle.kts file.
testModuleInfo {
requires("org.junit.jupiter.api")
}
A module may also define more test sets, like src/testIntgration/java
, by adding the corresponding
Feature convention plugin (e.g. id("org.hiero.gradle.feature.test-integration")
).
It is recommended to treat such tests as separate modules in the JPMS sense (blackbox tests) by adding a separate
module-info.java
. But it is also possible to not do that and define the requires
in the build.gradle.kts
file.
If you use a 3rd party module lke com.fasterxml.jackson.databind
, a version for that module needs to
be selected. For this, the
hiero-dependency-versions/build.gradle.kts
defines a so-called Gradle platform (also called BOM) that contains the versions of all 3rd party
modules used. If you want to upgrade the version of a module, do this here. Remember to run
./gradlew qualityGate
after the change. If you need to use a new 3rd party module in a
src/main/java/module-info.java file, you need to
add the version here.
(If the new module is not completely Java Module System compatible, you may also need to add or modify
patching rules).
Some 3rd party libraries are not yet fully Java Module System (JPMS) compatible. And sometimes 3rd party modules pull in other modules that are not yet fully compatible (which we may be able to exclude). Situations like this are treated as wrong/incomplete metadata in this Gradle setup and the file org.hiero.gradle.base.jpms-modules.gradle.kts contains the rules to adjust or extend the metadata of 3rd party libraries to address such problems.
An issue in this area only occurs when you add a new 3rd party module that is not fully compatible with JPMS or if you update an existing dependency such that the update pulls in a new 3rd party module that is not fully compatible. You will see an error like this:
> Failed to transform javax.inject-1.jar (javax.inject:javax.inject:1) to match attributes {artifactType=jar, javaModule=true, org.gradle.category=library, org.gradle.libraryelements=jar, org.gradle.status=release, org.gradle.usage=java-api}.
> Execution failed for ExtraJavaModuleInfoTransform: /Users/jendrik/projects/gradle/customers/hiero/hiero-gradle-conventions/build/tmp/test/work/.gradle-test-kit/caches/modules-2/files-2.1/javax.inject/javax.inject/1/6975da39a7040257bd51d21a231b76c915872d38/javax.inject-1.jar.
> Not a module and no mapping defined: javax.inject-1.jar
In these cases, first determine if adding the new 3rd party module is really needed/intended. If it is the case, only an update to these plugins can resolve the issue permanently. Follow the guidelines to make local changes to the plugins. Then modify the rules in org.hiero.gradle.base.jpms-modules.gradle.kts as needed. There are two levels of patching:
- Add missing
module-info.class
: This is done through theorg.gradlex.extra-java-module-info
plugin. Often it is sufficient to add a simple entry for the affected library. For example, to address the error above, you can addmodule("javax.inject:javax.inject", "javax.inject")
to theextraJavaModuleInfo
block. For more details, refer to the org.gradlex.extra-java-module-info plugin documentation. - Adjust metadata (POM file) of dependency:
This is required to solve more severe issues with the metadata of a library using the Gradle concept of
Component Metadata Rules. For a convenient definition of such rules, we use the
patch
notation provided by theorg.gradlex.jvm-dependency-conflict-resolution
plugin. For more details, refer to the org.gradlex.jvm-dependency-conflict-resolution plugin documentation.
If you have a solution that works locally, please open a PR. If you are unsure how to resolve an error, please open an issue that shows how to reproduce it.
Whether you’re fixing bugs, enhancing features, or improving documentation, your contributions are important — let’s build something great together!
Please read our contributing guide to see how you can get involved.
Insert the line
pluginManagement { includeBuild("<path-to-hiero-gradle-conventions>") }
in the top of your settings.gradle.kts
. For example, if this repository is cloned next to the project repository in
your local file system, the top part of your settings.gradle.kts
should look like this:
// SPDX-License-Identifier: Apache-2.0
pluginManagement { includeBuild("../hiero-gradle-conventions") }
plugins { id("org.hiero.gradle.build") version "0.3.1" }
After you inserted that line, reload your project in IntelliJ. You will now see hiero-gradle-conventions
next to your project in the workspace. You can now make changes to the files in src/main/kotlin.
Your changes are automatically used when running a build.
Each change done to the plugins should be covered by a test. The tests are located in src/test/kotlin. They are written in Kotlin using JUnit5, AssertJ and Gradle Test Kit. Each test creates an artificial project that applies the plugin(s) under test, runs a build and asserts build results – such as: state of tasks executed, console logging, created files. Take a look at the existing tests for more details.
Hiero uses the Linux Foundation Decentralised Trust Code of Conduct.