#14: buildlogic.pf-module-conventions.gradle.kts

buildSrc/src/main/kotlin/buildlogic.pf-module-conventions.gradle.kts

Path: ./buildSrc/src/main/kotlin/buildlogic.pf-module-conventions.gradle.kts

Type: Precompiled Gradle convention plugin (Kotlin DSL)

Lines: 64

Purpose: Shared build configuration applied to every ProjectForge module — Java 17, JUnit 5, dependency version pinning, repositories

Applied in: Each subproject's build.gradle.kts via plugins { id("buildlogic.pf-module-conventions") }

Source: GitHub

64 lines · 50 code · 4 comments · 10 blank

What it does

This is the heart of ProjectForge's build system. Every module (projectforge-business, projectforge-webapp, etc.) applies this plugin with one line, and gets:

Line-by-line

Lines 1–3: Base plugin

1 plugins {
2     id("java-library")
3 }

java-library extends java with api vs implementation separation. When module A applies this plugin and uses api() to declare a dependency, that dependency leaks to consumers of module A. This matters for the plugin system: plugins depend on ProjectForge modules via api dependencies.

Lines 5–11: Repositories

 5 repositories {
 6     mavenLocal()
 7     gradlePluginPortal()
 8     maven { url = uri("https://oss.sonatype.org/content/repositories/public/") }
 9     maven { url = uri("https://repo.maven.apache.org/maven2/") }
10     maven { url = uri("https://raw.githubusercontent.com/gephi/gephi/mvn-thirdparty-repo/") }
11 }

Five repository sources, searched in order:

LineRepositoryPurpose
6mavenLocal()Local ~/.m2/repository — for locally built artifacts during development
7gradlePluginPortal()Gradle plugins — Spring Boot plugin, etc.
8Sonatype OSSSnapshot and staging releases
9Maven2Standard Maven repository mirror
10gephi/gephi GitHubGephi — graph visualization library used by ProjectForge for network diagrams

Lines 13–18: Version catalog workaround

13 val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
14 
15 // libs.versions etc. not available in buildSrc. Must use findVersion and findLibrary instead.
16 
17 group = "org.projectforge"
18 version = libs.findVersion("org.projectforge").get().requiredVersion
⚠️ Key limitation: The libs.versions.xxx shorthand that works in regular build.gradle.kts files does NOT work inside buildSrc/ convention plugins. Instead, you must use findVersion() and findLibrary() as shown on line 13 and throughout this file.

Line 13: Gets the version catalog extension by type — the only way to access it in buildSrc.

Line 17: Sets group = "org.projectforge" — identical to the root build.gradle.kts line 8. Duplicated here because buildSrc is a separate project.

Line 18: Reads the version from the catalog (libs.versions.toml) — no hardcoded version number.

Lines 20–29: Java 17 configuration

20 extensions.configure<JavaPluginExtension> {
21     sourceCompatibility = JavaVersion.VERSION_17
22     targetCompatibility = JavaVersion.VERSION_17
23 }
24 
25 tasks.withType<JavaCompile> {
26     options.encoding = "UTF-8"
27     options.isIncremental = true
28     options.release.set(17)
29 }

Lines 21–22: sourceCompatibility and targetCompatibility — tell Gradle which Java version the source code uses (17) and which bytecode version to produce (17). These control language features and compilation output.

Line 28: options.release.set(17) — the --release 17 compiler flag. This is stronger than sourceCompatibility alone — it restricts the compiler to only Java 17 API symbols. Without this, building on JDK 21 could produce bytecode referencing Java 18+ APIs that don't exist on JDK 17, causing UnsupportedClassVersionError at runtime.

Why Java 17? It's the current LTS version supported by Spring Boot 3.x (required minimum). ProjectForge uses Kotlin which compiles to JVM bytecode, so the JDK version matters for both languages.

Lines 30–35: Kotlin compilation (commented out)

30 /*
31 tasks.withType<KotlinCompile> {
32     compilerOptions {
33         jvmTarget.set(JvmTarget.JVM_17)
34     }
35 }*/

Kotlin JVM target 17 — currently commented out. Kotlin compiles to 1.8 (Java 8) bytecode by default, which is compatible with Java 17. The file uses options.release = 17 on line 28 for Java files. The Kotlin equivalent would be uncommented when the project explicitly needs Java 17 Kotlin bytecode.

Lines 37–39: JUnit 5

37 tasks.withType<Test> {
38     useJUnitPlatform()
39 }

Enables JUnit 5 (Jupiter) for all test tasks across all modules. Without this, Gradle defaults to JUnit 4. ProjectForge migrated from mixed TestNG + JUnit4 to pure JUnit5 in commits like 20e07f9d1.

Lines 45–56: Jackson version forcing

45 configurations.all {
46     resolutionStrategy {
47         preferProjectModules()
48         // Force all Jackson module versions to match project's explicit Jackson version.
49         // Transitive dependencies (e.g. ez-vcard, groovy-yaml, flyway) may pull in newer Jackson versions.
50         val jacksonVersion = libs.findVersion("com.fasterxml.jackson").get().requiredVersion
51         force("com.fasterxml.jackson:jackson-bom:$jacksonVersion")
52         force("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
53         force("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
54         force("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
55     }
56 }

This is the most important section for build stability. Jackson is a transitive dependency of many libraries (ez-vcard, groovy-yaml, flyway). Each library may pull a different Jackson version, leading to runtime NoSuchMethodError or ClassNotFoundException.

How force() works: Gradle's dependency resolution collects all requested versions of Jackson across the entire dependency tree. The force() directive overrides any version — regardless of what any library requests, Jackson will be pinned to the version from libs.versions.toml.

Why four artifacts: Jackson is split into modules (jackson-core, jackson-databind, jackson-annotations) that must be version-synced. The jackson-bom (Bill of Materials) provides coordinated versions, but force() on individual artifacts is more explicit.

Without this section: Different subprojects could compile against different Jackson versions — the fat JAR would contain multiple Jackson JARs, and the classloader would pick whichever it finds first. This manifests as mysterious serialisation errors in production.

Lines 58–64: Shared dependencies

58 dependencies {
59     api(libs.findLibrary("org-jetbrains-kotlin-stdlib").get())
60     api(libs.findLibrary("io-github-microutils-kotlin-logging").get())
61     testImplementation(libs.findLibrary("org-junit-jupiter-api").get())
62     testImplementation(libs.findLibrary("org-junit-jupiter-engine").get())
63     testImplementation(libs.findLibrary("org-junit-platform-launcher").get())
64 }

Five dependencies applied to every module:

DependencyScopePurpose
kotlin-stdlibapiKotlin standard library — all modules use Kotlin
kotlin-loggingapiKotlin wrapper around SLF4J — microutils/kotlin-logging
junit-jupiter-apitestImplementationJUnit 5 test annotations (@Test, @BeforeEach)
junit-jupiter-enginetestImplementationJUnit 5 test execution engine
junit-platform-launchertestImplementationIDE/test runner integration

Key takeaways

#14: buildlogic.pf-module-conventions.gradle.kts

Path: ./buildSrc/src/main/kotlin/buildlogic.pf-module-conventions.gradle.kts

Type: Precompiled Gradle convention plugin (Kotlin DSL)

Lines: 64

Purpose: Shared build configuration applied to every ProjectForge module — Java 17, JUnit 5, dependency version pinning, repositories

Applied in: Each subproject's build.gradle.kts via plugins { id("buildlogic.pf-module-conventions") }

Source: GitHub

What it does

This is the heart of ProjectForge's build system. Every module (projectforge-business, projectforge-webapp, etc.) applies this plugin with one line, and gets:

Line-by-line

Lines 1–3: Base plugin

1 plugins {
2     id("java-library")
3 }

java-library extends java with api vs implementation separation. When module A applies this plugin and uses api() to declare a dependency, that dependency leaks to consumers of module A. This matters for the plugin system: plugins depend on ProjectForge modules via api dependencies.

Lines 5–11: Repositories

 5 repositories {
 6     mavenLocal()
 7     gradlePluginPortal()
 8     maven { url = uri("https://oss.sonatype.org/content/repositories/public/") }
 9     maven { url = uri("https://repo.maven.apache.org/maven2/") }
10     maven { url = uri("https://raw.githubusercontent.com/gephi/gephi/mvn-thirdparty-repo/") }
11 }

Five repository sources, searched in order:

LineRepositoryPurpose
6mavenLocal()Local ~/.m2/repository — for locally built artifacts during development
7gradlePluginPortal()Gradle plugins — Spring Boot plugin, etc.
8Sonatype OSSSnapshot and staging releases
9Maven2Standard Maven repository mirror
10gephi/gephi GitHubGephi — graph visualization library used by ProjectForge for network diagrams

Lines 13–18: Version catalog workaround

13 val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
14 
15 // libs.versions etc. not available in buildSrc. Must use findVersion and findLibrary instead.
16 
17 group = "org.projectforge"
18 version = libs.findVersion("org.projectforge").get().requiredVersion
⚠️ Key limitation: The libs.versions.xxx shorthand that works in regular build.gradle.kts files does NOT work inside buildSrc/ convention plugins. Instead, you must use findVersion() and findLibrary() as shown on line 13 and throughout this file.

Line 13: Gets the version catalog extension by type — the only way to access it in buildSrc.

Line 17: Sets group = "org.projectforge" — identical to the root build.gradle.kts line 8. Duplicated here because buildSrc is a separate project.

Line 18: Reads the version from the catalog (libs.versions.toml) — no hardcoded version number.

Lines 20–29: Java 17 configuration

20 extensions.configure<JavaPluginExtension> {
21     sourceCompatibility = JavaVersion.VERSION_17
22     targetCompatibility = JavaVersion.VERSION_17
23 }
24 
25 tasks.withType<JavaCompile> {
26     options.encoding = "UTF-8"
27     options.isIncremental = true
28     options.release.set(17)
29 }

Lines 21–22: sourceCompatibility and targetCompatibility — tell Gradle which Java version the source code uses (17) and which bytecode version to produce (17). These control language features and compilation output.

Line 28: options.release.set(17) — the --release 17 compiler flag. This is stronger than sourceCompatibility alone — it restricts the compiler to only Java 17 API symbols. Without this, building on JDK 21 could produce bytecode referencing Java 18+ APIs that don't exist on JDK 17, causing UnsupportedClassVersionError at runtime.

Why Java 17? It's the current LTS version supported by Spring Boot 3.x (required minimum). ProjectForge uses Kotlin which compiles to JVM bytecode, so the JDK version matters for both languages.

Lines 30–35: Kotlin compilation (commented out)

30 /*
31 tasks.withType<KotlinCompile> {
32     compilerOptions {
33         jvmTarget.set(JvmTarget.JVM_17)
34     }
35 }*/

Kotlin JVM target 17 — currently commented out. Kotlin compiles to 1.8 (Java 8) bytecode by default, which is compatible with Java 17. The file uses options.release = 17 on line 28 for Java files. The Kotlin equivalent would be uncommented when the project explicitly needs Java 17 Kotlin bytecode.

Lines 37–39: JUnit 5

37 tasks.withType<Test> {
38     useJUnitPlatform()
39 }

Enables JUnit 5 (Jupiter) for all test tasks across all modules. Without this, Gradle defaults to JUnit 4. ProjectForge migrated from mixed TestNG + JUnit4 to pure JUnit5 in commits like 20e07f9d1.

Lines 45–56: Jackson version forcing

45 configurations.all {
46     resolutionStrategy {
47         preferProjectModules()
48         // Force all Jackson module versions to match project's explicit Jackson version.
49         // Transitive dependencies (e.g. ez-vcard, groovy-yaml, flyway) may pull in newer Jackson versions.
50         val jacksonVersion = libs.findVersion("com.fasterxml.jackson").get().requiredVersion
51         force("com.fasterxml.jackson:jackson-bom:$jacksonVersion")
52         force("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
53         force("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
54         force("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
55     }
56 }

This is the most important section for build stability. Jackson is a transitive dependency of many libraries (ez-vcard, groovy-yaml, flyway). Each library may pull a different Jackson version, leading to runtime NoSuchMethodError or ClassNotFoundException.

How force() works: Gradle's dependency resolution collects all requested versions of Jackson across the entire dependency tree. The force() directive overrides any version — regardless of what any library requests, Jackson will be pinned to the version from libs.versions.toml.

Why four artifacts: Jackson is split into modules (jackson-core, jackson-databind, jackson-annotations) that must be version-synced. The jackson-bom (Bill of Materials) provides coordinated versions, but force() on individual artifacts is more explicit.

Without this section: Different subprojects could compile against different Jackson versions — the fat JAR would contain multiple Jackson JARs, and the classloader would pick whichever it finds first. This manifests as mysterious serialisation errors in production.

Lines 58–64: Shared dependencies

58 dependencies {
59     api(libs.findLibrary("org-jetbrains-kotlin-stdlib").get())
60     api(libs.findLibrary("io-github-microutils-kotlin-logging").get())
61     testImplementation(libs.findLibrary("org-junit-jupiter-api").get())
62     testImplementation(libs.findLibrary("org-junit-jupiter-engine").get())
63     testImplementation(libs.findLibrary("org-junit-platform-launcher").get())
64 }

Five dependencies applied to every module:

DependencyScopePurpose
kotlin-stdlibapiKotlin standard library — all modules use Kotlin
kotlin-loggingapiKotlin wrapper around SLF4J — microutils/kotlin-logging
junit-jupiter-apitestImplementationJUnit 5 test annotations (@Test, @BeforeEach)
junit-jupiter-enginetestImplementationJUnit 5 test execution engine
junit-platform-launchertestImplementationIDE/test runner integration

Key takeaways