1 // Copyright 2017 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import com.sun.tools.javac.api.JavacTool; 16 import com.sun.tools.javac.util.Context; 17 import java.io.BufferedOutputStream; 18 import java.io.ByteArrayOutputStream; 19 import java.io.IOException; 20 import java.io.InputStream; 21 import java.io.OutputStream; 22 import java.io.UncheckedIOException; 23 import java.lang.reflect.Method; 24 import java.nio.file.Files; 25 import java.nio.file.Path; 26 import java.nio.file.Paths; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collection; 30 import java.util.EnumSet; 31 import java.util.GregorianCalendar; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.SortedMap; 35 import java.util.TreeMap; 36 import java.util.jar.JarEntry; 37 import java.util.jar.JarFile; 38 import java.util.jar.JarOutputStream; 39 import java.util.zip.CRC32; 40 import java.util.zip.ZipEntry; 41 import javax.tools.JavaFileManager; 42 import javax.tools.JavaFileObject; 43 import javax.tools.JavaFileObject.Kind; 44 import javax.tools.StandardJavaFileManager; 45 import javax.tools.StandardLocation; 46 47 /** 48 * Output a jar file containing all classes on the platform classpath of the given JDK release. 49 * 50 * <p>usage: DumpPlatformClassPath <release version> <output jar> <path to target JDK>? 51 */ 52 public class DumpPlatformClassPath { 53 main(String[] args)54 public static void main(String[] args) throws Exception { 55 if (args.length != 2) { 56 System.err.println("usage: DumpPlatformClassPath <output jar> <path to target JDK>"); 57 System.exit(1); 58 } 59 Path output = Paths.get(args[0]); 60 Path targetJavabase = Paths.get(args[1]); 61 62 int hostMajorVersion = hostMajorVersion(); 63 boolean ok; 64 if (hostMajorVersion == 8) { 65 ok = dumpJDK8BootClassPath(output, targetJavabase); 66 } else { 67 ok = dumpJDK9AndNewerBootClassPath(hostMajorVersion, output, targetJavabase); 68 } 69 System.exit(ok ? 0 : 1); 70 } 71 72 // JDK 8 bootclasspath handling. 73 // * JDK 8 represents a bootclasspath as a search path of jars (rt.jar, etc.). 74 // * It does not support --release or --system. dumpJDK8BootClassPath(Path output, Path targetJavabase)75 static boolean dumpJDK8BootClassPath(Path output, Path targetJavabase) throws IOException { 76 List<Path> bootClassPathJars = getBootClassPathJars(targetJavabase); 77 writeClassPathJars(output, bootClassPathJars); 78 return true; 79 } 80 81 // JDK > 8 --host_javabase bootclasspath handling. 82 // (The default --host_javabase is currently JDK 9.) dumpJDK9AndNewerBootClassPath( int hostMajorVersion, Path output, Path targetJavabase)83 static boolean dumpJDK9AndNewerBootClassPath( 84 int hostMajorVersion, Path output, Path targetJavabase) throws IOException { 85 86 // JDK 9 and newer support cross-compiling to older platform versions using the --system 87 // and --release flags. 88 // * --system takes the path to a JDK root for JDK 9 and up, and causes the compilation 89 // to target the APIs from that JDK. 90 // * --release takes a language level (e.g. '9') and uses the API information baked in to 91 // the host JDK (in lib/ct.sym). 92 93 // Since --system only supports JDK >= 9, first check of the target JDK defines a JDK 8 94 // bootclasspath. 95 List<Path> bootClassPathJars = getBootClassPathJars(targetJavabase); 96 if (!bootClassPathJars.isEmpty()) { 97 writeClassPathJars(output, bootClassPathJars); 98 return true; 99 } 100 101 // Initialize a FileManager to process the --system argument, and then read the 102 // initialized bootclasspath data back out. 103 104 Context context = new Context(); 105 try { 106 JavacTool.create() 107 .getTask( 108 /* out = */ null, 109 /* fileManager = */ null, 110 /* diagnosticListener = */ null, 111 /* options = */ Arrays.asList("--system", String.valueOf(targetJavabase)), 112 /* classes = */ null, 113 /* compilationUnits = */ null, 114 context); 115 } catch (IllegalArgumentException e) { 116 throw new IllegalArgumentException( 117 String.format( 118 "Failed to collect system class path. Please ensure that the configured Java runtime" 119 + " ('%s') is a complete JDK. There are known issues with Homebrew versions of" 120 + " the Java runtime.", 121 targetJavabase.toRealPath()), 122 e); 123 } 124 StandardJavaFileManager fileManager = 125 (StandardJavaFileManager) context.get(JavaFileManager.class); 126 127 SortedMap<String, InputStream> entries = new TreeMap<>(); 128 for (JavaFileObject fileObject : 129 fileManager.list( 130 StandardLocation.PLATFORM_CLASS_PATH, 131 "", 132 EnumSet.of(Kind.CLASS), 133 /* recurse= */ true)) { 134 String binaryName = 135 fileManager.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fileObject); 136 entries.put(binaryName.replace('.', '/') + ".class", fileObject.openInputStream()); 137 } 138 writeEntries(output, entries); 139 return true; 140 } 141 142 /** Writes the given entry names and data to a jar archive at the given path. */ writeEntries(Path output, Map<String, InputStream> entries)143 private static void writeEntries(Path output, Map<String, InputStream> entries) 144 throws IOException { 145 if (!entries.containsKey("java/lang/Object.class")) { 146 throw new AssertionError( 147 "\nCould not find java.lang.Object on bootclasspath; something has gone terribly wrong.\n" 148 + "Please file a bug: https://github.com/bazelbuild/bazel/issues"); 149 } 150 try (OutputStream os = Files.newOutputStream(output); 151 BufferedOutputStream bos = new BufferedOutputStream(os, 65536); 152 JarOutputStream jos = new JarOutputStream(bos)) { 153 entries.entrySet().stream() 154 .forEachOrdered( 155 entry -> { 156 try { 157 addEntry(jos, entry.getKey(), entry.getValue()); 158 } catch (IOException e) { 159 throw new UncheckedIOException(e); 160 } 161 }); 162 } 163 } 164 165 /** Collects the entries of the given jar files into a map from jar entry names to their data. */ writeClassPathJars(Path output, Collection<Path> paths)166 private static void writeClassPathJars(Path output, Collection<Path> paths) throws IOException { 167 List<JarFile> jars = new ArrayList<>(); 168 for (Path path : paths) { 169 jars.add(new JarFile(path.toFile())); 170 } 171 SortedMap<String, InputStream> entries = new TreeMap<>(); 172 for (JarFile jar : jars) { 173 jar.stream() 174 .filter(p -> p.getName().endsWith(".class")) 175 .forEachOrdered( 176 entry -> { 177 try { 178 entries.put(entry.getName(), jar.getInputStream(entry)); 179 } catch (IOException e) { 180 throw new UncheckedIOException(e); 181 } 182 }); 183 } 184 writeEntries(output, entries); 185 for (JarFile jar : jars) { 186 jar.close(); 187 } 188 } 189 190 /** Returns paths to the entries of a JDK 8-style bootclasspath. */ getBootClassPathJars(Path javaHome)191 private static List<Path> getBootClassPathJars(Path javaHome) throws IOException { 192 List<Path> jars = new ArrayList<>(); 193 Path extDir = javaHome.resolve("jre/lib/ext"); 194 if (Files.exists(extDir)) { 195 for (Path extJar : Files.newDirectoryStream(extDir, "*.jar")) { 196 jars.add(extJar); 197 } 198 } 199 for (String jar : 200 Arrays.asList("rt.jar", "resources.jar", "jsse.jar", "jce.jar", "charsets.jar")) { 201 Path path = javaHome.resolve("jre/lib").resolve(jar); 202 if (Files.exists(path)) { 203 jars.add(path); 204 } 205 } 206 return jars; 207 } 208 209 // Use a fixed timestamp for deterministic jar output. 210 private static final long FIXED_TIMESTAMP = 211 new GregorianCalendar(2010, 0, 1, 0, 0, 0).getTimeInMillis(); 212 213 /** 214 * Add a jar entry to the given {@link JarOutputStream}, normalizing the entry timestamps to 215 * ensure deterministic build output. 216 */ addEntry(JarOutputStream jos, String name, InputStream input)217 private static void addEntry(JarOutputStream jos, String name, InputStream input) 218 throws IOException { 219 JarEntry je = new JarEntry(name); 220 je.setTime(FIXED_TIMESTAMP); 221 je.setMethod(ZipEntry.STORED); 222 byte[] bytes = toByteArray(input); 223 // When targeting JDK >= 10, patch the major version so it will be accepted by javac 9 224 // TODO(cushon): remove this after updating javac 225 if (bytes[7] > 53) { 226 bytes[7] = 53; 227 } 228 je.setSize(bytes.length); 229 CRC32 crc = new CRC32(); 230 crc.update(bytes); 231 je.setCrc(crc.getValue()); 232 jos.putNextEntry(je); 233 jos.write(bytes); 234 } 235 toByteArray(InputStream is)236 private static byte[] toByteArray(InputStream is) throws IOException { 237 byte[] buffer = new byte[8192]; 238 ByteArrayOutputStream boas = new ByteArrayOutputStream(); 239 while (true) { 240 int r = is.read(buffer); 241 if (r == -1) { 242 break; 243 } 244 boas.write(buffer, 0, r); 245 } 246 return boas.toByteArray(); 247 } 248 249 /** 250 * Returns the major version of the host Java runtime (e.g. '8' for JDK 8), using {@link 251 * Runtime#version} if it is available, and otherwise falling back to the {@code 252 * java.class.version} system. property. 253 */ hostMajorVersion()254 static int hostMajorVersion() { 255 try { 256 Method versionMethod = Runtime.class.getMethod("version"); 257 Object version = versionMethod.invoke(null); 258 return (int) version.getClass().getMethod("major").invoke(version); 259 } catch (ReflectiveOperationException e) { 260 // Runtime.version() isn't available on JDK 8; continue below 261 } 262 int version = (int) Double.parseDouble(System.getProperty("java.class.version")); 263 if (49 <= version && version <= 52) { 264 return version - (49 - 5); 265 } 266 throw new IllegalStateException( 267 "Unknown Java version: " + System.getProperty("java.specification.version")); 268 } 269 } 270