Revise DumpPlatformClassPath

see https://github.com/bazelbuild/bazel/issues/6179

PiperOrigin-RevId: 213946706
diff --git a/tools/jdk/DumpPlatformClassPath.java b/tools/jdk/DumpPlatformClassPath.java
index 2a0a474..4640e48 100644
--- a/tools/jdk/DumpPlatformClassPath.java
+++ b/tools/jdk/DumpPlatformClassPath.java
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import static java.util.Comparator.comparing;
-
 import com.sun.tools.javac.api.JavacTool;
 import com.sun.tools.javac.util.Context;
 import java.io.BufferedOutputStream;
@@ -23,8 +21,6 @@
 import java.io.OutputStream;
 import java.io.UncheckedIOException;
 import java.lang.reflect.Method;
-import java.net.URI;
-import java.nio.file.FileSystems;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -33,11 +29,13 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.EnumSet;
 import java.util.GregorianCalendar;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
 import java.util.jar.JarOutputStream;
@@ -53,157 +51,153 @@
 /**
  * Output a jar file containing all classes on the platform classpath of the given JDK release.
  *
- * <p>usage: DumpPlatformClassPath <release version> <output jar>
+ * <p>usage: DumpPlatformClassPath <release version> <output jar> <path to target JDK>?
  */
 public class DumpPlatformClassPath {
 
   public static void main(String[] args) throws Exception {
-    if (args.length != 2) {
-      System.err.println("usage: DumpPlatformClassPath <release version> <output jar>");
+    if (args.length < 2 || args.length > 3) {
+      System.err.println(
+          "usage: DumpPlatformClassPath <release version> <output jar> <path to target JDK>?");
       System.exit(1);
     }
     int release = Integer.parseInt(args[0]);
     Path output = Paths.get(args[1]);
-
-    Map<String, byte[]> entries = new HashMap<>();
-
-    // Legacy JDK 8 bootclasspath handling.
-    // TODO(cushon): make sure this has test coverage.
-    Path javaHome = Paths.get(System.getProperty("java.home"));
-    if (javaHome.endsWith("jre")) {
-      javaHome = javaHome.getParent();
+    Path targetJavabase = null;
+    if (args.length == 3) {
+      targetJavabase = Paths.get(args[2]);
     }
 
-    List<Path> jars = new ArrayList<>();
+    int hostMajorVersion = hostMajorVersion();
+    boolean ok;
+    if (hostMajorVersion == 8) {
+      ok = dumpJDK8BootClassPath(release, output, targetJavabase);
+    } else {
+      ok = dumpJDK9AndNewerBootClassPath(hostMajorVersion, release, output, targetJavabase);
+    }
+    System.exit(ok ? 0 : 1);
+  }
 
-    Path extDir = javaHome.resolve("jre/lib/ext");
-    if (Files.exists(extDir)) {
-      for (Path extJar : Files.newDirectoryStream(extDir, "*.jar")) {
-        jars.add(extJar);
+  // JDK 8 bootclasspath handling.
+  // * JDK 8 represents a bootclasspath as a search path of jars (rt.jar, etc.).
+  // * It does not support --release or --system.
+  static boolean dumpJDK8BootClassPath(int release, Path output, Path targetJavabase)
+      throws IOException {
+    if (release != 8) {
+      System.err.printf("error: --release=%s is not supported on --host_javabase=8\n", release);
+      return false;
+    }
+    List<Path> bootClassPathJars;
+    if (targetJavabase != null) {
+      bootClassPathJars = getBootClassPathJars(targetJavabase);
+    } else {
+      Path hostJavabase = Paths.get(System.getProperty("java.home"));
+      if (hostJavabase.endsWith("jre")) {
+        hostJavabase = hostJavabase.getParent();
       }
+      bootClassPathJars = getBootClassPathJars(hostJavabase);
     }
+    writeClassPathJars(output, bootClassPathJars);
+    return true;
+  }
 
-    for (String jar : Arrays.asList(
-        "rt.jar",
-        "resources.jar",
-        "jsse.jar",
-        "jce.jar",
-        "charsets.jar")) {
-      Path path = javaHome.resolve("jre/lib").resolve(jar);
-      if (Files.exists(path)) {
-        jars.add(path);
+  // JDK > 8 --host_javabase bootclasspath handling.
+  // (The default --host_javabase is currently JDK 9.)
+  static boolean dumpJDK9AndNewerBootClassPath(
+      int hostMajorVersion, int release, Path output, Path targetJavabase) throws IOException {
+
+    // JDK 9 and newer support cross-compiling to older platform versions using the --system
+    // and --release flags.
+    // * --system takes the path to a JDK root for JDK 9 and up, and causes the compilation
+    //     to target the APIs from that JDK.
+    // * --release takes a language level (e.g. '9') and uses the API information baked in to
+    //     the host JDK (in lib/ct.sym).
+
+    // Since --system only supports JDK >= 9, first check of the target JDK defines a JDK 8
+    // bootclasspath.
+    if (targetJavabase != null) {
+      List<Path> bootClassPathJars = getBootClassPathJars(targetJavabase);
+      if (!bootClassPathJars.isEmpty()) {
+        writeClassPathJars(output, bootClassPathJars);
+        return true;
       }
-    }
-
-    for (Path path : jars) {
-      try (JarFile jf = new JarFile(path.toFile())) {
-        jf.stream()
-            .forEachOrdered(
-                entry -> {
-                  try {
-                    entries.put(entry.getName(), toByteArray(jf.getInputStream(entry)));
-                  } catch (IOException e) {
-                    throw new UncheckedIOException(e);
-                  }
-                });
-      }
-    }
-
-    if (!entries.isEmpty()) {
-      // If we found a JDK 8 bootclasspath (rt.jar, etc.) then we're done.
-      //
-      // However JDK 8 only contains bootclasspath API information for the current release,
-      // so we're always going to get a JDK 8 API level regardless of what the user requested.
-      // Emit a warning if they wanted to target a different version.
-      if (release != 8) {
+      if (release == 8) {
         System.err.printf(
-            "warning: ignoring release %s on --host_javabase=%s\n",
-            release, System.getProperty("java.version"));
+            "warning: could not find a JDK 8 bootclasspath in %s, falling back to --release\n",
+            targetJavabase);
+      }
+    }
+
+    // Initialize a FileManager to process the --release or --system arguments, and then read the
+    // initialized bootclasspath data back out.
+
+    List<String> javacOptions =
+        targetJavabase != null
+            ? Arrays.asList("--system", String.valueOf(targetJavabase))
+            : Arrays.asList("--release", String.valueOf(release));
+
+    Context context = new Context();
+    JavacTool.create()
+        .getTask(
+            /* out = */ null,
+            /* fileManager = */ null,
+            /* diagnosticListener = */ null,
+            /* options = */ javacOptions,
+            /* classes = */ null,
+            /* compilationUnits = */ null,
+            context);
+    StandardJavaFileManager fileManager =
+        (StandardJavaFileManager) context.get(JavaFileManager.class);
+
+    SortedMap<String, InputStream> entries = new TreeMap<>();
+    if (hostMajorVersion == 9 && targetJavabase == null && release == 8) {
+      // Work-around: when running on a JDK 9 host_javabase with --release 8, the ct.sym
+      // handling isn't compatible with the FileManager#list code path in the branch below.
+      for (Path path : getLocationAsPaths(fileManager)) {
+        Files.walkFileTree(
+            path,
+            new SimpleFileVisitor<Path>() {
+              @Override
+              public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                  throws IOException {
+                if (file.getFileName().toString().endsWith(".sig")) {
+                  String outputPath = path.relativize(file).toString();
+                  outputPath =
+                      outputPath.substring(0, outputPath.length() - ".sig".length()) + ".class";
+                  entries.put(outputPath, Files.newInputStream(file));
+                }
+                return FileVisitResult.CONTINUE;
+              }
+            });
       }
     } else {
-      // JDK > 8 --host_javabase bootclasspath handling.
-      // The default --host_javabase is currently JDK 10.
-
-      // Set up a compilation with --release to initialize a filemanager
-      Context context = new Context();
-      JavacTool.create()
-          .getTask(
-              /* out = */ null,
-              /* fileManager = */ null,
-              /* diagnosticListener = */ null,
-              /* options = */ Arrays.asList("--release", String.valueOf(release)),
-              /* classes = */ null,
-              /* compilationUnits = */ null,
-              context);
-      StandardJavaFileManager fileManager =
-          (StandardJavaFileManager) context.get(JavaFileManager.class);
-
-      int majorVersion = majorVersion();
-      if (majorVersion == 9 && release == 8) {
-        // Work-around: when running on a JDK 9 host_javabase with --release 8, the ct.sym
-        // handling isn't compatible with the FileManager#list code path in the branch below.
-        for (Path path : getLocationAsPaths(fileManager)) {
-          Files.walkFileTree(
-              path,
-              new SimpleFileVisitor<Path>() {
-                @Override
-                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
-                    throws IOException {
-                  if (file.getFileName().toString().endsWith(".sig")) {
-                    String outputPath = path.relativize(file).toString();
-                    outputPath =
-                        outputPath.substring(0, outputPath.length() - ".sig".length()) + ".class";
-                    entries.put(outputPath, Files.readAllBytes(file));
-                  }
-                  return FileVisitResult.CONTINUE;
-                }
-              });
-        }
-      } else {
-        for (JavaFileObject fileObject :
-            fileManager.list(
-                StandardLocation.PLATFORM_CLASS_PATH,
-                "",
-                EnumSet.of(Kind.CLASS),
-                /* recurse= */ true)) {
-          String binaryName =
-              fileManager.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fileObject);
-          entries.put(
-              binaryName.replace('.', '/') + ".class", toByteArray(fileObject.openInputStream()));
-        }
+      for (JavaFileObject fileObject :
+          fileManager.list(
+              StandardLocation.PLATFORM_CLASS_PATH,
+              "",
+              EnumSet.of(Kind.CLASS),
+              /* recurse= */ true)) {
+        String binaryName =
+            fileManager.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fileObject);
+        entries.put(binaryName.replace('.', '/') + ".class", fileObject.openInputStream());
       }
-
-      // Include the jdk.unsupported module for compatibility with JDK 8.
-      // (see: https://bugs.openjdk.java.net/browse/JDK-8206937)
-      // `--release 8` only provides access to supported APIs, which excludes e.g. sun.misc.Unsafe.
-      Path module =
-          FileSystems.getFileSystem(URI.create("jrt:/")).getPath("modules/jdk.unsupported");
-      Files.walkFileTree(
-          module,
-          new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
-                throws IOException {
-              String name = path.getFileName().toString();
-              if (name.endsWith(".class") && !name.equals("module-info.class")) {
-                entries.put(module.relativize(path).toString(), Files.readAllBytes(path));
-              }
-              return super.visitFile(path, attrs);
-            }
-          });
     }
+    writeEntries(output, entries);
+    return true;
+  }
 
+  /** Writes the given entry names and data to a jar archive at the given path. */
+  private static void writeEntries(Path output, Map<String, InputStream> entries)
+      throws IOException {
     if (!entries.containsKey("java/lang/Object.class")) {
       throw new AssertionError(
           "\nCould not find java.lang.Object on bootclasspath; something has gone terribly wrong.\n"
               + "Please file a bug: https://github.com/bazelbuild/bazel/issues");
     }
-
     try (OutputStream os = Files.newOutputStream(output);
         BufferedOutputStream bos = new BufferedOutputStream(os, 65536);
         JarOutputStream jos = new JarOutputStream(bos)) {
       entries.entrySet().stream()
-          .sorted(comparing(Map.Entry::getKey))
           .forEachOrdered(
               entry -> {
                 try {
@@ -215,14 +209,64 @@
     }
   }
 
+  /** Collects the entries of the given jar files into a map from jar entry names to their data. */
+  private static void writeClassPathJars(Path output, Collection<Path> paths) throws IOException {
+    List<JarFile> jars = new ArrayList<>();
+    for (Path path : paths) {
+      jars.add(new JarFile(path.toFile()));
+    }
+    SortedMap<String, InputStream> entries = new TreeMap<>();
+    for (JarFile jar : jars) {
+      jar.stream()
+          .filter(p -> p.getName().endsWith(".class"))
+          .forEachOrdered(
+              entry -> {
+                try {
+                  entries.put(entry.getName(), jar.getInputStream(entry));
+                } catch (IOException e) {
+                  throw new UncheckedIOException(e);
+                }
+              });
+    }
+    writeEntries(output, entries);
+    for (JarFile jar : jars) {
+      jar.close();
+    }
+  }
+
+  /** Returns paths to the entries of a JDK 8-style bootclasspath. */
+  private static List<Path> getBootClassPathJars(Path javaHome) throws IOException {
+    List<Path> jars = new ArrayList<>();
+    Path extDir = javaHome.resolve("jre/lib/ext");
+    if (Files.exists(extDir)) {
+      for (Path extJar : Files.newDirectoryStream(extDir, "*.jar")) {
+        jars.add(extJar);
+      }
+    }
+    for (String jar :
+        Arrays.asList("rt.jar", "resources.jar", "jsse.jar", "jce.jar", "charsets.jar")) {
+      Path path = javaHome.resolve("jre/lib").resolve(jar);
+      if (Files.exists(path)) {
+        jars.add(path);
+      }
+    }
+    return jars;
+  }
+
   // Use a fixed timestamp for deterministic jar output.
   private static final long FIXED_TIMESTAMP =
       new GregorianCalendar(2010, 0, 1, 0, 0, 0).getTimeInMillis();
 
-  private static void addEntry(JarOutputStream jos, String name, byte[] bytes) throws IOException {
+  /**
+   * Add a jar entry to the given {@link JarOutputStream}, normalizing the entry timestamps to
+   * ensure deterministic build output.
+   */
+  private static void addEntry(JarOutputStream jos, String name, InputStream input)
+      throws IOException {
     JarEntry je = new JarEntry(name);
     je.setTime(FIXED_TIMESTAMP);
     je.setMethod(ZipEntry.STORED);
+    byte[] bytes = toByteArray(input);
     je.setSize(bytes.length);
     CRC32 crc = new CRC32();
     crc.update(bytes);
@@ -244,6 +288,10 @@
     return boas.toByteArray();
   }
 
+  /**
+   * Reflectively calls {@code StandardJavaFileManager#getLocationAsPaths}, which is only available
+   * in JDK 9 and newer.
+   */
   @SuppressWarnings("unchecked")
   private static Iterable<Path> getLocationAsPaths(StandardJavaFileManager fileManager) {
     try {
@@ -256,7 +304,12 @@
     }
   }
 
-  static int majorVersion() {
+  /**
+   * Returns the major version of the host Java runtime (e.g. '8' for JDK 8), using {@link
+   * Runtime#version} if it is available, and otherwise falling back to the {@code
+   * java.class.version} system. property.
+   */
+  static int hostMajorVersion() {
     try {
       Method versionMethod = Runtime.class.getMethod("version");
       Object version = versionMethod.invoke(null);