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);