Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add developer docs about modules and dependencies #83

Merged
merged 3 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 157 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ that also support these features and are compatible with the latest Gradle versi
- [From the command line](#from-the-command-line)
- [In IntelliJ](#in-intellij)
- [In GitHub Actions](#in-github-actions)
- [Defining modules and dependencies in a project that uses these plugins](#defining-modules-and-dependencies-in-a-project-that-uses-these-plugins) (for developers)

<a name="plugins"></a>

## Using the Convention Plugins

Apply the entry point plugin `org.hiero.gradle.build` in the `settings.gradle.kts` file. Additionally, define where
Expand Down Expand Up @@ -101,6 +103,7 @@ Each plugin configures a certain build aspect, following this naming pattern:
_Module Types_ that are then used in the `build.gradle.kts` files of the individual Modules of our software.

<a name="build"></a>

## Building a project that uses these plugins

## From the command line
Expand Down Expand Up @@ -244,10 +247,163 @@ If `feature.rust` and `feature.test-multios` is used, you can configure a matrix
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.

| Env Variable | Description |
| Parameters | Description |
|---------------------------------------------|-----------------------------------------------------------------------------|
| `-PskipInstallRustToolchains=<true\|false>` | Skip `installRustToolchains` task if all `cargoBuild*` tasks are FROM-CACHE |

<a name="modules"></a>

## Defining modules and dependencies in a project that uses these plugins

The [project structure](#project-structure) endorsed by this setup uses the
[Java Module System (JPMS)](https://www.oracle.com/corporate/features/understanding-java-9-modules.html)
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)](https://www.youtube.com/playlist?list=PLWQK2ZdV4Yl092zlY7Dy1knCmi0jhTH3H)
- The [documentation of the `org.gradlex.java-module-dependencies` plugin](https://github.com/gradlex-org/java-module-dependencies)

### Changing or adding modules (aka Gradle subprojects)

A module is defined by adding a
[build.gradle.kts](example/product-a/module-lib/build.gradle.kts)
and [module-info.java](example/product-a/module-lib/src/main/java/module-info.java)
in a [module](example/product-a/module-lib) folder inside the folder that represents the
[product](example/product-a) 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](example/settings.gradle.kts) in the root of the repository by using the
[notation provided by the `org.gradlex.java-module-dependencies` plugin](https://github.com/gradlex-org/java-module-dependencies?tab=readme-ov-file#project-structure-definition-when-using-this-plugin-as-settings-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](example/product-a/module-lib/build.gradle.kts) file, you define the type of the module by
using a [_Module_ convention plugin](#list-of-convention-plugins).

```
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](#list-of-convention-plugins).

### Changing or adding dependencies

With the _Java Module System (JPMS)_, dependencies between modules are
defined in the [src/main/java/module-info.java](example/product-a/module-lib/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](https://docs.gradle.org/current/userguide/java_library_plugin.html#declaring_module_dependencies)
– 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.

#### Adding or changing dependencies if module-info.java is missing

In addition to the production code of a module (located in [src/main/java](example/product-a/module-lib/src/main/java)),
your module will most likely also contain test code (located in [src/test/java](example/product-a/module-lib/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](example/product-a/module-lib/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](#list-of-convention-plugins) (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.

### Adding or changing the version of a 3rd party dependency

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](example/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](example/product-a/module-lib/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](#patching-3rd-party-modules)).

### Patching 3rd party modules

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](src/main/kotlin/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](#use-local-changes-to-plugins-in-a-project).
Then modify the rules in
[org.hiero.gradle.base.jpms-modules.gradle.kts](src/main/kotlin/org.hiero.gradle.base.jpms-modules.gradle.kts)
as needed. There are two levels of patching:

1. Add missing `module-info.class`:
This is done through the `org.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 add `module("javax.inject:javax.inject", "javax.inject")` to the `extraJavaModuleInfo` block.
For more details, refer to the
[org.gradlex.extra-java-module-info plugin documentation](https://github.com/gradlex-org/extra-java-module-info).
2. 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 the
`org.gradlex.jvm-dependency-conflict-resolution` plugin.
For more details, refer to the
[org.gradlex.jvm-dependency-conflict-resolution plugin documentation](https://gradlex.org/jvm-dependency-conflict-resolution/#patch-dsl-block).

If you have a solution that works locally, please [open a PR](https://github.com/hiero-ledger/hiero-gradle-conventions/pulls).
If you are unsure how to resolve an error, please [open an issue](https://github.com/hiero-ledger/hiero-gradle-conventions/issues/new)
that shows how to reproduce it.

## Contributing

Whether you’re fixing bugs, enhancing features, or improving documentation, your contributions are important — let’s build something great together!
Expand Down
2 changes: 1 addition & 1 deletion example/gradle/toolchain-versions.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
jdk=17.0.12
jdk=17.0.13
4 changes: 4 additions & 0 deletions example/hiero-dependency-versions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
plugins {
id("org.hiero.gradle.base.lifecycle")
id("org.hiero.gradle.base.jpms-modules")
id("org.hiero.gradle.check.spotless")
id("org.hiero.gradle.check.spotless-kotlin")
}

dependencies.constraints {
api("com.fasterxml.jackson.core:jackson-databind:2.16.0") {
because("com.fasterxml.jackson.databind")
}

api("org.junit.jupiter:junit-jupiter-api:5.10.2") { because("org.junit.jupiter.api") }
}
2 changes: 2 additions & 0 deletions example/product-a/module-lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
plugins { id("org.hiero.gradle.module.library") }

testModuleInfo { requires("org.junit.jupiter.api") }
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

public class ModuleLib {
public void use() {
internalImplementation();
}

protected void internalImplementation() {
new ObjectMapper();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
package org.hiero.product.module.lib;

import org.junit.jupiter.api.Test;

class ModuleLibTest {

@Test
void testModule() {
new ModuleLib().internalImplementation();
}
}
2 changes: 1 addition & 1 deletion example/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
plugins { id("org.hiero.gradle.build") version "0.1.0" }
plugins { id("org.hiero.gradle.build") version "0.3.0" }

rootProject.name = "example-repository"

Expand Down