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