Adds a jar filter to the IntelliJ IDE aspect.

Any java rule that mixes generated and non-generated sources
will produce a filtered jar containing only the generated
output.

For a java rule with only "normal" sources or only generated
source files, no filtered jar is produced.

This will allow the IDE to resolve those generated sources.

RELNOTES:None

--
MOS_MIGRATED_REVID=132113568
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 bbd9624..42d132f 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
@@ -165,7 +165,10 @@
         .attributeAspect("resources", this)
         .add(attr("$packageParser", LABEL).cfg(HOST).exec()
             .value(Label.parseAbsoluteUnchecked(
-                toolsRepository + "//tools/android:PackageParser")));
+                toolsRepository + "//tools/android:PackageParser")))
+        .add(attr("$jarFilter", LABEL).cfg(HOST).exec()
+            .value(Label.parseAbsoluteUnchecked(
+                toolsRepository + "//tools/android:JarFilter")));
 
     for (PrerequisiteAttr prerequisiteAttr : prerequisiteAttrs) {
       builder.attributeAspect(prerequisiteAttr.name, this);
@@ -339,17 +342,8 @@
     JavaRuleOutputJarsProvider outputJarsProvider =
         base.getProvider(JavaRuleOutputJarsProvider.class);
     if (outputJarsProvider != null && !androidStudioInfoSemantics.suppressJavaRuleInfo(base)) {
-      Artifact packageManifest = createPackageManifest(base, ruleContext);
-      if (packageManifest != null) {
-        providerBuilder.ideInfoFilesBuilder().add(packageManifest);
-        ruleContext.registerAction(
-            makePackageManifestAction(ruleContext,
-                packageManifest,
-                getJavaSourceForPackageManifest(ruleContext)));
-      }
-
       JavaRuleIdeInfo javaRuleIdeInfo = makeJavaRuleIdeInfo(base, ruleContext,
-          outputJarsProvider, ideResolveArtifacts, packageManifest);
+          outputJarsProvider, providerBuilder);
       outputBuilder.setJavaRuleIdeInfo(javaRuleIdeInfo);
     }
 
@@ -434,15 +428,6 @@
     return androidStudioInfoSemantics.checkForAdditionalCppRules(ruleClass);
   }
 
-  @Nullable private static Artifact createPackageManifest(ConfiguredTarget base,
-      RuleContext ruleContext) {
-    Collection<Artifact> sourceFiles = getJavaSourceForPackageManifest(ruleContext);
-    if (sourceFiles.isEmpty()) {
-      return null;
-    }
-    return derivedArtifact(base, ruleContext, ".manifest");
-  }
-
   private static Action[] makePackageManifestAction(
       RuleContext ruleContext,
       Artifact packageManifest,
@@ -481,18 +466,40 @@
         }
       };
 
+  private static Action[] makeFilteredJarAction(
+      RuleContext ruleContext,
+      List<Artifact> jars,
+      Artifact generatedPackageManifest,
+      Artifact output) {
+
+    return new SpawnAction.Builder()
+        .addInputs(jars)
+        .addInput(generatedPackageManifest)
+        .addOutput(output)
+        .setExecutable(ruleContext.getExecutablePrerequisite("$jarFilter", Mode.HOST))
+        .setCommandLine(CustomCommandLine.builder()
+            .addExecPaths("--jars", jars)
+            .addExecPath("--manifest", generatedPackageManifest)
+            .addExecPath("--output", output)
+            .build())
+        .useParameterFile(ParameterFileType.SHELL_QUOTED)
+        .setProgressMessage("Filtering generated code for " + ruleContext.getRule())
+        .setMnemonic("JarFilter")
+        .build(ruleContext);
+  }
+
   private static Artifact derivedArtifact(ConfiguredTarget base, RuleContext ruleContext,
       String suffix) {
     BuildConfiguration configuration = ruleContext.getConfiguration();
     assert configuration != null;
-    Root genfilesDirectory = configuration.getGenfilesDirectory(
+    Root binDirectory = configuration.getBinDirectory(
         ruleContext.getRule().getRepository());
 
     PathFragment derivedFilePath =
         getOutputFilePath(base, ruleContext, suffix);
 
     return ruleContext.getAnalysisEnvironment().getDerivedArtifact(
-        derivedFilePath, genfilesDirectory);
+        derivedFilePath, binDirectory);
   }
 
   private static AndroidRuleIdeInfo makeAndroidRuleIdeInfo(
@@ -610,9 +617,46 @@
       ConfiguredTarget base,
       RuleContext ruleContext,
       JavaRuleOutputJarsProvider outputJarsProvider,
-      NestedSetBuilder<Artifact> ideResolveArtifacts,
-      @Nullable Artifact packageManifest) {
+      AndroidStudioInfoFilesProvider.Builder providerBuilder) {
+    NestedSetBuilder<Artifact> ideResolveArtifacts = providerBuilder.ideResolveFilesBuilder();
     JavaRuleIdeInfo.Builder builder = JavaRuleIdeInfo.newBuilder();
+
+    List<Artifact> javaSources = Lists.newArrayList();
+    List<Artifact> generatedJavaSources = Lists.newArrayList();
+    getJavaSourcesForPackageManifest(ruleContext, javaSources, generatedJavaSources);
+
+    if (!javaSources.isEmpty()) {
+      Artifact packageManifest = derivedArtifact(base, ruleContext, ".manifest");
+      providerBuilder.ideInfoFilesBuilder().add(packageManifest);
+      ruleContext.registerAction(makePackageManifestAction(
+          ruleContext, packageManifest, javaSources));
+      builder.setPackageManifest(makeArtifactLocation(packageManifest));
+    }
+
+    if (!javaSources.isEmpty() && !generatedJavaSources.isEmpty()) {
+      Artifact generatedPackageManifest = derivedArtifact(
+          base, ruleContext, "-filtered-gen.manifest");
+      ruleContext.registerAction(makePackageManifestAction(ruleContext,
+          generatedPackageManifest, generatedJavaSources));
+      Artifact filteredGenJar = derivedArtifact(base, ruleContext, "-filtered-gen.jar");
+      List<Artifact> jars = Lists.newArrayList();
+      for (OutputJar outputJar : outputJarsProvider.getOutputJars()) {
+        Artifact jar = outputJar.getIJar();
+        if (jar == null) {
+          jar = outputJar.getClassJar();
+        }
+        if (jar == null) {
+          continue;
+        }
+        jars.add(jar);
+      }
+      ruleContext.registerAction(makeFilteredJarAction(ruleContext,
+          jars, generatedPackageManifest, filteredGenJar));
+      ideResolveArtifacts.add(filteredGenJar);
+      builder.setFilteredGenJar(makeLibraryArtifact(
+          ideResolveArtifacts, filteredGenJar, null, null));
+    }
+
     collectJarsFromOutputJarsProvider(builder, ideResolveArtifacts, outputJarsProvider);
 
     Artifact jdeps = outputJarsProvider.getJdeps();
@@ -632,10 +676,6 @@
       builder.addSources(makeArtifactLocation(sourceFile));
     }
 
-    if (packageManifest != null) {
-      builder.setPackageManifest(makeArtifactLocation(packageManifest));
-    }
-
     return builder.build();
   }
 
@@ -784,15 +824,18 @@
     }
   }
 
-  private static Collection<Artifact> getJavaSourceForPackageManifest(RuleContext ruleContext) {
+  private static void getJavaSourcesForPackageManifest(RuleContext ruleContext,
+      List<Artifact> javaSources, List<Artifact> generatedSources) {
     Collection<Artifact> srcs = getSources(ruleContext);
-    List<Artifact> javaSrcs = Lists.newArrayList();
     for (Artifact src : srcs) {
-      if (src.isSourceArtifact() && src.getRootRelativePathString().endsWith(".java")) {
-        javaSrcs.add(src);
+      if (src.getRootRelativePathString().endsWith(".java")) {
+        if (src.isSourceArtifact()) {
+          javaSources.add(src);
+        } else {
+          generatedSources.add(src);
+        }
       }
     }
-    return javaSrcs;
   }
 
   private static Collection<Artifact> getSources(RuleContext ruleContext) {
diff --git a/src/main/protobuf/android_studio_ide_info.proto b/src/main/protobuf/android_studio_ide_info.proto
index 07c86a5..fd5aa89 100644
--- a/src/main/protobuf/android_studio_ide_info.proto
+++ b/src/main/protobuf/android_studio_ide_info.proto
@@ -43,6 +43,7 @@
   ArtifactLocation package_manifest = 3;
   repeated ArtifactLocation sources = 4;
   ArtifactLocation jdeps = 5;
+  LibraryArtifact filtered_gen_jar = 6;
 }
 
 message CRuleIdeInfo {
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/BUILD b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
index 9de5aa7..0f5fe19 100644
--- a/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
+++ b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -5,6 +5,22 @@
 )
 
 java_test(
+    name = "JarFilterTest",
+    size = "small",
+    srcs = ["JarFilterTest.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:package_manifest_java_proto",
+        "//src/tools/android/java/com/google/devtools/build/android/ideinfo:jar_filter_lib",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party:junit4",
+        "//third_party:truth",
+        "//third_party/protobuf",
+    ],
+)
+
+java_test(
     name = "PackageParserTest",
     size = "small",
     srcs = ["PackageParserTest.java"],
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/JarFilterTest.java b/src/test/java/com/google/devtools/build/android/ideinfo/JarFilterTest.java
new file mode 100644
index 0000000..eedfb9f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/ideinfo/JarFilterTest.java
@@ -0,0 +1,94 @@
+// 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 com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.ArtifactLocation;
+import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
+import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link JarFilter}
+ */
+@RunWith(JUnit4.class)
+public class JarFilterTest {
+
+  @Test
+  public void testParseCommandLineArguments() throws Exception {
+    String[] args = new String[]{
+        "--jars",
+        "/tmp/1.jar:/tmp/2.jar",
+        "--output",
+        "/tmp/out.jar",
+        "--manifest",
+        "/tmp/manifest.file",
+    };
+    JarFilter.JarFilterOptions options = JarFilter.parseArgs(args);
+    assertThat(options.jars).containsExactly(
+        Paths.get("/tmp/1.jar"),
+        Paths.get("/tmp/2.jar")
+    );
+    assertThat(options.output.toString()).isEqualTo(Paths.get("/tmp/out.jar").toString());
+    assertThat(options.manifest.toString()).isEqualTo(Paths.get("/tmp/manifest.file").toString());
+  }
+
+  @Test
+  public void testFilterMethod() throws Exception {
+    List<String> prefixes = ImmutableList.of(
+        "com/google/foo/Foo",
+        "com/google/bar/Bar",
+        "com/google/baz/Baz"
+    );
+    assertThat(JarFilter.shouldKeep(prefixes, "com/google/foo/Foo.class")).isTrue();
+    assertThat(JarFilter.shouldKeep(prefixes, "com/google/foo/Foo$Inner.class")).isTrue();
+    assertThat(JarFilter.shouldKeep(prefixes, "com/google/bar/Bar.class")).isTrue();
+    assertThat(JarFilter.shouldKeep(prefixes, "com/google/foo/Foo/NotFoo.class")).isFalse();
+    assertThat(JarFilter.shouldKeep(prefixes, "wrong/com/google/foo/Foo.class")).isFalse();
+  }
+
+  @Test
+  public void testManifestParser() throws Exception {
+    PackageManifest packageManifest = PackageManifest.newBuilder()
+        .addSources(JavaSourcePackage.newBuilder()
+            .setArtifactLocation(ArtifactLocation.newBuilder()
+                .setIsSource(true)
+                .setRelativePath("com/google/foo/Foo.java"))
+            .setPackageString("com.google.foo"))
+        .addSources(JavaSourcePackage.newBuilder()
+            .setArtifactLocation(ArtifactLocation.newBuilder()
+                .setIsSource(true)
+                .setRelativePath("com/google/bar/Bar.java"))
+            .setPackageString("com.google.bar"))
+        .addSources(JavaSourcePackage.newBuilder()
+            .setArtifactLocation(ArtifactLocation.newBuilder()
+                .setIsSource(true)
+                .setRelativePath("some/path/Test.java"))
+            .setPackageString("com.google.test"))
+        .build();
+    assertThat(JarFilter.parsePackageManifest(packageManifest)).containsExactly(
+        "com/google/foo/Foo",
+        "com/google/bar/Bar",
+        "com/google/test/Test"
+    );
+  }
+}
+
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 5b47d44..0a7452d 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
@@ -183,6 +183,11 @@
         .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("java_binary(name = 'JarFilter',")
+        .add("          runtime_deps = [ ':JarFilter_import'],")
+        .add("          main_class = 'com.google.devtools.build.android.ideinfo.JarFilter')")
+        .add("java_import(name = 'JarFilter_import',")
+        .add("          jars = [ 'jar_filter_deploy.jar' ])")
         .add("java_binary(name = 'PackageParser',")
         .add("          runtime_deps = [ ':PackageParser_import'],")
         .add("          main_class = 'com.google.devtools.build.android.ideinfo.PackageParser')")
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 d06352e..0a375a2 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
@@ -118,6 +118,59 @@
   }
 
   @Test
+  public void testFilteredGenJarNotCreatedForSourceOnlyRule() throws Exception {
+    scratch.file(
+        "com/google/example/BUILD",
+        "java_library(",
+        "    name = 'simple',",
+        "    srcs = ['Test.java']",
+        ")");
+    Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:simple");
+    RuleIdeInfo ruleIdeInfo = getRuleInfoAndVerifyLabel(
+        "//com/google/example:simple", ruleIdeInfos);
+    assertThat(ruleIdeInfo.getJavaRuleIdeInfo().hasFilteredGenJar()).isFalse();
+  }
+
+  @Test
+  public void testFilteredGenJarNotCreatedForOnlyGenRule() throws Exception {
+    scratch.file(
+        "com/google/example/BUILD",
+        "genrule(",
+        "   name = 'gen_sources',",
+        "   outs = ['Gen.java'],",
+        "   cmd = '',",
+        ")",
+        "java_library(",
+        "    name = 'simple',",
+        "    srcs = [':gen_sources']",
+        ")");
+    Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:simple");
+    RuleIdeInfo ruleIdeInfo = getRuleInfoAndVerifyLabel(
+        "//com/google/example:simple", ruleIdeInfos);
+    assertThat(ruleIdeInfo.getJavaRuleIdeInfo().hasFilteredGenJar()).isFalse();
+  }
+
+  @Test
+  public void testFilteredGenJarIsCreatedForMixedGenAndSourcesRule() throws Exception {
+    scratch.file(
+        "com/google/example/BUILD",
+        "genrule(",
+        "   name = 'gen_sources',",
+        "   outs = ['Gen.java'],",
+        "   cmd = '',",
+        ")",
+        "java_library(",
+        "    name = 'simple',",
+        "    srcs = [':gen_sources', 'Test.java']",
+        ")");
+    Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:simple");
+    RuleIdeInfo ruleIdeInfo = getRuleInfoAndVerifyLabel(
+        "//com/google/example:simple", ruleIdeInfos);
+    assertThat(ruleIdeInfo.getJavaRuleIdeInfo().getFilteredGenJar().getJar().getRelativePath())
+        .isEqualTo("com/google/example/simple-filtered-gen.jar");
+  }
+
+  @Test
   public void testJavaLibraryWithDependencies() throws Exception {
     scratch.file(
         "com/google/example/BUILD",
diff --git a/src/test/java/com/google/devtools/build/lib/ideinfo/intellij_info.bzl b/src/test/java/com/google/devtools/build/lib/ideinfo/intellij_info.bzl
index 1ef6df9..79d1ae7 100644
--- a/src/test/java/com/google/devtools/build/lib/ideinfo/intellij_info.bzl
+++ b/src/test/java/com/google/devtools/build/lib/ideinfo/intellij_info.bzl
@@ -270,6 +270,7 @@
   if not hasattr(target, "java") or ctx.rule.kind == "proto_library":
     return (None, set(), set())
 
+  ide_info_files = set()
   sources = sources_from_rule(ctx)
 
   jars = [library_artifact(output) for output in target.java.outputs.jars]
@@ -287,8 +288,37 @@
 
   jdeps = artifact_location(target.java.outputs.jdeps)
 
-  package_manifest = build_java_package_manifest(target, ctx)
-  ide_info_files = set([package_manifest]) if package_manifest else set()
+  java_sources, gen_java_sources = java_sources_for_package_manifest(ctx)
+
+  package_manifest = None
+  if java_sources:
+    package_manifest = build_java_package_manifest(ctx, target, java_sources, ".manifest")
+    ide_info_files = ide_info_files | set([package_manifest])
+
+  filtered_gen_jar = None
+  if java_sources and gen_java_sources:
+    gen_package_manifest = build_java_package_manifest(
+        ctx,
+        target,
+        gen_java_sources,
+        "-filtered-gen.manifest"
+    )
+    jar_artifacts = []
+    for jar in target.java.outputs.jars:
+      if jar.ijar:
+        jar_artifacts.append(jar.ijar)
+      elif jar.class_jar:
+        jar_artifacts.append(jar.class_jar)
+    filtered_gen_jar_artifact = build_filtered_gen_jar(
+        ctx,
+        target,
+        jar_artifacts,
+        gen_package_manifest
+    )
+    ide_resolve_files = ide_resolve_files | set([filtered_gen_jar_artifact])
+    filtered_gen_jar = struct(
+        jar=artifact_location(filtered_gen_jar_artifact),
+    )
 
   java_rule_ide_info = struct_omit_none(
       sources = sources,
@@ -296,23 +326,20 @@
       jdeps = jdeps,
       generated_jars = gen_jars,
       package_manifest = artifact_location(package_manifest),
+      filtered_gen_jar = filtered_gen_jar,
   )
   return (java_rule_ide_info, ide_info_files, ide_resolve_files)
 
-def build_java_package_manifest(target, ctx):
-  """Builds a java package manifest and returns the output file."""
-  source_files = java_sources_for_package_manifest(ctx)
-  if not source_files:
-    return None
-
-  output = ctx.new_file(target.label.name + ".manifest")
+def build_java_package_manifest(ctx, target, source_files, suffix):
+  """Builds the java package manifest for the given source files."""
+  output = ctx.new_file(target.label.name + suffix)
 
   args = []
   args += ["--output_manifest", output.path]
   args += ["--sources"]
-  args += [":".join([f.root.path + "," + f.path for f in source_files])]
+  args += [":".join([f.root.path + "," + f.short_path for f in source_files])]
   argfile = ctx.new_file(ctx.configuration.bin_dir,
-                         target.label.name + ".manifest.params")
+                         target.label.name + suffix + ".params")
   ctx.file_action(output=argfile, content="\n".join(args))
 
   ctx.action(
@@ -325,15 +352,34 @@
   )
   return output
 
+def build_filtered_gen_jar(ctx, target, jars, manifest):
+  """Filters the passed jar to contain only classes from the given manifest."""
+  output = ctx.new_file(target.label.name + "-filtered-gen.jar")
+  args = []
+  args += ["--jars"]
+  args += [":".join([jar.path for jar in jars])]
+  args += ["--manifest", manifest.path]
+  args += ["--output", output.path]
+  ctx.action(
+      inputs = jars + [manifest],
+      outputs = [output],
+      executable = ctx.executable._jar_filter,
+      arguments = args,
+      mnemonic = "JarFilter",
+      progress_message = "Filtering generated code for " + str(target.label),
+  )
+  return output
+
 def java_sources_for_package_manifest(ctx):
   """Get the list of non-generated java sources to go in the package manifest."""
 
   if hasattr(ctx.rule.attr, "srcs"):
-    return [f
-            for src in ctx.rule.attr.srcs
-            for f in src.files
-            if f.is_source and f.basename.endswith(".java")]
-  return []
+    srcs = ctx.rule.attr.srcs
+    all_java_sources = [f for src in srcs for f in src.files if f.basename.endswith(".java")]
+    java_sources = [f for f in all_java_sources if f.is_source]
+    gen_java_sources = [f for f in all_java_sources if not f.is_source]
+    return java_sources, gen_java_sources
+  return [], []
 
 def build_android_rule_ide_info(target, ctx, legacy_resource_label):
   """Build AndroidRuleIdeInfo.
@@ -520,6 +566,11 @@
               cfg = HOST_CFG,
               executable = True,
               allow_files = True),
+          "_jar_filter": attr.label(
+              default = tool_label("//tools/android:JarFilter"),
+              cfg = HOST_CFG,
+              executable = True,
+              allow_files = True),
       },
       attr_aspects = ALL_DEPS.label + ALL_DEPS.label_list + [LEGACY_RESOURCE_ATTR],
       fragments = ["cpp"],
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
index c5c27e4..719aca6 100644
--- a/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -2,13 +2,44 @@
     name = "embedded_tools",
     srcs = [
         "BUILD.tools",
-        "classes_deploy.jar",
+        "jar_filter_classes_deploy.jar",
+        "package_parser_classes_deploy.jar",
     ],
     visibility = ["//src:__pkg__"],
 )
 
 java_binary(
-    name = "classes",
+    name = "jar_filter_classes",
+    main_class = "does.not.exist",
+    runtime_deps = [":jar_filter_lib"],
+)
+
+java_binary(
+    name = "JarFilter",
+    main_class = "com.google.devtools.build.android.ideinfo.JarFilter",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":jar_filter_lib"],
+)
+
+java_library(
+    name = "jar_filter_lib",
+    srcs = ["JarFilter.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_java_proto",
+        "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+        "//third_party:guava",
+        "//third_party:jsr305",
+        "//third_party/protobuf",
+    ],
+)
+
+java_binary(
+    name = "package_parser_classes",
     main_class = "does.not.exist",
     runtime_deps = [":package_parser_lib"],
 )
@@ -22,7 +53,12 @@
 
 java_library(
     name = "package_parser_lib",
-    srcs = glob(["*.java"]),
+    srcs = [
+        "ArtifactLocationConverter.java",
+        "ArtifactLocationListConverter.java",
+        "PackageParser.java",
+        "PackageParserIoProvider.java",
+    ],
     visibility = [
         "//devtools/blaze/integration:__pkg__",
         "//src/test/java/com/google/devtools/build/android/ideinfo:__pkg__",
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
index efa64fb..e6515db 100644
--- 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
@@ -1,12 +1,23 @@
 package(default_visibility = ["//visibility:public"])
 
 java_import(
-    name = "classes",
-    jars = [":classes_deploy.jar"],
+    name = "jar_filter_classes",
+    jars = [":jar_filter_classes_deploy.jar"],
+)
+
+java_binary(
+    name = "JarFilter",
+    main_class = "com.google.devtools.build.android.ideinfo.JarFilter",
+    runtime_deps = [":jar_filter_classes"],
+)
+
+java_import(
+    name = "package_parser_classes",
+    jars = [":package_parser_classes_deploy.jar"],
 )
 
 java_binary(
     name = "PackageParser",
     main_class = "com.google.devtools.build.android.ideinfo.PackageParser",
-    runtime_deps = [":classes"],
+    runtime_deps = [":package_parser_classes"],
 )
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/JarFilter.java b/src/tools/android/java/com/google/devtools/build/android/ideinfo/JarFilter.java
new file mode 100644
index 0000000..2a37356
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/JarFilter.java
@@ -0,0 +1,195 @@
+// 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.Lists;
+import com.google.common.io.Files;
+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.ArtifactLocation;
+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.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Filters a jar, keeping only the classes that are contained
+ * in the supplied package manifest.
+ */
+public final class JarFilter {
+
+  /** The options for a {@JarFilter} action. */
+  public static final class JarFilterOptions extends OptionsBase {
+    @Option(name = "jars",
+        defaultValue = "null",
+        converter = PathListConverter.class,
+        category = "input",
+        help = "A list of the paths to jars to filter for generated sources.")
+    public List<Path> jars;
+
+    @Option(name = "manifest",
+        defaultValue = "null",
+        converter = PathConverter.class,
+        category = "input",
+        help = "The path to a package manifest generated only from generated sources.")
+    public Path manifest;
+
+    @Option(name = "output",
+        defaultValue = "null",
+        converter = PathConverter.class,
+        category = "output",
+        help = "The path to the jar to output.")
+    public Path output;
+  }
+
+  private static final Logger logger = Logger.getLogger(JarFilter.class.getName());
+
+  public static void main(String[] args) throws Exception {
+    JarFilterOptions options = parseArgs(args);
+    Preconditions.checkNotNull(options.jars);
+    Preconditions.checkNotNull(options.manifest);
+    Preconditions.checkNotNull(options.output);
+
+    try {
+      List<String> archiveFileNamePrefixes = parsePackageManifest(options.manifest);
+      filterJars(options.jars, options.output, archiveFileNamePrefixes);
+    } catch (Throwable e) {
+      logger.log(Level.SEVERE, "Error parsing package strings", e);
+      System.exit(1);
+    }
+    System.exit(0);
+  }
+
+  @VisibleForTesting
+  static JarFilterOptions parseArgs(String[] args) {
+    args = parseParamFileIfUsed(args);
+    OptionsParser optionsParser = OptionsParser.newOptionsParser(JarFilterOptions.class);
+    optionsParser.parseAndExitUponError(args);
+    return optionsParser.getOptions(JarFilterOptions.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 static void filterJars(List<Path> jars, Path output,
+      List<String> archiveFileNamePrefixes) throws IOException {
+    final int bufferSize = 8 * 1024;
+    byte[] buffer = new byte[bufferSize];
+
+    try (ZipOutputStream outputStream = new ZipOutputStream(
+        new FileOutputStream(output.toFile()))) {
+      for (Path jar : jars) {
+        try (ZipFile sourceZipFile = new ZipFile(jar.toFile())) {
+          Enumeration<? extends ZipEntry> entries = sourceZipFile.entries();
+          while (entries.hasMoreElements()) {
+            ZipEntry entry = entries.nextElement();
+            if (!shouldKeep(archiveFileNamePrefixes, entry.getName())) {
+              continue;
+            }
+
+            ZipEntry newEntry = new ZipEntry(entry.getName());
+            outputStream.putNextEntry(newEntry);
+            try (InputStream inputStream = sourceZipFile.getInputStream(entry)) {
+              int len;
+              while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @VisibleForTesting
+  static boolean shouldKeep(List<String> archiveFileNamePrefixes, String name) {
+    for (String archiveFileNamePrefix : archiveFileNamePrefixes) {
+      if (name.startsWith(archiveFileNamePrefix)
+          && name.length() > archiveFileNamePrefix.length()) {
+        char c = name.charAt(archiveFileNamePrefix.length());
+        if (c == '.' || c == '$') {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Nullable
+  private static List<String> parsePackageManifest(Path manifest) throws IOException {
+    try (InputStream inputStream = java.nio.file.Files.newInputStream(manifest)) {
+      PackageManifest packageManifest = PackageManifest.parseFrom(inputStream);
+      return parsePackageManifest(packageManifest);
+    }
+  }
+
+  /**
+   * Reads the package manifest and computes a list of the expected jar archive
+   * file names.
+   *
+   * Eg.:
+   * file java/com/google/foo/Foo.java, package com.google.foo ->
+   * com/google/foo/Foo
+   */
+  @VisibleForTesting
+  static List<String> parsePackageManifest(PackageManifest packageManifest) {
+    List<String> result = Lists.newArrayList();
+    for (JavaSourcePackage javaSourcePackage : packageManifest.getSourcesList()) {
+      ArtifactLocation artifactLocation = javaSourcePackage.getArtifactLocation();
+      String packageString = javaSourcePackage.getPackageString();
+      String archiveFileNamePrefix = getArchiveFileNamePrefix(artifactLocation, packageString);
+      result.add(archiveFileNamePrefix);
+    }
+    return result;
+  }
+
+  @Nullable
+  private static String getArchiveFileNamePrefix(ArtifactLocation artifactLocation,
+      String packageString) {
+    String relativePath = artifactLocation.getRelativePath();
+    int lastSlashIndex = relativePath.lastIndexOf('/');
+    String fileName = lastSlashIndex != -1
+        ? relativePath.substring(lastSlashIndex + 1) : relativePath;
+    String className = fileName.substring(0, fileName.length() - ".java".length());
+    return packageString.replace('.', '/') + '/' + className;
+  }
+}