Add D8 metadata API

Change-Id: I1f52c242abbba425dbb96782d935c5e53138e68c
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index d3b820e..1b7ca868 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.InternalOptions.DETERMINISTIC_DEBUGGING;
 import static com.android.tools.r8.utils.MapConsumerUtils.wrapExistingMapConsumerIfNotNull;
 
+import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.dump.DumpOptions;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
@@ -14,6 +15,7 @@
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibrarySpecification;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.metadata.D8BuildMetadata;
 import com.android.tools.r8.naming.MapConsumer;
 import com.android.tools.r8.naming.ProguardMapStringConsumer;
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
@@ -112,6 +114,7 @@
     private final List<ProguardConfigurationSource> mainDexRules = new ArrayList<>();
     private boolean enableMissingLibraryApiModeling = false;
     private boolean enableRewritingOfArtProfilesIsNopCheck = false;
+    private Consumer<? super D8BuildMetadata> buildMetadataConsumer = null;
 
     private Builder() {
       this(new DefaultD8DiagnosticsHandler());
@@ -472,6 +475,16 @@
       return self();
     }
 
+    /**
+     * Set a consumer for receiving metadata about the current build intended for being stored in
+     * the app bundle.
+     */
+    public Builder setBuildMetadataConsumer(
+        Consumer<? super D8BuildMetadata> buildMetadataConsumer) {
+      this.buildMetadataConsumer = buildMetadataConsumer;
+      return self();
+    }
+
     @Override
     void validate() {
       if (isPrintHelp()) {
@@ -595,6 +608,7 @@
           partitionMapConsumer,
           enableMissingLibraryApiModeling,
           enableRewritingOfArtProfilesIsNopCheck,
+          buildMetadataConsumer,
           getAndroidPlatformBuild(),
           getArtProfilesForRewriting(),
           getStartupProfileProviders(),
@@ -619,6 +633,7 @@
   private final boolean enableMissingLibraryApiModeling;
   private final boolean enableRewritingOfArtProfilesIsNopCheck;
   private final DexItemFactory factory;
+  private final Consumer<? super D8BuildMetadata> buildMetadataConsumer;
 
   public static Builder builder() {
     return new Builder();
@@ -695,6 +710,7 @@
       PartitionMapConsumer partitionMapConsumer,
       boolean enableMissingLibraryApiModeling,
       boolean enableRewritingOfArtProfilesIsNopCheck,
+      Consumer<? super D8BuildMetadata> buildMetadataConsumer,
       boolean isAndroidPlatformBuild,
       List<ArtProfileForRewriting> artProfilesForRewriting,
       List<StartupProfileProvider> startupProfileProviders,
@@ -738,6 +754,7 @@
     this.enableMissingLibraryApiModeling = enableMissingLibraryApiModeling;
     this.enableRewritingOfArtProfilesIsNopCheck = enableRewritingOfArtProfilesIsNopCheck;
     this.factory = factory;
+    this.buildMetadataConsumer = buildMetadataConsumer;
   }
 
   private D8Command(boolean printHelp, boolean printVersion) {
@@ -757,12 +774,14 @@
     enableMissingLibraryApiModeling = false;
     enableRewritingOfArtProfilesIsNopCheck = false;
     factory = null;
+    buildMetadataConsumer = null;
   }
 
   @Override
   InternalOptions getInternalOptions() {
     InternalOptions internal = new InternalOptions(factory, getReporter());
     assert !internal.debug;
+    internal.d8BuildMetadataConsumer = buildMetadataConsumer;
     internal.debug = getMode() == CompilationMode.DEBUG;
     internal.programConsumer = getProgramConsumer();
     if (internal.isGeneratingClassFiles()) {
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 1adf080..70220ac 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -1276,7 +1276,7 @@
     internal.keptGraphConsumer = keptGraphConsumer;
     internal.mainDexKeptGraphConsumer = mainDexKeptGraphConsumer;
 
-    internal.buildMetadataConsumer = buildMetadataConsumer;
+    internal.r8BuildMetadataConsumer = 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 47e8a16..afe61c8 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -701,9 +701,15 @@
       }
     }
 
-    if (options.buildMetadataConsumer != null) {
+    if (options.d8BuildMetadataConsumer != null) {
+      assert !appView.hasClassHierarchy();
+      options.d8BuildMetadataConsumer.accept(
+          BuildMetadataFactory.create(appView.withoutClassHierarchy()));
+    }
+
+    if (options.r8BuildMetadataConsumer != null) {
       assert appView.hasClassHierarchy();
-      options.buildMetadataConsumer.accept(
+      options.r8BuildMetadataConsumer.accept(
           BuildMetadataFactory.create(appView.withClassHierarchy(), virtualFiles));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
index 486abda..7ebfa5a 100644
--- a/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
+++ b/src/main/java/com/android/tools/r8/metadata/BuildMetadataFactory.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.Version;
 import com.android.tools.r8.dex.VirtualFile;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.utils.InternalOptions;
@@ -12,6 +13,11 @@
 
 public class BuildMetadataFactory {
 
+  @SuppressWarnings("UnusedParameter")
+  public static D8BuildMetadata create(AppView<AppInfo> appView) {
+    return D8BuildMetadataImpl.builder().setVersion(Version.LABEL).build();
+  }
+
   public static R8BuildMetadata create(
       AppView<? extends AppInfoWithClassHierarchy> appView, List<VirtualFile> virtualFiles) {
     InternalOptions options = appView.options();
diff --git a/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java
new file mode 100644
index 0000000..652d320
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadata.java
@@ -0,0 +1,22 @@
+// 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.GsonBuilder;
+
+@KeepForApi
+public interface D8BuildMetadata {
+
+  static D8BuildMetadata fromJson(String json) {
+    return new GsonBuilder()
+        .excludeFieldsWithoutExposeAnnotation()
+        .create()
+        .fromJson(json, D8BuildMetadataImpl.class);
+  }
+
+  String getVersion();
+
+  String toJson();
+}
diff --git a/src/main/java/com/android/tools/r8/metadata/D8BuildMetadataImpl.java b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadataImpl.java
new file mode 100644
index 0000000..a55112a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/metadata/D8BuildMetadataImpl.java
@@ -0,0 +1,60 @@
+// 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.android.tools.r8.metadata.R8BuildMetadataImpl.Builder;
+import com.google.gson.Gson;
+import com.google.gson.annotations.Expose;
+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 D8BuildMetadataImpl implements D8BuildMetadata {
+
+  @Expose
+  @SerializedName("version")
+  private final String version;
+
+  public D8BuildMetadataImpl(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 D8BuildMetadataImpl build() {
+      return new D8BuildMetadataImpl(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 87ea197..0462ec8 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.D8BuildMetadata;
 import com.android.tools.r8.metadata.R8BuildMetadata;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.MapConsumer;
@@ -253,7 +254,8 @@
   private GlobalSyntheticsConsumer globalSyntheticsConsumer = null;
   private SyntheticInfoConsumer syntheticInfoConsumer = null;
 
-  public Consumer<? super R8BuildMetadata> buildMetadataConsumer = null;
+  public Consumer<? super D8BuildMetadata> d8BuildMetadataConsumer = null;
+  public Consumer<? super R8BuildMetadata> r8BuildMetadataConsumer = null;
   public DataResourceConsumer dataResourceConsumer;
   public FeatureSplitConfiguration featureSplitConfiguration;
 
diff --git a/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
new file mode 100644
index 0000000..a4fb5f6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
@@ -0,0 +1,56 @@
+// 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.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+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 D8BuildMetadataTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimesAndAllApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    D8BuildMetadata buildMetadata =
+        testForD8(parameters.getBackend())
+            .addInnerClasses(getClass())
+            .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 (!).
+    assertThat(json, containsString("\"version\":\"" + Version.LABEL + "\""));
+    buildMetadata = D8BuildMetadata.fromJson(json);
+    inspectDeserializedBuildMetadata(buildMetadata);
+  }
+
+  private void inspectDeserializedBuildMetadata(D8BuildMetadata buildMetadata) {
+    assertEquals(Version.LABEL, buildMetadata.getVersion());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
similarity index 98%
rename from src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
rename to src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
index cec355d..7ff0178 100644
--- a/src/test/java/com/android/tools/r8/metadata/BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
@@ -33,7 +33,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class BuildMetadataTest extends TestBase {
+public class R8BuildMetadataTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
diff --git a/src/test/testbase/java/com/android/tools/r8/D8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/D8TestBuilder.java
index aeb0e21..2c499f4 100644
--- a/src/test/testbase/java/com/android/tools/r8/D8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/D8TestBuilder.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.D8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.benchmarks.BenchmarkResults;
+import com.android.tools.r8.metadata.D8BuildMetadata;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.profile.art.ArtProfileConsumer;
 import com.android.tools.r8.profile.art.ArtProfileProvider;
@@ -13,6 +14,7 @@
 import com.android.tools.r8.profile.art.utils.ArtProfileTestingUtils;
 import com.android.tools.r8.startup.StartupProfileProvider;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.Box;
 import com.android.tools.r8.utils.InternalOptions;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -37,6 +39,7 @@
   private StringBuilder proguardMapOutputBuilder = null;
   private boolean enableMissingLibraryApiModeling = true;
   private List<ExternalArtProfile> residualArtProfiles = new ArrayList<>();
+  private Box<D8BuildMetadata> buildMetadata;
 
   @Override
   public boolean isD8TestBuilder() {
@@ -88,6 +91,9 @@
       BenchmarkResults benchmarkResults)
       throws CompilationFailedException {
     libraryDesugaringTestConfiguration.configure(builder);
+    if (buildMetadata != null) {
+      builder.setBuildMetadataConsumer(buildMetadata::set);
+    }
     builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
     ToolHelper.runAndBenchmarkD8(builder, optionsConsumer, benchmarkResults);
     return new D8TestCompileResult(
@@ -97,7 +103,8 @@
         getOutputMode(),
         libraryDesugaringTestConfiguration,
         getMapContent(),
-        residualArtProfiles);
+        residualArtProfiles,
+        buildMetadata != null ? buildMetadata.get() : null);
   }
 
   private String getMapContent() {
@@ -172,4 +179,10 @@
     return addOptionsModification(
         options -> options.horizontalClassMergerOptions().disableSyntheticMerging());
   }
+
+  public D8TestBuilder collectBuildMetadata() {
+    assert buildMetadata == null;
+    buildMetadata = new Box<>();
+    return self();
+  }
 }
diff --git a/src/test/testbase/java/com/android/tools/r8/D8TestCompileResult.java b/src/test/testbase/java/com/android/tools/r8/D8TestCompileResult.java
index 35bf33d..b86b826 100644
--- a/src/test/testbase/java/com/android/tools/r8/D8TestCompileResult.java
+++ b/src/test/testbase/java/com/android/tools/r8/D8TestCompileResult.java
@@ -6,6 +6,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.metadata.D8BuildMetadata;
 import com.android.tools.r8.profile.art.model.ExternalArtProfile;
 import com.android.tools.r8.profile.art.utils.ArtProfileInspector;
 import com.android.tools.r8.utils.AndroidApp;
@@ -19,6 +20,7 @@
 
 public class D8TestCompileResult extends TestCompileResult<D8TestCompileResult, D8TestRunResult> {
 
+  private final D8BuildMetadata buildMetadata;
   private final String proguardMap;
   private final List<ExternalArtProfile> residualArtProfiles;
 
@@ -29,8 +31,10 @@
       OutputMode outputMode,
       LibraryDesugaringTestConfiguration libraryDesugaringTestConfiguration,
       String proguardMap,
-      List<ExternalArtProfile> residualArtProfiles) {
+      List<ExternalArtProfile> residualArtProfiles,
+      D8BuildMetadata buildMetadata) {
     super(state, app, minApiLevel, outputMode, libraryDesugaringTestConfiguration);
+    this.buildMetadata = buildMetadata;
     this.proguardMap = proguardMap;
     this.residualArtProfiles = residualArtProfiles;
   }
@@ -40,6 +44,11 @@
     return this;
   }
 
+  public D8BuildMetadata getBuildMetadata() {
+    assert buildMetadata != null;
+    return buildMetadata;
+  }
+
   @Override
   public TestDiagnosticMessages getDiagnosticMessages() {
     return state.getDiagnosticsMessages();