diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 7005ede2..8b5916f7 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -103,7 +103,7 @@ val errorPronePluginVersion = "4.2.0"
* @see
* Protobuf Gradle Plugins Releases
*/
-val protobufPluginVersion = "0.9.5"
+val protobufPluginVersion = "0.9.4"
/**
* The version of Dokka Gradle Plugins.
diff --git a/buildSrc/src/main/kotlin/DependencyResolution.kt b/buildSrc/src/main/kotlin/DependencyResolution.kt
index b7b30a9e..124adb3f 100644
--- a/buildSrc/src/main/kotlin/DependencyResolution.kt
+++ b/buildSrc/src/main/kotlin/DependencyResolution.kt
@@ -85,6 +85,9 @@ fun NamedDomainObjectContainer.forceVersions() {
private fun ResolutionStrategy.forceProductionDependencies() {
@Suppress("DEPRECATION") // Force versions of SLF4J and Kotlin libs.
+ Protobuf.libs.forEach {
+ force(it)
+ }
force(
AnimalSniffer.lib,
AutoCommon.lib,
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/boms/Boms.kt b/buildSrc/src/main/kotlin/io/spine/dependency/boms/Boms.kt
new file mode 100644
index 00000000..1f866e55
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/boms/Boms.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.boms
+
+import io.spine.dependency.kotlinx.Coroutines
+import io.spine.dependency.lib.Jackson
+import io.spine.dependency.lib.Kotlin
+import io.spine.dependency.test.JUnit
+
+/**
+ * The collection of references to BOMs applied by [BomsPlugin].
+ */
+object Boms {
+
+ /**
+ * The base production BOMs.
+ */
+ val core = listOf(
+ Kotlin.bom,
+ Coroutines.bom
+ )
+
+ /**
+ * The BOMs for testing dependencies.
+ */
+ val testing = listOf(
+ JUnit.bom
+ )
+
+ /**
+ * Technology-based BOMs.
+ */
+ object Optional {
+ val jackson = listOf(
+ Jackson.bom
+ )
+ }
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt
new file mode 100644
index 00000000..fc576f49
--- /dev/null
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/boms/BomsPlugin.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2025, TeamDev. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Redistribution and use in source and/or binary forms, with or without
+ * modification, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.spine.dependency.boms
+
+import io.spine.dependency.lib.Kotlin
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+
+/**
+ * The plugin which forces versions of platforms declared in the [Boms] object.
+ *
+ * Versions are enforced via the
+ * [org.gradle.api.artifacts.dsl.DependencyHandler.enforcedPlatform] call
+ * for configurations of the project to which the plugin is applied.
+ *
+ * The configurations are selected by the "kind" of BOM.
+ *
+ * [Boms.core] are applied to:
+ * 1. Production configurations, such as `api` or `implementation`.
+ * 2. Compilation configurations.
+ * 3. All `ksp` configurations.
+ *
+ * [Boms.testing] are applied to all testing configurations.
+ *
+ * In addition to forcing BOM-based dependencies,
+ * the plugin [forces][org.gradle.api.artifacts.ResolutionStrategy.force] the versions
+ * of [Kotlin.StdLib.artefacts] for all configurations because even through Kotlin
+ * artefacts are forced with BOM, the `variants` in the dependencies cannot be
+ * picked by Gradle.
+ *
+ * Run Gradle with the [INFO][org.gradle.api.logging.Logger.isInfoEnabled] logging level
+ * to see the dependencies forced by this plugin.
+ */
+class BomsPlugin : Plugin {
+
+ private val productionConfigs = listOf(
+ "api",
+ "implementation",
+ "compileOnly",
+ "runtimeOnly"
+ )
+
+ override fun apply(project: Project) = with(project) {
+
+ fun log(message: () -> String) {
+ val logger = project.logger
+ if (logger.isInfoEnabled) {
+ logger.info(message.invoke())
+ }
+ }
+
+ fun Configuration.applyBoms(boms: List) {
+ boms.forEach { bom ->
+ withDependencies {
+ val platform = project.dependencies.enforcedPlatform(bom)
+ addLater(provider { platform })
+ log { "Applied BOM: `$bom` to the configuration: `${this@applyBoms.name}`." }
+ }
+ }
+ }
+
+ configurations.run {
+ matching { isCompilationConfig(it.name) }.all {
+ applyBoms(Boms.core)
+ }
+ matching { isKspConfig(it.name) }.all {
+ applyBoms(Boms.core)
+ }
+ matching { it.name in productionConfigs }.all {
+ applyBoms(Boms.core)
+ }
+ matching { isTestConfig(it.name) }.all {
+ applyBoms(Boms.core + Boms.testing)
+ }
+
+ fun Configuration.diagSuffix(): String =
+ "the configuration `$name` in the project: `${project.path}`."
+
+ matching { !supportsBom(it.name) }.all {
+ resolutionStrategy.eachDependency {
+ if (requested.group == Kotlin.group) {
+ val kotlinVersion = Kotlin.runtimeVersion
+ useVersion(kotlinVersion)
+ log { "Forced Kotlin version `$kotlinVersion` in " + this@all.diagSuffix() }
+ }
+ }
+ }
+
+ all {
+ resolutionStrategy {
+ // The versions for Kotlin are resoled above correctly.
+ // But that does not guarantees that Gradle picks up a correct `variant`.
+ Kotlin.StdLib.artefacts.forEach { artefact ->
+ force(artefact)
+ log { "Forced the version of `$artefact` in " + this@all.diagSuffix() }
+ }
+ }
+ }
+ }
+ }
+
+ private fun isCompilationConfig(name: String) =
+ name.contains("compile", ignoreCase = true) &&
+ // `comileProtoPath` or `compileTestProtoPath`.
+ !name.contains("ProtoPath", ignoreCase = true)
+
+ private fun isKspConfig(name: String) =
+ name.startsWith("ksp", ignoreCase = true)
+
+ private fun isTestConfig(name: String) =
+ name.startsWith("test", ignoreCase = true)
+
+ /**
+ * Tells if the configuration with the given [name] supports forcing
+ * versions via the BOM mechanism.
+ *
+ * Not all configurations supports forcing via BOM. E.g., the configurations created
+ * by Protobuf Gradle Plugin such as `compileProtoPath` or `extractIncludeProto` do
+ * not pick up versions of dependencies set via `enforcedPlatform(myBom)`.
+ */
+ private fun supportsBom(name: String) =
+ (isCompilationConfig(name) || isKspConfig(name) || isTestConfig(name))
+}
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt
index e0eedd67..45b1c4b8 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Kotlin.kt
@@ -57,16 +57,30 @@ object Kotlin {
const val bom = "$group:kotlin-bom:$runtimeVersion"
const val scriptRuntime = "$group:kotlin-script-runtime"
- const val stdLib = "$group:kotlin-stdlib"
- const val stdLibCommon = "$group:kotlin-stdlib-common"
- const val toolingCore = "$group:kotlin-tooling-core"
+ object StdLib {
+ private const val infix = "kotlin-stdlib"
+ const val itself = "$group:$infix"
+ const val common = "$group:$infix-common"
+ const val jdk7 = "$group:$infix-jdk7"
+ const val jdk8 = "$group:$infix-jdk8"
+
+ val artefacts = setOf(itself, common, jdk7, jdk8).map { "$it:$runtimeVersion" }
+ }
+
+ @Deprecated("Please use `StdLib.itself` instead.", ReplaceWith("StdLib.itself"))
+ const val stdLib = StdLib.itself
- @Deprecated("Please use `stdLib` instead.")
- const val stdLibJdk7 = "$group:kotlin-stdlib-jdk7"
+ @Deprecated("Please use `StdLib.common` instead.", ReplaceWith("StdLib.common"))
+ const val stdLibCommon = StdLib.common
- @Deprecated("Please use `stdLib` instead.")
- const val stdLibJdk8 = "$group:kotlin-stdlib-jdk8"
+ @Deprecated("Please use `StdLib.jdk7` instead.", ReplaceWith("StdLib.jdk7"))
+ const val stdLibJdk7 = StdLib.jdk7
+
+ @Deprecated("Please use `StdLib.jdk8` instead.")
+ const val stdLibJdk8 = StdLib.jdk8
+
+ const val toolingCore = "$group:kotlin-tooling-core"
const val reflect = "$group:kotlin-reflect"
const val testJUnit5 = "$group:kotlin-test-junit5"
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt
index f6572076..342d6db8 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/lib/Protobuf.kt
@@ -60,7 +60,7 @@ object Protobuf {
object GradlePlugin {
/**
* The version of this plugin is already specified in `buildSrc/build.gradle.kts` file.
- * Thus, when applying the plugin to projects build files, only the [id] should be used.
+ * Thus, when applying the plugin to project build files, only the [id] should be used.
*
* When changing the version, also change the version used in the `build.gradle.kts`.
*/
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt b/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt
index fd253e90..dbcf02b4 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/test/JUnit.kt
@@ -41,6 +41,9 @@ object JUnit {
* testImplementation(enforcedPlatform(JUnit.bom))
* }
* ```
+ * The version of JUnit is forced automatically by
+ * the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]
+ * when it is applied to the project.
*/
const val bom = "org.junit:junit-bom:$version"
diff --git a/buildSrc/src/main/kotlin/jvm-module.gradle.kts b/buildSrc/src/main/kotlin/jvm-module.gradle.kts
index 2fda6f03..b1587a77 100644
--- a/buildSrc/src/main/kotlin/jvm-module.gradle.kts
+++ b/buildSrc/src/main/kotlin/jvm-module.gradle.kts
@@ -28,12 +28,10 @@ import BuildSettings.javaVersion
import io.spine.dependency.build.CheckerFramework
import io.spine.dependency.build.Dokka
import io.spine.dependency.build.ErrorProne
+import io.spine.dependency.build.JSpecify
import io.spine.dependency.lib.Guava
-import io.spine.dependency.lib.JavaX
import io.spine.dependency.lib.Protobuf
-import io.spine.dependency.local.Logging
import io.spine.dependency.local.Reflect
-import io.spine.dependency.local.TestLib
import io.spine.dependency.test.Jacoco
import io.spine.gradle.checkstyle.CheckStyleConfig
import io.spine.gradle.github.pages.updateGitHubPages
@@ -43,8 +41,6 @@ import io.spine.gradle.javadoc.JavadocConfig
import io.spine.gradle.kotlin.applyJvmToolchain
import io.spine.gradle.kotlin.setFreeCompilerArgs
import io.spine.gradle.report.license.LicenseReporter
-import io.spine.gradle.testing.configureLogging
-import io.spine.gradle.testing.registerTestTasks
plugins {
`java-library`
@@ -71,7 +67,6 @@ project.run {
val generatedDir = "$projectDir/generated"
setTaskDependencies(generatedDir)
- setupTests()
configureGitHubPages()
}
@@ -134,12 +129,8 @@ fun Module.addDependencies() = dependencies {
api(Guava.lib)
compileOnlyApi(CheckerFramework.annotations)
- compileOnlyApi(JavaX.annotations)
+ api(JSpecify.annotations)
ErrorProne.annotations.forEach { compileOnlyApi(it) }
-
- implementation(Logging.lib)
-
- testImplementation(TestLib.lib)
}
fun Module.forceConfigurations() {
@@ -157,18 +148,6 @@ fun Module.forceConfigurations() {
}
}
-fun Module.setupTests() {
- tasks {
- registerTestTasks()
- test.configure {
- useJUnitPlatform {
- includeEngines("junit-jupiter")
- }
- configureLogging()
- }
- }
-}
-
fun Module.setTaskDependencies(generatedDir: String) {
tasks {
val cleanGenerated by registering(Delete::class) {
diff --git a/buildSrc/src/main/kotlin/module-testing.gradle.kts b/buildSrc/src/main/kotlin/module-testing.gradle.kts
index 23452c93..715852c7 100644
--- a/buildSrc/src/main/kotlin/module-testing.gradle.kts
+++ b/buildSrc/src/main/kotlin/module-testing.gradle.kts
@@ -33,6 +33,18 @@ import io.spine.dependency.test.Truth
import io.spine.gradle.testing.configureLogging
import io.spine.gradle.testing.registerTestTasks
+/**
+ * This convention plugin applies test dependencies and configures test-related tasks.
+ *
+ * The version of the [JUnit] platform must be applied via the [BomsPlugin][io.spine.dependency.boms.BomsPlugin]:
+ *
+ * ```kotlin
+ * apply()
+ * ```
+ */
+@Suppress("unused")
+private val about = ""
+
plugins {
`java-library`
}
@@ -46,6 +58,7 @@ dependencies {
forceJunitPlatform()
testImplementation(Jupiter.api)
+ testImplementation(Jupiter.params)
testImplementation(JUnit.pioneer)
testImplementation(Guava.testLib)