-
Bug
-
Resolution: Unresolved
-
P4
-
9, 11, 17, 21, 24
This issue was reported by Aaron Sheldon (asheldon@amazon.com).
If a `.jar` file which (for whatever reason) contains `.java` source files is on the classpath during a `javac` invocation without `-sourcepath` and `-d` arguments, then `javac` will silently update the corresponding `.class` files in the .jar file if the `.java` source files are newer than the corresponding class files (the latter can be changed with `-Xprefer:[newer,source]`).
Reproducer:
```
$ mkdir test && cd test;
$ echo 'public class A { static final String S = "A"; } ' > A.java
$ javac A.java
$ echo 'public class A { static final String S = "X"; } ' > A.java
$ jar -cf A.jar A.class A.java
$ mkdir repro && cd repro
$ echo 'public class B { static final String S = A.S; } ' > B.java
```
If we compile `B.java` with JDK 8:
```
$ ls
B.java
$ javac -cp ../A.jar B.java
$ ls
A.class B.class B.java
$ javap -constants 'B' | grep final
static final java.lang.String S = "X";
$ javap -constants -cp ../A.jar 'A' | grep final
static final java.lang.String S = "A";
```
If we compile `B.java` with JDK 9+:
```
$ rm *.class
$ ls
B.java
$ javac -cp ../A.jar B.java
$ ls
B.class B.java
$ javap -constants 'B' | grep final
static final java.lang.String S = "X";
$ javap -constants -cp ../A.jar 'A' | grep final
static final java.lang.String S = "X";
```
Notice how the invocation of `javac -cp ../A.jar B.java` for JDK 9+ silently replaces `A.class` in `A.jar` with a new version compiled from `A.java` in that `.jar` file.
If `A.jar` is not writable this will result in an uncaught exception in `javac`:
```
$ mkdir test && cd test;
$ echo 'public class A { static final String S = "A"; } ' > A.java
$ javac A.java
$ echo 'public class A { static final String S = "X"; } ' > A.java
$ jar -cf A.jar A.class A.java
$ mkdir repro && cd repro
$ echo 'public class B { static final String S = A.S; } ' > B.java
$ chmod -w ../A.jar
$ javac -cp ../A.jar B.java
An exception has occurred in the compiler (11.0.20). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.
java.nio.file.ReadOnlyFileSystemException
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.checkWritable(ZipFileSystem.java:175)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.newOutputStream(ZipFileSystem.java:543)
at jdk.zipfs/jdk.nio.zipfs.ZipPath.newOutputStream(ZipPath.java:859)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.newOutputStream(ZipFileSystemProvider.java:282)
at java.base/java.nio.file.Files.newOutputStream(Files.java:220)
at jdk.compiler/com.sun.tools.javac.file.PathFileObject.openOutputStream(PathFileObject.java:469)
at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter.writeClass(ClassWriter.java:1739)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.genCode(JavaCompiler.java:757)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.generate(JavaCompiler.java:1631)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.generate(JavaCompiler.java:1599)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:973)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:311)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
```
This exception has been barely fixed in JDK 21 and replaced by an even more cryptic compiler error (see [JDK-8200610: Compiling fails with java.nio.file.ReadOnlyFileSystemException](https://bugs.openjdk.org/browse/JDK-8200610)):
```
$ javac -cp ../A.jar B.java
../A.jar(/A.java):1: error: error while writing A: null
public class A { static final String S = "X"; }
^
1 error
```
The similar case, where `A.jar` is writable, but in a read-only directory or file system, hasn't been fixed byJDK-8200610 and still results in the following `java` exception:
```
$ javac -cp ../A.jar B.java
An exception has occurred in the compiler (21). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.
java.nio.file.AccessDeniedException: /tmp/zzz/test/repro/../zipfstmp13135106448247813016.tmp
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
at java.base/java.nio.file.Files.newByteChannel(Files.java:379)
at java.base/java.nio.file.Files.createFile(Files.java:657)
at java.base/java.nio.file.TempFileHelper.create(TempFileHelper.java:136)
at java.base/java.nio.file.TempFileHelper.createTempFile(TempFileHelper.java:159)
at java.base/java.nio.file.Files.createTempFile(Files.java:878)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.createTempFileInSameDirectoryAs(ZipFileSystem.java:1643)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.sync(ZipFileSystem.java:1779)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.lambda$close$9(ZipFileSystem.java:484)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:571)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.close(ZipFileSystem.java:483)
at jdk.compiler/com.sun.tools.javac.file.JavacFileManager$ArchiveContainer.close(JavacFileManager.java:657)
at jdk.compiler/com.sun.tools.javac.file.JavacFileManager.close(JavacFileManager.java:735)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:182)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:64)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:50)
```
This whole mess can be easily avoided by compiling with `-implicit:none` command line option, but this is not the default and seldomly used:
> *Controls the generation of class files for implicitly loaded source files. To automatically generate class files, use -implicit:class. To suppress class file generation, use -implicit:none. If this option is not specified, then the default is to automatically generate class files.*
`javac` has always used both, the `-classpath` and the `-sourcepath` for locating type information (see [Searching for Types](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html#BHCJJJAJ) for JDK 8 and [Searching for Module, Package and Type Declarations](https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#searching-for-module-package-and-type-declarations) for JDK 9+). The documentation clearly states that:
- *If the `-sourcepath` option is not specified, then the user class path is also searched for source files.* (for JDK 8) and
- *If not compiling code for modules, if the `--source-path` or `-sourcepath` option is not specified, then the user class path is also searched for source files.* (for JDK 9+)
The `javac` documentation (for both JDK and JDK 9+) also clearly warns about class recompilation if the source file for a class is found in addition to it's `.class` file:
> *Note: Classes found through the class path might be recompiled when their source files are also found.*
The `javac` documentation has also always specified that: "*If the -d option is not specified, then javac puts each class file in the same directory as the source file from which it was generated.*"
As the above reproducer demonstrates, JDK 8 and before have always violated this specification for source files located inside `.jar` files. Since JDK 9 however, the behavior of `javac` has changed. While it now conforms more closely to the documentation, I think it is unexpected and probably surprising for most user that `javac` can silently change `.jar` files on the class path.
A potential workaround might be to allways fall back to `-implicit:none` for sources loaded from `.jar` files on the classpath and perhaps issue a warning about it.
If a `.jar` file which (for whatever reason) contains `.java` source files is on the classpath during a `javac` invocation without `-sourcepath` and `-d` arguments, then `javac` will silently update the corresponding `.class` files in the .jar file if the `.java` source files are newer than the corresponding class files (the latter can be changed with `-Xprefer:[newer,source]`).
Reproducer:
```
$ mkdir test && cd test;
$ echo 'public class A { static final String S = "A"; } ' > A.java
$ javac A.java
$ echo 'public class A { static final String S = "X"; } ' > A.java
$ jar -cf A.jar A.class A.java
$ mkdir repro && cd repro
$ echo 'public class B { static final String S = A.S; } ' > B.java
```
If we compile `B.java` with JDK 8:
```
$ ls
B.java
$ javac -cp ../A.jar B.java
$ ls
A.class B.class B.java
$ javap -constants 'B' | grep final
static final java.lang.String S = "X";
$ javap -constants -cp ../A.jar 'A' | grep final
static final java.lang.String S = "A";
```
If we compile `B.java` with JDK 9+:
```
$ rm *.class
$ ls
B.java
$ javac -cp ../A.jar B.java
$ ls
B.class B.java
$ javap -constants 'B' | grep final
static final java.lang.String S = "X";
$ javap -constants -cp ../A.jar 'A' | grep final
static final java.lang.String S = "X";
```
Notice how the invocation of `javac -cp ../A.jar B.java` for JDK 9+ silently replaces `A.class` in `A.jar` with a new version compiled from `A.java` in that `.jar` file.
If `A.jar` is not writable this will result in an uncaught exception in `javac`:
```
$ mkdir test && cd test;
$ echo 'public class A { static final String S = "A"; } ' > A.java
$ javac A.java
$ echo 'public class A { static final String S = "X"; } ' > A.java
$ jar -cf A.jar A.class A.java
$ mkdir repro && cd repro
$ echo 'public class B { static final String S = A.S; } ' > B.java
$ chmod -w ../A.jar
$ javac -cp ../A.jar B.java
An exception has occurred in the compiler (11.0.20). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program and the following diagnostic in your report. Thank you.
java.nio.file.ReadOnlyFileSystemException
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.checkWritable(ZipFileSystem.java:175)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.newOutputStream(ZipFileSystem.java:543)
at jdk.zipfs/jdk.nio.zipfs.ZipPath.newOutputStream(ZipPath.java:859)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.newOutputStream(ZipFileSystemProvider.java:282)
at java.base/java.nio.file.Files.newOutputStream(Files.java:220)
at jdk.compiler/com.sun.tools.javac.file.PathFileObject.openOutputStream(PathFileObject.java:469)
at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter.writeClass(ClassWriter.java:1739)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.genCode(JavaCompiler.java:757)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.generate(JavaCompiler.java:1631)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.generate(JavaCompiler.java:1599)
at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:973)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:311)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
```
This exception has been barely fixed in JDK 21 and replaced by an even more cryptic compiler error (see [
```
$ javac -cp ../A.jar B.java
../A.jar(/A.java):1: error: error while writing A: null
public class A { static final String S = "X"; }
^
1 error
```
The similar case, where `A.jar` is writable, but in a read-only directory or file system, hasn't been fixed by
```
$ javac -cp ../A.jar B.java
An exception has occurred in the compiler (21). Please file a bug against the Java compiler via the Java bug reporting page (https://bugreport.java.com) after checking the Bug Database (https://bugs.java.com) for duplicates. Include your program, the following diagnostic, and the parameters passed to the Java compiler in your report. Thank you.
java.nio.file.AccessDeniedException: /tmp/zzz/test/repro/../zipfstmp13135106448247813016.tmp
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:90)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
at java.base/java.nio.file.Files.newByteChannel(Files.java:379)
at java.base/java.nio.file.Files.createFile(Files.java:657)
at java.base/java.nio.file.TempFileHelper.create(TempFileHelper.java:136)
at java.base/java.nio.file.TempFileHelper.createTempFile(TempFileHelper.java:159)
at java.base/java.nio.file.Files.createTempFile(Files.java:878)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.createTempFileInSameDirectoryAs(ZipFileSystem.java:1643)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.sync(ZipFileSystem.java:1779)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.lambda$close$9(ZipFileSystem.java:484)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:571)
at jdk.zipfs/jdk.nio.zipfs.ZipFileSystem.close(ZipFileSystem.java:483)
at jdk.compiler/com.sun.tools.javac.file.JavacFileManager$ArchiveContainer.close(JavacFileManager.java:657)
at jdk.compiler/com.sun.tools.javac.file.JavacFileManager.close(JavacFileManager.java:735)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:182)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:64)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:50)
```
This whole mess can be easily avoided by compiling with `-implicit:none` command line option, but this is not the default and seldomly used:
> *Controls the generation of class files for implicitly loaded source files. To automatically generate class files, use -implicit:class. To suppress class file generation, use -implicit:none. If this option is not specified, then the default is to automatically generate class files.*
`javac` has always used both, the `-classpath` and the `-sourcepath` for locating type information (see [Searching for Types](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html#BHCJJJAJ) for JDK 8 and [Searching for Module, Package and Type Declarations](https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#searching-for-module-package-and-type-declarations) for JDK 9+). The documentation clearly states that:
- *If the `-sourcepath` option is not specified, then the user class path is also searched for source files.* (for JDK 8) and
- *If not compiling code for modules, if the `--source-path` or `-sourcepath` option is not specified, then the user class path is also searched for source files.* (for JDK 9+)
The `javac` documentation (for both JDK and JDK 9+) also clearly warns about class recompilation if the source file for a class is found in addition to it's `.class` file:
> *Note: Classes found through the class path might be recompiled when their source files are also found.*
The `javac` documentation has also always specified that: "*If the -d option is not specified, then javac puts each class file in the same directory as the source file from which it was generated.*"
As the above reproducer demonstrates, JDK 8 and before have always violated this specification for source files located inside `.jar` files. Since JDK 9 however, the behavior of `javac` has changed. While it now conforms more closely to the documentation, I think it is unexpected and probably surprising for most user that `javac` can silently change `.jar` files on the class path.
A potential workaround might be to allways fall back to `-implicit:none` for sources loaded from `.jar` files on the classpath and perhaps issue a warning about it.
- relates to
-
JDK-8338852 javac: tighten the specification and implementation of 'newer' for '-Xprefer:[source, newer]'
- Open
-
JDK-8200610 Compiling fails with java.nio.file.ReadOnlyFileSystemException
- Resolved
-
JDK-8338685 javac: issue a warning if sources in a .jar file on the classpath override classes in that .jar file
- Open