src/jdk.compiler/share/classes/com/sun/tools/sjavac/JavacState.java

Print this page
rev 2819 : imported patch my-classpath-deps-00

@@ -23,24 +23,31 @@
  * questions.
  */
 
 package com.sun.tools.sjavac;
 
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintStream;
 import java.net.URI;
 import java.nio.file.NoSuchFileException;
 import java.text.SimpleDateFormat;
-import java.util.*;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import com.sun.tools.sjavac.options.Options;
+import com.sun.tools.sjavac.pubapi.PubApi;
 import com.sun.tools.sjavac.server.Sjavac;
 
 /**
  * The javac state class maintains the previous (prev) and the current (now)
  * build states and everything else that goes into the javac_state file.

@@ -266,28 +273,29 @@
 
     /**
      * Save the javac_state file.
      */
     public void save() throws IOException {
-        if (!needsSaving) return;
+        if (!needsSaving)
+            return;
         try (FileWriter out = new FileWriter(javacState)) {
             StringBuilder b = new StringBuilder();
             long millisNow = System.currentTimeMillis();
             Date d = new Date(millisNow);
-            SimpleDateFormat df =
-                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
+            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
             b.append("# javac_state ver 0.3 generated "+millisNow+" "+df.format(d)+"\n");
             b.append("# This format might change at any time. Please do not depend on it.\n");
+            b.append("# R arguments\n");
             b.append("# M module\n");
             b.append("# P package\n");
             b.append("# S C source_tobe_compiled timestamp\n");
             b.append("# S L link_only_source timestamp\n");
             b.append("# G C generated_source timestamp\n");
             b.append("# A artifact timestamp\n");
-            b.append("# D dependency\n");
+            b.append("# D S dependant -> source dependency\n");
+            b.append("# D C dependant -> classpath dependency\n");
             b.append("# I pubapi\n");
-            b.append("# R arguments\n");
             b.append("R ").append(theArgs).append("\n");
 
             // Copy over the javac_state for the packages that did not need recompilation.
             now.copyPackagesExcept(prev, recompiledPackages, new HashSet<String>());
             // Save the packages, ie package names, dependencies, pubapis and artifacts!

@@ -310,10 +318,12 @@
         boolean noFileFound = false;
         boolean foundCorrectVerNr = false;
         boolean newCommandLine = false;
         boolean syntaxError = false;
 
+        Log.debug("Loading javac state file: " + db.javacState);
+
         try (BufferedReader in = new BufferedReader(new FileReader(db.javacState))) {
             for (;;) {
                 String l = in.readLine();
                 if (l==null) break;
                 if (l.length()>=3 && l.charAt(1) == ' ') {

@@ -325,15 +335,18 @@
                         if (lastModule == null) { syntaxError = true; break; }
                         lastPackage = db.prev.loadPackage(lastModule, l);
                     } else
                     if (c == 'D') {
                         if (lastModule == null || lastPackage == null) { syntaxError = true; break; }
-                        lastPackage.loadDependency(l);
+                        char depType = l.charAt(2);
+                        if (depType != 'S' && depType != 'C')
+                            throw new RuntimeException("Bad dependency string: " + l);
+                        lastPackage.parseAndAddDependency(l.substring(4), depType == 'C');
                     } else
                     if (c == 'I') {
                         if (lastModule == null || lastPackage == null) { syntaxError = true; break; }
-                        lastPackage.loadPubapi(l);
+                        lastPackage.getPubApi().appendItem(l.substring(2)); // Strip "I "
                     } else
                     if (c == 'A') {
                         if (lastModule == null || lastPackage == null) { syntaxError = true; break; }
                         lastPackage.loadArtifact(l);
                     } else

@@ -486,15 +499,94 @@
 
     /**
      * Propagate recompilation through the dependency chains.
      * Avoid re-tainting packages that have already been compiled.
      */
-    public void taintPackagesDependingOnChangedPackages(Set<String> pkgs, Set<String> recentlyCompiled) {
+    public void taintPackagesDependingOnChangedPackages(Set<String> pkgsWithChangedPubApi, Set<String> recentlyCompiled) {
+        // For each to-be-recompiled-candidates...
+        for (Package pkg : new HashSet<>(prev.packages().values())) {
+            // Find out what it depends upon...
+            Set<String> deps = pkg.typeDependencies()
+                                  .values()
+                                  .stream()
+                                  .flatMap(s -> s.stream())
+                                  .collect(Collectors.toSet());
+            for (String dep : deps) {
+                String depPkg = ":" + dep.substring(0, dep.lastIndexOf('.'));
+                if (depPkg.equals(pkg.name()))
+                    continue;
+                // Checking if that dependency has changed
+                if (pkgsWithChangedPubApi.contains(depPkg) && !recentlyCompiled.contains(pkg.name())) {
+                    taintPackage(pkg.name(), "its depending on " + depPkg);
+                }
+            }
+        }
+    }
+
+    /**
+     * Compare the javac_state recorded public apis of packages on the classpath
+     * with the actual public apis on the classpath.
+     */
+    public void taintPackagesDependingOnChangedClasspathPackages() {
+
+        // 1. Collect fully qualified names of all interesting classpath dependencies
+        Set<String> fqDependencies = new HashSet<>();
         for (Package pkg : prev.packages().values()) {
-            for (String dep : pkg.dependencies()) {
-                if (pkgs.contains(dep) && !recentlyCompiled.contains(pkg.name())) {
-                    taintPackage(pkg.name(), " its depending on "+dep);
+            // Check if this package was compiled. If it's presence is recorded
+            // because it was on the class path and we needed to save it's
+            // public api, it's not a candidate for tainting.
+            if (pkg.sources().isEmpty())
+                continue;
+
+            pkg.typeClasspathDependencies().values().forEach(fqDependencies::addAll);
+        }
+
+        // 2. Extract the public APIs from the on disk .class files
+        // (Reason for doing step 1 in a separate phase is to avoid extracting
+        // public APIs of the same class twice.)
+        PubApiExtractor pubApiExtractor = new PubApiExtractor(options);
+        Map<String, PubApi> onDiskPubApi = new HashMap<>();
+        for (String cpDep : fqDependencies)
+            onDiskPubApi.put(cpDep, pubApiExtractor.getPubApi(cpDep));
+
+        // 3. Compare them with the public APIs as of last compilation (loaded from javac_state)
+        nextPkg:
+        for (Package pkg : prev.packages().values()) {
+            // Check if this package was compiled. If it's presence is recorded
+            // because it was on the class path and we needed to save it's
+            // public api, it's not a candidate for tainting.
+            if (pkg.sources().isEmpty())
+                continue;
+
+            Set<String> depsOfThisPkg = pkg.typeClasspathDependencies()
+                                           .values()
+                                           .stream()
+                                           .reduce(new HashSet<>(), Util::union);
+            for (String fqDep : depsOfThisPkg) {
+
+                String depPkg = ":" + fqDep.substring(0, fqDep.lastIndexOf('.'));
+                PubApi prevPkgApi = prev.packages().get(depPkg).getPubApi();
+
+                // This PubApi directly lists the members of the class, i.e. [ MEMBER1, MEMBER2, ... ]
+                PubApi prevDepApi = prevPkgApi.types.get(fqDep).pubApi;
+
+                // In order to dive *into* the class, we need to add
+                // .types.get(fqDep).pubApi below.
+                PubApi currentDepApi = onDiskPubApi.get(fqDep).types.get(fqDep).pubApi;
+
+                if (!currentDepApi.isBackwardCompatibleWith(prevDepApi)) {
+                    String apiDiff = currentDepApi.diff(prevDepApi);
+                    taintPackage(pkg.name(), "depends on classpath "
+                                + "package which has an updated package api ("
+                                + apiDiff + ")");
+                    //Log.debug("========================================");
+                    //Log.debug("------ PREV API ------------------------");
+                    //prevDepApi.asListOfStrings().forEach(Log::debug);
+                    //Log.debug("------ CURRENT API ---------------------");
+                    //currentDepApi.asListOfStrings().forEach(Log::debug);
+                    //Log.debug("========================================");
+                    continue nextPkg;
                 }
             }
         }
     }
 

@@ -658,18 +750,22 @@
                                            Set<String> recentlyCompiled,
                                            boolean[] rcValue) {
         Map<String,Transformer> suffixRules = new HashMap<>();
         suffixRules.put(".java", compileJavaPackages);
         compileJavaPackages.setExtra(args);
-
         rcValue[0] = perform(sjavac, binDir, suffixRules);
         recentlyCompiled.addAll(taintedPackages());
         clearTaintedPackages();
         boolean again = !packagesWithChangedPublicApis.isEmpty();
         taintPackagesDependingOnChangedPackages(packagesWithChangedPublicApis, recentlyCompiled);
         packagesWithChangedPublicApis = new HashSet<>();
         return again && rcValue[0];
+
+        // TODO: Figure out why 'again' checks packagesWithChangedPublicAPis.
+        // (It shouldn't matter if packages had changed pub apis as long as no
+        // one depends on them. Wouldn't it make more sense to let 'again'
+        // depend on taintedPackages?)
     }
 
     /**
      * Store the source into the set of sources belonging to the given transform.
      */

@@ -704,65 +800,98 @@
                     addFileToTransform(groupedSources, t, src);
                 }
             }
         }
         // Go through the transforms and transform them.
-        for (Map.Entry<Transformer,Map<String,Set<URI>>> e : groupedSources.entrySet()) {
+        for (Map.Entry<Transformer, Map<String, Set<URI>>> e : groupedSources.entrySet()) {
             Transformer t = e.getKey();
-            Map<String,Set<URI>> srcs = e.getValue();
-            // These maps need to be synchronized since multiple threads will be writing results into them.
-            Map<String,Set<URI>> packageArtifacts =
-                    Collections.synchronizedMap(new HashMap<String,Set<URI>>());
-            Map<String,Set<String>> packageDependencies =
-                    Collections.synchronizedMap(new HashMap<String,Set<String>>());
-            Map<String,String> packagePublicApis =
-                    Collections.synchronizedMap(new HashMap<String, String>());
+            Map<String, Set<URI>> srcs = e.getValue();
+            // These maps need to be synchronized since multiple threads will be
+            // writing results into them.
+            Map<String, Set<URI>> packageArtifacts = Collections.synchronizedMap(new HashMap<>());
+            Map<String, Map<String, Set<String>>> packageDependencies = Collections.synchronizedMap(new HashMap<>());
+            Map<String, Map<String, Set<String>>> packageCpDependencies = Collections.synchronizedMap(new HashMap<>());
+            Map<String, PubApi> packagePublicApis = Collections.synchronizedMap(new HashMap<>());
+            Map<String, PubApi> dependencyPublicApis = Collections.synchronizedMap(new HashMap<>());
 
             boolean  r = t.transform(sjavac,
                                      srcs,
                                      visibleSrcs,
                                      visibleClasses,
                                      prev.dependents(),
                                      outputDir.toURI(),
                                      packageArtifacts,
                                      packageDependencies,
+                                    packageCpDependencies,
                                      packagePublicApis,
+                                    dependencyPublicApis,
                                      0,
                                      isIncremental(),
                                      numCores,
                                      out,
                                      err);
-            if (!r) rc = false;
+            if (!r)
+                rc = false;
 
             for (String p : srcs.keySet()) {
                 recompiledPackages.add(p);
             }
             // The transform is done! Extract all the artifacts and store the info into the Package objects.
-            for (Map.Entry<String,Set<URI>> a : packageArtifacts.entrySet()) {
+            for (Map.Entry<String, Set<URI>> a : packageArtifacts.entrySet()) {
                 Module mnow = now.findModuleFromPackageName(a.getKey());
                 mnow.addArtifacts(a.getKey(), a.getValue());
             }
             // Extract all the dependencies and store the info into the Package objects.
-            for (Map.Entry<String,Set<String>> a : packageDependencies.entrySet()) {
-                Set<String> deps = a.getValue();
+            for (Map.Entry<String, Map<String, Set<String>>> a : packageDependencies.entrySet()) {
+                Map<String, Set<String>> deps = a.getValue();
                 Module mnow = now.findModuleFromPackageName(a.getKey());
-                mnow.setDependencies(a.getKey(), deps);
+                mnow.setDependencies(a.getKey(), deps, false);
             }
-            // Extract all the pubapis and store the info into the Package objects.
-            for (Map.Entry<String,String> a : packagePublicApis.entrySet()) {
-                Module mprev = prev.findModuleFromPackageName(a.getKey());
-                List<String> pubapi = Package.pubapiToList(a.getValue());
+            for (Map.Entry<String, Map<String, Set<String>>> a : packageCpDependencies.entrySet()) {
+                Map<String, Set<String>> deps = a.getValue();
                 Module mnow = now.findModuleFromPackageName(a.getKey());
-                mnow.setPubapi(a.getKey(), pubapi);
-                if (mprev.hasPubapiChanged(a.getKey(), pubapi)) {
+                mnow.setDependencies(a.getKey(), deps, true);
+            }
+
+            // This map contains the public api of the types that this
+            // compilation depended upon. This means that it may not contain
+            // full packages. In other words, we shouldn't remove knowledge of
+            // public apis but merge these with what we already have.
+            for (Map.Entry<String, PubApi> a : dependencyPublicApis.entrySet()) {
+                String pkg = a.getKey();
+                PubApi packagePartialPubApi = a.getValue();
+                Package pkgNow = now.findModuleFromPackageName(pkg).lookupPackage(pkg);
+                PubApi currentPubApi = pkgNow.getPubApi();
+                PubApi newPubApi = PubApi.mergeTypes(currentPubApi, packagePartialPubApi);
+                pkgNow.setPubapi(newPubApi);
+
+                // See JDK-8071904
+                if (now.packages().containsKey(pkg))
+                    now.packages().get(pkg).setPubapi(newPubApi);
+                else
+                    now.packages().put(pkg, pkgNow);
+            }
+
+            // The packagePublicApis cover entire packages (since sjavac compiles
+            // stuff on package level). This means that if a type is missing
+            // in the public api of a given package, it means that it has been
+            // removed. In other words, we should *set* the pubapi to whatever
+            // this map contains, and not merge it with what we already have.
+            for (Map.Entry<String, PubApi> a : packagePublicApis.entrySet()) {
+                String pkg = a.getKey();
+                PubApi newPubApi = a.getValue();
+                Module mprev = prev.findModuleFromPackageName(pkg);
+                Module mnow = now.findModuleFromPackageName(pkg);
+                mnow.setPubapi(pkg, newPubApi);
+                if (mprev.hasPubapiChanged(pkg, newPubApi)) {
                     // Aha! The pubapi of this package has changed!
                     // It can also be a new compile from scratch.
-                    if (mprev.lookupPackage(a.getKey()).existsInJavacState()) {
+                    if (mprev.lookupPackage(pkg).existsInJavacState()) {
                         // This is an incremental compile! The pubapi
                         // did change. Trigger recompilation of dependents.
-                        packagesWithChangedPublicApis.add(a.getKey());
-                        Log.info("The pubapi of "+Util.justPackageName(a.getKey())+" has changed!");
+                        packagesWithChangedPublicApis.add(pkg);
+                        Log.info("The API of " + Util.justPackageName(pkg) + " has changed!");
                     }
                 }
             }
         }
         return rc;

@@ -789,21 +918,25 @@
             }
         }
     }
 
     /**
-     * Compare the calculate source list, with an explicit list, usually supplied from the makefile.
-     * Used to detect bugs where the makefile and sjavac have different opinions on which files
-     * should be compiled.
-     */
-    public void compareWithMakefileList(File makefileSourceList) throws ProblemException {
-        // If we are building on win32 using for example cygwin the paths in the makefile source list
+     * Compare the calculate source list, with an explicit list, usually
+     * supplied from the makefile. Used to detect bugs where the makefile and
+     * sjavac have different opinions on which files should be compiled.
+     */
+    public void compareWithMakefileList(File makefileSourceList)
+            throws ProblemException {
+        // If we are building on win32 using for example cygwin the paths in the
+        // makefile source list
         // might be /cygdrive/c/.... which does not match c:\....
-        // We need to adjust our calculated sources to be identical, if necessary.
+        // We need to adjust our calculated sources to be identical, if
+        // necessary.
         boolean mightNeedRewriting = File.pathSeparatorChar == ';';
 
-        if (makefileSourceList == null) return;
+        if (makefileSourceList == null)
+            return;
 
         Set<String> calculatedSources = new HashSet<>();
         Set<String> listedSources = new HashSet<>();
 
         // Create a set of filenames with full paths.