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   * &lt;fully-qualified class name&gt;.&lt;method name&gt;
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   * &lt;fully-qualified class name&gt;.&lt;method name&gt;
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}