Move R8 bazel support classes from R8 into bazel

RELNOTES: None.
PiperOrigin-RevId: 302887671
diff --git a/src/test/java/com/google/devtools/build/android/BUILD b/src/test/java/com/google/devtools/build/android/BUILD
index ecec8be..f703692 100644
--- a/src/test/java/com/google/devtools/build/android/BUILD
+++ b/src/test/java/com/google/devtools/build/android/BUILD
@@ -7,6 +7,7 @@
         "//src/test/java/com/google/devtools/build/android/desugar:srcs",
         "//src/test/java/com/google/devtools/build/android/dexer:srcs",
         "//src/test/java/com/google/devtools/build/android/junctions:srcs",
+        "//src/test/java/com/google/devtools/build/android/r8:srcs",
         "//src/test/java/com/google/devtools/build/android/resources:srcs",
         "//src/test/java/com/google/devtools/build/android/testing/manifestmerge:srcs",
         "//src/test/java/com/google/devtools/build/android/xml:srcs",
diff --git a/src/test/java/com/google/devtools/build/android/r8/AllTests.java b/src/test/java/com/google/devtools/build/android/r8/AllTests.java
new file mode 100644
index 0000000..b433326
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/AllTests.java
@@ -0,0 +1,21 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import com.google.devtools.build.lib.testutil.ClasspathSuite;
+import org.junit.runner.RunWith;
+
+/** Test suite for dexer tests. */
+@RunWith(ClasspathSuite.class)
+public class AllTests {}
diff --git a/src/test/java/com/google/devtools/build/android/r8/BUILD b/src/test/java/com/google/devtools/build/android/r8/BUILD
new file mode 100644
index 0000000..df1a879
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/BUILD
@@ -0,0 +1,109 @@
+load("@rules_java//java:defs.bzl", "java_library", "java_test")
+
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["**"]),
+    visibility = ["//src/test/java/com/google/devtools/build/android:__pkg__"],
+)
+
+# Description:
+#   Tests for the bazel D8 bridge code.
+java_library(
+    name = "tests",
+    srcs = [
+        "AllTests.java",
+    ] + select({
+        "//external:has_androidsdk": glob(
+            ["*Test.java"],
+            exclude = [
+                "NoAndroidSdkStubTest.java",
+                "AllTests.java",
+            ],
+        ),
+        "//conditions:default": ["NoAndroidSdkStubTest.java"],
+    }),
+    deps = [
+        "//src/test/java/com/google/devtools/build/lib/testutil:TestSuite",
+        "//src/tools/android/java/com/google/devtools/build/android/r8",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+        "@bazel_tools//tools/java/runfiles",
+    ] + select({
+        "//external:has_androidsdk": [
+            "//external:android/dx_jar_import",
+            "//external:android/d8_jar_import",
+        ],
+        "//conditions:default": [],
+    }),
+)
+
+java_test(
+    name = "AllTests",
+    size = "small",
+    data = [
+        ":arithmetic",
+        ":barray",
+        ":dexmergersample",
+        ":naming001",
+        ":twosimpleclasses",
+    ],
+    jvm_flags = [
+        "-DCompatDexBuilderTests.twosimpleclasses=$(location :twosimpleclasses)",
+        "-DCompatDexBuilderTests.naming001=$(location :naming001)",
+        "-DCompatDxTests.arithmetic=$(location :arithmetic)",
+        "-DCompatDxTests.barray=$(location :barray)",
+        "-DDexFileMergerTest.dexmergersample=$(location :dexmergersample)",
+    ],
+    runtime_deps = [
+        ":tests",
+    ],
+)
+
+java_library(
+    name = "twosimpleclasses",
+    srcs = glob(["testdata/twosimpleclasses/*.java"]),
+)
+
+java_library(
+    name = "naming001",
+    srcs = glob(["testdata/naming001/*.java"]),
+)
+
+java_library(
+    name = "arithmetic",
+    srcs = glob(["testdata/arithmetic/*.java"]),
+)
+
+java_library(
+    name = "barray",
+    srcs = glob(["testdata/barray/*.java"]),
+)
+
+java_library(
+    name = "dexmergersample",
+    srcs = glob(["testdata/dexmergersample/*.java"]),
+)
+
+test_suite(
+    name = "windows_tests",
+    tags = [
+        "-no_windows",
+        "-slow",
+    ],
+    visibility = ["//visibility:private"],
+)
+
+test_suite(
+    name = "all_windows_tests",
+    tests = [
+        ":windows_tests",
+    ],
+    visibility = ["//src/test/java/com/google/devtools/build/android:__pkg__"],
+)
diff --git a/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java b/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java
new file mode 100644
index 0000000..24e3655
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/CompatDexBuilderTest.java
@@ -0,0 +1,106 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.OutputMode;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipFile;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for CompatDexBuilder. */
+@RunWith(JUnit4.class)
+public class CompatDexBuilderTest {
+  @Rule public TemporaryFolder temp = new TemporaryFolder();
+
+  @Test
+  public void compileManyClasses() throws IOException, InterruptedException, ExecutionException {
+    // Random set of classes from the R8 example test directory naming001.
+    final String inputJar = System.getProperty("CompatDexBuilderTests.naming001");
+    final List<String> classNames =
+        ImmutableList.of(
+            "A",
+            "B",
+            "C",
+            "D",
+            "E",
+            "F",
+            "G",
+            "H",
+            "I",
+            "J",
+            "K",
+            "L",
+            "Reflect2$A",
+            "Reflect2$B",
+            "Reflect2",
+            "Reflect");
+
+    // Run CompatDexBuilder on naming001.jar
+    Path outputZip = temp.getRoot().toPath().resolve("out.zip");
+    CompatDexBuilder.main(
+        new String[] {"--input_jar", inputJar, "--output_zip", outputZip.toString()});
+    assertThat(Files.exists(outputZip)).isTrue();
+
+    // Verify if all the classes have their corresponding ".class.dex" files in the zip.
+    Set<String> expectedNames = new HashSet<>();
+    for (String className : classNames) {
+      expectedNames.add(
+          "com/google/devtools/build/android/r8/testdata/naming001/" + className + ".class.dex");
+    }
+    try (ZipFile zipFile = new ZipFile(outputZip.toFile(), UTF_8)) {
+      zipFile.stream()
+          .forEach(
+              ze -> {
+                expectedNames.remove(ze.getName());
+              });
+    }
+    assertThat(expectedNames).isEmpty();
+  }
+
+  @Test
+  public void compileTwoClassesAndRun() throws Exception {
+    // Run CompatDexBuilder on dexMergeSample.jar
+    final String inputJar = System.getProperty("CompatDexBuilderTests.twosimpleclasses");
+    Path outputZip = temp.getRoot().toPath().resolve("out.zip");
+    CompatDexBuilder.main(
+        new String[] {"--input_jar", inputJar, "--output_zip", outputZip.toString()});
+
+    // Merge zip content into a single dex file.
+    Path d8OutDir = temp.newFolder().toPath();
+    D8.run(
+        D8Command.builder()
+            .setOutput(d8OutDir, OutputMode.DexIndexed)
+            .addProgramFiles(outputZip)
+            .build());
+
+    // TODO(sgjesse): Validate by running methods of Class1 and Class2.
+    // https://r8.googlesource.com/r8/+/5ee92486c896b918efb62e69bff5dfa79f30e7c2/src/test/java/com/android/tools/r8/compatdexbuilder/CompatDexBuilderTests.java#95
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/CompatDxTest.java b/src/test/java/com/google/devtools/build/android/r8/CompatDxTest.java
new file mode 100644
index 0000000..45ffc5c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/CompatDxTest.java
@@ -0,0 +1,233 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for CompatDx. */
+@RunWith(JUnit4.class)
+public class CompatDxTest {
+  private static final String EXAMPLE_JAR_FILE_1 = System.getProperty("CompatDxTests.arithmetic");
+  private static final String EXAMPLE_JAR_FILE_2 = System.getProperty("CompatDxTests.barray");
+
+  private static final String NO_LOCALS = "--no-locals";
+  private static final String NO_POSITIONS = "--positions=none";
+  private static final String MULTIDEX = "--multi-dex";
+  private static final String NUM_THREADS_5 = "--num-threads=5";
+
+  @Rule public TemporaryFolder temp = new TemporaryFolder();
+
+  @Test
+  public void noFilesTest() throws IOException {
+    runDexer("--no-files");
+  }
+
+  @Test
+  public void noOutputTest() throws IOException {
+    runDexerWithoutOutput(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void singleJarInputFile() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void multipleJarInputFiles() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE_1, EXAMPLE_JAR_FILE_2);
+  }
+
+  @Test
+  public void outputZipFile() throws IOException {
+    runDexerWithOutput("foo.dex.zip", NO_POSITIONS, NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void useMultipleThreads() throws IOException {
+    runDexer(NUM_THREADS_5, NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void withPositions() throws IOException {
+    runDexer(NO_LOCALS, MULTIDEX, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void withLocals() throws IOException {
+    runDexer(NO_POSITIONS, MULTIDEX, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void withoutMultidex() throws IOException {
+    runDexer(NO_POSITIONS, NO_LOCALS, EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void writeToNamedDexFile() throws IOException {
+    runDexerWithOutput("named-output.dex", EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void keepClassesSingleDexTest() throws IOException {
+    runDexerWithOutput("out.zip", "--keep-classes", EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void keepClassesMultiDexTest() throws IOException {
+    runDexerWithOutput("out.zip", "--keep-classes", "--multi-dex", EXAMPLE_JAR_FILE_1);
+  }
+
+  @Test
+  public void ignoreDexInArchiveTest() throws IOException {
+    // Create a JAR with both a .class and a .dex file (the .dex file is just empty).
+    Path jarWithClassesAndDex = temp.newFile("test.jar").toPath();
+    Files.copy(
+        Paths.get(EXAMPLE_JAR_FILE_1), jarWithClassesAndDex, StandardCopyOption.REPLACE_EXISTING);
+    jarWithClassesAndDex.toFile().setWritable(true);
+    URI uri = URI.create("jar:" + jarWithClassesAndDex.toUri());
+    try (FileSystem fileSystem =
+        FileSystems.newFileSystem(uri, ImmutableMap.of("create", "true"))) {
+      Path dexFile = fileSystem.getPath("classes.dex");
+      Files.newOutputStream(dexFile, StandardOpenOption.CREATE).close();
+    }
+
+    // Only test this with CompatDx, as dx does not like the empty .dex file.
+    List<String> d8Args =
+        ImmutableList.of("--output=" + temp.newFolder("out"), jarWithClassesAndDex.toString());
+    CompatDx.main(d8Args.toArray(new String[0]));
+  }
+
+  private void runDexer(String... args) throws IOException {
+    runDexerWithOutput("", args);
+  }
+
+  private void runDexerWithoutOutput(String... args) throws IOException {
+    runDexerWithOutput(null, args);
+  }
+
+  private Path getOutputD8() {
+    return temp.getRoot().toPath().resolve("d8-out");
+  }
+
+  private Path getOutputDX() {
+    return temp.getRoot().toPath().resolve("dx-out");
+  }
+
+  private void runDexerWithOutput(String out, String... args) throws IOException {
+    Path d8Out = null;
+    Path dxOut = null;
+    if (out != null) {
+      Path baseD8 = getOutputD8();
+      Path baseDX = getOutputDX();
+      Files.createDirectory(baseD8);
+      Files.createDirectory(baseDX);
+      d8Out = baseD8.resolve(out);
+      dxOut = baseDX.resolve(out);
+      assertThat(dxOut.toString()).isNotEqualTo(d8Out.toString());
+    }
+
+    List<String> d8Args = new ArrayList<>();
+    d8Args.add("--dex");
+    if (d8Out != null) {
+      d8Args.add("--output=" + d8Out);
+    }
+    Collections.addAll(d8Args, args);
+    System.out.println("running: d8 " + Joiner.on(" ").join(d8Args));
+    CompatDx.main(d8Args.toArray(new String[0]));
+
+    List<String> dxArgs = new ArrayList<>();
+    dxArgs.add("--dex");
+    if (dxOut != null) {
+      dxArgs.add("--output=" + dxOut);
+    }
+    Collections.addAll(dxArgs, args);
+    System.out.println("running: dx " + Joiner.on(" ").join(dxArgs));
+    com.android.dx.command.Main.main(dxArgs.toArray(new String[0]));
+
+    if (out == null) {
+      // Can't check output if explicitly not writing any.
+      return;
+    }
+
+    List<Path> d8Files;
+    try (Stream<Path> d8FilesStream =
+        Files.list(Files.isDirectory(d8Out) ? d8Out : d8Out.getParent())) {
+      d8Files = d8FilesStream.sorted().collect(toList());
+    }
+    List<Path> dxFiles;
+    try (Stream<Path> dxFilesStream =
+        Files.list(Files.isDirectory(dxOut) ? dxOut : dxOut.getParent())) {
+      dxFiles = dxFilesStream.sorted().collect(toList());
+    }
+    assertWithMessage("Out file names differ")
+        .that(
+            Joiner.on(System.lineSeparator())
+                .join(d8Files.stream().map(Path::getFileName).iterator()))
+        .isEqualTo(
+            Joiner.on(System.lineSeparator())
+                .join(dxFiles.stream().map(Path::getFileName).iterator()));
+    for (int i = 0; i < d8Files.size(); i++) {
+      if (FileUtils.isArchive(d8Files.get(i))) {
+        compareArchiveFiles(d8Files.get(i), dxFiles.get(i));
+      }
+    }
+  }
+
+  private static void compareArchiveFiles(Path d8File, Path dxFile) throws IOException {
+    ZipFile d8Zip = new ZipFile(d8File.toFile(), UTF_8);
+    ZipFile dxZip = new ZipFile(dxFile.toFile(), UTF_8);
+    // TODO(zerny): This should test resource containment too once supported.
+    Set<String> d8Content = d8Zip.stream().map(ZipEntry::getName).collect(toSet());
+    Set<String> dxContent =
+        dxZip.stream()
+            .map(ZipEntry::getName)
+            .filter(
+                name ->
+                    name.endsWith(FileUtils.DEX_EXTENSION)
+                        || name.endsWith(FileUtils.CLASS_EXTENSION))
+            .collect(toSet());
+    assertWithMessage("Expected dx and d8 output to contain same DEX anf class file entries")
+        .that(d8Content)
+        .containsExactlyElementsIn(dxContent);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/DexFileMergerTest.java b/src/test/java/com/google/devtools/build/android/r8/DexFileMergerTest.java
new file mode 100644
index 0000000..bd220bd
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/DexFileMergerTest.java
@@ -0,0 +1,177 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexFileMergerHelper;
+import com.android.tools.r8.ExtractMarker;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.errors.CompilationError;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+// import com.android.tools.r8.maindexlist.MainDexListTests;
+
+/** Test for DexFileMerger. */
+@RunWith(JUnit4.class)
+public class DexFileMergerTest {
+
+  @Rule public TemporaryFolder temp = new TemporaryFolder();
+
+  private static final String CLASSES_JAR = System.getProperty("DexFileMergerTest.dexmergersample");
+  private String class1Class;
+  private String class2Class;
+
+  @Before
+  public void setUp() throws Exception {
+    File jarUnzipFolder = temp.newFolder();
+    unzip(Paths.get(CLASSES_JAR).toString(), jarUnzipFolder, entry -> true);
+    class1Class =
+        jarUnzipFolder
+            + "/com/google/devtools/build/android/r8/testdata/dexmergersample/Class1.class";
+    class2Class =
+        jarUnzipFolder
+            + "/com/google/devtools/build/android/r8/testdata/dexmergersample/Class2.class";
+  }
+
+  private Path compileTwoClasses(OutputMode outputMode, boolean addMarker)
+      throws CompilationFailedException, IOException {
+    // Compile Class1 and Class2.
+    Path output = temp.newFolder().toPath().resolve("compiled.zip");
+    D8Command command =
+        D8Command.builder()
+            .setOutput(output, outputMode)
+            .addProgramFiles(Paths.get(class2Class))
+            .addProgramFiles(Paths.get(class1Class))
+            .build();
+
+    DexFileMergerHelper.runD8ForTesting(command, !addMarker);
+
+    return output;
+  }
+
+  private Path mergeWithDexMerger(Path input) throws Exception {
+    Path output = temp.newFolder().toPath().resolve("merged-with-dexmerger.zip");
+    DexFileMerger.main(new String[] {"--input", input.toString(), "--output", output.toString()});
+    return output;
+  }
+
+  private Path mergeWithD8(Path input) throws Exception {
+    Path output = temp.newFolder().toPath().resolve("merged-with-d8.zip");
+    D8.main(new String[] {input.toString(), "--output", output.toString()});
+    return output;
+  }
+
+  @Test
+  public void markerPreserved() throws Exception {
+    Path input = compileTwoClasses(OutputMode.DexIndexed, true);
+    assertThat(ExtractMarker.extractMarkerFromDexFile(input)).hasSize(1);
+    assertThat(ExtractMarker.extractMarkerFromDexFile(mergeWithDexMerger(input))).hasSize(1);
+    assertThat(ExtractMarker.extractMarkerFromDexFile(mergeWithD8(input))).hasSize(1);
+  }
+
+  @Test
+  public void markerNotAdded() throws Exception {
+    Path input = compileTwoClasses(OutputMode.DexIndexed, false);
+    assertThat(ExtractMarker.extractMarkerFromDexFile(input)).isEmpty();
+    assertThat(ExtractMarker.extractMarkerFromDexFile(mergeWithDexMerger(input))).isEmpty();
+    assertThat(ExtractMarker.extractMarkerFromDexFile(mergeWithD8(input))).isEmpty();
+  }
+
+  @Test
+  public void mergeTwoFiles() throws CompilationFailedException, IOException {
+    Path mergerInputZip = compileTwoClasses(OutputMode.DexFilePerClassFile, false);
+
+    Path mergerOutputZip = temp.getRoot().toPath().resolve("merger-out.zip");
+    DexFileMerger.main(
+        new String[] {
+          "--input", mergerInputZip.toString(), "--output", mergerOutputZip.toString()
+        });
+
+    // TODO(sgjesse): Validate by running methods of Class1 and Class2.
+    // https://r8.googlesource.com/r8/+/5ee92486c896b918efb62e69bff5dfa79f30e7c2/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java#103
+  }
+
+  // TODO(sgjesse): Port tests for merge overflow.
+  // https://r8.googlesource.com/r8/+/5ee92486c896b918efb62e69bff5dfa79f30e7c2/src/test/java/com/android/tools/r8/dexfilemerger/DexFileMergerTests.java#131
+
+  // Copied from R8 class com.android.tools.r8.utils.ZipUtils.
+  private interface OnEntryHandler {
+    void onEntry(ZipEntry entry, InputStream input) throws IOException;
+  }
+
+  // Copied from R8 class com.android.tools.r8.utils.ZipUtils.
+  private static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFileStr, UTF_8)) {
+      zipFile.stream()
+          .forEach(
+              entry -> {
+                try (InputStream entryStream = zipFile.getInputStream(entry)) {
+                  handler.onEntry(entry, entryStream);
+                } catch (IOException e) {
+                  throw new AssertionError(e);
+                }
+              });
+    }
+  }
+
+  // Copied from R8 class com.android.tools.r8.utils.ZipUtils.
+  private static List<File> unzip(String zipFile, File outDirectory, Predicate<ZipEntry> filter)
+      throws IOException {
+    final Path outDirectoryPath = outDirectory.toPath();
+    final List<File> outFiles = new ArrayList<>();
+    iter(
+        zipFile,
+        (entry, input) -> {
+          String name = entry.getName();
+          if (!entry.isDirectory() && filter.test(entry)) {
+            if (name.contains("..")) {
+              // Protect against malicious archives.
+              throw new CompilationError("Invalid entry name \"" + name + "\"");
+            }
+            Path outPath = outDirectoryPath.resolve(name);
+            File outFile = outPath.toFile();
+            outFile.getParentFile().mkdirs();
+            System.out.println(outFile);
+            try (OutputStream output = new FileOutputStream(outFile)) {
+              ByteStreams.copy(input, output);
+            }
+            outFiles.add(outFile);
+          }
+        });
+    return outFiles;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/NoAndroidSdkStubTest.java b/src/test/java/com/google/devtools/build/android/r8/NoAndroidSdkStubTest.java
new file mode 100644
index 0000000..80f033e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/NoAndroidSdkStubTest.java
@@ -0,0 +1,29 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Stub test to run if {@code android_sdk_repository} is not in the {@code WORKSPACE} file. */
+@RunWith(JUnit4.class)
+public class NoAndroidSdkStubTest {
+  @Test
+  public void printWarningMessageTest() {
+    System.out.println(
+        "Android tests are being skipped because no android_sdk_repository rule is set up in the "
+            + "WORKSPACE file.");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/arithmetic/Arithmetic.java b/src/test/java/com/google/devtools/build/android/r8/testdata/arithmetic/Arithmetic.java
new file mode 100644
index 0000000..64df6ba
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/arithmetic/Arithmetic.java
@@ -0,0 +1,175 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.arithmetic;
+
+import java.util.Arrays;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class Arithmetic {
+  static void addInts(int[] ints) {
+    int result = 0;
+    for (int i : ints) {
+      result += i;
+    }
+    System.out.println("addInts: " + Arrays.toString(ints) + " = " + result);
+  }
+
+  static void addDoubles(double[] doubles) {
+    double result = 0;
+    for (double d : doubles) {
+      result += d;
+    }
+    System.out.println("addDoubles: " + Arrays.toString(doubles) + " = " + result);
+  }
+
+  static void addLongs(long[] longs) {
+    long result = 0;
+    for (long l : longs) {
+      result += l;
+    }
+    System.out.println("addLongs: " + Arrays.toString(longs) + " = " + result);
+  }
+
+  @SuppressWarnings("IdentityBinaryExpression")
+  static void binaryOps() {
+    int i = 0;
+    System.out.println("i values:");
+    i = i + 1;
+    System.out.println(i);
+    i = 1 + i;
+    System.out.println(i);
+    i = i * 4;
+    System.out.println(i);
+    i = i * i;
+    System.out.println(i);
+    i = 4 * i;
+    System.out.println(i);
+    i = i / 4;
+    System.out.println(i);
+    i = i / i;
+    System.out.println(i);
+    i = i % i;
+    System.out.println(i);
+
+    long l = 0;
+    System.out.println("l values:");
+    l = l + 1;
+    System.out.println(l);
+    l = 1 + l;
+    System.out.println(l);
+    l = l * 4;
+    System.out.println(l);
+    l = l * l;
+    System.out.println(l);
+    l = 4 * l;
+    System.out.println(l);
+    l = l / 4;
+    System.out.println(l);
+    l = l / l;
+    System.out.println(l);
+    l = l % l;
+    System.out.println(l);
+
+    double d = 0.0;
+    System.out.println("d values: ");
+    d = d + 1.0;
+    System.out.println(d);
+    d = 1.0 + d;
+    System.out.println(d);
+    d = d * 4.0;
+    System.out.println(d);
+    d = d * d;
+    System.out.println(d);
+    d = 4.0 * d;
+    System.out.println(d);
+    d = d / 4.0;
+    System.out.println(d);
+    d = d / d;
+    System.out.println(d);
+    d = d % d;
+    System.out.println(d);
+
+    float f = 0.0f;
+    System.out.println("f values: ");
+    f = f + 1.0f;
+    System.out.println(f);
+    f = 1.0f + f;
+    System.out.println(f);
+    f = f * 4.0f;
+    System.out.println(f);
+    f = f * f;
+    System.out.println(f);
+    f = 4.0f * f;
+    System.out.println(f);
+    f = f / 4.0f;
+    System.out.println(f);
+    f = f / f;
+    System.out.println(f);
+    f = f % f;
+    System.out.println(f);
+  }
+
+  public static void moreOps() {
+    int a = 42;
+    int b = -a;
+    int shiftLeftA = a << 5;
+    int shiftRightA = a >> 5;
+    int uShiftRightA = -a >>> 5;
+    System.out.println(a + b + shiftLeftA + shiftRightA + uShiftRightA);
+    float c = 42.42f;
+    float d = -c;
+    System.out.println(c + d);
+    double e = 43.43;
+    double f = -e;
+    System.out.println(e + f);
+    long g = 5000000000L;
+    long h = -g;
+    long shiftLeftG = g << 8;
+    long shiftRightG = g >> 8;
+    long uShiftRightG = -g >>> 8;
+    System.out.println(g + h + shiftLeftG + shiftRightG + uShiftRightG);
+  }
+
+  public static void bitwiseInts(int x, int y) {
+    System.out.println(x & y);
+    System.out.println(x | y);
+    System.out.println(x ^ y);
+    System.out.println(~x);
+  }
+
+  public static void bitwiseLongs(long x, long y) {
+    System.out.println(x & y);
+    System.out.println(x | y);
+    System.out.println(x ^ y);
+    System.out.println(~x);
+  }
+
+  public static void main(String[] args) {
+    addInts(new int[] {});
+    addInts(new int[] {1});
+    addInts(new int[] {0, 1, 2, 3});
+    addDoubles(new double[] {0.0});
+    addDoubles(new double[] {0.0, 1.0, 2.0});
+    addDoubles(new double[] {0.0, 1.0, 2.0, 3.0});
+    long l = 0x0000000100000000L;
+    addLongs(new long[] {});
+    addLongs(new long[] {l});
+    addLongs(new long[] {l, l + 1, l + 2});
+    binaryOps();
+    moreOps();
+    bitwiseInts(12345, 54321);
+    bitwiseLongs(54321, 12345);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/barray/BArray.java b/src/test/java/com/google/devtools/build/android/r8/testdata/barray/BArray.java
new file mode 100644
index 0000000..f912e28
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/barray/BArray.java
@@ -0,0 +1,76 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.barray;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class BArray {
+
+  public static void main(String[] args) {
+    System.out.println("null boolean: " + readNullBooleanArray());
+    System.out.println("null byte: " + readNullByteArray());
+    System.out.println("boolean: " + readBooleanArray(writeBooleanArray(args)));
+    System.out.println("byte: " + readByteArray(writeByteArray(args)));
+  }
+
+  public static boolean readNullBooleanArray() {
+    boolean[] boolArray = null;
+    try {
+      return boolArray[0] || boolArray[1];
+    } catch (Throwable e) {
+      return true;
+    }
+  }
+
+  public static byte readNullByteArray() {
+    byte[] byteArray = null;
+    try {
+      return byteArray[0];
+    } catch (Throwable e) {
+      return 42;
+    }
+  }
+
+  public static boolean[] writeBooleanArray(String[] args) {
+    boolean[] array = new boolean[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = args[i].length() == 42;
+    }
+    return array;
+  }
+
+  public static byte[] writeByteArray(String[] args) {
+    byte[] array = new byte[args.length];
+    for (int i = 0; i < args.length; i++) {
+      array[i] = (byte) args[i].length();
+    }
+    return array;
+  }
+
+  public static boolean readBooleanArray(boolean[] boolArray) {
+    try {
+      return boolArray[0] || boolArray[1];
+    } catch (Throwable e) {
+      return true;
+    }
+  }
+
+  public static byte readByteArray(byte[] byteArray) {
+    try {
+      return byteArray[0];
+    } catch (Throwable e) {
+      return 42;
+    }
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class1.java b/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class1.java
new file mode 100644
index 0000000..9f7f3d3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class1.java
@@ -0,0 +1,22 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.dexmergersample;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class Class1 {
+  public static void main(String[] args) {
+    System.out.println("Class1");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class2.java b/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class2.java
new file mode 100644
index 0000000..61f15e3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/dexmergersample/Class2.java
@@ -0,0 +1,22 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.dexmergersample;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class Class2 {
+  public static void main(String[] args) {
+    System.out.println("Class2");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/A.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/A.java
new file mode 100644
index 0000000..757b8a9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/A.java
@@ -0,0 +1,37 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class A {
+
+  A() {}
+
+  A(int i) {}
+
+  static {
+    C.m();
+  }
+
+  void m() {}
+
+  @SuppressWarnings("unused")
+  private void privateFunc() {}
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/B.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/B.java
new file mode 100644
index 0000000..311d35f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/B.java
@@ -0,0 +1,23 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class B {}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/C.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/C.java
new file mode 100644
index 0000000..416465e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/C.java
@@ -0,0 +1,27 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class C {
+  static void f() {}
+
+  static void m() {}
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/D.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/D.java
new file mode 100644
index 0000000..6d1abe5
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/D.java
@@ -0,0 +1,37 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class D {
+  public void keep() {
+    System.out.println();
+  }
+
+  public static void main(String[] args) {
+    D d = new E();
+    d.keep();
+  }
+
+  public static void main2(String[] args) {
+    D d = new D();
+    d.keep();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/E.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/E.java
new file mode 100644
index 0000000..67431de
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/E.java
@@ -0,0 +1,26 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class E extends D {
+  @Override
+  public void keep() {}
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/F.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/F.java
new file mode 100644
index 0000000..efb87f9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/F.java
@@ -0,0 +1,26 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class F extends E {
+  @Override
+  public void keep() {}
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/G.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/G.java
new file mode 100644
index 0000000..cf5956c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/G.java
@@ -0,0 +1,31 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class G implements H {
+  @Override
+  public void m() {}
+
+  public static void main(String[] args) {
+    H i = new G();
+    i.m();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/H.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/H.java
new file mode 100644
index 0000000..dff350b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/H.java
@@ -0,0 +1,23 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public interface H extends I {}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/I.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/I.java
new file mode 100644
index 0000000..065ed70
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/I.java
@@ -0,0 +1,25 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public interface I {
+  void m();
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/J.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/J.java
new file mode 100644
index 0000000..232bc6d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/J.java
@@ -0,0 +1,47 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class J {
+  int[][][] m5(int a, boolean b, long c) {
+    return null;
+  }
+
+  int[][][] m3() {
+    return null;
+  }
+
+  int[][] m2() {
+    return null;
+  }
+
+  int[] m1() {
+    return null;
+  }
+
+  int m0() {
+    return 0;
+  }
+
+  int[][][] m() {
+    return null;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/K.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/K.java
new file mode 100644
index 0000000..e8e4a3e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/K.java
@@ -0,0 +1,45 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings({"PrivateConstructorForUtilityClass", "unused"})
+public class K {
+  private int i;
+  private int h;
+  private final int i2 = 7;
+  private static int j;
+  private static final int I3 = 7;
+
+  private static final Object o = "TAG";
+  private static final String TAG = "TAG";
+  private final String tag2 = "TAG";
+
+  static {
+    j = 6;
+  }
+
+  {
+    i = 6;
+  }
+
+  void keep() {
+    h = 7;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/L.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/L.java
new file mode 100644
index 0000000..d959e02
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/L.java
@@ -0,0 +1,29 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class L {
+  private static final String TAG = "TAG";
+
+  void onReceive() {
+    System.out.println(TAG);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect.java
new file mode 100644
index 0000000..141c58c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect.java
@@ -0,0 +1,68 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicLongFieldUpdater;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings({"PrivateConstructorForUtilityClass", "ClassCanBeStatic"})
+public class Reflect {
+  void keep() throws ClassNotFoundException {
+    Class.forName("naming001.Reflect2");
+    Class.forName("ClassThatDoesNotExists");
+  }
+
+  void keep2() throws NoSuchFieldException, SecurityException {
+    Reflect2.class.getField("fieldPublic");
+    Reflect2.class.getField("fieldPrivate");
+  }
+
+  void keep3() throws NoSuchFieldException, SecurityException {
+    Reflect2.class.getDeclaredField("fieldPublic");
+    Reflect2.class.getDeclaredField("fieldPrivate");
+  }
+
+  void keep4() throws SecurityException, NoSuchMethodException {
+    Reflect2.class.getMethod("m", new Class<?>[] {Reflect2.A.class});
+    Reflect2.class.getMethod("m", new Class<?>[] {Reflect2.B.class});
+    Reflect2.class.getMethod("methodThatDoesNotExist", new Class<?>[] {Reflect2.A.class});
+  }
+
+  void keep5() throws SecurityException, NoSuchMethodException {
+    Reflect2.class.getDeclaredMethod("m", new Class<?>[] {Reflect2.A.class});
+    Reflect2.class.getDeclaredMethod("m", new Class<?>[] {Reflect2.B.class});
+  }
+
+  void keep6() throws SecurityException {
+    AtomicIntegerFieldUpdater.newUpdater(Reflect2.class, "fieldPublic");
+  }
+
+  void keep7() throws SecurityException {
+    AtomicLongFieldUpdater.newUpdater(Reflect2.class, "fieldLong");
+    AtomicLongFieldUpdater.newUpdater(Reflect2.class, "fieldLong2");
+  }
+
+  void keep8() throws SecurityException {
+    AtomicReferenceFieldUpdater.newUpdater(Reflect2.class, Reflect2.A.class, "a");
+    AtomicReferenceFieldUpdater.newUpdater(Reflect2.class, Reflect2.A.class, "b");
+    AtomicReferenceFieldUpdater.newUpdater(Reflect2.class, Object.class, "c");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect2.java b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect2.java
new file mode 100644
index 0000000..4537ab4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/naming001/Reflect2.java
@@ -0,0 +1,51 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.naming001;
+
+/**
+ * Test class TODO(sgjesse): Update this class to be more targeted to testing CompatDexBuilder. The
+ * content of this class is pretty random. It was just the class that happened to be used for
+ * testing in the R8 repo. The class was actually there for testing some R8 renaming, and
+ * CompatDexBuilderTest just happened to piggy-bag on this class.
+ */
+@SuppressWarnings({"PrivateConstructorForUtilityClass", "ClassCanBeStatic", "unused"})
+public class Reflect2 {
+  public volatile int fieldPublic;
+
+  private volatile int fieldPrivate;
+
+  public volatile long fieldLong;
+
+  private volatile long fieldLong2;
+
+  volatile long fieldLong3;
+
+  protected volatile long fieldLong4;
+
+  public volatile A a;
+
+  public volatile B b;
+
+  private volatile Object c;
+
+  private void calledMethod() {}
+
+  public void m(A a) {}
+
+  private void privateMethod(B b) {}
+
+  class A {}
+
+  class B {}
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class1.java b/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class1.java
new file mode 100644
index 0000000..7976ba7
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class1.java
@@ -0,0 +1,22 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.twosimpleclasses;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class Class1 {
+  public static void main(String[] args) {
+    System.out.println("Class1");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class2.java b/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class2.java
new file mode 100644
index 0000000..a7e8100
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/r8/testdata/twosimpleclasses/Class2.java
@@ -0,0 +1,22 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8.testdata.twosimpleclasses;
+
+/** Test class */
+@SuppressWarnings("PrivateConstructorForUtilityClass")
+public class Class2 {
+  public static void main(String[] args) {
+    System.out.println("Class2");
+  }
+}
diff --git a/src/tools/android/java/com/android/tools/r8/BUILD b/src/tools/android/java/com/android/tools/r8/BUILD
new file mode 100644
index 0000000..ceb1f4f
--- /dev/null
+++ b/src/tools/android/java/com/android/tools/r8/BUILD
@@ -0,0 +1,35 @@
+# Description:
+#   Gateway classes to access package private parts of classes in com.android.tools.r8.
+
+load("@rules_java//java:defs.bzl", "java_library")
+
+licenses(["notice"])
+
+package(default_visibility = ["//javatests/com/google/devtools/build/android:__subpackages__"])
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+    visibility = [
+        "//src/java/com/google/devtools/build/android/r8:__pkg__",
+        "//src/tools/android/java/com/google/devtools/build/android:__pkg__",
+    ],
+)
+
+java_library(
+    name = "r8_support",
+    srcs = select({
+        "//external:has_androidsdk": glob(
+            ["*.java"],
+        ),
+        "//conditions:default": [],
+    }),
+    visibility = [
+        "//src/test/java/com/google/devtools/build/android/r8:__pkg__",
+        "//src/tools/android/java/com/google/devtools/build/android/r8:__pkg__",
+    ],
+    deps = select({
+        "//external:has_androidsdk": ["//external:android/d8_jar_import"],
+        "//conditions:default": [],
+    }),
+)
diff --git a/src/tools/android/java/com/android/tools/r8/CompatDxSupport.java b/src/tools/android/java/com/android/tools/r8/CompatDxSupport.java
new file mode 100644
index 0000000..31141b3
--- /dev/null
+++ b/src/tools/android/java/com/android/tools/r8/CompatDxSupport.java
@@ -0,0 +1,72 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.android.tools.r8;
+
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Class for accessing package private members of R8, which is used in the Bazel integration, but
+ * are not in the public API.
+ */
+public class CompatDxSupport {
+  public static void run(D8Command command, boolean minimalMainDex)
+      throws CompilationFailedException {
+    AndroidApp app = command.getInputApp();
+    InternalOptions options = command.getInternalOptions();
+    // DX allows --multi-dex without specifying a main dex list for legacy devices.
+    // That is broken, but for CompatDX we do the same to not break existing builds
+    // that are trying to transition.
+    try {
+      // Use reflection for:
+      //   <code>options.enableMainDexListCheck = false;</code>
+      // as bazel might link to an old r8.jar which does not have this field.
+      Field enableMainDexListCheck = options.getClass().getField("enableMainDexListCheck");
+      try {
+        enableMainDexListCheck.setBoolean(options, false);
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    } catch (NoSuchFieldException e) {
+      // Ignore if bazel is linking to an old r8.jar.
+    }
+
+    // DX has a minimal main dex flag. In compat mode only do minimal main dex
+    // if the flag is actually set.
+    options.minimalMainDex = minimalMainDex;
+
+    D8.runForTesting(app, options);
+  }
+
+  public static void enableDesugarBackportStatics(D8Command.Builder builder) {
+    // Use reflection for:
+    //   <code>builder.enableDesugarBackportStatics();</code>
+    // as bazel might link to an old r8.jar which does not have this field.
+    try {
+      Method enableDesugarBackportStatics =
+          builder.getClass().getMethod("enableDesugarBackportStatics");
+      try {
+        enableDesugarBackportStatics.invoke(builder);
+      } catch (ReflectiveOperationException e) {
+        throw new AssertionError(e);
+      }
+    } catch (NoSuchMethodException e) {
+      // Ignore if bazel is linking to an old r8.jar.
+    }
+  }
+
+  private CompatDxSupport() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD
index 4cccae8..56861bc 100644
--- a/src/tools/android/java/com/google/devtools/build/android/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -15,6 +15,7 @@
         "//src/tools/android/java/com/google/devtools/build/android/incrementaldeployment:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/junctions:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/proto:srcs",
+        "//src/tools/android/java/com/google/devtools/build/android/r8:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/ziputils:embedded_tools",
     ],
 )
@@ -28,6 +29,7 @@
         "//src/tools/android/java/com/google/devtools/build/android/desugar/scan",
         "//src/tools/android/java/com/google/devtools/build/android/dexer:dexerdeps",
         "//src/tools/android/java/com/google/devtools/build/android/idlclass:idlclass_lib",
+        "//src/tools/android/java/com/google/devtools/build/android/r8:dexerdeps",
         "//src/tools/android/java/com/google/devtools/build/android/ziputils:ziputils_lib",
     ],
 )
@@ -101,12 +103,14 @@
 filegroup(
     name = "srcs",
     srcs = glob(["**"]) + [
+        "//src/tools/android/java/com/android/tools/r8:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/desugar:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/dexer:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/idlclass:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/incrementaldeployment:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/junctions:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/proto:srcs",
+        "//src/tools/android/java/com/google/devtools/build/android/r8:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/resources:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/ziputils:srcs",
     ],
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/BUILD b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD
new file mode 100644
index 0000000..8335a71
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD
@@ -0,0 +1,56 @@
+# Description:
+#   Collection of dex utilities used in the bazel android actions.
+
+load("@rules_java//java:defs.bzl", "java_library")
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+    visibility = [
+        "//src/test/java/com/google/devtools/build/android/r8:__pkg__",
+        "//src/tools/android/java/com/google/devtools/build/android:__pkg__",
+    ],
+)
+
+filegroup(
+    name = "embedded_tools",
+    srcs = glob(
+        ["*.java"],
+        exclude = ["NoAndroidSdkStub.java"],
+    ) + ["BUILD.tools"],
+    visibility = ["//visibility:public"],
+)
+
+java_library(
+    name = "r8",
+    srcs = select({
+        "//external:has_androidsdk": glob(
+            ["*.java"],
+            exclude = ["NoAndroidSdkStub.java"],
+        ),
+        "//conditions:default": ["NoAndroidSdkStub.java"],
+    }),
+    visibility = ["//src/test/java/com/google/devtools/build/android/r8:__pkg__"],
+    deps = [
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/tools/android/java/com/android/tools/r8:r8_support",
+        "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+        "//third_party:guava",
+    ] + select({
+        "//external:has_androidsdk": ["//external:android/d8_jar_import"],
+        "//conditions:default": [],
+    }),
+)
+
+# The DexFileMerger and DexBuilder are built in BUILD.tools which is built in
+# a developers workspace, not the Bazel workspace. So we must bundle the
+# dependencies of those binaries into the embedded tools.
+java_library(
+    name = "dexerdeps",
+    visibility = [
+        "//src/tools/android/java/com/google/devtools/build/android:__pkg__",
+    ],
+    runtime_deps = [
+        # None right now. The D8/R8 jar has no dependencies.
+    ],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD.tools
new file mode 100644
index 0000000..766ae70
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/BUILD.tools
@@ -0,0 +1,23 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "r8",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//external:android/d8_jar_import",
+    ],
+)
+
+java_binary(
+    name = "DexBuilder",
+    main_class = "com.google.devtools.build.android.r8.DexBuilder",
+    visibility = ["//tools/android:__subpackages__"],
+    runtime_deps = [":r8"],
+)
+
+java_binary(
+    name = "DexFileMerger",
+    main_class = "com.google.devtools.build.android.r8.DexFileMerger",
+    visibility = ["//tools/android:__subpackages__"],
+    runtime_deps = [":r8"],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java
new file mode 100644
index 0000000..172e193
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDexBuilder.java
@@ -0,0 +1,202 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import static com.google.common.base.Verify.verify;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.CompatDxSupport;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Tool used by Bazel that converts a Jar file of .class files into a .zip file of .dex files, one
+ * per .class file, which we call a <i>dex archive</i>.
+ *
+ * <p>D8 version of DexBuilder.
+ */
+public class CompatDexBuilder {
+
+  private static class DexConsumer implements DexIndexedConsumer {
+
+    byte[] bytes;
+
+    @Override
+    public synchronized void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      verify(bytes == null, "Should not have been populated until now");
+      bytes = data.copyByteData();
+    }
+
+    byte[] getBytes() {
+      return bytes;
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      // Do nothing.
+    }
+  }
+
+  private String input;
+  private String output;
+  private int numberOfThreads = 8;
+  private boolean noLocals;
+  private boolean backportStatics;
+
+  public static void main(String[] args)
+      throws IOException, InterruptedException, ExecutionException {
+    new CompatDexBuilder().run(args);
+  }
+
+  @SuppressWarnings("JdkObsolete")
+  private void run(String[] args) throws IOException, InterruptedException, ExecutionException {
+    List<String> flags = new ArrayList<>();
+
+    for (String arg : args) {
+      if (arg.startsWith("@")) {
+        flags.addAll(Files.readAllLines(Paths.get(arg.substring(1))));
+      } else {
+        flags.add(arg);
+      }
+    }
+
+    for (int i = 0; i < flags.size(); i++) {
+      String flag = flags.get(i);
+      if (flag.startsWith("--positions=")) {
+        String positionsValue = flag.substring("--positions=".length());
+        if (positionsValue.startsWith("throwing") || positionsValue.startsWith("important")) {
+          noLocals = true;
+        }
+        continue;
+      }
+      if (flag.startsWith("--num-threads=")) {
+        numberOfThreads = Integer.parseInt(flag.substring("--num-threads=".length()));
+        continue;
+      }
+      switch (flag) {
+        case "--input_jar":
+          input = flags.get(++i);
+          break;
+        case "--output_zip":
+          output = flags.get(++i);
+          break;
+        case "--verify-dex-file":
+        case "--no-verify-dex-file":
+        case "--show_flags":
+        case "--no-optimize":
+        case "--nooptimize":
+        case "--help":
+          // Ignore
+          break;
+        case "--nolocals":
+          noLocals = true;
+          break;
+        case "--desugar-backport-statics":
+          backportStatics = true;
+          break;
+        default:
+          System.err.println("Unsupported option: " + flag);
+          System.exit(1);
+      }
+    }
+
+    if (input == null) {
+      System.err.println("No input jar specified");
+      System.exit(1);
+    }
+
+    if (output == null) {
+      System.err.println("No output jar specified");
+      System.exit(1);
+    }
+
+    ExecutorService executor = Executors.newWorkStealingPool(numberOfThreads);
+    try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(Paths.get(output)))) {
+
+      List<ZipEntry> toDex = new ArrayList<>();
+
+      try (ZipFile zipFile = new ZipFile(input, UTF_8)) {
+        final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        while (entries.hasMoreElements()) {
+          ZipEntry entry = entries.nextElement();
+          if (!entry.getName().endsWith(".class")) {
+            try (InputStream stream = zipFile.getInputStream(entry)) {
+              ZipUtils.addEntry(entry.getName(), stream, out);
+            }
+          } else {
+            toDex.add(entry);
+          }
+        }
+
+        List<Future<DexConsumer>> futures = new ArrayList<>(toDex.size());
+        for (ZipEntry classEntry : toDex) {
+          futures.add(executor.submit(() -> dexEntry(zipFile, classEntry, executor)));
+        }
+        for (int i = 0; i < futures.size(); i++) {
+          ZipEntry entry = toDex.get(i);
+          DexConsumer consumer = futures.get(i).get();
+          ZipUtils.addEntry(entry.getName() + ".dex", consumer.getBytes(), ZipEntry.STORED, out);
+        }
+      }
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  private DexConsumer dexEntry(ZipFile zipFile, ZipEntry classEntry, ExecutorService executor)
+      throws IOException, CompilationFailedException {
+    DexConsumer consumer = new DexConsumer();
+    D8Command.Builder builder = D8Command.builder();
+    builder
+        .setProgramConsumer(consumer)
+        .setMode(noLocals ? CompilationMode.RELEASE : CompilationMode.DEBUG)
+        .setMinApiLevel(13) // H_MR2.
+        .setDisableDesugaring(true);
+    if (backportStatics) {
+      CompatDxSupport.enableDesugarBackportStatics(builder);
+    }
+    try (InputStream stream = zipFile.getInputStream(classEntry)) {
+      builder.addClassProgramData(
+          ByteStreams.toByteArray(stream),
+          new ArchiveEntryOrigin(
+              classEntry.getName(), new PathOrigin(Paths.get(zipFile.getName()))));
+    }
+    D8.run(builder.build(), executor);
+    return consumer;
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/CompatDx.java b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDx.java
new file mode 100644
index 0000000..39e4bb0
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/CompatDx.java
@@ -0,0 +1,881 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import static com.google.common.base.Verify.verify;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.CompatDxSupport;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramConsumer;
+import com.android.tools.r8.Version;
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ArchiveResourceProvider;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.android.r8.CompatDx.DxCompatOptions.DxUsageMessage;
+import com.google.devtools.build.android.r8.CompatDx.DxCompatOptions.PositionInfo;
+import com.google.devtools.common.options.Converters.StringConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Dx compatibility interface for d8.
+ *
+ * <p>This should become a mostly drop-in replacement for uses of the DX dexer (eg, dx --dex ...).
+ */
+public class CompatDx {
+
+  private static final String USAGE_HEADER = "Usage: compatdx [options] <input files>";
+
+  /** Commandline options. */
+  public static class Options extends OptionsBase {
+    @Option(
+        name = "dex",
+        defaultValue = "true",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Generate dex output.")
+    public boolean dex;
+
+    @Option(
+        name = "debug",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Print debug information.")
+    public boolean debug;
+
+    @Option(
+        name = "verbose",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Print verbose information.")
+    public boolean verbose;
+
+    @Option(
+        name = "positions",
+        defaultValue = "lines",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "What source-position information to keep. One of: none, lines, important.")
+    public String positions;
+
+    @Option(
+        name = "no-locals",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Don't keep local variable information.")
+    public boolean noLocals;
+
+    @Option(
+        name = "statistics",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Print statistics information.")
+    public boolean statistics;
+
+    @Option(
+        name = "no-optimize",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Don't optimize.")
+    public boolean noOptimize;
+
+    @Option(
+        name = "optimize-list",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "File listing methods to optimize.")
+    public String optimizeList;
+
+    @Option(
+        name = "no-optimize-list",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "File listing methods not to optimize.")
+    public String noOptimizeList;
+
+    @Option(
+        name = "no-strict",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Disable strict file/class name checks.")
+    public boolean noStrict;
+
+    @Option(
+        name = "keep-classes",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Keep input class files in in output jar.")
+    public boolean keepClasses;
+
+    @Option(
+        name = "output",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "Output file or directory.")
+    public String output;
+
+    @Option(
+        name = "dump-to",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "File to dump information to.")
+    public String dumpTo;
+
+    @Option(
+        name = "dump-width",
+        defaultValue = "8",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Max width for columns in dump output.")
+    public int dumpWidth;
+
+    @Option(
+        name = "dump-method",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "Method to dump information for.")
+    public String methodToDump;
+
+    @Option(
+        name = "dump",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Dump information.")
+    public boolean dump;
+
+    @Option(
+        name = "verbose-dump",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Dump verbose information.")
+    public boolean verboseDump;
+
+    @Option(
+        name = "no-files",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Don't fail if given no files.")
+    public boolean noFiles;
+
+    @Option(
+        name = "core-library",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Construct a core library.")
+    public boolean coreLibrary;
+
+    @Option(
+        name = "num-threads",
+        defaultValue = "1",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Number of threads to run with.")
+    public int numThreads;
+
+    @Option(
+        name = "incremental",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Merge result with the output if it exists.")
+    public boolean incremental;
+
+    @Option(
+        name = "force-jumbo",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Force use of string-jumbo instructions.")
+    public boolean forceJumbo;
+
+    @Option(
+        name = "no-warning",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Suppress warnings.")
+    public boolean noWarning;
+
+    @Option(
+        name = "set-max-idx-number",
+        defaultValue = "0",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Undocumented: Set maximal index number to use in a dex file.")
+    public int maxIndexNumber;
+
+    @Option(
+        name = "minimal-main-dex",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Produce smallest possible main dex.")
+    public boolean minimalMainDex;
+
+    @Option(
+        name = "main-dex-list",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        converter = StringConverter.class,
+        help = "File listing classes that must be in the main dex file.")
+    public String mainDexList;
+
+    @Option(
+        name = "multi-dex",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Allow generation of multi-dex.")
+    public boolean multiDex;
+
+    @Option(
+        name = "min-sdk-version",
+        defaultValue = "1",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Minimum Android API level compatibility.")
+    public int minApiLevel;
+
+    @Option(
+        name = "desugar-backport-statics",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Backport additional Java 8 APIs.")
+    public boolean backportStatics;
+
+    @Option(
+        name = "input-list",
+        defaultValue = "null",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        converter = StringConverter.class,
+        help = "File listing input files.")
+    public String inputList;
+
+    @Option(
+        name = "version",
+        defaultValue = "false", // dx's default
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        allowMultiple = false,
+        help = "Print the version of this tool.")
+    public boolean version;
+  }
+
+  /** Compatibility options parsing for the DX --dex sub-command. */
+  public static class DxCompatOptions {
+
+    // Final values after parsing.
+    // Note: These are ordered by their occurrence in "dx --help"
+    public final boolean help;
+    public final boolean version;
+    public final boolean debug;
+    public final boolean verbose;
+    public final PositionInfo positions;
+    public final boolean noLocals;
+    public final boolean noOptimize;
+    public final boolean statistics;
+    public final String optimizeList;
+    public final String noOptimizeList;
+    public final boolean noStrict;
+    public final boolean keepClasses;
+    public final String output;
+    public final String dumpTo;
+    public final int dumpWidth;
+    public final String methodToDump;
+    public final boolean verboseDump;
+    public final boolean dump;
+    public final boolean noFiles;
+    public final boolean coreLibrary;
+    public final int numThreads;
+    public final boolean incremental;
+    public final boolean forceJumbo;
+    public final boolean noWarning;
+    public final boolean multiDex;
+    public final String mainDexList;
+    public final boolean minimalMainDex;
+    public final int minApiLevel;
+    public final boolean backportStatics;
+    public final String inputList;
+    public final List<String> inputs;
+    // Undocumented option
+    public final int maxIndexNumber;
+
+    /**
+     * Values for dx --positions flag. Corresponding to "none", "important", "lines", "throwing".
+     */
+    public enum PositionInfo {
+      NONE,
+      IMPORTANT,
+      LINES,
+      THROWING
+    }
+
+    /** Exception thrown on invalid dx compat usage. */
+    public static class DxUsageMessage extends Exception {
+
+      public final String message;
+
+      DxUsageMessage(String message) {
+        this.message = message;
+      }
+
+      void printHelpOn(PrintStream sink) throws IOException {
+        sink.println(message);
+      }
+    }
+
+    private DxCompatOptions(Options options, List<String> remaining) {
+      help = false;
+      version = options.version;
+      debug = options.debug;
+      verbose = options.debug;
+      switch (options.positions) {
+        case "none":
+          positions = PositionInfo.NONE;
+          break;
+        case "lines":
+          positions = PositionInfo.LINES;
+          break;
+        case "throwing":
+          positions = PositionInfo.THROWING;
+          break;
+        case "important":
+          positions = PositionInfo.IMPORTANT;
+          break;
+        default:
+          throw new AssertionError("Unreachable");
+      }
+      noLocals = options.noLocals;
+      noOptimize = options.noOptimize;
+      statistics = options.statistics;
+      optimizeList = options.optimizeList;
+      noOptimizeList = options.noOptimizeList;
+      noStrict = options.noStrict;
+      keepClasses = options.keepClasses;
+      output = options.output;
+      dumpTo = options.dumpTo;
+      dumpWidth = options.dumpWidth;
+      methodToDump = options.methodToDump;
+      dump = options.dump;
+      verboseDump = options.verboseDump;
+      noFiles = options.noFiles;
+      coreLibrary = options.coreLibrary;
+      numThreads = options.numThreads;
+      incremental = options.incremental;
+      forceJumbo = options.forceJumbo;
+      noWarning = options.noWarning;
+      multiDex = options.multiDex;
+      mainDexList = options.mainDexList;
+      minimalMainDex = options.minimalMainDex;
+      minApiLevel = options.minApiLevel;
+      backportStatics = options.backportStatics;
+      inputList = options.inputList;
+      inputs = remaining;
+      maxIndexNumber = options.maxIndexNumber;
+    }
+
+    public static DxCompatOptions parse(String[] args) {
+      OptionsParser optionsParser = OptionsParser.builder().optionsClasses(Options.class).build();
+      optionsParser.parseAndExitUponError(args);
+      Options options = optionsParser.getOptions(Options.class);
+      return new DxCompatOptions(options, optionsParser.getResidue());
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    try {
+      run(args);
+    } catch (DxUsageMessage e) {
+      System.err.println(USAGE_HEADER);
+      e.printHelpOn(System.err);
+      System.exit(1);
+    } catch (CompilationFailedException e) {
+      throw new AssertionError("Failure", e);
+    }
+  }
+
+  private static void run(String[] args)
+      throws DxUsageMessage, IOException, CompilationFailedException {
+    DxCompatOptions dexArgs = DxCompatOptions.parse(args);
+    if (dexArgs.version) {
+      System.out.println("CompatDx " + Version.getVersionString());
+      return;
+    }
+    CompilationMode mode = CompilationMode.RELEASE;
+    Path output = null;
+    List<Path> inputs = new ArrayList<>();
+    boolean singleDexFile = !dexArgs.multiDex;
+    Path mainDexList = null;
+    int numberOfThreads = 1;
+
+    for (String path : dexArgs.inputs) {
+      processPath(Paths.get(path), inputs);
+    }
+    if (inputs.isEmpty()) {
+      if (dexArgs.noFiles) {
+        return;
+      }
+      throw new DxUsageMessage("No input files specified");
+    }
+
+    if (dexArgs.dump && dexArgs.verbose) {
+      System.out.println("Warning: dump is not supported");
+    }
+
+    if (dexArgs.verboseDump) {
+      throw new Unimplemented("verbose dump file not yet supported");
+    }
+
+    if (dexArgs.methodToDump != null) {
+      throw new Unimplemented("method-dump not yet supported");
+    }
+
+    if (dexArgs.output != null) {
+      output = Paths.get(dexArgs.output);
+      if (FileUtils.isDexFile(output)) {
+        if (!singleDexFile) {
+          throw new DxUsageMessage("Cannot output to a single dex-file when running with multidex");
+        }
+      } else if (!FileUtils.isArchive(output)
+          && (!output.toFile().exists() || !output.toFile().isDirectory())) {
+        throw new DxUsageMessage(
+            "Unsupported output file or output directory does not exist. "
+                + "Output must be a directory or a file of type dex, apk, jar or zip.");
+      }
+    }
+
+    if (dexArgs.dumpTo != null && dexArgs.verbose) {
+      System.out.println("dump-to file not yet supported");
+    }
+
+    if (dexArgs.positions == PositionInfo.NONE && dexArgs.verbose) {
+      System.out.println("Warning: no support for positions none.");
+    }
+
+    if (dexArgs.positions == PositionInfo.LINES && !dexArgs.noLocals) {
+      mode = CompilationMode.DEBUG;
+    }
+
+    if (dexArgs.incremental) {
+      throw new Unimplemented("incremental merge not supported yet");
+    }
+
+    if (dexArgs.forceJumbo && dexArgs.verbose) {
+      System.out.println(
+          "Warning: no support for forcing jumbo-strings.\n"
+              + "Strings will only use jumbo-string indexing if necessary.\n"
+              + "Make sure that any dex merger subsequently used "
+              + "supports correct handling of jumbo-strings (eg, D8/R8 does).");
+    }
+
+    if (dexArgs.noOptimize && dexArgs.verbose) {
+      System.out.println("Warning: no support for not optimizing");
+    }
+
+    if (dexArgs.optimizeList != null) {
+      throw new Unimplemented("no support for optimize-method list");
+    }
+
+    if (dexArgs.noOptimizeList != null) {
+      throw new Unimplemented("no support for dont-optimize-method list");
+    }
+
+    if (dexArgs.statistics && dexArgs.verbose) {
+      System.out.println("Warning: no support for printing statistics");
+    }
+
+    if (dexArgs.numThreads > 1) {
+      numberOfThreads = dexArgs.numThreads;
+    }
+
+    if (dexArgs.mainDexList != null) {
+      mainDexList = Paths.get(dexArgs.mainDexList);
+    }
+
+    if (dexArgs.noStrict) {
+      if (dexArgs.verbose) {
+        System.out.println("Warning: conservative main-dex list not yet supported");
+      }
+    } else {
+      if (dexArgs.verbose) {
+        System.out.println("Warning: strict name checking not yet supported");
+      }
+    }
+
+    if (dexArgs.minimalMainDex && dexArgs.verbose) {
+      System.out.println("Warning: minimal main-dex support is not yet supported");
+    }
+
+    if (dexArgs.maxIndexNumber != 0 && dexArgs.verbose) {
+      System.out.println("Warning: internal maximum-index setting is not supported");
+    }
+
+    if (numberOfThreads < 1) {
+      throw new DxUsageMessage("Invalid numThreads value of " + numberOfThreads);
+    }
+    ExecutorService executor = Executors.newWorkStealingPool(numberOfThreads);
+
+    try {
+      D8Command.Builder builder = D8Command.builder();
+      inputs.forEach(
+          input ->
+              builder.addProgramResourceProvider(ArchiveResourceProvider.fromArchive(input, true)));
+
+      builder
+          // .addProgramFiles(inputs)
+          .setProgramConsumer(createConsumer(inputs, output, singleDexFile, dexArgs.keepClasses))
+          .setMode(mode)
+          .setDisableDesugaring(true) // DX does not desugar.
+          .setMinApiLevel(dexArgs.minApiLevel);
+      if (mainDexList != null) {
+        builder.addMainDexListFiles(mainDexList);
+      }
+      if (dexArgs.backportStatics) {
+        CompatDxSupport.enableDesugarBackportStatics(builder);
+      }
+      CompatDxSupport.run(builder.build(), dexArgs.minimalMainDex);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  private static ProgramConsumer createConsumer(
+      List<Path> inputs, Path output, boolean singleDexFile, boolean keepClasses)
+      throws DxUsageMessage {
+    if (output == null) {
+      return DexIndexedConsumer.emptyConsumer();
+    }
+    if (singleDexFile) {
+      return new SingleDexFileConsumer(
+          FileUtils.isDexFile(output)
+              ? new NamedDexFileConsumer(output)
+              : createDexConsumer(output, inputs, keepClasses));
+    }
+    return createDexConsumer(output, inputs, keepClasses);
+  }
+
+  private static DexIndexedConsumer createDexConsumer(
+      Path output, List<Path> inputs, boolean keepClasses) throws DxUsageMessage {
+    if (keepClasses) {
+      if (!FileUtils.isArchive(output)) {
+        throw new DxCompatOptions.DxUsageMessage(
+            "Output must be an archive when --keep-classes is set.");
+      }
+      return new ArchiveConsumer(output, inputs);
+    }
+    return FileUtils.isArchive(output)
+        ? new ArchiveConsumer(output)
+        : new DexIndexedConsumer.DirectoryConsumer(output);
+  }
+
+  private static class SingleDexFileConsumer extends DexIndexedConsumer.ForwardingConsumer {
+
+    private byte[] bytes = null;
+
+    public SingleDexFileConsumer(DexIndexedConsumer consumer) {
+      super(consumer);
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      if (fileIndex > 0) {
+        throw new CompilationError(
+            "Compilation result could not fit into a single dex file. "
+                + "Reduce the input-program size or run with --multi-dex enabled");
+      }
+      verify(bytes == null, "Should not have been populated until now");
+      // Store a copy of the bytes as we may not assume the backing is valid after accept returns.
+      bytes = data.copyByteData();
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (bytes != null) {
+        super.accept(0, ByteDataView.of(bytes), null, handler);
+      }
+      super.finished(handler);
+    }
+  }
+
+  private static class NamedDexFileConsumer extends DexIndexedConsumer.ForwardingConsumer {
+
+    private final Path output;
+
+    public NamedDexFileConsumer(Path output) {
+      super(null);
+      this.output = output;
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      StandardOpenOption[] options = {
+        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING
+      };
+      try (OutputStream stream = new BufferedOutputStream(Files.newOutputStream(output, options))) {
+        stream.write(data.getBuffer(), data.getOffset(), data.getLength());
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, new PathOrigin(output)));
+      }
+    }
+  }
+
+  /**
+   * Consumer for writing the generated classes.dex files to an archive. Supports writing the input
+   * class files to the same archive as well.
+   */
+  private static class ArchiveConsumer implements DexIndexedConsumer {
+    private final Path path;
+    private final List<Path> inputs;
+    private final Origin origin;
+    private ZipOutputStream stream;
+    private int nextClassesDexIndex;
+    private final Map<Integer, ClassesDexFileData> pendingClassesDexFiles = new HashMap<>();
+
+    /** Content of a classes.dex file */
+    private static class ClassesDexFileData {
+      private final int index;
+      private final ByteDataView content;
+
+      private ClassesDexFileData(int index, ByteDataView content) {
+        this.index = index;
+        this.content = content;
+      }
+    }
+
+    ArchiveConsumer(Path path) {
+      this(path, ImmutableList.of());
+    }
+
+    ArchiveConsumer(Path path, List<Path> inputs) {
+      this.path = path;
+      this.inputs = inputs;
+      this.origin = new PathOrigin(path);
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      ensureOpenArchive(handler);
+      addIndexedClassesDexFile(fileIndex, data, handler);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      verify(pendingClassesDexFiles.isEmpty(), "All DEX files should have been written");
+      if (stream == null) {
+        return;
+      }
+      try {
+        writeInputClassesToArchive(handler);
+        stream.close();
+        stream = null;
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+
+    private synchronized void addIndexedClassesDexFile(
+        int fileIndex, ByteDataView data, DiagnosticsHandler handler) {
+      // Always add the classes.dex files in <code>fileIndex</code> order to have stable output.
+      // Store the ones which arrive out-of-order and write as soon as possible.
+      pendingClassesDexFiles.put(
+          fileIndex, new ClassesDexFileData(fileIndex, ByteDataView.of(data.copyByteData())));
+      while (pendingClassesDexFiles.containsKey(nextClassesDexIndex)) {
+        ClassesDexFileData classesDexFileData = pendingClassesDexFiles.get(nextClassesDexIndex);
+        writeClassesDexFile(classesDexFileData, handler);
+        pendingClassesDexFiles.remove(nextClassesDexIndex);
+        nextClassesDexIndex++;
+      }
+    }
+
+    /** Get or open the zip output stream. */
+    private synchronized void ensureOpenArchive(DiagnosticsHandler handler) {
+      if (stream != null) {
+        return;
+      }
+      try {
+        stream =
+            new ZipOutputStream(
+                Files.newOutputStream(
+                    path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+
+    private void writeClassesDexFile(
+        ClassesDexFileData classesDexFileData, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(
+            getDexFileName(classesDexFileData.index),
+            classesDexFileData.content,
+            ZipEntry.DEFLATED,
+            stream);
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+
+    protected String getDexFileName(int fileIndex) {
+      return "classes" + (fileIndex == 0 ? "" : (fileIndex + 1)) + FileUtils.DEX_EXTENSION;
+    }
+
+    private void writeClassFile(String name, ByteDataView content, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(name, content, ZipEntry.DEFLATED, stream);
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+
+    @SuppressWarnings("JdkObsolete") // Uses Enumeration by design.
+    private void writeInputClassesToArchive(DiagnosticsHandler handler) throws IOException {
+      // For each input archive file, add all class files within.
+      for (Path input : inputs) {
+        if (FileUtils.isArchive(input)) {
+          try (ZipFile zipFile = new ZipFile(input.toFile(), UTF_8)) {
+            final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+              ZipEntry entry = entries.nextElement();
+              if (FileUtils.isClassFile(entry.getName())) {
+                try (InputStream entryStream = zipFile.getInputStream(entry)) {
+                  byte[] bytes = ByteStreams.toByteArray(entryStream);
+                  writeClassFile(entry.getName(), ByteDataView.of(bytes), handler);
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private static void processPath(Path path, List<Path> files) throws IOException {
+    if (!Files.exists(path)) {
+      throw new CompilationError("File does not exist: " + path);
+    }
+    if (Files.isDirectory(path)) {
+      processDirectory(path, files);
+      return;
+    }
+    if (FileUtils.isZipFile(path) || FileUtils.isJarFile(path) || FileUtils.isClassFile(path)) {
+      files.add(path);
+      return;
+    }
+    if (FileUtils.isApkFile(path)) {
+      throw new Unimplemented("apk files not yet supported: " + path);
+    }
+  }
+
+  private static void processDirectory(Path directory, List<Path> files) throws IOException {
+    verify(Files.exists(directory), "Directory must exist");
+
+    try (Stream<Path> pathStream = Files.list(directory)) {
+      for (Path file : pathStream.collect(toList())) {
+        processPath(file, files);
+      }
+    }
+  }
+
+  private CompatDx() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/DexFileMerger.java b/src/tools/android/java/com/google/devtools/build/android/r8/DexFileMerger.java
new file mode 100644
index 0000000..d0ef675
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/DexFileMerger.java
@@ -0,0 +1,444 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
+import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.android.Converters.ExistingPathConverter;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.common.options.EnumConverter;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionDocumentationCategory;
+import com.google.devtools.common.options.OptionEffectTag;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Tool used by Bazel as a replacement for Android's {@code dx} tool that assembles a single or, if
+ * allowed and necessary, multiple {@code .dex} files from a given archive of {@code .dex} and
+ * {@code .class} files. The tool merges the {@code .dex} files it encounters into a single file and
+ * additionally encodes any {@code .class} files it encounters. If multidex is allowed then the tool
+ * will generate multiple files subject to the {@code .dex} file format's limits on the number of
+ * methods and fields.
+ *
+ * <p>D8 version of DexFileMerger.
+ */
+public class DexFileMerger {
+  /** File name prefix of a {@code .dex} file automatically loaded in an archive. */
+  private static final String DEX_PREFIX = "classes";
+
+  private static final String DEFAULT_OUTPUT_ARCHIVE_FILENAME = "classes.dex.jar";
+
+  private static final boolean PRINT_ARGS = false;
+
+  /** Strategies for outputting multiple {@code .dex} files supported by {@link DexFileMerger}. */
+  public enum MultidexStrategy {
+    /** Create exactly one .dex file. The operation will fail if .dex limits are exceeded. */
+    OFF,
+    /** Create exactly one &lt;prefixN&gt;.dex file with N taken from the (single) input archive. */
+    GIVEN_SHARD,
+    /**
+     * Assemble .dex files similar to {@link com.android.dx.command.dexer.Main dx}, with all but one
+     * file as large as possible.
+     */
+    MINIMAL,
+    /**
+     * Allow some leeway and sometimes use additional .dex files to speed up processing. This option
+     * exists to give flexibility but it often (or always) may be identical to {@link #MINIMAL}.
+     */
+    BEST_EFFORT;
+
+    public boolean isMultidexAllowed() {
+      switch (this) {
+        case OFF:
+        case GIVEN_SHARD:
+          return false;
+        case MINIMAL:
+        case BEST_EFFORT:
+          return true;
+      }
+      throw new AssertionError("Unknown: " + this);
+    }
+  }
+
+  /** Option converter for {@link MultidexStrategy}. */
+  public static class MultidexStrategyConverter extends EnumConverter<MultidexStrategy> {
+    public MultidexStrategyConverter() {
+      super(MultidexStrategy.class, "multidex strategy");
+    }
+  }
+
+  /** Commandline options. */
+  public static class Options extends OptionsBase {
+    @Option(
+        name = "input",
+        allowMultiple = true,
+        defaultValue = "",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        converter = ExistingPathConverter.class,
+        abbrev = 'i',
+        help =
+            "Input archives with .dex files to merge.  Inputs are processed in given order, so"
+                + " classes from later inputs will be added after earlier inputs.  Duplicate"
+                + " classes are dropped.")
+    public List<Path> inputArchives;
+
+    @Option(
+        name = "output",
+        defaultValue = DEFAULT_OUTPUT_ARCHIVE_FILENAME,
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        converter = PathConverter.class,
+        abbrev = 'o',
+        help = "Output archive to write.")
+    public Path outputArchive;
+
+    @Option(
+        name = "multidex",
+        defaultValue = "off",
+        category = "multidex",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        converter = MultidexStrategyConverter.class,
+        help = "Allow more than one .dex file in the output.")
+    public MultidexStrategy multidexMode;
+
+    @Option(
+        name = "main-dex-list",
+        defaultValue = "null",
+        category = "multidex",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        converter = ExistingPathConverter.class,
+        help = "List of classes to be placed into \"main\" classes.dex file.")
+    public Path mainDexListFile;
+
+    @Option(
+        name = "minimal-main-dex",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help =
+            "If true, *only* classes listed in --main_dex_list file are placed into \"main\" "
+                + "classes.dex file.")
+    public boolean minimalMainDex;
+
+    @Option(
+        name = "verbose",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "If true, print information about the merged files and resulting files to stdout.")
+    public boolean verbose;
+
+    @Option(
+        name = "max-bytes-wasted-per-file",
+        defaultValue = "0",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help =
+            "Limit on conservatively allocated but unused bytes per dex file, which can enable "
+                + "faster merging.")
+    public int wasteThresholdPerDex;
+
+    // Undocumented dx option for testing multidex logic
+    @Option(
+        name = "set-max-idx-number",
+        defaultValue = "0",
+        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Limit on fields and methods in a single dex file.")
+    public int maxNumberOfIdxPerDex;
+
+    @Option(
+        name = "forceJumbo",
+        defaultValue = "false",
+        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Typically not needed flag intended to imitate dx's --forceJumbo.")
+    public boolean forceJumbo;
+
+    @Option(
+        name = "dex_prefix",
+        defaultValue = DEX_PREFIX,
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.UNKNOWN},
+        help = "Dex file output prefix.")
+    public String dexPrefix;
+  }
+
+  private static Options parseArguments(String[] args) throws IOException {
+    OptionsParser optionsParser =
+        OptionsParser.builder()
+            .optionsClasses(Options.class)
+            .argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
+            .build();
+    optionsParser.parseAndExitUponError(args);
+
+    return optionsParser.getOptions(Options.class);
+  }
+
+  /**
+   * Implements a DexIndexedConsumer writing into a ZipStream with support for custom dex file name
+   * prefix, reindexing a single dex output file to a nonzero index and reporting if any data has
+   * been written.
+   */
+  private static class ArchiveConsumer implements DexIndexedConsumer {
+    private final Path path;
+    private final String prefix;
+    private final Integer singleFixedFileIndex;
+    private final Origin origin;
+    private ZipOutputStream stream = null;
+
+    private int highestIndexWritten = -1;
+    private final Map<Integer, Runnable> writers = new TreeMap<>();
+    private boolean hasWrittenSomething = false;
+
+    /** If singleFixedFileIndex is not null then we expect only one output dex file */
+    private ArchiveConsumer(Path path, String prefix, Integer singleFixedFileIndex) {
+      this.path = path;
+      this.prefix = prefix;
+      this.singleFixedFileIndex = singleFixedFileIndex;
+      this.origin = new PathOrigin(path);
+    }
+
+    private boolean hasWrittenSomething() {
+      return hasWrittenSomething;
+    }
+
+    private String getDexFileName(int fileIndex) {
+      if (singleFixedFileIndex != null) {
+        fileIndex = singleFixedFileIndex;
+      }
+      return prefix + (fileIndex == 0 ? "" : (fileIndex + 1)) + FileUtils.DEX_EXTENSION;
+    }
+
+    @Override
+    public synchronized void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      if (singleFixedFileIndex != null && fileIndex != 0) {
+        handler.error(new StringDiagnostic("Result does not fit into a single dex file."));
+        return;
+      }
+      // Make a copy of the actual bytes as they will possibly be accessed later by the runner.
+      final byte[] bytes = data.copyByteData();
+      writers.put(fileIndex, () -> writeEntry(fileIndex, bytes, handler));
+
+      while (writers.containsKey(highestIndexWritten + 1)) {
+        ++highestIndexWritten;
+        writers.get(highestIndexWritten).run();
+        writers.remove(highestIndexWritten);
+      }
+    }
+
+    /** Get or open the zip output stream. */
+    private synchronized ZipOutputStream getStream(DiagnosticsHandler handler) {
+      if (stream == null) {
+        try {
+          stream =
+              new ZipOutputStream(
+                  Files.newOutputStream(
+                      path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+        } catch (IOException e) {
+          handler.error(new ExceptionDiagnostic(e, origin));
+        }
+      }
+      return stream;
+    }
+
+    private void writeEntry(int fileIndex, byte[] data, DiagnosticsHandler handler) {
+      try {
+        ZipUtils.writeToZipStream(
+            getDexFileName(fileIndex),
+            ByteDataView.of(data),
+            ZipEntry.DEFLATED,
+            getStream(handler));
+        hasWrittenSomething = true;
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (!writers.isEmpty()) {
+        handler.error(
+            new StringDiagnostic(
+                "Failed to write zip, for a multidex output some of the classes.dex files were"
+                    + " not produced."));
+      }
+      try {
+        if (stream != null) {
+          stream.close();
+          stream = null;
+        }
+      } catch (IOException e) {
+        handler.error(new ExceptionDiagnostic(e, origin));
+      }
+    }
+  }
+
+  private static int parseFileIndexFromShardFilename(String inputArchive) {
+    Pattern namingPattern = Pattern.compile("([0-9]+)\\..*");
+    String name = new File(inputArchive).getName();
+    Matcher matcher = namingPattern.matcher(name);
+    if (!matcher.matches()) {
+      throw new IllegalStateException(
+          String.format(
+              "Expect input named <N>.xxx.zip for --multidex=given_shard but got %s.", name));
+    }
+    int shard = Integer.parseInt(matcher.group(1));
+    if (shard <= 0) {
+      throw new IllegalStateException(
+          String.format("Expect positive N in input named <N>.xxx.zip but got %d.", shard));
+    }
+    return shard;
+  }
+
+  public static void run(String[] args) throws CompilationFailedException, IOException {
+    Options options = parseArguments(args);
+
+    if (options.inputArchives.isEmpty()) {
+      throw new IllegalStateException("Need at least one --input");
+    }
+
+    if (options.mainDexListFile != null && options.inputArchives.size() != 1) {
+      throw new IllegalStateException(
+          "--main-dex-list only supported with exactly one --input, use DexFileSplitter for more");
+    }
+
+    if (!options.multidexMode.isMultidexAllowed()) {
+      if (options.mainDexListFile != null) {
+        throw new IllegalStateException(
+            "--main-dex-list is only supported with multidex enabled, but mode is: "
+                + options.multidexMode);
+      }
+      if (options.minimalMainDex) {
+        throw new IllegalStateException(
+            "--minimal-main-dex is only supported with multidex enabled, but mode is: "
+                + options.multidexMode);
+      }
+    }
+
+    D8Command.Builder builder = D8Command.builder();
+
+    Map<String, Integer> inputOrdering =
+        Maps.newHashMapWithExpectedSize(options.inputArchives.size());
+    int sequenceNumber = 0;
+    for (Path s : options.inputArchives) {
+      builder.addProgramFiles(s);
+      inputOrdering.put(s.toString(), sequenceNumber++);
+    }
+
+    // Determine enabling multidexing and file indexing.
+    Integer singleFixedFileIndex = null;
+    switch (options.multidexMode) {
+      case OFF:
+        singleFixedFileIndex = 0;
+        break;
+      case GIVEN_SHARD:
+        if (options.inputArchives.size() != 1) {
+          throw new IllegalStateException("'--multidex=given_shard' requires exactly one --input.");
+        }
+        singleFixedFileIndex =
+            parseFileIndexFromShardFilename(options.inputArchives.get(0).toString()) - 1;
+        break;
+      case MINIMAL:
+      case BEST_EFFORT:
+        // Nothing to do.
+        break;
+    }
+
+    if (options.mainDexListFile != null) {
+      builder.addMainDexListFiles(options.mainDexListFile);
+    }
+
+    ArchiveConsumer consumer =
+        new ArchiveConsumer(options.outputArchive, options.dexPrefix, singleFixedFileIndex);
+    builder.setProgramConsumer(consumer);
+
+    // Try to run through com.android.tools.r8.DexFileMergerHelper.run. If not found, which
+    // can happen when bazel use a d8.jar from a Platform SDK, fall back to plain D8 execution.
+    try {
+      Class<?> dexFileMergerHelper = Class.forName("com.android.tools.r8.DexFileMergerHelper");
+      try {
+        Method run =
+            dexFileMergerHelper.getDeclaredMethod("run", D8Command.class, Boolean.class, Map.class);
+        // DexFileMergerHelper.run(builder.build(), options.minimalMainDex, inputOrdering);
+        run.invoke(null, builder.build(), options.minimalMainDex, inputOrdering);
+      } catch (NoSuchMethodException e) {
+        D8.run(builder.build());
+      } catch (ReflectiveOperationException e) {
+        throw new AssertionError("Unable to invoke run in DexFileMergerHelper", e);
+      }
+    } catch (ClassNotFoundException e) {
+      D8.run(builder.build());
+    }
+
+    // If input was empty we still need to write out an empty zip.
+    if (!consumer.hasWrittenSomething()) {
+      File f = options.outputArchive.toFile();
+      try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {}
+    }
+  }
+
+  public static void main(String[] args) throws CompilationFailedException {
+    try {
+      if (PRINT_ARGS) {
+        printArgs(args);
+      }
+      run(args);
+    } catch (CompilationFailedException | IOException e) {
+      System.err.println("Merge failed: " + e.getMessage());
+      throw new CompilationFailedException("Merge failed: " + e.getMessage());
+    }
+  }
+
+  private static void printArgs(String[] args) {
+    System.err.print("r8.DexFileMerger");
+    for (String s : args) {
+      System.err.printf(" %s", s);
+    }
+    System.err.println();
+  }
+
+  private DexFileMerger() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/FileUtils.java b/src/tools/android/java/com/google/devtools/build/android/r8/FileUtils.java
new file mode 100644
index 0000000..e54cfe0
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/FileUtils.java
@@ -0,0 +1,94 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import com.google.common.base.Ascii;
+import java.nio.file.Path;
+
+class FileUtils {
+  public static final String AAR_EXTENSION = ".aar";
+  public static final String APK_EXTENSION = ".apk";
+  public static final String CLASS_EXTENSION = ".class";
+  public static final String DEX_EXTENSION = ".dex";
+  public static final String JAR_EXTENSION = ".jar";
+  public static final String ZIP_EXTENSION = ".zip";
+  public static final String MODULE_INFO_CLASS = "module-info.class";
+  public static final String META_INF = "meta-inf";
+
+  private static boolean hasExtension(String name, String extension) {
+    return Ascii.toLowerCase(name).endsWith(extension);
+  }
+
+  private static boolean hasExtension(Path path, String extension) {
+    return hasExtension(path.getFileName().toString(), extension);
+  }
+
+  static boolean isDexFile(Path path) {
+    return hasExtension(path, DEX_EXTENSION);
+  }
+
+  static boolean isClassFile(String name) {
+    name = Ascii.toLowerCase(name);
+    // Android does not support Java 9 module, thus skip module-info.
+    if (name.equals(MODULE_INFO_CLASS)) {
+      return false;
+    }
+    if (name.startsWith(META_INF) || name.startsWith("/" + META_INF)) {
+      return false;
+    }
+    return name.endsWith(CLASS_EXTENSION);
+  }
+
+  static boolean isClassFile(Path path) {
+    return isClassFile(path.getFileName().toString());
+  }
+
+  static boolean isJarFile(String name) {
+    return hasExtension(name, JAR_EXTENSION);
+  }
+
+  static boolean isJarFile(Path path) {
+    return hasExtension(path, JAR_EXTENSION);
+  }
+
+  static boolean isZipFile(String name) {
+    return hasExtension(name, ZIP_EXTENSION);
+  }
+
+  static boolean isZipFile(Path path) {
+    return hasExtension(path, ZIP_EXTENSION);
+  }
+
+  static boolean isApkFile(String name) {
+    return hasExtension(name, APK_EXTENSION);
+  }
+
+  static boolean isApkFile(Path path) {
+    return hasExtension(path, APK_EXTENSION);
+  }
+
+  static boolean isAarFile(String name) {
+    return hasExtension(name, AAR_EXTENSION);
+  }
+
+  static boolean isAarFile(Path path) {
+    return hasExtension(path, AAR_EXTENSION);
+  }
+
+  static boolean isArchive(Path path) {
+    return isApkFile(path) || isJarFile(path) || isZipFile(path) || isAarFile(path);
+  }
+
+  private FileUtils() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/NoAndroidSdkStub.java b/src/tools/android/java/com/google/devtools/build/android/r8/NoAndroidSdkStub.java
new file mode 100644
index 0000000..6fc6dae
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/NoAndroidSdkStub.java
@@ -0,0 +1,15 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+class NoAndroidSdkStub {}
diff --git a/src/tools/android/java/com/google/devtools/build/android/r8/ZipUtils.java b/src/tools/android/java/com/google/devtools/build/android/r8/ZipUtils.java
new file mode 100644
index 0000000..aa25e3e
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/r8/ZipUtils.java
@@ -0,0 +1,63 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.r8;
+
+import com.android.tools.r8.ByteDataView;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+class ZipUtils {
+  static void addEntry(String name, InputStream stream, ZipOutputStream zip) throws IOException {
+    ZipUtils.addEntry(name, ByteStreams.toByteArray(stream), ZipEntry.STORED, zip);
+  }
+
+  static void addEntry(String name, byte[] bytes, int compressionMethod, ZipOutputStream zip)
+      throws IOException {
+    CRC32 crc = new CRC32();
+    crc.update(bytes);
+    ZipEntry entry = createEntry(name, bytes.length, crc.getValue(), compressionMethod);
+    zip.putNextEntry(entry);
+    zip.write(bytes);
+    zip.closeEntry();
+  }
+
+  static void writeToZipStream(
+      String name, ByteDataView content, int compressionMethod, ZipOutputStream zip)
+      throws IOException {
+    byte[] buffer = content.getBuffer();
+    int offset = content.getOffset();
+    int length = content.getLength();
+    CRC32 crc = new CRC32();
+    crc.update(buffer, offset, length);
+    ZipEntry entry = createEntry(name, length, crc.getValue(), compressionMethod);
+    zip.putNextEntry(entry);
+    zip.write(buffer, offset, length);
+    zip.closeEntry();
+  }
+
+  private static ZipEntry createEntry(String name, int length, long crc, int compressionMethod) {
+    ZipEntry entry = new ZipEntry(name);
+    entry.setMethod(compressionMethod);
+    entry.setSize(length);
+    entry.setCrc(crc);
+    entry.setTime(0);
+    return entry;
+  }
+
+  private ZipUtils() {}
+}