Add spdx-gradle-plugin to generate the R8 SBOM

This is using the plugins extension point to provide the download location
from the origin.json which is present in the downloaded dependencies in
third_party/dependencies and third_party/dependencies_new.

The spdx-gradle-plugin currently needs some patches to
work with the R8 project. These changes are in a GitHub fork intended
for upstream merging.

For now the plugin is built from that fork, which is given version
0.2.0-r8-patch01.

To support depending on this locally built plugin the
create_local_maven_with_dependencies.py script has been updated to also
be able to pull from a local repository until the official plugin version
can be used.

An additional third_party/dependencies_plugin has been added for Gradle
plugin dependencies.

The steps to build the plugin:

Pull the spdx-gradle-plugin fork https://github.com/spdx/spdx-gradle-plugin
and build it locally from 567d00cec5f3244ba657189ca1a4d08a823ce1b3 (branch
r8-patch01).

Build:

  ./gradlew -Dmaven.repo.local=/tmp/spdx-gradle-plugin publishToMavenLocal

Update third_party/dependencies_plugin:

  tools/create_local_maven_with_dependencies.py \
    --no-upload \
    --studio <studio-path> \
    --plugin-deps \
    --include-maven-local /tmp/spdx-gradle-plugin

Bug: b/280466318
Change-Id: I980ab44afded4704ed43565cb5f19abd1314b16e
diff --git a/.gitignore b/.gitignore
index 2fcc852..062fe33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,6 +93,8 @@
 third_party/dependencies.tar.gz
 third_party/dependencies_new/
 third_party/dependencies_new.tar.gz
+third_party/dependencies_plugin/
+third_party/dependencies_plugin.tar.gz
 third_party/desugar/desugar_*.tar.gz
 third_party/desugar/desugar_*/
 third_party/examples
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index 7ce6cc7..47b5da0 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -2,14 +2,25 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.net.URI
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.io.path.exists
+import java.nio.file.Files.readString
 import net.ltgt.gradle.errorprone.errorprone
+import org.gradle.api.artifacts.ModuleVersionIdentifier
 import org.gradle.api.artifacts.component.ModuleComponentIdentifier
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.spdx.sbom.gradle.SpdxSbomTask
+import org.spdx.sbom.gradle.extensions.DefaultSpdxSbomTaskExtension
+
+import com.google.gson.Gson
 
 plugins {
   `kotlin-dsl`
   id("dependencies-plugin")
   id("net.ltgt.errorprone") version "3.0.1"
+  id("org.spdx.sbom") version "0.2.0-r8-patch01"
 }
 
 java {
@@ -35,6 +46,33 @@
   errorprone(Deps.errorprone)
 }
 
+if (project.hasProperty("spdxVersion")) {
+  project.version = project.property("spdxVersion")
+}
+
+spdxSbom {
+  targets {
+    create("r8") {
+      // Use of both compileClasspath and runtimeClasspath due to how the
+      // dependencies jar is built and dependencies above therefore use
+      // compileOnly for actual runtime dependencies.
+      configurations.set(listOf("compileClasspath", "runtimeClasspath"))
+      scm {
+        uri.set("https://r8.googlesource.com/r8/")
+        if (project.hasProperty("spdxRevision")) {
+          revision.set(project.property("spdxRevision").toString())
+        }
+      }
+      document {
+        name.set("R8 Compiler Suite")
+        namespace.set("https://r8.googlesource.com/r8/-" + project.version + ".jar")
+        creator.set("Organization: Google LLC")
+        packageSupplier.set("Organization: Google LLC")
+      }
+    }
+  }
+}
+
 val keepAnnoJarTask = projectTask("keepanno", "jar")
 val resourceShrinkerJarTask = projectTask("resourceshrinker", "jar")
 val resourceShrinkerDepsTask = projectTask("resourceshrinker", "depsJar")
@@ -61,6 +99,50 @@
     dependsOn(gradle.includedBuild("shared").task(":downloadDeps"))
   }
 
+  withType<SpdxSbomTask> {
+    taskExtension.set(object : DefaultSpdxSbomTaskExtension() {
+      override fun mapRepoUri(input: URI?, moduleId: ModuleVersionIdentifier): URI? {
+
+        // Locate the file origin.json with URL for download location.
+        fun getOriginJson() : java.nio.file.Path {
+          var repositoryDir =
+              moduleId.group.replace('.', '/') + "/" + moduleId.name + "/" + moduleId.version
+          val path : Path =
+              Paths.get("third_party", "dependencies", repositoryDir, "origin.json");
+          val path_new : Path =
+              Paths.get("third_party", "dependencies_new", repositoryDir, "origin.json");
+          return if (path.exists()) path else path_new
+        }
+
+        // Simple data model of the content of origin.json generated by the tool to download
+        // and create a local repository. E.g.:
+        /*
+            {
+              "artifacts": [
+                {
+                  "file": "org/ow2/asm/asm/9.5/asm-9.5.pom",
+                  "repo": "https://repo1.maven.org/maven2/",
+                  "artifact": "org.ow2.asm:asm:pom:9.5"
+                },
+                {
+                  "file": "org/ow2/asm/asm/9.5/asm-9.5.jar",
+                  "repo": "https://repo1.maven.org/maven2/",
+                  "artifact": "org.ow2.asm:asm:jar:9.5"
+                }
+              ]
+            }
+        */
+        data class Artifact(val file: String, val repo: String, val artifact: String)
+        data class Artifacts(val artifacts: List<Artifact>)
+
+        // Read origin.json.
+        val json = readString(getOriginJson());
+        val artifacts = Gson().fromJson(json, Artifacts::class.java);
+        return URI.create(artifacts.artifacts.get(0).repo)
+      }
+    })
+  }
+
   val consolidatedLicense by registering {
     dependsOn(gradle.includedBuild("shared").task(":downloadDeps"))
     val root = getRoot()
diff --git a/d8_r8/main/settings.gradle.kts b/d8_r8/main/settings.gradle.kts
index 4e1d975..d4f7ab8 100644
--- a/d8_r8/main/settings.gradle.kts
+++ b/d8_r8/main/settings.gradle.kts
@@ -5,7 +5,7 @@
 pluginManagement {
   repositories {
     maven {
-      url = uri("file:../../third_party/dependencies")
+      url = uri("file:../../third_party/dependencies_plugin")
     }
     maven {
       url = uri("file:../../third_party/dependencies_new")
diff --git a/d8_r8/settings.gradle.kts b/d8_r8/settings.gradle.kts
index ac9216a..ad7577f 100644
--- a/d8_r8/settings.gradle.kts
+++ b/d8_r8/settings.gradle.kts
@@ -51,7 +51,7 @@
   process.waitFor()
   if (process.exitValue() != 0) {
     throw GradleException(
-      "Bootstrapping dependencies_new download failed:\n"
+      "Bootstrapping ${outputDir} download failed:\n"
         + "${String(process.getErrorStream().readAllBytes(),
                     java.nio.charset.StandardCharsets.UTF_8)}\n"
         + String(process.getInputStream().readAllBytes(),
@@ -68,6 +68,7 @@
 val thirdParty = getRepoRoot().resolve("third_party")
 downloadFromGoogleStorage(thirdParty.resolve("dependencies"))
 downloadFromGoogleStorage(thirdParty.resolve("dependencies_new"))
+downloadFromGoogleStorage(thirdParty.resolve("dependencies_plugin"))
 
 pluginManagement {
   repositories {
diff --git a/third_party/dependencies_plugin.tar.gz.sha1 b/third_party/dependencies_plugin.tar.gz.sha1
new file mode 100644
index 0000000..08fb793
--- /dev/null
+++ b/third_party/dependencies_plugin.tar.gz.sha1
@@ -0,0 +1 @@
+77f5f4042c4340df908bb39cdb40a67009195cac
\ No newline at end of file
diff --git a/tools/archive.py b/tools/archive.py
index 84c0da3..1632231 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -222,6 +222,12 @@
         default_pom_file = os.path.join(temp, 'r8.pom')
         create_maven_release.write_default_r8_pom_file(default_pom_file,
                                                        version)
+        gradle.RunGradle([
+            ':main:spdxSbom',
+            '-PspdxVersion=' + version,
+            '-PspdxRevision=' + GetGitHash()
+        ])
+
         for_archiving = [
             utils.R8_JAR, utils.R8LIB_JAR, utils.R8LIB_JAR + '.map',
             utils.R8LIB_JAR + '_map.zip', utils.R8_FULL_EXCLUDE_DEPS_JAR,
@@ -237,7 +243,9 @@
             utils.R8RETRACE_JAR + '_map.zip', utils.R8RETRACE_EXCLUDE_DEPS_JAR,
             utils.R8RETRACE_EXCLUDE_DEPS_JAR + '.map',
             utils.R8RETRACE_EXCLUDE_DEPS_JAR + '_map.zip',
-            utils.KEEPANNO_ANNOTATIONS_JAR, utils.GENERATED_LICENSE
+            utils.KEEPANNO_ANNOTATIONS_JAR,
+            utils.GENERATED_LICENSE,
+            'd8_r8/main/build/spdx/r8.spdx.json'
         ]
         for file in for_archiving:
             file_name = os.path.basename(file)
diff --git a/tools/create_local_maven_with_dependencies.py b/tools/create_local_maven_with_dependencies.py
index 4ae9d25..e5d8885 100755
--- a/tools/create_local_maven_with_dependencies.py
+++ b/tools/create_local_maven_with_dependencies.py
@@ -11,13 +11,13 @@
 
 import utils
 
-# The local_maven_repository_generator orderes the repositories by name, so
+# The local_maven_repository_generator orders the repositories by name, so
 # prefix with X- to control the order, as many dependencies are present
-# in several repositories.
+# in several repositories. Save A- for additional local repository.
 REPOSITORIES = [
-    'A-Google=https://maven.google.com/',
-    'B-Maven Central=https://repo1.maven.org/maven2/',
-    "C-Gradle Plugins=https://plugins.gradle.org/m2/",
+    'B-Google=https://maven.google.com/',
+    'C-Maven Central=https://repo1.maven.org/maven2/',
+    "D-Gradle Plugins=https://plugins.gradle.org/m2/",
 ]
 
 ANDRDID_SUPPORT_VERSION = '25.4.0'
@@ -97,6 +97,11 @@
         version=PROTOBUF_VERSION),
 ]
 
+PLUGIN_DEPENDENCIES = [
+  'org.spdx.sbom:org.spdx.sbom.gradle.plugin:0.2.0-r8-patch01',
+  # See https://github.com/FasterXML/jackson-core/issues/999.
+  'ch.randelshofer:fastdoubleparser:0.8.0',
+]
 
 def dependencies_tar(dependencies_path):
     return os.path.join(os.path.dirname(dependencies_path),
@@ -143,32 +148,89 @@
         required=True,
         help='Path to a studio-main checkout (to get the tool '
         '//tools/base/bazel:local_maven_repository_generator_cli)')
+    result.add_argument(
+        '--plugin-deps',
+        '--plugin_deps',
+        default=False,
+        action='store_true',
+        help='Build repository Gradle plugin dependncies')
+    result.add_argument(
+        '--include-maven-local',
+        '--include_maven_local',
+        metavar=('<path>'),
+        default=False,
+        help='Path to maven local repository to include as dependency source')
+    result.add_argument(
+        '--no-upload',
+        '--no_upload',
+        default=False,
+        action='store_true',
+        help="Don't upload to Google CLoud Storage")
     return result.parse_args()
 
 
+def set_utime(path):
+    os.utime(path, (0, 0))
+    for root, dirs, files in os.walk(path):
+      for f in files:
+          os.utime(os.path.join(root, f), (0, 0))
+      for d in dirs:
+          os.utime(os.path.join(root, d), (0, 0))
+
 def main():
     args = parse_options()
 
-    dependencies_path = os.path.join(utils.THIRD_PARTY, 'dependencies')
-    print("Downloading to " + dependencies_path)
-    remove_local_maven_repository(dependencies_path)
-    create_local_maven_repository(args, dependencies_path, REPOSITORIES,
-                                  BUILD_DEPENDENCIES + TEST_DEPENDENCIES)
+    repositories = REPOSITORIES
+    if args.include_maven_local:
+        # Add the local repository as the first for it to take precedence.
+        # Use A- prefix as current local_maven_repository_generator orderes by name.
+        repositories = ['A-Local=file://%s' % args.include_maven_local] + REPOSITORIES
 
-    dependencies_new_path = os.path.join(utils.THIRD_PARTY, 'dependencies_new')
-    print("Downloading to " + dependencies_new_path)
-    remove_local_maven_repository(dependencies_new_path)
-    create_local_maven_repository(args, dependencies_new_path, REPOSITORIES,
-                                  NEW_DEPENDENCIES)
+    dependencies = []
 
-    print("Uploading to Google Cloud Storage:")
-    with utils.ChangedWorkingDirectory(utils.THIRD_PARTY):
-        for dependency in ['dependencies', 'dependencies_new']:
-            cmd = [
-                'upload_to_google_storage.py', '-a', '--bucket', 'r8-deps',
-                dependency
-            ]
-            subprocess.check_call(cmd)
+    if args.plugin_deps:
+        dependencies_plugin_path = os.path.join(utils.THIRD_PARTY, 'dependencies_plugin')
+        remove_local_maven_repository(dependencies_plugin_path)
+        print("Downloading to " + dependencies_plugin_path)
+        create_local_maven_repository(
+            args, dependencies_plugin_path, repositories, PLUGIN_DEPENDENCIES)
+        set_utime(dependencies_plugin_path)
+        dependencies.append('dependencies_plugin')
+    else:
+        dependencies_path = os.path.join(utils.THIRD_PARTY, 'dependencies')
+        remove_local_maven_repository(dependencies_path)
+        print("Downloading to " + dependencies_path)
+        create_local_maven_repository(
+            args, dependencies_path, repositories, BUILD_DEPENDENCIES + TEST_DEPENDENCIES)
+        set_utime(dependencies_path)
+        dependencies.append('dependencies')
+        dependencies_new_path = os.path.join(utils.THIRD_PARTY, 'dependencies_new')
+        remove_local_maven_repository(dependencies_new_path)
+        print("Downloading to " + dependencies_new_path)
+        create_local_maven_repository(
+           args, dependencies_new_path, repositories, NEW_DEPENDENCIES)
+        set_utime(dependencies_new_path)
+        dependencies.append('dependencies_new')
+
+    upload_cmds = []
+    for dependency in dependencies:
+        upload_cmds.append([
+            'upload_to_google_storage.py',
+            '-a',
+            '--bucket',
+            'r8-deps',
+            dependency])
+
+    if not args.no_upload:
+        print("Uploading to Google Cloud Storage:")
+        with utils.ChangedWorkingDirectory(utils.THIRD_PARTY):
+            for cmd in upload_cmds:
+                subprocess.check_call(cmd)
+    else:
+        print("Not uploading to Google Cloud Storage. "
+            + "Run the following commands in %s to do so manually" % utils.THIRD_PARTY)
+        for cmd in upload_cmds:
+            print(" ".join(cmd))
 
 
 if __name__ == '__main__':