1 /*
<lambda>null2  * Copyright 2023 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.sbom
18 
19 import androidx.build.AndroidXPlaygroundRootImplPlugin
20 import androidx.build.BundleInsideHelper
21 import androidx.build.ProjectLayoutType
22 import androidx.build.addToBuildOnServer
23 import androidx.build.getDistributionDirectory
24 import androidx.build.getPrebuiltsRoot
25 import androidx.build.getSupportRootFolder
26 import androidx.build.gitclient.getHeadShaProvider
27 import androidx.inspection.gradle.EXPORT_INSPECTOR_DEPENDENCIES
28 import androidx.inspection.gradle.IMPORT_INSPECTOR_DEPENDENCIES
29 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
30 import java.io.File
31 import java.net.URI
32 import java.util.UUID
33 import org.gradle.api.GradleException
34 import org.gradle.api.Project
35 import org.gradle.api.artifacts.ModuleVersionIdentifier
36 import org.gradle.api.tasks.Copy
37 import org.gradle.api.tasks.bundling.AbstractArchiveTask
38 import org.gradle.api.tasks.bundling.Zip
39 import org.gradle.jvm.tasks.Jar
40 import org.gradle.kotlin.dsl.apply
41 import org.gradle.kotlin.dsl.getByType
42 import org.spdx.sbom.gradle.SpdxSbomExtension
43 import org.spdx.sbom.gradle.SpdxSbomTask
44 import org.spdx.sbom.gradle.extensions.DefaultSpdxSbomTaskExtension
45 import org.spdx.sbom.gradle.project.ProjectInfo
46 import org.spdx.sbom.gradle.project.ScmInfo
47 
48 /**
49  * Tells whether the contents of the Configuration with the given name should be listed in our sbom
50  *
51  * That is, this tells whether the corresponding Configuration contains dependencies that get
52  * embedded into our build artifact
53  */
54 private fun Project.shouldSbomIncludeConfigurationName(configurationName: String): Boolean {
55     return when (configurationName) {
56         BundleInsideHelper.CONFIGURATION_NAME -> true
57         "shadowed" -> true
58         // compileClasspath is included by the Shadow plugin by default but projects that
59         // declare a "shadowed" configuration exclude the "compileClasspath" configuration from
60         // the shadowJar task
61         "compileClasspath" -> appliesShadowPlugin() && configurations.findByName("shadowed") == null
62         EXPORT_INSPECTOR_DEPENDENCIES -> true
63         IMPORT_INSPECTOR_DEPENDENCIES -> true
64         // https://github.com/spdx/spdx-gradle-plugin/issues/12
65         sbomEmptyConfiguration -> true
66         else -> false
67     }
68 }
69 
70 // An empty Configuration for the sbom plugin to ensure it has at least one Configuration
71 private const val sbomEmptyConfiguration = "sbomEmpty"
72 
73 // some tasks that don't embed configurations having external dependencies
74 private val excludeTaskNames =
75     setOf(
76         "distZip",
77         "shadowDistZip",
78         "annotationsZip",
79         "protoLiteJar",
80         "bundleDebugLocalLintAar",
81         "bundleReleaseLocalLintAar",
82         "bundleDebugAar",
83         "bundleReleaseAar",
84         "bundleAndroidMainAar",
85         "bundleAndroidMainLocalLintAar",
86         "repackageAndroidMainAar",
87         "repackageAarWithResourceApiAndroidMain"
88     )
89 
90 /**
91  * Lists the Configurations that we should declare we're embedding into the output of this task
92  *
93  * The immediate inputs to the task are not generally mentioned here: external entities aren't
94  * interested in knowing that our .aar file contains a classes.jar
95  *
96  * The external dependencies that embed into our artifacts are what we mention here: external
97  * entities might be interested in knowing if, for example, we embed protobuf-javalite into our
98  * artifact
99  *
100  * The purpose of this function is to detect new archive tasks and remind developers to update
101  * shouldSbomIncludeConfigurationName
102  */
Projectnull103 private fun Project.listSbomConfigurationNamesForArchive(task: AbstractArchiveTask): List<String> {
104     if (task is Jar && task !is ShadowJar) {
105         // Jar tasks don't generally embed other dependencies in them
106         return listOf()
107     }
108     if (task is Zip && task.name.endsWith("Klib")) {
109         // klib zip tasks don't generally embed other dependencies in them
110         return listOf()
111     }
112 
113     val projectPath = path
114     val taskName = task.name
115 
116     // some tasks that embed other configurations
117     if (taskName == BundleInsideHelper.REPACKAGE_TASK_NAME) {
118         return listOf(BundleInsideHelper.CONFIGURATION_NAME)
119     }
120     if (
121         projectPath.contains("inspection") &&
122             (taskName == "assembleInspectorJarRelease" ||
123                 taskName == "inspectionShadowDependenciesRelease")
124     ) {
125         return listOf(EXPORT_INSPECTOR_DEPENDENCIES)
126     }
127 
128     if (excludeTaskNames.contains(taskName)) return listOf()
129     if (projectPath == ":compose:lint:internal-lint-checks")
130         return listOf() // we don't publish these lint checks
131     if (projectPath.contains("integration-tests"))
132         return listOf() // we don't publish integration tests
133     if (taskName.startsWith("zip") && taskName.contains("ResultsOf") && taskName.contains("Test"))
134         return listOf() // we don't publish test results
135 
136     // ShadowJar tasks have a `configurations` property that lists the configurations that
137     // are inputs to the task, but they don't also list file inputs
138     // If a project only has one shadowJar task (named "shadowJar"), for now we assume
139     // that it doesn't include any external files that aren't already declared in
140     // its configurations.
141     // If a project has multiple shadowJar tasks, we ask the developer to provide
142     // this metadata somehow by failing below
143     if (taskName == "shadowJar" || taskName == "shadowLibraryJar") {
144         // If the task is a ShadowJar task, we can just ask it which configurations it intends to
145         // embed
146         // We separately validate that this list is correct in
147         val shadowTask = task as? ShadowJar
148         if (shadowTask != null) {
149             val configurations =
150                 configurations.filter { conf -> shadowTask.configurations.contains(conf) }
151             return configurations.map { conf -> conf.name }
152         }
153     }
154 
155     if (taskName == "stubAar") {
156         return listOf()
157     }
158 
159     throw GradleException(
160         "Not sure which external dependencies are included in $projectPath:$taskName of type " +
161             "${task::class.java} (this is used for publishing sboms). Please update " +
162             "Sbom.kt's listSbomConfigurationNamesForArchive and " +
163             "shouldSbomIncludeConfigurationName"
164     )
165 }
166 
167 /** Validates that the inputs of the given archive task are recognized */
Projectnull168 private fun Project.validateArchiveInputsRecognized(task: AbstractArchiveTask) {
169     val configurationNames = listSbomConfigurationNamesForArchive(task)
170     for (configurationName in configurationNames) {
171         if (!shouldSbomIncludeConfigurationName(configurationName)) {
172             throw GradleException(
173                 "Task listSbomConfigurationNamesForArchive(\"${task.name}\") = " +
174                     "$configurationNames but " +
175                     "shouldSbomIncludeConfigurationName(\"$configurationName\") = false. " +
176                     "You probably should update shouldSbomIncludeConfigurationName to match"
177             )
178         }
179     }
180 }
181 
182 /** Validates that the inputs of each archive task are recognized */
Projectnull183 fun Project.validateAllArchiveInputsRecognized() {
184     tasks.withType(Zip::class.java).configureEach { task -> validateArchiveInputsRecognized(task) }
185     tasks.withType(ShadowJar::class.java).configureEach { task ->
186         validateArchiveInputsRecognized(task)
187     }
188 }
189 
190 /** Enables the publishing of an sbom that lists our embedded dependencies */
Projectnull191 fun Project.configureSbomPublishing() {
192     val uuid = coordinatesToUUID().toString()
193     val projectName = name
194     val projectVersion = version.toString()
195 
196     configurations.create(sbomEmptyConfiguration) { emptyConfiguration ->
197         emptyConfiguration.isCanBeConsumed = false
198     }
199     apply(plugin = "org.spdx.sbom")
200     val repos = getRepoPublicUrls()
201     val headShaProvider = getHeadShaProvider(this)
202     val supportRootDir = getSupportRootFolder()
203 
204     val allowPublicRepos = System.getenv("ALLOW_PUBLIC_REPOS") != null
205     val sbomPublishDir = getSbomPublishDir()
206 
207     val sbomBuiltFile = layout.buildDirectory.file("spdx/release.spdx.json").get().asFile
208 
209     val publishTask =
210         tasks.register("exportSboms", Copy::class.java) { publishTask ->
211             publishTask.destinationDir = sbomPublishDir
212             val sbomBuildDir = sbomBuiltFile.parentFile
213             publishTask.from(sbomBuildDir)
214             publishTask.rename(sbomBuiltFile.name, "$projectName-$projectVersion.spdx.json")
215 
216             publishTask.doFirst {
217                 if (!sbomBuiltFile.exists()) {
218                     throw GradleException("sbom file does not exist: $sbomBuiltFile")
219                 }
220             }
221         }
222 
223     tasks.withType(SpdxSbomTask::class.java).configureEach { task ->
224         val sbomProjectDir = projectDir
225 
226         task.taskExtension.set(
227             object : DefaultSpdxSbomTaskExtension() {
228                 override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI {
229                     val uriString = repoUri.toString()
230                     for (repo in repos) {
231                         val ourRepoUrl = repo.key
232                         val publicRepoUrl = repo.value
233                         if (uriString.startsWith(ourRepoUrl)) {
234                             return URI.create(publicRepoUrl)
235                         }
236                         if (allowPublicRepos) {
237                             if (uriString.startsWith(publicRepoUrl)) {
238                                 return URI.create(publicRepoUrl)
239                             }
240                         }
241                     }
242                     throw GradleException(
243                         "Cannot determine public repo url for repo $uriString artifact $artifact"
244                     )
245                 }
246 
247                 override fun mapScmForProject(
248                     original: ScmInfo,
249                     projectInfo: ProjectInfo
250                 ): ScmInfo {
251                     val url = getGitRemoteUrl(projectInfo.projectDirectory, supportRootDir)
252                     return ScmInfo.from("git", url, headShaProvider.get())
253                 }
254 
255                 override fun shouldCreatePackageForProject(projectInfo: ProjectInfo): Boolean {
256                     // sbom should include the project it describes
257                     if (sbomProjectDir.equals(projectInfo.projectDirectory)) return true
258                     // sbom doesn't need to list our projects as dependencies;
259                     // they're implementation details
260                     // Example: glance:glance-appwidget uses glance:glance-appwidget-proto
261                     if (pathContains(supportRootDir, projectInfo.projectDirectory)) return false
262                     // sbom should list remaining project dependencies
263                     return true
264                 }
265             }
266         )
267     }
268 
269     val sbomExtension = extensions.getByType<SpdxSbomExtension>()
270     val sbomConfigurations = mutableListOf<String>()
271 
272     afterEvaluate {
273         configurations.configureEach { configuration ->
274             if (shouldSbomIncludeConfigurationName(configuration.name)) {
275                 sbomConfigurations.add(configuration.name)
276             }
277         }
278 
279         sbomExtension.targets.create("release") { target ->
280             val googleOrganization = "Organization: Google LLC"
281             val document = target.document
282             document.namespace.set("https://spdx.google.com/$uuid")
283             document.creator.set(googleOrganization)
284             document.packageSupplier.set(googleOrganization)
285 
286             target.configurations.set(sbomConfigurations)
287         }
288         addToBuildOnServer(tasks.named("spdxSbomForRelease"))
289         publishTask.configure { task -> task.dependsOn("spdxSbomForRelease") }
290     }
291 }
292 
293 // Returns a UUID whose contents are based on the project's coordinates (group:artifact:version)
Projectnull294 private fun Project.coordinatesToUUID(): UUID {
295     val coordinates = "$group:$name:$version"
296     val bytes = coordinates.toByteArray()
297     return UUID.nameUUIDFromBytes(bytes)
298 }
299 
pathContainsnull300 private fun pathContains(ancestor: File, child: File): Boolean {
301     val childNormalized = child.getCanonicalPath() + File.separator
302     val ancestorNormalized = ancestor.getCanonicalPath() + File.separator
303     return childNormalized.startsWith(ancestorNormalized)
304 }
305 
getGitRemoteUrlnull306 private fun getGitRemoteUrl(dir: File, supportRootDir: File): String {
307     if (pathContains(supportRootDir, dir)) {
308         return "android.googlesource.com/platform/frameworks/support"
309     }
310 
311     val notoFontsDir = File("$supportRootDir/../../external/noto-fonts")
312     if (pathContains(notoFontsDir, dir)) {
313         return "android.googlesource.com/platform/external/noto-fonts"
314     }
315 
316     val icingDir = File("$supportRootDir/../../external/icing")
317     if (pathContains(icingDir, dir)) {
318         return "android.googlesource.com/platform/external/icing"
319     }
320     throw GradleException("Could not identify git remote url for project at $dir")
321 }
322 
Projectnull323 private fun Project.getSbomPublishDir(): File {
324     val groupPath = group.toString().replace(".", "/")
325     return File(getDistributionDirectory(), "sboms/$groupPath/$name/$version")
326 }
327 
328 private const val MAVEN_CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2"
329 private const val GMAVEN_REPO_URL = "https://dl.google.com/android/maven2"
330 
331 /** Returns a mapping from local repo url to public repo url */
getRepoPublicUrlsnull332 private fun Project.getRepoPublicUrls(): Map<String, String> {
333     return if (ProjectLayoutType.isPlayground(this)) {
334         mapOf(
335             MAVEN_CENTRAL_REPO_URL to MAVEN_CENTRAL_REPO_URL,
336             AndroidXPlaygroundRootImplPlugin.INTERNAL_PREBUILTS_REPO_URL to GMAVEN_REPO_URL
337         )
338     } else {
339         mapOf(
340             "file:${getPrebuiltsRoot()}/androidx/external" to MAVEN_CENTRAL_REPO_URL,
341             "file:${getPrebuiltsRoot()}/androidx/internal" to GMAVEN_REPO_URL
342         )
343     }
344 }
345 
Projectnull346 private fun Project.appliesShadowPlugin() = pluginManager.hasPlugin("com.gradleup.shadow")
347