001package daikon.dcomp; 002 003import static java.nio.charset.StandardCharsets.UTF_8; 004 005import daikon.DynComp; 006import daikon.plumelib.bcelutil.BcelUtil; 007import daikon.plumelib.options.Options; 008import daikon.plumelib.reflection.Signatures; 009import java.io.File; 010import java.io.FileInputStream; 011import java.io.FileNotFoundException; 012import java.io.IOException; 013import java.io.InputStream; 014import java.io.PrintWriter; 015import java.io.UncheckedIOException; 016import java.net.URI; 017import java.nio.file.DirectoryStream; 018import java.nio.file.FileSystem; 019import java.nio.file.FileSystems; 020import java.nio.file.Files; 021import java.nio.file.Path; 022import java.time.LocalDateTime; 023import java.time.ZoneId; 024import java.time.format.DateTimeFormatter; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Enumeration; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031import java.util.jar.JarEntry; 032import java.util.jar.JarFile; 033import org.apache.bcel.Const; 034import org.apache.bcel.classfile.ClassParser; 035import org.apache.bcel.classfile.JavaClass; 036import org.apache.bcel.generic.ClassGen; 037import org.apache.bcel.generic.MethodGen; 038import org.apache.bcel.generic.Type; 039import org.checkerframework.checker.nullness.qual.NonNull; 040import org.checkerframework.checker.signature.qual.BinaryName; 041 042/** 043 * BuildJDK uses {@link DCInstrument} to add comparability instrumentation to Java class files, then 044 * stores the modified files into a directory identified by a (required) command line argument. 045 * 046 * <p>DCInstrument duplicates each method of a class file. The new methods are distinguished by the 047 * addition of a final parameter of type DCompMarker and are instrumented to track comparability. 048 * Based on its invocation arguments, DynComp will decide whether to call the instrumented or 049 * uninstrumented version of a method. 050 */ 051@SuppressWarnings({ 052 "mustcall:type.argument", 053 "mustcall:type.arguments.not.inferred" 054}) // assignments into owning collection 055public class BuildJDK { 056 057 /** Creates a new BuildJDK. */ 058 private BuildJDK() {} 059 060 /** 061 * The "java.home" system property. Note that there is also a JAVA_HOME variable that contains 062 * {@code System.getenv("JAVA_HOME")}. 063 */ 064 public static final String java_home = System.getProperty("java.home"); 065 066 /** Whether to print information about the classes being instrumented. */ 067 private static boolean verbose = false; 068 069 /** Number of class files processed; used for progress display. */ 070 private int _numFilesProcessed = 0; 071 072 /** Name of file in output jar containing the static-fields map. */ 073 private static String static_field_id_filename = "dcomp_jdk_static_field_id"; 074 075 /** 076 * Collects names of all methods that DCInstrument could not process. Should be empty. Format is 077 * <fully-qualified class name>.<method name> 078 */ 079 private static List<String> skipped_methods = new ArrayList<>(); 080 081 /** 082 * A list of methods known to cause DCInstrument to fail. This is used to remove known problems 083 * from the list of failures displayed at the end of BuildJDK's execution. Format is 084 * <fully-qualified class name>.<method name> 085 */ 086 public static List<String> known_uninstrumentable_methods = 087 Arrays.asList( 088 // None at present 089 ); 090 091 /** 092 * Instruments each class file in the Java runtime and puts the result in the first non-option 093 * command-line argument. 094 * 095 * <p>By default, BuildJDK will locate the appropriate Java runtime library and instrument each of 096 * its member class files. However, if there are additional arguments on the command line after 097 * the destination directory, then we assume these are class files to be instrumented and the Java 098 * runtime library is not used. This usage is primarily for testing purposes. 099 * 100 * @param args arguments being passed to BuildJDK 101 * @throws IOException if unable to read or write file {@code dcomp_jdk_static_field_id} or if 102 * unable to write {@code jdk_classes.txt} 103 */ 104 @SuppressWarnings("builder:required.method.not.called") // assignment into collection of @Owning 105 public static void main(String[] args) throws IOException { 106 107 System.out.println("BuildJDK starting at " + LocalDateTime.now(ZoneId.systemDefault())); 108 109 BuildJDK build = new BuildJDK(); 110 111 Options options = 112 new Options( 113 "daikon.BuildJDK [options] dest_dir [classfiles...]", 114 DynComp.class, 115 DCInstrument.class); 116 String[] cl_args = options.parse(true, args); 117 if (cl_args.length < 1) { 118 System.err.println("must specify destination dir"); 119 options.printUsage(); 120 System.exit(1); 121 } 122 verbose = DynComp.verbose; 123 124 File dest_dir = new File(cl_args[0]); 125 126 /** 127 * Key is a class file name, value is a stream that opens that file name. 128 * 129 * <p>We want to share code to read and instrument the Java class file members of a jar file 130 * (JDK 8) or a module file (JDK 9+). However, jar files and module files are located in two 131 * completely different file systems. So we open an InputStream for each class file we wish to 132 * instrument and save it in the class_stream_map with the file name as the key. From that point 133 * the code to instrument a class file can be shared. 134 */ 135 Map<String, InputStream> class_stream_map; 136 137 if (cl_args.length > 1) { 138 139 // Arguments are <destdir> [<classfiles>...] 140 @SuppressWarnings("nullness:assignment") // https://tinyurl.com/cfissue/3224 141 @NonNull String[] class_files = Arrays.copyOfRange(cl_args, 1, cl_args.length); 142 143 // Instrumenting a specific list of class files is usually used for testing. 144 // But if we're using it to fix a broken classfile, then we need 145 // to restore the static-fields map from when our runtime jar was originally 146 // built. We assume it is in the destination directory. 147 DCInstrument.restore_static_field_id(new File(dest_dir, static_field_id_filename)); 148 System.out.printf( 149 "Restored %d entries in static map.%n", DCInstrument.static_field_id.size()); 150 151 class_stream_map = new HashMap<>(); 152 for (String classFileName : class_files) { 153 try { 154 class_stream_map.put(classFileName, new FileInputStream(classFileName)); 155 } catch (FileNotFoundException e) { 156 throw new Error("File not found: " + classFileName, e); 157 } 158 } 159 160 // Instrument the classes identified in class_stream_map. 161 build.instrument_classes(dest_dir, class_stream_map); 162 163 } else { 164 165 check_java_home(); 166 167 if (BcelUtil.javaVersion > 8) { 168 class_stream_map = build.gather_runtime_from_modules(); 169 } else { 170 class_stream_map = build.gather_runtime_from_jar(); 171 } 172 173 // Instrument the Java runtime classes identified in class_stream_map. 174 build.instrument_classes(dest_dir, class_stream_map); 175 176 // We've finished instrumenting all the class files. Now we create some 177 // abstract interface classes for use by the DynComp runtime. 178 build.addInterfaceClasses(dest_dir.getName()); 179 180 // Write out the file containing the static-fields map. 181 System.out.printf("Found %d static fields.%n", DCInstrument.static_field_id.size()); 182 DCInstrument.save_static_field_id(new File(dest_dir, static_field_id_filename)); 183 184 // Write out the list of all classes in the jar file 185 File jdk_classes_file = new File(dest_dir, "java/lang/jdk_classes.txt"); 186 System.out.printf("Writing a list of class names to %s%n", jdk_classes_file); 187 // Class names are written in internal form. 188 try (PrintWriter pw = new PrintWriter(jdk_classes_file, UTF_8.name())) { 189 for (String classFileName : class_stream_map.keySet()) { 190 pw.println(classFileName.replace(".class", "")); 191 } 192 } 193 } 194 195 // Print out any methods that could not be instrumented 196 print_skipped_methods(); 197 198 System.out.println("BuildJDK done at " + LocalDateTime.now(ZoneId.systemDefault())); 199 } 200 201 /** Verify that java.home and JAVA_HOME match. Exits the JVM if there is an error. */ 202 public static void check_java_home() { 203 204 // We are going to instrument the default Java runtime library. 205 // We need to verify where we should look for it. 206 207 String JAVA_HOME = System.getenv("JAVA_HOME"); 208 if (JAVA_HOME == null) { 209 if (verbose) { 210 System.out.println("JAVA_HOME not defined; using java.home: " + java_home); 211 } 212 JAVA_HOME = java_home; 213 } 214 215 File jrt = new File(JAVA_HOME); 216 if (!jrt.exists()) { 217 System.err.printf("Java home directory %s does not exist.%n", jrt); 218 System.exit(1); 219 } 220 221 try { 222 jrt = jrt.getCanonicalFile(); 223 } catch (Exception e) { 224 System.err.printf("Error geting canonical file for %s: %s", jrt, e.getMessage()); 225 System.exit(1); 226 } 227 228 JAVA_HOME = jrt.getAbsolutePath(); 229 if (!java_home.startsWith(JAVA_HOME)) { 230 System.err.printf( 231 "JAVA_HOME (%s) does not agree with java.home (%s).%n", JAVA_HOME, java_home); 232 System.err.printf("Please correct your Java environment.%n"); 233 System.exit(1); 234 } 235 } 236 237 /** 238 * For Java 8 the Java runtime is located in rt.jar. This method creates an InputStream for each 239 * class in rt.jar and returns this information in a map from class file name to InputStream. 240 * 241 * @return a map from class file name to the associated InputStream 242 */ 243 @SuppressWarnings({ 244 "JdkObsolete", // JarEntry.entries() returns Enumeration 245 "builder:required.method.not.called" // assignment into collection of @Owning 246 }) 247 Map<String, InputStream> gather_runtime_from_jar() { 248 249 Map<String, InputStream> class_stream_map = new HashMap<>(); 250 String jar_name = java_home + "/lib/rt.jar"; 251 System.out.printf("using jar file %s%n", jar_name); 252 try { 253 JarFile jfile = new JarFile(jar_name); 254 // Get each class to be instrumented and store it away 255 Enumeration<JarEntry> entries = jfile.entries(); 256 while (entries.hasMoreElements()) { 257 JarEntry entry = entries.nextElement(); 258 // System.out.printf("processing entry %s%n", entry); 259 final String entryName = entry.getName(); 260 if (entryName.endsWith("/") || entryName.endsWith("~")) { 261 continue; 262 } 263 264 // Get the InputStream for this file 265 InputStream is = jfile.getInputStream(entry); 266 class_stream_map.put(entryName, is); 267 } 268 } catch (Exception e) { 269 throw new Error("Problem while reading " + jar_name, e); 270 } 271 return class_stream_map; 272 } 273 274 /** 275 * For Java 9+ the Java runtime is located in a series of modules. At this time, we are only 276 * pre-instrumenting the java.base module. This method initializes the DirectoryStream used to 277 * explore java.base. It calls gather_runtime_from_modules_directory to process the directory 278 * structure. 279 * 280 * @return a map from class file name to the associated InputStream 281 */ 282 Map<String, InputStream> gather_runtime_from_modules() { 283 284 Map<String, InputStream> class_stream_map = new HashMap<>(); 285 FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/")); 286 Path modules = fs.getPath("/modules"); 287 // The path java_home+/lib/modules is the file in the host file system that 288 // corresponds to the modules file in the jrt: file system. 289 System.out.printf("using modules directory %s%n", java_home + "/lib/modules"); 290 try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(modules, "java.base*")) { 291 for (Path moduleDir : directoryStream) { 292 gather_runtime_from_modules_directory( 293 moduleDir, moduleDir.toString().length(), class_stream_map); 294 } 295 } catch (IOException e) { 296 throw new UncheckedIOException(e); 297 } 298 return class_stream_map; 299 } 300 301 /** 302 * This is a helper method for {@link #gather_runtime_from_modules}. It recurses down the module 303 * directory tree, selects the classes we want to instrument, creates an InputStream for each of 304 * these classes, and adds this information to the {@code class_stream_map} argument. 305 * 306 * @param path module file, which might be subdirectory 307 * @param modulePrefixLength length of "/module/..." path prefix before start of actual member 308 * path 309 * @param class_stream_map a map from class file name to InputStream that collects the results 310 */ 311 @SuppressWarnings("builder:required.method.not.called") // assignment into collection of @Owning 312 void gather_runtime_from_modules_directory( 313 Path path, int modulePrefixLength, Map<String, InputStream> class_stream_map) { 314 315 if (Files.isDirectory(path)) { 316 try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) { 317 for (Path subpath : directoryStream) { 318 gather_runtime_from_modules_directory(subpath, modulePrefixLength, class_stream_map); 319 } 320 } catch (IOException e) { 321 throw new UncheckedIOException(e); 322 } 323 } else { 324 String entryName = path.toString().substring(modulePrefixLength + 1); 325 try { 326 // Get the InputStream for this file 327 InputStream is = Files.newInputStream(path); 328 class_stream_map.put(entryName, is); 329 } catch (Exception e) { 330 throw new Error(e); 331 } 332 } 333 } 334 335 /** 336 * Instrument each of the classes indentified by the class_stream_map argument. 337 * 338 * @param dest_dir where to store the instrumented classes 339 * @param class_stream_map maps from class file name to an input stream on that file 340 */ 341 void instrument_classes(File dest_dir, Map<String, InputStream> class_stream_map) { 342 343 try { 344 // Create the destination directory 345 dest_dir.mkdirs(); 346 347 // Process each file. 348 for (String classFileName : class_stream_map.keySet()) { 349 if (verbose) { 350 System.out.println("instrument_classes: " + classFileName); 351 } 352 353 if (classFileName.equals("module-info.class")) { 354 System.out.printf("Skipping file %s%n", classFileName); 355 continue; 356 } 357 358 // Handle non-.class files and Object.class. In JDK 8, copy them unchanged. 359 // For JDK 9+ we do not copy as these items will be loaded from the original module file. 360 if (!classFileName.endsWith(".class") || classFileName.equals("java/lang/Object.class")) { 361 if (BcelUtil.javaVersion > 8) { 362 if (verbose) { 363 System.out.printf("Skipping file %s%n", classFileName); 364 } 365 continue; 366 } 367 // This File constructor ignores dest_dir if classFileName is absolute. 368 File classFile = new File(dest_dir, classFileName); 369 if (classFile.getParentFile() == null) { 370 throw new Error("This can't happen: " + classFile); 371 } 372 classFile.getParentFile().mkdirs(); 373 if (verbose) { 374 System.out.println("Copying Object.class or non-classfile: " + classFile); 375 } 376 try (InputStream in = class_stream_map.get(classFileName)) { 377 Files.copy(in, classFile.toPath()); 378 } 379 continue; 380 } 381 382 // Get the binary for this class 383 JavaClass jc; 384 try (InputStream is = class_stream_map.get(classFileName)) { 385 ClassParser parser = new ClassParser(is, classFileName); 386 jc = parser.parse(); 387 } catch (Throwable e) { 388 throw new Error("Failed to parse classfile " + classFileName, e); 389 } 390 391 // Instrument the class file. 392 try { 393 instrumentClassFile(jc, dest_dir, classFileName, class_stream_map.size()); 394 } catch (Throwable e) { 395 throw new Error("Couldn't instrument " + classFileName, e); 396 } 397 } 398 } catch (Exception e) { 399 throw new Error(e); 400 } 401 } 402 403 /** 404 * Add abstract interface classes needed by the DynComp runtime. 405 * 406 * @param destDir where to store the interface classes 407 */ 408 private void addInterfaceClasses(String destDir) { 409 // Create the DcompMarker class which is used to identify instrumented calls. 410 createDCompClass(destDir, "DCompMarker", false); 411 412 // The remainer of the generated classes are needed for JDK 9+ only. 413 if (BcelUtil.javaVersion > 8) { 414 createDCompClass(destDir, "DCompInstrumented", true); 415 createDCompClass(destDir, "DCompClone", false); 416 createDCompClass(destDir, "DCompToString", false); 417 } 418 } 419 420 /** 421 * Create an abstract interface class for use by the DynComp runtime. 422 * 423 * @param destDir where to store the new class 424 * @param className name of class 425 * @param dcompInstrumented if true, add equals_dcomp_instrumented method to class 426 */ 427 private void createDCompClass( 428 String destDir, @BinaryName String className, boolean dcompInstrumented) { 429 try { 430 ClassGen dcomp_class = 431 new ClassGen( 432 Signatures.addPackage("java.lang", className), 433 "java.lang.Object", 434 "daikon.dcomp.BuildJDK tool", 435 Const.ACC_INTERFACE | Const.ACC_PUBLIC | Const.ACC_ABSTRACT, 436 new @BinaryName String[0]); 437 dcomp_class.setMinor(0); 438 // Convert from JDK version number to ClassFile major_version. 439 // A bit of a hack, but seems OK. 440 dcomp_class.setMajor(BcelUtil.javaVersion + 44); 441 442 if (dcompInstrumented) { 443 @SuppressWarnings("nullness:argument") // null instruction list is ok for abstract 444 MethodGen mg = 445 new MethodGen( 446 Const.ACC_PUBLIC | Const.ACC_ABSTRACT, 447 Type.BOOLEAN, 448 new Type[] {Type.OBJECT}, 449 new String[] {"o"}, 450 "equals_dcomp_instrumented", 451 dcomp_class.getClassName(), 452 null, 453 dcomp_class.getConstantPool()); 454 dcomp_class.addMethod(mg.getMethod()); 455 } 456 457 dcomp_class 458 .getJavaClass() 459 .dump( 460 // Path.of exists in Java 11 and later. 461 new File(new File(new File(destDir, "java"), "lang"), className + ".class")); 462 } catch (Exception e) { 463 throw new Error(e); 464 } 465 } 466 467 /** Formats just the time part of a DateTime. */ 468 private DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); 469 470 /** 471 * Instruments the JavaClass {@code jc} (whose name is {@code classFileName}). Writes the 472 * resulting class to its corresponding location in the directory outputDir. 473 * 474 * @param jc JavaClass to be instrumented 475 * @param outputDir output directory for instrumented class 476 * @param classFileName name of class to be instrumented 477 * @param classTotal total number of classes to be processed; used for progress display 478 * @throws IOException if unable to write out instrumented class 479 */ 480 private void instrumentClassFile( 481 JavaClass jc, File outputDir, String classFileName, int classTotal) 482 throws java.io.IOException { 483 if (verbose) { 484 System.out.printf("processing target %s%n", classFileName); 485 } 486 DCInstrument dci = new DCInstrument(jc, true, null); 487 JavaClass inst_jc; 488 inst_jc = dci.instrument_jdk(); 489 skipped_methods.addAll(dci.get_skipped_methods()); 490 File classfile = new File(classFileName); 491 File dir; 492 if (classfile.getParent() == null) { 493 dir = outputDir; 494 } else { 495 dir = new File(outputDir, classfile.getParent()); 496 } 497 dir.mkdirs(); 498 File classpath = new File(dir, classfile.getName()); 499 if (verbose) { 500 System.out.printf("writing to file %s%n", classpath); 501 } 502 inst_jc.dump(classpath); 503 _numFilesProcessed++; 504 if (((_numFilesProcessed % 100) == 0) && (System.console() != null)) { 505 System.out.printf( 506 "Processed %d/%d classes at %s%n", 507 _numFilesProcessed, 508 classTotal, 509 LocalDateTime.now(ZoneId.systemDefault()).format(timeFormatter)); 510 } 511 } 512 513 /** 514 * Print information about methods that were not instrumented. This happens when a method fails 515 * BCEL's verifier (which is more strict than Java's). 516 */ 517 private static void print_skipped_methods() { 518 519 if (skipped_methods.isEmpty()) { 520 // System.out.printf("No methods were skipped.%n"); 521 return; 522 } 523 524 System.err.println( 525 "Warning: The following JDK methods could not be instrumented. DynComp will"); 526 System.err.println("still work as long as these methods are not called by your application."); 527 System.err.println("If your application calls one, it will throw a NoSuchMethodException."); 528 529 List<String> unknown = new ArrayList<>(skipped_methods); 530 unknown.removeAll(known_uninstrumentable_methods); 531 List<String> known = new ArrayList<>(skipped_methods); 532 known.retainAll(known_uninstrumentable_methods); 533 534 if (!unknown.isEmpty()) { 535 System.err.println("Please report the following problems to the Daikon maintainers."); 536 System.err.println( 537 "Please give sufficient details; see \"Reporting problems\" in the Daikon manual."); 538 for (String method : unknown) { 539 System.err.printf(" %s%n", method); 540 } 541 } 542 if (!known.isEmpty()) { 543 System.err.printf("The following are known problems; you do not need to report them."); 544 for (String method : known) { 545 System.err.printf(" %s%n", method); 546 } 547 } 548 } 549}