| /* |
| * Copyright 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.build.sbom |
| |
| import androidx.build.AndroidXPlaygroundRootImplPlugin |
| import androidx.build.BundleInsideHelper |
| import androidx.build.GMavenZipTask |
| import androidx.build.ProjectLayoutType |
| import androidx.build.addToBuildOnServer |
| import androidx.build.getDistributionDirectory |
| import androidx.build.getPrebuiltsRoot |
| import androidx.build.getSupportRootFolder |
| import androidx.build.gitclient.getHeadShaProvider |
| import androidx.inspection.gradle.EXPORT_INSPECTOR_DEPENDENCIES |
| import androidx.inspection.gradle.IMPORT_INSPECTOR_DEPENDENCIES |
| import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar |
| import java.io.File |
| import java.net.URI |
| import java.util.UUID |
| import org.gradle.api.GradleException |
| import org.gradle.api.Project |
| import org.gradle.api.artifacts.Configuration |
| import org.gradle.api.artifacts.ModuleVersionIdentifier |
| import org.gradle.api.tasks.Copy |
| import org.gradle.api.tasks.bundling.AbstractArchiveTask |
| import org.gradle.api.tasks.bundling.Zip |
| import org.gradle.jvm.tasks.Jar |
| import org.gradle.kotlin.dsl.apply |
| import org.gradle.kotlin.dsl.getByType |
| import org.spdx.sbom.gradle.SpdxSbomExtension |
| import org.spdx.sbom.gradle.SpdxSbomTask |
| import org.spdx.sbom.gradle.extensions.DefaultSpdxSbomTaskExtension |
| import org.spdx.sbom.gradle.project.ProjectInfo |
| import org.spdx.sbom.gradle.project.ScmInfo |
| |
| /** |
| * Tells whether the contents of the Configuration with the given name should be listed in our sbom |
| * |
| * That is, this tells whether the corresponding Configuration contains dependencies that get |
| * embedded into our build artifact |
| */ |
| fun Project.shouldSbomIncludeConfigurationName(configurationName: String): Boolean { |
| return when (configurationName) { |
| BundleInsideHelper.CONFIGURATION_NAME -> true |
| "shadowed" -> true |
| // compileClasspath is included by the Shadow plugin by default but projects that |
| // declare a "shadowed" configuration exclude the "compileClasspath" configuration from |
| // the shadowJar task |
| "compileClasspath" -> |
| appliesShadowPlugin() && project.configurations.findByName("shadowed") == null |
| EXPORT_INSPECTOR_DEPENDENCIES -> true |
| IMPORT_INSPECTOR_DEPENDENCIES -> true |
| // https://github.com/spdx/spdx-gradle-plugin/issues/12 |
| sbomEmptyConfiguration -> true |
| else -> false |
| } |
| } |
| |
| // An empty Configuration for the sbom plugin to ensure it has at least one Configuration |
| private val sbomEmptyConfiguration = "sbomEmpty" |
| |
| // some tasks that don't embed configurations having external dependencies |
| private val excludeTaskNames = |
| setOf( |
| "distZip", |
| "shadowDistZip", |
| "annotationsZip", |
| "protoLiteJar", |
| "bundleDebugLocalLintAar", |
| "bundleReleaseLocalLintAar", |
| "bundleDebugAar", |
| "bundleReleaseAar" |
| ) |
| |
| /** |
| * Lists the Configurations that we should declare we're embedding into the output of this task |
| * |
| * The immediate inputs to the task are not generally mentioned here: external entities aren't |
| * interested in knowing that our .aar file contains a classes.jar |
| * |
| * The external dependencies that embed into our artifacts are what we mention here: external |
| * entities might be interested in knowing if, for example, we embed protobuf-javalite into our |
| * artifact |
| * |
| * The purpose of this function is to detect new archive tasks and remind developers to update |
| * shouldSbomIncludeConfigurationName |
| */ |
| fun Project.listSbomConfigurationNamesForArchive(task: AbstractArchiveTask): List<String> { |
| if (task is Jar && !(task is ShadowJar)) { |
| // Jar tasks don't generally embed other dependencies in them |
| return listOf() |
| } |
| if (task is GMavenZipTask) { |
| // A GMavenZipTask just zips one or more artifacts we've already built |
| return listOf() |
| } |
| |
| val projectPath = project.path |
| val taskName = task.name |
| |
| // some tasks that embed other configurations |
| if (taskName == BundleInsideHelper.REPACKAGE_TASK_NAME) { |
| return listOf(BundleInsideHelper.CONFIGURATION_NAME) |
| } |
| if ( |
| projectPath.contains("inspection") && |
| (taskName == "assembleInspectorJarRelease" || |
| taskName == "inspectionShadowDependenciesRelease") |
| ) { |
| return listOf(EXPORT_INSPECTOR_DEPENDENCIES) |
| } |
| |
| if (excludeTaskNames.contains(taskName)) return listOf() |
| if (projectPath == ":compose:lint:internal-lint-checks") |
| return listOf() // we don't publish these lint checks |
| if (projectPath.contains("integration-tests")) |
| return listOf() // we don't publish integration tests |
| if (taskName.startsWith("zip") && taskName.contains("ResultsOf") && taskName.contains("Test")) |
| return listOf() // we don't publish test results |
| if (projectPath == ":compose:compiler:compiler" && taskName == "embeddedPlugin") return listOf() |
| |
| // ShadowJar tasks have a `configurations` property that lists the configurations that |
| // are inputs to the task, but they don't also list file inputs |
| // If a project only has one shadowJar task (named "shadowJar"), for now we assume |
| // that it doesn't include any external files that aren't already declared in |
| // its configurations. |
| // If a project has multiple shadowJar tasks, we ask the developer to provide |
| // this metadata somehow by failing below |
| if (taskName == "shadowJar") { |
| // If the task is a ShadowJar task, we can just ask it which configurations it intends to |
| // embed |
| // We separately validate that this list is correct in |
| val shadowTask = task as? ShadowJar |
| if (shadowTask != null) { |
| val configurations = |
| project.configurations.filter { conf -> shadowTask.configurations.contains(conf) } |
| return configurations.map { conf -> conf.name } |
| } |
| } |
| |
| if (taskName == "stubAar") { |
| return listOf() |
| } |
| |
| throw GradleException( |
| "Not sure which external dependencies are included in $projectPath:$taskName of type " + |
| "${task::class.java} (this is used for publishing sboms). Please update " + |
| "Sbom.kt's listSbomConfigurationNamesForArchive and " + |
| "shouldSbomIncludeConfigurationName" |
| ) |
| } |
| |
| /** Returns which configurations are used by the given task that we should list in an sbom */ |
| fun Project.listSbomConfigurationsForArchive(task: AbstractArchiveTask): List<Configuration> { |
| val configurationNames = listSbomConfigurationNamesForArchive(task) |
| return configurationNames.map { configurationName -> |
| val resolved = project.configurations.findByName(configurationName) |
| if (resolved == null) { |
| throw GradleException( |
| "listSbomConfigurationsForArchive($task) expected to find " + |
| "configuration $configurationName but it does not exist" |
| ) |
| } |
| resolved |
| } |
| } |
| |
| /** Validates that the inputs of the given archive task are recognized */ |
| fun Project.validateArchiveInputsRecognized(task: AbstractArchiveTask) { |
| val configurationNames = task.project.listSbomConfigurationNamesForArchive(task) |
| for (configurationName in configurationNames) { |
| if (!task.project.shouldSbomIncludeConfigurationName(configurationName)) { |
| throw GradleException( |
| "Task listSbomConfigurationNamesForArchive(\"${task.name}\") = " + |
| "$configurationNames but " + |
| "shouldSbomIncludeConfigurationName(\"$configurationName\") = false. " + |
| "You probably should update shouldSbomIncludeConfigurationName to match" |
| ) |
| } |
| } |
| } |
| |
| /** Validates that the inputs of each archive task are recognized */ |
| fun Project.validateAllArchiveInputsRecognized() { |
| project.tasks.withType(Zip::class.java).configureEach { task -> |
| project.validateArchiveInputsRecognized(task) |
| } |
| project.tasks.withType(ShadowJar::class.java).configureEach { task -> |
| project.validateArchiveInputsRecognized(task) |
| } |
| } |
| |
| /** Enables the publishing of an sbom that lists our embedded dependencies */ |
| fun Project.configureSbomPublishing() { |
| val uuid = project.coordinatesToUUID().toString() |
| val projectName = project.name |
| val projectVersion = project.version.toString() |
| |
| project.configurations.create(sbomEmptyConfiguration) { emptyConfiguration -> |
| emptyConfiguration.isCanBeConsumed = false |
| } |
| project.apply(plugin = "org.spdx.sbom") |
| val repos = getRepoPublicUrls() |
| val headShaProvider = getHeadShaProvider(project) |
| val supportRootDir = getSupportRootFolder() |
| |
| val allowPublicRepos = System.getenv("ALLOW_PUBLIC_REPOS") != null |
| val sbomPublishDir = project.getSbomPublishDir() |
| |
| val sbomBuiltFile = |
| project.layout.buildDirectory.file("spdx/release.spdx.json").get().getAsFile() |
| |
| val publishTask = |
| project.tasks.register("exportSboms", Copy::class.java) { publishTask -> |
| publishTask.destinationDir = sbomPublishDir |
| val sbomBuildDir = sbomBuiltFile.parentFile |
| publishTask.from(sbomBuildDir) |
| publishTask.rename(sbomBuiltFile.name, "$projectName-$projectVersion.spdx.json") |
| |
| publishTask.doFirst { |
| if (!sbomBuiltFile.exists()) { |
| throw GradleException("sbom file does not exist: $sbomBuiltFile") |
| } |
| } |
| } |
| |
| project.tasks.withType(SpdxSbomTask::class.java).configureEach { task -> |
| val sbomProjectDir = project.projectDir |
| |
| task.taskExtension.set( |
| object : DefaultSpdxSbomTaskExtension() { |
| override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI { |
| val uriString = repoUri.toString() |
| for (repo in repos) { |
| val ourRepoUrl = repo.key |
| val publicRepoUrl = repo.value |
| if (uriString.startsWith(ourRepoUrl)) { |
| return URI.create(publicRepoUrl) |
| } |
| if (allowPublicRepos) { |
| if (uriString.startsWith(publicRepoUrl)) { |
| return URI.create(publicRepoUrl) |
| } |
| } |
| } |
| throw GradleException( |
| "Cannot determine public repo url for repo $uriString artifact $artifact" |
| ) |
| } |
| |
| override fun mapScmForProject( |
| original: ScmInfo, |
| projectInfo: ProjectInfo |
| ): ScmInfo { |
| val url = getGitRemoteUrl(projectInfo.projectDirectory, supportRootDir) |
| return ScmInfo.from("git", url, headShaProvider.get()) |
| } |
| |
| override fun shouldCreatePackageForProject(projectInfo: ProjectInfo): Boolean { |
| // sbom should include the project it describes |
| if (sbomProjectDir.equals(projectInfo.projectDirectory)) return true |
| // sbom doesn't need to list our projects as dependencies; |
| // they're implementation details |
| // Example: glance:glance-appwidget uses glance:glance-appwidget-proto |
| if (pathContains(supportRootDir, projectInfo.projectDirectory)) return false |
| // sbom should list remaining project dependencies |
| return true |
| } |
| } |
| ) |
| } |
| |
| val sbomExtension = project.extensions.getByType<SpdxSbomExtension>() |
| val sbomConfigurations = mutableListOf<String>() |
| |
| project.afterEvaluate { |
| project.configurations.configureEach { configuration -> |
| if (shouldSbomIncludeConfigurationName(configuration.name)) { |
| sbomConfigurations.add(configuration.getName()) |
| } |
| } |
| |
| sbomExtension.targets.create("release") { target -> |
| val googleOrganization = "Organization: Google LLC" |
| val document = target.document |
| document.namespace.set("https://spdx.google.com/$uuid") |
| document.creator.set(googleOrganization) |
| document.packageSupplier.set(googleOrganization) |
| |
| target.getConfigurations().set(sbomConfigurations) |
| } |
| project.addToBuildOnServer(tasks.named("spdxSbomForRelease")) |
| publishTask.configure { task -> task.dependsOn("spdxSbomForRelease") } |
| } |
| } |
| |
| // Returns a UUID whose contents are based on the project's coordinates (group:artifact:version) |
| fun Project.coordinatesToUUID(): UUID { |
| val coordinates = "${project.group}:${project.name}:${project.version}" |
| val bytes = coordinates.toByteArray() |
| return UUID.nameUUIDFromBytes(bytes) |
| } |
| |
| fun pathContains(ancestor: File, child: File): Boolean { |
| val childNormalized = child.getCanonicalPath() + File.separator |
| val ancestorNormalized = ancestor.getCanonicalPath() + File.separator |
| return childNormalized.startsWith(ancestorNormalized) |
| } |
| |
| fun getGitRemoteUrl(dir: File, supportRootDir: File): String { |
| if (pathContains(supportRootDir, dir)) { |
| return "android.googlesource.com/platform/frameworks/support" |
| } |
| |
| val notoFontsDir = File("$supportRootDir/../../external/noto-fonts") |
| if (pathContains(notoFontsDir, dir)) { |
| return "android.googlesource.com/platform/external/noto-fonts" |
| } |
| |
| val icingDir = File("$supportRootDir/../../external/icing") |
| if (pathContains(icingDir, dir)) { |
| return "android.googlesource.com/platform/external/icing" |
| } |
| throw GradleException("Could not identify git remote url for project at $dir") |
| } |
| |
| fun Project.getSbomPublishDir(): File { |
| val groupPath = project.group.toString().replace(".", "/") |
| return File(getDistributionDirectory(), "sboms/$groupPath/${project.name}/${project.version}") |
| } |
| |
| private const val MAVEN_CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2" |
| private const val GMAVEN_REPO_URL = "https://dl.google.com/android/maven2" |
| /** Returns a mapping from local repo url to public repo url */ |
| private fun Project.getRepoPublicUrls(): Map<String, String> { |
| return if (ProjectLayoutType.isPlayground(this)) { |
| mapOf( |
| MAVEN_CENTRAL_REPO_URL to MAVEN_CENTRAL_REPO_URL, |
| AndroidXPlaygroundRootImplPlugin.INTERNAL_PREBUILTS_REPO_URL to GMAVEN_REPO_URL |
| ) |
| } else { |
| mapOf( |
| "file:${project.getPrebuiltsRoot()}/androidx/external" to MAVEN_CENTRAL_REPO_URL, |
| "file:${project.getPrebuiltsRoot()}/androidx/internal" to GMAVEN_REPO_URL |
| ) |
| } |
| } |
| |
| private fun Project.appliesShadowPlugin() = |
| pluginManager.hasPlugin("com.github.johnrengelman.shadow") |