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)