Skip to content

Commit

Permalink
Introduce Java 9 module compatibility (#5543)
Browse files Browse the repository at this point in the history
Motivation:

Some users are having issues with using Armeria as a Java 9 module
because:

- Armeria core JAR contains `module-info.class` from Bouncy Castle,
which should be removed during the shading process.
- Armeria doesn't define module metadata at all

Modifications:

- Updated `gradle-scripts` to exclude `module-info.class` while shading.
- This removes Bouncy Castle's `module-info.class` from the final JAR,
fixing the issue mentioned in the previous section.
- Updated `gradle-scripts` to auto-inject the `Automatic-Module-Name`
property into `META-INF/MANIFEST.MF` so that JVM auto-generates the
module metadata.
- Miscellaneous:
- Overwrote the `Created-By` property in `META-INF/MANIFEST.MF` so that
the manifest file's content doesn't change depending on JDK version.

Result:

- (new feature) Armeria JARs are now Java modules.

---------

Co-authored-by: minux <minu.song@linecorp.com>
  • Loading branch information
trustin and minwoox authored Apr 5, 2024
1 parent a36344d commit b1a1044
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 14 deletions.
10 changes: 8 additions & 2 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,23 @@ tasks.withType(Jar) {
tasks.trimShadedJar.doLast {
// outjars is a file, so only one jar generated for sure
def trimmed = tasks.trimShadedJar.outJarFiles[0].toPath()
ant.jar(destfile: trimmed.toString(), update: true) {

ant.jar(destfile: trimmed.toString(), update: true, duplicate: 'fail') {
zipfileset(src: tasks.shadedJar.archivePath) {
include(name: 'META-INF/versions/**')
}

// Do not let Ant put the properties that harm the build reproducibility, such as JDK version.
delegate.manifest {
attribute(name: 'Created-By', value: "Gradle ${gradle.gradleVersion}")
}
}
}

tasks.shadedTest.exclude 'META-INF/versions/**'

dependencies {
mrJarVersions.each { version->
mrJarVersions.each { version ->
// Common to reference classes in main sourceset from Java 9 one (e.g., to return a common interface)
"java${version}Implementation" files(sourceSets.main.output.classesDirs) { builtBy compileJava }

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ publishUrlForSnapshot=https://oss.sonatype.org/content/repositories/snapshots/
publishUsernameProperty=ossrhUsername
publishPasswordProperty=ossrhPassword
publishSignatureRequired=true
automaticModuleNames=true

# Gradle options
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError
Expand Down
28 changes: 28 additions & 0 deletions gradle/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ sensible defaults. By applying them, you can:
- [Shading a multi-module project with `relocate` flag](#shading-a-multi-module-project-with-relocate-flag)
- [Setting a Java target version with the `java(\\d+)` flag](#setting-a-java-target-version-with-the-javad-flag)
- [Setting a Kotlin target version with the `kotlin(\\d+\\.\\d+)` flag](#setting-a-koltin-target-version-with-the-kotlindd-flag)
- [Automatic module names](#automatic-module-names)
- [Tagging conveniently with `release` task](#tagging-conveniently-with-release-task)

<!-- /MarkdownTOC -->
Expand Down Expand Up @@ -98,6 +99,7 @@ sensible defaults. By applying them, you can:
```
group=com.doe.john.myexample
version=0.0.1-SNAPSHOT
versionPattern=^[0-9]+\\.[0-9]+\\.[0-9]+$
projectName=My Example
projectUrl=https://www.example.com/
projectDescription=My example project
Expand All @@ -118,6 +120,7 @@ sensible defaults. By applying them, you can:
googleAnalyticsId=UA-XXXXXXXX
javaSourceCompatibility=1.8
javaTargetCompatibility=1.8
automaticModuleNames=false
```

5. That's all. You now have two Java subprojects with sensible defaults.
Expand Down Expand Up @@ -672,6 +675,31 @@ However, if you want to compile a Kotlin module with a different language versio

For example, `kotlin1.6` flag makes your Kotlin module compatible with language version 1.6 and API version 1.6.

## Automatic module names

By specifying the `automaticModuleNames=true` property in `settings.gradle`, every `java` project's JAR
file will contain the `Automatic-Module-Name` property in its `MANIFEST.MF`, auto-generated from the group ID
and artifact ID. For example:

- groupId: `com.example`, artifactId: `foo-bar`
- module name: `com.example.foo.bar`
- groupId: `com.example.foo`, artifactId: `foo-bar`
- module name: `com.example.foo.bar`

If enabled, each project with `java` flag will have the `automaticModuleName` property.

You can override the automatic module name of a certain project via the `automaticModuleNameOverrides`
extension property:

```groovy
ext {
// Change the automatic module name of project ':bar' to 'com.example.fubar'.
automaticModuleNameOverrides = [
':bar': 'com.example.fubar'
]
}
```

## Tagging conveniently with `release` task

The task called `release` is added at the top level project. It will update the
Expand Down
60 changes: 49 additions & 11 deletions gradle/scripts/lib/common-info.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,9 @@ allprojects {
ext {
artifactId = {
// Use the overridden one if available.
if (rootProject.ext.has('artifactIdOverrides')) {
def overrides = rootProject.ext.artifactIdOverrides
if (!(overrides instanceof Map)) {
throw new IllegalStateException("artifactIdOverrides must be a Map: ${overrides}")
}

for (Map.Entry<String, String> e : overrides.entrySet()) {
if (rootProject.project(e.key) == project) {
return e.value
}
}
def overriddenArtifactId = findOverridden('artifactIdOverrides', project)
if (overriddenArtifactId != null) {
return overriddenArtifactId
}

// Generate from the project names otherwise.
Expand All @@ -70,3 +62,49 @@ allprojects {
}.call()
}
}

// Check whether to enable automatic module names.
def isAutomaticModuleNameEnabled = 'true' == rootProject.findProperty('automaticModuleNames')

allprojects {
ext {
automaticModuleName = {
if (!isAutomaticModuleNameEnabled) {
return null
}

// Use the overridden one if available.
def overriddenAutomaticModuleName = findOverridden('automaticModuleNameOverrides', project)
if (overriddenAutomaticModuleName != null) {
return overriddenAutomaticModuleName
}

// Generate from the groupId and artifactId otherwise.
def groupIdComponents = String.valueOf(rootProject.group).split("\\.").toList()
def artifactIdComponents =
String.valueOf(project.ext.artifactId).replace('-', '.').split("\\.").toList()
if (groupIdComponents.last() == artifactIdComponents.first()) {
return String.join('.', groupIdComponents + artifactIdComponents.drop(1))
} else {
return String.join('.', groupIdComponents + artifactIdComponents)
}
}.call()
}
}

def findOverridden(String overridesPropertyName, Project project) {
if (rootProject.ext.has(overridesPropertyName)) {
def overrides = rootProject.ext.get(overridesPropertyName)
if (!(overrides instanceof Map)) {
throw new IllegalStateException("rootProject.ext.${overridesPropertyName} must be a Map: ${overrides}")
}

for (Map.Entry<String, String> e : overrides.entrySet()) {
if (rootProject.project(e.key) == project) {
return String.valueOf(e.value)
}
}
}

return null
}
11 changes: 11 additions & 0 deletions gradle/scripts/lib/java-shade.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,23 @@ configure(relocatedProjects) {
configureShadowTask(project, delegate, true)
archiveBaseName.set("${project.archivesBaseName}-shaded")

// Exclude the legacy file listing.
exclude '/META-INF/INDEX.LIST'
// Exclude the class signature files.
exclude '/META-INF/*.SF'
exclude '/META-INF/*.DSA'
exclude '/META-INF/*.RSA'
// Exclude the files generated by Maven
exclude '/META-INF/maven/**'
// Exclude the module metadata that'll become invalid after relocation.
exclude '**/module-info.class'

// Set the 'Automatic-Module-Name' property in MANIFEST.MF.
if (project.ext.automaticModuleName != null) {
manifest {
attributes('Automatic-Module-Name': project.ext.automaticModuleName)
}
}
}
tasks.assemble.dependsOn tasks.shadedJar

Expand Down
16 changes: 16 additions & 0 deletions gradle/scripts/lib/java.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import java.util.regex.Pattern

// Determine which version of JDK should be used for builds.
def buildJdkVersion = Integer.parseInt(JavaVersion.current().getMajorVersion())
if (rootProject.hasProperty('buildJdkVersion')) {
def jdkVersion = Integer.parseInt(String.valueOf(rootProject.findProperty('buildJdkVersion')))
Expand Down Expand Up @@ -139,6 +140,12 @@ configure(projectsWithFlags('java')) {
registerFeature('optional') {
usingSourceSet(sourceSets.main)
}

// Do not let Gradle infer the module path if automatic module name is enabled,
// because it means the JAR will rely on JDK's automatic module metadata generation.
if (project.ext.automaticModuleName != null) {
modularity.inferModulePath = false
}
}

// Set the sensible compiler options.
Expand All @@ -154,6 +161,15 @@ configure(projectsWithFlags('java')) {
options.compilerArgs += '-parameters'
}

// Set the 'Automatic-Module-Name' property in 'MANIFEST.MF' if `automaticModuleName` is not null.
if (project.ext.automaticModuleName != null) {
tasks.named('jar') {
manifest {
attributes('Automatic-Module-Name': project.ext.automaticModuleName)
}
}
}

project.ext.configureFlakyTests = { Test testTask ->
def flakyTests = rootProject.findProperty('flakyTests')
if (flakyTests == 'true') {
Expand Down
10 changes: 9 additions & 1 deletion gradle/scripts/lib/prerequisite.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ plugins {
''')
}

['projectName', 'projectUrl', 'inceptionYear', 'licenseName', 'licenseUrl', 'scmUrl', 'scmConnection',
['group', 'version', 'projectName', 'projectUrl', 'inceptionYear', 'licenseName', 'licenseUrl', 'scmUrl', 'scmConnection',
'scmDeveloperConnection', 'publishUrlForRelease', 'publishUrlForSnapshot', 'publishUsernameProperty',
'publishPasswordProperty'].each {
if (rootProject.findProperty(it) == null) {
throw new IllegalStateException('''Add project info properties to gradle.properties:
group=com.doe.john.myexample
version=0.0.1-SNAPSHOT
versionPattern=^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$
projectName=My Example
projectUrl=https://www.example.com/
projectDescription=My example project
Expand All @@ -31,7 +34,12 @@ publishUrlForRelease=https://oss.sonatype.org/service/local/staging/deploy/maven
publishUrlForSnapshot=https://oss.sonatype.org/content/repositories/snapshots/
publishUsernameProperty=ossrhUsername
publishPasswordProperty=ossrhPassword
publishSignatureRequired=true
versionPattern=^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$
googleAnalyticsId=UA-XXXXXXXX
javaSourceCompatibility=1.8
javaTargetCompatibility=1.8
automaticModuleNames=false
''')
}
}

0 comments on commit b1a1044

Please sign in to comment.