Version 2.1.66

Cherry pick: Allow D8 to take a proguard mapping file
CL: https://r8-review.googlesource.com/c/r8/+/53020

Bug: 150332381
Change-Id: I5c5880b797af635c233958755961220a11cf5f20
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 5781482..ceae49c 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -119,6 +119,12 @@
       return super.addClasspathResourceProvider(provider);
     }
 
+    /** Set input proguard map used for distribution of classes in multi-dex. */
+    public Builder setProguardInputMapFile(Path proguardInputMap) {
+      getAppBuilder().setProguardMapInputData(proguardInputMap);
+      return self();
+    }
+
     /**
      * Indicate if compilation is to intermediate results, i.e., intended for later merging.
      *
diff --git a/src/main/java/com/android/tools/r8/D8CommandParser.java b/src/main/java/com/android/tools/r8/D8CommandParser.java
index 3c7fdd2..c81cbcf 100644
--- a/src/main/java/com/android/tools/r8/D8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/D8CommandParser.java
@@ -29,6 +29,7 @@
           "--output",
           "--lib",
           "--classpath",
+          "--pg-map",
           MIN_API_FLAG,
           "--main-dex-list",
           "--main-dex-list-output",
@@ -128,6 +129,7 @@
                       + "# Minimum Android API level compatibility, default: "
                       + AndroidApiLevel.getDefault().getLevel()
                       + ".",
+                  "  --pg-map <file>         # Use <file> as a mapping file for distribution.",
                   "  --intermediate          # Compile an intermediate result intended for later",
                   "                          # merging.",
                   "  --file-per-class        # Produce a separate dex file per input class",
@@ -212,6 +214,8 @@
         outputMode = OutputMode.DexFilePerClass;
       } else if (arg.equals("--classfile")) {
         outputMode = OutputMode.ClassFile;
+      } else if (arg.equals("--pg-map")) {
+        builder.setProguardInputMapFile(Paths.get(nextArg));
       } else if (arg.equals("--output")) {
         if (outputPath != null) {
           builder.error(
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index b9a633b..b83b334 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "2.1.65";
+  public static final String LABEL = "2.1.66";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index ca70ab0..5190adb 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -89,7 +89,9 @@
 
   public final LazyLoadedDexApplication read(ExecutorService executorService) throws IOException {
     return read(
-        null, executorService, ProgramClassCollection.defaultConflictResolver(options.reporter));
+        inputApp.getProguardMapInputData(),
+        executorService,
+        ProgramClassCollection.defaultConflictResolver(options.reporter));
   }
 
   public final LazyLoadedDexApplication read(
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index 7d2dbdc..a419f4a 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -219,6 +219,10 @@
     return transaction.getNumberOfFields();
   }
 
+  public int getNumberOfClasses() {
+    return transaction.getNumberOfClasses();
+  }
+
   void throwIfFull(boolean hasMainDexList, Reporter reporter) {
     if (!isFull()) {
       return;
@@ -458,7 +462,8 @@
                 fillStrategy,
                 0,
                 writer.initClassLens,
-                writer.namingLens)
+                writer.namingLens,
+                options)
             .call();
       }
     }
@@ -523,7 +528,8 @@
                 fillStrategy,
                 fileIndexOffset,
                 writer.initClassLens,
-                writer.namingLens)
+                writer.namingLens,
+                options)
             .call();
       }
       addFeatureSplitFiles(featureSplitClasses, fillStrategy);
@@ -750,6 +756,10 @@
       return methods.size() + base.getNumberOfMethods();
     }
 
+    int getNumberOfClasses() {
+      return classes.size() + base.classes.size();
+    }
+
     int getNumberOfFields() {
       return fields.size() + base.getNumberOfFields();
     }
@@ -787,9 +797,6 @@
           && types.isEmpty() && strings.isEmpty();
     }
 
-    int getNumberOfClasses() {
-      return classes.size() + base.classes.size();
-    }
   }
 
   /**
@@ -919,6 +926,7 @@
     private final Map<DexProgramClass, String> originalNames;
     private final DexItemFactory dexItemFactory;
     private final FillStrategy fillStrategy;
+    private final InternalOptions options;
     private final VirtualFileCycler cycler;
 
     PackageSplitPopulator(
@@ -929,11 +937,13 @@
         FillStrategy fillStrategy,
         int fileIndexOffset,
         InitClassLens initClassLens,
-        NamingLens namingLens) {
+        NamingLens namingLens,
+        InternalOptions options) {
       this.classes = new ArrayList<>(classes);
       this.originalNames = originalNames;
       this.dexItemFactory = dexItemFactory;
       this.fillStrategy = fillStrategy;
+      this.options = options;
       this.cycler = new VirtualFileCycler(files, initClassLens, namingLens, fileIndexOffset);
     }
 
@@ -1005,7 +1015,7 @@
           nonPackageClasses.add(clazz);
           continue;
         }
-        if (current.isFilledEnough(fillStrategy) || current.isFull()) {
+        if (isFullEnough(current, options)) {
           current.abortTransaction();
           // We allow for a final rollback that has at most 20% of classes in it.
           // This is a somewhat random number that was empirically chosen.
@@ -1049,6 +1059,14 @@
       return newPackageAssignments;
     }
 
+    private boolean isFullEnough(VirtualFile current, InternalOptions options) {
+      if (options.testing.limitNumberOfClassesPerDex > 0
+          && current.getNumberOfClasses() > options.testing.limitNumberOfClassesPerDex) {
+        return true;
+      }
+      return current.isFilledEnough(fillStrategy) || current.isFull();
+    }
+
     private void addNonPackageClasses(
         VirtualFileCycler cycler, List<DexProgramClass> nonPackageClasses) {
       cycler.restart();
@@ -1076,9 +1094,8 @@
 
     private VirtualFile getVirtualFile(VirtualFileCycler cycler) {
       VirtualFile current = null;
-      while (cycler.hasNext()
-          && (current = cycler.next()).isFilledEnough(fillStrategy)) {}
-      if (current == null || current.isFilledEnough(fillStrategy)) {
+      while (cycler.hasNext() && isFullEnough(current = cycler.next(), options)) {}
+      if (current == null || isFullEnough(current, options)) {
         current = cycler.addFile();
       }
       return current;
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index faebc96..7752da9 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -107,6 +107,7 @@
   private final ImmutableList<InternalArchiveClassFileProvider> archiveProvidersToClose;
 
   private final StringResource proguardMapOutputData;
+  private final StringResource proguardMapInputData;
   private final List<StringResource> mainDexListResources;
   private final List<String> mainDexClasses;
 
@@ -176,6 +177,7 @@
       ImmutableList<ClassFileResourceProvider> libraryResourceProviders,
       ImmutableList<InternalArchiveClassFileProvider> archiveProvidersToClose,
       StringResource proguardMapOutputData,
+      StringResource proguardMapInputData,
       List<StringResource> mainDexListResources,
       List<String> mainDexClasses) {
     this.programResourceProviders = programResourceProviders;
@@ -184,6 +186,7 @@
     this.libraryResourceProviders = libraryResourceProviders;
     this.archiveProvidersToClose = archiveProvidersToClose;
     this.proguardMapOutputData = proguardMapOutputData;
+    this.proguardMapInputData = proguardMapInputData;
     this.mainDexListResources = mainDexListResources;
     this.mainDexClasses = mainDexClasses;
     assert verifyInternalProvidersInCloseSet(classpathResourceProviders, archiveProvidersToClose);
@@ -344,6 +347,11 @@
     return proguardMapOutputData;
   }
 
+  /** Get the proguard-map associated with an input "app" if it exists. */
+  public StringResource getProguardMapInputData() {
+    return proguardMapInputData;
+  }
+
   /**
    * True if the main dex list resources exists.
    */
@@ -381,6 +389,7 @@
         libraryResourceProviders,
         archiveProvidersToClose,
         proguardMapOutputData,
+        proguardMapInputData,
         ImmutableList.of(),
         ImmutableList.of());
   }
@@ -712,8 +721,8 @@
     private List<String> mainDexListClasses = new ArrayList<>();
     private boolean ignoreDexInArchive = false;
 
-    // Proguard map data is output only data. This should never be used as input to a compilation.
     private StringResource proguardMapOutputData;
+    private StringResource proguardMapInputData;
 
     private final Reporter reporter;
 
@@ -731,6 +740,7 @@
       archiveProvidersToClose.addAll(app.archiveProvidersToClose);
       mainDexListResources = app.mainDexListResources;
       mainDexListClasses = app.mainDexClasses;
+      proguardMapInputData = app.proguardMapInputData;
     }
 
     public Reporter getReporter() {
@@ -1049,6 +1059,11 @@
       return this;
     }
 
+    public Builder setProguardMapInputData(Path mapPath) {
+      this.proguardMapInputData = StringResource.fromFile(mapPath);
+      return this;
+    }
+
     /**
      * Add a main-dex list file.
      */
@@ -1144,6 +1159,7 @@
           ImmutableList.copyOf(libraryResourceProviders),
           ImmutableList.copyOf(archiveProvidersToClose),
           proguardMapOutputData,
+          proguardMapInputData,
           mainDexListResources,
           mainDexListClasses);
     }
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 6264823..af62b23 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1206,6 +1206,8 @@
     // Option for testing outlining with interface array arguments, see b/132420510.
     public boolean allowOutlinerInterfaceArrayArguments = false;
 
+    public int limitNumberOfClassesPerDex = -1;
+
     public MinifierTestingOptions minifier = new MinifierTestingOptions();
 
     // Testing hooks to trigger effects in various compiler places.
diff --git a/src/test/java/com/android/tools/r8/D8CommandTest.java b/src/test/java/com/android/tools/r8/D8CommandTest.java
index ce051a5..63d14a1 100644
--- a/src/test/java/com/android/tools/r8/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/D8CommandTest.java
@@ -596,6 +596,17 @@
   }
 
   @Test
+  public void pgInputMap() throws CompilationFailedException, IOException, ResourceException {
+    Path mapFile = temp.newFile().toPath();
+    FileUtils.writeTextFile(
+        mapFile,
+        "com.android.tools.r8.ApiLevelException -> com.android.tools.r8.a:",
+        "    boolean $assertionsDisabled -> c");
+    D8Command d8Command = parse("--pg-map", mapFile.toString());
+    assertFalse(d8Command.getInputApp().getProguardMapInputData().getString().isEmpty());
+  }
+
+  @Test
   public void numThreadsOption() throws Exception {
     assertEquals(ThreadUtils.NOT_SPECIFIED, parse().getThreadCount());
     assertEquals(1, parse("--thread-count", "1").getThreadCount());
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index aeaf585..fbe8b07 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -71,6 +71,7 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
@@ -419,6 +420,11 @@
         .build();
   }
 
+  protected List<String> classNamesFromDexFile(Path dexFile) throws IOException {
+    return new CodeInspector(dexFile)
+        .allClasses().stream().map(FoundClassSubject::toString).collect(Collectors.toList());
+  }
+
   /**
    * Build an AndroidApp with the specified test classes.
    */
diff --git a/src/test/java/com/android/tools/r8/d8/ProguardMapSortByTest.java b/src/test/java/com/android/tools/r8/d8/ProguardMapSortByTest.java
new file mode 100644
index 0000000..49e78f6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/d8/ProguardMapSortByTest.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2020, 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.d8;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.FileUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ProguardMapSortByTest extends TestBase {
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public ProguardMapSortByTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testMapSorting() throws Exception {
+    Path mappingFile = temp.newFile().toPath();
+    // Sort the classes, so that they are in 3 packages _not_ sorted by their original name.
+    FileUtils.writeTextFile(
+        mappingFile,
+        "com.A.a -> " + C.class.getTypeName() + ":",
+        "com.A.b -> " + B.class.getTypeName() + ":",
+        "com.B.a -> " + D.class.getTypeName() + ":",
+        "com.B.b -> " + E.class.getTypeName() + ":",
+        "com.C.a -> " + A.class.getTypeName() + ":");
+    D8TestBuilder d8TestBuilder =
+        testForD8()
+            .addOptionsModification(
+                internalOptions -> internalOptions.testing.limitNumberOfClassesPerDex = 2);
+    if (parameters.getApiLevel().isLessThan(AndroidApiLevel.L)) {
+      d8TestBuilder.addMainDexListClasses(A.class);
+    }
+    Path outputDir = temp.newFolder().toPath();
+    d8TestBuilder
+        .release()
+        .addProgramClasses(A.class, B.class, C.class, D.class, E.class)
+        .setMinApi(parameters.getApiLevel())
+        .apply(b -> b.getBuilder().setProguardInputMapFile(mappingFile))
+        .run(parameters.getRuntime(), A.class)
+        .assertSuccessWithOutputLines("Hello world!")
+        .app()
+        .writeToDirectory(outputDir, OutputMode.DexIndexed);
+    Map<String, String> mapping = new HashMap<>();
+    for (String dexFile : ImmutableList.of("classes.dex", "classes2.dex", "classes3.dex")) {
+      Path resolvedDexFile = outputDir.resolve(dexFile);
+      assertTrue(Files.exists(resolvedDexFile));
+      classNamesFromDexFile(resolvedDexFile).forEach(name -> mapping.put(name, dexFile));
+    }
+    // Check that the classes are grouped by their original package name.
+    assertEquals(mapping.get(B.class.getTypeName()), mapping.get(C.class.getTypeName()));
+    assertEquals(mapping.get(D.class.getTypeName()), mapping.get(E.class.getTypeName()));
+    assertTrue(
+        mapping.values().stream().filter(s -> s.equals(mapping.get(A.class.getTypeName()))).count()
+            == 1);
+  }
+
+  static class A {
+    public static void main(String[] args) {
+      System.out.println("Hello world!");
+    }
+  }
+
+  static class B {
+    public static void foo() {
+      System.out.println("foo");
+    }
+  }
+
+  static class C {
+    public static void bar() {
+      System.out.println("bar");
+    }
+  }
+
+  static class D {
+    public static void foobar() {
+      System.out.println("foobar");
+    }
+  }
+
+  static class E {
+    public static void barfoo() {
+      System.out.println("barfoo");
+    }
+  }
+}