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

javac shouldn't silently change .jar files on the classpath

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: P4 P4
    • 25
    • 9, 11, 17, 21, 24
    • tools
    • 9
    • generic
    • generic

      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 by JDK-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.

            jlahoda Jan Lahoda
            simonis Volker Simonis
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: