1 /*
<lambda>null2 * Copyright 2022 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.buildInfo
18
19 import androidx.build.AndroidXExtension
20 import androidx.build.AndroidXMultiplatformExtension
21 import androidx.build.LibraryGroup
22 import androidx.build.PlatformGroup
23 import androidx.build.PlatformIdentifier
24 import androidx.build.addToBuildOnServer
25 import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.TASK_NAME
26 import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.requiresDocs
27 import androidx.build.getBuildInfoDirectory
28 import androidx.build.getProjectZipPath
29 import androidx.build.getSupportRootFolder
30 import androidx.build.gitclient.getHeadShaProvider
31 import androidx.build.jetpad.LibraryBuildInfoFile
32 import com.google.common.annotations.VisibleForTesting
33 import com.google.gson.GsonBuilder
34 import java.io.File
35 import org.gradle.api.DefaultTask
36 import org.gradle.api.Project
37 import org.gradle.api.Task
38 import org.gradle.api.artifacts.Dependency
39 import org.gradle.api.artifacts.DependencyConstraint
40 import org.gradle.api.artifacts.ModuleVersionIdentifier
41 import org.gradle.api.artifacts.ProjectDependency
42 import org.gradle.api.component.ComponentWithCoordinates
43 import org.gradle.api.component.ComponentWithVariants
44 import org.gradle.api.file.RegularFileProperty
45 import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
46 import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependencyConstraint
47 import org.gradle.api.internal.artifacts.ivyservice.projectmodule.ProjectComponentPublication
48 import org.gradle.api.internal.component.SoftwareComponentInternal
49 import org.gradle.api.provider.ListProperty
50 import org.gradle.api.provider.Property
51 import org.gradle.api.provider.Provider
52 import org.gradle.api.provider.SetProperty
53 import org.gradle.api.publish.PublishingExtension
54 import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
55 import org.gradle.api.tasks.Input
56 import org.gradle.api.tasks.Optional
57 import org.gradle.api.tasks.OutputFile
58 import org.gradle.api.tasks.TaskAction
59 import org.gradle.api.tasks.TaskProvider
60 import org.gradle.kotlin.dsl.configure
61 import org.gradle.work.DisableCachingByDefault
62 import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
63
64 /**
65 * This task generates a library build information file containing the artifactId, groupId, and
66 * version of public androidx dependencies and release checklist of the library for consumption by
67 * the Jetpack Release Service (JetPad).
68 *
69 * Example: If this task is configured
70 * - for a project with group name "myGroup"
71 * - on a variant with artifactId "myArtifact",
72 * - and root project outDir is "out"
73 * - and environment variable DIST_DIR is not set
74 *
75 * then the build info file will be written to
76 * "out/dist/build-info/myGroup_myArtifact_build_info.txt"
77 */
78 @DisableCachingByDefault(because = "uses git sha as input")
79 abstract class CreateLibraryBuildInfoFileTask : DefaultTask() {
80 init {
81 group = "Help"
82 description = "Generates a file containing library build information serialized to json"
83 }
84
85 @get:OutputFile abstract val outputFile: RegularFileProperty
86
87 @get:Input abstract val artifactId: Property<String>
88
89 @get:Input abstract val groupId: Property<String>
90
91 @get:Input abstract val version: Property<String>
92
93 @get:Optional @get:Input abstract val kotlinVersion: Property<String>
94
95 @get:Input abstract val projectDir: Property<String>
96
97 @get:Input abstract val commit: Property<String>
98
99 @get:Input abstract val groupIdRequiresSameVersion: Property<Boolean>
100
101 @get:Input abstract val projectZipPath: Property<String>
102
103 @get:[Input Optional]
104 abstract val dependencyList: ListProperty<LibraryBuildInfoFile.Dependency>
105
106 @get:[Input Optional]
107 abstract val dependencyConstraintList: ListProperty<LibraryBuildInfoFile.Dependency>
108
109 @get:[Input Optional]
110 abstract val testModuleNames: SetProperty<String>
111
112 /** the local project directory without the full framework/support root directory path */
113 @get:Input abstract val projectSpecificDirectory: Property<String>
114
115 /** Whether the project should be included in docs-public/build.gradle. */
116 @get:Input abstract val shouldPublishDocs: Property<Boolean>
117
118 /** Whether the artifact is from a KMP project. */
119 @get:Input abstract val kmp: Property<Boolean>
120
121 /** The project's build target */
122 @get:Input abstract val target: Property<String>
123
124 /** The list of KMP artifact children */
125 @get:[Input Optional]
126 abstract val kmpChildren: SetProperty<String>
127
128 private fun writeJsonToFile(info: LibraryBuildInfoFile) {
129 val resolvedOutputFile: File = outputFile.get().asFile
130 val outputDir = resolvedOutputFile.parentFile
131 if (!outputDir.exists()) {
132 if (!outputDir.mkdirs()) {
133 throw RuntimeException("Failed to create output directory: $outputDir")
134 }
135 }
136 if (!resolvedOutputFile.exists()) {
137 if (!resolvedOutputFile.createNewFile()) {
138 throw RuntimeException(
139 "Failed to create output dependency dump file: $resolvedOutputFile"
140 )
141 }
142 }
143
144 // Create json object from the artifact instance
145 val gson = GsonBuilder().serializeNulls().setPrettyPrinting().create()
146 val serializedInfo: String = gson.toJson(info)
147 resolvedOutputFile.writeText(serializedInfo)
148 }
149
150 private fun resolveAndCollectDependencies(): LibraryBuildInfoFile {
151 val libraryBuildInfoFile = LibraryBuildInfoFile()
152 libraryBuildInfoFile.artifactId = artifactId.get()
153 libraryBuildInfoFile.groupId = groupId.get()
154 libraryBuildInfoFile.version = version.get()
155 libraryBuildInfoFile.path = projectDir.get()
156 libraryBuildInfoFile.sha = commit.get()
157 libraryBuildInfoFile.groupIdRequiresSameVersion = groupIdRequiresSameVersion.get()
158 libraryBuildInfoFile.projectZipPath = projectZipPath.get()
159 libraryBuildInfoFile.kotlinVersion = kotlinVersion.orNull
160 libraryBuildInfoFile.checks = ArrayList()
161 libraryBuildInfoFile.dependencies =
162 if (dependencyList.isPresent) ArrayList(dependencyList.get()) else ArrayList()
163 libraryBuildInfoFile.dependencyConstraints =
164 if (dependencyConstraintList.isPresent) ArrayList(dependencyConstraintList.get())
165 else ArrayList()
166 libraryBuildInfoFile.shouldPublishDocs = shouldPublishDocs.get()
167 libraryBuildInfoFile.isKmp = kmp.get()
168 libraryBuildInfoFile.target = target.get()
169 libraryBuildInfoFile.kmpChildren =
170 if (kmpChildren.isPresent) kmpChildren.get() else emptySet()
171 libraryBuildInfoFile.testModuleNames =
172 if (testModuleNames.isPresent) testModuleNames.get() else emptySet()
173 return libraryBuildInfoFile
174 }
175
176 /**
177 * Task: createLibraryBuildInfoFile Iterates through each configuration of the project and
178 * builds the set of all dependencies. Then adds each dependency to the Artifact class as a
179 * project or prebuilt dependency. Finally, writes these dependencies to a json file as a json
180 * object.
181 */
182 @TaskAction
183 fun createLibraryBuildInfoFile() {
184 val resolvedArtifact = resolveAndCollectDependencies()
185 writeJsonToFile(resolvedArtifact)
186 }
187
188 companion object {
189 const val TASK_NAME = "createLibraryBuildInfoFiles"
190
191 fun setup(
192 project: Project,
193 mavenGroup: LibraryGroup?,
194 variant: VariantPublishPlan,
195 shaProvider: Provider<String>,
196 shouldPublishDocs: Boolean,
197 isKmp: Boolean,
198 target: String,
199 kmpChildren: Set<String>,
200 testModuleNames: Provider<Set<String>>,
201 ): TaskProvider<CreateLibraryBuildInfoFileTask> {
202 return project.tasks.register(
203 TASK_NAME + variant.taskSuffix,
204 CreateLibraryBuildInfoFileTask::class.java
205 ) { task ->
206 val group = project.group.toString()
207 val artifactId = variant.artifactId
208 task.outputFile.set(
209 File(project.getBuildInfoDirectory(), "${group}_${artifactId}_build_info.txt")
210 )
211 task.artifactId.set(artifactId)
212 task.groupId.set(group)
213 task.version.set(project.version.toString())
214 task.kotlinVersion.set(project.getKotlinPluginVersion())
215 task.projectDir.set(
216 project.projectDir.absolutePath.removePrefix(
217 project.getSupportRootFolder().absolutePath
218 )
219 )
220 task.commit.set(shaProvider)
221 task.groupIdRequiresSameVersion.set(mavenGroup?.requireSameVersion ?: false)
222 task.projectZipPath.set(project.getProjectZipPath())
223
224 // Note:
225 // `project.projectDir.toString().removePrefix(project.rootDir.toString())`
226 // does not work because the project rootDir is not guaranteed to be a
227 // substring of the projectDir
228 task.projectSpecificDirectory.set(
229 project.projectDir.absolutePath.removePrefix(
230 project.getSupportRootFolder().absolutePath
231 )
232 )
233
234 // lazily compute the task dependency list based on the variant dependencies.
235 task.dependencyList.set(variant.dependencies.map { it.asBuildInfoDependencies() })
236 task.dependencyConstraintList.set(
237 variant.dependencyConstraints.map { it.asBuildInfoDependencies() }
238 )
239 task.shouldPublishDocs.set(shouldPublishDocs)
240 task.kmp.set(isKmp)
241 task.target.set(target)
242 task.kmpChildren.set(kmpChildren)
243
244 // We only want test module names for the parent build info file for Gradle projects
245 // that have multiple build info files, like KMP.
246 if (variant.taskSuffix.isBlank()) {
247 task.testModuleNames.set(testModuleNames)
248 }
249 }
250 }
251
252 fun List<Dependency>.asBuildInfoDependencies() =
253 filter { it.group.isAndroidXDependency() }
254 .map {
255 LibraryBuildInfoFile.Dependency().apply {
256 this.artifactId = it.name.toString()
257 this.groupId = it.group.toString()
258 this.version = it.version.toString()
259 this.isTipOfTree =
260 it is ProjectDependency || it is BuildInfoVariantDependency
261 }
262 }
263 .toHashSet()
264 .sortedWith(compareBy({ it.groupId }, { it.artifactId }, { it.version }))
265
266 @JvmName("dependencyConstraintsasBuildInfoDependencies")
267 fun List<DependencyConstraint>.asBuildInfoDependencies() =
268 filter { it.group.isAndroidXDependency() }
269 .map {
270 LibraryBuildInfoFile.Dependency().apply {
271 this.artifactId = it.name.toString()
272 this.groupId = it.group.toString()
273 this.version = it.version.toString()
274 this.isTipOfTree = it is DefaultProjectDependencyConstraint
275 }
276 }
277 .toHashSet()
278 .sortedWith(compareBy({ it.groupId }, { it.artifactId }, { it.version }))
279
280 private fun String?.isAndroidXDependency() =
281 this != null &&
282 startsWith("androidx.") &&
283 !startsWith("androidx.test") &&
284 !startsWith("androidx.databinding") &&
285 !startsWith("androidx.media3")
286 }
287 }
288
289 // Tasks that create a json files of a project's variant's dependencies
Projectnull290 fun Project.addCreateLibraryBuildInfoFileTasks(
291 androidXExtension: AndroidXExtension,
292 androidXKmpExtension: AndroidXMultiplatformExtension,
293 ) {
294 androidXExtension.ifReleasing {
295 val anchorTask = tasks.register("${TASK_NAME}Anchor")
296 addToBuildOnServer(anchorTask)
297 configure<PublishingExtension> {
298
299 /**
300 * Select the appropriate target based on if the project targets any Apple platforms
301 *
302 * If the project targets any Apple platform then the project can only be built on the
303 * 'androidx_multiplatform_mac' target. Otherwise the 'androidx' build target is used.
304 */
305 val buildTarget =
306 if (hasApplePlatform(androidXKmpExtension.supportedPlatforms)) {
307 "androidx_multiplatform_mac"
308 } else {
309 "androidx"
310 }
311
312 // Unfortunately, dependency information is only available through internal API
313 // (See https://github.com/gradle/gradle/issues/21345).
314 publications.withType(MavenPublicationInternal::class.java).configureEach { mavenPub ->
315 // java-gradle-plugin creates marker publications that are aliases of the
316 // main publication. We do not track these aliases.
317 if (!mavenPub.isAlias) {
318 createTaskForComponent(
319 anchorTask = anchorTask,
320 pub = mavenPub,
321 libraryGroup = androidXExtension.mavenGroup,
322 artifactId = mavenPub.artifactId,
323 shouldPublishDocs = androidXExtension.requiresDocs(),
324 isKmp = androidXKmpExtension.supportedPlatforms.isNotEmpty(),
325 buildTarget = buildTarget,
326 kmpChildren = androidXKmpExtension.supportedPlatforms.map { it.id }.toSet(),
327 testModuleNames = androidXExtension.testModuleNames,
328 isolatedProjectEnabled = androidXExtension.isIsolatedProjectsEnabled(),
329 )
330 }
331 }
332 }
333 }
334 }
335
Projectnull336 private fun Project.createTaskForComponent(
337 anchorTask: TaskProvider<Task>,
338 pub: ProjectComponentPublication,
339 libraryGroup: LibraryGroup?,
340 artifactId: String,
341 shouldPublishDocs: Boolean,
342 isKmp: Boolean,
343 buildTarget: String,
344 kmpChildren: Set<String>,
345 testModuleNames: Provider<Set<String>>,
346 isolatedProjectEnabled: Boolean,
347 ) {
348 val task =
349 createBuildInfoTask(
350 pub = pub,
351 libraryGroup = libraryGroup,
352 artifactId = artifactId,
353 shaProvider = getHeadShaProvider(project),
354 shouldPublishDocs = shouldPublishDocs,
355 isKmp = isKmp,
356 buildTarget = buildTarget,
357 kmpChildren = kmpChildren,
358 testModuleNames = testModuleNames,
359 )
360 anchorTask.configure { it.dependsOn(task) }
361 if (!isolatedProjectEnabled) {
362 addTaskToAggregateBuildInfoFileTask(task)
363 }
364 }
365
Projectnull366 private fun Project.createBuildInfoTask(
367 pub: ProjectComponentPublication,
368 libraryGroup: LibraryGroup?,
369 artifactId: String,
370 shaProvider: Provider<String>,
371 shouldPublishDocs: Boolean,
372 isKmp: Boolean,
373 buildTarget: String,
374 kmpChildren: Set<String>,
375 testModuleNames: Provider<Set<String>>,
376 ): TaskProvider<CreateLibraryBuildInfoFileTask> {
377 val kmpTaskSuffix = computeTaskSuffix(name, artifactId)
378 return CreateLibraryBuildInfoFileTask.setup(
379 project = project,
380 mavenGroup = libraryGroup,
381 variant =
382 VariantPublishPlan(
383 artifactId = artifactId,
384 taskSuffix = kmpTaskSuffix,
385 dependencies =
386 pub.component.map { component ->
387 val usageDependencies =
388 component.usages.orEmpty().flatMap { it.dependencies }
389 usageDependencies + dependenciesOnKmpVariants(component)
390 },
391 dependencyConstraints =
392 pub.component.map { component ->
393 component.usages.orEmpty().flatMap { it.dependencyConstraints }
394 }
395 ),
396 shaProvider = shaProvider,
397 // There's a build_info file for each KMP platform, but only the artifact without a platform
398 // suffix is listed in docs-public/build.gradle.
399 shouldPublishDocs = shouldPublishDocs && kmpTaskSuffix == "",
400 isKmp = isKmp,
401 target = buildTarget,
402 kmpChildren = kmpChildren.map { modifyKmpChildrenForBuildInfo(it) }.toSet(),
403 testModuleNames = testModuleNames,
404 )
405 }
406
modifyKmpChildrenForBuildInfonull407 private fun modifyKmpChildrenForBuildInfo(kmpChild: String): String {
408 // Jetbrains converts the "wasmJs" target to "wasm-js", which does not match the convention
409 // for other KMP targets. This is tracked in https://youtrack.jetbrains.com/issue/KT-70072
410 // For now, handle this case separately.
411 val specialMapping = mapOf("wasmJs" to "wasm-js")
412 return specialMapping[kmpChild] ?: kmpChild.lowercase()
413 }
414
dependenciesOnKmpVariantsnull415 private fun dependenciesOnKmpVariants(component: SoftwareComponentInternal) =
416 (component as? ComponentWithVariants)?.variants.orEmpty().mapNotNull {
417 (it as? ComponentWithCoordinates)?.coordinates?.asDependency()
418 }
419
ModuleVersionIdentifiernull420 private fun ModuleVersionIdentifier.asDependency() =
421 BuildInfoVariantDependency(group, name, version)
422
423 class BuildInfoVariantDependency(group: String, name: String, version: String) :
424 DefaultExternalModuleDependency(group, name, version)
425
426 // For examples, see CreateLibraryBuildInfoFileTaskTest
427 @VisibleForTesting
428 fun computeTaskSuffix(projectName: String, artifactId: String) =
429 artifactId.substringAfter(projectName).split("-").joinToString("") { word ->
430 word.replaceFirstChar { it.uppercase() }
431 }
432
433 /**
434 * Indicates if any of the given [PlatformIdentifier]s targets an Apple platform
435 *
436 * @param supportedPlatforms the set of [PlatformIdentifier] to examine
437 * @return true if any [PlatformIdentifier]s targets an Apple platform, false otherwise
438 */
439 @VisibleForTesting
hasApplePlatformnull440 fun hasApplePlatform(supportedPlatforms: Set<PlatformIdentifier>) =
441 supportedPlatforms.any { it.group == PlatformGroup.MAC }
442