/** Save the javac_state file. */
  public void save() throws IOException {
    if (!needsSaving) return;
    try (FileWriter out = new FileWriter(javacStateFilename)) {
      StringBuilder b = new StringBuilder();
      long millisNow = System.currentTimeMillis();
      Date d = new Date(millisNow);
      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("# 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("# 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!
      // I.e. the lot.
      Module.saveModules(now.modules(), b);

      String s = b.toString();
      out.write(s, 0, s.length());
    }
  }
 /** Return those files belonging to now, but not prev. */
 private Set<Source> calculateAddedSources() {
   Set<Source> added = new HashSet<>();
   for (String src : now.sources().keySet()) {
     if (prev.sources().get(src) == null) {
       added.add(now.sources().get(src));
     }
   }
   return added;
 }
 /** Return those files belonging to prev, but not now. */
 private Set<Source> calculateRemovedSources() {
   Set<Source> removed = new HashSet<>();
   for (String src : prev.sources().keySet()) {
     if (now.sources().get(src) == null) {
       removed.add(prev.sources().get(src));
     }
   }
   return removed;
 }
  /** Remove artifacts that are no longer produced when compiling! */
  public void removeSuperfluousArtifacts(Set<String> recentlyCompiled) {
    // Nothing to do, if nothing was recompiled.
    if (recentlyCompiled.size() == 0) return;

    for (String pkg : now.packages().keySet()) {
      // If this package has not been recompiled, skip the check.
      if (!recentlyCompiled.contains(pkg)) continue;
      Collection<File> arts = now.artifacts().values();
      for (File f : fetchPrevArtifacts(pkg).values()) {
        if (!arts.contains(f)) {
          Log.debug("Removing " + f.getPath() + " since it is now superfluous!");
          if (f.exists()) f.delete();
        }
      }
    }
  }
  /**
   * Scan all output dirs for artifacts and remove those files (artifacts?) that are not recognized
   * as such, in the javac_state file.
   */
  public void removeUnidentifiedArtifacts() {
    Set<File> allKnownArtifacts = new HashSet<>();
    for (Package pkg : prev.packages().values()) {
      for (File f : pkg.artifacts().values()) {
        allKnownArtifacts.add(f);
      }
    }
    // Do not forget about javac_state....
    allKnownArtifacts.add(javacState);

    for (File f : binArtifacts) {
      if (!allKnownArtifacts.contains(f)) {
        Log.debug("Removing " + f.getPath() + " since it is unknown to the javac_state.");
        f.delete();
      }
    }
    for (File f : headerArtifacts) {
      if (!allKnownArtifacts.contains(f)) {
        Log.debug("Removing " + f.getPath() + " since it is unknown to the javac_state.");
        f.delete();
      }
    }
    for (File f : gensrcArtifacts) {
      if (!allKnownArtifacts.contains(f)) {
        Log.debug("Removing " + f.getPath() + " since it is unknown to the javac_state.");
        f.delete();
      }
    }
  }
 /** Lookup the artifacts generated for this package in the previous build. */
 private Map<String, File> fetchPrevArtifacts(String pkg) {
   Package p = prev.packages().get(pkg);
   if (p != null) {
     return p.artifacts();
   }
   return new HashMap<>();
 }
 /**
  * Return those files where the timestamp is newer. If a source file timestamp suddenly is older
  * than what is known about it in javac_state, then consider it modified, but print a warning!
  */
 private Set<Source> calculateModifiedSources() {
   Set<Source> modified = new HashSet<>();
   for (String src : now.sources().keySet()) {
     Source n = now.sources().get(src);
     Source t = prev.sources().get(src);
     if (prev.sources().get(src) != null) {
       if (t != null) {
         if (n.lastModified() > t.lastModified()) {
           modified.add(n);
         } else if (n.lastModified() < t.lastModified()) {
           modified.add(n);
           Log.warn("The source file " + n.name() + " timestamp has moved backwards in time.");
         }
       }
     }
   }
   return modified;
 }
 /**
  * Propagate recompilation through the dependency chains. Avoid re-tainting packages that have
  * already been compiled.
  */
 public void taintPackagesDependingOnChangedPackages(
     Set<String> pkgs, Set<String> recentlyCompiled) {
   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);
       }
     }
   }
 }
 /** If artifacts have gone missing, force a recompile of the packages they belong to. */
 public void taintPackagesThatMissArtifacts() {
   for (Package pkg : prev.packages().values()) {
     for (File f : pkg.artifacts().values()) {
       if (!f.exists()) {
         // Hmm, the artifact on disk does not exist! Someone has removed it....
         // Lets rebuild the package.
         taintPackage(pkg.name(), "" + f + " is missing.");
       }
     }
   }
 }
 /** Mark a java package as tainted, ie it needs recompilation. */
 public void taintPackage(String name, String because) {
   if (!taintedPackages.contains(name)) {
     if (because != null)
       Log.debug("Tainting " + Util.justPackageName(name) + " because " + because);
     // It has not been tainted before.
     taintedPackages.add(name);
     needsSaving();
     Package nowp = now.packages().get(name);
     if (nowp != null) {
       for (String d : nowp.dependents()) {
         taintPackage(d, because);
       }
     }
   }
 }
  /**
   * 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.
    boolean mightNeedRewriting = File.pathSeparatorChar == ';';

    if (makefileSourceList == null) return;

    Set<String> calculatedSources = new HashSet<>();
    Set<String> listedSources = new HashSet<>();

    // Create a set of filenames with full paths.
    for (Source s : now.sources().values()) {
      // Don't include link only sources when comparing sources to compile
      if (!s.isLinkedOnly()) {
        calculatedSources.add(s.file().getPath());
      }
    }
    // Read in the file and create another set of filenames with full paths.
    try {
      BufferedReader in = new BufferedReader(new FileReader(makefileSourceList));
      for (; ; ) {
        String l = in.readLine();
        if (l == null) break;
        l = l.trim();
        if (mightNeedRewriting) {
          if (l.indexOf(":") == 1 && l.indexOf("\\") == 2) {
            // Everything a-ok, the format is already C:\foo\bar
          } else if (l.indexOf(":") == 1 && l.indexOf("/") == 2) {
            // The format is C:/foo/bar, rewrite into the above format.
            l = l.replaceAll("/", "\\\\");
          } else if (l.charAt(0) == '/' && l.indexOf("/", 1) != -1) {
            // The format might be: /cygdrive/c/foo/bar, rewrite into the above format.
            // Do not hardcode the name cygdrive here.
            int slash = l.indexOf("/", 1);
            l = l.replaceAll("/", "\\\\");
            l = "" + l.charAt(slash + 1) + ":" + l.substring(slash + 2);
          }
          if (Character.isLowerCase(l.charAt(0))) {
            l = Character.toUpperCase(l.charAt(0)) + l.substring(1);
          }
        }
        listedSources.add(l);
      }
    } catch (FileNotFoundException e) {
      throw new ProblemException(
          "Could not open " + makefileSourceList.getPath() + " since it does not exist!");
    } catch (IOException e) {
      throw new ProblemException("Could not read " + makefileSourceList.getPath());
    }

    for (String s : listedSources) {
      if (!calculatedSources.contains(s)) {
        throw new ProblemException(
            "The makefile listed source " + s + " was not calculated by the smart javac wrapper!");
      }
    }

    for (String s : calculatedSources) {
      if (!listedSources.contains(s)) {
        throw new ProblemException(
            "The smart javac wrapper calculated source " + s + " was not listed by the makefiles!");
      }
    }
  }
  /**
   * For all packages, find all sources belonging to the package, group the sources based on their
   * transformers and apply the transformers on each source code group.
   */
  private boolean perform(File outputDir, Map<String, Transformer> suffixRules) {
    boolean rc = true;
    // Group sources based on transforms. A source file can only belong to a single transform.
    Map<Transformer, Map<String, Set<URI>>> groupedSources = new HashMap<>();
    for (Source src : now.sources().values()) {
      Transformer t = suffixRules.get(src.suffix());
      if (t != null) {
        if (taintedPackages.contains(src.pkg().name()) && !src.isLinkedOnly()) {
          addFileToTransform(groupedSources, t, src);
        }
      }
    }
    // Go through the transforms and transform them.
    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>());

      boolean r =
          t.transform(
              srcs,
              visibleSrcs,
              visibleClasses,
              prev.dependents(),
              outputDir.toURI(),
              packageArtifacts,
              packageDependencies,
              packagePublicApis,
              0,
              isIncremental(),
              numCores,
              out,
              err);
      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()) {
        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();
        Module mnow = now.findModuleFromPackageName(a.getKey());
        mnow.setDependencies(a.getKey(), deps);
      }
      // 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());
        Module mnow = now.findModuleFromPackageName(a.getKey());
        mnow.setPubapi(a.getKey(), pubapi);
        if (mprev.hasPubapiChanged(a.getKey(), pubapi)) {
          // Aha! The pubapi of this package has changed!
          // It can also be a new compile from scratch.
          if (mprev.lookupPackage(a.getKey()).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!");
          }
        }
      }
    }
    return rc;
  }
 /** Returns true if this is an incremental build. */
 public boolean isIncremental() {
   return !prev.sources().isEmpty();
 }