Skip to content

Commit

Permalink
Add platform dependency analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
jjohannes committed Jan 3, 2024
1 parent 1c7eee6 commit 28060da
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
7 changes: 6 additions & 1 deletion gradle/platform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
1 change: 1 addition & 0 deletions gradle/platform/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pluginManagement {
plugins {
id("org.example.settings")
}
includeBuild("../..")
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("java-platform")
id("org.example.base")
id("org.example.dependency-analysis-platform")
id("org.gradlex.java-module-versions")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<DependencyFormatCheck>("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<PlatformVersionConsistencyCheck>("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\")"
Original file line number Diff line number Diff line change
@@ -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<String>

@get:Input
abstract val declaredDependencies : MapProperty<String, List<String>> // Map of 'scope' to 'coordinates'

@get:Input
abstract val declaredConstraints : MapProperty<String, List<String>> // 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())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ResolvedComponentResult>

@get:Internal
abstract val classpathFromPlatform: SetProperty<ResolvedComponentResult>

@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())
}
}
}

0 comments on commit 28060da

Please sign in to comment.