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