Add class-file program-resource provider for the system modules of a JDK

This makes it possible to use the system modules from JDK 9+ as library
classpath. These JDKs does not have a rt.jar file, but a file system
provided by the "jtr:" file system provider.

Bug: 143335486
Change-Id: If1fac4623742e5f72774845249f546fa076da5c1
diff --git a/src/main/java/com/android/tools/r8/JdkClassFileProvider.java b/src/main/java/com/android/tools/r8/JdkClassFileProvider.java
new file mode 100644
index 0000000..5e35cf0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/JdkClassFileProvider.java
@@ -0,0 +1,172 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Lazy Java class file resource provider loading class files from a JDK.
+ *
+ * <p>The descriptor index is built eagerly upon creating the provider and subsequent requests for
+ * resources in the descriptor set will then force the read of JDK content.
+ *
+ * <p>Currently only JDK's of version 9 or higher with a lib/jrt-fs.jar file present is supported.
+ */
+@Keep
+public class JdkClassFileProvider implements ClassFileResourceProvider, Closeable {
+  private Origin origin;
+  private final Set<String> descriptors = new HashSet<>();
+  private final Map<String, String> descriptorToModule = new HashMap<>();
+  private URLClassLoader jrtFsJarLoader;
+  private FileSystem jrtFs;
+
+  /**
+   * Creates a lazy class-file program-resource provider for the system modules of the running Java
+   * VM.
+   *
+   * <p>Only supported if the current VM is of version 9 or higher.
+   */
+  public static ClassFileResourceProvider fromSystemJdk() throws IOException {
+    return new JdkClassFileProvider();
+  }
+
+  /**
+   * Creates a lazy class-file program-resource provider for the system modules of a JDK.
+   *
+   * @param home Location of the JDK to read the program-resources from.
+   *     <p>Only supported if the JDK to read is of version 9 or higher.
+   */
+  public static ClassFileResourceProvider fromSystemModulesJdk(Path home) throws IOException {
+    Path jrtFsJar = home.resolve("lib").resolve("jrt-fs.jar");
+    if (!Files.exists(jrtFsJar)) {
+      throw new NoSuchFileException(jrtFsJar.toString());
+    }
+    return new JdkClassFileProvider(home);
+  }
+
+  /**
+   * Creates a lazy class-file program-resource provider for the runtime of a JDK.
+   *
+   * <p>This will load the program-resources form the system modules for JDK of version 9 or higher.
+   *
+   * <p>This will load <code>rt.jar</code> for JDK of version 8 and lower.
+   *
+   * @param home Location of the JDK to read the program-resources from.
+   */
+  public static ClassFileResourceProvider fromJdkHome(Path home) throws IOException {
+    Path jrtFsJar = home.resolve("lib").resolve("jrt-fs.jar");
+    if (Files.exists(jrtFsJar)) {
+      return JdkClassFileProvider.fromSystemModulesJdk(home);
+    }
+    // JDK has rt.jar in jre/lib/rt.jar.
+    Path rtJar = home.resolve("jre").resolve("lib").resolve("rt.jar");
+    if (Files.exists(rtJar)) {
+      return fromJavaRuntimeJar(rtJar);
+    }
+    // JRE has rt.jar in lib/rt.jar.
+    rtJar = home.resolve("lib").resolve("rt.jar");
+    if (Files.exists(rtJar)) {
+      return fromJavaRuntimeJar(rtJar);
+    }
+    throw new IOException("Path " + home + " does not look like a Java home");
+  }
+
+  public static ClassFileResourceProvider fromJavaRuntimeJar(Path archive) throws IOException {
+    return new ArchiveClassFileProvider(archive);
+  }
+
+  private JdkClassFileProvider() throws IOException {
+    origin = Origin.unknown();
+    collectDescriptors(FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap()));
+  }
+
+  /**
+   * Creates a lazy class-file program-resource provider for the system modules of a JDK.
+   *
+   * @param home Location of the JDK to read the program-resources from.
+   *     <p>Only supported if the JDK to read is of version 9 or higher.
+   */
+  private JdkClassFileProvider(Path home) throws IOException {
+    origin = new PathOrigin(home);
+    Path jrtFsJar = home.resolve("lib").resolve("jrt-fs.jar");
+    assert Files.exists(jrtFsJar);
+    jrtFsJarLoader = new URLClassLoader(new URL[] {jrtFsJar.toUri().toURL()});
+    FileSystem jrtFs =
+        FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap(), jrtFsJarLoader);
+    collectDescriptors(jrtFs);
+  }
+
+  private void collectDescriptors(FileSystem jrtFs) throws IOException {
+    this.jrtFs = jrtFs;
+    Files.walk(jrtFs.getPath("/modules"))
+        .forEach(
+            path -> {
+              if (FileUtils.isClassFile(path)) {
+                DescriptorUtils.ModuleAndDescriptor moduleAndDescriptor =
+                    DescriptorUtils.guessJrtModuleAndTypeDescriptor(path.toString());
+                descriptorToModule.put(
+                    moduleAndDescriptor.getDescriptor(), moduleAndDescriptor.getModule());
+                descriptors.add(moduleAndDescriptor.getDescriptor());
+              }
+            });
+  }
+
+  @Override
+  public Set<String> getClassDescriptors() {
+    return Collections.unmodifiableSet(descriptors);
+  }
+
+  @Override
+  public ProgramResource getProgramResource(String descriptor) {
+    if (!descriptors.contains(descriptor)) {
+      return null;
+    }
+    try {
+      return ProgramResource.fromBytes(
+          Origin.unknown(),
+          ProgramResource.Kind.CF,
+          Files.readAllBytes(
+              jrtFs.getPath(
+                  "modules",
+                  descriptorToModule.get(descriptor),
+                  DescriptorUtils.getPathFromDescriptor(descriptor))),
+          Collections.singleton(descriptor));
+    } catch (IOException e) {
+      throw new CompilationError("Failed to read '" + descriptor, origin);
+    }
+  }
+
+  @Override
+  protected void finalize() throws Throwable {
+    close();
+    super.finalize();
+  }
+
+  @Override
+  public void close() throws IOException {
+    jrtFs.close();
+    if (jrtFsJarLoader != null) {
+      jrtFsJarLoader.close();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index af056a8..adbaad4 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils;
 
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.FileUtils.MODULES_PREFIX;
 
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
@@ -429,6 +430,46 @@
     return 'L' + descriptor + ';';
   }
 
+  public static class ModuleAndDescriptor {
+    private final String module;
+    private final String descriptor;
+
+    ModuleAndDescriptor(String module, String descriptor) {
+      this.module = module;
+      this.descriptor = descriptor;
+    }
+
+    public String getModule() {
+      return module;
+    }
+
+    public String getDescriptor() {
+      return descriptor;
+    }
+  }
+
+  /**
+   * Guess module and class descriptor from the location of a class file in a jrt file system.
+   *
+   * @param name the location in a jrt file system of the class file to convert to descriptor
+   * @return module and java class descriptor
+   */
+  public static ModuleAndDescriptor guessJrtModuleAndTypeDescriptor(String name) {
+    assert name != null;
+    assert name.endsWith(CLASS_EXTENSION)
+        : "Name " + name + " must have " + CLASS_EXTENSION + " suffix";
+    assert name.startsWith(MODULES_PREFIX)
+        : "Name " + name + " must have " + MODULES_PREFIX + " prefix";
+    assert name.charAt(MODULES_PREFIX.length()) == '/';
+    int moduleNameEnd = name.indexOf('/', MODULES_PREFIX.length() + 1);
+    String module = name.substring(MODULES_PREFIX.length() + 1, moduleNameEnd);
+    String descriptor = name.substring(moduleNameEnd + 1, name.length() - CLASS_EXTENSION.length());
+    if (descriptor.indexOf(JAVA_PACKAGE_SEPARATOR) != -1) {
+      throw new CompilationError("Unexpected class file name: " + name);
+    }
+    return new ModuleAndDescriptor(module, 'L' + descriptor + ';');
+  }
+
   public static String getPathFromDescriptor(String descriptor) {
     // We are quite loose on names here to support testing illegal names, too.
     assert descriptor.startsWith("L");
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 24c35fb..19fc10a 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -30,6 +30,7 @@
   public static final String JAVA_EXTENSION = ".java";
   public static final String KT_EXTENSION = ".kt";
   public static final String MODULE_INFO_CLASS = "module-info.class";
+  public static final String MODULES_PREFIX = "/modules";
 
   public static final boolean isAndroid =
       System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik");
diff --git a/src/test/java/com/android/tools/r8/CompileWithJdkClassFileProviderTest.java b/src/test/java/com/android/tools/r8/CompileWithJdkClassFileProviderTest.java
new file mode 100644
index 0000000..1754bff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/CompileWithJdkClassFileProviderTest.java
@@ -0,0 +1,655 @@
+package com.android.tools.r8;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.List;
+import org.hamcrest.core.StringContains;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+@RunWith(Parameterized.class)
+public class CompileWithJdkClassFileProviderTest extends TestBase implements Opcodes {
+
+  @Parameters(name = "{0}, library: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withAllRuntimes().build(), CfVm.values());
+  }
+
+  private final TestParameters parameters;
+  private final CfVm library;
+
+  public CompileWithJdkClassFileProviderTest(TestParameters parameters, CfVm library) {
+    this.parameters = parameters;
+    this.library = library;
+  }
+
+  @Test
+  public void compileSimpleCodeWithJdkLibrary() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJdkHome(TestRuntime.getCheckedInJDKHome(library));
+
+    testForR8(parameters.getBackend())
+        .addLibraryProvider(provider)
+        .addProgramClasses(TestRunner.class)
+        .addKeepMainRule(TestRunner.class)
+        .setMinApi(AndroidApiLevel.B)
+        .run(parameters.getRuntime(), TestRunner.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void compileSimpleCodeWithSystemJdk() throws Exception {
+    // Don't run duplicate tests (library is not used by the test).
+    assumeTrue(library == CfVm.JDK8);
+
+    ClassFileResourceProvider provider = JdkClassFileProvider.fromSystemJdk();
+
+    testForR8(parameters.getBackend())
+        .addLibraryProvider(provider)
+        .addProgramClasses(TestRunner.class)
+        .addKeepMainRule(TestRunner.class)
+        .setMinApi(AndroidApiLevel.B)
+        .run(parameters.getRuntime(), TestRunner.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void compileCodeWithJava9APIUsage() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJdkHome(TestRuntime.getCheckedInJDKHome(library));
+
+    TestShrinkerBuilder<?, ?, ?, ?, ?> testBuilder =
+        testForR8(parameters.getBackend())
+            .addLibraryProvider(provider)
+            .addProgramClassFileData(dumpClassWhichUseJava9Flow())
+            .addKeepMainRule("MySubscriber");
+
+    if (library == CfVm.JDK8) {
+      try {
+        // java.util.concurrent.Flow$Subscriber is not present in JDK8 rt.jar.
+        testBuilder.compileWithExpectedDiagnostics(
+            diagnotics -> {
+              diagnotics.assertErrorsCount(1);
+              diagnotics.assertWarningsCount(1);
+              diagnotics.assertInfosCount(0);
+              assertThat(
+                  diagnotics.getErrors().get(0).getDiagnosticMessage(),
+                  StringContains.containsString("java.util.concurrent.Flow$Subscriber"));
+              assertThat(
+                  diagnotics.getWarnings().get(0).getDiagnosticMessage(),
+                  StringContains.containsString("java.util.concurrent.Flow$Subscriber"));
+            });
+      } catch (CompilationFailedException e) {
+        return;
+      }
+      fail("Expected compilation error");
+    } else {
+      if (parameters.getRuntime().isDex()) {
+        // java.util.concurrent.Flow$Subscriber is not present on Android.
+        testBuilder
+            .run(parameters.getRuntime(), "MySubscriber")
+            .assertFailureWithErrorThatMatches(
+                anyOf(
+                    // Dalvik 4.0.4
+                    containsString("java.lang.NoClassDefFoundError: MySubscriber"),
+                    // Other Dalviks.
+                    containsString(
+                        "java.lang.ClassNotFoundException: Didn't find class \"MySubscriber\""),
+                    // Art.
+                    containsString(
+                        "java.lang.NoClassDefFoundError: "
+                            + "Failed resolution of: Ljava/util/concurrent/Flow$Subscriber;")));
+      } else {
+        if (parameters.getRuntime().asCf().getVm() == CfVm.JDK8) {
+          // java.util.concurrent.Flow$Subscriber not present in JDK8.
+          testBuilder
+              .run(parameters.getRuntime(), "MySubscriber")
+              .assertFailureWithErrorThatMatches(
+                  containsString("Could not find or load main class MySubscriber"));
+
+        } else {
+          // java.util.concurrent.Flow$Subscriber present in JDK9+.
+          testBuilder
+              .run(parameters.getRuntime(), "MySubscriber")
+              .disassemble()
+              .assertSuccessWithOutputLines("Got : 1", "Got : 2", "Got : 3", "Done");
+        }
+      }
+    }
+
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  /*
+   * The code below is from compiling the following source with javac from OpenJDK 9.0.4 with
+   * options:
+   *
+   *    -source 1.8 -target 1.8
+   *
+   * Note that -source 1.8 on on Java 9 will use the builtin boot class path (including
+   * java.util.concurrent.Flow) if no explicit boot classpath is specified.
+   *
+   * import java.util.List;
+   * import java.lang.Thread;
+   * import java.util.concurrent.Flow.Subscriber;
+   * import java.util.concurrent.Flow.Subscription;
+   * import java.util.concurrent.SubmissionPublisher;
+   * import java.util.concurrent.locks.Condition;
+   * import java.util.concurrent.locks.Lock;
+   * import java.util.concurrent.locks.ReentrantLock;
+   *
+   * public class MySubscriber<T> implements Subscriber<T> {
+   *   final static Lock lock = new ReentrantLock();
+   *   final static Condition done  = lock.newCondition();
+   *
+   *   private Subscription subscription;
+   *
+   *   @Override
+   *   public void onSubscribe(Subscription subscription) {
+   *     this.subscription = subscription;
+   *     subscription.request(1);
+   *   }
+   *
+   *   @Override
+   *   public void onNext(T item) {
+   *     System.out.println("Got : " + item);
+   *     subscription.request(1);
+   *   }
+   *   @Override
+   *   public void onError(Throwable t) {
+   *     t.printStackTrace();
+   *   }
+   *
+   *   @Override
+   *   public void onComplete() {
+   *     System.out.println("Done");
+   *     signalCondition(done);
+   *   }
+   *
+   *   public static void awaitCondition(Condition condition) throws Exception {
+   *     lock.lock();
+   *     try {
+   *       condition.await();
+   *     } finally {
+   *       lock.unlock();
+   *     }
+   *   }
+   *
+   *   public static void signalCondition(Condition condition) {
+   *     lock.lock();
+   *     try {
+   *       condition.signal();
+   *     } finally {
+   *       lock.unlock();
+   *     }
+   *   }
+   *
+   *   public static void main(String[] args) throws Exception {
+   *     SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
+   *     MySubscriber<String> subscriber = new MySubscriber<>();
+   *     publisher.subscribe(subscriber);
+   *     List<String> items = List.of("1", "2", "3");
+   *
+   *     items.forEach(publisher::submit);
+   *     publisher.close();
+   *
+   *     awaitCondition(done);
+   *   }
+   * }
+   *
+   */
+  public static byte[] dumpClassWhichUseJava9Flow() {
+
+    ClassWriter classWriter = new ClassWriter(0);
+    FieldVisitor fieldVisitor;
+    MethodVisitor methodVisitor;
+
+    classWriter.visit(
+        V1_8,
+        ACC_PUBLIC | ACC_SUPER,
+        "MySubscriber",
+        "<T:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/concurrent/Flow$Subscriber<TT;>;",
+        "java/lang/Object",
+        new String[] {"java/util/concurrent/Flow$Subscriber"});
+
+    classWriter.visitSource("MySubscriber.java", null);
+
+    classWriter.visitInnerClass(
+        "java/util/concurrent/Flow$Subscription",
+        "java/util/concurrent/Flow",
+        "Subscription",
+        ACC_PUBLIC | ACC_STATIC | ACC_ABSTRACT | ACC_INTERFACE);
+
+    classWriter.visitInnerClass(
+        "java/util/concurrent/Flow$Subscriber",
+        "java/util/concurrent/Flow",
+        "Subscriber",
+        ACC_PUBLIC | ACC_STATIC | ACC_ABSTRACT | ACC_INTERFACE);
+
+    classWriter.visitInnerClass(
+        "java/lang/invoke/MethodHandles$Lookup",
+        "java/lang/invoke/MethodHandles",
+        "Lookup",
+        ACC_PUBLIC | ACC_FINAL | ACC_STATIC);
+
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_FINAL | ACC_STATIC, "lock", "Ljava/util/concurrent/locks/Lock;", null, null);
+      fieldVisitor.visitEnd();
+    }
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_FINAL | ACC_STATIC, "done", "Ljava/util/concurrent/locks/Condition;", null, null);
+      fieldVisitor.visitEnd();
+    }
+    {
+      fieldVisitor =
+          classWriter.visitField(
+              ACC_PRIVATE, "subscription", "Ljava/util/concurrent/Flow$Subscription;", null, null);
+      fieldVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(10, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 1);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC, "onSubscribe", "(Ljava/util/concurrent/Flow$Subscription;)V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(18, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitFieldInsn(
+          PUTFIELD, "MySubscriber", "subscription", "Ljava/util/concurrent/Flow$Subscription;");
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(19, label1);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitInsn(LCONST_1);
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/Flow$Subscription", "request", "(J)V", true);
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(20, label2);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(3, 2);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(ACC_PUBLIC, "onNext", "(Ljava/lang/Object;)V", "(TT;)V", null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(24, label0);
+      methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
+      methodVisitor.visitLdcInsn("Got : ");
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
+          false);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/lang/StringBuilder",
+          "append",
+          "(Ljava/lang/Object;)Ljava/lang/StringBuilder;",
+          false);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(25, label1);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitFieldInsn(
+          GETFIELD, "MySubscriber", "subscription", "Ljava/util/concurrent/Flow$Subscription;");
+      methodVisitor.visitInsn(LCONST_1);
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/Flow$Subscription", "request", "(J)V", true);
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(26, label2);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(3, 2);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(ACC_PUBLIC, "onError", "(Ljava/lang/Throwable;)V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(29, label0);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/lang/Throwable", "printStackTrace", "()V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(30, label1);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 2);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "onComplete", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(34, label0);
+      methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      methodVisitor.visitLdcInsn("Done");
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(35, label1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "done", "Ljava/util/concurrent/locks/Condition;");
+      methodVisitor.visitMethodInsn(
+          INVOKESTATIC,
+          "MySubscriber",
+          "signalCondition",
+          "(Ljava/util/concurrent/locks/Condition;)V",
+          false);
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(36, label2);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(2, 1);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC | ACC_STATIC,
+              "awaitCondition",
+              "(Ljava/util/concurrent/locks/Condition;)V",
+              null,
+              new String[] {"java/lang/Exception"});
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      Label label1 = new Label();
+      Label label2 = new Label();
+      methodVisitor.visitTryCatchBlock(label0, label1, label2, null);
+      Label label3 = new Label();
+      methodVisitor.visitLabel(label3);
+      methodVisitor.visitLineNumber(39, label3);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "lock", "()V", true);
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(41, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Condition", "await", "()V", true);
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(43, label1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "unlock", "()V", true);
+      Label label4 = new Label();
+      methodVisitor.visitLabel(label4);
+      methodVisitor.visitLineNumber(44, label4);
+      Label label5 = new Label();
+      methodVisitor.visitJumpInsn(GOTO, label5);
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(43, label2);
+      methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"});
+      methodVisitor.visitVarInsn(ASTORE, 1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "unlock", "()V", true);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitInsn(ATHROW);
+      methodVisitor.visitLabel(label5);
+      methodVisitor.visitLineNumber(45, label5);
+      methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 2);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC | ACC_STATIC,
+              "signalCondition",
+              "(Ljava/util/concurrent/locks/Condition;)V",
+              null,
+              null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      Label label1 = new Label();
+      Label label2 = new Label();
+      methodVisitor.visitTryCatchBlock(label0, label1, label2, null);
+      Label label3 = new Label();
+      methodVisitor.visitLabel(label3);
+      methodVisitor.visitLineNumber(48, label3);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "lock", "()V", true);
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(50, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Condition", "signal", "()V", true);
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(52, label1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "unlock", "()V", true);
+      Label label4 = new Label();
+      methodVisitor.visitLabel(label4);
+      methodVisitor.visitLineNumber(53, label4);
+      Label label5 = new Label();
+      methodVisitor.visitJumpInsn(GOTO, label5);
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(52, label2);
+      methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"});
+      methodVisitor.visitVarInsn(ASTORE, 1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/concurrent/locks/Lock", "unlock", "()V", true);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitInsn(ATHROW);
+      methodVisitor.visitLabel(label5);
+      methodVisitor.visitLineNumber(54, label5);
+      methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 2);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor =
+          classWriter.visitMethod(
+              ACC_PUBLIC | ACC_STATIC,
+              "main",
+              "([Ljava/lang/String;)V",
+              null,
+              new String[] {"java/lang/Exception"});
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(57, label0);
+      methodVisitor.visitTypeInsn(NEW, "java/util/concurrent/SubmissionPublisher");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL, "java/util/concurrent/SubmissionPublisher", "<init>", "()V", false);
+      methodVisitor.visitVarInsn(ASTORE, 1);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(58, label1);
+      methodVisitor.visitTypeInsn(NEW, "MySubscriber");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitMethodInsn(INVOKESPECIAL, "MySubscriber", "<init>", "()V", false);
+      methodVisitor.visitVarInsn(ASTORE, 2);
+      Label label2 = new Label();
+      methodVisitor.visitLabel(label2);
+      methodVisitor.visitLineNumber(59, label2);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitVarInsn(ALOAD, 2);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL,
+          "java/util/concurrent/SubmissionPublisher",
+          "subscribe",
+          "(Ljava/util/concurrent/Flow$Subscriber;)V",
+          false);
+      Label label3 = new Label();
+      methodVisitor.visitLabel(label3);
+      methodVisitor.visitLineNumber(60, label3);
+      methodVisitor.visitLdcInsn("1");
+      methodVisitor.visitLdcInsn("2");
+      methodVisitor.visitLdcInsn("3");
+      methodVisitor.visitMethodInsn(
+          INVOKESTATIC,
+          "java/util/List",
+          "of",
+          "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;",
+          true);
+      methodVisitor.visitVarInsn(ASTORE, 3);
+      Label label4 = new Label();
+      methodVisitor.visitLabel(label4);
+      methodVisitor.visitLineNumber(62, label4);
+      methodVisitor.visitVarInsn(ALOAD, 3);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitMethodInsn(
+          INVOKESTATIC,
+          "java/util/Objects",
+          "requireNonNull",
+          "(Ljava/lang/Object;)Ljava/lang/Object;",
+          false);
+      methodVisitor.visitInsn(POP);
+      methodVisitor.visitInvokeDynamicInsn(
+          "accept",
+          "(Ljava/util/concurrent/SubmissionPublisher;)Ljava/util/function/Consumer;",
+          new Handle(
+              Opcodes.H_INVOKESTATIC,
+              "java/lang/invoke/LambdaMetafactory",
+              "metafactory",
+              "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
+              false),
+          new Object[] {
+            Type.getType("(Ljava/lang/Object;)V"),
+            new Handle(
+                Opcodes.H_INVOKEVIRTUAL,
+                "java/util/concurrent/SubmissionPublisher",
+                "submit",
+                "(Ljava/lang/Object;)I",
+                false),
+            Type.getType("(Ljava/lang/String;)V")
+          });
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE, "java/util/List", "forEach", "(Ljava/util/function/Consumer;)V", true);
+      Label label5 = new Label();
+      methodVisitor.visitLabel(label5);
+      methodVisitor.visitLineNumber(63, label5);
+      methodVisitor.visitVarInsn(ALOAD, 1);
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/util/concurrent/SubmissionPublisher", "close", "()V", false);
+      Label label6 = new Label();
+      methodVisitor.visitLabel(label6);
+      methodVisitor.visitLineNumber(65, label6);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "done", "Ljava/util/concurrent/locks/Condition;");
+      methodVisitor.visitMethodInsn(
+          INVOKESTATIC,
+          "MySubscriber",
+          "awaitCondition",
+          "(Ljava/util/concurrent/locks/Condition;)V",
+          false);
+      Label label7 = new Label();
+      methodVisitor.visitLabel(label7);
+      methodVisitor.visitLineNumber(66, label7);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(3, 4);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(11, label0);
+      methodVisitor.visitTypeInsn(NEW, "java/util/concurrent/locks/ReentrantLock");
+      methodVisitor.visitInsn(DUP);
+      methodVisitor.visitMethodInsn(
+          INVOKESPECIAL, "java/util/concurrent/locks/ReentrantLock", "<init>", "()V", false);
+      methodVisitor.visitFieldInsn(
+          PUTSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(12, label1);
+      methodVisitor.visitFieldInsn(
+          GETSTATIC, "MySubscriber", "lock", "Ljava/util/concurrent/locks/Lock;");
+      methodVisitor.visitMethodInsn(
+          INVOKEINTERFACE,
+          "java/util/concurrent/locks/Lock",
+          "newCondition",
+          "()Ljava/util/concurrent/locks/Condition;",
+          true);
+      methodVisitor.visitFieldInsn(
+          PUTSTATIC, "MySubscriber", "done", "Ljava/util/concurrent/locks/Condition;");
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(2, 0);
+      methodVisitor.visitEnd();
+    }
+    classWriter.visitEnd();
+
+    return classWriter.toByteArray();
+  }
+
+  public static class TestRunner {
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JdkClassFileProviderTest.java b/src/test/java/com/android/tools/r8/JdkClassFileProviderTest.java
new file mode 100644
index 0000000..d6f38af
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/JdkClassFileProviderTest.java
@@ -0,0 +1,120 @@
+package com.android.tools.r8;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.objectweb.asm.Opcodes;
+
+public class JdkClassFileProviderTest extends TestBase implements Opcodes {
+
+  @Test
+  public void testInvalid8RuntimeClassPath() throws Exception {
+    Path path = temp.newFolder().toPath();
+    try {
+      JdkClassFileProvider.fromJdkHome(path);
+      fail("Not supposed to succeed");
+    } catch (IOException e) {
+      assertThat(e.toString(), containsString(path.toString()));
+      assertThat(e.toString(), containsString("does not look like a Java home"));
+    }
+  }
+
+  @Test
+  public void testJdk8JavHome() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJdkHome(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK8));
+    assertJavaLangObject(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void testJdk8RuntimeClassPath() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJavaRuntimeJar(
+            ToolHelper.getJavaHome(TestRuntime.CfVm.JDK8)
+                .resolve("jre")
+                .resolve("lib")
+                .resolve("rt.jar"));
+    assertJavaLangObject(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void testJdk8SystemModules() throws Exception {
+    try {
+      JdkClassFileProvider.fromSystemModulesJdk(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK8));
+      fail("Not supposed to succeed");
+    } catch (NoSuchFileException e) {
+      assertThat(e.toString(), containsString("lib/jrt-fs.jar"));
+    }
+  }
+
+  @Test
+  public void testJdk9JavaHome() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJdkHome(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK9));
+    assertJavaLangObject(provider);
+    assertJavaUtilConcurrentFlowSubscriber(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void testJdk9SystemModules() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromSystemModulesJdk(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK9));
+    assertJavaLangObject(provider);
+    assertJavaUtilConcurrentFlowSubscriber(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void testJdk11JavaHome() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromJdkHome(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK11));
+    assertJavaLangObject(provider);
+    assertJavaUtilConcurrentFlowSubscriber(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  @Test
+  public void testJdk11SystemModules() throws Exception {
+    ClassFileResourceProvider provider =
+        JdkClassFileProvider.fromSystemModulesJdk(ToolHelper.getJavaHome(TestRuntime.CfVm.JDK11));
+    assertJavaLangObject(provider);
+    assertJavaUtilConcurrentFlowSubscriber(provider);
+    assert provider instanceof AutoCloseable;
+    ((AutoCloseable) provider).close();
+  }
+
+  private void assertJavaLangObject(ClassFileResourceProvider provider) throws Exception {
+    assertTrue(provider.getClassDescriptors().contains("Ljava/lang/Object;"));
+    assertTrue(
+        ByteStreams.toByteArray(provider.getProgramResource("Ljava/lang/Object;").getByteStream())
+                .length
+            > 0);
+  }
+
+  private void assertJavaUtilConcurrentFlowSubscriber(ClassFileResourceProvider provider)
+      throws Exception {
+    assertTrue(provider.getClassDescriptors().contains("Ljava/util/concurrent/Flow$Subscriber;"));
+    assertTrue(
+        ByteStreams.toByteArray(
+                    provider
+                        .getProgramResource("Ljava/util/concurrent/Flow$Subscriber;")
+                        .getByteStream())
+                .length
+            > 0);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index 4c17ac7..d834ebc 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -69,6 +69,10 @@
       return this.ordinal() <= other.ordinal();
     }
 
+    public boolean hasModularRuntime() {
+      return this != JDK8;
+    }
+
     @Override
     public String toString() {
       return name;
@@ -109,10 +113,12 @@
     return CHECKED_IN_JDKS.containsKey(jdk);
   }
 
-  public static Path getCheckInJDKPathFor(CfVm jdk) {
-    return Paths.get("third_party", "openjdk")
-        .resolve(CHECKED_IN_JDKS.get(jdk))
-        .resolve(Paths.get("bin", "java"));
+  public static Path getCheckedInJDKHome(CfVm jdk) {
+    return Paths.get("third_party", "openjdk").resolve(CHECKED_IN_JDKS.get(jdk));
+  }
+
+  public static Path getCheckedInJDKPathFor(CfVm jdk) {
+    return getCheckedInJDKHome(jdk).resolve(Paths.get("bin", "java"));
   }
 
   public static TestRuntime getDefaultJavaRuntime() {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 38fb7c7..0445166 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1273,8 +1273,7 @@
       List<String> extraOptions,
       Path... filesToCompile)
       throws IOException {
-    String jvm = runtime == null ? getSystemJavaExecutable() : getJavaExecutable(runtime);
-    List<String> cmdline = new ArrayList<String>(Arrays.asList(jvm));
+    List<String> cmdline = new ArrayList<>(Arrays.asList(getJavaExecutable(runtime)));
     cmdline.add("-jar");
     cmdline.add(KT_PRELOADER);
     cmdline.add("org.jetbrains.kotlin.preloading.Preloader");
@@ -1419,7 +1418,7 @@
 
   public static String getJavaExecutable(CfVm runtime) {
     if (TestRuntime.isCheckedInJDK(runtime)) {
-      return TestRuntime.getCheckInJDKPathFor(runtime).toString();
+      return TestRuntime.getCheckedInJDKPathFor(runtime).toString();
     } else {
       // TODO(b/127785410): Always assume a non-null runtime.
       assert runtime == null || TestParametersBuilder.isSystemJdk(runtime);
@@ -1427,6 +1426,10 @@
     }
   }
 
+  public static Path getJavaHome(CfVm runtime) {
+    return TestRuntime.getCheckedInJDKHome(runtime);
+  }
+
   public static ProcessResult runArtRaw(ArtCommandBuilder builder) throws IOException {
     return runArtProcessRaw(builder);
   }