Emit a 'manifest' file containing information about the compiled sources

For each compilation unit, JavaBuilder will record:

-source path
-package name
-a list of top-level class names
-whether the file was generated by an annotation processor

--
MOS_MIGRATED_REVID=96158093
diff --git a/src/java_tools/buildjar/BUILD b/src/java_tools/buildjar/BUILD
index 073272b..2877371 100644
--- a/src/java_tools/buildjar/BUILD
+++ b/src/java_tools/buildjar/BUILD
@@ -6,6 +6,7 @@
     main_class = "com.google.devtools.build.buildjar.BazelJavaBuilder",
     deps = [
         "//src/main/protobuf:proto_deps",
+        "//src/main/protobuf:proto_java_compilation",
         "//src/main/protobuf:proto_worker_protocol",
         "//third_party:error_prone",
         "//third_party:guava",
@@ -194,6 +195,7 @@
     srcjars = [
         "//src/main/protobuf:proto_deps_srcjar",
         "//src/main/protobuf:proto_worker_protocol_srcjar",
+        "//src/main/protobuf:proto_java_compilation_srcjar",
     ],
 )
 
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractJavaBuilder.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractJavaBuilder.java
index 766d1f5..035d3b3 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractJavaBuilder.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractJavaBuilder.java
@@ -168,14 +168,12 @@
         if (build.getGeneratedSourcesOutputJar() != null) {
           buildGensrcJar(build, err);
         }
-        if (build.getGeneratedClassOutputJar() != null) {
-          buildGenclassJar(build);
-        }
       }
       successful = true;
     } finally {
       build.getDependencyModule().emitUsedClasspath(build.getClassPath());
       build.getDependencyModule().emitDependencyInformation(build.getClassPath(), successful);
+      build.getProcessingModule().emitManifestProto();
       shutdown(err);
     }
   }
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractLibraryBuilder.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractLibraryBuilder.java
index c402cc0..cf1c985 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractLibraryBuilder.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/AbstractLibraryBuilder.java
@@ -22,12 +22,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Enumeration;
@@ -84,30 +78,6 @@
   }
 
   /**
-   * Build a jar file containing class files that were generated by an annotation processor.
-   */
-  public void buildGenclassJar(final JavaLibraryBuildRequest build) throws IOException {
-    final JarCreator jar = new JarCreator(build.getGeneratedClassOutputJar());
-    jar.setNormalize(true);
-    jar.setCompression(build.compressJar());
-    final Path classDir = Paths.get(build.getClassDir()).toAbsolutePath();
-    Files.walkFileTree(
-        classDir,
-        new SimpleFileVisitor<Path>() {
-          @Override
-          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
-              throws IOException {
-            if (build.getProcessingModule().isGeneratedClass(file)) {
-              file = file.toAbsolutePath();
-              jar.addEntry(classDir.relativize(file).toString(), file.toString());
-            }
-            return FileVisitResult.CONTINUE;
-          }
-        });
-    jar.execute();
-  }
-
-  /**
    * Adds a collection of resource entries. Each entry is a string composed of a
    * pair of parts separated by a colon ':'. The name of the resource comes from
    * the second part, and the path to the resource comes from the whole string
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JavaLibraryBuildRequest.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JavaLibraryBuildRequest.java
index 4d14071..6fc97bb 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JavaLibraryBuildRequest.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/JavaLibraryBuildRequest.java
@@ -141,9 +141,10 @@
     if (optionsParser.getSourceGenDir() != null) {
       processingBuilder.setSourceGenDir(Paths.get(optionsParser.getSourceGenDir()));
     }
-    if (optionsParser.getClassDir() != null) {
-      processingBuilder.setClassDir(Paths.get(optionsParser.getClassDir()));
+    if (optionsParser.getManifestProtoPath() != null) {
+      processingBuilder.setManifestProtoPath(Paths.get(optionsParser.getManifestProtoPath()));
     }
+    processingBuilder.addAllSourceRoots(optionsParser.getSourceRoots());
     this.processingModule = processingBuilder.build();
 
     ImmutableList.Builder<BlazeJavaCompilerPlugin> pluginsBuilder =
@@ -184,7 +185,7 @@
     this.javacOpts = ImmutableList.copyOf(optionsParser.getJavacOpts());
     this.sourceGenDir = optionsParser.getSourceGenDir();
     this.generatedSourcesOutputJar = optionsParser.getGeneratedSourcesOutputJar();
-    this.generatedClassOutputJar = optionsParser.getGeneratedClassOutputJar();
+    this.generatedClassOutputJar = optionsParser.getManifestProtoPath();
   }
 
   public ImmutableList<String> getJavacOpts() {
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/OptionsParser.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/OptionsParser.java
index 93c6c94..2e18449 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/OptionsParser.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/OptionsParser.java
@@ -51,7 +51,8 @@
 
   private String sourceGenDir;
   private String generatedSourcesOutputJar;
-  private String generatedClassOutputJar;
+  private String manifestProtoPath;
+  private final Set<String> sourceRoots = new HashSet<>();
 
   private final List<String> sourceFiles = new ArrayList<>();
   private final List<String> sourceJars = new ArrayList<>();
@@ -95,8 +96,7 @@
    *
    * @throws InvalidCommandLineException on an invalid option being passed.
    */
-  private void processCommandlineArgs(Deque<String> argQueue)
-      throws InvalidCommandLineException {
+  private void processCommandlineArgs(Deque<String> argQueue) throws InvalidCommandLineException {
     for (String arg = argQueue.pollFirst(); arg != null; arg = argQueue.pollFirst()) {
       switch (arg) {
         case "--javacopts":
@@ -106,18 +106,20 @@
           // terminator to the passed arguments.
           collectFlagArguments(javacOpts, argQueue, "--");
           break;
-        case "--direct_dependency": {
-          String jar = getArgument(argQueue, arg);
-          String target = getArgument(argQueue, arg);
-          directJarsToTargets.put(jar, target);
-          break;
-        }
-        case "--indirect_dependency": {
-          String jar = getArgument(argQueue, arg);
-          String target = getArgument(argQueue, arg);
-          indirectJarsToTargets.put(jar, target);
-          break;
-        }
+        case "--direct_dependency":
+          {
+            String jar = getArgument(argQueue, arg);
+            String target = getArgument(argQueue, arg);
+            directJarsToTargets.put(jar, target);
+            break;
+          }
+        case "--indirect_dependency":
+          {
+            String jar = getArgument(argQueue, arg);
+            String target = getArgument(argQueue, arg);
+            indirectJarsToTargets.put(jar, target);
+            break;
+          }
         case "--strict_java_deps":
           strictJavaDeps = getArgument(argQueue, arg);
           break;
@@ -139,8 +141,11 @@
         case "--generated_sources_output":
           generatedSourcesOutputJar = getArgument(argQueue, arg);
           break;
-        case "--classes_from_generated_sources_output":
-          generatedClassOutputJar = getArgument(argQueue, arg);
+        case "--output_manifest_proto":
+          manifestProtoPath = getArgument(argQueue, arg);
+          break;
+        case "--source_roots":
+          collectFlagArguments(sourceRoots, argQueue, "-");
           break;
         case "--sources":
           collectFlagArguments(sourceFiles, argQueue, "-");
@@ -227,8 +232,7 @@
    * @param arg the argument to pre-process.
    * @throws java.io.IOException if one of the files containing options cannot be read.
    */
-  private static void expandArgument(Deque<String> expanded, String arg)
-      throws IOException {
+  private static void expandArgument(Deque<String> expanded, String arg) throws IOException {
     if (arg.startsWith("@") && !arg.startsWith("@@")) {
       for (String line : Files.readAllLines(Paths.get(arg.substring(1)), UTF_8)) {
         if (line.length() > 0) {
@@ -248,8 +252,8 @@
    * @param args
    * @param terminatorPrefix the terminator prefix to stop collecting of argument flags.
    */
-  private static void collectFlagArguments(Collection<String> output, Deque<String> args,
-      String terminatorPrefix) {
+  private static void collectFlagArguments(
+      Collection<String> output, Deque<String> args, String terminatorPrefix) {
     for (String arg = args.pollFirst(); arg != null; arg = args.pollFirst()) {
       if (arg.startsWith(terminatorPrefix)) {
         args.addFirst(arg);
@@ -275,8 +279,7 @@
         break;
       }
       if (arg.contains(",")) {
-        throw new InvalidCommandLineException("processor argument may not contain commas: "
-            + arg);
+        throw new InvalidCommandLineException("processor argument may not contain commas: " + arg);
       }
       output.add(arg);
     }
@@ -339,8 +342,12 @@
     return generatedSourcesOutputJar;
   }
 
-  public String getGeneratedClassOutputJar() {
-    return generatedClassOutputJar;
+  public String getManifestProtoPath() {
+    return manifestProtoPath;
+  }
+
+  public Set<String> getSourceRoots() {
+    return sourceRoots;
   }
 
   public List<String> getSourceFiles() {
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingModule.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingModule.java
index e9d4c18..65a831f 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingModule.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingModule.java
@@ -15,50 +15,108 @@
 package com.google.devtools.build.buildjar.javac.plugins.processing;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin;
+import com.google.devtools.build.buildjar.proto.JavaCompilation.CompilationUnit;
+import com.google.devtools.build.buildjar.proto.JavaCompilation.Manifest;
 
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.HashSet;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * A module for information about the compilation's annotation processing.
  */
 public class AnnotationProcessingModule {
-  
+
   /**
    * A builder for {@link AnnotationProcessingModule}s.
    */
   public static class Builder {
     private Path sourceGenDir;
-    private Path classDir;
+    private Path manifestProto;
+    private ImmutableSet.Builder<Path> sourceRoots = ImmutableSet.builder();
 
     private Builder() {}
 
     public AnnotationProcessingModule build() {
-      return new AnnotationProcessingModule(sourceGenDir, classDir);
+      return new AnnotationProcessingModule(
+          sourceGenDir, manifestProto, validateSourceRoots(sourceRoots.build()));
+    }
+
+    /**
+     * Verify that source roots do not contain other source roots.
+     *
+     * <p>If one source root is an ancestor of another, the source path to
+     * use in the manifest will be ambiguous.
+     */
+    private ImmutableSet<Path> validateSourceRoots(ImmutableSet<Path> roots) {
+      // It's sad that this is quadratic, but the number of source roots
+      // should be <= 2.
+      for (Path a : roots) {
+        for (Path b : roots) {
+          if (a.equals(b) || b.getNameCount() == 0) {
+            continue;
+          }
+          if (a.startsWith(b)) {
+            throw new IllegalArgumentException(
+                String.format("Source root %s is a parent of %s", b, a));
+          }
+        }
+      }
+      return roots;
     }
 
     public void setSourceGenDir(Path sourceGenDir) {
-      this.sourceGenDir = sourceGenDir.toAbsolutePath();
+      this.sourceGenDir = sourceGenDir;
     }
 
-    public void setClassDir(Path classDir) {
-      this.classDir = classDir.toAbsolutePath();
+    public void setManifestProtoPath(Path manifestProto) {
+      this.manifestProto = manifestProto.toAbsolutePath();
+    }
+
+    public void addAllSourceRoots(Set<String> sourceRoots) {
+      for (String root : sourceRoots) {
+        this.sourceRoots.add(Paths.get(root));
+      }
     }
   }
 
   private final boolean enabled;
   private final Path sourceGenDir;
-  private final Path classDir;
+  private final Path manifestProto;
+  private final ImmutableSet<Path> sourceRoots;
 
-  public Path sourceGenDir() {
-    return sourceGenDir;
+  public boolean isGenerated(Path path) {
+    return path.startsWith(sourceGenDir);
   }
 
-  private AnnotationProcessingModule(Path sourceGenDir, Path classDir) {
+  public Path stripSourceRoot(Path path) {
+    if (path.startsWith(sourceGenDir)) {
+      return sourceGenDir.relativize(path);
+    }
+    for (Path sourceRoot : sourceRoots) {
+      if (path.startsWith(sourceRoot)) {
+        return sourceRoot.relativize(path);
+      }
+    }
+    return path;
+  }
+
+  private AnnotationProcessingModule(
+      Path sourceGenDir, Path manifestProto, ImmutableSet<Path> sourceRoots) {
     this.sourceGenDir = sourceGenDir;
-    this.classDir = classDir;
-    this.enabled = sourceGenDir != null && classDir != null;
+    this.manifestProto = manifestProto;
+    this.sourceRoots = sourceRoots;
+    this.enabled = sourceGenDir != null && manifestProto != null;
   }
 
   public static Builder builder() {
@@ -71,38 +129,33 @@
     }
   }
 
-  /**
-   * The set of prefixes of generated class files.
-   */
-  private final HashSet<String> pathPrefixes = new HashSet<>();
+  private final Map<String, CompilationUnit> units = new HashMap<>();
 
-  /**
-   * Record the prefix of a group of generated class files.
-   *
-   * <p>Prefixes are used to handle generated inner classes. Since
-   * e.g. j/c/g/Foo.class and j/c/g/Foo$Inner.class both correspond to the
-   * same generated source file, only the prefix "j/c/g/Foo" is recorded.
-   */
-  void recordPrefix(String pathPrefix) {
-    pathPrefixes.add(pathPrefix);
+  public void recordUnit(CompilationUnit unit) {
+    units.put(unit.getPath(), unit);
   }
 
-  /** Returns true if the given path is to a generated source file. */
-  public boolean isGeneratedSource(Path sourcePath) {
-    return sourcePath.toAbsolutePath().startsWith(sourceGenDir);
+  private Manifest buildManifestProto() {
+    Manifest.Builder builder = Manifest.newBuilder();
+
+    List<String> keys = new ArrayList<>(units.keySet());
+    Collections.sort(keys);
+    for (String key : keys) {
+      CompilationUnit unit = units.get(key);
+      builder.addCompilationUnit(unit);
+    }
+
+    return builder.build();
   }
 
-  /** Returns true if the given path is to a generated class file. */
-  public boolean isGeneratedClass(Path path) {
-    if (!path.getFileName().toString().endsWith(".class")) {
-      return false;
+  public void emitManifestProto() throws IOException {
+    if (!enabled) {
+      return;
     }
-    String prefix = classDir.relativize(path.toAbsolutePath()).toString();
-    prefix = prefix.substring(0, prefix.length() - ".class".length());
-    int idx = prefix.lastIndexOf('$');
-    if (idx > 0) {
-      prefix = prefix.substring(0, idx);
+    try (OutputStream out = Files.newOutputStream(manifestProto)) {
+      buildManifestProto().writeTo(out);
+    } catch (IOException ex) {
+      throw new IOException("Cannot write manifest to " + manifestProto, ex);
     }
-    return pathPrefixes.contains(prefix);
   }
 }
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingPlugin.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingPlugin.java
index 315e6fe..ad8b0d9 100644
--- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingPlugin.java
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins/processing/AnnotationProcessingPlugin.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.buildjar.javac.plugins.processing;
 
 import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin;
+import com.google.devtools.build.buildjar.proto.JavaCompilation.CompilationUnit;
 
 import com.sun.tools.javac.comp.AttrContext;
 import com.sun.tools.javac.comp.Env;
@@ -42,34 +43,32 @@
   @Override
   public void postAttribute(Env<AttrContext> env) {
     if (toplevels.add(env.toplevel)) {
-      recordSymbols(env.toplevel);
+      recordInfo(env.toplevel);
     }
   }
 
-  /**
-   * For each top-level type, record the path prefixes of that type's class,
-   * and all inner and anonymous classes declared inside that type.
-   *
-   * <p>e.g. for j.c.g.Foo we record the prefix j/c/g/Foo, which will match
-   * j/c/g/Foo.class, j/c/g/Foo$Inner.class, j/c/g/Foo$1.class, etc.
-   */
-  private void recordSymbols(JCCompilationUnit toplevel) {
-    if (toplevel.sourcefile == null) {
-      return;
+  private void recordInfo(JCCompilationUnit toplevel) {
+    CompilationUnit.Builder builder = CompilationUnit.newBuilder();
+
+    if (toplevel.sourcefile != null) {
+      // FileObject#getName() returns the original exec root-relative path of
+      // the source file, which is want we want.
+      // Paths.get(sourcefile.toUri()) would absolutize the path.
+      Path path = Paths.get(toplevel.sourcefile.getName());
+      builder.setPath(processingModule.stripSourceRoot(path).toString());
+      builder.setGeneratedByAnnotationProcessor(processingModule.isGenerated(path));
     }
-    Path sourcePath = Paths.get(toplevel.sourcefile.toUri());
-    if (!processingModule.isGeneratedSource(sourcePath)) {
-      return;
-    }
-    String packageBase = "";
+
     if (toplevel.getPackageName() != null) {
-      packageBase = toplevel.getPackageName().toString().replace('.', '/') + "/";
+      builder.setPkg(toplevel.getPackageName().toString());
     }
+
     for (JCTree decl : toplevel.defs) {
       if (decl instanceof JCClassDecl) {
-        String pathPrefix = packageBase + ((JCClassDecl) decl).getSimpleName();
-        processingModule.recordPrefix(pathPrefix);
+        builder.addTopLevel(((JCClassDecl) decl).getSimpleName().toString());
       }
     }
+
+    processingModule.recordUnit(builder.build());
   }
 }
diff --git a/src/main/protobuf/BUILD b/src/main/protobuf/BUILD
index 8ae832a..f896347 100644
--- a/src/main/protobuf/BUILD
+++ b/src/main/protobuf/BUILD
@@ -5,6 +5,7 @@
 FILES = [
     "build",
     "deps",
+    "java_compilation",
     "crosstool_config",
     "extra_actions_base",
     "test_status",
diff --git a/src/main/protobuf/java_compilation.proto b/src/main/protobuf/java_compilation.proto
new file mode 100644
index 0000000..4b15058
--- /dev/null
+++ b/src/main/protobuf/java_compilation.proto
@@ -0,0 +1,41 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// Java library manifest, for use by IDEs.
+
+syntax = "proto2";
+
+package blaze.buildjar;
+
+option java_package = "com.google.devtools.build.buildjar.proto";
+
+// Information about a single compilation unit (.java file)
+message CompilationUnit {
+  // The path to the compilation unit
+  optional string path = 1;
+
+  // The package of the source file
+  optional string pkg = 2;
+
+  // Whether the source was generated by an annotation processor
+  optional bool generated_by_annotation_processor = 3;
+
+  // The list of top-level types in the compilation unit
+  repeated string top_level = 4;
+}
+
+// Top-level message found in .manifest artifacts
+message Manifest {
+  repeated CompilationUnit compilation_unit = 1;
+}