From 837ae662438c1c925476b91e6cbd1fd24c1aa772 Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Mon, 2 Oct 2023 13:54:46 +0200 Subject: [PATCH] Add 'checkPlatformVersionConsistency' task --- gradle/platform/build.gradle.kts | 7 ++- gradle/platform/settings.gradle.kts | 4 ++ ...le.dependency-analysis-platform.gradle.kts | 31 ++++++++++ .../PlatformVersionConsistencyCheck.kt | 60 +++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt diff --git a/gradle/platform/build.gradle.kts b/gradle/platform/build.gradle.kts index fc6fff8e..fd8bf7ea 100644 --- a/gradle/platform/build.gradle.kts +++ b/gradle/platform/build.gradle.kts @@ -19,7 +19,7 @@ dependencies.constraints { api("com.sun.mail:jakarta.mail:1.6.7") { version { reject("[2.0.0,)") } } // Upgrade to 2.x requires newer Jakarta APIs api("jakarta.inject:jakarta.inject-api:1.0.5") { version { reject("[2.0.0,)") } } // Upgrade to 2.x requires newer Jakarta APIs api("jakarta.servlet:jakarta.servlet-api:4.0.4") { version { reject("[5.0.0,)") } } // Stay Tomcat 8 compatible - api("junit:junit:4.13.2") + api("org.apache.commons:commons-lang3:3.9") api("org.apache.solr:solr-solrj:7.7.3") { version { reject("[8.0.0,)") } } // API changes in 8 require production code changes api("org.apache.velocity:velocity-engine-core:2.3") api("org.apache.zookeeper:zookeeper:3.8.0") @@ -27,3 +27,8 @@ dependencies.constraints { api("org.opensaml:opensaml:2.6.4") api("org.reflections:reflections:0.9.11") { version { reject("[0.9.12,)") } } // Upgrade breaks 'com.github.racc:typesafeconfig-guice' } + +dependencies { + // list the 'main' modules of our software product for 'checkPlatformVersionConsistency' + product("org.example.product:app") +} diff --git a/gradle/platform/settings.gradle.kts b/gradle/platform/settings.gradle.kts index 9cfb420c..f0cfaa39 100644 --- a/gradle/platform/settings.gradle.kts +++ b/gradle/platform/settings.gradle.kts @@ -4,3 +4,7 @@ pluginManagement { plugins { id("org.example.settings") } +dependencyResolutionManagement { + // Include the 'main' project for checkPlatformVersionConsistency anlysis + includeBuild("../..") +} diff --git a/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts index b7d3b9a8..41aa0628 100644 --- a/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts +++ b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts @@ -1,4 +1,5 @@ import org.example.dependencyanalysis.DependencyVersionUpgradesCheck +import org.example.dependencyanalysis.PlatformVersionConsistencyCheck plugins { id("java-platform") @@ -29,3 +30,33 @@ tasks.register("checkForDependencyVersionUpgrade apiDependencyConstraints.set(configurations.api.get().dependencyConstraints.map { "${it.group}:${it.name}:${it.version}" }) latestReleasesResolutionResult.set(latestReleasesPath.map { it.incoming.resolutionResult.allComponents }) } + +// Install a task that checks the consistency of the platform against the resolution result of the product +val product = configurations.dependencyScope("product").get() +dependencies { + product(platform(project(path))) +} +val purePlatformVersions = configurations.dependencyScope("purePlatformVersions") { + withDependencies { + add(project.dependencies.platform(project(project.path))) + // Create a dependency for eac constraint defined in the platform (this is to check for unused entries) + configurations.api.get().dependencyConstraints.forEach { constraint -> + add(project.dependencies.create("${constraint.group}:${constraint.name}") { isTransitive = false }) + } + } +} +val fullProductRuntimeClasspath = configurations.resolvable("fullProductRuntimeClasspath") { + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + extendsFrom(product) +} +val purePlatformVersionsPath = configurations.resolvable("purePlatformVersionsPath") { + attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + extendsFrom(purePlatformVersions.get()) +} +tasks.register("checkPlatformVersionConsistency") { + group = HelpTasksPlugin.HELP_GROUP + productClasspath.set(fullProductRuntimeClasspath.map { it.incoming.resolutionResult.allComponents }) + classpathFromPlatform.set(purePlatformVersionsPath.map { it.incoming.resolutionResult.allComponents }) +} diff --git a/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt new file mode 100644 index 00000000..c59aceca --- /dev/null +++ b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt @@ -0,0 +1,60 @@ +package org.example.dependencyanalysis + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Category +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction + +abstract class PlatformVersionConsistencyCheck : DefaultTask() { + + @get:Internal + abstract val productClasspath: SetProperty + + @get:Internal + abstract val classpathFromPlatform: SetProperty + + @TaskAction + fun check() { + val publishedCategory = Attribute.of( Category.CATEGORY_ATTRIBUTE.name, String::class.java) + val resolvedToDeclaredVersions = + productClasspath.get().filter { it.moduleVersion?.group != "org.example.product" }.associate { cpEntry -> + cpEntry.moduleVersion to cpEntry.dependents.find { + it.from.variants.any { variant -> + variant.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)?.name == Category.REGULAR_PLATFORM + || variant.attributes.getAttribute(publishedCategory) == Category.REGULAR_PLATFORM + } + }?.let { + (it.requested as ModuleComponentSelector).version + } + } + + + val unnecessaryEntries = classpathFromPlatform.get().filter { platformEntry -> + productClasspath.get().none { platformEntry.moduleVersion?.module == it.moduleVersion?.module } + } + val missingEntries = resolvedToDeclaredVersions.filter { it.value == null }.map { it.key } + val wrongEntries = resolvedToDeclaredVersions.filter { it.value != null && it.key?.version != it.value } + + if (unnecessaryEntries.isNotEmpty() || missingEntries.isNotEmpty() || wrongEntries.isNotEmpty()) { + throw RuntimeException(""" + The following entries are not used in production code: + + ${unnecessaryEntries.joinToString("\n ") { "api(\"${it.moduleVersion}\")" }} + + The following transitive dependencies are not listed in the platform: + + ${missingEntries.joinToString("\n ") { "api(\"${it}\")" }} + + The following dependencies should be updated to the resolved versions: + + ${wrongEntries.keys.joinToString("\n ") { "api(\"${it}\") - currently declared '${wrongEntries[it]}'" }} + + """.trimIndent()) + } + } +} +