Skip to content

Commit 22cd3d4

Browse files
authored
feat: new task for producing a version catalog file containing all dependencies (#1389)
* Add a task for producing a version catalog file containing all dependencies * Address PR feedback
1 parent ffc5b54 commit 22cd3d4

File tree

8 files changed

+183
-17
lines changed

8 files changed

+183
-17
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ local.properties
1515

1616
.bash_history
1717
*.salive
18+
19+
# kotlin 2 makes a temp dir in gradle build root directories.
20+
.kotlin/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2024. Tony Robalik.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.autonomousapps.android
4+
5+
import com.autonomousapps.android.projects.DuplicateDependencyVersionsProject
6+
import org.gradle.util.GradleVersion
7+
8+
import static com.autonomousapps.kit.truth.BuildResultSubject.buildResults
9+
import static com.autonomousapps.utils.Runner.build
10+
import static com.google.common.truth.Truth.assertAbout
11+
import static com.google.common.truth.Truth.assertThat
12+
13+
final class AllDependenciesSpec extends AbstractAndroidSpec {
14+
15+
def "can generate a version catalog file with all dependencies (#gradleVersion AGP #agpVersion)"() {
16+
given:
17+
def project = new DuplicateDependencyVersionsProject(agpVersion as String)
18+
gradleProject = project.gradleProject
19+
20+
when:
21+
def result = build(gradleVersion as GradleVersion, gradleProject.rootDir, 'computeAllDependencies')
22+
23+
then: 'all dependencies report'
24+
def report = project.actualAllDependencies()
25+
assertThat(report).isEqualTo(project.expectedAllDependencies)
26+
27+
where:
28+
[gradleVersion, agpVersion] << multivariableDataPipe([GRADLE_8_0], [AGP_8_0.version])
29+
}
30+
}

src/functionalTest/groovy/com/autonomousapps/android/projects/DuplicateDependencyVersionsProject.groovy

+37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.autonomousapps.android.projects
44

5+
import com.autonomousapps.internal.OutputPathsKt
56
import com.autonomousapps.kit.GradleProject
67
import com.autonomousapps.kit.gradle.Dependency
78

@@ -70,6 +71,42 @@ final class DuplicateDependencyVersionsProject extends AbstractAndroidProject {
7071
return resolvedDependenciesReport(gradleProject, projectPath)
7172
}
7273

74+
String actualAllDependencies() {
75+
return gradleProject.singleArtifact(':', OutputPathsKt.getAllLibsVersionsTomlPath()).asPath.text
76+
}
77+
78+
String expectedAllDependencies = '''\
79+
[libraries]
80+
androidx-activity-activity-1-0-0 = { module = "androidx.activity:activity", version = "1.0.0" }
81+
androidx-annotation-annotation-1-1-0 = { module = "androidx.annotation:annotation", version = "1.1.0" }
82+
androidx-appcompat-appcompat-1-1-0 = { module = "androidx.appcompat:appcompat", version = "1.1.0" }
83+
androidx-appcompat-appcompat-resources-1-1-0 = { module = "androidx.appcompat:appcompat-resources", version = "1.1.0" }
84+
androidx-arch-core-core-common-2-1-0 = { module = "androidx.arch.core:core-common", version = "2.1.0" }
85+
androidx-arch-core-core-runtime-2-0-0 = { module = "androidx.arch.core:core-runtime", version = "2.0.0" }
86+
androidx-collection-collection-1-1-0 = { module = "androidx.collection:collection", version = "1.1.0" }
87+
androidx-core-core-1-1-0 = { module = "androidx.core:core", version = "1.1.0" }
88+
androidx-cursoradapter-cursoradapter-1-0-0 = { module = "androidx.cursoradapter:cursoradapter", version = "1.0.0" }
89+
androidx-customview-customview-1-0-0 = { module = "androidx.customview:customview", version = "1.0.0" }
90+
androidx-drawerlayout-drawerlayout-1-0-0 = { module = "androidx.drawerlayout:drawerlayout", version = "1.0.0" }
91+
androidx-fragment-fragment-1-1-0 = { module = "androidx.fragment:fragment", version = "1.1.0" }
92+
androidx-interpolator-interpolator-1-0-0 = { module = "androidx.interpolator:interpolator", version = "1.0.0" }
93+
androidx-lifecycle-lifecycle-common-2-1-0 = { module = "androidx.lifecycle:lifecycle-common", version = "2.1.0" }
94+
androidx-lifecycle-lifecycle-livedata-2-0-0 = { module = "androidx.lifecycle:lifecycle-livedata", version = "2.0.0" }
95+
androidx-lifecycle-lifecycle-livedata-core-2-0-0 = { module = "androidx.lifecycle:lifecycle-livedata-core", version = "2.0.0" }
96+
androidx-lifecycle-lifecycle-runtime-2-1-0 = { module = "androidx.lifecycle:lifecycle-runtime", version = "2.1.0" }
97+
androidx-lifecycle-lifecycle-viewmodel-2-1-0 = { module = "androidx.lifecycle:lifecycle-viewmodel", version = "2.1.0" }
98+
androidx-loader-loader-1-0-0 = { module = "androidx.loader:loader", version = "1.0.0" }
99+
androidx-savedstate-savedstate-1-0-0 = { module = "androidx.savedstate:savedstate", version = "1.0.0" }
100+
androidx-vectordrawable-vectordrawable-1-1-0 = { module = "androidx.vectordrawable:vectordrawable", version = "1.1.0" }
101+
androidx-vectordrawable-vectordrawable-animated-1-1-0 = { module = "androidx.vectordrawable:vectordrawable-animated", version = "1.1.0" }
102+
androidx-versionedparcelable-versionedparcelable-1-1-0 = { module = "androidx.versionedparcelable:versionedparcelable", version = "1.1.0" }
103+
androidx-viewpager-viewpager-1-0-0 = { module = "androidx.viewpager:viewpager", version = "1.0.0" }
104+
junit-junit-4-11 = { module = "junit:junit", version = "4.11" }
105+
junit-junit-4-12 = { module = "junit:junit", version = "4.12" }
106+
junit-junit-4-13 = { module = "junit:junit", version = "4.13" }
107+
org-hamcrest-hamcrest-core-1-3 = { module = "org.hamcrest:hamcrest-core", version = "1.3" }
108+
'''.stripIndent()
109+
73110
String expectedOutput = '''\
74111
Your build uses 28 dependencies, representing 26 distinct 'libraries.' 1 libraries have multiple versions across the build. These are:
75112
* junit:junit:{4.11,4.12,4.13}'''.stripIndent()

src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt

+2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ internal class RootOutputPaths(private val project: Project) {
105105
val duplicateDependenciesPath = file("$ROOT_DIR/duplicate-dependencies-report.json")
106106
val buildHealthPath = file("$ROOT_DIR/build-health-report.json")
107107
val consoleReportPath = file("$ROOT_DIR/build-health-report.txt")
108+
val allLibsVersionsTomlPath = file("$ROOT_DIR/allLibs.versions.toml")
108109
val shouldFailPath = file("$ROOT_DIR/should-fail.txt")
109110

110111
val workPlanDir = dir("$ROOT_DIR/work-plan")
@@ -127,4 +128,5 @@ fun getAdvicePathV2() = "$ROOT_DIR/final-advice.json"
127128
fun getAggregateAdvicePathV2() = "$ROOT_DIR/final-advice.json"
128129
fun getFinalAdvicePathV2() = "$ROOT_DIR/build-health-report.json"
129130
fun getDuplicateDependenciesReport() = "$ROOT_DIR/duplicate-dependencies-report.json"
131+
fun getAllLibsVersionsTomlPath() = "$ROOT_DIR/allLibs.versions.toml"
130132
fun getResolvedDependenciesReport() = "$ROOT_DIR/resolved-dependencies-report.txt"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.autonomousapps.internal.utils
2+
3+
import com.autonomousapps.model.Coordinates
4+
import com.autonomousapps.model.ModuleCoordinates
5+
import org.gradle.api.file.ConfigurableFileCollection
6+
7+
/**
8+
* Reads the set of all module coordinates for the the given artifact files.
9+
* Must only be called in a task action.
10+
*/
11+
internal fun ConfigurableFileCollection.dependencyCoordinates(): Set<ModuleCoordinates> =
12+
this.files
13+
.flatMap { it.readLines() }
14+
.map {
15+
val external = Coordinates.of(it)
16+
check(external is ModuleCoordinates) { "ModuleCoordinates expected. Was $it." }
17+
external
18+
}.toSet()

src/main/kotlin/com/autonomousapps/subplugin/RootPlugin.kt

+19-8
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import com.autonomousapps.internal.RootOutputPaths
1111
import com.autonomousapps.internal.advice.DslKind
1212
import com.autonomousapps.internal.artifacts.DagpArtifacts
1313
import com.autonomousapps.internal.artifacts.Publisher.Companion.interProjectPublisher
14+
import com.autonomousapps.internal.artifacts.Resolver
1415
import com.autonomousapps.internal.artifacts.Resolver.Companion.interProjectResolver
1516
import com.autonomousapps.internal.artifactsFor
1617
import com.autonomousapps.internal.utils.log
1718
import com.autonomousapps.internal.utils.project.buildPath
1819
import com.autonomousapps.services.GlobalDslService
1920
import com.autonomousapps.tasks.*
2021
import org.gradle.api.Project
22+
import org.gradle.api.file.FileCollection
23+
import org.gradle.api.provider.Provider
2124
import org.gradle.kotlin.dsl.register
2225

2326
// TODO(tsr): inline
@@ -96,21 +99,19 @@ internal class RootPlugin(private val project: Project) {
9699
val paths = RootOutputPaths(this)
97100

98101
val computeDuplicatesTask = tasks.register<ComputeDuplicateDependenciesTask>("computeDuplicateDependencies") {
99-
resolvedDependenciesReports.setFrom(resolvedDepsResolver.internal.map { c ->
100-
c.incoming.artifactView {
101-
// Not all projects in the build will have DAGP applied, meaning they won't have any artifact to consume.
102-
// Setting `lenient(true)` means we can still have a dependency on those projects, and not fail this task when
103-
// we find nothing there.
104-
lenient(true)
105-
}.artifacts.artifactFiles
106-
})
102+
resolvedDependenciesReports.setFrom(resolvedDepsResolver.artifactFilesProvider())
107103
output.set(paths.duplicateDependenciesPath)
108104
}
109105

110106
tasks.register<PrintDuplicateDependenciesTask>("printDuplicateDependencies") {
111107
duplicateDependenciesReport.set(computeDuplicatesTask.flatMap { it.output })
112108
}
113109

110+
tasks.register<ComputeAllDependenciesTask>("computeAllDependencies") {
111+
resolvedDependenciesReports.setFrom(resolvedDepsResolver.artifactFilesProvider())
112+
output.set(paths.allLibsVersionsTomlPath)
113+
}
114+
114115
val generateBuildHealthTask = tasks.register<GenerateBuildHealthTask>("generateBuildHealth") {
115116
projectHealthReports.setFrom(adviceResolver.internal.map { it.artifactsFor("json").artifactFiles })
116117
reportingConfig.set(dagpExtension.reportingHandler.config())
@@ -159,4 +160,14 @@ internal class RootPlugin(private val project: Project) {
159160
}
160161
}
161162
}
163+
164+
private fun Resolver<DagpArtifacts>.artifactFilesProvider(): Provider<FileCollection> =
165+
this.internal.map { c ->
166+
c.incoming.artifactView {
167+
// Not all projects in the build will have DAGP applied, meaning they won't have any artifact to consume.
168+
// Setting `lenient(true)` means we can still have a dependency on those projects, and not fail this task when
169+
// we find nothing there.
170+
lenient(true)
171+
}.artifacts.artifactFiles
172+
}
162173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) 2024. Tony Robalik.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.autonomousapps.tasks
4+
5+
import com.autonomousapps.TASK_GROUP_DEP
6+
import com.autonomousapps.internal.utils.getAndDelete
7+
import com.autonomousapps.internal.utils.dependencyCoordinates
8+
import com.autonomousapps.model.ModuleCoordinates
9+
import org.gradle.api.DefaultTask
10+
import org.gradle.api.file.ConfigurableFileCollection
11+
import org.gradle.api.file.RegularFileProperty
12+
import org.gradle.api.tasks.CacheableTask
13+
import org.gradle.api.tasks.InputFiles
14+
import org.gradle.api.tasks.OutputFile
15+
import org.gradle.api.tasks.PathSensitive
16+
import org.gradle.api.tasks.PathSensitivity
17+
import org.gradle.api.tasks.TaskAction
18+
19+
@CacheableTask
20+
abstract class ComputeAllDependenciesTask : DefaultTask() {
21+
22+
init {
23+
group = TASK_GROUP_DEP
24+
description = "Generates a version catalog file (allLibs.versions.toml) containing all dependencies in the project."
25+
}
26+
27+
@get:PathSensitive(PathSensitivity.RELATIVE)
28+
@get:InputFiles
29+
abstract val resolvedDependenciesReports: ConfigurableFileCollection
30+
31+
@get:OutputFile
32+
abstract val output: RegularFileProperty
33+
34+
@TaskAction
35+
fun action() {
36+
val outputFile = output.getAndDelete()
37+
38+
val libs: Set<String> = resolvedDependenciesReports
39+
.dependencyCoordinates()
40+
.map { "${it.toVersionCatalogAlias()} = { module = \"${it.identifier}\", version = \"${it.resolvedVersion}\" }" }
41+
.toSortedSet()
42+
43+
val tomlContent = buildString {
44+
appendLine("[libraries]")
45+
libs.forEach { appendLine(it) }
46+
}
47+
48+
outputFile.writeText(tomlContent)
49+
50+
logger.quiet("Generated version catalog for all dependencies, containing ${libs.size} entries:\n${outputFile.absolutePath} ")
51+
}
52+
53+
private fun ModuleCoordinates.toVersionCatalogAlias(): String {
54+
return "${this.identifier}-${this.resolvedVersion}"
55+
.split(':', '.')
56+
// replace reserved keywords with safe alternatives
57+
.joinToString(separator = "-") { tomlReservedKeywordMappings.getOrDefault(it, it) }
58+
.lowercase()
59+
}
60+
61+
private companion object {
62+
private val tomlReservedKeywordMappings = mapOf(
63+
"extensions" to "extensionz",
64+
"class" to "clazz",
65+
"convention" to "convencion",
66+
"bundles" to "bundlez",
67+
"versions" to "versionz",
68+
"plugins" to "pluginz",
69+
)
70+
}
71+
}

src/main/kotlin/com/autonomousapps/tasks/ComputeDuplicateDependenciesTask.kt

+3-9
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ package com.autonomousapps.tasks
44

55
import com.autonomousapps.TASK_GROUP_DEP
66
import com.autonomousapps.internal.utils.bufferWriteJsonMapSet
7+
import com.autonomousapps.internal.utils.dependencyCoordinates
78
import com.autonomousapps.internal.utils.getAndDelete
8-
import com.autonomousapps.model.Coordinates
9-
import com.autonomousapps.model.ModuleCoordinates
109
import org.gradle.api.DefaultTask
1110
import org.gradle.api.file.ConfigurableFileCollection
1211
import org.gradle.api.file.RegularFileProperty
@@ -33,13 +32,8 @@ abstract class ComputeDuplicateDependenciesTask : DefaultTask() {
3332

3433
val map = sortedMapOf<String, SortedSet<String>>()
3534

36-
resolvedDependenciesReports.files
37-
.flatMap { it.readLines() }
38-
.map {
39-
val external = Coordinates.of(it)
40-
check(external is ModuleCoordinates) { "ModuleCoordinates expected. Was $it." }
41-
external
42-
}
35+
resolvedDependenciesReports
36+
.dependencyCoordinates()
4337
.forEach {
4438
map.merge(it.identifier, sortedSetOf(it.resolvedVersion)) { acc, inc ->
4539
acc.apply { addAll(inc) }

0 commit comments

Comments
 (0)