ASwB aspect: parse java packages during execution

--
MOS_MIGRATED_REVID=109305952
diff --git a/src/BUILD b/src/BUILD
index 5f5f348..70aad12 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -74,6 +74,7 @@
         "//third_party:srcs",
         "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/jarhelper:srcs",
         "//src/tools/android/java/com/google/devtools/build/android:embedded_tools",
+        "//src/tools/android/java/com/google/devtools/build/android/ideinfo:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/idlclass:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/incrementaldeployment:srcs",
         "//src/tools/android/java/com/google/devtools/build/android/ziputils:embedded_tools",
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 4691a7a..c4c0435 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -709,6 +709,7 @@
         ":android-rules",
         ":build-base",
         ":collect",
+        ":common",
         ":concurrent",
         ":java-rules",
         ":packages-internal",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
index 4857c21..bdf4639 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -486,6 +486,14 @@
         }
       };
 
+  public static final Function<Artifact, String> ABSOLUTE_PATH_STRING =
+      new Function<Artifact, String>() {
+        @Override
+        public String apply(Artifact artifact) {
+          return artifact.getPath().getPathString();
+        }
+      };
+
   /**
    * Converts a collection of artifacts into execution-time path strings, and
    * adds those to a given collection. Middleman artifacts are ignored by this
@@ -510,6 +518,16 @@
   }
 
   /**
+   * Lazily converts artifacts into absolute path strings. Middleman artifacts are ignored by
+   * this method.
+   */
+  public static Iterable<String> toAbsolutePaths(Iterable<Artifact> artifacts) {
+    return Iterables.transform(
+        Iterables.filter(artifacts, MIDDLEMAN_FILTER),
+        ABSOLUTE_PATH_STRING);
+  }
+
+  /**
    * Lazily converts artifacts into root-relative path strings. Middleman artifacts are ignored by
    * this method.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java b/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java
index 0053bf5..cdb90eb 100644
--- a/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java
+++ b/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java
@@ -15,12 +15,20 @@
 package com.google.devtools.build.lib.ideinfo;
 
 import static com.google.common.collect.Iterables.transform;
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import com.google.common.io.ByteSource;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionOwner;
 import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
 import com.google.devtools.build.lib.actions.Root;
 import com.google.devtools.build.lib.analysis.AnalysisUtils;
 import com.google.devtools.build.lib.analysis.ConfiguredAspect;
@@ -31,7 +39,9 @@
 import com.google.devtools.build.lib.analysis.RuleContext;
 import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
 import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
 import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -66,6 +76,7 @@
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 import javax.annotation.Nullable;
 
@@ -108,10 +119,23 @@
     }
   };
 
+  /** White-list for rules potentially having .java srcs */
+  private static final Set<Kind> JAVA_SRC_RULES = ImmutableSet.of(
+      Kind.JAVA_LIBRARY,
+      Kind.JAVA_TEST,
+      Kind.JAVA_BINARY,
+      Kind.ANDROID_LIBRARY,
+      Kind.ANDROID_BINARY,
+      Kind.ANDROID_TEST,
+      Kind.ANDROID_ROBOELECTRIC_TEST);
+
   @Override
   public AspectDefinition getDefinition(AspectParameters aspectParameters) {
     AspectDefinition.Builder builder = new AspectDefinition.Builder(NAME)
-        .attributeAspect("runtime_deps", AndroidStudioInfoAspect.class);
+        .attributeAspect("runtime_deps", AndroidStudioInfoAspect.class)
+        .add(attr("$packageParser", LABEL).cfg(HOST).exec()
+            .value(Label.parseAbsoluteUnchecked(
+                Constants.TOOLS_REPOSITORY + "//tools/android:PackageParser")));
 
     for (PrerequisiteAttr prerequisiteAttr : PREREQUISITE_ATTRS) {
       builder.attributeAspect(prerequisiteAttr.name, AndroidStudioInfoAspect.class);
@@ -233,11 +257,14 @@
       NestedSet<Label> directDependencies,
       AndroidStudioInfoFilesProvider.Builder providerBuilder) {
 
-    Artifact ideInfoFile = ideInfoArtifact(base, ruleContext, ASWB_BUILD_SUFFIX);
-    Artifact ideInfoTextFile = ideInfoArtifact(base, ruleContext, ASWB_BUILD_TEXT_SUFFIX);
-
+    Artifact ideInfoFile = derivedArtifact(base, ruleContext, ASWB_BUILD_SUFFIX);
+    Artifact ideInfoTextFile = derivedArtifact(base, ruleContext, ASWB_BUILD_TEXT_SUFFIX);
+    Artifact packageManifest = createPackageManifest(base, ruleContext, ruleKind);
     providerBuilder.ideInfoFilesBuilder().add(ideInfoFile);
     providerBuilder.ideInfoTextFilesBuilder().add(ideInfoTextFile);
+    if (packageManifest != null) {
+      providerBuilder.ideInfoFilesBuilder().add(packageManifest);
+    }
     NestedSetBuilder<Artifact> ideResolveArtifacts = providerBuilder.ideResolveFilesBuilder();
 
     RuleIdeInfo.Builder outputBuilder = RuleIdeInfo.newBuilder();
@@ -263,7 +290,9 @@
         || ruleKind == Kind.ANDROID_TEST
         || ruleKind == Kind.ANDROID_ROBOELECTRIC_TEST
         || ruleKind == Kind.PROTO_LIBRARY) {
-      outputBuilder.setJavaRuleIdeInfo(makeJavaRuleIdeInfo(base, ruleContext, ideResolveArtifacts));
+      JavaRuleIdeInfo javaRuleIdeInfo = makeJavaRuleIdeInfo(
+          base, ruleContext, ideResolveArtifacts, packageManifest);
+      outputBuilder.setJavaRuleIdeInfo(javaRuleIdeInfo);
     }
     if (ruleKind == Kind.ANDROID_LIBRARY
         || ruleKind == Kind.ANDROID_BINARY
@@ -287,21 +316,58 @@
         makeProtoWriteAction(ruleContext.getActionOwner(), ruleIdeInfo, ideInfoFile));
     ruleContext.registerAction(
         makeProtoTextWriteAction(ruleContext.getActionOwner(), ruleIdeInfo, ideInfoTextFile));
+    if (packageManifest != null) {
+      ruleContext.registerAction(
+          makePackageManifestAction(ruleContext, packageManifest, getJavaSources(ruleContext))
+      );
+    }
 
     return provider;
   }
 
-  private static Artifact ideInfoArtifact(ConfiguredTarget base, RuleContext ruleContext,
+  @Nullable private static Artifact createPackageManifest(ConfiguredTarget base,
+      RuleContext ruleContext, Kind ruleKind) {
+    if (!JAVA_SRC_RULES.contains(ruleKind)) {
+      return null;
+    }
+    Collection<Artifact> sourceFiles = getJavaSources(ruleContext);
+    if (sourceFiles.isEmpty()) {
+      return null;
+    }
+    return derivedArtifact(base, ruleContext, ".manifest");
+  }
+
+  private static Action[] makePackageManifestAction(
+      RuleContext ruleContext,
+      Artifact packageManifest,
+      Collection<Artifact> sourceFiles) {
+
+    return new SpawnAction.Builder()
+        .addInputs(sourceFiles)
+        .addOutput(packageManifest)
+        .setExecutable(ruleContext.getExecutablePrerequisite("$packageParser", Mode.HOST))
+        .setCommandLine(CustomCommandLine.builder()
+            .addExecPath("--output_manifest", packageManifest)
+            .addJoinStrings("--sources_absolute_paths", ":", Artifact.toAbsolutePaths(sourceFiles))
+            .addJoinExecPaths("--sources_execution_paths", ":", sourceFiles)
+            .build())
+        .useParameterFile(ParameterFileType.SHELL_QUOTED)
+        .setProgressMessage("Parsing java package strings for " + ruleContext.getRule())
+        .setMnemonic("JavaPackageManifest")
+        .build(ruleContext);
+  }
+
+  private static Artifact derivedArtifact(ConfiguredTarget base, RuleContext ruleContext,
       String suffix) {
     BuildConfiguration configuration = ruleContext.getConfiguration();
     assert configuration != null;
     Root genfilesDirectory = configuration.getGenfilesDirectory();
 
-    PathFragment ideBuildFilePath =
+    PathFragment derivedFilePath =
         getOutputFilePath(base, ruleContext, suffix);
 
     return ruleContext.getAnalysisEnvironment().getDerivedArtifact(
-        ideBuildFilePath, genfilesDirectory);
+        derivedFilePath, genfilesDirectory);
   }
 
   private static AndroidRuleIdeInfo makeAndroidRuleIdeInfo(
@@ -401,7 +467,8 @@
   private static JavaRuleIdeInfo makeJavaRuleIdeInfo(
       ConfiguredTarget base,
       RuleContext ruleContext,
-      NestedSetBuilder<Artifact> ideResolveArtifacts) {
+      NestedSetBuilder<Artifact> ideResolveArtifacts,
+      @Nullable Artifact packageManifest) {
     JavaRuleIdeInfo.Builder builder = JavaRuleIdeInfo.newBuilder();
     JavaRuleOutputJarsProvider outputJarsProvider =
         base.getProvider(JavaRuleOutputJarsProvider.class);
@@ -428,6 +495,10 @@
       builder.addSources(makeArtifactLocation(sourceFile));
     }
 
+    if (packageManifest != null) {
+      builder.setPackageManifest(makeArtifactLocation(packageManifest));
+    }
+
     return builder.build();
   }
 
@@ -522,6 +593,17 @@
     }
   }
 
+  private static Collection<Artifact> getJavaSources(RuleContext ruleContext) {
+    Collection<Artifact> srcs = getSources(ruleContext);
+    List<Artifact> javaSrcs = Lists.newArrayList();
+    for (Artifact src : srcs) {
+      if (src.getRootRelativePathString().endsWith(".java")) {
+        javaSrcs.add(src);
+      }
+    }
+    return javaSrcs;
+  }
+
   private static Collection<Artifact> getSources(RuleContext ruleContext) {
     return ruleContext.attributes().has("srcs", BuildType.LABEL_LIST)
         ? ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list()
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
index 97221ab..e98f815 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
@@ -43,7 +43,7 @@
  * with advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
  */
 @Immutable @ThreadSafe
-public final class PathFragment implements Comparable<PathFragment>, Serializable {
+public final class  PathFragment implements Comparable<PathFragment>, Serializable {
 
   public static final int INVALID_SEGMENT = -1;
 
diff --git a/src/main/protobuf/BUILD b/src/main/protobuf/BUILD
index 6254b6a..7bff28a 100644
--- a/src/main/protobuf/BUILD
+++ b/src/main/protobuf/BUILD
@@ -10,6 +10,7 @@
     "crosstool_config",
     "extra_actions_base",
     "android_studio_ide_info",
+    "package_manifest",
     "test_status",
     "bundlemerge",
     "xcodegen",
diff --git a/src/main/protobuf/package_manifest.proto b/src/main/protobuf/package_manifest.proto
new file mode 100644
index 0000000..dd5f4fc
--- /dev/null
+++ b/src/main/protobuf/package_manifest.proto
@@ -0,0 +1,31 @@
+// Copyright 2015 The Bazel Authors. 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.
+
+syntax = "proto3";
+
+package blaze;
+
+
+option java_package = "com.google.devtools.build.lib.ideinfo.androidstudio";
+
+option java_generate_equals_and_hash = true;
+
+message JavaSourcePackage {
+  string absolute_path = 1;
+  string package_string = 2;
+}
+
+message PackageManifest {
+  repeated JavaSourcePackage sources = 1;
+}
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/BUILD b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
new file mode 100644
index 0000000..79f75dd
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -0,0 +1,14 @@
+java_test(
+    name = "PackageParserTest",
+    size = "small",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/protobuf:package_manifest_proto",
+        "//src/tools/android/java/com/google/devtools/build/android/ideinfo:package_parser_lib",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:protobuf",
+        "//third_party:truth",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java b/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java
new file mode 100644
index 0000000..a02ac30
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java
@@ -0,0 +1,192 @@
+// Copyright 2015 The Bazel Authors. 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.
+
+package com.google.devtools.build.android.ideinfo;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.protobuf.MessageLite;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Unit tests for {@link PackageParser}
+ */
+@RunWith(JUnit4.class)
+public class PackageParserTest {
+
+  private static class MockPackageParserIoProvider extends PackageParserIoProvider {
+    private final Map<Path, InputStream> sources = Maps.newHashMap();
+    private StringWriter writer = new StringWriter();
+
+    public MockPackageParserIoProvider addSource(String filePath, String javaSrc) {
+      try {
+        sources.put(Paths.get(filePath), new ByteArrayInputStream(javaSrc.getBytes("UTF-8")));
+      } catch (UnsupportedEncodingException e) {
+        fail(e.getMessage());
+      }
+      return this;
+    }
+
+    public void reset() {
+      sources.clear();
+      writer = new StringWriter();
+    }
+
+    public List<Path> getPaths() {
+      return Lists.newArrayList(sources.keySet());
+    }
+
+    @Nonnull
+    @Override
+    public BufferedReader getReader(Path file) throws IOException {
+      InputStream input = sources.get(file);
+      return new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public void writeProto(@Nonnull MessageLite message, @Nonnull Path file) throws IOException {
+      writer.write(message.toString());
+    }
+  }
+
+  private MockPackageParserIoProvider mockIoProvider;
+  private PackageParser parser;
+
+  @Before
+  public void setUp() {
+    mockIoProvider = new MockPackageParserIoProvider();
+    parser = new PackageParser(mockIoProvider);
+  }
+
+  private Map<Path, String> parsePackageStrings() throws Exception {
+    List<Path> paths = mockIoProvider.getPaths();
+    return parser.parsePackageStrings(paths, paths);
+  }
+
+  @Test
+  public void testParseCommandLineArguments() throws Exception {
+    String[] args = new String[] {
+        "--output_manifest",
+        "/tmp/out.manifest",
+        "--sources_absolute_paths",
+        "/path/test1.java:/path/test2.java",
+        "--sources_execution_paths",
+        "/path/test1.java:/path/test2.java"
+    };
+    PackageParser.PackageParserOptions options = PackageParser.parseArgs(args);
+    assertThat(options.outputManifest.toString()).isEqualTo("/tmp/out.manifest");
+    assertThat(options.sourcesAbsolutePaths).hasSize(2);
+    assertThat(options.sourcesExecutionPaths).hasSize(2);
+    assertThat(options.sourcesAbsolutePaths.get(0).toString()).isEqualTo("/path/test1.java");
+    assertThat(options.sourcesAbsolutePaths.get(1).toString()).isEqualTo("/path/test2.java");
+  }
+
+  @Test
+  public void testReadNoSources() throws Exception {
+    Map<Path, String> map = parsePackageStrings();
+    assertThat(map).isEmpty();
+  }
+
+  @Test
+  public void testSingleRead() throws Exception {
+    mockIoProvider
+        .addSource("java/com/google/Bla.java",
+            "package com.test;\n public class Bla {}\"");
+    Map<Path, String> map = parsePackageStrings();
+    assertThat(map).hasSize(1);
+    assertThat(map).containsEntry(Paths.get("java/com/google/Bla.java"), "com.test");
+  }
+
+  @Test
+  public void testMultiRead() throws Exception {
+    mockIoProvider
+        .addSource("java/com/google/Bla.java",
+            "package com.test;\n public class Bla {}\"")
+        .addSource("java/com/other/Foo.java",
+            "package com.other;\n public class Foo {}\"");
+    Map<Path, String> map = parsePackageStrings();
+    assertThat(map).hasSize(2);
+    assertThat(map).containsEntry(Paths.get("java/com/google/Bla.java"), "com.test");
+    assertThat(map).containsEntry(Paths.get("java/com/other/Foo.java"), "com.other");
+  }
+
+  @Test
+  public void testReadSomeInvalid() throws Exception {
+    mockIoProvider
+        .addSource("java/com/google/Bla.java",
+            "package %com.test;\n public class Bla {}\"")
+        .addSource("java/com/other/Foo.java",
+            "package com.other;\n public class Foo {}\"");
+    Map<Path, String> map = parsePackageStrings();
+    assertThat(map).hasSize(1);
+    assertThat(map).containsEntry(Paths.get("java/com/other/Foo.java"), "com.other");
+  }
+
+  @Test
+  public void testReadAllInvalid() throws Exception {
+    mockIoProvider
+        .addSource("java/com/google/Bla.java",
+            "#package com.test;\n public class Bla {}\"")
+        .addSource("java/com/other/Foo.java",
+            "package com.other\n public class Foo {}\"");
+    Map<Path, String> map = parsePackageStrings();
+    assertThat(map).isEmpty();
+  }
+
+  @Test
+  public void testWriteEmptyMap() throws Exception {
+    parser.writeManifest(
+        Maps.<Path, String> newHashMap(), Paths.get("/java/com/google/test.manifest"));
+    assertThat(mockIoProvider.writer.toString()).isEmpty();
+  }
+
+  @Test
+  public void testWriteMap() throws Exception {
+    Map<Path, String> map = ImmutableMap.of(
+        Paths.get("/java/com/google/Bla.java"), "com.google",
+        Paths.get("/java/com/other/Foo.java"), "com.other"
+    );
+    parser.writeManifest(map, Paths.get("/java/com/google/test.manifest"));
+
+    String writtenString = mockIoProvider.writer.toString();
+    assertThat(writtenString).contains("absolute_path: \"/java/com/google/Bla.java\"");
+    assertThat(writtenString).contains("package_string: \"com.google\"");
+    assertThat(writtenString).contains("absolute_path: \"/java/com/other/Foo.java\"");
+    assertThat(writtenString).contains("package_string: \"com.other\"");
+  }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 707b33d..b02e782 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -600,6 +600,7 @@
         "//src/main/java/com/google/devtools/build/lib:bazel-rules",
         "//src/main/java/com/google/devtools/build/lib:build-base",
         "//src/main/java/com/google/devtools/build/lib:collect",
+        "//src/main/java/com/google/devtools/build/lib:common",
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:ideinfo",
         "//src/main/java/com/google/devtools/build/lib:packages",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index 65f4c90..619ba2f 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -181,7 +181,12 @@
         .add("sh_binary(name = 'shuffle_jars', srcs = ['empty.sh'])")
         .add("sh_binary(name = 'strip_resources', srcs = ['empty.sh'])")
         .add("sh_binary(name = 'build_incremental_dexmanifest', srcs = ['empty.sh'])")
-        .add("sh_binary(name = 'incremental_install', srcs = ['empty.sh'])");
+        .add("sh_binary(name = 'incremental_install', srcs = ['empty.sh'])")
+        .add("java_binary(name = 'PackageParser',")
+        .add("          runtime_deps = [ ':PackageParser_import'],")
+        .add("          main_class = 'com.google.devtools.build.android.ideinfo.PackageParser')")
+        .add("java_import(name = 'PackageParser_import',")
+        .add("          jars = [ 'package_parser_deploy.jar' ])");
 
     for (Attribute attr : attrs) {
       if (attr.getType() == LABEL && attr.isMandatory() && !attr.getName().startsWith(":")) {
diff --git a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java
index bb8de48..27ec6a6 100644
--- a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java
+++ b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java
@@ -57,7 +57,24 @@
         "com/google/example/libsimple-src.jar"
     );
   }
-
+  
+  public void testPackageManifestCreated() throws Exception {
+    scratch.file(
+        "com/google/example/BUILD",
+        "java_library(",
+        "    name = 'simple',",
+        "    srcs = ['simple/Simple.java']",
+        ")");
+    Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:simple");
+    assertThat(ruleIdeInfos.size()).isEqualTo(1);
+    RuleIdeInfo ruleIdeInfo = getRuleInfoAndVerifyLabel(
+        "//com/google/example:simple", ruleIdeInfos);
+    
+    ArtifactLocation packageManifest = ruleIdeInfo.getJavaRuleIdeInfo().getPackageManifest();
+    assertNotNull(packageManifest);
+    assertEquals(packageManifest.getRelativePath(), "com/google/example/simple.manifest");
+  }
+  
   public void testJavaLibraryProtoWithDependencies() throws Exception {
     scratch.file(
         "com/google/example/BUILD",
@@ -83,7 +100,7 @@
     assertThat(complexRuleIdeInfo.getDependenciesList())
         .containsExactly("//com/google/example:simple");
   }
-
+  
   public void testJavaLibraryWithTransitiveDependencies() throws Exception {
     scratch.file(
         "com/google/example/BUILD",
@@ -320,6 +337,33 @@
         .containsExactly("//com/google/example:foobar", "//com/google/example:imp")
         .inOrder();
   }
+  
+  public void testNoPackageManifestForExports() throws Exception {
+    scratch.file(
+        "com/google/example/BUILD",
+        "java_library(",
+        "   name = 'foobar',",
+        "   srcs = ['FooBar.java'],",
+        ")",
+        "java_import(",
+        "   name = 'imp',",
+        "   jars = ['a.jar', 'b.jar'],",
+        "   deps = [':foobar'],",
+        "   exports = [':foobar'],",
+        ")",
+        "java_library(",
+        "   name = 'lib',",
+        "   srcs = ['Lib.java'],",
+        "   deps = [':imp'],",
+        ")");
+    
+    Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:lib");
+    RuleIdeInfo libInfo = getRuleInfoAndVerifyLabel("//com/google/example:lib", ruleIdeInfos);
+    RuleIdeInfo impInfo = getRuleInfoAndVerifyLabel("//com/google/example:imp", ruleIdeInfos);
+   
+    assertThat(!impInfo.getJavaRuleIdeInfo().hasPackageManifest()).isTrue();
+    assertThat(libInfo.getJavaRuleIdeInfo().hasPackageManifest()).isTrue();
+  }
 
   public void testGeneratedJavaImportFilesAreAddedToOutputGroup() throws Exception {
     scratch.file(
diff --git a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java
index 4a26cc9..cb3ca8b 100644
--- a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java
@@ -21,11 +21,13 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult;
 import com.google.devtools.build.lib.analysis.ConfiguredAspect;
 import com.google.devtools.build.lib.analysis.OutputGroupProvider;
 import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo.ArtifactLocation;
@@ -136,14 +138,24 @@
     Iterable<Artifact> artifacts = provider.getIdeInfoFiles();
     ImmutableMap.Builder<String, RuleIdeInfo> builder = ImmutableMap.builder();
     for (Artifact artifact : artifacts) {
-      BinaryFileWriteAction generatingAction =
-          (BinaryFileWriteAction)  getGeneratingAction(artifact);
-      RuleIdeInfo ruleIdeInfo = RuleIdeInfo.parseFrom(generatingAction.getSource().openStream());
-      builder.put(ruleIdeInfo.getLabel(), ruleIdeInfo);
+      Action generatingAction = getGeneratingAction(artifact);
+      if (generatingAction instanceof BinaryFileWriteAction) {
+        BinaryFileWriteAction writeAction = (BinaryFileWriteAction) generatingAction;
+        RuleIdeInfo ruleIdeInfo = RuleIdeInfo.parseFrom(writeAction.getSource().openStream());
+        builder.put(ruleIdeInfo.getLabel(), ruleIdeInfo);
+      } else { 
+        verifyPackageManifestSpawnAction(generatingAction);
+      }
     }
     return builder.build();
   }
-
+  
+  protected final void verifyPackageManifestSpawnAction(Action genAction) {
+    assertEquals(genAction.getMnemonic(), "JavaPackageManifest");
+    SpawnAction action = (SpawnAction) genAction;
+    assertFalse(action.isShellCommand());
+  }
+  
   protected List<String> getOutputGroupResult(String outputGroup) {
     OutputGroupProvider outputGroupProvider =
         this.configuredAspect.getProvider(OutputGroupProvider.class);
diff --git a/src/test/shell/bazel/test-setup.sh b/src/test/shell/bazel/test-setup.sh
index d5393e7..bf80071 100755
--- a/src/test/shell/bazel/test-setup.sh
+++ b/src/test/shell/bazel/test-setup.sh
@@ -120,6 +120,23 @@
     data = ["//src/tools/android/java/com/google/devtools/build/android/idlclass:IdlClass"],
 )
 
+filegroup(
+    name = "package_parser",
+    srcs = ["//src/tools/android/java/com/google/devtools/build/android/ideinfo:PackageParser_deploy.jar"],
+)
+
+java_binary(
+    name = "PackageParser",
+    main_class = "com.google.devtools.build.android.ideinfo.PackageParser",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":package_parser_import"],
+)
+
+java_import(
+    name = "package_parser_import",
+    jars = [":package_parser"],
+)
+
 sh_binary(
     name = "merge_manifests",
     srcs = ["fail.sh"],
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD
index 74c1255..9abd062 100644
--- a/src/tools/android/java/com/google/devtools/build/android/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -37,10 +37,12 @@
     srcs = glob(["*.java"]),
     deps = [
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:package_manifest_proto",
         "//third_party:android_common",
         "//third_party:apache_commons_compress",
         "//third_party:asm",
         "//third_party:guava",
         "//third_party:jsr305",
+        "//third_party:protobuf",
     ],
 )
diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java
index 8a10c52..f69aac1 100644
--- a/src/tools/android/java/com/google/devtools/build/android/Converters.java
+++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java
@@ -26,6 +26,8 @@
 import java.nio.file.Files;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -154,4 +156,30 @@
       super(VariantConfiguration.Type.class, "variant configuration type");
     }
   }
+
+  /**
+   * Validating converter for a list of Paths.
+   * A Path is considered valid if it resolves to a file.
+   */
+  public static class PathListConverter implements Converter<List<Path>> {
+
+    final PathConverter baseConverter = new PathConverter();
+
+    @Override
+    public List<Path> convert(String input) throws OptionsParsingException {
+      List<Path> list = new ArrayList<>();
+      for (String piece : input.split(":")) {
+        if (!piece.isEmpty()) {
+          list.add(baseConverter.convert(piece));
+        }
+      }
+      return Collections.unmodifiableList(list);
+    }
+
+    @Override
+    public String getTypeDescription() {
+      return "a colon-separated list of paths";
+    }
+  }
+
 }
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD
new file mode 100644
index 0000000..b520a81
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -0,0 +1,38 @@
+filegroup(
+    name = "embedded_tools",
+    srcs = [
+        "BUILD.tools",
+        "classes_deploy.jar",
+    ],
+    visibility = ["//src:__pkg__"],
+)
+
+java_binary(
+    name = "classes",
+    main_class = "does.not.exist",
+    runtime_deps = [":package_parser_lib"],
+)
+
+java_binary(
+    name = "PackageParser",
+    main_class = "com.google.devtools.build.android.ideinfo.PackageParser",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":package_parser_lib"],
+)
+
+java_library(
+    name = "package_parser_lib",
+    srcs = glob(["*.java"]),
+    visibility = [
+        "//devtools/blaze/integration:__pkg__",
+        "//src/test/java/com/google/devtools/build/android/ideinfo:__pkg__",
+    ],
+    deps = [
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:package_manifest_proto",
+        "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:protobuf",
+    ],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools
new file mode 100644
index 0000000..efa64fb
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools
@@ -0,0 +1,12 @@
+package(default_visibility = ["//visibility:public"])
+
+java_import(
+    name = "classes",
+    jars = [":classes_deploy.jar"],
+)
+
+java_binary(
+    name = "PackageParser",
+    main_class = "com.google.devtools.build.android.ideinfo.PackageParser",
+    runtime_deps = [":classes"],
+)
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java
new file mode 100644
index 0000000..5e84d3c
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java
@@ -0,0 +1,206 @@
+// Copyright 2015 The Bazel Authors. 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.
+
+package com.google.devtools.build.android.ideinfo;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.devtools.build.android.Converters.PathConverter;
+import com.google.devtools.build.android.Converters.PathListConverter;
+import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
+import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Parses the package string from each of the source .java files
+ */
+public class PackageParser {
+
+  /** The options for a {@PackageParser} action. */
+  public static final class PackageParserOptions extends OptionsBase {
+    @Option(name = "sources_absolute_paths",
+        defaultValue = "null",
+        converter = PathListConverter.class,
+        category = "input",
+        help = "The absolute paths of the java source files. The expected format is a "
+               + "colon-separated list.")
+    public List<Path> sourcesAbsolutePaths;
+
+    @Option(name = "sources_execution_paths",
+        defaultValue = "null",
+        converter = PathListConverter.class,
+        category = "input",
+        help = "The execution paths of the java source files. The expected format is a "
+            + "colon-separated list.")
+    public List<Path> sourcesExecutionPaths;
+
+    @Option(name = "output_manifest",
+        defaultValue = "null",
+        converter = PathConverter.class,
+        category = "output",
+        help = "The path to the manifest file this parser writes to.")
+    public Path outputManifest;
+  }
+
+  private static final Logger logger = Logger.getLogger(PackageParser.class.getName());
+
+  private static final Pattern JAVA_PACKAGE_PATTERN =
+      Pattern.compile("^\\s*package\\s+([\\w\\.]+);");
+
+  public static void main(String[] args) throws Exception {
+    PackageParserOptions options = parseArgs(args);
+    Preconditions.checkNotNull(options.sourcesAbsolutePaths);
+    Preconditions.checkNotNull(options.sourcesExecutionPaths);
+    Preconditions.checkState(
+        options.sourcesAbsolutePaths.size() == options.sourcesExecutionPaths.size());
+    Preconditions.checkNotNull(options.outputManifest);
+
+    try {
+      PackageParser parser = new PackageParser(PackageParserIoProvider.INSTANCE);
+      Map<Path, String> outputMap = parser.parsePackageStrings(options.sourcesAbsolutePaths,
+          options.sourcesExecutionPaths);
+      parser.writeManifest(outputMap, options.outputManifest);
+    } catch (Throwable e) {
+      logger.log(Level.SEVERE, "Error parsing package strings", e);
+      System.exit(1);
+    }
+    System.exit(0);
+  }
+
+  @VisibleForTesting
+  public static PackageParserOptions parseArgs(String[] args) {
+    args = parseParamFileIfUsed(args);
+    OptionsParser optionsParser = OptionsParser.newOptionsParser(PackageParserOptions.class);
+    optionsParser.parseAndExitUponError(args);
+    return optionsParser.getOptions(PackageParserOptions.class);
+  }
+
+  private static String[] parseParamFileIfUsed(@Nonnull String[] args) {
+    if (args.length != 1 || !args[0].startsWith("@")) {
+      return args;
+    }
+    File paramFile = new File(args[0].substring(1));
+    try {
+      return Files.readLines(paramFile, StandardCharsets.UTF_8).toArray(new String[0]);
+    } catch (IOException e) {
+      throw new RuntimeException("Error parsing param file: " + args[0], e);
+    }
+  }
+
+  private final PackageParserIoProvider ioProvider;
+
+  @VisibleForTesting
+  public PackageParser(@Nonnull PackageParserIoProvider ioProvider) {
+    this.ioProvider = ioProvider;
+  }
+
+  @VisibleForTesting
+  public void writeManifest(@Nonnull Map<Path, String> sourceToPackageMap, Path outputFile)
+      throws IOException {
+    if (sourceToPackageMap.isEmpty()) {
+      return;
+    }
+    PackageManifest.Builder builder = PackageManifest.newBuilder();
+    for (Entry<Path, String> entry : sourceToPackageMap.entrySet()) {
+      builder.addSources(JavaSourcePackage.newBuilder()
+          .setAbsolutePath(entry.getKey().toAbsolutePath().toString())
+          .setPackageString(entry.getValue()));
+    }
+
+    try {
+      ioProvider.writeProto(builder.build(), outputFile);
+    } catch (IOException e) {
+      logger.log(Level.SEVERE, "Error writing package manifest", e);
+      throw e;
+    }
+  }
+
+  @Nonnull
+  @VisibleForTesting
+  public Map<Path, String> parsePackageStrings(@Nonnull List<Path> absolutePaths,
+      @Nonnull List<Path> executionPaths) throws Exception {
+
+    ListeningExecutorService executorService = MoreExecutors.listeningDecorator(
+        Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));
+
+    Map<Path, ListenableFuture<String>> futures = Maps.newHashMap();
+    for (int i = 0; i < absolutePaths.size(); i++) {
+      final Path source = executionPaths.get(i);
+      futures.put(absolutePaths.get(i), executorService.submit(new Callable<String>() {
+        @Override
+        public String call() throws Exception {
+          return getDeclaredPackageOfJavaFile(source);
+        }
+      }));
+    }
+    Map<Path, String> map = Maps.newHashMap();
+    for (Entry<Path, ListenableFuture<String>> entry : futures.entrySet()) {
+      String value = entry.getValue().get();
+      if (value != null) {
+        map.put(entry.getKey(), value);
+      }
+    }
+    return map;
+  }
+
+  @Nullable
+  private String getDeclaredPackageOfJavaFile(@Nonnull Path source) {
+    try (BufferedReader reader = ioProvider.getReader(source)) {
+      return parseDeclaredPackage(reader);
+
+    } catch (IOException e) {
+      logger.log(Level.WARNING, "Error parsing package string from java source: " + source, e);
+      return null;
+    }
+  }
+
+  @VisibleForTesting
+  @Nullable
+  public static String parseDeclaredPackage(@Nonnull BufferedReader reader) throws IOException {
+    String line;
+    while ((line = reader.readLine()) != null) {
+      Matcher packageMatch = JAVA_PACKAGE_PATTERN.matcher(line);
+      if (packageMatch.find()) {
+        return packageMatch.group(1);
+      }
+    }
+    return null;
+  }
+
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java
new file mode 100644
index 0000000..be555c8
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java
@@ -0,0 +1,49 @@
+// Copyright 2015 The Bazel Authors. 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.
+
+package com.google.devtools.build.android.ideinfo;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.MessageLite;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Provides a BufferedReader for the source java files,
+ * and a writer for the output proto
+ */
+@VisibleForTesting
+public class PackageParserIoProvider {
+
+  public static final PackageParserIoProvider INSTANCE = new PackageParserIoProvider();
+
+  public void writeProto(@Nonnull MessageLite message, @Nonnull Path file) throws IOException {
+    try (OutputStream out = Files.newOutputStream(file)) {
+      message.writeTo(out);
+    }
+  }
+
+  @Nonnull
+  public BufferedReader getReader(Path file) throws IOException {
+    return Files.newBufferedReader(file, StandardCharsets.UTF_8);
+  }
+
+}