Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8269829

Add an -Xprefer policy to prefer class files when modified times are equal

XMLWordPrintable

    • Icon: Enhancement Enhancement
    • Resolution: Unresolved
    • Icon: P3 P3
    • tbd
    • None
    • tools

      With -Xprefer:newer, ties are broken arbitrarily if there is both a class and source file with the same modified timestamp.

      One of the motivating use-cases for this enhancement is deterministic/reproducible builds where timestamps on input files are set to a fixed value, as discussed previously on compiler-dev: http://mail.openjdk.java.net/pipermail/compiler-dev/2013-November/008072.html

      There was some discussion of updating -Xprefer: to make more policies available: http://mail.openjdk.java.net/pipermail/compiler-dev/2013-November/008089.html

      Something like 'always prefer .class' or 'prefer .class if the timestamps are the same' would address this use-case.

      The following demo shows that javac resolves either a .class or .java file depending on the order of jar entries, if both entries have the same timestamp and correspond to the same Java class:

      ===
      import static java.nio.charset.StandardCharsets.UTF_8;
      import static java.util.Locale.ENGLISH;

      import java.io.IOException;
      import java.net.URL;
      import java.net.URLClassLoader;
      import java.nio.file.Files;
      import java.nio.file.Path;
      import java.time.LocalDateTime;
      import java.util.List;
      import java.util.concurrent.Callable;
      import java.util.jar.JarEntry;
      import java.util.jar.JarOutputStream;
      import javax.tools.JavaCompiler;
      import javax.tools.JavaCompiler.CompilationTask;
      import javax.tools.StandardJavaFileManager;
      import javax.tools.StandardLocation;
      import javax.tools.ToolProvider;
      import org.objectweb.asm.ClassWriter;
      import org.objectweb.asm.FieldVisitor;
      import org.objectweb.asm.MethodVisitor;
      import org.objectweb.asm.Opcodes;

      public class Test {

        public static void main(String[] args) throws Exception {
          run(true, false);
          run(false, false);
          run(true, true);
          run(false, true);
        }

        private static void run(boolean sourceFirst, boolean emptySourcePath) throws Exception {
          Path tmp = Files.createTempDirectory("tmp");
          Path libJar = tmp.resolve("lib.jar");
          try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(libJar))) {
            byte[] sourceData = "class B { static final String X = \"B.java\"; }".getBytes(UTF_8);
            if (sourceFirst) {
              addEntry(jos, "B.java", sourceData);
              addEntry(jos, "B.class", classData());
            } else {
              addEntry(jos, "B.class", classData());
              addEntry(jos, "B.java", sourceData);
            }
          }

          Path source = tmp.resolve("A.java");
          Files.write(
              source,
              List.of(
                  "import java.util.concurrent.Callable;",
                  "public class A implements Callable<String> {",
                  " public String call() {",
                  " return B.X;",
                  " }",
                  "}"),
              UTF_8);

          Path classOut = tmp.resolve("output");
          Files.createDirectory(classOut);

          JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
          StandardJavaFileManager fileManager =
              javaCompiler.getStandardFileManager(/* diagnosticListener= */ null, ENGLISH, UTF_8);

          fileManager.setLocationFromPaths(StandardLocation.CLASS_PATH, List.of(libJar));
          if (emptySourcePath) {
            fileManager.setLocationFromPaths(StandardLocation.SOURCE_PATH, List.of());
          }
          fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, List.of(classOut));

          CompilationTask task =
              javaCompiler.getTask(
                  /* out= */ null,
                  /* fileManager= */ fileManager,
                  /* diagnosticListener= */ null,
                  /* options= */ null,
                  /* classes= */ null,
                  /* compilationUnits= */ fileManager.getJavaFileObjects(source));
          task.call();

          URLClassLoader classLoader = new URLClassLoader(new URL[]{classOut.toUri().toURL()});
          @SuppressWarnings("unchecked")
          Callable<String> callable =
              classLoader.loadClass("A").asSubclass(Callable.class).getConstructor().newInstance();
          System.err.printf("source first? %5s, empty source path? %5s, result: %s\n", sourceFirst, emptySourcePath, callable.call());
        }

        private static void addEntry(JarOutputStream jos, String name, byte[] content)
            throws IOException {
          JarEntry je = new JarEntry(name);
          je.setTimeLocal(LocalDateTime.of(2010, 1, 1, 0, 0, 0));
          jos.putNextEntry(je);
          jos.write(content);
        }

        public static byte[] classData() {

          ClassWriter classWriter = new ClassWriter(0);

          classWriter.visit(Opcodes.V11, Opcodes.ACC_SUPER, "B", null, "java/lang/Object", null);

          FieldVisitor fieldVisitor =
              classWriter.visitField(
                  Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC,
                  "X",
                  "Ljava/lang/String;",
                  null,
                  "B.class");
          fieldVisitor.visitEnd();

          MethodVisitor methodVisitor =
              classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
          methodVisitor.visitCode();
          methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
          methodVisitor.visitMethodInsn(
              Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
          methodVisitor.visitInsn(Opcodes.RETURN);
          methodVisitor.visitMaxs(1, 1);
          methodVisitor.visitEnd();

          classWriter.visitEnd();

          return classWriter.toByteArray();
        }
      }
      ===

      javac -cp asm-9.2.jar:asm-util-9.2.jar Test.java -d . && java -cp asm-9.2.jar:asm-util-9.2.jar:. Test
      ...
      source first? true, empty source path? false, result: B.class
      source first? false, empty source path? false, result: B.java
      source first? true, empty source path? true, result: B.class
      source first? false, empty source path? true, result: B.class

            cushon Liam Miller-Cushon
            cushon Liam Miller-Cushon
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated: