-
Enhancement
-
Resolution: Unresolved
-
P3
-
None
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
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