From 28060dae8507927d3b5c6bd1694ebf425404027a Mon Sep 17 00:00:00 2001 From: Jendrik Johannes Date: Wed, 3 Jan 2024 09:01:28 +0100 Subject: [PATCH] Add platform dependency analysis --- .../PluginApplicationOrderAnalysis.kt | 1 - gradle/platform/build.gradle.kts | 7 ++- gradle/platform/settings.gradle.kts | 1 + .../kotlin/org.example.platform.gradle.kts | 1 + ...le.dependency-analysis-platform.gradle.kts | 54 ++++++++++++++++ .../DependencyFormatCheck.kt | 59 +++++++++++++++++ .../PlatformVersionConsistencyCheck.kt | 63 +++++++++++++++++++ 7 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts create mode 100644 gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/DependencyFormatCheck.kt create mode 100644 gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt diff --git a/gradle/meta-plugins/plugin-analysis-plugins/src/main/kotlin/org/example/pluginanalysis/PluginApplicationOrderAnalysis.kt b/gradle/meta-plugins/plugin-analysis-plugins/src/main/kotlin/org/example/pluginanalysis/PluginApplicationOrderAnalysis.kt index d6071d4a..7a4e00d4 100644 --- a/gradle/meta-plugins/plugin-analysis-plugins/src/main/kotlin/org/example/pluginanalysis/PluginApplicationOrderAnalysis.kt +++ b/gradle/meta-plugins/plugin-analysis-plugins/src/main/kotlin/org/example/pluginanalysis/PluginApplicationOrderAnalysis.kt @@ -6,7 +6,6 @@ import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import java.io.File abstract class PluginApplicationOrderAnalysis : DefaultTask() { diff --git a/gradle/platform/build.gradle.kts b/gradle/platform/build.gradle.kts index 1c4e7c30..515154e3 100644 --- a/gradle/platform/build.gradle.kts +++ b/gradle/platform/build.gradle.kts @@ -13,10 +13,15 @@ dependencies { moduleInfo { version("com.google.common", "30.1-jre") version("jakarta.activation") { require("1.2.2"); reject("[2.0.0,)") } // Upgrade to 2.x requires newer Jakarta APIs + version("java.inject") { require("1.0.5"); reject("[2.0.0,)") } // Upgrade to 2.x requires newer Jakarta APIs version("jakarta.mail") { require("1.6.7"); reject("[2.0.0,)") } // Upgrade to 2.x requires newer Jakarta APIs version("jakarta.servlet", "6.0.0") - version("java.inject") { require("1.0.5"); reject("[2.0.0,)") } // Upgrade to 2.x requires newer Jakarta APIs version("javax.annotations.jsr305", "3.0.2") version("org.assertj.core", "3.22.0") version("velocity.engine.core", "2.3") } + +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..5b7f2cee 100644 --- a/gradle/platform/settings.gradle.kts +++ b/gradle/platform/settings.gradle.kts @@ -4,3 +4,4 @@ pluginManagement { plugins { id("org.example.settings") } +includeBuild("../..") diff --git a/gradle/plugins/base-plugins/src/main/kotlin/org.example.platform.gradle.kts b/gradle/plugins/base-plugins/src/main/kotlin/org.example.platform.gradle.kts index fef5f720..7f862a9c 100644 --- a/gradle/plugins/base-plugins/src/main/kotlin/org.example.platform.gradle.kts +++ b/gradle/plugins/base-plugins/src/main/kotlin/org.example.platform.gradle.kts @@ -1,6 +1,7 @@ plugins { id("java-platform") id("org.example.base") + id("org.example.dependency-analysis-platform") id("org.gradlex.java-module-versions") } 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 new file mode 100644 index 00000000..2e2a66b1 --- /dev/null +++ b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org.example.dependency-analysis-platform.gradle.kts @@ -0,0 +1,54 @@ +import org.example.dependencyanalysis.DependencyFormatCheck +import org.example.dependencyanalysis.PlatformVersionConsistencyCheck + +plugins { + id("java-platform") + id("org.gradlex.java-module-dependencies") +} + +val checkDependencyFormatting = tasks.register("checkDependencyFormatting") { + group = LifecycleBasePlugin.VERIFICATION_GROUP + + buildFilePath.set(project.buildFile.absolutePath) + declaredDependencies.put("api", provider { configurations.api.get().dependencies.map { d -> d.toDeclaredString() } }) + declaredDependencies.put("runtime", provider { configurations.runtime.get().dependencies.map { d -> d.toDeclaredString() } }) + declaredConstraints.put("api", provider { configurations.api.get().dependencyConstraints.map { d -> d.toDeclaredString() } }) + declaredConstraints.put("runtime", provider { configurations.runtime.get().dependencyConstraints.map { d -> d.toDeclaredString() } }) +} + +// 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 each 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 }) +} + +tasks.check { + dependsOn(checkDependencyFormatting) +} + +fun Dependency.toDeclaredString() = "$group:$name:$version" +fun DependencyConstraint.toDeclaredString() = "version(\"${javaModuleDependencies.moduleName("$group:$name").getOrElse("$group:$name")}\", \"$version\")" diff --git a/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/DependencyFormatCheck.kt b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/DependencyFormatCheck.kt new file mode 100644 index 00000000..6b04f4a8 --- /dev/null +++ b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/DependencyFormatCheck.kt @@ -0,0 +1,59 @@ +package org.example.dependencyanalysis + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +/** + * Check that 'dependencies' are defined in alphabetical order and without version. + */ +abstract class DependencyFormatCheck : DefaultTask() { + + @get:Input + abstract val buildFilePath : Property + + @get:Input + abstract val declaredDependencies : MapProperty> // Map of 'scope' to 'coordinates' + + @get:Input + abstract val declaredConstraints : MapProperty> // Map of 'scope' to 'coordinates' + + @TaskAction + fun check() { + declaredDependencies.get().forEach { (scope, dependencies) -> + val declaredInBuildFile = dependencies.filter { + // Ignore dependencies that are defined in our plugins + it !in listOf( + "org.example.product:platform", + "org.slf4j:slf4j-simple", + "org.junit.jupiter:junit-jupiter-engine", + "org.junit.jupiter:junit-jupiter") + } + val sortedProject = declaredInBuildFile.filter { it.startsWith(":") }.sorted() + val sortedExternal = declaredInBuildFile.filter { !it.startsWith(":") }.sorted() + if (declaredInBuildFile != sortedProject + sortedExternal) { + throw RuntimeException(""" + ${buildFilePath.get()} + + Module versions are not declared in alphabetical order. Please use this order: + ${sortedProject.joinToString("\n ") {"${scope}(project(\"${it}\"))"}} + ${sortedExternal.joinToString("\n ") {"${scope}(\"${it}\")"}} + """.trimIndent()) + } + } + + declaredConstraints.get().forEach { (scope, constraints) -> + val sortedConstraints = constraints.sorted() + if (constraints != sortedConstraints) { + throw RuntimeException(""" + ${buildFilePath.get()} + + $scope dependency constraints are not declared in alphabetical order. Please use this order: + ${sortedConstraints.joinToString("\n ")} + """.trimIndent()) + } + } + } +} 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..dda68726 --- /dev/null +++ b/gradle/plugins/dependency-analysis-plugins/src/main/kotlin/org/example/dependencyanalysis/PlatformVersionConsistencyCheck.kt @@ -0,0 +1,63 @@ +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 + +/** + * Checks that all versions declared in the platform are used and are consistent with the consistent resolution result. + */ +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()) + } + } +} +