Add API for outputting build metadata

Bug: b/356283022
Change-Id: I9674b33779927cc2eb4713e3a2840dc43873c0d3
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index a5caffa..bf1f0c0 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.keepanno.asm.KeepEdgeReader;
 import com.android.tools.r8.keepanno.ast.KeepDeclaration;
 import com.android.tools.r8.keepanno.keeprules.KeepRuleExtractor;
+import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.naming.SourceFileRewriter;
@@ -133,6 +134,7 @@
     private GraphConsumer keptGraphConsumer = null;
     private GraphConsumer mainDexKeptGraphConsumer = null;
     private InputDependencyGraphConsumer inputDependencyGraphConsumer = null;
+    private Consumer<? super R8BuildMetadata> buildMetadataConsumer = null;
     private final FeatureSplitConfiguration.Builder featureSplitConfigurationBuilder =
         FeatureSplitConfiguration.builder();
     private String synthesizedClassPrefix = "";
@@ -407,6 +409,16 @@
     }
 
     /**
+     * Set a consumer for receiving metadata about the current build intended for being stored in
+     * the app bundle.
+     */
+    public Builder setBuildMetadataConsumer(
+        Consumer<? super R8BuildMetadata> buildMetadataConsumer) {
+      this.buildMetadataConsumer = buildMetadataConsumer;
+      return self();
+    }
+
+    /**
      * Set the output path-and-mode.
      *
      * <p>Setting the output path-and-mode will override any previous set consumer or any previous
@@ -777,7 +789,8 @@
               androidResourceProvider,
               androidResourceConsumer,
               resourceShrinkerConfiguration,
-              keepSpecifications);
+              keepSpecifications,
+              buildMetadataConsumer);
 
       if (inputDependencyGraphConsumer != null) {
         inputDependencyGraphConsumer.finished();
@@ -972,6 +985,7 @@
   private final AndroidResourceProvider androidResourceProvider;
   private final AndroidResourceConsumer androidResourceConsumer;
   private final ResourceShrinkerConfiguration resourceShrinkerConfiguration;
+  private final Consumer<? super R8BuildMetadata> buildMetadataConsumer;
 
   /** Get a new {@link R8Command.Builder}. */
   public static Builder builder() {
@@ -1070,7 +1084,8 @@
       AndroidResourceProvider androidResourceProvider,
       AndroidResourceConsumer androidResourceConsumer,
       ResourceShrinkerConfiguration resourceShrinkerConfiguration,
-      List<KeepSpecificationSource> keepSpecifications) {
+      List<KeepSpecificationSource> keepSpecifications,
+      Consumer<? super R8BuildMetadata> buildMetadataConsumer) {
     super(
         inputApp,
         mode,
@@ -1119,6 +1134,7 @@
     this.androidResourceProvider = androidResourceProvider;
     this.androidResourceConsumer = androidResourceConsumer;
     this.resourceShrinkerConfiguration = resourceShrinkerConfiguration;
+    this.buildMetadataConsumer = buildMetadataConsumer;
   }
 
   private R8Command(boolean printHelp, boolean printVersion) {
@@ -1147,6 +1163,7 @@
     androidResourceProvider = null;
     androidResourceConsumer = null;
     resourceShrinkerConfiguration = null;
+    buildMetadataConsumer = null;
   }
 
   public DexItemFactory getDexItemFactory() {
@@ -1259,6 +1276,7 @@
     internal.keptGraphConsumer = keptGraphConsumer;
     internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
 
+    internal.buildMetadataConsumer = buildMetadataConsumer;
     internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
 
     internal.featureSplitConfiguration = featureSplitConfiguration;
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 4d1c64e..e4aa9dc 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -47,6 +47,7 @@
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.metadata.BuildMetadataFactory;
 import com.android.tools.r8.naming.KotlinModuleSynthesizer;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.ProguardMapSupplier.ProguardMapId;
@@ -724,6 +725,12 @@
             kotlinModuleSynthesizer);
       }
     }
+
+    if (options.buildMetadataConsumer != null) {
+      assert appView.hasClassHierarchy();
+      options.buildMetadataConsumer.accept(
+          BuildMetadataFactory.create(appView.withClassHierarchy()));
+    }
   }
 
   private static void adaptAndPassDataResources(
diff --git a/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
new file mode 100644
index 0000000..7d5b12d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
+package com.android.tools.r8.metadata;
+
+import com.android.tools.r8.Version;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+
+public class BuildMetadataFactory {
+
+  @SuppressWarnings("UnusedVariable")
+  public static R8BuildMetadata create(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return R8BuildMetadataImpl.builder().setVersion(Version.getVersionString()).build();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
new file mode 100644
index 0000000..e186575
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadata.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
+package com.android.tools.r8.metadata;
+
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.google.gson.Gson;
+
+@KeepForApi
+public interface R8BuildMetadata {
+
+  static R8BuildMetadata fromJson(String json) {
+    return new Gson().fromJson(json, R8BuildMetadataImpl.class);
+  }
+
+  String getVersion();
+
+  String toJson();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java
new file mode 100644
index 0000000..1032696
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/R8BuildMetadataImpl.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
+package com.android.tools.r8.metadata;
+
+import com.android.tools.r8.keepanno.annotations.AnnotationPattern;
+import com.android.tools.r8.keepanno.annotations.FieldAccessFlags;
+import com.android.tools.r8.keepanno.annotations.KeepConstraint;
+import com.android.tools.r8.keepanno.annotations.KeepItemKind;
+import com.android.tools.r8.keepanno.annotations.UsedByReflection;
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+@UsedByReflection(
+    description = "Keep and preserve @SerializedName for correct (de)serialization",
+    constraints = {KeepConstraint.LOOKUP},
+    constrainAnnotations = @AnnotationPattern(constant = SerializedName.class),
+    kind = KeepItemKind.CLASS_AND_FIELDS,
+    fieldAccess = {FieldAccessFlags.PRIVATE},
+    fieldAnnotatedByClassConstant = SerializedName.class)
+public class R8BuildMetadataImpl implements R8BuildMetadata {
+
+  @SerializedName("version")
+  private final String version;
+
+  public R8BuildMetadataImpl(String version) {
+    this.version = version;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public String getVersion() {
+    return version;
+  }
+
+  @Override
+  public String toJson() {
+    return new Gson().toJson(this);
+  }
+
+  public static class Builder {
+
+    private String version;
+
+    public Builder setVersion(String version) {
+      this.version = version;
+      return this;
+    }
+
+    public R8BuildMetadataImpl build() {
+      return new R8BuildMetadataImpl(version);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 8cee41d..615f440 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -76,6 +76,7 @@
 import com.android.tools.r8.ir.desugar.nest.Nest;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
+import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.MapVersion;
@@ -252,6 +253,7 @@
   private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
   private SyntheticInfoConsumer syntheticInfoConsumer = null;
 
+  public Consumer<? super R8BuildMetadata> buildMetadataConsumer = null;
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
 
diff --git a/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
new file mode 100644
index 0000000..da2b2d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// 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.
+package com.android.tools.r8.metadata;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.Version;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class BuildMetadataTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8BuildMetadata buildMetadata =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(getClass())
+            .addKeepMainRule(Main.class)
+            .collectBuildMetadata()
+            .setMinApi(parameters)
+            .compile()
+            .getBuildMetadata();
+    String json = buildMetadata.toJson();
+    // Inspecting the exact contents is not important here, but it *is* important to test that the
+    // property names are unobfuscated when testing with R8lib (!).
+    assertEquals("{\"version\":\"main (build engineering)\"}", json);
+    buildMetadata = R8BuildMetadata.fromJson(json);
+    assertEquals(Version.getVersionString(), buildMetadata.getVersion());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {}
+  }
+}
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 0b7d432..be791e9 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.keepanno.KeepAnnoTestUtils;
+import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
@@ -37,6 +38,7 @@
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ArchiveResourceProvider;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.MapIdTemplateProvider;
@@ -89,6 +91,7 @@
   private final List<Path> features = new ArrayList<>();
   private Path resourceShrinkerOutput = null;
   private HashMap<String, Path> resourceShrinkerOutputForFeatures = new HashMap<>();
+  private Box<R8BuildMetadata> buildMetadata;
 
   @Override
   public boolean isR8TestBuilder() {
@@ -148,6 +151,9 @@
     builder.setEnableIsolatedSplits(enableIsolatedSplits);
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
     builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
+    if (buildMetadata != null) {
+      builder.setBuildMetadataConsumer(buildMetadata::set);
+    }
     StringBuilder pgConfOutput = wrapProguardConfigConsumer(builder);
     ToolHelper.runAndBenchmarkR8WithoutResult(builder, optionsConsumer, benchmarkResults);
     R8TestCompileResult compileResult =
@@ -164,7 +170,8 @@
             features,
             residualArtProfiles,
             resourceShrinkerOutput,
-            resourceShrinkerOutputForFeatures);
+            resourceShrinkerOutputForFeatures,
+            buildMetadata.get());
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.getDiagnosticMessages().assertAllDiagnosticsMatch(new IsAnything<>());
@@ -1003,4 +1010,10 @@
     getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output, input));
     return self();
   }
+
+  public T collectBuildMetadata() {
+    assert buildMetadata == null;
+    buildMetadata = new Box<>();
+    return self();
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
index 4edcc33..f9db531 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestCompileResult.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.ResourceTableInspector;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
 import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
+import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
@@ -46,6 +47,7 @@
   private final List<ExternalArtProfile> residualArtProfiles;
   private final Path resourceShrinkerOutput;
   private final Map<String, Path> resourceShrinkerOutputForFeatures;
+  private final R8BuildMetadata buildMetadata;
 
   R8TestCompileResult(
       TestState state,
@@ -60,7 +62,8 @@
       List<Path> features,
       List<ExternalArtProfile> residualArtProfiles,
       Path resourceShrinkerOutput,
-      HashMap<String, Path> resourceShrinkerOutputForFeatures) {
+      HashMap<String, Path> resourceShrinkerOutputForFeatures,
+      R8BuildMetadata buildMetadata) {
     super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
@@ -70,6 +73,7 @@
     this.residualArtProfiles = residualArtProfiles;
     this.resourceShrinkerOutput = resourceShrinkerOutput;
     this.resourceShrinkerOutputForFeatures = resourceShrinkerOutputForFeatures;
+    this.buildMetadata = buildMetadata;
   }
 
   public R8TestCompileResult benchmarkResourceSize(BenchmarkResults results) throws IOException {
@@ -82,6 +86,11 @@
     return this;
   }
 
+  public R8BuildMetadata getBuildMetadata() {
+    assert buildMetadata != null;
+    return buildMetadata;
+  }
+
   @Override
   public TestDiagnosticMessages getDiagnosticMessages() {
     return state.getDiagnosticsMessages();