Revise DumpPlatformClassPath see https://github.com/bazelbuild/bazel/issues/6179 PiperOrigin-RevId: 213946706
diff --git a/tools/jdk/BUILD b/tools/jdk/BUILD index 41b21b5..585596c 100644 --- a/tools/jdk/BUILD +++ b/tools/jdk/BUILD
@@ -7,6 +7,7 @@ "JDK8_JVM_OPTS", "JDK9_JVM_OPTS", "DEFAULT_JAVACOPTS", + "bootclasspath", ) # Used to distinguish toolchains used for Java development, ie the JavaToolchainProvider. @@ -199,28 +200,23 @@ # in RELEASES, using javac to read those APIs via the infrastructure added # for the --release flag (see http://openjdk.java.net/jeps/247). [ - genrule( + bootclasspath( name = "platformclasspath%d" % release, - srcs = ["DumpPlatformClassPath.java"], - outs = ["platformclasspath%d.jar" % release], - cmd = """ -set -eu -TMPDIR=$$(mktemp -d -t tmp.XXXXXXXX) -$(JAVABASE)/bin/javac -source 8 -target 8 \ - -Xlint:-options \ - -cp $(JAVABASE)/lib/tools.jar \ - -d $$TMPDIR $< -$(JAVA) -XX:+IgnoreUnrecognizedVMOptions \ - --add-exports=jdk.compiler/com.sun.tools.javac.platform=ALL-UNNAMED \ - -cp $$TMPDIR DumpPlatformClassPath %d $@ -rm -rf $$TMPDIR -""" % release, - toolchains = ["@bazel_tools//tools/jdk:current_host_java_runtime"], - tools = ["@bazel_tools//tools/jdk:current_host_java_runtime"], + src = "DumpPlatformClassPath.java", + host_javabase = "current_java_runtime", + release = "%s" % release, ) for release in RELEASES ] +bootclasspath( + name = "platformclasspath", + src = "DumpPlatformClassPath.java", + host_javabase = "current_java_runtime", + release = "8", # fall-back only + target_javabase = "current_java_runtime", +) + default_java_toolchain( name = "toolchain_hostjdk8", bootclasspath = [":platformclasspath8"], @@ -231,9 +227,12 @@ # Default to the Java 8 language level. # TODO(cushon): consider if/when we should increment this? -alias( +default_java_toolchain( name = "toolchain_hostjdk9", - actual = "toolchain_java8", + bootclasspath = [":platformclasspath"], + jvm_opts = JDK9_JVM_OPTS, + source_version = "8", + target_version = "8", ) # The 'vanilla' toolchain is an unsupported alternative to the default.
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);
diff --git a/tools/jdk/default_java_toolchain.bzl b/tools/jdk/default_java_toolchain.bzl index fce644d..4f03257 100644 --- a/tools/jdk/default_java_toolchain.bzl +++ b/tools/jdk/default_java_toolchain.bzl
@@ -106,3 +106,73 @@ cmd = "cp $(JAVABASE)/%s $@" % src, outs = [src], ) + +def _bootclasspath(ctx): + host_javabase = ctx.attr.host_javabase[java_common.JavaRuntimeInfo] + + class_dir = ctx.actions.declare_directory("%s_classes" % ctx.label.name) + + args = ctx.actions.args() + args.add("-source") + args.add("8") + args.add("-target") + args.add("8") + args.add("-Xlint:-options") + args.add("-cp") + args.add("%s/lib/tools.jar" % host_javabase.java_home) + args.add("-d") + args.add(class_dir) + args.add(ctx.file.src) + + ctx.actions.run( + executable = "%s/bin/javac" % host_javabase.java_home, + inputs = [ctx.file.src] + ctx.files.host_javabase, + outputs = [class_dir], + arguments = [args], + ) + + bootclasspath = ctx.outputs.jar + + inputs = [class_dir] + ctx.files.host_javabase + + args = ctx.actions.args() + args.add("-XX:+IgnoreUnrecognizedVMOptions") + args.add("--add-exports=jdk.compiler/com.sun.tools.javac.platform=ALL-UNNAMED") + args.add_joined( + "-cp", + [class_dir, "%s/lib/tools.jar" % host_javabase.java_home], + join_with = ctx.configuration.host_path_separator, + ) + args.add("DumpPlatformClassPath") + args.add(ctx.attr.release) + args.add(bootclasspath) + + if ctx.attr.target_javabase: + inputs.extend(ctx.files.target_javabase) + args.add(ctx.attr.target_javabase[java_common.JavaRuntimeInfo].java_home) + + ctx.actions.run( + executable = str(host_javabase.java_executable_exec_path), + inputs = inputs, + outputs = [bootclasspath], + arguments = [args], + ) + +bootclasspath = rule( + implementation = _bootclasspath, + attrs = { + "host_javabase": attr.label( + cfg = "host", + providers = [java_common.JavaRuntimeInfo], + ), + "release": attr.string(), + "src": attr.label( + cfg = "host", + allow_single_file = True, + ), + "target_javabase": attr.label( + providers = [java_common.JavaRuntimeInfo], + ), + }, + outputs = {"jar": "%{name}.jar"}, +)