1 /*
<lambda>null2 * Copyright 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.build
18
19 import androidx.build.logging.TERMINAL_RED
20 import androidx.build.logging.TERMINAL_RESET
21 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
22 import com.autonomousapps.AbstractPostProcessingTask
23 import com.autonomousapps.model.ModuleCoordinates
24 import com.autonomousapps.model.ProjectAdvice
25 import com.autonomousapps.model.ProjectCoordinates
26 import com.google.gson.Gson
27 import com.google.gson.GsonBuilder
28 import java.io.File
29 import kotlin.text.appendLine
30 import org.gradle.api.Project
31 import org.gradle.api.file.RegularFileProperty
32 import org.gradle.api.tasks.CacheableTask
33 import org.gradle.api.tasks.InputFile
34 import org.gradle.api.tasks.Internal
35 import org.gradle.api.tasks.Optional
36 import org.gradle.api.tasks.OutputFile
37 import org.gradle.api.tasks.PathSensitive
38 import org.gradle.api.tasks.PathSensitivity
39 import org.gradle.api.tasks.TaskAction
40
41 /**
42 * Task that reports dependency analysis advice for the project. It gets advice from the dependency
43 * analysis gradle plugin and checks the baselines for the advice already captured and only reports
44 * if additional violations are found.
45 */
46 @CacheableTask
47 abstract class ReportDependencyAnalysisAdviceTask : AbstractPostProcessingTask() {
48 init {
49 group = "Verification"
50 description = "Task for generating advice for dependency analysis"
51 }
52
53 @get:Internal abstract val baseLineFile: RegularFileProperty
54
55 @InputFile
56 @Optional
57 @PathSensitive(PathSensitivity.NONE)
58 fun getDependencyAnalysisBaseline(): File? = baseLineFile.get().asFile.takeIf { it.exists() }
59
60 @get:Internal val projectPath: String = project.path
61 @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
62 @get:Internal
63 val isPublishedLibrary: Boolean =
64 project.extensions.getByType(AndroidXExtension::class.java).type ==
65 SoftwareType.PUBLISHED_LIBRARY
66
67 @TaskAction
68 fun getAdvice() {
69 val projectAdvice =
70 this@ReportDependencyAnalysisAdviceTask.projectAdvice().toAndroidxProjectAdvice()
71
72 val baselineAdvice =
73 Gson()
74 .fromJson<AndroidxProjectAdvice>(
75 getDependencyAnalysisBaseline()?.readText(),
76 AndroidxProjectAdvice::class.java
77 )
78
79 val advice =
80 if (baselineAdvice != null) {
81 getIncrementalAdvice(
82 projectAdvice.dependencyAdvice.filter {
83 !baselineAdvice.dependencyAdvice.contains(it)
84 }
85 )
86 } else {
87 getIncrementalAdvice(projectAdvice.dependencyAdvice)
88 }
89
90 if (advice.isNotBlank()) {
91 error(
92 """
93 There are some new dependencies added to this change that might be misconfigured:
94 $advice
95 ********************************************************************************
96 $TERMINAL_RED
97 To get a complete list of misconfigured dependencies, please run:
98 ./gradlew $projectPath:projectHealth.
99 To update the dependency analysis baseline file, please run:
100 ./gradlew $projectPath:updateDependencyAnalysisBaseline
101 $TERMINAL_RESET
102 ********************************************************************************
103 """
104 .trimIndent()
105 )
106 }
107 }
108
109 private fun getIncrementalAdvice(missingDependencyAdvice: List<DependencyAdvice>): String {
110 // Skip the reporting of modify dependencies for now, so that advice is easier to follow.
111 val unused = mutableSetOf<String>()
112 val transitive = mutableSetOf<String>()
113 val advice = StringBuilder()
114
115 missingDependencyAdvice.forEach {
116 // Don't fail CI if test source set has misconfigured dependencies
117 if (it.fromConfiguration?.contains("test", ignoreCase = true) == true) {
118 return@forEach
119 }
120 if (it.toConfiguration?.contains("test", ignoreCase = true) == true) {
121 return@forEach
122 }
123
124 val isCompileOnly =
125 it.toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true
126 val isTransitiveDependencyAdvice =
127 it.fromConfiguration == null && it.toConfiguration != null && !isCompileOnly
128 val isUnusedDependencyAdvice =
129 it.fromConfiguration != null && it.toConfiguration == null
130
131 val identifier =
132 if (it.coordinates.type == "project") {
133 "project(${it.coordinates.identifier})"
134 } else {
135 "'${it.coordinates.identifier}:${it.coordinates.resolvedVersion}'"
136 }
137 if (isTransitiveDependencyAdvice) {
138 transitive.add("${it.toConfiguration}($identifier)")
139 }
140 if (isUnusedDependencyAdvice) {
141 unused.add("${it.fromConfiguration}($identifier)")
142 }
143 }
144 if (unused.isNotEmpty()) {
145 advice.appendLine("Unused dependencies which should be removed:")
146 advice.appendLine(unused.sorted().joinToString(separator = "\n"))
147 }
148 if (transitive.isNotEmpty()) {
149 advice.appendLine("These transitive dependencies can be declared directly:")
150 advice.appendLine(transitive.sorted().joinToString(separator = "\n"))
151 }
152 return advice.toString()
153 }
154 }
155
156 /** Task to update dependency analysis baselines for the project. */
157 @CacheableTask
158 abstract class UpdateDependencyAnalysisBaseLineTask : AbstractPostProcessingTask() {
159 init {
160 group = "Verification"
161 description = "Task for updating dependency analysis baselines"
162 }
163
164 @get:OutputFile abstract val outputFile: RegularFileProperty
165 @get:Internal val isKMP: Boolean = project.multiplatformExtension != null
166 @get:Internal
167 val isPublishedLibrary: Boolean =
168 project.extensions.getByType(AndroidXExtension::class.java).type ==
169 SoftwareType.PUBLISHED_LIBRARY
170
171 @TaskAction
updateBaseLineForDependencyAnalysisAdvicenull172 fun updateBaseLineForDependencyAnalysisAdvice() {
173 val projectAdvice =
174 this@UpdateDependencyAnalysisBaseLineTask.projectAdvice().toAndroidxProjectAdvice()
175 val outputFile = outputFile.get()
176 val gson = GsonBuilder().setPrettyPrinting().create()
177 outputFile.asFile.writeText(gson.toJson(projectAdvice))
178 }
179 }
180
181 /**
182 * Configure the dependency analysis gradle plugin and register new post-processing tasks:
183 * 1. Updating the baselines for advice provided by the plugin.
184 * 2. Getting any incremental advice not captured in the baselines.
185 */
configureDependencyAnalysisPluginnull186 internal fun Project.configureDependencyAnalysisPlugin() {
187 plugins.apply("com.autonomousapps.dependency-analysis")
188
189 val updateDependencyAnalysisBaselineTask =
190 tasks.register(
191 "updateDependencyAnalysisBaseline",
192 UpdateDependencyAnalysisBaseLineTask::class.java
193 ) { task ->
194 task.outputFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json"))
195 task.cacheEvenIfNoOutputs()
196 // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
197 task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
198 }
199
200 val reportDependencyAnalysisAdviceTask =
201 tasks.register(
202 "reportDependencyAnalysisAdvice",
203 ReportDependencyAnalysisAdviceTask::class.java
204 ) { task ->
205 var baselineFile = layout.projectDirectory.file("dependencyAnalysis-baseline.json")
206 task.baseLineFile.set(baselineFile)
207 task.cacheEvenIfNoOutputs()
208 // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
209 task.onlyIf { !(task.isKMP) && task.isPublishedLibrary }
210 }
211
212 val dependencyAnalysisSubExtension =
213 extensions.getByType(com.autonomousapps.DependencyAnalysisSubExtension::class.java)
214 dependencyAnalysisSubExtension.registerPostProcessingTask(reportDependencyAnalysisAdviceTask)
215 dependencyAnalysisSubExtension.registerPostProcessingTask(updateDependencyAnalysisBaselineTask)
216
217 // Ignore advice for runTimeOnly, compileOnly or incorrect dependency configs
218 // since it affects downstream consumers
219 dependencyAnalysisSubExtension.issues { it.onIncorrectConfiguration { it.severity("ignore") } }
220 dependencyAnalysisSubExtension.issues { it.onRuntimeOnly { it.severity("ignore") } }
221 dependencyAnalysisSubExtension.issues { it.onCompileOnly { it.severity("ignore") } }
222
223 // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved
224 // Enable CI check for published libraries
225 if (
226 multiplatformExtension == null && androidXExtension.type == SoftwareType.PUBLISHED_LIBRARY
227 ) {
228 addToBuildOnServer(reportDependencyAnalysisAdviceTask)
229 }
230 }
231
232 /**
233 * Helper data classes to store the advice provided Dependency Analysis Gradle plugin in baselines.
234 */
235 internal data class AndroidxProjectAdvice(
236 val projectPath: String,
237 val dependencyAdvice: List<DependencyAdvice>
238 )
239
240 internal data class DependencyAdvice(
241 val coordinates: Coordinates,
242 val fromConfiguration: String?,
243 val toConfiguration: String?
244 )
245
246 internal data class Coordinates(
247 val type: String,
248 val identifier: String,
249 val resolvedVersion: String?
250 )
251
252 /** Convert advice reported by DAGP into format suitable for storing in baselines. */
toAndroidxProjectAdvicenull253 internal fun ProjectAdvice.toAndroidxProjectAdvice(): AndroidxProjectAdvice {
254 return AndroidxProjectAdvice(
255 projectPath = projectPath,
256 dependencyAdvice =
257 dependencyAdvice.map {
258 val type =
259 if (it.coordinates is ProjectCoordinates) {
260 "project"
261 } else {
262 "module"
263 }
264 val resolvedVersion =
265 if (it.coordinates is ModuleCoordinates) {
266 (it.coordinates as ModuleCoordinates).resolvedVersion
267 } else {
268 null
269 }
270 DependencyAdvice(
271 coordinates =
272 Coordinates(
273 identifier = it.coordinates.identifier,
274 resolvedVersion = resolvedVersion,
275 type = type
276 ),
277 fromConfiguration = it.fromConfiguration,
278 toConfiguration = it.toConfiguration
279 )
280 }
281 )
282 }
283