Add --build-metadata-output option to D8/R8 CLI

Bug: b/290648731
Change-Id: Ia1de5da083720b61f62dba64f082f0530306fdab
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
index 48f5c36..8359c03 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommandParser.java
@@ -21,6 +21,7 @@
     C extends BaseCompilerCommand, B extends BaseCompilerCommand.Builder<C, B>> {
 
   protected static final String ART_PROFILE_FLAG = "--art-profile";
+  protected static final String BUILD_METADATA_OUTPUT_FLAG = "--build-metadata-output";
   protected static final String MIN_API_FLAG = "--min-api";
   protected static final String STARTUP_PROFILE_FLAG = "--startup-profile";
   protected static final String THREAD_COUNT_FLAG = "--thread-count";
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index a16189b..f7daa25 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
 import com.android.tools.r8.profile.startup.StartupProfileProviderUtils;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
@@ -18,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -45,7 +47,8 @@
           "--desugared-lib-pg-conf-output",
           THREAD_COUNT_FLAG,
           ART_PROFILE_FLAG,
-          STARTUP_PROFILE_FLAG);
+          STARTUP_PROFILE_FLAG,
+          BUILD_METADATA_OUTPUT_FLAG);
 
   // Note: this must be a subset of OPTIONS_WITH_ONE_PARAMETER.
   private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS = ImmutableSet.of(ART_PROFILE_FLAG);
@@ -213,6 +216,7 @@
   }
 
   private D8Command.Builder parse(String[] args, Origin origin, D8Command.Builder builder) {
+    Path buildMetadataOutputPath = null;
     CompilationMode compilationMode = null;
     Path outputPath = null;
     Path globalsOutputPath = null;
@@ -360,6 +364,19 @@
         Path startupProfilePath = Paths.get(nextArg);
         builder.addStartupProfileProviders(
             StartupProfileProviderUtils.createFromHumanReadableArtProfile(startupProfilePath));
+      } else if (arg.equals(BUILD_METADATA_OUTPUT_FLAG)) {
+        if (buildMetadataOutputPath != null) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot output build metadata to both '"
+                      + buildMetadataOutputPath
+                      + "' and '"
+                      + nextArg
+                      + "'",
+                  origin));
+          continue;
+        }
+        buildMetadataOutputPath = Paths.get(nextArg);
       } else if (arg.startsWith("--")) {
         if (tryParseAssertionArgument(builder, arg, origin)) {
           continue;
@@ -384,6 +401,17 @@
     if (!classpathBuilder.isEmpty()) {
       builder.addClasspathResourceProvider(classpathBuilder.build());
     }
+    if (buildMetadataOutputPath != null) {
+      final Path finalBuildMetadataOutputPath = buildMetadataOutputPath;
+      builder.setBuildMetadataConsumer(
+          buildMetadata -> {
+            try {
+              FileUtils.writeTextFile(finalBuildMetadataOutputPath, buildMetadata.toJson());
+            } catch (IOException e) {
+              throw new UncheckedIOException(e);
+            }
+          });
+    }
     if (compilationMode != null) {
       builder.setMode(compilationMode);
     }
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index 898f4eb..9765bfe 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
 import com.android.tools.r8.profile.startup.StartupProfileProviderUtils;
 import com.android.tools.r8.utils.ArchiveResourceProvider;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.MapIdTemplateProvider;
 import com.android.tools.r8.utils.SourceFileTemplateProvider;
@@ -22,6 +23,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -59,7 +62,8 @@
           "--source-file-template",
           ART_PROFILE_FLAG,
           STARTUP_PROFILE_FLAG,
-          THREAD_COUNT_FLAG);
+          THREAD_COUNT_FLAG,
+          BUILD_METADATA_OUTPUT_FLAG);
 
   // Note: this must be a subset of OPTIONS_WITH_ONE_PARAMETER.
   private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS =
@@ -208,6 +212,7 @@
 
   private void parse(
       String[] args, Origin argsOrigin, R8Command.Builder builder, ParseState state) {
+    Path buildMetadataOutputPath = null;
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
     FeatureSplitConfigCollector featureSplitConfigCollector = new FeatureSplitConfigCollector();
     for (int i = 0; i < expandedArgs.length; i++) {
@@ -359,6 +364,19 @@
         Path startupProfilePath = Paths.get(nextArg);
         builder.addStartupProfileProviders(
             StartupProfileProviderUtils.createFromHumanReadableArtProfile(startupProfilePath));
+      } else if (arg.equals(BUILD_METADATA_OUTPUT_FLAG)) {
+        if (buildMetadataOutputPath != null) {
+          builder.error(
+              new StringDiagnostic(
+                  "Cannot output build metadata to both '"
+                      + buildMetadataOutputPath
+                      + "' and '"
+                      + nextArg
+                      + "'",
+                  argsOrigin));
+          continue;
+        }
+        buildMetadataOutputPath = Paths.get(nextArg);
       } else if (arg.startsWith("--")) {
         if (tryParseAssertionArgument(builder, arg, argsOrigin)) {
           continue;
@@ -382,6 +400,17 @@
     }
     addFeatureSplitConfigs(
         builder, featureSplitConfigCollector.getConfigs(), state.includeDataResources);
+    if (buildMetadataOutputPath != null) {
+      final Path finalBuildMetadataOutputPath = buildMetadataOutputPath;
+      builder.setBuildMetadataConsumer(
+          buildMetadata -> {
+            try {
+              FileUtils.writeTextFile(finalBuildMetadataOutputPath, buildMetadata.toJson());
+            } catch (IOException e) {
+              throw new UncheckedIOException(e);
+            }
+          });
+    }
   }
 
   private void addFeatureSplitConfigs(
diff --git a/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
index cdcdbfb..b2e0a3a 100644
--- a/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
@@ -10,6 +10,8 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
 import com.android.tools.r8.LibraryDesugaringTestConfiguration;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -17,6 +19,9 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.Version;
 import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.origin.EmbeddedOrigin;
+import com.android.tools.r8.utils.FileUtils;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -38,7 +43,7 @@
   }
 
   @Test
-  public void test() throws Exception {
+  public void testApi() throws Exception {
     D8BuildMetadata buildMetadata =
         testForD8(parameters.getBackend())
             .addInnerClasses(getClass())
@@ -51,6 +56,32 @@
             .setMinApi(parameters)
             .compile()
             .getBuildMetadata();
+    inspect(buildMetadata);
+  }
+
+  @Test
+  public void testCli() throws Exception {
+    Path buildMetadataOutputPath = temp.newFile("d8.txt").toPath();
+    String[] args =
+        new String[] {
+          "--build-metadata-output",
+          buildMetadataOutputPath.toString(),
+          "--desugared-lib",
+          LibraryDesugaringSpecification.JDK11.getSpecification().toString(),
+          "--lib",
+          ToolHelper.getMostRecentAndroidJar().toString(),
+          "--min-api",
+          Integer.toString(parameters.getApiLevel().getLevel()),
+          "--release",
+          ToolHelper.getClassFileForTestClass(Main.class).toString()
+        };
+    D8.run(D8Command.parse(args, EmbeddedOrigin.INSTANCE).build());
+    D8BuildMetadata buildMetadata =
+        D8BuildMetadata.fromJson(FileUtils.readTextFile(buildMetadataOutputPath));
+    inspect(buildMetadata);
+  }
+
+  private void inspect(D8BuildMetadata buildMetadata) {
     String json = buildMetadata.toJson();
     System.out.println(json);
     // Inspecting the exact contents is not important here, but it *is* important to test that the