Skip to content

Commit

Permalink
Customized dependency analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
jjohannes committed Dec 4, 2023
1 parent f8875e2 commit 8f4c321
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 1 deletion.
1 change: 1 addition & 0 deletions gradle/plugins/base-plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ plugins {
dependencies {
implementation(platform(project(":plugins-platform")))

implementation(project(":dependency-analysis-plugins"))
implementation(project(":dependency-rules-plugins"))
}
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")
}

// Depend on other Platforms/BOMs to align versions for libraries that consist of multiple components (like Jackson)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("lifecycle-base")
id("org.example.dependency-analysis-root")
}

// Configure the ':tasks' task of the root project to only show
Expand Down Expand Up @@ -49,3 +50,9 @@ tasks.register("checkForDependencyVulnerabilities") {
description = "Check current dependencies for known vulnerabilities"
dependsOn(":app:dependencyCheckAnalyze")
}

tasks.register("checkForDependencyVersionUpgrades") {
group = mainBuildGroup
description = "Check for version upgrades (runs weekly on CI)"
dependsOn(gradle.includedBuild("platform").task(":$name"))
}
6 changes: 6 additions & 0 deletions gradle/plugins/dependency-analysis-plugins/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
plugins {
`kotlin-dsl`
}

dependencies {
implementation(platform(project(":plugins-platform")))

implementation("com.autonomousapps:dependency-analysis-gradle-plugin")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import org.example.dependencyanalysis.DependencyFormatCheck
import org.example.dependencyanalysis.DependencyVersionUpgradesCheck
import org.example.dependencyanalysis.PlatformVersionConsistencyCheck

plugins {
id("java-platform")
}

val checkDependencyFormatting = tasks.register<DependencyFormatCheck>("checkDependencyFormatting") {
group = LifecycleBasePlugin.VERIFICATION_GROUP

buildFilePath.set(project.buildFile.absolutePath)
shouldNotHaveVersions.set(false)
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() } })
}

val latestReleases = configurations.dependencyScope("dependencyVersionUpgrades") {
withDependencies {
add(project.dependencies.platform(project(project.path)))
configurations.api.get().dependencies.forEach {
add(project.dependencies.platform("${it.group}:${it.name}:latest.release") { isTransitive = false })
}
configurations.api.get().dependencyConstraints.forEach {
add(project.dependencies.create("${it.group}:${it.name}:latest.release") { isTransitive = false })
}
}
}
val latestReleasesPath = configurations.resolvable("latestReleasesPath") {
attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attributes.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
extendsFrom(latestReleases.get())
}
tasks.register<DependencyVersionUpgradesCheck>("checkForDependencyVersionUpgrades") {
group = HelpTasksPlugin.HELP_GROUP
projectName.set(project.name)
apiDependencies.set(configurations.api.get().dependencies.map { "${it.group}:${it.name}:${it.version}" })
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<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() = "$group:$name:$version"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import com.autonomousapps.DependencyAnalysisSubExtension
import org.example.dependencyanalysis.DependencyFormatCheck
import org.example.dependencyanalysis.DependencyScopeCheck

plugins {
id("java")
}

val checkDependencyFormatting = tasks.register<DependencyFormatCheck>("checkDependencyFormatting") {
group = LifecycleBasePlugin.VERIFICATION_GROUP

buildFilePath.set(project.buildFile.absolutePath)
shouldNotHaveVersions.set(true)
sourceSets.all {
declaredDependencies.put(implementationConfigurationName, provider { configurations.getByName(implementationConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(runtimeOnlyConfigurationName, provider { configurations.getByName(runtimeOnlyConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(compileOnlyConfigurationName, provider { configurations.getByName(compileOnlyConfigurationName).dependencies.map { d -> d.toDeclaredString() } })
declaredDependencies.put(apiConfigurationName, provider { configurations.findByName(apiConfigurationName)?.dependencies?.map { d -> d.toDeclaredString() } ?: emptyList() })
declaredDependencies.put(compileOnlyApiConfigurationName, provider { configurations.findByName(compileOnlyApiConfigurationName)?.dependencies?.map { d -> d.toDeclaredString() } ?: emptyList() })
}
}

val checkDependencyScopes = tasks.register<DependencyScopeCheck>("checkDependencyScopes") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
shouldRunAfter(checkDependencyFormatting)
}

tasks.check {
dependsOn(checkDependencyFormatting)
dependsOn(checkDependencyScopes)
}

plugins.withId("com.autonomousapps.dependency-analysis") {
extensions.getByType<DependencyAnalysisSubExtension>().registerPostProcessingTask(checkDependencyScopes)
}

fun Dependency.toDeclaredString() = when(this) {
is ProjectDependency -> ":$name"
else -> "$group:$name${if (version == null) "" else ":$version"}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
id("com.autonomousapps.dependency-analysis")
}

// Lifecycle task to check dependency scopes in all subprojects
tasks.register("checkDependencyScopes") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Check all dependency scopes (api vs implementation) and find unused dependencies"
dependsOn(subprojects.map { "${it.path}:checkDependencyScopes"})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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'

@get:Input
abstract val shouldNotHaveVersions : Property<Boolean>

@TaskAction
fun check() {
declaredDependencies.get().forEach { (scope, dependencies) ->
if (shouldNotHaveVersions.get()) {
dependencies.forEach { coordinates ->
if (coordinates.count { it == ':' } == 2 && !coordinates.startsWith("org.jetbrains.kotlin:kotlin-stdlib:")) {
throw RuntimeException("""
${buildFilePath.get()}
Dependencies with versions are not allowed. Please declare the dependency as follows:
${scope}("${coordinates.substring(0, coordinates.lastIndexOf(':'))}")
All versions must be declared in 'gradle/platform'.
If the version is not yet defined there, add the following to 'gradle/platform/build.gradle.kts':
api("$coordinates")
""".trimIndent())
}
}
}

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()}
$scope dependencies 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 ") {"${scope}(\"${it}\")"}}
""".trimIndent())
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.example.dependencyanalysis

import com.autonomousapps.AbstractPostProcessingTask
import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectCoordinates
import org.gradle.api.tasks.TaskAction

/**
* Task that uses the 'com.autonomousapps.dependency-analysis' plugin to find unused dependencies and check
* 'api' vs. 'implementation' scopes.
*/
abstract class DependencyScopeCheck : AbstractPostProcessingTask() {

@TaskAction
fun check() {
val projectAdvice = projectAdvice().dependencyAdvice
if (projectAdvice.isNotEmpty()) {
val toAdd = projectAdvice.filter { it.toConfiguration != null && it.toConfiguration != "runtimeOnly" }
.map { it.declaration(it.toConfiguration) }.sorted()
val toRemove =
projectAdvice.filter { it.fromConfiguration != null }.map { it.declaration(it.fromConfiguration) }
.sorted()

throw RuntimeException(
"""
${projectAdvice().projectPath.substring(1)}/build.gradle.kts
Please add the following dependency declarations:
${toAdd.joinToString("\n ") { it }}
Please remove the following dependency declarations:
${toRemove.joinToString("\n ") { it }}
""".trimIndent()
)
}
}

private fun Advice.declaration(conf: String?) =
if (coordinates is ProjectCoordinates) "${conf}(project(\"${coordinates.identifier}\"))"
else "${conf}(\"${coordinates.identifier}\")"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.example.dependencyanalysis

import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction

/**
* Checks if new versions are available for what is declared in the platform.
*/
abstract class DependencyVersionUpgradesCheck : DefaultTask() {

@get:Internal
abstract val projectName: Property<String>

@get:Internal
abstract val apiDependencies: SetProperty<String>

@get:Internal
abstract val apiDependencyConstraints: SetProperty<String>

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

@TaskAction
fun check() {
val platformDependencyUpgrades = apiDependencies.get().filter { declared -> declared.resolvedVersion() != declared.version() }
val constraintUpgrades = apiDependencyConstraints.get().filter { declared -> declared.resolvedVersion() != declared.version() }
if (platformDependencyUpgrades.isNotEmpty() || constraintUpgrades.isNotEmpty()) {
throw RuntimeException("""
${projectName.get()}/build.gradle.kts
The following dependency versions should be upgraded in 'gradle/platform/build.gradle.kts' (dependencies {} block):
${platformDependencyUpgrades.joinToString("\n ") { "api(platform(\"${it.ga()}:${it.resolvedVersion()}\"))" }}
The following dependency versions should be upgraded in 'gradle/platform/build.gradle.kts' (dependencies.constraints {} block):
${constraintUpgrades.joinToString("\n ") { "api(\"${it.ga()}:${it.resolvedVersion()}\")" }}
If we cannot perform an upgrade, please add a '{ version { reject("...") } }' statement and a comment
for the versions we cannot support at the moment.
""".trimIndent())
}
}

private fun String.ga() = substring(0, lastIndexOf(":"))

private fun String.version() = substring(lastIndexOf(":") + 1)

private fun String.resolvedVersion() =
latestReleasesResolutionResult.get().find {
it.moduleVersion!!.module.toString() == ga()
}?.moduleVersion?.version ?: version() // if no fitting version could be determined, return the declared one

}
Loading

0 comments on commit 8f4c321

Please sign in to comment.