Merge pull request #129 from brendandouglas/master

Import of bazel plugin using copybara
diff --git a/BUILD b/BUILD
index 5e26650..1d898ce 100644
--- a/BUILD
+++ b/BUILD
@@ -30,6 +30,14 @@
     ],
 )
 
+# UE-specific IJwB tests
+test_suite(
+    name = "ijwb_ue_tests",
+    tests = [
+        "//golang:integration_tests",
+    ],
+)
+
 # ASwB tests, run with an Android Studio plugin SDK
 test_suite(
     name = "aswb_tests",
@@ -38,6 +46,8 @@
         "//aswb:unit_tests",
         "//base:integration_tests",
         "//base:unit_tests",
+        "//cpp:integration_tests",
+        "//cpp:unit_tests",
         "//java:integration_tests",
         "//java:unit_tests",
     ],
diff --git a/CHANGELOG b/CHANGELOG
index 32034b6..15ceaa6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,53 @@
+v2017.08.28
+===========
+* CLion test output: make URLs and bazel targets clickable
+* Retrieve Javadocs for unattached sources
+* CLion: incremental sync retains more caches. Prefill caches on project reload.
+
+v2017.08.14
+===========
+* Fix spurious 'unused' warnings for AutoFactory-annotated classes
+* Python: Test UI support for paramaterized python tests
+* Python: Linkify stack traces in Bazel Console view
+* Test UI: fix timeouts not being marked as failures
+* Go: migrate Go code to the latest JetBrains plugin
+
+v2017.08.01
+===========
+* Add a user setting to suppress the Bazel console during sync
+* Add support for IntelliJ 2017.1.5
+* ASwB: fix generated resources not resolving
+* Fix unresolved references when targets are built with multiple Bazel configurations
+* Python: fix 'argument list too long' errors when debugging
+
+v2017.07.17
+===========
+* Explicitly deprioritize older android/gwt-specific versions of libraries during sync
+* Improve test finder heuristics when creating run configurations
+
+v2017.07.05
+===========
+* CLwB: Show "unsynced" diagnostic file status for C++ files
+
+v2017.06.19
+===========
+* Improve performance when indexing proto_library targets in the working set.
+* Fix incorrectly reusing existing, but different, run configurations.
+* Order BUILD file structure view by target name, not rule type.
+
+v2017.06.05
+===========
+* Add Scala support to IntelliJ.
+* Add 'sync_flags' .blazeproject section, for flags only applied during sync.
+* Android Studio: NDK plugins are now optional.
+* CLion: Improve performance by prefetching required genfiles during sync.
+
+v2017.05.22
+===========
+* TypeScript: Support multiple ts_config rules in .bazelproject
+* Android Studio: Index javac jar for javax.lang classes
+* Show failed test targets in test result UI
+
 v2017.05.08
 ===========
 * Add Python support to CLion
diff --git a/README.md b/README.md
index 439b85e..1489497 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # An IntelliJ plugin for [Bazel](http://bazel.build) projects
 
-This is an early-access version of our Blaze plugins for IntelliJ,
+This is an early-access version of our Bazel plugins for IntelliJ,
 Android Studio, and CLion.
 
 This code drop is for educational purposes only and is currently
@@ -13,8 +13,7 @@
 ## Installation
 
 You can find our plugin in the Jetbrains plugin repository by going to
-`Settings -> Browse Repositories`, and searching for `IntelliJ with Bazel`,
-`Android Studio with Bazel`, or `CLion with Bazel`.
+`Settings -> Browse Repositories`, and searching for `Bazel`.
 
 ## Usage
 
diff --git a/WORKSPACE b/WORKSPACE
index 9a4eaff..7d1168f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -2,44 +2,40 @@
 
 # Long-lived download links available at: https://www.jetbrains.com/intellij-repository/releases
 
-# The plugin api for IntelliJ 2017.1.1. This is required to build IJwB,
+# The plugin api for IntelliJ 2017.2. This is required to build IJwB,
 # and run integration tests.
 new_http_archive(
-    name = "intellij_ce_2017_1_1",
+    name = "intellij_ce_2017_2_2",
     build_file = "intellij_platform_sdk/BUILD.idea",
-    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2017.1.1/ideaIC-2017.1.1.zip",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2017.2.2/ideaIC-2017.2.2.zip",
+    sha256 = "4e14343ecaab00a3a02f2e51995a46cdbea90e20d336f806a1ec40892bbd7e53",
 )
 
-# The plugin api for IntelliJ 2016.3.1. This is required to build IJwB,
+# The plugin api for IntelliJ 2017.1. This is required to build IJwB,
 # and run integration tests.
 new_http_archive(
-    name = "intellij_ce_2016_3_1",
+    name = "intellij_ce_2017_1_5",
     build_file = "intellij_platform_sdk/BUILD.idea",
-    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2016.3.1/ideaIC-2016.3.1.zip",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2017.1.5/ideaIC-2017.1.5.zip",
+    sha256 = "d11beb116f500ecbf75b0a1098dfaad696bc8a15edceae163f53bc511ab79445"
 )
 
-# The plugin api for IntelliJ 2016.2.4. This is required to build IJwB,
-# and run integration tests.
+# The plugin api for IntelliJ UE 2017.2. This is required to run UE-specific
+# integration tests.
 new_http_archive(
-    name = "IC_162_2032_8",
+    name = "intellij_ue_2017_2_2",
     build_file = "intellij_platform_sdk/BUILD.idea",
-    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2016.2.4/ideaIC-2016.2.4.zip",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIU/2017.2.2/ideaIU-2017.2.2.zip",
+    sha256 = "b5a07d41b255799b2cdf2aa8b1c154aa35bf88deac4ae3718b2bae257b68d47d",
 )
 
-# The plugin api for CLion 2016.2.2. This is required to build CLwB,
-# and run integration tests.
+# The plugin api for IntelliJ UE 2017.1. This is required to run UE-specific
+# integration tests.
 new_http_archive(
-    name = "CL_162_1967_7",
-    build_file = "intellij_platform_sdk/BUILD.clion",
-    url = "https://download.jetbrains.com/cpp/CLion-2016.2.2.tar.gz",
-)
-
-# The plugin api for CLion 2016.3.2. This is required to build CLwB,
-# and run integration tests.
-new_http_archive(
-    name = "clion_2016_3_2",
-    build_file = "intellij_platform_sdk/BUILD.clion",
-    url = "https://download.jetbrains.com/cpp/CLion-2016.3.2.tar.gz",
+    name = "intellij_ue_2017_1_5",
+    build_file = "intellij_platform_sdk/BUILD.idea",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIU/2017.1.5/ideaIU-2017.1.5.zip",
+    sha256 = "6d887f79ca7853923060499cac7778b30fc82414f112deb8245d59179892bbad"
 )
 
 # The plugin api for CLion 2017.1.1. This is required to build CLwB,
@@ -48,6 +44,16 @@
     name = "clion_2017_1_1",
     build_file = "intellij_platform_sdk/BUILD.clion",
     url = "https://download.jetbrains.com/cpp/CLion-2017.1.1.tar.gz",
+    sha256 = "9abd6bd38801ae6cf29db2cd133c700e8da11841093de872312fe33ed51309ae",
+)
+
+# The plugin api for CLion 2017.2.0. This is required to build CLwB,
+# and run integration tests.
+new_http_archive(
+    name = "clion_2017_2_1",
+    build_file = "intellij_platform_sdk/BUILD.clion",
+    url = "https://download.jetbrains.com/cpp/CLion-2017.2.1.tar.gz",
+    sha256 = "acd3d09a37a3fa922a85a48635d1b230d559ea68917e2e7895caf16460d50c13",
 )
 
 # The plugin api for Android Studio 2.3.1. This is required to build ASwB,
@@ -56,18 +62,16 @@
     name = "android_studio_2_3_1_0",
     build_file = "intellij_platform_sdk/BUILD.android_studio",
     url = "https://dl.google.com/dl/android/studio/ide-zips/2.3.1.0/android-studio-ide-162.3871768-linux.zip",
+    sha256 = "36520f21678f80298b5df5fe5956db17a5984576f895fdcaa36ab0dbfb408433",
 )
 
-# Python plugin for IntelliJ CE 2016.3. Required at compile-time for python-specific features.
+# The plugin api for Android Studio 3.0 Beta 1. This is required to build ASwB,
+# and run integration tests.
 new_http_archive(
-    name = "python_2016_3",
-    build_file_content = "\n".join([
-        "java_import(",
-        "    name = 'python',",
-        "    jars = ['python/lib/python.jar'],",
-        "    visibility = ['//visibility:public'],",
-        ")"]),
-    url = "https://plugins.jetbrains.com/files/7322/32326/python-community-163.298.zip",
+    name = "android_studio_3_0_0_9",
+    build_file = "intellij_platform_sdk/BUILD.android_studio",
+    url = "https://dl.google.com/dl/android/studio/ide-zips/3.0.0.9/android-studio-ide-171.4243858-linux.zip",
+    sha256 = "422cc6c85ee62186bdb81facd970c0058575dc45ba39f6fd8b326b430269f28c",
 )
 
 # Python plugin for IntelliJ CE 2017.1. Required at compile-time for python-specific features.
@@ -79,7 +83,66 @@
         "    jars = ['python-ce/lib/python-ce.jar'],",
         "    visibility = ['//visibility:public'],",
         ")"]),
-    url = "https://plugins.jetbrains.com/files/7322/33704/python-ce-2017.1.171.3780.116.zip",
+    url = "https://download.plugins.jetbrains.com/7322/35756/python-ce-2017.1.171.4694.26.zip",
+    sha256 = "640d116ff01bcc2f87c75d20da642f17fcd86ed69929e4e0247ffc0df8aa780f",
+)
+
+# Python plugin for IntelliJ CE 2017.2. Required at compile-time for python-specific features.
+new_http_archive(
+    name = "python_2017_2",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'python',",
+        "    jars = ['python-ce/lib/python-ce.jar'],",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://download.plugins.jetbrains.com/7322/37356/python-ce-2017.2.172.3544.31.zip",
+    sha256 = "c7ee48c0bafb29f4a18eaac804b113c4dcdfeaaae174d9003c9ad96e44df6fe0",
+)
+
+# Go plugin for IntelliJ UE and CLion 2017.2.1. Required at compile-time for Bazel integration.
+new_http_archive(
+    name = "go_2017_2",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'go',",
+        "    jars = glob(['intellij-go/lib/*.jar']),",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://download.plugins.jetbrains.com/9568/37740/intellij-go-172.3757.46.zip",
+    sha256 = "3e5eb5415a05e6c30e79c263135c2937cc05e310e553889bd69eefa819705f9c",
+)
+
+# Go plugin for IntelliJ UE and CLion 2017.1.5. Required at compile-time for Bazel integration.
+new_http_archive(
+    name = "go_2017_1",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'go',",
+        "    jars = glob(['intellij-go/lib/*.jar']),",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://download.plugins.jetbrains.com/9568/36389/intellij-go-171.4694.61.zip",
+    sha256 = "16bf70045360c1c2a056c9ae540626dffa3680b8283cb89febaff3a9499b7101",
+)
+
+# Scala plugin for IntelliJ CE 2017.2 EAP. Required at compile-time for scala-specific features.
+new_http_archive(
+    name = "scala_2017_2",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'scala',",
+        "    jars = [",
+        "        'Scala/lib/scala-plugin.jar',",
+        "        'Scala/lib/compiler-settings.jar',",
+        "        'Scala/lib/scala-library.jar',",
+        "        'Scala/lib/scalameta120.jar',",
+        "        'Scala/lib/scalatest-finders-patched.jar',",
+        "    ],",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+   url = "https://download.plugins.jetbrains.com/1347/35283/scala-intellij-bin-2017.2.2.zip",
+    sha256 = "1f0eef98da44dbc3f4f22b399a9175897aca448fd80405eca77fd61bd5fb7219",
 )
 
 # Scala plugin for IntelliJ CE 2017.1. Required at compile-time for scala-specific features.
@@ -87,17 +150,18 @@
     name = "scala_2017_1",
     build_file_content = "\n".join([
         "java_import(",
-        "    name = 'scala-library',",
-        "    jars = ['Scala/lib/scala-library.jar'],",
-        ")",
-        "",
-        "java_import(",
         "    name = 'scala',",
-        "    jars = ['Scala/lib/scala-plugin.jar'],",
-        "    runtime_deps = [':scala-library'],",
+        "    jars = [",
+        "        'Scala/lib/scala-plugin.jar',",
+        "        'Scala/lib/compiler-settings.jar',",
+        "        'Scala/lib/scala-library.jar',",
+        "        'Scala/lib/scalameta120.jar',",
+        "        'Scala/lib/scalatest-finders-patched.jar',",
+        "    ],",
         "    visibility = ['//visibility:public'],",
         ")"]),
     url = "https://plugins.jetbrains.com/files/1347/33637/scala-intellij-bin-2017.1.15.zip",
+    sha256 = "b58670a3b52584effc6dd3d014e77fe80e2795b5e5e58716548ecc1452eca6cf",
 )
 
 # LICENSE: Common Public License 1.0
@@ -141,3 +205,17 @@
     artifact = "com.googlecode.jarjar:jarjar:1.3",
     sha1 = "b81c2719c63fa8e6f3eca5b11b8e9b5ad79463db",
 )
+
+# LICENSE: The Apache Software License, Version 2.0
+maven_jar(
+    name = "auto_value",
+    artifact = "com.google.auto.value:auto-value:1.3",
+    sha1 = "4961194f62915eb45e21940537d60ac53912c57d",
+)
+
+# LICENSE: The Apache Software License, Version 2.0
+maven_jar(
+    name = "error_prone_annotations",
+    artifact = "com.google.errorprone:error_prone_annotations:2.0.15",
+    sha1 = "822652ed7196d119b35d2e22eb9cd4ffda11e640",
+)
diff --git a/aspect/BUILD b/aspect/BUILD
new file mode 100644
index 0000000..7f19914
--- /dev/null
+++ b/aspect/BUILD
@@ -0,0 +1,29 @@
+#
+# Description: Bazel aspect bundled with the Bazel IntelliJ plugin.
+#
+
+licenses(["notice"])  # Apache 2.0
+
+# the aspect files that will be bundled with the final plugin zip
+filegroup(
+    name = "aspect_files",
+    srcs = [
+        "WORKSPACE",
+        "intellij_info.bzl",
+        "intellij_info_impl.bzl",
+        ":BUILD.bazel",
+        "//aspect/tools:JarFilter_deploy.jar",
+        "//aspect/tools:PackageParser_deploy.jar",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+# BUILD file bundled with the aspect must not override the BUILD file
+# used for development. So we name it BUILD.aspect, and rename prior
+# to bundling with the plugin.
+genrule(
+    name = "rename_files",
+    srcs = ["BUILD.aspect"],
+    outs = ["BUILD.bazel"],
+    cmd = "cp $< $@",
+)
diff --git a/aspect/BUILD.aspect b/aspect/BUILD.aspect
new file mode 100644
index 0000000..c109e12
--- /dev/null
+++ b/aspect/BUILD.aspect
@@ -0,0 +1,30 @@
+#
+# Description:
+# The final form of the BUILD file accessed at runtime as an external WORKSPACE.
+#
+
+licenses(["notice"])  # Apache 2.0
+
+java_binary(
+    name = "JarFilter_bin",
+    main_class = "com.google.idea.blaze.aspect.JarFilter",
+    runtime_deps = [":jar_filter_lib"],
+    visibility = ["//visibility:public"],
+)
+
+java_import(
+    name = "jar_filter_lib",
+    jars = ["JarFilter_deploy.jar"],
+)
+
+java_binary(
+    name = "PackageParser_bin",
+    main_class = "com.google.idea.blaze.aspect.PackageParser",
+    runtime_deps = [":package_parser_lib"],
+    visibility = ["//visibility:public"],
+)
+
+java_import(
+    name = "package_parser_lib",
+    jars = ["PackageParser_deploy.jar"],
+)
\ No newline at end of file
diff --git a/aspect/WORKSPACE b/aspect/WORKSPACE
new file mode 100644
index 0000000..e11b403
--- /dev/null
+++ b/aspect/WORKSPACE
@@ -0,0 +1 @@
+workspace(name = "intellij_aspect")
\ No newline at end of file
diff --git a/aspect/intellij_info.bzl b/aspect/intellij_info.bzl
new file mode 100644
index 0000000..3224efe
--- /dev/null
+++ b/aspect/intellij_info.bzl
@@ -0,0 +1,20 @@
+"""Bazel-specific intellij aspect."""
+
+load(
+    "//:intellij_info_impl.bzl",
+    "make_intellij_info_aspect",
+    "intellij_info_aspect_impl",
+)
+
+def tool_label(tool_name):
+  """Returns a label that points to a tool target in the bundled aspect workspace."""
+  return Label("//:" + tool_name + "_bin")
+
+semantics = struct(
+    tool_label = tool_label,
+)
+
+def _aspect_impl(target, ctx):
+  return intellij_info_aspect_impl(target, ctx, semantics)
+
+intellij_info_aspect = make_intellij_info_aspect(_aspect_impl, semantics)
diff --git a/aspect/intellij_info_impl.bzl b/aspect/intellij_info_impl.bzl
new file mode 100644
index 0000000..c5968e9
--- /dev/null
+++ b/aspect/intellij_info_impl.bzl
@@ -0,0 +1,753 @@
+"""Implementation of IntelliJ-specific information collecting aspect."""
+
+# Compile-time dependency attributes, grouped by type.
+DEPS = [
+    "_cc_toolchain",  # From cc rules
+    "_stl",  # From cc rules
+    "malloc",  # From cc_binary rules
+    "_java_toolchain",  # From java rules
+    "deps",
+    "exports",
+    "_robolectric",  # From android_robolectric_test
+    "_android_sdk",  # from android rules
+    "aidl_lib",  # from android_sdk
+    "_scala_toolchain",  # From scala rules
+]
+
+# Run-time dependency attributes, grouped by type.
+RUNTIME_DEPS = [
+    "runtime_deps",
+]
+
+PREREQUISITE_DEPS = []
+
+# Dependency type enum
+COMPILE_TIME = 0
+RUNTIME = 1
+
+##### Helpers
+
+def struct_omit_none(**kwargs):
+  """A replacement for standard `struct` function that omits the fields with None value."""
+  d = {name: kwargs[name] for name in kwargs if kwargs[name] != None}
+  return struct(**d)
+
+def artifact_location(f):
+  """Creates an ArtifactLocation proto from a File."""
+  if f == None:
+    return None
+
+  return to_artifact_location(
+      f.path,
+      f.root.path if not f.is_source else "",
+      f.is_source,
+      is_external_artifact(f.owner),
+  )
+
+def to_artifact_location(exec_path, root_exec_path_fragment, is_source, is_external):
+  """Derives workspace path from other path fragments, and creates an ArtifactLocation proto."""
+  # Bazel 0.4.4 has directory structure:
+  # exec_path = (root_fragment)? + (external/repo_name)? + relative_path
+  # Bazel 0.4.5 has planned directory structure:
+  # exec_path = (../repo_name)? + (root_fragment)? + relative_path
+  # Handle both cases by trying to strip the external workspace prefix before and after removing
+  # root_exec_path_fragment.
+  relative_path = strip_external_workspace_prefix(exec_path)
+  relative_path = strip_root_exec_path_fragment(relative_path, root_exec_path_fragment)
+  # Remove this line when Bazel 0.4.4 and earlier no longer need to be supported.
+  relative_path = strip_external_workspace_prefix(relative_path)
+
+  root_exec_path_fragment = exec_path[:-(len("/" + relative_path))]
+
+  return struct_omit_none(
+      relative_path = relative_path,
+      is_source = is_source,
+      is_external = is_external,
+      root_execution_path_fragment = root_exec_path_fragment,
+      is_new_external_version = True,
+  )
+
+def strip_root_exec_path_fragment(path, root_fragment):
+  if root_fragment and path.startswith(root_fragment + "/"):
+    return path[len(root_fragment + "/"):]
+  return path
+
+def strip_external_workspace_prefix(path):
+  """Either 'external/workspace_name/' or '../workspace_name/'."""
+  # Label.EXTERNAL_PATH_PREFIX is due to change from 'external' to '..' in Bazel 0.4.5.
+  # This code is for forwards and backwards compatibility.
+  # Remove the 'external/' check when Bazel 0.4.4 and earlier no longer need to be supported.
+  if path.startswith("../") or path.startswith("external/"):
+    return "/".join(path.split("/")[2:])
+  return path
+
+def is_external_artifact(label):
+  """Determines whether a label corresponds to an external artifact."""
+  # Label.EXTERNAL_PATH_PREFIX is due to change from 'external' to '..' in Bazel 0.4.5.
+  # This code is for forwards and backwards compatibility.
+  # Remove the 'external' check when Bazel 0.4.4 and earlier no longer need to be supported.
+  return label.workspace_root.startswith("external") or label.workspace_root.startswith("..")
+
+def source_directory_tuple(resource_file):
+  """Creates a tuple of (exec_path, root_exec_path_fragment, is_source, is_external)."""
+  relative_path = str(android_common.resource_source_directory(resource_file))
+  root_exec_path_fragment = resource_file.root.path if not resource_file.is_source else None
+  return (
+      relative_path if resource_file.is_source else root_exec_path_fragment + "/" + relative_path,
+      root_exec_path_fragment,
+      resource_file.is_source,
+      is_external_artifact(resource_file.owner)
+  )
+
+def all_unique_source_directories(resources):
+  """Builds a list of unique ArtifactLocation protos."""
+  # Sets can contain tuples, but cannot contain structs.
+  # Use set of tuples to unquify source directories.
+  source_directory_tuples = depset([source_directory_tuple(f) for f in resources])
+  return [to_artifact_location(
+      exec_path,
+      root_path_fragment,
+      is_source,
+      is_external)
+          for (exec_path, root_path_fragment, is_source, is_external) in source_directory_tuples]
+
+def build_file_artifact_location(ctx):
+  """Creates an ArtifactLocation proto representing a location of a given BUILD file."""
+  return to_artifact_location(
+      ctx.build_file_path,
+      ctx.build_file_path,
+      True,
+      is_external_artifact(ctx.label)
+  )
+
+def get_source_jar(output):
+  if hasattr(output, "source_jar"):
+    return output.source_jar
+  return None
+
+def library_artifact(java_output):
+  """Creates a LibraryArtifact representing a given java_output."""
+  if java_output == None or java_output.class_jar == None:
+    return None
+  return struct_omit_none(
+      jar = artifact_location(java_output.class_jar),
+      interface_jar = artifact_location(java_output.ijar),
+      source_jar = artifact_location(get_source_jar(java_output)),
+  )
+
+def annotation_processing_jars(annotation_processing):
+  """Creates a LibraryArtifact representing Java annotation processing jars."""
+  return struct_omit_none(
+      jar = artifact_location(annotation_processing.class_jar),
+      source_jar = artifact_location(annotation_processing.source_jar),
+  )
+
+def jars_from_output(output):
+  """Collect jars for intellij-resolve-files from Java output."""
+  if output == None:
+    return []
+  return [jar
+          for jar in [output.class_jar, output.ijar, get_source_jar(output)]
+          if jar != None and not jar.is_source]
+
+# TODO(salguarnieri) Remove once skylark provides the path safe string from a PathFragment.
+def replace_empty_path_with_dot(path):
+  return path or "."
+
+def sources_from_target(ctx):
+  """Get the list of sources from a target as artifact locations."""
+  return artifacts_from_target_list_attr(ctx, "srcs")
+
+def artifacts_from_target_list_attr(ctx, attr_name):
+  """Converts a list of targets to a list of artifact locations."""
+  return [artifact_location(f)
+          for target in getattr(ctx.rule.attr, attr_name, [])
+          for f in target.files]
+
+def _collect_target_from_attr(rule_attrs, attr_name, result):
+  """Collects the targets from the given attr into the result."""
+  if not hasattr(rule_attrs, attr_name):
+    return
+  attr_value = getattr(rule_attrs, attr_name)
+  type_name = type(attr_value)
+  if type_name == "Target":
+    result.append(attr_value)
+  elif type_name == "list":
+    result.extend(attr_value)
+
+def collect_targets_from_attrs(rule_attrs, attrs):
+  """Returns a list of targets from the given attributes."""
+  result = []
+  for attr_name in attrs:
+    _collect_target_from_attr(rule_attrs, attr_name, result)
+  return [target for target in result if is_valid_aspect_target(target)]
+
+def targets_to_labels(targets):
+  """Returns a set of label strings for the given targets."""
+  return depset([str(target.label) for target in targets])
+
+def list_omit_none(value):
+  """Returns a list of the value, or the empty list if None."""
+  return [value] if value else []
+
+def is_valid_aspect_target(target):
+  """Returns whether the target has had the aspect run on it."""
+  return hasattr(target, "intellij_info")
+
+def get_aspect_ids(ctx, target):
+  """Returns the all aspect ids, filtering out self."""
+  aspect_ids = None
+  if hasattr(ctx, "aspect_ids"):
+    aspect_ids = ctx.aspect_ids
+  elif hasattr(target, "aspect_ids"):
+    aspect_ids = target.aspect_ids
+  else:
+    return None
+  return [aspect_id for aspect_id in aspect_ids if "intellij_info_aspect" not in aspect_id]
+
+def make_target_key(label, aspect_ids):
+  """Returns a TargetKey proto struct from a target."""
+  return struct_omit_none(
+      label = str(label),
+      aspect_ids = tuple(aspect_ids) if aspect_ids else None
+  )
+
+def make_dep(dep, dependency_type):
+  """Returns a Dependency proto struct."""
+  return struct(
+      target = dep.intellij_info.target_key,
+      dependency_type = dependency_type,
+  )
+
+def make_deps(deps, dependency_type):
+  """Returns a list of Dependency proto structs."""
+  return [make_dep(dep, dependency_type) for dep in deps]
+
+def make_dep_from_label(label, dependency_type):
+  """Returns a Dependency proto struct from a label."""
+  return struct(
+      target = struct(label = str(label)),
+      dependency_type = dependency_type,
+  )
+
+def update_set_in_dict(input_dict, key, other_set):
+  """Updates depset in dict, merging it with another depset."""
+  input_dict[key] = input_dict.get(key, depset()) | other_set
+
+##### Builders for individual parts of the aspect output
+
+def collect_py_info(target, ctx, ide_info, ide_info_file, output_groups):
+  """Updates Python-specific output groups, returns false if not a Python target."""
+  if not hasattr(target, "py"):
+    return False
+
+  ide_info["py_ide_info"] = struct_omit_none(
+      sources = sources_from_target(ctx),
+  )
+  transitive_sources = target.py.transitive_sources
+  # TODO(brendandouglas): target to python files only
+  compile_files = target.output_group("files_to_compile_INTERNAL_")
+
+  update_set_in_dict(output_groups, "intellij-info-py", depset([ide_info_file]))
+  update_set_in_dict(output_groups, "intellij-compile-py", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve-py", transitive_sources)
+
+  # Add to legacy output groups for backwards compatibility
+  update_set_in_dict(output_groups, "intellij-compile", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve", transitive_sources)
+  return True
+
+def _collect_generated_proto_go_sources(target):
+  """Returns a depset of proto go source files generated by this target."""
+  if not hasattr(target, "proto_go_api_info"):
+    return None
+  files = getattr(target.proto_go_api_info, "files_to_build", [])
+  return depset([f for f in files if f.basename.endswith(".pb.go")])
+
+def collect_go_info(target, ide_info, ide_info_file, output_groups):
+  """Updates Go-specific output groups, returns false if not a recognized Go target."""
+  # currently there's no Go Skylark API, with the only exception being proto_library targets
+  if not hasattr(target, "proto_go_api_info"):
+    return False
+
+  generated_sources = _collect_generated_proto_go_sources(target)
+
+  ide_info["go_ide_info"] = struct_omit_none(
+      generated_sources = [artifact_location(f) for f in generated_sources],
+  )
+
+  update_set_in_dict(output_groups, "intellij-info-go", depset([ide_info_file]))
+  update_set_in_dict(output_groups, "intellij-compile-go", generated_sources)
+  update_set_in_dict(output_groups, "intellij-resolve-go", generated_sources)
+  return True
+
+def collect_cpp_info(target, ctx, ide_info, ide_info_file, output_groups):
+  """Updates C++-specific output groups, returns false if not a C++ target."""
+  if not hasattr(target, "cc"):
+    return False
+
+  sources = artifacts_from_target_list_attr(ctx, "srcs")
+  headers = artifacts_from_target_list_attr(ctx, "hdrs")
+  textual_headers = artifacts_from_target_list_attr(ctx, "textual_hdrs")
+
+  target_includes = []
+  if hasattr(ctx.rule.attr, "includes"):
+    target_includes = ctx.rule.attr.includes
+  target_defines = []
+  if hasattr(ctx.rule.attr, "defines"):
+    target_defines = ctx.rule.attr.defines
+  target_copts = []
+  if hasattr(ctx.rule.attr, "copts"):
+    target_copts = ctx.rule.attr.copts
+
+  cc_provider = target.cc
+
+  c_info = struct_omit_none(
+      source = sources,
+      header = headers,
+      textual_header = textual_headers,
+      target_include = target_includes,
+      target_define = target_defines,
+      target_copt = target_copts,
+      transitive_include_directory = cc_provider.include_directories,
+      transitive_quote_include_directory = cc_provider.quote_include_directories,
+      transitive_define = cc_provider.defines,
+      transitive_system_include_directory = cc_provider.system_include_directories,
+  )
+  ide_info["c_ide_info"] = c_info
+  resolve_files = cc_provider.transitive_headers
+  # TODO(brendandouglas): target to cpp files only
+  compile_files = target.output_group("files_to_compile_INTERNAL_")
+
+  update_set_in_dict(output_groups, "intellij-info-cpp", depset([ide_info_file]))
+  update_set_in_dict(output_groups, "intellij-compile-cpp", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve-cpp", resolve_files)
+
+  # Add to legacy output groups for backwards compatibility
+  update_set_in_dict(output_groups, "intellij-compile", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve", resolve_files)
+  return True
+
+def collect_c_toolchain_info(ctx, ide_info, ide_info_file, output_groups):
+  """Updates cc_toolchain-relevant output groups, returns false if not a cc_toolchain target."""
+  if ctx.rule.kind != "cc_toolchain":
+    return False
+
+  # This should exist because we requested it in our aspect definition.
+  cc_fragment = ctx.fragments.cpp
+
+  c_toolchain_info = struct_omit_none(
+      target_name = cc_fragment.target_gnu_system_name,
+      base_compiler_option = cc_fragment.compiler_options(ctx.features),
+      c_option = cc_fragment.c_options,
+      cpp_option = cc_fragment.cxx_options(ctx.features),
+      link_option = cc_fragment.link_options,
+      unfiltered_compiler_option = cc_fragment.unfiltered_compiler_options(ctx.features),
+      preprocessor_executable = replace_empty_path_with_dot(
+          str(cc_fragment.preprocessor_executable)),
+      cpp_executable = str(cc_fragment.compiler_executable),
+      built_in_include_directory = [str(d)
+                                    for d in cc_fragment.built_in_include_directories],
+  )
+  ide_info["c_toolchain_ide_info"] = c_toolchain_info
+  update_set_in_dict(output_groups, "intellij-info-cpp", depset([ide_info_file]))
+  return True
+
+def get_java_provider(target):
+  if hasattr(target, "proto_java"):
+    return target.proto_java
+  if hasattr(target, "java"):
+    return target.java
+  if hasattr(target, "scala"):
+    return target.scala
+  return None
+
+def get_java_jars(outputs):
+  """Handle both Java (java.outputs.jars list) and Scala (single scala.outputs) targets."""
+  if hasattr(outputs, "jars"):
+    return outputs.jars
+  if hasattr(outputs, "class_jar"):
+    return [outputs]
+  return []
+
+def collect_java_info(target, ctx, semantics, ide_info, ide_info_file, output_groups):
+  """Updates Java-specific output groups, returns false if not a Java target."""
+  java = get_java_provider(target)
+  if not java:
+    return False
+
+  java_semantics = semantics.java if hasattr(semantics, "java") else None
+  if java_semantics and java_semantics.skip_target(target, ctx):
+    return False
+
+  ide_info_files = depset()
+  sources = sources_from_target(ctx)
+  java_jars = get_java_jars(java.outputs)
+  jars = [library_artifact(output) for output in java_jars]
+  class_jars = [output.class_jar for output in java_jars if output and output.class_jar]
+  output_jars = [jar for output in java_jars for jar in jars_from_output(output)]
+  resolve_files = depset(output_jars)
+  compile_files = depset(class_jars)
+
+  gen_jars = []
+  if (hasattr(java, "annotation_processing") and
+      java.annotation_processing and
+      java.annotation_processing.enabled):
+    gen_jars = [annotation_processing_jars(java.annotation_processing)]
+    resolve_files = resolve_files | depset([
+        jar for jar in [java.annotation_processing.class_jar,
+                        java.annotation_processing.source_jar]
+        if jar != None and not jar.is_source])
+    compile_files = compile_files | depset([
+        jar for jar in [java.annotation_processing.class_jar]
+        if jar != None and not jar.is_source])
+
+  jdeps = None
+  if hasattr(java.outputs, "jdeps"):
+    jdeps = artifact_location(java.outputs.jdeps)
+
+  java_sources, gen_java_sources, srcjars = divide_java_sources(ctx)
+
+  if java_semantics:
+    srcjars = java_semantics.filter_source_jars(target, ctx, srcjars)
+
+  package_manifest = None
+  if java_sources:
+    package_manifest = build_java_package_manifest(ctx, target, java_sources, ".java-manifest")
+    ide_info_files = ide_info_files | depset([package_manifest])
+
+  filtered_gen_jar = None
+  if java_sources and (gen_java_sources or srcjars):
+    filtered_gen_jar, filtered_gen_resolve_files = build_filtered_gen_jar(
+        ctx,
+        target,
+        java,
+        gen_java_sources,
+        srcjars
+    )
+    resolve_files = resolve_files | filtered_gen_resolve_files
+
+  java_info = struct_omit_none(
+      sources = sources,
+      jars = jars,
+      jdeps = jdeps,
+      generated_jars = gen_jars,
+      package_manifest = artifact_location(package_manifest),
+      filtered_gen_jar = filtered_gen_jar,
+      main_class = ctx.rule.attr.main_class if hasattr(ctx.rule.attr, "main_class") else None,
+  )
+
+  ide_info["java_ide_info"] = java_info
+  ide_info_files += depset([ide_info_file])
+  update_set_in_dict(output_groups, "intellij-info-java", ide_info_files)
+  update_set_in_dict(output_groups, "intellij-compile-java", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve-java", resolve_files)
+
+  # Add to legacy output groups for backwards compatibility
+  update_set_in_dict(output_groups, "intellij-info-text", ide_info_files)
+  update_set_in_dict(output_groups, "intellij-compile", compile_files)
+  update_set_in_dict(output_groups, "intellij-resolve", resolve_files)
+  return True
+
+def _package_manifest_file_argument(f):
+  artifact = artifact_location(f)
+  is_external = "1" if is_external_artifact(f.owner) else "0"
+  return artifact.root_execution_path_fragment + "," + artifact.relative_path + "," + is_external
+
+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([_package_manifest_file_argument(f) for f in source_files])]
+  argfile = ctx.new_file(ctx.configuration.bin_dir,
+                         target.label.name + suffix + ".params")
+  ctx.file_action(output=argfile, content="\n".join(args))
+
+  ctx.action(
+      inputs = source_files + [argfile],
+      outputs = [output],
+      executable = ctx.executable._package_parser,
+      arguments = ["@" + argfile.path],
+      mnemonic = "JavaPackageManifest",
+      progress_message = "Parsing java package strings for " + str(target.label),
+  )
+  return output
+
+def build_filtered_gen_jar(ctx, target, java, gen_java_sources, srcjars):
+  """Filters the passed jar to contain only classes from the given manifest."""
+  jar_artifacts = []
+  source_jar_artifacts = []
+  for jar in java.outputs.jars:
+    if jar.ijar:
+      jar_artifacts.append(jar.ijar)
+    elif jar.class_jar:
+      jar_artifacts.append(jar.class_jar)
+    if jar.source_jar:
+      source_jar_artifacts.append(jar.source_jar)
+
+  filtered_jar = ctx.new_file(target.label.name + "-filtered-gen.jar")
+  filtered_source_jar = ctx.new_file(target.label.name + "-filtered-gen-src.jar")
+  args = []
+  for jar in jar_artifacts:
+    args += ["--filter_jar", jar.path]
+  for jar in source_jar_artifacts:
+    args += ["--filter_source_jar", jar.path]
+  args += ["--filtered_jar", filtered_jar.path]
+  args += ["--filtered_source_jar", filtered_source_jar.path]
+  if gen_java_sources:
+    for java_file in gen_java_sources:
+      args += ["--keep_java_file", java_file.path]
+  if srcjars:
+    for source_jar in srcjars:
+      args += ["--keep_source_jar", source_jar.path]
+  ctx.action(
+      inputs = jar_artifacts + source_jar_artifacts + gen_java_sources + srcjars,
+      outputs = [filtered_jar, filtered_source_jar],
+      executable = ctx.executable._jar_filter,
+      arguments = args,
+      mnemonic = "JarFilter",
+      progress_message = "Filtering generated code for " + str(target.label),
+  )
+  output_jar = struct(
+      jar=artifact_location(filtered_jar),
+      source_jar=artifact_location(filtered_source_jar),
+  )
+  intellij_resolve_files = depset([filtered_jar, filtered_source_jar])
+  return output_jar, intellij_resolve_files
+
+def divide_java_sources(ctx):
+  """Divide sources into plain java, generated java, and srcjars."""
+
+  java_sources = []
+  gen_java_sources = []
+  srcjars = []
+  if hasattr(ctx.rule.attr, "srcs"):
+    srcs = ctx.rule.attr.srcs
+    for src in srcs:
+      for f in src.files:
+        if f.basename.endswith(".java"):
+          if f.is_source:
+            java_sources.append(f)
+          else:
+            gen_java_sources.append(f)
+        elif f.basename.endswith(".srcjar"):
+          srcjars.append(f)
+
+  return java_sources, gen_java_sources, srcjars
+
+def collect_android_info(target, ctx, semantics, ide_info, ide_info_file, output_groups):
+  """Updates Android-specific output groups, returns false if not a Android target."""
+  if not hasattr(target, "android"):
+    return False
+
+  android_semantics = semantics.android if hasattr(semantics, "android") else None
+  extra_ide_info = android_semantics.extra_ide_info(target, ctx) if android_semantics else {}
+
+  android = target.android
+  android_info = struct_omit_none(
+      java_package = android.java_package,
+      idl_import_root = android.idl.import_root if hasattr(android.idl, "import_root") else None,
+      manifest = artifact_location(android.manifest),
+      apk = artifact_location(android.apk),
+      dependency_apk = [artifact_location(apk) for apk in android.apks_under_test],
+      has_idl_sources = android.idl.output != None,
+      idl_jar = library_artifact(android.idl.output),
+      generate_resource_class = android.defines_resources,
+      resources = all_unique_source_directories(android.resources),
+      resource_jar = library_artifact(android.resource_jar),
+      **extra_ide_info
+  )
+  resolve_files = depset(jars_from_output(android.idl.output))
+
+  if android.manifest and not android.manifest.is_source:
+    resolve_files = resolve_files | depset([android.manifest])
+
+  ide_info["android_ide_info"] = android_info
+  update_set_in_dict(output_groups, "intellij-info-android", depset([ide_info_file]))
+  update_set_in_dict(output_groups, "intellij-resolve-android", resolve_files)
+
+  # Add to legacy output groups for backwards compatibility
+  update_set_in_dict(output_groups, "intellij-resolve", resolve_files)
+  return True
+
+def collect_android_sdk_info(ctx, ide_info, ide_info_file, output_groups):
+  """Updates android_sdk-relevant groups, returns false if not an android_sdk target."""
+  if ctx.rule.kind != "android_sdk":
+    return False
+  android_jar_file = list(ctx.rule.attr.android_jar.files)[0]
+  ide_info["android_sdk_ide_info"] = struct(
+      android_jar = artifact_location(android_jar_file),
+  )
+  update_set_in_dict(output_groups, "intellij-info-android", depset([ide_info_file]))
+  return True
+
+def build_test_info(ctx):
+  """Build TestInfo."""
+  if not is_test_rule(ctx):
+    return None
+  return struct_omit_none(
+      size = ctx.rule.attr.size,
+  )
+
+def is_test_rule(ctx):
+  kind_string = ctx.rule.kind
+  return kind_string.endswith("_test")
+
+def collect_java_toolchain_info(target, ide_info, ide_info_file, output_groups):
+  """Updates java_toolchain-relevant output groups, returns false if not a java_toolchain target."""
+  if not hasattr(target, "java_toolchain"):
+    return False
+  toolchain_info = target.java_toolchain
+  javac_jar_file = toolchain_info.javac_jar if hasattr(toolchain_info, "javac_jar") else None
+  ide_info["java_toolchain_ide_info"] = struct_omit_none(
+      source_version = toolchain_info.source_version,
+      target_version = toolchain_info.target_version,
+      javac_jar = artifact_location(javac_jar_file),
+  )
+  update_set_in_dict(output_groups, "intellij-info-java", depset([ide_info_file]))
+  return True
+
+##### Main aspect function
+
+def intellij_info_aspect_impl(target, ctx, semantics):
+  """Aspect implementation function."""
+  tags = ctx.rule.attr.tags
+  if "no-ide" in tags:
+    return struct()
+
+  rule_attrs = ctx.rule.attr
+
+  # Collect direct dependencies
+  direct_dep_targets = collect_targets_from_attrs(
+      rule_attrs, semantics_extra_deps(DEPS, semantics, "extra_deps"))
+  direct_deps = make_deps(direct_dep_targets, COMPILE_TIME)
+
+  # Add exports from direct dependencies
+  exported_deps_from_deps = []
+  for dep in direct_dep_targets:
+    exported_deps_from_deps = exported_deps_from_deps + dep.intellij_info.export_deps
+
+  # Combine into all compile time deps
+  compiletime_deps = direct_deps + exported_deps_from_deps
+
+  # Propagate my own exports
+  export_deps = []
+  if hasattr(target, "java"):
+    transitive_exports = target.java.transitive_exports
+    export_deps = [make_dep_from_label(label, COMPILE_TIME) for label in transitive_exports]
+    # Empty android libraries export all their dependencies.
+    if ctx.rule.kind == "android_library":
+      if not hasattr(rule_attrs, "srcs") or not ctx.rule.attr.srcs:
+        export_deps = export_deps + compiletime_deps
+  export_deps = list(depset(export_deps))
+
+  # runtime_deps
+  runtime_dep_targets = collect_targets_from_attrs(
+      rule_attrs, semantics_extra_deps(RUNTIME_DEPS, semantics, "extra_runtime_deps"))
+  runtime_deps = make_deps(runtime_dep_targets, RUNTIME)
+  all_deps = list(depset(compiletime_deps + runtime_deps))
+
+  # extra prerequisites
+  extra_prerequisite_targets = collect_targets_from_attrs(
+      rule_attrs, semantics_extra_deps(PREREQUISITE_DEPS, semantics, "extra_prerequisites"))
+
+  # Roll up output files from my prerequisites
+  prerequisites = direct_dep_targets + runtime_dep_targets + extra_prerequisite_targets
+  output_groups = dict()
+  for dep in prerequisites:
+    for k, v in dep.intellij_info.output_groups.items():
+      update_set_in_dict(output_groups, k, v)
+
+  # Initialize the ide info dict, and corresponding output file
+  # This will be passed to each language-specific handler to fill in as required
+  file_name = target.label.name
+  aspect_ids = get_aspect_ids(ctx, target)
+  if aspect_ids:
+    aspect_hash = hash(".".join(aspect_ids))
+    file_name = file_name + "-" + str(aspect_hash)
+  file_name = file_name + ".intellij-info.txt"
+  ide_info_file = ctx.new_file(file_name)
+
+  target_key = make_target_key(target.label, aspect_ids)
+  ide_info = dict(
+      key = target_key,
+      kind_string = ctx.rule.kind,
+      deps = list(all_deps),
+      build_file_artifact_location = build_file_artifact_location(ctx),
+      tags = tags,
+      features = ctx.features,
+  )
+  # Collect test info
+  ide_info["test_info"] = build_test_info(ctx)
+
+  handled = False
+  handled = collect_py_info(target, ctx, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_cpp_info(target, ctx, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_c_toolchain_info(ctx, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_go_info(target, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_java_info(target, ctx, semantics, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_java_toolchain_info(target, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_android_info(target, ctx, semantics, ide_info, ide_info_file, output_groups) or handled
+  handled = collect_android_sdk_info(ctx, ide_info, ide_info_file, output_groups) or handled
+
+  # Any extra ide info
+  if hasattr(semantics, "extra_ide_info"):
+    handled = semantics.extra_ide_info(target, ctx, ide_info, ide_info_file, output_groups) or handled
+
+  # Add to generic output group if it's not handled by a language-specific handler
+  if not handled:
+    update_set_in_dict(output_groups, "intellij-info-generic", depset([ide_info_file]))
+
+  # Add to legacy output group for backwards compatibility
+  update_set_in_dict(output_groups, "intellij-info-text", depset([ide_info_file]))
+
+  # Output the ide information file.
+  info = struct_omit_none(**ide_info)
+  ctx.file_action(ide_info_file, info.to_proto())
+
+  # Return providers.
+  return struct_omit_none(
+      output_groups = output_groups,
+      intellij_info = struct(
+          target_key = target_key,
+          output_groups = output_groups,
+          export_deps = export_deps,
+      ),
+  )
+
+def semantics_extra_deps(base, semantics, name):
+  if not hasattr(semantics, name):
+    return base
+  extra_deps = getattr(semantics, name)
+  return base + extra_deps
+
+def make_intellij_info_aspect(aspect_impl, semantics):
+  """Creates the aspect given the semantics."""
+  tool_label = semantics.tool_label
+  deps = semantics_extra_deps(DEPS, semantics, "extra_deps")
+  runtime_deps = semantics_extra_deps(RUNTIME_DEPS, semantics, "extra_runtime_deps")
+  prerequisite_deps = semantics_extra_deps(PREREQUISITE_DEPS, semantics, "extra_prerequisites")
+
+  attr_aspects = deps + runtime_deps + prerequisite_deps
+
+  return aspect(
+      attrs = {
+          "_package_parser": attr.label(
+              default = tool_label("PackageParser"),
+              cfg = "host",
+              executable = True,
+              allow_files = True),
+          "_jar_filter": attr.label(
+              default = tool_label("JarFilter"),
+              cfg = "host",
+              executable = True,
+              allow_files = True),
+      },
+      attr_aspects = attr_aspects,
+      fragments = ["cpp"],
+      implementation = aspect_impl,
+      required_aspect_providers = ["proto_java"],
+  )
diff --git a/aspect/tools/BUILD b/aspect/tools/BUILD
new file mode 100644
index 0000000..fe42712
--- /dev/null
+++ b/aspect/tools/BUILD
@@ -0,0 +1,84 @@
+#
+# Description:
+# Tools needed by the bazel plugin's aspect.
+#
+
+package(default_visibility = ["//aspect:__pkg__"])
+
+licenses(["notice"])  # Apache 2.0
+
+# To prevent versioning conflicts when developing internally, we always use the same
+# guava version bundled with the IntelliJ plugin API.
+java_library(
+    name = "guava",
+    exports = ["//intellij_platform_sdk:guava"],
+)
+
+java_library(
+    name = "lib",
+    srcs = glob(["src/**/*.java"]),
+    deps = [
+        ":guava",
+        "//proto:proto_deps",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+java_binary(
+    name = "JarFilter",
+    main_class = "com.google.idea.blaze.aspect.JarFilter",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":lib"],
+)
+
+java_binary(
+    name = "PackageParser",
+    main_class = "com.google.idea.blaze.aspect.PackageParser",
+    visibility = ["//visibility:public"],
+    runtime_deps = [":lib"],
+)
+
+java_library(
+    name = "test_lib",
+    testonly = 1,
+    exports = [
+        ":guava",
+        ":lib",
+        "//intellij_platform_sdk:truth",
+        "//proto:proto_deps",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
+
+java_test(
+    name = "JarFilterTest",
+    size = "medium",
+    srcs = ["tests/unittests/com/google/idea/blaze/aspect/JarFilterTest.java"],
+    test_class = "com.google.idea.blaze.aspect.JarFilterTest",
+    deps = [":test_lib"],
+)
+
+java_test(
+    name = "PackageParserTest",
+    size = "small",
+    srcs = ["tests/unittests/com/google/idea/blaze/aspect/PackageParserTest.java"],
+    test_class = "com.google.idea.blaze.aspect.PackageParserTest",
+    deps = [":test_lib"],
+)
+
+java_test(
+    name = "OptionParserTest",
+    size = "small",
+    srcs = ["tests/unittests/com/google/idea/blaze/aspect/OptionParserTest.java"],
+    test_class = "com.google.idea.blaze.aspect.OptionParserTest",
+    deps = [":test_lib"],
+)
+
+java_test(
+    name = "ArtifactLocationParserTest",
+    size = "small",
+    srcs = ["tests/unittests/com/google/idea/blaze/aspect/ArtifactLocationParserTest.java"],
+    test_class = "com.google.idea.blaze.aspect.ArtifactLocationParserTest",
+    deps = [":test_lib"],
+)
diff --git a/aspect/tools/src/com/google/idea/blaze/aspect/ArtifactLocationParser.java b/aspect/tools/src/com/google/idea/blaze/aspect/ArtifactLocationParser.java
new file mode 100644
index 0000000..6a5088c
--- /dev/null
+++ b/aspect/tools/src/com/google/idea/blaze/aspect/ArtifactLocationParser.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.ArtifactLocation;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/** Parses an {@link ArtifactLocation} from a comma-separate list of string-encoded fields. */
+@VisibleForTesting
+public final class ArtifactLocationParser {
+  private ArtifactLocationParser() {}
+
+  private static final Splitter SPLITTER = Splitter.on(',');
+
+  @VisibleForTesting
+  static final String INVALID_FORMAT =
+      "Expected format rootExecutionPathFragment,relPath,isExternal";
+
+  private static Path getPath(String pathString) {
+    return FileSystems.getDefault().getPath(pathString);
+  }
+
+  /** Parse a colon-separated list of string-encoded {@link ArtifactLocation}s. */
+  static List<ArtifactLocation> parseList(String input) {
+    ImmutableList.Builder<ArtifactLocation> builder = ImmutableList.builder();
+    for (String piece : input.split(":")) {
+      if (!piece.isEmpty()) {
+        builder.add(parse(piece));
+      }
+    }
+    return builder.build();
+  }
+
+  /** Parse an {@link ArtifactLocation} from a comma-separate list of string-encoded fields. */
+  static ArtifactLocation parse(String input) {
+    Iterator<String> values = SPLITTER.split(input).iterator();
+    try {
+      Path rootExecutionPathFragment = getPath(values.next());
+      Path relPath = getPath(values.next());
+      boolean isExternal = values.next().equals("1");
+      if (values.hasNext()) {
+        throw new IllegalArgumentException(INVALID_FORMAT);
+      }
+
+      boolean isSource = rootExecutionPathFragment.toString().isEmpty();
+      return ArtifactLocation.newBuilder()
+          .setRootExecutionPathFragment(rootExecutionPathFragment.toString())
+          .setRelativePath(relPath.toString())
+          .setIsSource(isSource)
+          .setIsExternal(isExternal)
+          .build();
+
+    } catch (NoSuchElementException e) {
+      throw new IllegalArgumentException(INVALID_FORMAT);
+    }
+  }
+}
diff --git a/aspect/tools/src/com/google/idea/blaze/aspect/JarFilter.java b/aspect/tools/src/com/google/idea/blaze/aspect/JarFilter.java
new file mode 100644
index 0000000..53cb58d
--- /dev/null
+++ b/aspect/tools/src/com/google/idea/blaze/aspect/JarFilter.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import javax.annotation.Nullable;
+
+/** Filters a jar, keeping only the classes that are indicated. */
+public final class JarFilter {
+
+  /** The options for a {@link JarFilter} action. */
+  @VisibleForTesting
+  static final class JarFilterOptions {
+    List<Path> filterJars;
+    List<Path> filterSourceJars;
+    List<Path> keepJavaFiles;
+    List<Path> keepSourceJars;
+    Path filteredJar;
+    Path filteredSourceJar;
+  }
+
+  @VisibleForTesting
+  static JarFilterOptions parseArgs(String[] args) {
+    args = parseParamFileIfUsed(args);
+    JarFilterOptions options = new JarFilterOptions();
+    options.filterJars = OptionParser.parseMultiOption(args, "filter_jar", PATH_PARSER);
+    options.filterSourceJars =
+        OptionParser.parseMultiOption(args, "filter_source_jar", PATH_PARSER);
+    options.keepJavaFiles = OptionParser.parseMultiOption(args, "keep_java_file", PATH_PARSER);
+    options.keepSourceJars = OptionParser.parseMultiOption(args, "keep_source_jar", PATH_PARSER);
+    options.filteredJar = OptionParser.parseSingleOption(args, "filtered_jar", PATH_PARSER);
+    options.filteredSourceJar =
+        OptionParser.parseSingleOption(args, "filtered_source_jar", PATH_PARSER);
+    return options;
+  }
+
+  private static final Function<String, Path> PATH_PARSER =
+      string -> FileSystems.getDefault().getPath(string);
+
+  private static final Logger logger = Logger.getLogger(JarFilter.class.getName());
+
+  private static final Pattern JAVA_PACKAGE_PATTERN =
+      Pattern.compile("^\\s*package\\s+([\\w\\.]+);");
+
+  public static void main(String[] args) throws Exception {
+    JarFilterOptions options = parseArgs(args);
+    try {
+      main(options);
+    } catch (Throwable e) {
+      logger.log(Level.SEVERE, "Error filtering jars", e);
+      System.exit(1);
+    }
+    System.exit(0);
+  }
+
+  @VisibleForTesting
+  static void main(JarFilterOptions options) throws Exception {
+    Preconditions.checkNotNull(options.filteredJar);
+
+    if (options.filterJars == null) {
+      options.filterJars = ImmutableList.of();
+    }
+    if (options.filterSourceJars == null) {
+      options.filterSourceJars = ImmutableList.of();
+    }
+
+    final List<String> archiveFileNamePrefixes = Lists.newArrayList();
+    if (options.keepJavaFiles != null) {
+      archiveFileNamePrefixes.addAll(parseJavaFiles(options.keepJavaFiles));
+    }
+    if (options.keepSourceJars != null) {
+      archiveFileNamePrefixes.addAll(parseSrcJars(options.keepSourceJars));
+    }
+
+    filterJars(
+        options.filterJars,
+        options.filteredJar,
+        string -> shouldKeepClass(archiveFileNamePrefixes, string));
+    if (options.filteredSourceJar != null) {
+      filterJars(
+          options.filterSourceJars,
+          options.filteredSourceJar,
+          string -> shouldKeepJavaFile(archiveFileNamePrefixes, string));
+    }
+  }
+
+  private static String[] parseParamFileIfUsed(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);
+    }
+  }
+
+  /** Finds the expected jar archive file name prefixes for the java files. */
+  private static List<String> parseJavaFiles(List<Path> javaFiles) throws IOException {
+    ListeningExecutorService executorService =
+        MoreExecutors.listeningDecorator(
+            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));
+
+    List<ListenableFuture<String>> futures = Lists.newArrayList();
+    for (final Path javaFile : javaFiles) {
+      futures.add(
+          executorService.submit(
+              () -> {
+                String packageString = getDeclaredPackageOfJavaFile(javaFile);
+                return packageString != null
+                    ? getArchiveFileNamePrefix(javaFile.toString(), packageString)
+                    : null;
+              }));
+    }
+    try {
+      List<String> archiveFileNamePrefixes = Futures.allAsList(futures).get();
+      List<String> result = Lists.newArrayList();
+      for (String archiveFileNamePrefix : archiveFileNamePrefixes) {
+        if (archiveFileNamePrefix != null) {
+          result.add(archiveFileNamePrefix);
+        }
+      }
+      return result;
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new IOException(e);
+    } catch (ExecutionException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private static List<String> parseSrcJars(List<Path> srcJars) throws IOException {
+    List<String> result = Lists.newArrayList();
+    for (Path srcJar : srcJars) {
+      try (ZipFile sourceZipFile = new ZipFile(srcJar.toFile())) {
+        Enumeration<? extends ZipEntry> entries = sourceZipFile.entries();
+        while (entries.hasMoreElements()) {
+          ZipEntry entry = entries.nextElement();
+          if (!entry.getName().endsWith(".java")) {
+            continue;
+          }
+          try (BufferedReader reader =
+              new BufferedReader(
+                  new InputStreamReader(sourceZipFile.getInputStream(entry), UTF_8))) {
+            String packageString = parseDeclaredPackage(reader);
+            if (packageString != null) {
+              String archiveFileNamePrefix =
+                  getArchiveFileNamePrefix(entry.getName(), packageString);
+              result.add(archiveFileNamePrefix);
+            }
+          }
+        }
+      }
+    }
+    return result;
+  }
+
+  @Nullable
+  private static String getDeclaredPackageOfJavaFile(Path javaFile) {
+    try (BufferedReader reader =
+        java.nio.file.Files.newBufferedReader(javaFile, StandardCharsets.UTF_8)) {
+      return parseDeclaredPackage(reader);
+
+    } catch (IOException e) {
+      logger.log(Level.WARNING, "Error parsing package string from java source: " + javaFile, e);
+      return null;
+    }
+  }
+
+  @Nullable
+  private static String parseDeclaredPackage(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;
+  }
+
+  /**
+   * Computes the expected archive file name prefix of a java class.
+   *
+   * <p>Eg.: file java/com/google/foo/Foo.java, package com.google.foo -> com/google/foo/Foo
+   */
+  private static String getArchiveFileNamePrefix(String javaFile, String packageString) {
+    int lastSlashIndex = javaFile.lastIndexOf('/');
+    // On Windows, the separator could be '\\'
+    if (lastSlashIndex == -1) {
+      lastSlashIndex = javaFile.lastIndexOf('\\');
+    }
+    String fileName = lastSlashIndex != -1 ? javaFile.substring(lastSlashIndex + 1) : javaFile;
+    String className = fileName.substring(0, fileName.length() - ".java".length());
+    return packageString.replace('.', '/') + '/' + className;
+  }
+
+  /** Filters a list of jars, keeping anything matching the passed predicate. */
+  private static void filterJars(List<Path> jars, Path output, Predicate<String> shouldKeep)
+      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.test(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 shouldKeepClass(List<String> archiveFileNamePrefixes, String name) {
+    if (!name.endsWith(".class")) {
+      return false;
+    }
+    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;
+  }
+
+  private static boolean shouldKeepJavaFile(List<String> archiveFileNamePrefixes, String name) {
+    if (!name.endsWith(".java")) {
+      return false;
+    }
+    String nameWithoutJava = name.substring(0, name.length() - ".java".length());
+    return archiveFileNamePrefixes.contains(nameWithoutJava);
+  }
+}
diff --git a/aspect/tools/src/com/google/idea/blaze/aspect/OptionParser.java b/aspect/tools/src/com/google/idea/blaze/aspect/OptionParser.java
new file mode 100644
index 0000000..9e9bf4c
--- /dev/null
+++ b/aspect/tools/src/com/google/idea/blaze/aspect/OptionParser.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+/**
+ * A very light-weight command-line argument parser.
+ *
+ * <p>Expects args array of the form ["--arg_name1", "value1", "--arg_name2", "value2", ...]
+ */
+public final class OptionParser {
+  private OptionParser() {}
+
+  /**
+   * Searches for '--[name]' in the args list, and parses the following argument using the supplied
+   * parser. Returns null if not such argument found.
+   *
+   * @throws IllegalArgumentException if '--[name]' appears more than once, as this indicates a
+   *     programming error on the client side.
+   */
+  @Nullable
+  static <T> T parseSingleOption(String[] args, String name, Function<String, T> parser) {
+    String argName = "--" + name;
+    for (int i = 0; i < args.length; i++) {
+      if (!args[i].equals(argName)) {
+        continue;
+      }
+      if (i == args.length - 1) {
+        throw new IllegalArgumentException("Expected value after " + args[i]);
+      }
+      for (int j = i + 2; j < args.length; j++) {
+        if (args[j].equals(argName)) {
+          throw new IllegalArgumentException("Expected " + args[i] + " to appear at most once");
+        }
+      }
+      return parser.apply(args[i + 1]);
+    }
+    return null;
+  }
+
+  /**
+   * Parse all '--[name]' flags in `args`, return a list of all their parsed values.
+   *
+   * <p>When args[i] = `--[name]`, then args[i+1] is parsed for the value, using `parser`.
+   *
+   * <p>Otherwise args[i] is ignored.
+   *
+   * <p>Returns an empty list if no occurrences of the flag are found.
+   */
+  static <T> List<T> parseMultiOption(String[] args, String name, Function<String, T> parser) {
+    List<T> result = null;
+    String argName = "--" + name;
+    for (int i = 0; i < args.length; i++) {
+      if (!args[i].equals(argName)) {
+        continue;
+      }
+      if (i == args.length - 1) {
+        throw new IllegalArgumentException("Expected value after " + args[i]);
+      }
+      T parsed = parser.apply(args[i + 1]);
+      if (parsed != null) {
+        if (result == null) {
+          result = new ArrayList<>();
+        }
+        result.add(parsed);
+      }
+      i++;
+    }
+    return result == null ? ImmutableList.of() : result;
+  }
+}
diff --git a/aspect/tools/src/com/google/idea/blaze/aspect/PackageParser.java b/aspect/tools/src/com/google/idea/blaze/aspect/PackageParser.java
new file mode 100644
index 0000000..a3a44c1
--- /dev/null
+++ b/aspect/tools/src/com/google/idea/blaze/aspect/PackageParser.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+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.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.ArtifactLocation;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.JavaSourcePackage;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.PackageManifest;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+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.Nullable;
+
+/** Parses the package string from each of the source .java files. */
+public class PackageParser {
+
+  /** The options for a {@link PackageParser} action. */
+  @VisibleForTesting
+  static final class PackageParserOptions {
+    List<ArtifactLocation> sources;
+    Path outputManifest;
+  }
+
+  @VisibleForTesting
+  static PackageParserOptions parseArgs(String[] args) {
+    args = parseParamFileIfUsed(args);
+    PackageParserOptions options = new PackageParserOptions();
+    options.sources =
+        OptionParser.parseSingleOption(args, "sources", ArtifactLocationParser::parseList);
+    options.outputManifest =
+        OptionParser.parseSingleOption(
+            args, "output_manifest", string -> FileSystems.getDefault().getPath(string));
+    return options;
+  }
+
+  private static final Logger logger = Logger.getLogger(PackageParser.class.getName());
+
+  private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([\\w\\.]+)");
+
+  public static void main(String[] args) throws Exception {
+    PackageParserOptions options = parseArgs(args);
+    Preconditions.checkNotNull(options.outputManifest);
+
+    try {
+      PackageParser parser = new PackageParser(PackageParserIoProvider.INSTANCE);
+      Map<ArtifactLocation, String> outputMap = parser.parsePackageStrings(options.sources);
+      parser.writeManifest(outputMap, options.outputManifest);
+    } catch (Throwable e) {
+      logger.log(Level.SEVERE, "Error parsing package strings", e);
+      System.exit(1);
+    }
+    System.exit(0);
+  }
+
+  private static Path getExecutionPath(ArtifactLocation location) {
+    return Paths.get(location.getRootExecutionPathFragment(), location.getRelativePath());
+  }
+
+  private static String[] parseParamFileIfUsed(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
+  PackageParser(PackageParserIoProvider ioProvider) {
+    this.ioProvider = ioProvider;
+  }
+
+  @VisibleForTesting
+  void writeManifest(Map<ArtifactLocation, String> sourceToPackageMap, Path outputFile)
+      throws IOException {
+    PackageManifest.Builder builder = PackageManifest.newBuilder();
+    for (Entry<ArtifactLocation, String> entry : sourceToPackageMap.entrySet()) {
+      JavaSourcePackage.Builder srcBuilder =
+          JavaSourcePackage.newBuilder()
+              .setPackageString(entry.getValue())
+              .setArtifactLocation(entry.getKey());
+      builder.addSources(srcBuilder.build());
+    }
+
+    try {
+      ioProvider.writeProto(builder.build(), outputFile);
+    } catch (IOException e) {
+      logger.log(Level.SEVERE, "Error writing package manifest", e);
+      throw e;
+    }
+  }
+
+  @VisibleForTesting
+  Map<ArtifactLocation, String> parsePackageStrings(List<ArtifactLocation> sources)
+      throws Exception {
+
+    ListeningExecutorService executorService =
+        MoreExecutors.listeningDecorator(
+            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));
+
+    Map<ArtifactLocation, ListenableFuture<String>> futures = Maps.newHashMap();
+    for (final ArtifactLocation source : sources) {
+      futures.put(source, executorService.submit(() -> getDeclaredPackageOfJavaFile(source)));
+    }
+    Map<ArtifactLocation, String> map = Maps.newHashMap();
+    for (Entry<ArtifactLocation, ListenableFuture<String>> entry : futures.entrySet()) {
+      String value = entry.getValue().get();
+      if (value != null) {
+        map.put(entry.getKey(), value);
+      }
+    }
+    return map;
+  }
+
+  @Nullable
+  private String getDeclaredPackageOfJavaFile(ArtifactLocation source) {
+    try (BufferedReader reader = ioProvider.getReader(getExecutionPath(source))) {
+      return parseDeclaredPackage(reader);
+
+    } catch (IOException e) {
+      logger.log(Level.WARNING, "Error parsing package string from java source: " + source, e);
+      return null;
+    }
+  }
+
+  @Nullable
+  private static String parseDeclaredPackage(BufferedReader reader) throws IOException {
+    String line;
+    while ((line = reader.readLine()) != null) {
+      Matcher packageMatch = PACKAGE_PATTERN.matcher(line);
+      if (packageMatch.find()) {
+        return packageMatch.group(1);
+      }
+    }
+    return null;
+  }
+}
diff --git a/aspect/tools/src/com/google/idea/blaze/aspect/PackageParserIoProvider.java b/aspect/tools/src/com/google/idea/blaze/aspect/PackageParserIoProvider.java
new file mode 100644
index 0000000..3d965c2
--- /dev/null
+++ b/aspect/tools/src/com/google/idea/blaze/aspect/PackageParserIoProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.repackaged.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;
+
+/** Provides a BufferedReader for the source java files, and a writer for the output proto */
+@VisibleForTesting
+public class PackageParserIoProvider {
+
+  static final PackageParserIoProvider INSTANCE = new PackageParserIoProvider();
+
+  void writeProto(MessageLite message, Path file) throws IOException {
+    try (OutputStream out = Files.newOutputStream(file)) {
+      message.writeTo(out);
+    }
+  }
+
+  BufferedReader getReader(Path file) throws IOException {
+    return Files.newBufferedReader(file, StandardCharsets.UTF_8);
+  }
+}
diff --git a/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/ArtifactLocationParserTest.java b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/ArtifactLocationParserTest.java
new file mode 100644
index 0000000..9985f48
--- /dev/null
+++ b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/ArtifactLocationParserTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.ArtifactLocation;
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests {@link ArtifactLocationParser}. */
+@RunWith(JUnit4.class)
+public class ArtifactLocationParserTest {
+
+  @Test
+  public void testConverterSourceArtifact() {
+    ArtifactLocation parsed =
+        ArtifactLocationParser.parse(Joiner.on(',').join("", "test.java", "0"));
+    assertThat(parsed)
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("test.java").toString())
+                .setIsSource(true)
+                .setIsExternal(false)
+                .build());
+  }
+
+  @Test
+  public void testConverterDerivedArtifact() {
+    ArtifactLocation parsed =
+        ArtifactLocationParser.parse(Joiner.on(',').join("bin", "java/com/test.java", "0"));
+    assertThat(parsed)
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRootExecutionPathFragment(Paths.get("bin").toString())
+                .setRelativePath(Paths.get("java/com/test.java").toString())
+                .setIsSource(false)
+                .setIsExternal(false)
+                .build());
+  }
+
+  @Test
+  public void testConverterExternal() {
+    ArtifactLocation externalArtifact =
+        ArtifactLocationParser.parse(Joiner.on(',').join("", "test.java", "1"));
+    assertThat(externalArtifact)
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("test.java").toString())
+                .setIsSource(true)
+                .setIsExternal(true)
+                .build());
+    ArtifactLocation nonExternalArtifact =
+        ArtifactLocationParser.parse(Joiner.on(',').join("", "test.java", "0"));
+    assertThat(nonExternalArtifact)
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("test.java").toString())
+                .setIsSource(true)
+                .setIsExternal(false)
+                .build());
+  }
+
+  @Test
+  public void testInvalidFormatFails() {
+    assertFails("/root", ArtifactLocationParser.INVALID_FORMAT);
+    assertFails("/root,exec,rel,extra", ArtifactLocationParser.INVALID_FORMAT);
+  }
+
+  @Test
+  public void testParsingArtifactLocationList() {
+    String input =
+        Joiner.on(':')
+            .join(
+                Joiner.on(',').join("", "test.java", "0"),
+                Joiner.on(',').join("bin", "java/com/test.java", "0"),
+                Joiner.on(',').join("", "test.java", "1"));
+
+    assertThat(ArtifactLocationParser.parseList(input))
+        .containsExactly(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("test.java").toString())
+                .setIsSource(true)
+                .build(),
+            ArtifactLocation.newBuilder()
+                .setRootExecutionPathFragment(Paths.get("bin").toString())
+                .setRelativePath(Paths.get("java/com/test.java").toString())
+                .setIsSource(false)
+                .build(),
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("test.java").toString())
+                .setIsSource(true)
+                .setIsExternal(true)
+                .build());
+  }
+
+  private void assertFails(String input, String expectedError) {
+    try {
+      ArtifactLocationParser.parse(input);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage(expectedError);
+    }
+  }
+}
diff --git a/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/JarFilterTest.java b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/JarFilterTest.java
new file mode 100644
index 0000000..8153676
--- /dev/null
+++ b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/JarFilterTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.google.idea.blaze.aspect.JarFilter.JarFilterOptions;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link JarFilter} */
+@RunWith(JUnit4.class)
+public class JarFilterTest {
+
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+
+  @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.shouldKeepClass(prefixes, "com/google/foo/Foo.class")).isTrue();
+    assertThat(JarFilter.shouldKeepClass(prefixes, "com/google/foo/Foo$Inner.class")).isTrue();
+    assertThat(JarFilter.shouldKeepClass(prefixes, "com/google/bar/Bar.class")).isTrue();
+    assertThat(JarFilter.shouldKeepClass(prefixes, "com/google/foo/Foo/NotFoo.class")).isFalse();
+    assertThat(JarFilter.shouldKeepClass(prefixes, "wrong/com/google/foo/Foo.class")).isFalse();
+  }
+
+  @Test
+  public void fullIntegrationTest() throws Exception {
+    File fooJava = folder.newFile("Foo.java");
+    Files.write("package com.google.foo; class Foo { class Inner {} }".getBytes(UTF_8), fooJava);
+
+    File barJava = folder.newFile("Bar.java");
+    Files.write("package com.google.foo.bar; class Bar {}".getBytes(UTF_8), barJava);
+
+    File srcJar = folder.newFile("gen.srcjar");
+    try (ZipOutputStream zo = new ZipOutputStream(new FileOutputStream(srcJar))) {
+      zo.putNextEntry(new ZipEntry("com/google/foo/gen/Gen.java"));
+      zo.write("package gen; class Gen {}".getBytes(UTF_8));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/gen/Gen2.java"));
+      zo.write("package gen; class Gen2 {}".getBytes(UTF_8));
+      zo.closeEntry();
+    }
+
+    File src3Jar = folder.newFile("gen3.srcjar");
+    try (ZipOutputStream zo = new ZipOutputStream(new FileOutputStream(src3Jar))) {
+      zo.putNextEntry(new ZipEntry("com/google/foo/gen/Gen3.java"));
+      zo.write("package gen; class Gen3 {}".getBytes(UTF_8));
+      zo.closeEntry();
+    }
+
+    File filterJar = folder.newFile("foo.jar");
+    try (ZipOutputStream zo = new ZipOutputStream(new FileOutputStream(filterJar))) {
+      zo.putNextEntry(new ZipEntry("com/google/foo/Foo.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/Foo$Inner.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/bar/Bar.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen2.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen3.class"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/Foo2.class"));
+      zo.closeEntry();
+    }
+    File filterSrcJar = folder.newFile("foo-src.jar");
+    try (ZipOutputStream zo = new ZipOutputStream(new FileOutputStream(filterSrcJar))) {
+      zo.putNextEntry(new ZipEntry("com/google/foo/Foo.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/bar/Bar.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen2.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("gen/Gen3.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/Foo2.java"));
+      zo.closeEntry();
+      zo.putNextEntry(new ZipEntry("com/google/foo/bar/Bar2.java"));
+      zo.closeEntry();
+    }
+
+    File filteredJar = folder.newFile("foo-filtered-gen.jar");
+    File filteredSourceJar = folder.newFile("foo-filtered-gen-src.jar");
+
+    String[] args =
+        new String[] {
+          "--keep_java_file",
+          fooJava.getPath(),
+          "--keep_java_file",
+          barJava.getPath(),
+          "--keep_source_jar",
+          srcJar.getPath(),
+          "--keep_source_jar",
+          src3Jar.getPath(),
+          "--filter_jar",
+          filterJar.getPath(),
+          "--filter_source_jar",
+          filterSrcJar.getPath(),
+          "--filtered_jar",
+          filteredJar.getPath(),
+          "--filtered_source_jar",
+          filteredSourceJar.getPath()
+        };
+    JarFilterOptions options = JarFilter.parseArgs(args);
+    JarFilter.main(options);
+
+    List<String> filteredJarNames = Lists.newArrayList();
+    try (ZipFile zipFile = new ZipFile(filteredJar)) {
+      Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry zipEntry = entries.nextElement();
+        filteredJarNames.add(zipEntry.getName());
+      }
+    }
+
+    List<String> filteredSourceJarNames = Lists.newArrayList();
+    try (ZipFile zipFile = new ZipFile(filteredSourceJar)) {
+      Enumeration<? extends ZipEntry> entries = zipFile.entries();
+      while (entries.hasMoreElements()) {
+        ZipEntry zipEntry = entries.nextElement();
+        filteredSourceJarNames.add(zipEntry.getName());
+      }
+    }
+
+    assertThat(filteredJarNames)
+        .containsExactly(
+            "com/google/foo/Foo.class",
+            "com/google/foo/Foo$Inner.class",
+            "com/google/foo/bar/Bar.class",
+            "gen/Gen.class",
+            "gen/Gen2.class",
+            "gen/Gen3.class");
+
+    assertThat(filteredSourceJarNames)
+        .containsExactly(
+            "com/google/foo/Foo.java",
+            "com/google/foo/bar/Bar.java",
+            "gen/Gen.java",
+            "gen/Gen2.java",
+            "gen/Gen3.java");
+  }
+}
diff --git a/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/OptionParserTest.java b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/OptionParserTest.java
new file mode 100644
index 0000000..c506e7a
--- /dev/null
+++ b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/OptionParserTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OptionParser}. */
+@RunWith(JUnit4.class)
+public class OptionParserTest {
+  @Test
+  public void testParseSingleOption() throws Exception {
+    // Test parsing of a flag that doesn't occur.
+    assertThat(
+            OptionParser.parseSingleOption(
+                new String[] {"--foo", "1", "--bar", "2"}, "unknown", String::toString))
+        .isNull();
+    // Test parsing of an integer flag.
+    assertThat(
+            OptionParser.<Integer>parseSingleOption(
+                new String[] {"--foo", "1", "--bar", "2"}, "bar", Integer::parseInt))
+        .isEqualTo(2);
+    // Test parsing of a flag that's missing its value: if there's a subsequent entry and it can be
+    // parsed, it'll be taken as the flag's value.
+    assertThat(
+            OptionParser.parseSingleOption(
+                new String[] {"--foo", "--bar", "--baz"}, "foo", String::toString))
+        .isEqualTo("--bar");
+    // Test parsing of a flag that's missing its value: if there's no subsequent entry, an exception
+    // is thrown.
+    try {
+      OptionParser.parseSingleOption(new String[] {"--foo", "1", "--bar"}, "bar", String::toString);
+      fail("Expected failure");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Expected value after --bar");
+    }
+    // Test that a single-value flag should not appear multiple times.
+    try {
+      OptionParser.parseSingleOption(
+          new String[] {"--foo", "1", "--foo", "2"}, "foo", String::toString);
+      fail("Expected failure");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Expected --foo to appear at most once");
+    }
+  }
+
+  @Test
+  public void testParseMultiOption() throws Exception {
+    // Test parsing of a flag that doesn't occur.
+    assertThat(
+            OptionParser.parseMultiOption(
+                new String[] {"--foo", "1", "--bar", "2"}, "unknown", String::toString))
+        .isEmpty();
+    // Test parsing of an integer multi-flag. Ensure that the flag name is matched wholly, and
+    // doesn't match other flags that it's a prefix of.
+    assertThat(
+            OptionParser.parseMultiOption(
+                new String[] {"--foo", "1", "--foooo", "2", "--foo", "3"},
+                "foo",
+                Integer::parseInt))
+        .containsExactly(1, 3)
+        .inOrder();
+    // Test that the --name=value style of flags is not supported (for sake of simplicity).
+    assertThat(
+            OptionParser.parseMultiOption(
+                new String[] {"--foo", "1", "--foo=2", "--foo", "3"}, "foo", Integer::parseInt))
+        .containsExactly(1, 3)
+        .inOrder();
+    // Test parsing a multi-flag where one occurrence cannot consume a value.
+    try {
+      OptionParser.parseMultiOption(
+          new String[] {"--foo", "1", "--bar", "2", "--foo"}, "foo", String::toString);
+      fail("Expected failure");
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Expected value after --foo");
+    }
+  }
+}
diff --git a/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/PackageParserTest.java b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/PackageParserTest.java
new file mode 100644
index 0000000..61a6892
--- /dev/null
+++ b/aspect/tools/tests/unittests/com/google/idea/blaze/aspect/PackageParserTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2017 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.idea.blaze.aspect;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.ArtifactLocation;
+import com.google.repackaged.protobuf.MessageLite;
+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.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** 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 final List<ArtifactLocation> sourceLocations = Lists.newArrayList();
+    private StringWriter writer = new StringWriter();
+
+    public MockPackageParserIoProvider addSource(ArtifactLocation source, String javaSrc) {
+      try {
+        Path path = Paths.get(source.getRootExecutionPathFragment(), source.getRelativePath());
+        sources.put(path, new ByteArrayInputStream(javaSrc.getBytes("UTF-8")));
+        sourceLocations.add(source);
+
+      } catch (UnsupportedEncodingException | InvalidPathException e) {
+        fail(e.getMessage());
+      }
+      return this;
+    }
+
+    public List<ArtifactLocation> getSourceLocations() {
+      return Lists.newArrayList(sourceLocations);
+    }
+
+    @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(MessageLite message, Path file) throws IOException {
+      writer.write(message.toString());
+    }
+  }
+
+  private static final ArtifactLocation DUMMY_SOURCE_ARTIFACT =
+      ArtifactLocation.newBuilder()
+          .setRelativePath("java/com/google/Foo.java")
+          .setIsSource(true)
+          .build();
+
+  private static final ArtifactLocation DUMMY_DERIVED_ARTIFACT =
+      ArtifactLocation.newBuilder()
+          .setRootExecutionPathFragment("bin")
+          .setRelativePath("java/com/google/Bla.java")
+          .setIsSource(false)
+          .build();
+
+  private static final ArtifactLocation DUMMY_SCALA_SOURCE_ARTIFACT =
+      ArtifactLocation.newBuilder()
+          .setRelativePath("scala/com/google/Foo.scala")
+          .setIsSource(true)
+          .build();
+
+  private static final ArtifactLocation DUMMY_SCALA_DERIVED_ARTIFACT =
+      ArtifactLocation.newBuilder()
+          .setRootExecutionPathFragment("bin")
+          .setRelativePath("scala/com/google/Bla.scala")
+          .setIsSource(false)
+          .build();
+
+  private MockPackageParserIoProvider mockIoProvider;
+  private PackageParser parser;
+
+  @Before
+  public void setUp() {
+    mockIoProvider = new MockPackageParserIoProvider();
+    parser = new PackageParser(mockIoProvider);
+  }
+
+  private Map<ArtifactLocation, String> parsePackageStrings() throws Exception {
+    List<ArtifactLocation> sources = mockIoProvider.getSourceLocations();
+    return parser.parsePackageStrings(sources);
+  }
+
+  @Test
+  public void testParseCommandLineArguments() throws Exception {
+    String[] args =
+        new String[] {
+          "--output_manifest",
+          "/tmp/out.manifest",
+          "--sources",
+          Joiner.on(':').join(",java/com/google/Foo.java,0", "bin/out,java/com/google/Bla.java,0")
+        };
+    PackageParser.PackageParserOptions options = PackageParser.parseArgs(args);
+    assertThat(options.outputManifest.toString())
+        .isEqualTo(Paths.get("/tmp/out.manifest").toString());
+    assertThat(options.sources).hasSize(2);
+    assertThat(options.sources.get(0))
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("java/com/google/Foo.java").toString())
+                .setIsSource(true)
+                .build());
+    assertThat(options.sources.get(1))
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRootExecutionPathFragment(Paths.get("bin/out").toString())
+                .setRelativePath(Paths.get("java/com/google/Bla.java").toString())
+                .setIsSource(false)
+                .build());
+  }
+
+  @Test
+  public void testReadNoSources() throws Exception {
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).isEmpty();
+  }
+
+  @Test
+  public void testSingleRead() throws Exception {
+    mockIoProvider.addSource(DUMMY_SOURCE_ARTIFACT, "package com.google;\n public class Bla {}\"");
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).hasSize(1);
+    assertThat(map).containsEntry(DUMMY_SOURCE_ARTIFACT, "com.google");
+  }
+
+  @Test
+  public void testMultiRead() throws Exception {
+    mockIoProvider
+        .addSource(DUMMY_SOURCE_ARTIFACT, "package com.test;\n public class Foo {}\"")
+        .addSource(DUMMY_DERIVED_ARTIFACT, "package com.other;\n public class Bla {}\"");
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).hasSize(2);
+    assertThat(map).containsEntry(DUMMY_SOURCE_ARTIFACT, "com.test");
+    assertThat(map).containsEntry(DUMMY_DERIVED_ARTIFACT, "com.other");
+  }
+
+  @Test
+  public void testReadSomeInvalid() throws Exception {
+    mockIoProvider
+        .addSource(DUMMY_SOURCE_ARTIFACT, "package %com.test;\n public class Foo {}\"")
+        .addSource(DUMMY_DERIVED_ARTIFACT, "package com.other;\n public class Bla {}\"");
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).hasSize(1);
+    assertThat(map).containsEntry(DUMMY_DERIVED_ARTIFACT, "com.other");
+  }
+
+  @Test
+  public void testReadAllInvalid() throws Exception {
+    mockIoProvider
+        .addSource(DUMMY_SOURCE_ARTIFACT, "#package com.test;\n public class Foo {}\"")
+        .addSource(DUMMY_DERIVED_ARTIFACT, "package %com.other\n public class Bla {}\"");
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).isEmpty();
+  }
+
+  @Test
+  public void testReadScala() throws Exception {
+    mockIoProvider
+        .addSource(DUMMY_SCALA_SOURCE_ARTIFACT, "package com.test\n class Foo {}\"")
+        .addSource(DUMMY_SCALA_DERIVED_ARTIFACT, "package com.other {}\n object Bla {}\"");
+    Map<ArtifactLocation, String> map = parsePackageStrings();
+    assertThat(map).containsEntry(DUMMY_SCALA_SOURCE_ARTIFACT, "com.test");
+    assertThat(map).containsEntry(DUMMY_SCALA_DERIVED_ARTIFACT, "com.other");
+  }
+
+  @Test
+  public void testWriteEmptyMap() throws Exception {
+    parser.writeManifest(Maps.newHashMap(), Paths.get("/java/com/google/test.manifest"));
+    assertThat(mockIoProvider.writer.toString()).isEmpty();
+  }
+
+  @Test
+  public void testWriteMap() throws Exception {
+    Map<ArtifactLocation, String> map =
+        ImmutableMap.of(DUMMY_SOURCE_ARTIFACT, "com.google", DUMMY_DERIVED_ARTIFACT, "com.other");
+    parser.writeManifest(map, Paths.get("/java/com/google/test.manifest"));
+
+    String writtenString = mockIoProvider.writer.toString();
+    assertThat(writtenString)
+        .contains(String.format("relative_path: \"%s\"", DUMMY_SOURCE_ARTIFACT.getRelativePath()));
+    assertThat(writtenString).contains("package_string: \"com.google\"");
+
+    assertThat(writtenString)
+        .contains(
+            String.format(
+                "root_execution_path_fragment: \"%s\"",
+                DUMMY_DERIVED_ARTIFACT.getRootExecutionPathFragment()));
+    assertThat(writtenString)
+        .contains(String.format("relative_path: \"%s\"", DUMMY_DERIVED_ARTIFACT.getRelativePath()));
+    assertThat(writtenString).contains("package_string: \"com.other\"");
+  }
+
+  @Test
+  public void testHandlesOldFormat() throws Exception {
+    String[] args =
+        new String[] {
+          "--output_manifest",
+          "/tmp/out.manifest",
+          "--sources",
+          Joiner.on(':')
+              .join(
+                  ",java/com/google/Foo.java,/usr/local/google/code",
+                  "bin,java/com/google/Bla.java,/usr/local/_tmp/code/bin")
+        };
+    PackageParser.PackageParserOptions options = PackageParser.parseArgs(args);
+    assertThat(options.outputManifest.toString())
+        .isEqualTo(Paths.get("/tmp/out.manifest").toString());
+    assertThat(options.sources).hasSize(2);
+    assertThat(options.sources.get(0))
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRelativePath(Paths.get("java/com/google/Foo.java").toString())
+                .setIsSource(true)
+                .build());
+    assertThat(options.sources.get(1))
+        .isEqualTo(
+            ArtifactLocation.newBuilder()
+                .setRootExecutionPathFragment(Paths.get("bin").toString())
+                .setRelativePath(Paths.get("java/com/google/Bla.java").toString())
+                .setIsSource(false)
+                .build());
+  }
+}
diff --git a/aswb/2.3/src/com/android/tools/idea/npw/project/AndroidProjectPaths.java b/aswb/2.3/src/com/android/tools/idea/npw/project/AndroidProjectPaths.java
new file mode 100644
index 0000000..8124a41
--- /dev/null
+++ b/aswb/2.3/src/com/android/tools/idea/npw/project/AndroidProjectPaths.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 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.android.tools.idea.npw.project;
+
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Fake {@link AndroidProjectPaths} for 2.3. */
+public interface AndroidProjectPaths {
+  File getModuleRoot();
+
+  File getSrcDirectory(@Nullable String packageName);
+
+  File getTestDirectory(@Nullable String packageName);
+
+  File getResDirectory();
+
+  File getAidlDirectory(@Nullable String packageName);
+
+  File getManifestDirectory();
+}
diff --git a/aswb/2.3/src/com/android/tools/idea/res/IdeResourceNameValidator.java b/aswb/2.3/src/com/android/tools/idea/res/IdeResourceNameValidator.java
new file mode 100644
index 0000000..117da24
--- /dev/null
+++ b/aswb/2.3/src/com/android/tools/idea/res/IdeResourceNameValidator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 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.android.tools.idea.res;
+
+import com.android.resources.ResourceFolderType;
+
+/** Fake {@link IdeResourceNameValidator} for 2.3, renamed from {@link ResourceNameValidator}. */
+public class IdeResourceNameValidator {
+  public final ResourceNameValidator delegate;
+
+  public IdeResourceNameValidator(ResourceNameValidator delegate) {
+    this.delegate = delegate;
+  }
+
+  public static IdeResourceNameValidator forFilename(
+      ResourceFolderType type, String implicitExtension) {
+    return new IdeResourceNameValidator(ResourceNameValidator.create(false, type));
+  }
+
+  public String getErrorText(String inputString) {
+    return delegate.getErrorText(inputString);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java
new file mode 100644
index 0000000..b1fa492
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.model;
+
+import com.android.tools.idea.model.AndroidModel;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import javax.annotation.Nullable;
+
+/** Compatibility adapter for {@link AndroidModel}. */
+public abstract class AndroidModelAdapter implements AndroidModel {
+  @Nullable
+  @Override
+  public Long getLastBuildTimestamp(Project project) {
+    return null;
+  }
+
+  public abstract boolean isClassFileOutOfDate(Module module, String fqcn, VirtualFile classFile);
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java
new file mode 100644
index 0000000..c91afbe
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.project;
+
+import com.android.tools.idea.gradle.util.Projects;
+import com.android.tools.idea.project.AndroidProjectInfo;
+import com.intellij.openapi.project.Project;
+
+/** Compatibility adapter for {@link AndroidProjectInfo}. */
+public class AndroidProjectInfoAdapter {
+  public static boolean requiredAndroidModelMissing(Project project) {
+    return Projects.requiredAndroidModelMissing(project);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java
new file mode 100644
index 0000000..8af0cec
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.project;
+
+import com.android.tools.idea.npw.project.AndroidSourceSet;
+import com.android.tools.idea.project.BuildSystemService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.facet.AndroidFacet;
+
+/** Compatibility adapter for {@link BuildSystemService}. */
+public abstract class BuildSystemServiceAdapter extends BuildSystemService {
+  public abstract String mergeBuildFiles(
+      String dependencies,
+      String destinationContents,
+      Project project,
+      @Nullable String supportLibVersionFilter);
+
+  public abstract List<AndroidSourceSet> getSourceSets(
+      AndroidFacet facet, @Nullable VirtualFile targetDirectory);
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java
new file mode 100644
index 0000000..b58d477
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.res;
+
+import com.android.tools.idea.res.AppResourceRepository;
+import com.intellij.openapi.module.Module;
+
+/** Compatibility adapter for {@link AppResourceRepository}. */
+public class AppResourceRepositoryAdapter {
+  public static AppResourceRepository getOrCreateInstance(Module module) {
+    return AppResourceRepository.getAppResources(module, true);
+  }
+
+  public static AppResourceRepository findExistingInstance(Module module) {
+    return AppResourceRepository.getAppResources(module, false);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java
new file mode 100644
index 0000000..e7361b0
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.resources.ResourceType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.actions.CreateXmlResourceDialog;
+
+/** Compatibility adapter for {@link CreateXmlResourceDialog}. */
+public class CreateXmlResourceDialogAdapter {
+  public static ValidationInfo checkIfResourceAlreadyExists(
+      Project project,
+      VirtualFile resourceDir,
+      String resourceName,
+      @Nullable String resourceValue,
+      ResourceType resourceType,
+      List<String> dirNames,
+      String fileName) {
+    return CreateXmlResourceDialog.checkIfResourceAlreadyExists(
+        project, resourceDir, resourceName, resourceType, dirNames, fileName);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java
new file mode 100644
index 0000000..620cf18
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.tools.idea.res.IdeResourceNameValidator;
+import com.android.tools.idea.res.ResourceNameValidator;
+import org.jetbrains.android.actions.CreateXmlResourcePanel;
+
+/** Compatibility adapter for {@link CreateXmlResourcePanel}. */
+public abstract class CreateXmlResourcePanelAdapter implements CreateXmlResourcePanel {
+  protected abstract IdeResourceNameValidator getResourceNameValidatorCompat();
+
+  @Override
+  public ResourceNameValidator getResourceNameValidator() {
+    return getResourceNameValidatorCompat().delegate;
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java
new file mode 100644
index 0000000..c7d8f3d
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.res.IdeResourceNameValidator;
+import com.android.tools.idea.res.ResourceNameValidator;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import org.jetbrains.android.actions.CreateXmlResourcePanel;
+import org.jetbrains.android.actions.NewResourceCreationHandler;
+
+/** Compatibility adapter for {@link NewResourceCreationHandler}. */
+public abstract class NewResourceCreationHandlerAdapter implements NewResourceCreationHandler {
+  protected abstract CreateXmlResourcePanel createNewResourceValuePanelCompat(
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
+      @Nullable String resourceName,
+      @Nullable String resourceValue,
+      boolean chooseName,
+      boolean chooseValue,
+      boolean chooseFilename,
+      @Nullable VirtualFile defaultFile,
+      @Nullable VirtualFile contextFile,
+      Function<Module, IdeResourceNameValidator> nameValidatorFactory);
+
+  @Override
+  public CreateXmlResourcePanel createNewResourceValuePanel(
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
+      @Nullable String resourceName,
+      @Nullable String resourceValue,
+      boolean chooseName,
+      boolean chooseValue,
+      boolean chooseFilename,
+      @Nullable VirtualFile defaultFile,
+      @Nullable VirtualFile contextFile,
+      Function<Module, ResourceNameValidator> nameValidatorFactory) {
+    return createNewResourceValuePanelCompat(
+        module,
+        resourceType,
+        folderType,
+        resourceName,
+        resourceValue,
+        chooseName,
+        chooseValue,
+        chooseFilename,
+        defaultFile,
+        contextFile,
+        m -> new IdeResourceNameValidator(nameValidatorFactory.apply(m)));
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java b/aswb/2.3/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java
new file mode 100644
index 0000000..471b696
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.android.sync.BlazeNdkDependencySyncPlugin;
+
+/** Compatibility indirection for {@link BlazeNdkDependencySyncPlugin}. */
+public class BlazeNdkDependencySyncPluginCompat {
+  public static final ImmutableMap<String, String> REQUIRED_PLUGINS =
+      ImmutableMap.of(
+          "Android NDK Support",
+          "com.android.tools.ndk",
+          "NDK WorkspaceManager Support",
+          "com.android.tools.ndk.workspace");
+}
diff --git a/aswb/3.0/src/META-INF/ndk-workspace-contents.xml b/aswb/3.0/src/META-INF/ndk-workspace-contents.xml
new file mode 100644
index 0000000..da35648
--- /dev/null
+++ b/aswb/3.0/src/META-INF/ndk-workspace-contents.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright 2017 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.
+  -->
+<idea-plugin>
+  <extensions defaultExtensionNs="com.android.tools.ndk">
+    <workspaceProvider implementation="com.google.idea.blaze.android.ndk.workspace.BlazeNdkWorkspaceProvider"/>
+  </extensions>
+</idea-plugin>
\ No newline at end of file
diff --git a/aswb/3.0/src/com/google/idea/blaze/android/ndk/workspace/BlazeNdkWorkspaceProvider.java b/aswb/3.0/src/com/google/idea/blaze/android/ndk/workspace/BlazeNdkWorkspaceProvider.java
new file mode 100644
index 0000000..2a3353e
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/blaze/android/ndk/workspace/BlazeNdkWorkspaceProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.ndk.workspace;
+
+import com.android.tools.ndk.workspace.NdkWorkspaceProvider;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import javax.annotation.Nullable;
+
+/** Extension to provide an NDK workspace. */
+public class BlazeNdkWorkspaceProvider extends NdkWorkspaceProvider {
+  @Nullable
+  @Override
+  public OCWorkspace findNdkWorkspace(Project project) {
+    if (Blaze.isBlazeProject(project)) {
+      return BlazeCWorkspace.getInstance(project);
+    }
+    return null;
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java
new file mode 100644
index 0000000..c992e5a
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/model/AndroidModelAdapter.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.model;
+
+import com.android.tools.idea.model.AndroidModel;
+
+/** Compatibility adapter for {@link AndroidModel}. */
+public abstract class AndroidModelAdapter implements AndroidModel {}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java
new file mode 100644
index 0000000..8ef7963
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/project/AndroidProjectInfoAdapter.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.project;
+
+import com.android.tools.idea.project.AndroidProjectInfo;
+import com.intellij.openapi.project.Project;
+
+/** Compatibility adapter for {@link AndroidProjectInfo}. */
+public class AndroidProjectInfoAdapter {
+  public static boolean requiredAndroidModelMissing(Project project) {
+    return AndroidProjectInfo.getInstance(project).requiredAndroidModelMissing();
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java
new file mode 100644
index 0000000..e6201dd
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/project/BuildSystemServiceAdapter.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.project;
+
+import com.android.tools.idea.project.BuildSystemService;
+
+/** Compatibility adapter for {@link BuildSystemService}. */
+public abstract class BuildSystemServiceAdapter extends BuildSystemService {}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java
new file mode 100644
index 0000000..890d132
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/res/AppResourceRepositoryAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.res;
+
+import com.android.tools.idea.res.AppResourceRepository;
+import com.intellij.openapi.module.Module;
+
+/** Compatibility adapter for {@link AppResourceRepository}. */
+public class AppResourceRepositoryAdapter {
+  public static AppResourceRepository getOrCreateInstance(Module module) {
+    return AppResourceRepository.getOrCreateInstance(module);
+  }
+
+  public static AppResourceRepository findExistingInstance(Module module) {
+    return AppResourceRepository.findExistingInstance(module);
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java
new file mode 100644
index 0000000..9a424e1
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourceDialogAdapter.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.resources.ResourceType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.actions.CreateXmlResourceDialog;
+
+/** Compatibility adapter for {@link CreateXmlResourceDialog}. */
+public class CreateXmlResourceDialogAdapter {
+  public static ValidationInfo checkIfResourceAlreadyExists(
+      Project project,
+      VirtualFile resourceDir,
+      String resourceName,
+      @Nullable String resourceValue,
+      ResourceType resourceType,
+      List<String> dirNames,
+      String fileName) {
+    return CreateXmlResourceDialog.checkIfResourceAlreadyExists(
+        project, resourceDir, resourceName, resourceValue, resourceType, dirNames, fileName);
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java
new file mode 100644
index 0000000..15cf7b5
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/CreateXmlResourcePanelAdapter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.tools.idea.res.IdeResourceNameValidator;
+import org.jetbrains.android.actions.CreateXmlResourcePanel;
+
+/** Compatibility adapter for {@link CreateXmlResourcePanel}. */
+public abstract class CreateXmlResourcePanelAdapter implements CreateXmlResourcePanel {
+
+  protected abstract IdeResourceNameValidator getResourceNameValidatorCompat();
+
+  @Override
+  public IdeResourceNameValidator getResourceNameValidator() {
+    return getResourceNameValidatorCompat();
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java
new file mode 100644
index 0000000..5c3f51f
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/resources/actions/NewResourceCreationHandlerAdapter.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.resources.actions;
+
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.res.IdeResourceNameValidator;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import org.jetbrains.android.actions.CreateXmlResourcePanel;
+import org.jetbrains.android.actions.NewResourceCreationHandler;
+
+/** Compatibility adapter for {@link NewResourceCreationHandler}. */
+public abstract class NewResourceCreationHandlerAdapter implements NewResourceCreationHandler {
+  public abstract CreateXmlResourcePanel createNewResourceValuePanelCompat(
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
+      @Nullable String resourceName,
+      @Nullable String resourceValue,
+      boolean chooseName,
+      boolean chooseValue,
+      boolean chooseFilename,
+      @Nullable VirtualFile defaultFile,
+      @Nullable VirtualFile contextFile,
+      Function<Module, IdeResourceNameValidator> nameValidatorFactory);
+
+  @Override
+  public CreateXmlResourcePanel createNewResourceValuePanel(
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
+      @Nullable String resourceName,
+      @Nullable String resourceValue,
+      boolean chooseName,
+      boolean chooseValue,
+      boolean chooseFilename,
+      @Nullable VirtualFile defaultFile,
+      @Nullable VirtualFile contextFile,
+      Function<Module, IdeResourceNameValidator> nameValidatorFactory) {
+    return createNewResourceValuePanelCompat(
+        module,
+        resourceType,
+        folderType,
+        resourceName,
+        resourceValue,
+        chooseName,
+        chooseValue,
+        chooseFilename,
+        defaultFile,
+        contextFile,
+        nameValidatorFactory);
+  }
+}
diff --git a/aswb/3.0/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java b/aswb/3.0/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java
new file mode 100644
index 0000000..3faaaf1
--- /dev/null
+++ b/aswb/3.0/src/com/google/idea/sdkcompat/android/sync/BlazeNdkDependencySyncPluginCompat.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 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.idea.sdkcompat.android.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.android.sync.BlazeNdkDependencySyncPlugin;
+
+/** Compatibility indirection for {@link BlazeNdkDependencySyncPlugin}. */
+public class BlazeNdkDependencySyncPluginCompat {
+  public static final ImmutableMap<String, String> REQUIRED_PLUGINS =
+      ImmutableMap.of("Android NDK Support", "com.android.tools.ndk");
+}
diff --git a/aswb/3.0/tests/unittests/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPathsTest.java b/aswb/3.0/tests/unittests/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPathsTest.java
new file mode 100644
index 0000000..ef64612
--- /dev/null
+++ b/aswb/3.0/tests/unittests/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPathsTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.npw.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.android.builder.model.SourceProvider;
+import com.android.tools.idea.npw.project.AndroidProjectPaths;
+import com.android.tools.idea.npw.project.AndroidSourceSet;
+import com.android.tools.idea.project.BuildSystemService;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.project.BlazeBuildSystemService;
+import com.google.idea.blaze.android.sync.model.idea.SourceProviderImpl;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.facet.FacetType;
+import com.intellij.facet.FacetTypeRegistry;
+import com.intellij.facet.impl.FacetTypeRegistryImpl;
+import com.intellij.mock.MockModule;
+import com.intellij.mock.MockVirtualFile;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaDirectoryService;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiPackage;
+import com.intellij.psi.impl.file.PsiPackageImpl;
+import java.io.File;
+import java.util.List;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.facet.AndroidFacetConfiguration;
+import org.jetbrains.android.facet.AndroidFacetType;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link BlazeAndroidProjectPaths}. */
+@RunWith(JUnit4.class)
+public class BlazeAndroidProjectPathsTest extends BlazeTestCase {
+  private VirtualFile root = new MockVirtualFile(true, "root");
+  private VirtualFile resource = new MockVirtualFile(true, "root/resource");
+  private VirtualFile target = new MockVirtualFile(true, "root/library/com/google/target");
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    mockFacetRegistry(applicationServices);
+    mockBlazeImportSettings(projectServices);
+    mockPsiPackage(applicationServices, projectServices);
+
+    registerExtensionPoint(
+            ExtensionPointName.create("com.android.project.buildSystemService"),
+            BuildSystemService.class)
+        .registerExtension(new BlazeBuildSystemService());
+  }
+
+  /**
+   * If we have a resource module and a target directory, then we can get the res dir from the
+   * module, and use the target directory for everything else.
+   */
+  @Test
+  public void getResourceSourceSetsWithTargetDirectory() {
+    AndroidFacet facet = mockResourceFacet();
+    File resourceFile = VfsUtilCore.virtualToIoFile(resource);
+    File targetFile = VfsUtilCore.virtualToIoFile(target);
+    List<AndroidSourceSet> sourceSets = AndroidSourceSet.getSourceSets(facet, target);
+    assertThat(sourceSets).hasSize(1);
+    AndroidSourceSet sourceSet = sourceSets.get(0);
+    AndroidProjectPaths paths = sourceSet.getPaths();
+    assertThat(sourceSet.getName()).isEqualTo("com.google.target");
+    assertThat(paths.getModuleRoot()).isEqualTo(resourceFile);
+    assertThat(paths.getSrcDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getTestDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getResDirectory()).isEqualTo(new File(resourceFile, "res"));
+    assertThat(paths.getAidlDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getManifestDirectory()).isEqualTo(targetFile);
+  }
+
+  /**
+   * If we have a target directory but no resource module, we'll assume the res dir is just
+   * target/res.
+   */
+  @Test
+  public void getWorkspaceSourceSetsWithTargetDirectory() {
+    AndroidFacet facet = mockWorkspaceFacet();
+    File rootFile = VfsUtilCore.virtualToIoFile(root);
+    File targetFile = VfsUtilCore.virtualToIoFile(target);
+    List<AndroidSourceSet> sourceSets = AndroidSourceSet.getSourceSets(facet, target);
+    assertThat(sourceSets).hasSize(1);
+    AndroidSourceSet sourceSet = sourceSets.get(0);
+    AndroidProjectPaths paths = sourceSet.getPaths();
+    assertThat(sourceSet.getName()).isEqualTo("com.google.target");
+    assertThat(paths.getModuleRoot()).isEqualTo(rootFile);
+    assertThat(paths.getSrcDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getTestDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getResDirectory()).isEqualTo(new File(targetFile, "res"));
+    assertThat(paths.getAidlDirectory(null)).isEqualTo(targetFile);
+    assertThat(paths.getManifestDirectory()).isEqualTo(targetFile);
+  }
+
+  /**
+   * If no target directory is given, but we have a resource module, we can still figure out some
+   * paths.
+   */
+  @Test
+  public void getResourceSourceSetsWithNoTargetDirectory() {
+    AndroidFacet facet = mockResourceFacet();
+    File rootFile = VfsUtilCore.virtualToIoFile(root);
+    File resourceFile = VfsUtilCore.virtualToIoFile(resource);
+    List<AndroidSourceSet> sourceSets = AndroidSourceSet.getSourceSets(facet, null);
+    assertThat(sourceSets).hasSize(1);
+    AndroidSourceSet sourceSet = sourceSets.get(0);
+    AndroidProjectPaths paths = sourceSet.getPaths();
+    assertThat(sourceSet.getName()).isEqualTo("com.google.resource");
+    assertThat(paths.getModuleRoot()).isEqualTo(resourceFile);
+    assertThat(paths.getSrcDirectory(null)).isEqualTo(resourceFile);
+    assertThat(paths.getTestDirectory(null)).isEqualTo(resourceFile);
+    assertThat(paths.getResDirectory()).isEqualTo(new File(resourceFile, "res"));
+    assertThat(paths.getAidlDirectory(null)).isEqualTo(resourceFile);
+    assertThat(paths.getManifestDirectory()).isEqualTo(resourceFile);
+  }
+
+  /**
+   * If no target directory is given, and we have the workspace module, we'll just use the module
+   * root.
+   */
+  @Test
+  public void getWorkspaceSourceSetsWithNoTargetDirectory() {
+    AndroidFacet facet = mockWorkspaceFacet();
+    File rootFile = VfsUtilCore.virtualToIoFile(root);
+    List<AndroidSourceSet> sourceSets = AndroidSourceSet.getSourceSets(facet, null);
+    assertThat(sourceSets).hasSize(1);
+    AndroidSourceSet sourceSet = sourceSets.get(0);
+    AndroidProjectPaths paths = sourceSet.getPaths();
+    assertThat(sourceSet.getName()).isEqualTo(".workspace");
+    assertThat(paths.getModuleRoot()).isEqualTo(rootFile);
+    assertThat(paths.getSrcDirectory(null)).isEqualTo(rootFile);
+    assertThat(paths.getTestDirectory(null)).isEqualTo(rootFile);
+    assertThat(paths.getResDirectory()).isEqualTo(new File(rootFile, "res"));
+    assertThat(paths.getAidlDirectory(null)).isEqualTo(rootFile);
+    assertThat(paths.getManifestDirectory()).isEqualTo(rootFile);
+  }
+
+  private void mockBlazeImportSettings(Container projectServices) {
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
+    importSettingsManager.setImportSettings(
+        new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+  }
+
+  private void mockPsiPackage(Container applicationServices, Container projectServices) {
+    projectServices.register(PsiManager.class, mock(PsiManager.class));
+    applicationServices.register(JavaDirectoryService.class, mock(JavaDirectoryService.class));
+    PsiManager manager = PsiManager.getInstance(project);
+    PsiDirectory targetPsiDirectory = mock(PsiDirectory.class);
+    PsiPackage targetPsiPackage = new PsiPackageImpl(manager, "com.google.target");
+    when(PsiManager.getInstance(project).findDirectory(target)).thenReturn(targetPsiDirectory);
+    when(JavaDirectoryService.getInstance().getPackage(targetPsiDirectory))
+        .thenReturn(targetPsiPackage);
+  }
+
+  private void mockFacetRegistry(Container applicationServices) {
+    applicationServices.register(FacetTypeRegistry.class, new FacetTypeRegistryImpl());
+    registerExtensionPoint(FacetType.EP_NAME, FacetType.class)
+        .registerExtension(new AndroidFacetType());
+  }
+
+  private AndroidFacet mockWorkspaceFacet() {
+    String name = ".workspace";
+    File rootFile = VfsUtilCore.virtualToIoFile(root);
+    SourceProvider sourceProvider =
+        new SourceProviderImpl(name, new File(rootFile, "AndroidManifest.xml"), ImmutableList.of());
+    return new MockAndroidFacet(project, name, root, sourceProvider);
+  }
+
+  private AndroidFacet mockResourceFacet() {
+    String name = "com.google.resource";
+    File resourceFile = VfsUtilCore.virtualToIoFile(resource);
+    SourceProvider sourceProvider =
+        new SourceProviderImpl(
+            name,
+            new File(resourceFile, "AndroidManifest.xml"),
+            ImmutableList.of(new File(resourceFile, "res")));
+    return new MockAndroidFacet(project, name, resource, sourceProvider);
+  }
+
+  private static class MockAndroidFacet extends AndroidFacet {
+    private SourceProvider sourceProvider;
+
+    public MockAndroidFacet(
+        Project project, String name, VirtualFile root, SourceProvider sourceProvider) {
+      super(new MockModule(project, () -> {}), AndroidFacet.NAME, new AndroidFacetConfiguration());
+      MockModule module = (MockModule) getModule();
+      module.setName(name);
+      ModuleRootManager rootManager = mock(ModuleRootManager.class);
+      when(rootManager.getContentRoots()).thenReturn(new VirtualFile[] {root});
+      module.addComponent(ModuleRootManager.class, rootManager);
+      this.sourceProvider = sourceProvider;
+    }
+
+    @Override
+    public SourceProvider getMainSourceProvider() {
+      return sourceProvider;
+    }
+  }
+}
diff --git a/aswb/3.0/tests/unittests/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModelTest.java b/aswb/3.0/tests/unittests/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModelTest.java
new file mode 100644
index 0000000..4432472
--- /dev/null
+++ b/aswb/3.0/tests/unittests/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModelTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2016 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.idea.blaze.android.sync.model.idea;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.android.builder.model.SourceProvider;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.actions.BlazeBuildService;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManagerImpl;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.sdkcompat.android.model.AndroidModelAdapter;
+import com.intellij.mock.MockFileDocumentManagerImpl;
+import com.intellij.mock.MockModule;
+import com.intellij.mock.MockPsiManager;
+import com.intellij.mock.MockVirtualFile;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.fileTypes.MockFileTypeManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.JarFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.JavaPsiFacadeImpl;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.ProjectScopeBuilder;
+import com.intellij.psi.search.ProjectScopeBuilderImpl;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link BlazeAndroidModel}. */
+@RunWith(JUnit4.class)
+public class BlazeAndroidModelTest extends BlazeTestCase {
+  private Module module;
+  private AndroidModelAdapter model;
+  private MockJavaPsiFacade facade;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    applicationServices.register(FileTypeManager.class, new MockFileTypeManager());
+    applicationServices.register(
+        FileDocumentManager.class, new MockFileDocumentManagerImpl(null, null));
+    applicationServices.register(VirtualFileManager.class, mock(VirtualFileManager.class));
+    applicationServices.register(BlazeBuildService.class, new BlazeBuildService());
+    projectServices.register(ProjectScopeBuilder.class, new ProjectScopeBuilderImpl(project));
+    projectServices.register(ProjectViewManager.class, new MockProjectViewManager());
+    projectServices.register(
+        BlazeProjectDataManager.class, new BlazeProjectDataManagerImpl(project));
+
+    BlazeImportSettingsManager manager = new BlazeImportSettingsManager();
+    manager.setImportSettings(new BlazeImportSettings("", "", "", "", BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, manager);
+
+    facade =
+        new MockJavaPsiFacade(
+            project,
+            new MockPsiManager(project),
+            ImmutableList.of("com.google.example.Modified", "com.google.example.NotModified"));
+
+    projectServices.register(JavaPsiFacade.class, facade);
+    module = new MockModule(() -> {});
+    model = new BlazeAndroidModel(project, module, null, mock(SourceProvider.class), null, "", 0);
+  }
+
+  @Test
+  public void testIsClassFileOutOfDate() {
+    VirtualFile modifiedJarFile =
+        new MockJarVirtualFile(
+            "/build/com/google/example/libmodified.jar",
+            facade.getTimestamp("com.google.example.Modified") - 100);
+    VirtualFile notModifiedJarFile =
+        new MockJarVirtualFile(
+            "/build/com/google/example/libnotmodified.jar",
+            facade.getTimestamp("com.google.example.NotModified") + 100);
+    VirtualFile modifiedClassFile =
+        new MockClassVirtualFile(
+            "/build/com/google/example/libmodified.jar!/com/google/example/Modified.class",
+            modifiedJarFile);
+    VirtualFile notModifiedClassFile =
+        new MockClassVirtualFile(
+            "/build/com/google/example/libnotmodified.jar!/com/google/example/NotModified.class",
+            notModifiedJarFile);
+    assertThat(model.isClassFileOutOfDate(module, "com.google.example.Modified", modifiedClassFile))
+        .isTrue();
+    assertThat(
+            model.isClassFileOutOfDate(
+                module, "com.google.example.NotModified", notModifiedClassFile))
+        .isFalse();
+
+    BlazeBuildService.getInstance().buildProject(project);
+    assertThat(model.isClassFileOutOfDate(module, "com.google.example.Modified", modifiedClassFile))
+        .isFalse();
+    assertThat(
+            model.isClassFileOutOfDate(
+                module, "com.google.example.NotModified", notModifiedClassFile))
+        .isFalse();
+  }
+
+  private static class MockJavaPsiFacade extends JavaPsiFacadeImpl {
+    private ImmutableMap<String, PsiClass> classes;
+    private ImmutableMap<String, Long> timestamps;
+
+    MockJavaPsiFacade(
+        Project project, PsiManager psiManager, ImmutableCollection<String> classNames) {
+      super(project, psiManager, null, null);
+      ImmutableMap.Builder<String, PsiClass> classesBuilder = ImmutableMap.builder();
+      ImmutableMap.Builder<String, Long> timestampsBuilder = ImmutableMap.builder();
+      for (String className : classNames) {
+        VirtualFile virtualFile =
+            new MockVirtualFile("/src/" + className.replace('.', '/') + ".java");
+        PsiFile psiFile = mock(PsiFile.class);
+        when(psiFile.getVirtualFile()).thenReturn(virtualFile);
+        PsiClass psiClass = mock(PsiClass.class);
+        when(psiClass.getContainingFile()).thenReturn(psiFile);
+        classesBuilder.put(className, psiClass);
+        timestampsBuilder.put(className, virtualFile.getTimeStamp());
+      }
+      classes = classesBuilder.build();
+      timestamps = timestampsBuilder.build();
+    }
+
+    @Nullable
+    @Override
+    public PsiClass findClass(String qualifiedName, GlobalSearchScope scope) {
+      if (scope.equals(GlobalSearchScope.projectScope(getProject()))) {
+        return classes.get(qualifiedName);
+      }
+      return null;
+    }
+
+    long getTimestamp(String qualifiedName) {
+      return timestamps.get(qualifiedName);
+    }
+  }
+
+  private static class MockClassVirtualFile extends MockVirtualFile {
+    private static JarFileSystem fileSystem = mock(JarFileSystem.class);
+
+    MockClassVirtualFile(String name, VirtualFile jar) {
+      super(name);
+      when(fileSystem.getVirtualFileForJar(this)).thenReturn(jar);
+    }
+
+    @Override
+    public VirtualFileSystem getFileSystem() {
+      return fileSystem;
+    }
+  }
+
+  private static class MockJarVirtualFile extends MockVirtualFile {
+    private long timestamp;
+
+    MockJarVirtualFile(String name, long timestamp) {
+      super(name);
+      this.timestamp = timestamp;
+    }
+
+    @Override
+    public long getTimeStamp() {
+      return timestamp;
+    }
+  }
+
+  private static class MockProjectViewManager extends ProjectViewManager {
+    private ProjectViewSet set = new ProjectViewSet(ImmutableList.of());
+
+    @Nullable
+    @Override
+    public ProjectViewSet getProjectViewSet() {
+      return set;
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet reloadProjectView(
+        BlazeContext context, WorkspacePathResolver workspacePathResolver) {
+      return null;
+    }
+  }
+}
diff --git a/aswb/BUILD b/aswb/BUILD
index 7772ebc..93784b5 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -8,20 +8,58 @@
     "//build_defs:build_defs.bzl",
     "intellij_plugin",
     "merged_plugin_xml",
+    "optional_plugin_xml",
+    "plugin_deploy_zip",
+    "repackaged_files",
     "stamped_plugin_xml",
 )
+load(
+    "//build_defs:intellij_plugin_debug_target.bzl",
+    "intellij_plugin_debug_target",
+)
 load("//:version.bzl", "VERSION")
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+load(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
     srcs = [
         "src/META-INF/aswb.xml",
         "//base:plugin_xml",
-        "//cpp:plugin_xml",
         "//java:plugin_xml",
     ],
 )
 
+optional_plugin_xml(
+    name = "optional_cpp_xml",
+    module = "com.intellij.modules.cidr.lang",
+    plugin_xml = "//cpp:plugin_xml",
+)
+
+optional_plugin_xml(
+    name = "optional_ndk_xml",
+    module = "com.android.tools.ndk",
+    plugin_xml = ":merged_ndk_contents_xml",
+)
+
+merged_plugin_xml(
+    name = "merged_ndk_contents_xml",
+    srcs = ["src/META-INF/ndk-contents.xml"] + select_for_plugin_api({
+        "android-studio-2.3.1.0": [],
+        "android-studio-3.0.0.9": ["3.0/src/META-INF/ndk-workspace-contents.xml"],
+    }),
+)
+
+OPTIONAL_PLUGIN_XMLS = [
+    "//java:optional_xml",
+    ":optional_cpp_xml",
+    ":optional_ndk_xml",
+]
+
 merged_plugin_xml(
     name = "merged_plugin_xml",
     srcs = [
@@ -35,7 +73,7 @@
     changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.aswb",
-    plugin_name = "Android Studio with Bazel",
+    plugin_name = "Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
     version = VERSION,
@@ -43,17 +81,19 @@
 
 java_library(
     name = "aswb_lib",
-    srcs = glob(["src/**/*.java"]),
+    srcs = glob(["src/**/*.java"]) + select_for_plugin_api({
+        "android-studio-2.3.1.0": glob(["2.3/src/**/*.java"]),
+        "android-studio-3.0.0.9": glob(["3.0/src/**/*.java"]),
+    }),
     resources = glob(["resources/**/*"]),
-    runtime_deps = [
-        "//cpp",
-    ],
     deps = [
         "//base",
         "//common/experiments",
+        "//cpp",
         "//intellij_platform_sdk:plugin_api",
         "//java",
         "//proto:proto_deps",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
@@ -68,15 +108,12 @@
     ],
 )
 
-load(
-    "//testing:test_defs.bzl",
-    "intellij_integration_test_suite",
-    "intellij_unit_test_suite",
-)
-
 intellij_unit_test_suite(
     name = "unit_tests",
-    srcs = glob(["tests/unittests/**/*.java"]),
+    srcs = glob(["tests/unittests/**/*.java"]) + select_for_plugin_api({
+        "android-studio-2.3.1.0": [],
+        "android-studio-3.0.0.9": glob(["3.0/tests/unittests/**/*.java"]),
+    }),
     test_package_root = "com.google.idea.blaze.android",
     deps = [
         ":aswb_lib",
@@ -92,9 +129,20 @@
     ],
 )
 
-intellij_integration_test_suite(
+test_suite(
     name = "integration_tests",
-    srcs = glob(["tests/integrationtests/**/*.java"]),
+    tests = [
+        ":NdkDependenciesTest",
+        ":normal_integration_tests",
+    ],
+)
+
+intellij_integration_test_suite(
+    name = "normal_integration_tests",
+    srcs = glob(
+        ["tests/integrationtests/**/*.java"],
+        exclude = ["tests/integrationtests/com/google/idea/blaze/android/plugin/NdkDependenciesTest.java"],
+    ),
     platform_prefix = "AndroidStudio",
     required_plugins = "com.google.idea.bazel.aswb",
     test_package_root = "com.google.idea.blaze.android",
@@ -113,15 +161,69 @@
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//java",
         "//proto:proto_deps",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
 )
 
+intellij_integration_test_suite(
+    name = "NdkDependenciesTest",
+    srcs = ["tests/integrationtests/com/google/idea/blaze/android/plugin/NdkDependenciesTest.java"],
+    platform_prefix = "AndroidStudio",
+    required_plugins = "com.google.idea.bazel.aswb",
+    test_package_root = "com.google.idea.blaze.android",
+    runtime_deps = [
+        ":aswb_bazel",
+        "//cpp",
+        "//java",
+    ],
+    deps = [
+        ":aswb_lib",
+        ":integration_test_utils",
+        "//base",
+        "//base:integration_test_utils",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//proto:proto_deps",
+        "@junit//jar",
+    ],
+)
+
 intellij_plugin(
     name = "aswb_bazel",
+    optional_plugin_xmls = OPTIONAL_PLUGIN_XMLS,
     plugin_xml = ":stamped_plugin_xml",
     deps = [
         ":aswb_lib",
     ],
 )
+
+repackaged_files(
+    name = "plugin_jar",
+    srcs = [":aswb_bazel"],
+    prefix = "aswb/lib",
+)
+
+repackaged_files(
+    name = "aspect_directory",
+    srcs = ["//aspect:aspect_files"],
+    prefix = "aswb/aspect",
+)
+
+intellij_plugin_debug_target(
+    name = "aswb_bazel_dev",
+    deps = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+)
+
+plugin_deploy_zip(
+    name = "aswb_bazel_zip",
+    srcs = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+    zip_filename = "aswb_bazel.zip",
+)
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index 4bf7584..1faf854 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -51,14 +51,14 @@
 
   <extensionPoints>
     <extensionPoint qualifiedName="com.google.idea.blaze.BuildSystemAndroidJdkProvider" interface="com.google.idea.blaze.android.sync.BuildSystemAndroidJdkProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.AndroidBinaryLaunchMethodsProvider" interface="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.AndroidTestLaunchMethodsProvider" interface="com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <SyncPlugin implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncPlugin"/>
     <SyncListener implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncListener"/>
-    <SyncListener implementation="com.google.idea.blaze.android.cppimpl.BlazeNdkSupportEnabler"/>
     <SyncListener implementation="com.google.idea.blaze.android.manifest.ManifestParser$ClearManifestParser"/>
-    <RunConfigurationFactory implementation="com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationFactory"/>
     <JavaSyncAugmenter implementation="com.google.idea.blaze.android.sync.BlazeAndroidJavaSyncAugmenter"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.android.sync.AndroidPrefetchFileSource"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandlerProvider"/>
@@ -66,27 +66,14 @@
     <BuildSystemAndroidJdkProvider implementation="com.google.idea.blaze.android.sync.BazelAndroidJdkProvider"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.android.run.test.smrunner.BlazeAndroidTestEventsHandler"/>
     <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection$AndroidSdkPlatformProjectViewDefaultValueProvider"/>
+    <AndroidBinaryLaunchMethodsProvider implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProviderImpl"/>
+    <AndroidTestLaunchMethodsProvider implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProviderImpl"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.android.ide">
     <sdkEventListener implementation="com.google.idea.blaze.android.sdk.AndroidSdkListener"/>
   </extensions>
 
-  <!-- BEGIN NDK SUPPORT -->
-  <extensions defaultExtensionNs="com.intellij">
-    <applicationService serviceInterface="com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider"
-                        serviceImplementation="com.google.idea.blaze.android.cppimpl.debug.BlazeNativeAndroidDebuggerIdProviderImpl"/>
-  </extensions>
-
-  <extensions defaultExtensionNs="cidr.debugger">
-    <languageSupportFactory implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAndroidNativeDebuggerLanguageSupportFactory"/>
-  </extensions>
-
-  <extensions defaultExtensionNs="com.android.run">
-    <androidDebugger implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAutoAndroidDebugger"/>
-  </extensions>
-  <!-- END NDK SUPPORT -->
-
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <BlazeUserSettingsContributor implementation="com.google.idea.blaze.android.settings.BlazeAndroidUserSettingsContributor$BlazeAndroidUserSettingsProvider"/>
   </extensions>
diff --git a/aswb/src/META-INF/ndk-contents.xml b/aswb/src/META-INF/ndk-contents.xml
new file mode 100644
index 0000000..d08d022
--- /dev/null
+++ b/aswb/src/META-INF/ndk-contents.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ Copyright 2017 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.
+  -->
+<idea-plugin>
+  <depends>com.android.tools.ndk</depends>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncListener implementation="com.google.idea.blaze.android.cppimpl.BlazeNdkSupportEnabler"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <applicationService serviceInterface="com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider"
+                        serviceImplementation="com.google.idea.blaze.android.cppimpl.debug.BlazeNativeAndroidDebuggerIdProviderImpl"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="cidr.debugger">
+    <languageSupportFactory implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAndroidNativeDebuggerLanguageSupportFactory"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.android.run">
+    <androidDebugger implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAutoAndroidDebugger"/>
+  </extensions>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
index 75e1a12..f806286 100644
--- a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -25,10 +25,12 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.cpp.OCWorkspaceProvider;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceModificationTrackersCompatUtils;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.TransactionGuard;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 
 final class BlazeNdkSupportEnabler extends SyncListener.Adapter {
 
@@ -56,29 +58,36 @@
    * @param enabled if true, turn on C support in the IDE. If false, turn off C support in the IDE.
    */
   private static void enableCSupportInIde(Project project, boolean enabled) {
-    OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
+    OCWorkspace workspace = OCWorkspaceProvider.getWorkspace(project);
+    if (workspace == null) {
+      // NDK workspace manager plugin isn't enabled.
+      return;
+    }
     Boolean isCurrentlyEnabled = !LANGUAGE_SUPPORT_DISABLED.get(project, false);
     if (isCurrentlyEnabled != enabled) {
       NdkHelper.disableCppLanguageSupport(project, !enabled);
-      rebuildSymbols(project, workspace);
+      rebuildSymbols(project);
     }
   }
 
-  private static void rebuildSymbols(Project project, OCWorkspace workspace) {
-    ApplicationManager.getApplication()
-        .runReadAction(
-            () -> {
-              if (project.isDisposed()) {
-                return;
-              }
-              // Notifying BuildSettingsChangeTracker in unitTestMode will leads to a dead lock.
-              // See b/23087433 for more information.
-              if (!ApplicationManager.getApplication().isUnitTestMode()) {
-                workspace
-                    .getModificationTrackers()
-                    .getBuildSettingsChangesTracker()
-                    .incModificationCount();
-              }
-            });
+  private static void rebuildSymbols(Project project) {
+    TransactionGuard.getInstance()
+        .submitTransactionLater(
+            project,
+            () ->
+                ApplicationManager.getApplication().runReadAction(() -> doRebuildSymbols(project)));
+  }
+
+  private static void doRebuildSymbols(Project project) {
+    if (project.isDisposed()) {
+      return;
+    }
+    // Notifying BuildSettingsChangeTracker in unitTestMode will leads to a dead lock.
+    // See b/23087433 for more information.
+    if (!ApplicationManager.getApplication().isUnitTestMode()) {
+      OCWorkspaceModificationTrackersCompatUtils.getTrackers(project)
+          .getBuildSettingsChangesTracker()
+          .incModificationCount();
+    }
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPaths.java b/aswb/src/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPaths.java
new file mode 100644
index 0000000..6f797eb
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/npw/project/BlazeAndroidProjectPaths.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.npw.project;
+
+import com.android.SdkConstants;
+import com.android.builder.model.SourceProvider;
+import com.android.tools.idea.npw.project.AndroidProjectPaths;
+import com.android.tools.idea.npw.project.AndroidSourceSet;
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaDirectoryService;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiPackage;
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.facet.AndroidFacet;
+
+/**
+ * Project paths for a Blaze Android project.
+ *
+ * <p>We mostly just take whatever directory the user specified and put the new component there.
+ * Unlike Gradle, Blaze has no strict requirements regarding the structure of an Android project,
+ * but there are some common conventions:
+ *
+ * <pre>
+ * google3/
+ * |-java/com/google/foo/bar/... (module root)
+ * | |-BUILD
+ * | |-AndroidManifest.xml (manifest directory)
+ * | |-Baz.java (source directory of com.google.foo.bar.Baz)
+ * | |-Baz.aidl (aidl directory, option 1)
+ * | |-aidl/
+ * | | `-com/google/foo/bar/Baz.aidl (aidl directory, option 2)
+ * | `-res/... (res directory, one of the few things required by the build system)
+ * `-javatest/com/google/foo/bar/...
+ *   |-BUILD
+ *   `-BazTest.java (test directory of com.google.foo.bar.BazTest)
+ * </pre>
+ *
+ * However, this is also possible (package name unrelated to directory structure):
+ *
+ * <pre>
+ * google3/experimental/users/foo/my/own/project/
+ * |-Baz.java (com.google.foo.bar.Baz)
+ * `-BazTest.java (com.google.foo.bar.BazTest)
+ * </pre>
+ *
+ * So is this (versioned paths that aren't reflected by the package name):
+ *
+ * <pre>
+ * google3/third_party/com/google/foo/bar/
+ * |-v1/Baz.java (com.google.foo.bar.Baz)
+ * `-v2/Baz.java (com.google.foo.bar.Baz)
+ * </pre>
+ */
+public class BlazeAndroidProjectPaths implements AndroidProjectPaths {
+  @Nullable private File moduleRoot;
+  @Nullable private File srcDirectory;
+  @Nullable private File resDirectory;
+
+  @Nullable
+  @Override
+  public File getModuleRoot() {
+    return moduleRoot;
+  }
+
+  @Nullable
+  @Override
+  public File getSrcDirectory(@Nullable String packageName) {
+    return srcDirectory;
+  }
+
+  @Nullable
+  @Override
+  public File getTestDirectory(@Nullable String packageName) {
+    return srcDirectory;
+  }
+
+  @Nullable
+  @Override
+  public File getResDirectory() {
+    return resDirectory;
+  }
+
+  @Nullable
+  @Override
+  public File getAidlDirectory(@Nullable String packageName) {
+    return srcDirectory;
+  }
+
+  @Nullable
+  @Override
+  public File getManifestDirectory() {
+    return srcDirectory;
+  }
+
+  /**
+   * The new component wizard uses {@link AndroidSourceSet#getName()} for the default package name
+   * of the new component. If we can figure it out from the target directory here, then we can pass
+   * it to the new component wizard.
+   */
+  private static String getPackageName(Project project, VirtualFile targetDirectory) {
+    PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(targetDirectory);
+    if (psiDirectory == null) {
+      return null;
+    }
+    PsiPackage psiPackage = JavaDirectoryService.getInstance().getPackage(psiDirectory);
+    if (psiPackage == null) {
+      return null;
+    }
+    return psiPackage.getQualifiedName();
+  }
+
+  public static List<AndroidSourceSet> getSourceSets(
+      AndroidFacet androidFacet, @Nullable VirtualFile targetDirectory) {
+    Module module = androidFacet.getModule();
+    BlazeAndroidProjectPaths paths = new BlazeAndroidProjectPaths();
+    VirtualFile[] roots = ModuleRootManager.getInstance(module).getContentRoots();
+    if (roots.length > 0) {
+      paths.moduleRoot = VfsUtilCore.virtualToIoFile(roots[0]);
+    }
+
+    // We have a res dir if this happens to be a resource module.
+    SourceProvider sourceProvider = androidFacet.getMainSourceProvider();
+    paths.resDirectory = Iterables.getFirst(sourceProvider.getResDirectories(), null);
+
+    // If this happens to be a resource package,
+    // the module name (resource package) would be more descriptive than the facet name (Android).
+    // Otherwise, .workspace is still better than (Android).
+    String name = androidFacet.getModule().getName();
+    if (targetDirectory != null) {
+      String packageName = getPackageName(module.getProject(), targetDirectory);
+      if (packageName != null) {
+        name = packageName;
+      }
+      paths.srcDirectory = VfsUtilCore.virtualToIoFile(targetDirectory);
+    } else {
+      // People usually put the manifest file with their sources.
+      paths.srcDirectory = sourceProvider.getManifestFile().getParentFile();
+    }
+    if (paths.resDirectory == null) {
+      paths.resDirectory = new File(paths.srcDirectory, SdkConstants.FD_RES);
+    }
+    return Collections.singletonList(new AndroidSourceSet(name, paths));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java b/aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
index ac68a65..ce5ff5c 100644
--- a/aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
+++ b/aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
@@ -15,7 +15,8 @@
  */
 package com.google.idea.blaze.android.project;
 
-import com.android.tools.idea.project.BuildSystemService;
+import com.android.tools.idea.npw.project.AndroidSourceSet;
+import com.google.idea.blaze.android.npw.project.BlazeAndroidProjectPaths;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
 import com.google.idea.blaze.base.actions.BlazeBuildService;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
@@ -25,6 +26,7 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.sdkcompat.android.project.BuildSystemServiceAdapter;
 import com.intellij.openapi.fileEditor.FileEditorManager;
 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
 import com.intellij.openapi.module.Module;
@@ -32,9 +34,12 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiElement;
 import java.io.File;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.facet.AndroidFacet;
 
 /** Blaze implementation of {@link BuildSystemService} for build system specific operations. */
-public class BlazeBuildSystemService extends BuildSystemService {
+public class BlazeBuildSystemService extends BuildSystemServiceAdapter {
   @Override
   public boolean isApplicable(Project project) {
     return Blaze.isBlazeProject(project);
@@ -89,4 +94,20 @@
       }
     }
   }
+
+  @Override
+  public String mergeBuildFiles(
+      String dependencies,
+      String destinationContents,
+      Project project,
+      @Nullable String supportLibVersionFilter) {
+    // TODO: check if necessary to implement.
+    return null;
+  }
+
+  @Override
+  public List<AndroidSourceSet> getSourceSets(
+      AndroidFacet facet, @Nullable VirtualFile targetDirectory) {
+    return BlazeAndroidProjectPaths.getSourceSets(facet, targetDirectory);
+  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java
index 6c132b0..dc57eab 100644
--- a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java
@@ -15,10 +15,11 @@
  */
 package com.google.idea.blaze.android.resources.actions;
 
+import com.android.SdkConstants;
 import com.android.ide.common.resources.configuration.FolderConfiguration;
 import com.android.resources.ResourceConstants;
 import com.android.resources.ResourceFolderType;
-import com.android.tools.idea.res.ResourceNameValidator;
+import com.android.tools.idea.res.IdeResourceNameValidator;
 import com.google.common.annotations.VisibleForTesting;
 import com.intellij.CommonBundle;
 import com.intellij.ide.actions.TemplateKindCombo;
@@ -221,7 +222,8 @@
     if (typeName != null) {
       ResourceFolderType type = ResourceFolderType.getFolderType(typeName);
       if (type != null) {
-        ResourceNameValidator validator = ResourceNameValidator.create(true, type);
+        IdeResourceNameValidator validator =
+            IdeResourceNameValidator.forFilename(type, SdkConstants.DOT_XML);
         return validator.getErrorText(fileName);
       }
     }
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java
index cf4a6d8..2290928 100644
--- a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java
@@ -17,7 +17,9 @@
 
 import com.android.resources.ResourceFolderType;
 import com.android.resources.ResourceType;
-import com.android.tools.idea.res.ResourceNameValidator;
+import com.android.tools.idea.res.IdeResourceNameValidator;
+import com.google.idea.sdkcompat.android.resources.actions.CreateXmlResourceDialogAdapter;
+import com.google.idea.sdkcompat.android.resources.actions.CreateXmlResourcePanelAdapter;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
 import com.intellij.openapi.module.Module;
@@ -40,24 +42,22 @@
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
+import javax.annotation.Nullable;
 import javax.swing.DefaultComboBoxModel;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
 import org.jetbrains.android.actions.CreateXmlResourceDialog;
-import org.jetbrains.android.actions.CreateXmlResourcePanel;
 import org.jetbrains.android.actions.CreateXmlResourceSubdirPanel;
 import org.jetbrains.android.actions.CreateXmlResourceSubdirPanel.Parent;
 import org.jetbrains.android.util.AndroidResourceUtil;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Embeddable UI for selecting how to create a new resource value (which XML file and directories to
  * place it).
  */
-public class BlazeCreateXmlResourcePanel implements CreateXmlResourcePanel, Parent {
+public class BlazeCreateXmlResourcePanel extends CreateXmlResourcePanelAdapter implements Parent {
 
   private JPanel myPanel;
   private JTextField myNameField;
@@ -76,14 +76,14 @@
   private JBLabel myDirectoriesLabel;
   private CreateXmlResourceSubdirPanel mySubdirPanel;
 
-  private ResourceNameValidator myResourceNameValidator;
+  private final IdeResourceNameValidator myResourceNameValidator;
   @Nullable private VirtualFile myContextFile;
   @Nullable private VirtualFile myResDirectory;
 
   public BlazeCreateXmlResourcePanel(
-      @NotNull Module module,
-      @NotNull ResourceType resourceType,
-      @NotNull ResourceFolderType folderType,
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
       @Nullable String resourceName,
       @Nullable String resourceValue,
       boolean chooseName,
@@ -91,7 +91,7 @@
       boolean chooseFilename,
       @Nullable VirtualFile defaultFile,
       @Nullable VirtualFile contextFile,
-      @NotNull Function<Module, ResourceNameValidator> nameValidatorFactory) {
+      Function<Module, IdeResourceNameValidator> nameValidatorFactory) {
     setupUi();
     setChangeNameVisible(false);
     setChangeValueVisible(false);
@@ -178,7 +178,7 @@
   }
 
   @Override
-  public void resetFromFile(@NotNull VirtualFile file, @NotNull Project project) {
+  public void resetFromFile(VirtualFile file, Project project) {
     final VirtualFile parent = file.getParent();
     if (parent == null) {
       return;
@@ -247,16 +247,21 @@
       return new ValidationInfo("choose directories", myDirectoriesPanel);
     }
 
-    return CreateXmlResourceDialog.checkIfResourceAlreadyExists(
-        myModule.getProject(), resourceDir, resourceName, myResourceType, directoryNames, fileName);
+    return CreateXmlResourceDialogAdapter.checkIfResourceAlreadyExists(
+        myModule.getProject(),
+        resourceDir,
+        resourceName,
+        null,
+        myResourceType,
+        directoryNames,
+        fileName);
   }
 
   @Override
-  public ResourceNameValidator getResourceNameValidator() {
+  public IdeResourceNameValidator getResourceNameValidatorCompat() {
     return myResourceNameValidator;
   }
 
-  @NotNull
   @Override
   public Module getModule() {
     return myModule;
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java
index 6bf7deb..c2b67aa 100644
--- a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java
@@ -18,8 +18,9 @@
 import com.android.ide.common.resources.configuration.FolderConfiguration;
 import com.android.resources.ResourceFolderType;
 import com.android.resources.ResourceType;
-import com.android.tools.idea.res.ResourceNameValidator;
+import com.android.tools.idea.res.IdeResourceNameValidator;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.android.resources.actions.NewResourceCreationHandlerAdapter;
 import com.intellij.openapi.actionSystem.DataContext;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
@@ -27,41 +28,37 @@
 import com.intellij.psi.PsiDirectory;
 import java.util.Collection;
 import java.util.function.Function;
+import javax.annotation.Nullable;
 import org.jetbrains.android.actions.CreateResourceDirectoryDialogBase;
 import org.jetbrains.android.actions.CreateResourceFileDialogBase;
 import org.jetbrains.android.actions.CreateTypedResourceFileAction;
 import org.jetbrains.android.actions.CreateXmlResourcePanel;
-import org.jetbrains.android.actions.NewResourceCreationHandler;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Decides which create resource dialogs to use for Blaze projects. */
-public class BlazeNewResourceCreationHandler implements NewResourceCreationHandler {
+public class BlazeNewResourceCreationHandler extends NewResourceCreationHandlerAdapter {
 
   @Override
-  public boolean isApplicable(@NotNull Project project) {
+  public boolean isApplicable(Project project) {
     return Blaze.isBlazeProject(project);
   }
 
-  @NotNull
   @Override
   public CreateResourceDirectoryDialogBase createNewResourceDirectoryDialog(
-      @NotNull Project project,
+      Project project,
       @Nullable Module module,
       @Nullable ResourceFolderType resType,
       @Nullable PsiDirectory resDirectory,
       @Nullable DataContext dataContext,
-      @NotNull CreateResourceDirectoryDialogBase.ValidatorFactory validatorFactory) {
+      CreateResourceDirectoryDialogBase.ValidatorFactory validatorFactory) {
     return new BlazeCreateResourceDirectoryDialog(
         project, module, resType, resDirectory, dataContext, validatorFactory);
   }
 
-  @NotNull
   @Override
   public CreateResourceFileDialogBase createNewResourceFileDialog(
-      @NotNull AndroidFacet facet,
-      @NotNull Collection<CreateTypedResourceFileAction> actions,
+      AndroidFacet facet,
+      Collection<CreateTypedResourceFileAction> actions,
       @Nullable ResourceFolderType folderType,
       @Nullable String filename,
       @Nullable String rootElement,
@@ -70,7 +67,7 @@
       boolean chooseModule,
       @Nullable PsiDirectory resDirectory,
       @Nullable DataContext dataContext,
-      @NotNull CreateResourceFileDialogBase.ValidatorFactory validatorFactory) {
+      CreateResourceFileDialogBase.ValidatorFactory validatorFactory) {
     return new BlazeCreateResourceFileDialog(
         facet,
         actions,
@@ -86,10 +83,10 @@
   }
 
   @Override
-  public CreateXmlResourcePanel createNewResourceValuePanel(
-      @NotNull Module module,
-      @NotNull ResourceType resourceType,
-      @NotNull ResourceFolderType folderType,
+  public CreateXmlResourcePanel createNewResourceValuePanelCompat(
+      Module module,
+      ResourceType resourceType,
+      ResourceFolderType folderType,
       @Nullable String resourceName,
       @Nullable String resourceValue,
       boolean chooseName,
@@ -97,7 +94,7 @@
       boolean chooseFilename,
       @Nullable VirtualFile defaultFile,
       @Nullable VirtualFile contextFile,
-      @NotNull Function<Module, ResourceNameValidator> nameValidatorFactory) {
+      Function<Module, IdeResourceNameValidator> nameValidatorFactory) {
     return new BlazeCreateXmlResourcePanel(
         module,
         resourceType,
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
index eaff918..8609d14 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
@@ -23,7 +23,9 @@
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDebuggerManager;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDeployTargetManager;
+import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.BlazeInvocationContext;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.state.RunConfigurationFlagsState;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
@@ -44,7 +46,8 @@
 public class BlazeAndroidRunConfigurationCommonState implements RunConfigurationState {
   private static final String DEPLOY_TARGET_STATES_TAG = "android-deploy-target-states";
   private static final String DEBUGGER_STATES_TAG = "android-debugger-states";
-  private static final String USER_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
   private static final String NATIVE_DEBUG_ATTR = "blaze-native-debug";
 
   // We need to split "-c dbg" into two flags because we pass flags
@@ -56,15 +59,18 @@
   private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
   private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
 
-  private final RunConfigurationFlagsState userFlags;
+  private final RunConfigurationFlagsState blazeFlags;
+  private final RunConfigurationFlagsState exeFlags;
   private boolean nativeDebuggingEnabled = false;
 
   public BlazeAndroidRunConfigurationCommonState(String buildSystemName, boolean isAndroidTest) {
     this.deployTargetManager = new BlazeAndroidRunConfigurationDeployTargetManager(isAndroidTest);
     this.debuggerManager = new BlazeAndroidRunConfigurationDebuggerManager(this);
-    this.userFlags =
+    this.blazeFlags =
+        new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystemName + " flags:");
+    this.exeFlags =
         new RunConfigurationFlagsState(
-            USER_FLAG_TAG, String.format("Custom %s build flags:", buildSystemName));
+            USER_EXE_FLAG_TAG, "Executable flags (mobile-install only):");
   }
 
   public BlazeAndroidRunConfigurationDeployTargetManager getDeployTargetManager() {
@@ -76,7 +82,11 @@
   }
 
   public RunConfigurationFlagsState getBlazeFlagsState() {
-    return userFlags;
+    return blazeFlags;
+  }
+
+  public RunConfigurationFlagsState getExeFlagsState() {
+    return exeFlags;
   }
 
   public boolean isNativeDebuggingEnabled() {
@@ -88,9 +98,11 @@
   }
 
   public ImmutableList<String> getExpandedBuildFlags(
-      Project project, ProjectViewSet projectViewSet) {
+      Project project, ProjectViewSet projectViewSet, BlazeCommandName command) {
     return ImmutableList.<String>builder()
-        .addAll(BlazeFlags.buildFlags(project, projectViewSet))
+        .addAll(
+            BlazeFlags.blazeFlags(
+                project, projectViewSet, command, BlazeInvocationContext.RunConfiguration))
         .addAll(getBlazeFlagsState().getExpandedFlags())
         .addAll(getNativeDebuggerFlags())
         .build();
@@ -117,7 +129,8 @@
 
   @Override
   public void readExternal(Element element) throws InvalidDataException {
-    userFlags.readExternal(element);
+    blazeFlags.readExternal(element);
+    exeFlags.readExternal(element);
     setNativeDebuggingEnabled(Boolean.parseBoolean(element.getAttributeValue(NATIVE_DEBUG_ATTR)));
 
     Element deployTargetStatesElement = element.getChild(DEPLOY_TARGET_STATES_TAG);
@@ -133,7 +146,8 @@
 
   @Override
   public void writeExternal(Element element) throws WriteExternalException {
-    userFlags.writeExternal(element);
+    blazeFlags.writeExternal(element);
+    exeFlags.writeExternal(element);
     element.setAttribute(NATIVE_DEBUG_ATTR, Boolean.toString(nativeDebuggingEnabled));
 
     element.removeChildren(DEPLOY_TARGET_STATES_TAG);
@@ -155,12 +169,14 @@
   private static class BlazeAndroidRunConfigurationCommonStateEditor
       implements RunConfigurationStateEditor {
 
-    private final RunConfigurationStateEditor userFlagsEditor;
+    private final RunConfigurationStateEditor blazeFlagsEditor;
+    private final RunConfigurationStateEditor exeFlagsEditor;
     private final JCheckBox enableNativeDebuggingCheckBox;
 
     BlazeAndroidRunConfigurationCommonStateEditor(
         BlazeAndroidRunConfigurationCommonState state, Project project) {
-      userFlagsEditor = state.userFlags.getEditor(project);
+      blazeFlagsEditor = state.blazeFlags.getEditor(project);
+      exeFlagsEditor = state.exeFlags.getEditor(project);
       enableNativeDebuggingCheckBox = new JCheckBox("Enable native debugging", false);
     }
 
@@ -168,7 +184,8 @@
     public void resetEditorFrom(RunConfigurationState genericState) {
       BlazeAndroidRunConfigurationCommonState state =
           (BlazeAndroidRunConfigurationCommonState) genericState;
-      userFlagsEditor.resetEditorFrom(state.userFlags);
+      blazeFlagsEditor.resetEditorFrom(state.blazeFlags);
+      exeFlagsEditor.resetEditorFrom(state.exeFlags);
       enableNativeDebuggingCheckBox.setSelected(state.isNativeDebuggingEnabled());
     }
 
@@ -176,13 +193,15 @@
     public void applyEditorTo(RunConfigurationState genericState) {
       BlazeAndroidRunConfigurationCommonState state =
           (BlazeAndroidRunConfigurationCommonState) genericState;
-      userFlagsEditor.applyEditorTo(state.userFlags);
+      blazeFlagsEditor.applyEditorTo(state.blazeFlags);
+      exeFlagsEditor.applyEditorTo(state.exeFlags);
       state.setNativeDebuggingEnabled(enableNativeDebuggingCheckBox.isSelected());
     }
 
     @Override
     public JComponent createComponent() {
-      List<Component> result = Lists.newArrayList(userFlagsEditor.createComponent());
+      List<Component> result =
+          Lists.newArrayList(blazeFlagsEditor.createComponent(), exeFlagsEditor.createComponent());
       if (NdkSupport.NDK_SUPPORT.getValue()) {
         result.add(enableNativeDebuggingCheckBox);
       }
@@ -191,7 +210,8 @@
 
     @Override
     public void setComponentEnabled(boolean enabled) {
-      userFlagsEditor.setComponentEnabled(enabled);
+      blazeFlagsEditor.setComponentEnabled(enabled);
+      exeFlagsEditor.setComponentEnabled(enabled);
       enableNativeDebuggingCheckBox.setEnabled(enabled);
     }
   }
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java
deleted file mode 100644
index 30955bf..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2016 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.idea.blaze.android.run;
-
-import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TargetKey;
-import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.primitives.Kind;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
-import com.intellij.execution.configurations.ConfigurationFactory;
-import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.openapi.project.Project;
-
-/** Creates run configurations for android_binary and android_test. */
-public class BlazeAndroidRunConfigurationFactory extends BlazeRunConfigurationFactory {
-  @Override
-  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
-    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
-    return target != null && target.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST);
-  }
-
-  @Override
-  protected ConfigurationFactory getConfigurationFactory() {
-    return BlazeCommandRunConfigurationType.getInstance().getFactory();
-  }
-
-  @Override
-  public void setupConfiguration(RunConfiguration configuration, Label target) {
-    final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(target);
-    blazeConfig.setGeneratedName();
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java
index 992f414..85bd239 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.android.run;
 
-import com.android.tools.idea.gradle.util.Projects;
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
@@ -25,6 +24,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.android.project.AndroidProjectInfoAdapter;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
@@ -73,7 +73,7 @@
       return errors;
     }
     final Project project = module.getProject();
-    if (Projects.requiredAndroidModelMissing(project)) {
+    if (AndroidProjectInfoAdapter.requiredAndroidModelMissing(project)) {
       errors.add(ValidationError.fatal(SYNC_FAILED_ERR_MSG));
     }
     return errors;
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java
index d89f58e..40f7789 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java
@@ -17,31 +17,24 @@
 
 import com.android.tools.idea.run.ApkProvisionException;
 import com.android.tools.idea.run.ApplicationIdProvider;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Computable;
+import javax.annotation.Nullable;
 import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Application id provider for android_binary. */
 public class BlazeAndroidBinaryApplicationIdProvider implements ApplicationIdProvider {
-  private final Project project;
-  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+  private final BlazeApkBuildStep buildStep;
 
-  public BlazeAndroidBinaryApplicationIdProvider(
-      Project project, ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
-    this.project = project;
-    this.deployInfoFuture = deployInfoFuture;
+  public BlazeAndroidBinaryApplicationIdProvider(BlazeApkBuildStep buildStep) {
+    this.buildStep = buildStep;
   }
 
-  @NotNull
   @Override
   public String getPackageName() throws ApkProvisionException {
-    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    BlazeAndroidDeployInfo deployInfo = buildStep.getDeployInfo();
     Manifest manifest = deployInfo.getMergedManifest();
     if (manifest == null) {
       throw new ApkProvisionException(
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProvider.java
new file mode 100644
index 0000000..8aca02d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.run.binary;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import java.util.Arrays;
+import java.util.List;
+
+/** Provides a list of supported launch methods for android binaries. */
+public interface BlazeAndroidBinaryLaunchMethodsProvider {
+  ExtensionPointName<BlazeAndroidBinaryLaunchMethodsProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.AndroidBinaryLaunchMethodsProvider");
+
+  static AndroidBinaryLaunchMethodComboEntry[] getAllLaunchMethods(Project project) {
+    return Arrays.stream(EP_NAME.getExtensions())
+        .flatMap(extension -> extension.getLaunchMethods(project).stream())
+        .toArray(AndroidBinaryLaunchMethodComboEntry[]::new);
+  }
+
+  List<AndroidBinaryLaunchMethodComboEntry> getLaunchMethods(Project project);
+
+  /** All possible binary launch methods. */
+  enum AndroidBinaryLaunchMethod {
+    NON_BLAZE,
+    MOBILE_INSTALL,
+    MOBILE_INSTALL_V2,
+  }
+
+  /** Launch methods wrapped for display in a combo box. */
+  class AndroidBinaryLaunchMethodComboEntry {
+    final AndroidBinaryLaunchMethod launchMethod;
+    private final String description;
+
+    public AndroidBinaryLaunchMethodComboEntry(
+        AndroidBinaryLaunchMethod launchMethod, String description) {
+      this.launchMethod = launchMethod;
+      this.description = description;
+    }
+
+    @Override
+    public String toString() {
+      return description;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProviderImpl.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProviderImpl.java
new file mode 100644
index 0000000..8d02764
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryLaunchMethodsProviderImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.run.binary;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+
+/** Provides a list of supported launch methods from bazel and blaze for android binaries. */
+public class BlazeAndroidBinaryLaunchMethodsProviderImpl
+    implements BlazeAndroidBinaryLaunchMethodsProvider {
+  @Override
+  public List<AndroidBinaryLaunchMethodComboEntry> getLaunchMethods(Project project) {
+    String blaze = Blaze.buildSystemName(project);
+    return ImmutableList.of(
+        new AndroidBinaryLaunchMethodComboEntry(
+            AndroidBinaryLaunchMethod.NON_BLAZE, String.format("Run without using %s", blaze)),
+        new AndroidBinaryLaunchMethodComboEntry(
+            AndroidBinaryLaunchMethod.MOBILE_INSTALL,
+            String.format("Run with %s mobile-install", blaze.toLowerCase())));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
index cb6b23c..a1abc9b 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
@@ -32,7 +32,6 @@
 import com.android.tools.idea.run.tasks.LaunchTasksProvider;
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
@@ -50,7 +49,6 @@
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 
 /** Run context for android_binary. */
 class BlazeAndroidBinaryNormalBuildRunContext implements BlazeAndroidRunContext {
@@ -65,24 +63,23 @@
   private final BlazeApkProvider apkProvider;
   private final ApplicationIdProvider applicationIdProvider;
 
-  public BlazeAndroidBinaryNormalBuildRunContext(
+  BlazeAndroidBinaryNormalBuildRunContext(
       Project project,
       AndroidFacet facet,
       RunConfiguration runConfiguration,
       ExecutionEnvironment env,
       BlazeAndroidBinaryRunConfigurationState configState,
       Label label,
-      ImmutableList<String> buildFlags) {
+      ImmutableList<String> blazeFlags) {
     this.project = project;
     this.facet = facet;
     this.runConfiguration = runConfiguration;
     this.env = env;
     this.configState = configState;
     this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
-    this.buildStep = new BlazeApkBuildStepNormalBuild(project, label, buildFlags);
-    this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
-    this.applicationIdProvider =
-        new BlazeAndroidBinaryApplicationIdProvider(project, buildStep.getDeployInfo());
+    this.buildStep = new BlazeApkBuildStepNormalBuild(project, label, blazeFlags);
+    this.apkProvider = new BlazeApkProvider(project, buildStep);
+    this.applicationIdProvider = new BlazeAndroidBinaryApplicationIdProvider(buildStep);
   }
 
   @Override
@@ -94,11 +91,10 @@
   }
 
   @Override
-  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
+  public void augmentLaunchOptions(LaunchOptions.Builder options) {
     options.setDeploy(true).setOpenLogcatAutomatically(true);
   }
 
-  @NotNull
   @Override
   public ConsoleProvider getConsoleProvider() {
     return consoleProvider;
@@ -153,8 +149,12 @@
             launchOptions.isDebug(),
             UserIdHelper.getFlagsFromUserId(userId));
 
-    BlazeAndroidDeployInfo deployInfo =
-        Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
+    BlazeAndroidDeployInfo deployInfo;
+    try {
+      deployInfo = buildStep.getDeployInfo();
+    } catch (ApkProvisionException e) {
+      throw new ExecutionException(e);
+    }
 
     return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
         project,
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
index 37619aa..c64bb10 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
@@ -18,6 +18,7 @@
 import com.android.tools.idea.fd.InstantRunUtils;
 import com.android.tools.idea.run.AndroidSessionInfo;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethod;
 import com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallDebugExecutor;
 import com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallRunExecutor;
 import com.intellij.execution.ExecutionException;
@@ -30,12 +31,11 @@
 import com.intellij.execution.runners.DefaultProgramRunner;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.execution.ui.RunContentDescriptor;
-import org.jetbrains.annotations.NotNull;
 
 /** Program runner for {@link BlazeAndroidRunConfiguration} */
 public class BlazeAndroidBinaryProgramRunner extends DefaultProgramRunner {
   @Override
-  public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) {
+  public boolean canRun(String executorId, RunProfile profile) {
     BlazeAndroidRunConfigurationHandler handler =
         BlazeAndroidRunConfigurationHandler.getHandlerFrom(profile);
     if (handler == null) {
@@ -51,15 +51,17 @@
     if (!(handler instanceof BlazeAndroidBinaryRunConfigurationHandler)) {
       return false;
     }
-    return ((BlazeAndroidBinaryRunConfigurationHandler) handler).getState().mobileInstall()
+    AndroidBinaryLaunchMethod launchMethod =
+        ((BlazeAndroidBinaryRunConfigurationHandler) handler).getState().getLaunchMethod();
+    return (AndroidBinaryLaunchMethod.MOBILE_INSTALL.equals(launchMethod)
+            || AndroidBinaryLaunchMethod.MOBILE_INSTALL_V2.equals(launchMethod))
         && (IncrementalInstallDebugExecutor.EXECUTOR_ID.equals(executorId)
             || IncrementalInstallRunExecutor.EXECUTOR_ID.equals(executorId));
   }
 
   @Override
   protected RunContentDescriptor doExecute(
-      @NotNull final RunProfileState state, @NotNull final ExecutionEnvironment env)
-      throws ExecutionException {
+      final RunProfileState state, final ExecutionEnvironment env) throws ExecutionException {
     RunContentDescriptor descriptor = super.doExecute(state, env);
     if (descriptor != null) {
       ProcessHandler processHandler = descriptor.getProcessHandler();
@@ -84,7 +86,6 @@
   }
 
   @Override
-  @NotNull
   public String getRunnerId() {
     return "AndroidProgramRunner";
   }
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
index f57d51f..0a26f27 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
+import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
@@ -104,9 +105,14 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
 
-    ImmutableList<String> buildFlags =
-        configState.getCommonState().getExpandedBuildFlags(project, projectViewSet);
-    BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
+    ImmutableList<String> blazeFlags =
+        configState
+            .getCommonState()
+            .getExpandedBuildFlags(project, projectViewSet, BlazeCommandName.RUN);
+    ImmutableList<String> exeFlags =
+        ImmutableList.copyOf(configState.getCommonState().getExeFlagsState().getExpandedFlags());
+    BlazeAndroidRunContext runContext =
+        createRunContext(project, facet, environment, blazeFlags, exeFlags);
 
     return new BlazeAndroidRunConfigurationRunner(
         module,
@@ -120,14 +126,18 @@
       Project project,
       AndroidFacet facet,
       ExecutionEnvironment env,
-      ImmutableList<String> buildFlags) {
-    if (configState.mobileInstall()) {
-      return new BlazeAndroidBinaryMobileInstallRunContext(
-          project, facet, configuration, env, configState, getLabel(), buildFlags);
-    } else {
-      return new BlazeAndroidBinaryNormalBuildRunContext(
-          project, facet, configuration, env, configState, getLabel(), buildFlags);
+      ImmutableList<String> blazeFlags,
+      ImmutableList<String> exeFlags) {
+    switch (configState.getLaunchMethod()) {
+      case MOBILE_INSTALL:
+      case MOBILE_INSTALL_V2:
+        return new BlazeAndroidBinaryMobileInstallRunContext(
+            project, facet, configuration, env, configState, getLabel(), blazeFlags, exeFlags);
+      case NON_BLAZE:
+        return new BlazeAndroidBinaryNormalBuildRunContext(
+            project, facet, configuration, env, configState, getLabel(), blazeFlags);
     }
+    throw new AssertionError();
   }
 
   @Override
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
index 8bc2076..0dff73d 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethod;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.intellij.openapi.project.Project;
@@ -38,11 +39,15 @@
   public static final String DO_NOTHING = "do_nothing";
   public static final String LAUNCH_DEEP_LINK = "launch_deep_link";
 
-  private static final String MOBILE_INSTALL_ATTR = "blaze-mobile-install";
+  private static final String LAUNCH_METHOD_ATTR = "launch-method";
+  @Deprecated private static final String MOBILE_INSTALL_ATTR = "blaze-mobile-install";
+  // Remove once v2 becomes default.
   private static final String USE_SPLIT_APKS_IF_POSSIBLE = "use-split-apks-if-possible";
+
   private static final String WORK_PROFILE_ATTR = "use-work-profile-if-present";
   private static final String USER_ID_ATTR = "user-id";
-  private boolean mobileInstall = false;
+
+  private AndroidBinaryLaunchMethod launchMethod = AndroidBinaryLaunchMethod.NON_BLAZE;
   private boolean useSplitApksIfPossible = false;
   private boolean useWorkProfileIfPresent = false;
   private Integer userId;
@@ -65,12 +70,12 @@
     return commonState;
   }
 
-  boolean mobileInstall() {
-    return mobileInstall;
+  public AndroidBinaryLaunchMethod getLaunchMethod() {
+    return launchMethod;
   }
 
-  void setMobileInstall(boolean mobileInstall) {
-    this.mobileInstall = mobileInstall;
+  void setLaunchMethod(AndroidBinaryLaunchMethod launchMethod) {
+    this.launchMethod = launchMethod;
   }
 
   public boolean useSplitApksIfPossible() {
@@ -137,7 +142,16 @@
     setActivityClass(Strings.nullToEmpty(element.getAttributeValue(ACTIVITY_CLASS)));
     String modeValue = element.getAttributeValue(MODE);
     setMode(Strings.isNullOrEmpty(modeValue) ? LAUNCH_DEFAULT_ACTIVITY : modeValue);
-    setMobileInstall(Boolean.parseBoolean(element.getAttributeValue(MOBILE_INSTALL_ATTR)));
+    String launchMethodAttribute = element.getAttributeValue(LAUNCH_METHOD_ATTR);
+    if (launchMethodAttribute != null) {
+      launchMethod = AndroidBinaryLaunchMethod.valueOf(launchMethodAttribute);
+    } else {
+      if (Boolean.parseBoolean(element.getAttributeValue(MOBILE_INSTALL_ATTR))) {
+        launchMethod = AndroidBinaryLaunchMethod.MOBILE_INSTALL;
+      } else {
+        launchMethod = AndroidBinaryLaunchMethod.NON_BLAZE;
+      }
+    }
     setUseSplitApksIfPossible(
         Boolean.parseBoolean(element.getAttributeValue(USE_SPLIT_APKS_IF_POSSIBLE)));
     setUseWorkProfileIfPresent(Boolean.parseBoolean(element.getAttributeValue(WORK_PROFILE_ATTR)));
@@ -177,7 +191,7 @@
     element.setAttribute(DEEP_LINK, deepLink);
     element.setAttribute(ACTIVITY_CLASS, activityClass);
     element.setAttribute(MODE, mode);
-    element.setAttribute(MOBILE_INSTALL_ATTR, Boolean.toString(mobileInstall));
+    element.setAttribute(LAUNCH_METHOD_ATTR, launchMethod.name());
     element.setAttribute(USE_SPLIT_APKS_IF_POSSIBLE, Boolean.toString(useSplitApksIfPossible));
     element.setAttribute(WORK_PROFILE_ATTR, Boolean.toString(useWorkProfileIfPresent));
 
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
index 8ce6279..65697d1 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
@@ -16,6 +16,8 @@
 package com.google.idea.blaze.android.run.binary;
 
 import com.android.tools.idea.run.activity.ActivityLocatorUtils;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethod;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethodComboEntry;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.ui.IntegerTextField;
@@ -49,6 +51,7 @@
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
@@ -72,7 +75,7 @@
   private JRadioButton launchNothingButton;
   private JRadioButton launchDefaultButton;
   private JRadioButton launchCustomButton;
-  private JCheckBox mobileInstallCheckBox;
+  private JComboBox<AndroidBinaryLaunchMethodComboEntry> launchMethodComboBox;
   private JCheckBox splitApksCheckBox;
   private JCheckBox useWorkProfileIfPresentCheckBox;
   private JLabel userIdLabel;
@@ -131,8 +134,12 @@
     launchDefaultButton.addActionListener(listener);
     launchNothingButton.addActionListener(listener);
 
-    mobileInstallCheckBox.addActionListener(
-        e -> splitApksCheckBox.setVisible(mobileInstallCheckBox.isSelected()));
+    launchMethodComboBox.addActionListener(
+        e ->
+            splitApksCheckBox.setVisible(
+                ((AndroidBinaryLaunchMethodComboEntry) launchMethodComboBox.getSelectedItem())
+                    .launchMethod.equals(AndroidBinaryLaunchMethod.MOBILE_INSTALL) // v1 only
+                ));
 
     useWorkProfileIfPresentCheckBox.addActionListener(e -> updateEnabledState());
   }
@@ -155,12 +162,18 @@
       activityField.getChildComponent().setText(state.getActivityClass());
     }
 
-    mobileInstallCheckBox.setSelected(state.mobileInstall());
+    for (int i = 0; i < launchMethodComboBox.getItemCount(); ++i) {
+      if (launchMethodComboBox.getItemAt(i).launchMethod.equals(state.getLaunchMethod())) {
+        launchMethodComboBox.setSelectedIndex(i);
+        break;
+      }
+    }
     splitApksCheckBox.setSelected(state.useSplitApksIfPossible());
     useWorkProfileIfPresentCheckBox.setSelected(state.useWorkProfileIfPresent());
 
     userIdField.setValue(state.getUserId());
-    splitApksCheckBox.setVisible(state.mobileInstall());
+    splitApksCheckBox.setVisible(
+        state.getLaunchMethod().equals(AndroidBinaryLaunchMethod.MOBILE_INSTALL));
 
     updateEnabledState();
   }
@@ -180,7 +193,9 @@
     } else {
       state.setMode(BlazeAndroidBinaryRunConfigurationState.DO_NOTHING);
     }
-    state.setMobileInstall(mobileInstallCheckBox.isSelected());
+    state.setLaunchMethod(
+        ((AndroidBinaryLaunchMethodComboEntry) launchMethodComboBox.getSelectedItem())
+            .launchMethod);
     state.setUseSplitApksIfPossible(splitApksCheckBox.isSelected());
     state.setUseWorkProfileIfPresent(useWorkProfileIfPresentCheckBox.isSelected());
   }
@@ -199,7 +214,7 @@
     launchNothingButton.setEnabled(componentEnabled);
     launchDefaultButton.setEnabled(componentEnabled);
     launchCustomButton.setEnabled(componentEnabled);
-    mobileInstallCheckBox.setEnabled(componentEnabled);
+    launchMethodComboBox.setEnabled(componentEnabled);
     splitApksCheckBox.setEnabled(componentEnabled);
     useWorkProfileIfPresentCheckBox.setEnabled(componentEnabled);
   }
@@ -423,10 +438,10 @@
             null,
             0,
             false));
-    mobileInstallCheckBox = new JCheckBox();
-    mobileInstallCheckBox.setText(" Use mobile-install");
+    launchMethodComboBox =
+        new JComboBox<>(BlazeAndroidBinaryLaunchMethodsProvider.getAllLaunchMethods(project));
     panel.add(
-        mobileInstallCheckBox,
+        launchMethodComboBox,
         new GridConstraints(
             0,
             0,
@@ -442,7 +457,7 @@
             0,
             false));
     splitApksCheckBox = new JCheckBox();
-    splitApksCheckBox.setText(" Use --split_apks where possible");
+    splitApksCheckBox.setText("Use --split_apks where possible");
     panel.add(
         splitApksCheckBox,
         new GridConstraints(
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
index da33631..ff1ab76 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.android.run.binary.mobileinstall;
 
 import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ApkProvisionException;
 import com.android.tools.idea.run.ApplicationIdProvider;
 import com.android.tools.idea.run.ConsolePrinter;
 import com.android.tools.idea.run.ConsoleProvider;
@@ -29,10 +30,10 @@
 import com.android.tools.idea.run.tasks.LaunchTasksProvider;
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationIdProvider;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationLaunchTaskProvider;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryConsoleProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethod;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
 import com.google.idea.blaze.android.run.binary.UserIdHelper;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
@@ -49,7 +50,6 @@
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 
 /** Run context for android_binary. */
 public class BlazeAndroidBinaryMobileInstallRunContext implements BlazeAndroidRunContext {
@@ -70,7 +70,8 @@
       ExecutionEnvironment env,
       BlazeAndroidBinaryRunConfigurationState configState,
       Label label,
-      ImmutableList<String> buildFlags) {
+      ImmutableList<String> blazeFlags,
+      ImmutableList<String> exeFlags) {
     this.project = project;
     this.facet = facet;
     this.runConfiguration = runConfiguration;
@@ -79,9 +80,14 @@
     this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
     this.buildStep =
         new BlazeApkBuildStepMobileInstall(
-            project, env, label, buildFlags, configState.useSplitApksIfPossible());
-    this.applicationIdProvider =
-        new BlazeAndroidBinaryApplicationIdProvider(project, buildStep.getDeployInfo());
+            project,
+            env,
+            label,
+            blazeFlags,
+            exeFlags,
+            configState.useSplitApksIfPossible(),
+            configState.getLaunchMethod().equals(AndroidBinaryLaunchMethod.MOBILE_INSTALL_V2));
+    this.applicationIdProvider = new BlazeAndroidBinaryApplicationIdProvider(buildStep);
   }
 
   @Override
@@ -93,11 +99,10 @@
   public void augmentEnvironment(ExecutionEnvironment env) {}
 
   @Override
-  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
+  public void augmentLaunchOptions(LaunchOptions.Builder options) {
     options.setDeploy(false).setOpenLogcatAutomatically(true);
   }
 
-  @NotNull
   @Override
   public ConsoleProvider getConsoleProvider() {
     return consoleProvider;
@@ -144,9 +149,12 @@
             project,
             launchOptions.isDebug(),
             UserIdHelper.getFlagsFromUserId(userId));
-
-    BlazeAndroidDeployInfo deployInfo =
-        Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
+    BlazeAndroidDeployInfo deployInfo;
+    try {
+      deployInfo = buildStep.getDeployInfo();
+    } catch (ApkProvisionException e) {
+      throw new ExecutionException(e);
+    }
 
     return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
         project,
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
index 582afea..6bec217 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
@@ -16,12 +16,12 @@
 package com.google.idea.blaze.android.run.binary.mobileinstall;
 
 import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ApkProvisionException;
 import com.android.tools.idea.run.DeviceFutures;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
 import com.google.common.util.concurrent.UncheckedExecutionException;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
@@ -59,21 +59,27 @@
   private final Project project;
   private final ExecutionEnvironment env;
   private final Label label;
-  private final ImmutableList<String> buildFlags;
+  private final ImmutableList<String> blazeFlags;
+  private final ImmutableList<String> exeFlags;
   private final boolean useSplitApksIfPossible;
-  private final SettableFuture<BlazeAndroidDeployInfo> deployInfoFuture = SettableFuture.create();
+  private final boolean mobileInstallV2;
+  private BlazeAndroidDeployInfo deployInfo = null;
 
   public BlazeApkBuildStepMobileInstall(
       Project project,
       ExecutionEnvironment env,
       Label label,
-      ImmutableList<String> buildFlags,
-      boolean useSplitApksIfPossible) {
+      ImmutableList<String> blazeFlags,
+      ImmutableList<String> exeFlags,
+      boolean useSplitApksIfPossible,
+      boolean mobileInstallV2) {
     this.project = project;
     this.env = env;
     this.label = label;
-    this.buildFlags = buildFlags;
+    this.blazeFlags = blazeFlags;
+    this.exeFlags = exeFlags;
     this.useSplitApksIfPossible = useSplitApksIfPossible;
+    this.mobileInstallV2 = mobileInstallV2;
   }
 
   @Override
@@ -95,31 +101,50 @@
                 BlazeCommand.builder(
                     Blaze.getBuildSystemProvider(project).getBinaryPath(),
                     BlazeCommandName.MOBILE_INSTALL);
-            command.addBlazeFlags(BlazeFlags.adbSerialFlags(device.getSerialNumber()));
+
+            if (mobileInstallV2) {
+              // Will become no-op once v2 is default.
+              command.addBlazeFlags("--mode=skylark");
+            }
+
+            if (mobileInstallV2) {
+              command.addExeFlags(BlazeFlags.DEVICE, device.getSerialNumber());
+            } else {
+              command.addBlazeFlags(
+                  BlazeFlags.ADB_ARG + "-s ", BlazeFlags.ADB_ARG + device.getSerialNumber());
+            }
 
             if (USE_SDK_ADB.getValue()) {
               File adb = AndroidSdkUtils.getAdb(project);
               if (adb != null) {
-                command.addBlazeFlags(ImmutableList.of("--adb", adb.toString()));
+                if (mobileInstallV2) {
+                  command.addExeFlags(BlazeFlags.ADB_PATH, adb.toString());
+                } else {
+                  command.addBlazeFlags(BlazeFlags.ADB, adb.toString());
+                }
               }
             }
 
-            // split-apks only supported for API level 23 and above
-            if (useSplitApksIfPossible && device.getVersion().getApiLevel() >= 23) {
-              command.addBlazeFlags(BlazeFlags.SPLIT_APKS);
-            } else if (incrementalInstall) {
-              command.addBlazeFlags(BlazeFlags.INCREMENTAL);
+            // These flags are obsolete in V2.
+            if (!mobileInstallV2) {
+              // split-apks only supported for API level 23 and above
+              if (useSplitApksIfPossible && device.getVersion().getApiLevel() >= 23) {
+                command.addBlazeFlags(BlazeFlags.SPLIT_APKS);
+              } else if (incrementalInstall) {
+                command.addBlazeFlags(BlazeFlags.INCREMENTAL);
+              }
             }
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
             BlazeApkDeployInfoProtoHelper deployInfoHelper =
-                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+                new BlazeApkDeployInfoProtoHelper(project, blazeFlags);
             BuildResultHelper buildResultHelper = deployInfoHelper.getBuildResultHelper();
 
             command
                 .addTargets(label)
-                .addBlazeFlags(buildFlags)
-                .addBlazeFlags(buildResultHelper.getBuildFlags());
+                .addBlazeFlags(blazeFlags)
+                .addBlazeFlags(buildResultHelper.getBuildFlags())
+                .addExeFlags(exeFlags);
 
             SaveUtil.saveAllFiles();
             int retVal =
@@ -138,12 +163,10 @@
               return;
             }
 
-            BlazeAndroidDeployInfo deployInfo = deployInfoHelper.readDeployInfo(context);
+            deployInfo = deployInfoHelper.readDeployInfo(context);
             if (deployInfo == null) {
               IssueOutput.error("Could not read apk deploy info from build").submit(context);
-              return;
             }
-            deployInfoFuture.set(deployInfo);
           }
         };
 
@@ -163,8 +186,12 @@
     return context.shouldContinue();
   }
 
-  public ListenableFuture<BlazeAndroidDeployInfo> getDeployInfo() {
-    return deployInfoFuture;
+  @Override
+  public BlazeAndroidDeployInfo getDeployInfo() throws ApkProvisionException {
+    if (deployInfo != null) {
+      return deployInfo;
+    }
+    throw new ApkProvisionException("Failed to read APK deploy info");
   }
 
   @Nullable
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
index 98e2e87..bed1ef9 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
@@ -22,9 +22,11 @@
 import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.repackaged.devtools.build.lib.rules.android.deployinfo.AndroidDeployInfoOuterClass;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
@@ -49,8 +51,12 @@
     this.project = project;
     this.buildFlags = buildFlags;
     this.workspaceRoot = WorkspaceRoot.fromProject(project);
+
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     this.buildResultHelper =
-        BuildResultHelper.forFiles(fileName -> fileName.endsWith(".deployinfo.pb"));
+        BuildResultHelper.forFiles(
+            projectData.blazeVersionData, fileName -> fileName.endsWith(".deployinfo.pb"));
   }
 
   public BuildResultHelper getBuildResultHelper() {
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java
index bdcc5d6..3f1f644 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java
@@ -21,30 +21,26 @@
 import com.android.tools.idea.run.ApkProvisionException;
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.android.run.runner.AaptUtil;
+import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
 /** Apk provider from deploy info proto */
 public class BlazeApkProvider implements ApkProvider {
   private final Project project;
-  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+  private final BlazeApkBuildStep buildStep;
 
-  public BlazeApkProvider(
-      Project project, ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
+  public BlazeApkProvider(Project project, BlazeApkBuildStep buildStep) {
     this.project = project;
-    this.deployInfoFuture = deployInfoFuture;
+    this.buildStep = buildStep;
   }
 
-  @NotNull
   @Override
-  public Collection<ApkInfo> getApks(@NotNull IDevice device) throws ApkProvisionException {
-    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+  public Collection<ApkInfo> getApks(IDevice device) throws ApkProvisionException {
+    BlazeAndroidDeployInfo deployInfo = buildStep.getDeployInfo();
     ImmutableList.Builder<ApkInfo> apkInfos = ImmutableList.builder();
     for (File apk : deployInfo.getApksToDeploy()) {
       apkInfos.add(new ApkInfo(apk, manifestPackageForApk(apk)));
@@ -52,8 +48,7 @@
     return apkInfos.build();
   }
 
-  @NotNull
-  private String manifestPackageForApk(@NotNull final File apk) throws ApkProvisionException {
+  private String manifestPackageForApk(final File apk) throws ApkProvisionException {
     try {
       return AaptUtil.getApkManifestPackage(project, apk);
     } catch (AaptUtil.AaptUtilException e) {
@@ -66,7 +61,6 @@
     }
   }
 
-  @NotNull
   @Override
   public List<ValidationError> validate() {
     return ImmutableList.of();
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
index bf56acb..66fe3f0 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
@@ -42,6 +42,7 @@
 import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -190,7 +191,10 @@
   @Override
   public boolean executeBeforeRunTask(ExecutionEnvironment env) {
     final Project project = env.getProject();
-    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    BlazeConsolePopupBehavior consolePopupBehavior =
+        BlazeUserSettings.getInstance().getSuppressConsoleForRunAction()
+            ? BlazeConsolePopupBehavior.NEVER
+            : BlazeConsolePopupBehavior.ALWAYS;
     return Scope.root(
         context -> {
           context
@@ -198,7 +202,7 @@
               .push(new ExperimentScope())
               .push(
                   new BlazeConsoleScope.Builder(project)
-                      .setSuppressConsole(suppressConsole)
+                      .setPopupBehavior(consolePopupBehavior)
                       .build())
               .push(new IdeaLogScope());
 
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java
index b437f7f..74a0f24 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java
@@ -15,14 +15,18 @@
  */
 package com.google.idea.blaze.android.run.runner;
 
+import com.android.tools.idea.run.ApkProvisionException;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.base.scope.BlazeContext;
 
 /** Builds the APK. */
 public interface BlazeApkBuildStep {
   /**
-   * Builds an optionally installs the APK.
+   * Builds and optionally installs the APK.
    *
    * @return True to continue the launch.
    */
   boolean build(BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession);
+
+  BlazeAndroidDeployInfo getDeployInfo() throws ApkProvisionException;
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
index 58daf4f..11c7f03 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
@@ -15,10 +15,10 @@
  */
 package com.google.idea.blaze.android.run.runner;
 
+import com.android.tools.idea.run.ApkProvisionException;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
@@ -38,14 +38,13 @@
 import com.intellij.execution.ExecutionException;
 import com.intellij.openapi.project.Project;
 import java.util.concurrent.CancellationException;
-import org.jetbrains.annotations.NotNull;
 
 /** Builds the APK using normal blaze build. */
 public class BlazeApkBuildStepNormalBuild implements BlazeApkBuildStep {
   private final Project project;
   private final Label label;
   private final ImmutableList<String> buildFlags;
-  private final SettableFuture<BlazeAndroidDeployInfo> deployInfoFuture = SettableFuture.create();
+  private BlazeAndroidDeployInfo deployInfo = null;
 
   public BlazeApkBuildStepNormalBuild(
       Project project, Label label, ImmutableList<String> buildFlags) {
@@ -60,7 +59,7 @@
     final ScopedTask buildTask =
         new ScopedTask(context) {
           @Override
-          protected void execute(@NotNull BlazeContext context) {
+          protected void execute(BlazeContext context) {
             BlazeCommand.Builder command =
                 BlazeCommand.builder(
                     Blaze.getBuildSystemProvider(project).getBinaryPath(), BlazeCommandName.BUILD);
@@ -92,12 +91,10 @@
               context.setHasError();
               return;
             }
-            BlazeAndroidDeployInfo deployInfo = deployInfoHelper.readDeployInfo(context);
+            deployInfo = deployInfoHelper.readDeployInfo(context);
             if (deployInfo == null) {
               IssueOutput.error("Could not read apk deploy info from build").submit(context);
-              return;
             }
-            deployInfoFuture.set(deployInfo);
           }
         };
 
@@ -117,7 +114,11 @@
     return context.shouldContinue();
   }
 
-  public ListenableFuture<BlazeAndroidDeployInfo> getDeployInfo() {
-    return deployInfoFuture;
+  @Override
+  public BlazeAndroidDeployInfo getDeployInfo() throws ApkProvisionException {
+    if (deployInfo != null) {
+      return deployInfo;
+    }
+    throw new ApkProvisionException("Failed to read APK deploy info");
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
index 92311c4..4915256 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
@@ -17,11 +17,11 @@
 
 import com.android.tools.idea.run.ConsoleProvider;
 import com.android.tools.idea.testartifacts.instrumented.AndroidTestConsoleProperties;
-import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestUiSession;
 import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
-import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.executors.DefaultDebugExecutor;
 import com.intellij.execution.filters.TextConsoleBuilderFactory;
 import com.intellij.execution.process.ProcessHandler;
@@ -35,39 +35,43 @@
 /** Console provider for android_test */
 class AndroidTestConsoleProvider implements ConsoleProvider {
   private final Project project;
-  private final RunConfiguration runConfiguration;
+  private final BlazeCommandRunConfiguration runConfiguration;
   private final BlazeAndroidTestRunConfigurationState configState;
-  @Nullable private final BlazeTestEventsHandler testEventsHandler;
+  @Nullable private final BlazeTestUiSession testUiSession;
 
   AndroidTestConsoleProvider(
       Project project,
-      RunConfiguration runConfiguration,
+      BlazeCommandRunConfiguration runConfiguration,
       BlazeAndroidTestRunConfigurationState configState,
-      @Nullable BlazeTestEventsHandler testEventsHandler) {
+      @Nullable BlazeTestUiSession testUiSession) {
     this.project = project;
     this.runConfiguration = runConfiguration;
     this.configState = configState;
-    this.testEventsHandler = testEventsHandler;
+    this.testUiSession = testUiSession;
   }
 
   @Override
   public ConsoleView createAndAttach(Disposable parent, ProcessHandler handler, Executor executor)
       throws ExecutionException {
-    if (!configState.isRunThroughBlaze()) {
-      return getStockConsoleProvider().createAndAttach(parent, handler, executor);
+    switch (configState.getLaunchMethod()) {
+      case BLAZE_TEST:
+        ConsoleView console = createBlazeTestConsole(executor);
+        console.attachToProcess(handler);
+        return console;
+      case NON_BLAZE:
+      case MOBILE_INSTALL:
+        return getStockConsoleProvider().createAndAttach(parent, handler, executor);
     }
-    ConsoleView console = createBlazeTestConsole(executor);
-    console.attachToProcess(handler);
-    return console;
+    throw new AssertionError();
   }
 
   private ConsoleView createBlazeTestConsole(Executor executor) {
-    if (testEventsHandler == null || isDebugging(executor)) {
+    if (testUiSession == null || isDebugging(executor)) {
       // SM runner console not yet supported when debugging, because we're calling this once per
       // test case (see ConnectBlazeTestDebuggerTask::setUpForReattachingDebugger)
       return TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
     }
-    return SmRunnerUtils.getConsoleView(project, runConfiguration, executor, testEventsHandler);
+    return SmRunnerUtils.getConsoleView(project, runConfiguration, executor, testUiSession);
   }
 
   private static boolean isDebugging(Executor executor) {
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java
index 71f37fd..9ad030c 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java
@@ -18,31 +18,24 @@
 import com.android.tools.idea.run.ApkProvisionException;
 import com.android.tools.idea.run.ApplicationIdProvider;
 import com.google.common.collect.Iterables;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Computable;
+import javax.annotation.Nullable;
 import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Application id provider for android_binary. */
 public class BlazeAndroidTestApplicationIdProvider implements ApplicationIdProvider {
-  private final Project project;
-  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+  private final BlazeApkBuildStep buildStep;
 
-  public BlazeAndroidTestApplicationIdProvider(
-      Project project, ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
-    this.project = project;
-    this.deployInfoFuture = deployInfoFuture;
+  BlazeAndroidTestApplicationIdProvider(BlazeApkBuildStep buildStep) {
+    this.buildStep = buildStep;
   }
 
-  @NotNull
   @Override
   public String getPackageName() throws ApkProvisionException {
-    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    BlazeAndroidDeployInfo deployInfo = buildStep.getDeployInfo();
     Manifest manifest = Iterables.getFirst(deployInfo.getAdditionalMergedManifests(), null);
     if (manifest == null) {
       // The application may not have a separate package,
@@ -61,7 +54,7 @@
   @Nullable
   @Override
   public String getTestPackageName() throws ApkProvisionException {
-    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    BlazeAndroidDeployInfo deployInfo = buildStep.getDeployInfo();
     Manifest manifest = deployInfo.getMergedManifest();
     if (manifest == null) {
       throw new ApkProvisionException(
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProvider.java
new file mode 100644
index 0000000..5258c85
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.run.test;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import java.util.Arrays;
+import java.util.List;
+
+/** Provides a list of supported launch methods for android tests. */
+public interface BlazeAndroidTestLaunchMethodsProvider {
+  ExtensionPointName<BlazeAndroidTestLaunchMethodsProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.AndroidTestLaunchMethodsProvider");
+
+  static AndroidTestLaunchMethodComboEntry[] getAllLaunchMethods(Project project) {
+    return Arrays.stream(EP_NAME.getExtensions())
+        .flatMap(extension -> extension.getLaunchMethods(project).stream())
+        .toArray(AndroidTestLaunchMethodComboEntry[]::new);
+  }
+
+  List<AndroidTestLaunchMethodComboEntry> getLaunchMethods(Project project);
+
+  /** All possible test launch methods. */
+  enum AndroidTestLaunchMethod {
+    NON_BLAZE,
+    BLAZE_TEST,
+    MOBILE_INSTALL,
+  }
+
+  /** Launch methods wrapped for display in a combo box. */
+  class AndroidTestLaunchMethodComboEntry {
+    final AndroidTestLaunchMethod launchMethod;
+    private final String description;
+
+    public AndroidTestLaunchMethodComboEntry(
+        AndroidTestLaunchMethod launchMethod, String description) {
+      this.launchMethod = launchMethod;
+      this.description = description;
+    }
+
+    @Override
+    public String toString() {
+      return description;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProviderImpl.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProviderImpl.java
new file mode 100644
index 0000000..f60327b
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchMethodsProviderImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.run.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+
+/** Provides a list of supported launch methods from bazel and blaze for android tests. */
+public class BlazeAndroidTestLaunchMethodsProviderImpl
+    implements BlazeAndroidTestLaunchMethodsProvider {
+  @Override
+  public List<AndroidTestLaunchMethodComboEntry> getLaunchMethods(Project project) {
+    String blaze = Blaze.buildSystemName(project);
+    return ImmutableList.of(
+        new AndroidTestLaunchMethodComboEntry(
+            AndroidTestLaunchMethod.NON_BLAZE, String.format("Run without using %s", blaze)),
+        new AndroidTestLaunchMethodComboEntry(
+            AndroidTestLaunchMethod.BLAZE_TEST,
+            String.format("Run with %s test", blaze.toLowerCase())));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
index 4b94b3c..adece56 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
@@ -24,6 +24,7 @@
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
+import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
@@ -102,9 +103,14 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
 
-    ImmutableList<String> buildFlags =
-        configState.getCommonState().getExpandedBuildFlags(project, projectViewSet);
-    BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
+    ImmutableList<String> blazeFlags =
+        configState
+            .getCommonState()
+            .getExpandedBuildFlags(project, projectViewSet, BlazeCommandName.TEST);
+    ImmutableList<String> exeFlags =
+        ImmutableList.copyOf(configState.getCommonState().getExeFlagsState().getExpandedFlags());
+    BlazeAndroidRunContext runContext =
+        createRunContext(project, facet, environment, blazeFlags, exeFlags);
 
     return new BlazeAndroidRunConfigurationRunner(
         module,
@@ -118,9 +124,10 @@
       Project project,
       AndroidFacet facet,
       ExecutionEnvironment env,
-      ImmutableList<String> buildFlags) {
+      ImmutableList<String> blazeFlags,
+      ImmutableList<String> exeFlags) {
     return new BlazeAndroidTestRunContext(
-        project, facet, configuration, env, configState, getLabel(), buildFlags);
+        project, facet, configuration, env, configState, getLabel(), blazeFlags, exeFlags);
   }
 
   @Override
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
index 074b17b..e67388e 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider.AndroidTestLaunchMethod;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.intellij.openapi.project.Project;
@@ -34,7 +35,8 @@
 /** State specific for the android test configuration. */
 final class BlazeAndroidTestRunConfigurationState implements RunConfigurationState {
 
-  private static final String RUN_THROUGH_BLAZE_ATTR = "blaze-run-through-blaze";
+  private static final String LAUNCH_METHOD_ATTR = "launch-method";
+  @Deprecated private static final String RUN_THROUGH_BLAZE_ATTR = "blaze-run-through-blaze";
 
   public static final int TEST_ALL_IN_MODULE = 0;
   public static final int TEST_ALL_IN_PACKAGE = 1;
@@ -60,8 +62,7 @@
   private String packageName = "";
   private String extraOptions = "";
 
-  // Whether to delegate to 'blaze test'.
-  private boolean runThroughBlaze;
+  private AndroidTestLaunchMethod launchMethod = AndroidTestLaunchMethod.NON_BLAZE;
 
   private final BlazeAndroidRunConfigurationCommonState commonState;
 
@@ -74,12 +75,12 @@
   }
 
   @Contract(pure = true)
-  boolean isRunThroughBlaze() {
-    return runThroughBlaze;
+  AndroidTestLaunchMethod getLaunchMethod() {
+    return launchMethod;
   }
 
-  void setRunThroughBlaze(boolean runThroughBlaze) {
-    this.runThroughBlaze = runThroughBlaze;
+  void setLaunchMethod(AndroidTestLaunchMethod launchMethod) {
+    this.launchMethod = launchMethod;
   }
 
   public int getTestingType() {
@@ -152,7 +153,17 @@
     className = Strings.nullToEmpty(element.getAttributeValue(CLASS_NAME));
     packageName = Strings.nullToEmpty(element.getAttributeValue(PACKAGE_NAME));
     extraOptions = Strings.nullToEmpty(element.getAttributeValue(EXTRA_OPTIONS));
-    runThroughBlaze = Boolean.parseBoolean(element.getAttributeValue(RUN_THROUGH_BLAZE_ATTR));
+
+    String launchMethodAttribute = element.getAttributeValue(LAUNCH_METHOD_ATTR);
+    if (launchMethodAttribute != null) {
+      launchMethod = AndroidTestLaunchMethod.valueOf(launchMethodAttribute);
+    } else {
+      if (Boolean.parseBoolean(element.getAttributeValue(RUN_THROUGH_BLAZE_ATTR))) {
+        launchMethod = AndroidTestLaunchMethod.BLAZE_TEST;
+      } else {
+        launchMethod = AndroidTestLaunchMethod.NON_BLAZE;
+      }
+    }
 
     for (Map.Entry<String, String> entry : getLegacyValues(element).entrySet()) {
       String value = entry.getValue();
@@ -187,7 +198,7 @@
   public void writeExternal(Element element) throws WriteExternalException {
     commonState.writeExternal(element);
 
-    element.setAttribute(RUN_THROUGH_BLAZE_ATTR, Boolean.toString(runThroughBlaze));
+    element.setAttribute(LAUNCH_METHOD_ATTR, launchMethod.name());
     element.setAttribute(TESTING_TYPE, Integer.toString(testingType));
     element.setAttribute(INSTRUMENTATION_RUNNER_CLASS, instrumentationRunnerClass);
     element.setAttribute(METHOD_NAME, methodName);
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
index 5e5e6c8..0bf5c63 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
@@ -21,9 +21,9 @@
 import static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_CLASS;
 import static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_METHOD;
 
+import com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider.AndroidTestLaunchMethodComboEntry;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
-import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.LabeledComponent;
@@ -38,7 +38,7 @@
 import java.util.ResourceBundle;
 import javax.swing.AbstractButton;
 import javax.swing.ButtonGroup;
-import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
@@ -61,7 +61,7 @@
   private JPanel panel;
   private LabeledComponent<EditorTextField> runnerComponent;
   private JBLabel labelTest;
-  private JCheckBox runThroughBlazeTestCheckBox;
+  private JComboBox<AndroidTestLaunchMethodComboEntry> launchMethodComboBox;
   private final JRadioButton[] testingType2RadioButton = new JRadioButton[4];
 
   private boolean componentEnabled = true;
@@ -351,13 +351,10 @@
             null,
             0,
             false));
-    runThroughBlazeTestCheckBox = new JCheckBox();
-    runThroughBlazeTestCheckBox.setText(
-        String.format("Run through '%s test'", Blaze.buildSystemName(project).toLowerCase()));
-    runThroughBlazeTestCheckBox.setToolTipText(
-        String.format("Slower, but more truthful to %s", Blaze.buildSystemName(project)));
+    launchMethodComboBox =
+        new JComboBox<>(BlazeAndroidTestLaunchMethodsProvider.getAllLaunchMethods(project));
     panel.add(
-        runThroughBlazeTestCheckBox,
+        launchMethodComboBox,
         new GridConstraints(
             0,
             0,
@@ -452,7 +449,8 @@
         (BlazeAndroidTestRunConfigurationState) genericState;
     commonStateEditor.applyEditorTo(state.getCommonState());
 
-    state.setRunThroughBlaze(runThroughBlazeTestCheckBox.isSelected());
+    state.setLaunchMethod(
+        ((AndroidTestLaunchMethodComboEntry) launchMethodComboBox.getSelectedItem()).launchMethod);
 
     state.setTestingType(getTestingType());
     state.setClassName(classComponent.getComponent().getText());
@@ -467,8 +465,12 @@
         (BlazeAndroidTestRunConfigurationState) genericState;
     commonStateEditor.resetEditorFrom(state.getCommonState());
 
-    runThroughBlazeTestCheckBox.setSelected(state.isRunThroughBlaze());
-
+    for (int i = 0; i < launchMethodComboBox.getItemCount(); ++i) {
+      if (launchMethodComboBox.getItemAt(i).launchMethod.equals(state.getLaunchMethod())) {
+        launchMethodComboBox.setSelectedIndex(i);
+        break;
+      }
+    }
     updateButtonsAndLabelComponents(state.getTestingType());
     packageComponent.getComponent().setText(state.getPackageName());
     classComponent.getComponent().setText(state.getClassName());
@@ -498,7 +500,7 @@
     methodComponent.setEnabled(componentEnabled);
     runnerComponent.setEnabled(componentEnabled);
     labelTest.setEnabled(componentEnabled);
-    runThroughBlazeTestCheckBox.setEnabled(componentEnabled);
+    launchMethodComboBox.setEnabled(componentEnabled);
     for (JComponent button : testingType2RadioButton) {
       if (button != null) {
         button.setEnabled(componentEnabled);
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
index 9962acf..b8d5966 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
@@ -32,7 +32,7 @@
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.run.binary.mobileinstall.BlazeApkBuildStepMobileInstall;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
@@ -41,9 +41,12 @@
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStepNormalBuild;
+import com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider.AndroidTestLaunchMethod;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestUiSession;
+import com.google.idea.blaze.base.run.smrunner.TestUiSessionProvider;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.executors.DefaultDebugExecutor;
@@ -54,7 +57,6 @@
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 
 /** Run context for android_test. */
 class BlazeAndroidTestRunContext implements BlazeAndroidRunContext {
@@ -65,47 +67,55 @@
   private final ExecutionEnvironment env;
   private final BlazeAndroidTestRunConfigurationState configState;
   private final Label label;
-  private final ImmutableList<String> buildFlags;
+  private final ImmutableList<String> blazeFlags;
   private final List<Runnable> launchTaskCompleteListeners = Lists.newArrayList();
   private final ConsoleProvider consoleProvider;
-  private final BlazeApkBuildStepNormalBuild buildStep;
+  private final BlazeApkBuildStep buildStep;
   private final ApplicationIdProvider applicationIdProvider;
   private final ApkProvider apkProvider;
 
-  public BlazeAndroidTestRunContext(
+  BlazeAndroidTestRunContext(
       Project project,
       AndroidFacet facet,
       BlazeCommandRunConfiguration runConfiguration,
       ExecutionEnvironment env,
       BlazeAndroidTestRunConfigurationState configState,
       Label label,
-      ImmutableList<String> buildFlags) {
+      ImmutableList<String> blazeFlags,
+      ImmutableList<String> exeFlags) {
     this.project = project;
     this.facet = facet;
     this.runConfiguration = runConfiguration;
     this.env = env;
     this.label = label;
     this.configState = configState;
-    this.buildStep = new BlazeApkBuildStepNormalBuild(project, label, buildFlags);
-    this.applicationIdProvider =
-        new BlazeAndroidTestApplicationIdProvider(project, buildStep.getDeployInfo());
-    this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
+    this.buildStep =
+        configState.getLaunchMethod().equals(AndroidTestLaunchMethod.MOBILE_INSTALL)
+            ? new BlazeApkBuildStepMobileInstall(
+                project, env, label, blazeFlags, exeFlags, false, true)
+            : new BlazeApkBuildStepNormalBuild(project, label, blazeFlags);
+    this.applicationIdProvider = new BlazeAndroidTestApplicationIdProvider(buildStep);
+    this.apkProvider = new BlazeApkProvider(project, buildStep);
 
-    BlazeTestEventsHandler testEventsHandler = null;
-    if (!isDebugging(env.getExecutor())) {
-      testEventsHandler =
-          BlazeTestEventsHandler.getHandlerForTarget(project, runConfiguration.getTarget());
-      assert (testEventsHandler != null);
-      this.buildFlags =
+    BlazeTestUiSession testUiSession =
+        canUseTestUi(env.getExecutor())
+            ? TestUiSessionProvider.createForTarget(project, runConfiguration.getTarget())
+            : null;
+    if (testUiSession != null) {
+      this.blazeFlags =
           ImmutableList.<String>builder()
-              .addAll(BlazeTestEventsHandler.getBlazeFlags(project))
-              .addAll(buildFlags)
+              .addAll(testUiSession.getBlazeFlags())
+              .addAll(blazeFlags)
               .build();
     } else {
-      this.buildFlags = buildFlags;
+      this.blazeFlags = blazeFlags;
     }
     this.consoleProvider =
-        new AndroidTestConsoleProvider(project, runConfiguration, configState, testEventsHandler);
+        new AndroidTestConsoleProvider(project, runConfiguration, configState, testUiSession);
+  }
+
+  private static boolean canUseTestUi(Executor executor) {
+    return !isDebugging(executor);
   }
 
   private static boolean isDebugging(Executor executor) {
@@ -122,7 +132,7 @@
 
   @Override
   public void augmentLaunchOptions(LaunchOptions.Builder options) {
-    options.setDeploy(!configState.isRunThroughBlaze());
+    options.setDeploy(!configState.getLaunchMethod().equals(AndroidTestLaunchMethod.BLAZE_TEST));
   }
 
   @Override
@@ -160,25 +170,35 @@
   @Override
   public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions)
       throws ExecutionException {
-    if (!configState.isRunThroughBlaze()) {
-      BlazeAndroidDeployInfo deployInfo =
-          Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
-      if (!deployInfo.getDataToDeploy().isEmpty()) {
-        throw new ExecutionException(
-            "This test target has data dependencies (defined in the 'data' attribute).\n"
-                + "These can only be installed if the configuration is run through 'blaze test'.\n"
-                + "Check the \"Run through 'blaze test'\" checkbox on your "
-                + "run configuration and try again.");
-      }
+    switch (configState.getLaunchMethod()) {
+      case NON_BLAZE:
+        BlazeAndroidDeployInfo deployInfo;
+        try {
+          deployInfo = buildStep.getDeployInfo();
+        } catch (ApkProvisionException e) {
+          throw new ExecutionException(e);
+        }
+        if (!deployInfo.getDataToDeploy().isEmpty()) {
+          throw new ExecutionException(
+              String.format(
+                  "This test target has data dependencies (defined in the 'data' attribute).\n"
+                      + "These can only be installed if the configuration is run through blaze.\n"
+                      + "Choose \"Run with %1$s test\" on your run configuration and try again.",
+                  Blaze.getBuildSystem(project).getLowerCaseName()));
+        }
+        // fall through
+      case BLAZE_TEST:
+        Collection<ApkInfo> apks;
+        try {
+          apks = apkProvider.getApks(device);
+        } catch (ApkProvisionException e) {
+          throw new ExecutionException(e);
+        }
+        return ImmutableList.of(new DeployApkTask(project, launchOptions, apks));
+      case MOBILE_INSTALL:
+        return ImmutableList.of();
     }
-
-    Collection<ApkInfo> apks;
-    try {
-      apks = apkProvider.getApks(device);
-    } catch (ApkProvisionException e) {
-      throw new ExecutionException(e);
-    }
-    return ImmutableList.of(new DeployApkTask(project, launchOptions, apks));
+    throw new AssertionError();
   }
 
   @Nullable
@@ -190,27 +210,35 @@
       AndroidDebuggerState androidDebuggerState,
       ProcessHandlerLaunchStatus processHandlerLaunchStatus)
       throws ExecutionException {
-    if (configState.isRunThroughBlaze()) {
-      return new BlazeAndroidTestLaunchTask(
-          project,
-          label,
-          buildFlags,
-          new BlazeAndroidTestFilter(
-              configState.getTestingType(),
-              configState.getClassName(),
-              configState.getMethodName(),
-              configState.getPackageName()),
-          this,
-          launchOptions.isDebug());
+    switch (configState.getLaunchMethod()) {
+      case BLAZE_TEST:
+        return new BlazeAndroidTestLaunchTask(
+            project,
+            label,
+            blazeFlags,
+            new BlazeAndroidTestFilter(
+                configState.getTestingType(),
+                configState.getClassName(),
+                configState.getMethodName(),
+                configState.getPackageName()),
+            this,
+            launchOptions.isDebug());
+      case NON_BLAZE:
+      case MOBILE_INSTALL:
+        BlazeAndroidDeployInfo deployInfo;
+        try {
+          deployInfo = buildStep.getDeployInfo();
+        } catch (ApkProvisionException e) {
+          throw new ExecutionException(e);
+        }
+        return StockAndroidTestLaunchTask.getStockTestLaunchTask(
+            configState,
+            applicationIdProvider,
+            launchOptions.isDebug(),
+            deployInfo,
+            processHandlerLaunchStatus);
     }
-    BlazeAndroidDeployInfo deployInfo =
-        Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
-    return StockAndroidTestLaunchTask.getStockTestLaunchTask(
-        configState,
-        applicationIdProvider,
-        launchOptions.isDebug(),
-        deployInfo,
-        processHandlerLaunchStatus);
+    throw new AssertionError();
   }
 
   @Override
@@ -218,21 +246,25 @@
   public DebugConnectorTask getDebuggerTask(
       AndroidDebugger androidDebugger,
       AndroidDebuggerState androidDebuggerState,
-      @NotNull Set<String> packageIds,
+      Set<String> packageIds,
       boolean monitorRemoteProcess)
       throws ExecutionException {
-    if (configState.isRunThroughBlaze()) {
-      return new ConnectBlazeTestDebuggerTask(
-          env.getProject(), androidDebugger, packageIds, applicationIdProvider, this);
+    switch (configState.getLaunchMethod()) {
+      case BLAZE_TEST:
+        return new ConnectBlazeTestDebuggerTask(
+            env.getProject(), androidDebugger, packageIds, applicationIdProvider, this);
+      case NON_BLAZE:
+      case MOBILE_INSTALL:
+        return androidDebugger.getConnectDebuggerTask(
+            env,
+            null,
+            packageIds,
+            facet,
+            androidDebuggerState,
+            runConfiguration.getType().getId(),
+            monitorRemoteProcess);
     }
-    return androidDebugger.getConnectDebuggerTask(
-        env,
-        null,
-        packageIds,
-        facet,
-        androidDebuggerState,
-        runConfiguration.getType().getId(),
-        monitorRemoteProcess);
+    throw new AssertionError();
   }
 
   void onLaunchTaskComplete() {
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
index a10773e..01f2b86 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
@@ -25,6 +25,7 @@
 import com.android.tools.idea.testartifacts.instrumented.AndroidTestListener;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider.AndroidTestLaunchMethod;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.Computable;
@@ -37,6 +38,10 @@
 final class StockAndroidTestLaunchTask implements LaunchTask {
   private static final Logger LOG = Logger.getInstance(StockAndroidTestLaunchTask.class);
 
+  private static final String TEST_FILE_ARG = "testFile";
+  private static final String TEST_FILE_LOCATION_FORMAT =
+      "/data/local/tmp/deployment/%s/test/all_tests.txt";
+
   private final BlazeAndroidTestRunConfigurationState configState;
   private final String instrumentationTestRunner;
   private final String testApplicationId;
@@ -165,6 +170,10 @@
 
     final RemoteAndroidTestRunner runner =
         new RemoteAndroidTestRunner(testApplicationId, instrumentationTestRunner, device);
+    if (configState.getLaunchMethod().equals(AndroidTestLaunchMethod.MOBILE_INSTALL)) {
+      runner.addInstrumentationArg(
+          TEST_FILE_ARG, String.format(TEST_FILE_LOCATION_FORMAT, testApplicationId));
+    }
     switch (configState.getTestingType()) {
       case BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_MODULE:
         break;
@@ -177,6 +186,9 @@
       case BlazeAndroidTestRunConfigurationState.TEST_METHOD:
         runner.setMethodName(configState.getClassName(), configState.getMethodName());
         break;
+      default:
+        LOG.error(String.format("Unrecognized testing type: %d", configState.getTestingType()));
+        return false;
     }
     runner.setDebug(waitForDebugger);
     runner.setRunOptions(configState.getExtraOptions());
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java
index 947a070..1a5bc9c 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java
@@ -30,7 +30,6 @@
 import com.intellij.psi.PsiMethod;
 import com.intellij.util.io.URLUtil;
 import java.util.Collection;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -38,11 +37,11 @@
 import javax.annotation.Nullable;
 
 /** Provides java-specific methods needed by the SM-runner test UI. */
-public class BlazeAndroidTestEventsHandler extends BlazeTestEventsHandler {
+public class BlazeAndroidTestEventsHandler implements BlazeTestEventsHandler {
 
   @Override
-  protected EnumSet<Kind> handledKinds() {
-    return EnumSet.of(Kind.ANDROID_TEST);
+  public boolean handlesKind(@Nullable Kind kind) {
+    return kind == Kind.ANDROID_TEST;
   }
 
   @Override
diff --git a/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java b/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
index 821f098..930b5e3 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
@@ -20,10 +20,10 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.Collection;
 import java.util.Set;
 
 /** Adds the resource directories outside our source roots to prefetch. */
@@ -32,8 +32,9 @@
   public void addFilesToPrefetch(
       Project project,
       ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
       BlazeProjectData blazeProjectData,
-      Collection<File> files) {
+      Set<File> files) {
     BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
     if (syncData == null) {
       return;
@@ -46,7 +47,7 @@
   }
 
   @Override
-  public Set<String> prefetchSrcFileExtensions() {
+  public Set<String> prefetchFileExtensions() {
     return ImmutableSet.of("xml");
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
index 73bb993..03f8bf0 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
@@ -17,10 +17,12 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
-import java.util.Collection;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import java.util.List;
 
 class BlazeAndroidLibrarySource extends LibrarySource.Adapter {
   private final BlazeProjectData blazeProjectData;
@@ -30,7 +32,7 @@
   }
 
   @Override
-  public Collection<BlazeLibrary> getLibraries() {
+  public List<BlazeLibrary> getLibraries() {
     BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
     if (syncData == null) {
       return ImmutableList.of();
@@ -39,6 +41,10 @@
     if (syncData.importResult.resourceLibrary != null) {
       libraries.add(syncData.importResult.resourceLibrary);
     }
+    if (syncData.importResult.javacJar != null) {
+      libraries.add(
+          new BlazeJarLibrary(new LibraryArtifact(null, syncData.importResult.javacJar, null)));
+    }
     return libraries.build();
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeNdkDependencySyncPlugin.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeNdkDependencySyncPlugin.java
new file mode 100644
index 0000000..0177a1e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeNdkDependencySyncPlugin.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.sync;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.sdkcompat.android.sync.BlazeNdkDependencySyncPluginCompat;
+import com.intellij.openapi.project.Project;
+import java.util.stream.Collectors;
+
+/**
+ * Returns an error during sync (with quick-fix) if NDK support is requested, but the required
+ * plugin dependencies aren't enabled.
+ */
+public final class BlazeNdkDependencySyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  private static class PluginNameAndId {
+    final String name;
+    final String id;
+
+    PluginNameAndId(String pluginName, String pluginId) {
+      this.name = pluginName;
+      this.id = pluginId;
+    }
+
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  private static final ImmutableList<PluginNameAndId> REQUIRED_PLUGINS =
+      ImmutableList.copyOf(
+          BlazeNdkDependencySyncPluginCompat.REQUIRED_PLUGINS
+              .entrySet()
+              .stream()
+              .map(e -> new PluginNameAndId(e.getKey(), e.getValue()))
+              .collect(Collectors.toList()));
+
+  /** Returns the IDs of the plugins required for NDK support. */
+  @VisibleForTesting
+  public static ImmutableList<String> getPluginsRequiredForNdkSupport() {
+    return ImmutableList.copyOf(
+        REQUIRED_PLUGINS.stream().map(plugin -> plugin.id).collect(Collectors.toList()));
+  }
+
+  @Override
+  public boolean validate(
+      Project project, BlazeContext context, BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C)) {
+      return true;
+    }
+    boolean missingPlugin = false;
+    for (PluginNameAndId plugin : REQUIRED_PLUGINS) {
+      if (!PluginUtils.isPluginEnabled(plugin.id)) {
+        missingPlugin = true;
+        notifyMissingPlugin(context, plugin);
+      }
+    }
+    return !missingPlugin;
+  }
+
+  private static void notifyMissingPlugin(BlazeContext context, PluginNameAndId plugin) {
+    String msg =
+        String.format(
+            "Plugin '%s' required for NDK support isn't enabled.\n"
+                + "Click here to install/enable it, then restart the IDE",
+            plugin.name);
+    IssueOutput.error(msg)
+        .navigatable(PluginUtils.installOrEnablePluginNavigable(plugin.id))
+        .submit(context);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java b/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
index 3cb5351..f9228c6 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
@@ -33,6 +33,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -45,6 +46,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -88,7 +90,7 @@
         targetMap
             .targets()
             .stream()
-            .filter(target -> target.kind.getLanguageClass() == LanguageClass.ANDROID)
+            .filter(target -> target.kind.languageClass == LanguageClass.ANDROID)
             .filter(target -> target.androidIdeInfo != null)
             .filter(importFilter::isSourceTarget)
             .filter(target -> !importFilter.excludeTarget(target))
@@ -114,7 +116,20 @@
         buildAndroidResourceModules(workspaceBuilder);
     BlazeResourceLibrary resourceLibrary = createResourceLibrary(androidResourceModules);
 
-    return new BlazeAndroidImportResult(androidResourceModules, resourceLibrary);
+    return new BlazeAndroidImportResult(
+        androidResourceModules, resourceLibrary, getJavacJar(targetMap.targets()));
+  }
+
+  private static ArtifactLocation getJavacJar(Collection<TargetIdeInfo> targets) {
+    return targets
+        .stream()
+        .filter(target -> target.kind == Kind.JAVA_TOOLCHAIN)
+        .map(
+            target ->
+                target.javaToolchainIdeInfo != null ? target.javaToolchainIdeInfo.javacJar : null)
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse(null);
   }
 
   private void addSourceTarget(
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java
index b4750a2..0ac6cbd 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.android.sync.model;
 
 import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import java.io.Serializable;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -23,15 +24,18 @@
 /** The result of a blaze import operation. */
 @Immutable
 public class BlazeAndroidImportResult implements Serializable {
-  private static final long serialVersionUID = 3L;
+  private static final long serialVersionUID = 4L;
 
   public final ImmutableCollection<AndroidResourceModule> androidResourceModules;
   @Nullable public final BlazeResourceLibrary resourceLibrary;
+  @Nullable public final ArtifactLocation javacJar;
 
   public BlazeAndroidImportResult(
       ImmutableCollection<AndroidResourceModule> androidResourceModules,
-      @Nullable BlazeResourceLibrary resourceLibrary) {
+      @Nullable BlazeResourceLibrary resourceLibrary,
+      @Nullable ArtifactLocation javacJar) {
     this.androidResourceModules = androidResourceModules;
     this.resourceLibrary = resourceLibrary;
+    this.javacJar = javacJar;
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
index b89365f..79cd572 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
@@ -17,28 +17,36 @@
 
 import com.android.builder.model.SourceProvider;
 import com.android.sdklib.AndroidVersion;
-import com.android.tools.idea.model.AndroidModel;
 import com.android.tools.idea.model.ClassJarProvider;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.google.idea.blaze.base.actions.BlazeBuildService;
+import com.google.idea.sdkcompat.android.model.AndroidModelAdapter;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.vfs.JarFileSystem;
 import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.search.GlobalSearchScope;
 import java.io.File;
 import java.util.List;
 import java.util.Set;
+import javax.annotation.Nullable;
 import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Contains Android-Blaze related state necessary for configuring an IDEA project based on a
  * user-selected build variant.
  */
-public class BlazeAndroidModel implements AndroidModel {
+public class BlazeAndroidModel extends AndroidModelAdapter {
   private Project project;
   private final File rootDirPath;
   private final SourceProvider sourceProvider;
@@ -172,9 +180,58 @@
   }
 
   @Override
-  @Nullable
-  public Long getLastBuildTimestamp(Project project) {
-    // TODO(jvoung): Coordinate with blaze build actions to be able determine last build time.
-    return null;
+  public boolean isClassFileOutOfDate(Module module, String fqcn, VirtualFile classFile) {
+    VirtualFile sourceFile =
+        ApplicationManager.getApplication()
+            .runReadAction(
+                (Computable<VirtualFile>)
+                    () -> {
+                      PsiClass psiClass =
+                          JavaPsiFacade.getInstance(project)
+                              .findClass(fqcn, GlobalSearchScope.projectScope(project));
+                      if (psiClass == null) {
+                        return null;
+                      }
+                      PsiFile psiFile = psiClass.getContainingFile();
+                      if (psiFile == null) {
+                        return null;
+                      }
+                      return psiFile.getVirtualFile();
+                    });
+    if (sourceFile == null) {
+      return false;
+    }
+
+    // Edited but not yet saved?
+    if (FileDocumentManager.getInstance().isFileModified(sourceFile)) {
+      return true;
+    }
+
+    long sourceTimeStamp = sourceFile.getTimeStamp();
+    long buildTimeStamp = classFile.getTimeStamp();
+
+    if (classFile.getFileSystem() instanceof JarFileSystem) {
+      JarFileSystem jarFileSystem = (JarFileSystem) classFile.getFileSystem();
+      VirtualFile jarFile = jarFileSystem.getVirtualFileForJar(classFile);
+      if (jarFile != null) {
+        if (jarFile.getFileSystem() instanceof LocalFileSystem) {
+          // The virtual file timestamp could be stale since we don't watch this file.
+          buildTimeStamp = VfsUtilCore.virtualToIoFile(jarFile).lastModified();
+        } else {
+          buildTimeStamp = jarFile.getTimeStamp();
+        }
+      }
+    }
+
+    if (sourceTimeStamp > buildTimeStamp) {
+      // It's possible that the source file's timestamp has been updated, but the content remains
+      // same. In this case, blaze will not try to rebuild the jar, we have to also check whether
+      // the user recently clicked the build button. So they can at least manually get rid of the
+      // error.
+      Long projectBuildTimeStamp = BlazeBuildService.getLastBuildTimeStamp(project);
+      return projectBuildTimeStamp == null || sourceTimeStamp > projectBuildTimeStamp;
+    }
+
+    return false;
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
index 9641ced..cbad879 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
@@ -32,6 +32,7 @@
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.google.idea.sdkcompat.android.res.AppResourceRepositoryAdapter;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.module.Module;
@@ -44,20 +45,21 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
-import org.jetbrains.annotations.Nullable;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
 
 /** Collects class jars from the user's build. */
 public class BlazeClassJarProvider extends ClassJarProvider {
 
   private final Project project;
-  private AtomicBoolean pendingModuleJarsRefresh;
-  private AtomicBoolean pendingDependencyJarsRefresh;
+  private final AtomicBoolean pendingJarsRefresh;
 
   public BlazeClassJarProvider(final Project project) {
     this.project = project;
-    this.pendingModuleJarsRefresh = new AtomicBoolean(false);
-    this.pendingDependencyJarsRefresh = new AtomicBoolean(false);
+    this.pendingJarsRefresh = new AtomicBoolean(false);
   }
 
   @Override
@@ -69,6 +71,7 @@
       return null;
     }
 
+    TargetMap targetMap = blazeProjectData.targetMap;
     ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
     AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
     TargetIdeInfo target = blazeProjectData.targetMap.get(registry.getTargetKey(module));
@@ -84,9 +87,19 @@
 
     String classNamePath = className.replace('.', File.separatorChar) + SdkConstants.DOT_CLASS;
 
+    List<LibraryArtifact> jarsToSearch = Lists.newArrayList(target.javaIdeInfo.jars);
+    jarsToSearch.addAll(
+        TransitiveDependencyMap.getInstance(project)
+            .getTransitiveDependencies(target.key)
+            .stream()
+            .map(targetMap::get)
+            .filter(Objects::nonNull)
+            .flatMap(BlazeClassJarProvider::getNonResourceJars)
+            .collect(Collectors.toList()));
+
     List<File> missingClassJars = Lists.newArrayList();
-    for (LibraryArtifact jar : target.javaIdeInfo.jars) {
-      if (jar.classJar == null) {
+    for (LibraryArtifact jar : jarsToSearch) {
+      if (jar.classJar == null || jar.classJar.isSource()) {
         continue;
       }
       File classJarFile = decoder.decode(jar.classJar);
@@ -104,10 +117,21 @@
       }
     }
 
-    maybeRefreshJars(missingClassJars, pendingModuleJarsRefresh);
+    maybeRefreshJars(missingClassJars, pendingJarsRefresh);
     return null;
   }
 
+  private static Stream<LibraryArtifact> getNonResourceJars(TargetIdeInfo target) {
+    if (target.javaIdeInfo == null) {
+      return null;
+    }
+    Stream<LibraryArtifact> jars = target.javaIdeInfo.jars.stream();
+    if (target.androidIdeInfo != null) {
+      jars = jars.filter(jar -> !jar.equals(target.androidIdeInfo.resourceJar));
+    }
+    return jars;
+  }
+
   @Nullable
   private static VirtualFile findClassInJar(final VirtualFile classJar, String classNamePath) {
     VirtualFile jarRoot = getJarRootForLocalFile(classJar);
@@ -137,38 +161,26 @@
       return results;
     }
 
-    AppResourceRepository repository = AppResourceRepository.getAppResources(module, true);
+    AppResourceRepository repository = AppResourceRepositoryAdapter.getOrCreateInstance(module);
 
-    List<File> missingClassJars = Lists.newArrayList();
     for (TargetKey dependencyTargetKey :
         TransitiveDependencyMap.getInstance(project).getTransitiveDependencies(target.key)) {
       TargetIdeInfo dependencyTarget = targetMap.get(dependencyTargetKey);
       if (dependencyTarget == null) {
         continue;
       }
-      JavaIdeInfo javaIdeInfo = dependencyTarget.javaIdeInfo;
-      AndroidIdeInfo androidIdeInfo = dependencyTarget.androidIdeInfo;
 
-      // Add all non-resource jars to be searched.
-      // Multiple resource jars will have ID conflicts unless generated dynamically.
+      // Add all import jars as external libraries.
+      JavaIdeInfo javaIdeInfo = dependencyTarget.javaIdeInfo;
       if (javaIdeInfo != null) {
         for (LibraryArtifact jar : javaIdeInfo.jars) {
-          if (androidIdeInfo != null && jar.equals(androidIdeInfo.resourceJar)) {
-            // No resource jars.
-            continue;
-          }
-          // Some of these could be empty class jars from resource only android_library targets.
-          // A potential optimization could be to filter out jars like these,
-          // so we don't waste time fetching and searching them.
-          // TODO: benchmark to see if optimization is worthwhile.
-          if (jar.classJar != null) {
-            File classJarFile = decoder.decode(jar.classJar);
+          if (jar.classJar != null && jar.classJar.isSource()) {
             VirtualFile classJar =
-                VirtualFileSystemProvider.getInstance().getSystem().findFileByIoFile(classJarFile);
+                VirtualFileSystemProvider.getInstance()
+                    .getSystem()
+                    .findFileByIoFile(decoder.decode(jar.classJar));
             if (classJar != null) {
               results.add(classJar);
-            } else if (classJarFile.exists()) {
-              missingClassJars.add(classJarFile);
             }
           }
         }
@@ -185,13 +197,13 @@
       // The resource repository remembers the dynamic IDs that it handed out and when the layoutlib
       // calls to ask about the name and content of a given resource ID, the repository can just
       // answer what it has already stored.
+      AndroidIdeInfo androidIdeInfo = dependencyTarget.androidIdeInfo;
       if (androidIdeInfo != null && repository != null) {
         ResourceClassRegistry.get(module.getProject())
             .addLibrary(repository, androidIdeInfo.resourceJavaPackage);
       }
     }
 
-    maybeRefreshJars(missingClassJars, pendingDependencyJarsRefresh);
     return results;
   }
 
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
index c4eb537..17b2fb7 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
@@ -67,8 +67,8 @@
       return null;
     }
 
-    String androidSdk = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
-    Integer androidMinSdk = projectViewSet.getScalarValue(AndroidMinSdkSection.KEY);
+    String androidSdk = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY).orElse(null);
+    Integer androidMinSdk = projectViewSet.getScalarValue(AndroidMinSdkSection.KEY).orElse(null);
 
     if (androidSdk == null) {
       ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/plugin/NdkDependenciesTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/plugin/NdkDependenciesTest.java
new file mode 100644
index 0000000..88e15b1
--- /dev/null
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/plugin/NdkDependenciesTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.plugin;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
+import com.google.idea.blaze.android.sync.BlazeNdkDependencySyncPlugin;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+import com.google.idea.testing.DisablePluginsTestRule;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that ASwB successfully loads when the NDK plugins aren't enabled. Must be run as a
+ * stand-alone test, because it requires its own {@link Application}, and only one such object can
+ * be constructed per test suite.
+ */
+@RunWith(JUnit4.class)
+public class NdkDependenciesTest extends BlazeIntegrationTestCase {
+
+  @ClassRule
+  public static TestRule setDisabledPlugins =
+      new DisablePluginsTestRule(BlazeNdkDependencySyncPlugin.getPluginsRequiredForNdkSupport());
+
+  @Test
+  public void testNdkPluginsInstalled() {
+    assertThat(PluginUtils.isPluginInstalled("com.android.tools.ndk")).isTrue();
+    assertThat(PluginUtils.isPluginInstalled("com.google.idea.bazel.aswb")).isTrue();
+  }
+
+  @Test
+  public void testPluginLoadsWithoutNdkPlugins() {
+    assertThat(PluginUtils.isPluginEnabled("com.android.tools.ndk")).isFalse();
+    assertThat(PluginUtils.isPluginEnabled("com.google.idea.bazel.aswb")).isTrue();
+
+    // Plugins with classloader failures can be spuriously marked as enabled. Also test that
+    // ASwB plugin classes are actually loaded.
+    assertThat(BlazeAndroidUserSettings.getInstance()).isNotNull();
+  }
+}
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/AswbPrefetchFileSourceTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/AswbPrefetchFileSourceTest.java
new file mode 100644
index 0000000..85fe799
--- /dev/null
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/AswbPrefetchFileSourceTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.prefetch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for file extensions prefetched in ASwB. */
+@RunWith(JUnit4.class)
+public class AswbPrefetchFileSourceTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testPrefetchedExtensions() {
+    assertThat(PrefetchFileSource.getAllPrefetchFileExtensions())
+        .containsExactly(
+            "java", "proto", "c", "cc", "cpp", "cxx", "c++", "C", "h", "hh", "hpp", "hxx", "inc",
+            "xml");
+  }
+}
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/CPrefetchFileSourceTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/CPrefetchFileSourceTest.java
new file mode 100644
index 0000000..354f1e3
--- /dev/null
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/prefetch/CPrefetchFileSourceTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2017 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.idea.blaze.android.prefetch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.google.idea.blaze.cpp.CPrefetchFileSource;
+import java.io.File;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link CPrefetchFileSource}. */
+@RunWith(JUnit4.class)
+public class CPrefetchFileSourceTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testSourceFilesInProjectIgnored() {
+    ProjectViewSet projectViewSet =
+        parseProjectView(
+            "directories:",
+            "  java/com/google",
+            "targets:",
+            "  //java/com/google:lib",
+            "additional_languages:",
+            "  c",
+            "android_sdk_platform: android-25");
+
+    BlazeProjectData projectData =
+        MockBlazeProjectDataBuilder.builder(workspaceRoot)
+            .setTargetMap(
+                TargetMapBuilder.builder()
+                    .addTarget(
+                        TargetIdeInfo.builder()
+                            .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                            .setLabel("//java/com/google:lib")
+                            .setKind("cc_library")
+                            .addSource(sourceRoot("java/com/google/native.cc"))
+                            .addSource(sourceRoot("java/com/google/native.h")))
+                    .build())
+            .setWorkspaceLanguageSettings(
+                LanguageSupport.createWorkspaceLanguageSettings(projectViewSet))
+            .build();
+
+    Set<File> filesToPrefetch = new HashSet<>();
+    new CPrefetchFileSource()
+        .addFilesToPrefetch(
+            getProject(),
+            projectViewSet,
+            getImportRoots(projectViewSet),
+            projectData,
+            filesToPrefetch);
+
+    assertThat(filesToPrefetch).isEmpty();
+  }
+
+  @Test
+  public void testCppHeaderFilesOutsideProjectIncluded() {
+    ProjectViewSet projectViewSet =
+        parseProjectView(
+            "directories:",
+            "  java/com/google",
+            "targets:",
+            "  //java/com/google:lib",
+            "additional_languages:",
+            "  c",
+            "android_sdk_platform: android-25");
+
+    BlazeProjectData projectData =
+        MockBlazeProjectDataBuilder.builder(workspaceRoot)
+            .setTargetMap(
+                TargetMapBuilder.builder()
+                    .addTarget(
+                        TargetIdeInfo.builder()
+                            .setBuildFile(sourceRoot("third_party/library/BUILD"))
+                            .setLabel("//third_party/library:dep")
+                            .setKind("cc_library")
+                            .setCInfo(
+                                CIdeInfo.builder()
+                                    .addSource(sourceRoot("third_party/library/main.cc"))
+                                    .addHeader(sourceRoot("third_party/library/dep.h"))
+                                    .addHeader(sourceRoot("third_party/library/other.h"))
+                                    .addTextualHeader(sourceRoot("third_party/library/textual.h"))))
+                    .build())
+            .setWorkspaceLanguageSettings(
+                LanguageSupport.createWorkspaceLanguageSettings(projectViewSet))
+            .build();
+
+    Set<File> filesToPrefetch = new HashSet<>();
+    new CPrefetchFileSource()
+        .addFilesToPrefetch(
+            getProject(),
+            projectViewSet,
+            getImportRoots(projectViewSet),
+            projectData,
+            filesToPrefetch);
+
+    assertThat(filesToPrefetch)
+        .containsExactly(
+            workspaceFile("third_party/library/dep.h"),
+            workspaceFile("third_party/library/other.h"),
+            workspaceFile("third_party/library/textual.h"));
+  }
+
+  @Test
+  public void testJavaSourceFilesIgnored() {
+    ProjectViewSet projectViewSet =
+        parseProjectView(
+            "directories:",
+            "  java/com/google",
+            "targets:",
+            "  //java/com/google:lib",
+            "additional_languages:",
+            "  c",
+            "android_sdk_platform: android-25");
+
+    BlazeProjectData projectData =
+        MockBlazeProjectDataBuilder.builder(workspaceRoot)
+            .setTargetMap(
+                TargetMapBuilder.builder()
+                    .addTarget(
+                        TargetIdeInfo.builder()
+                            .setBuildFile(sourceRoot("third_party/library/BUILD"))
+                            .setLabel("//third_party/library:lib")
+                            .setKind("java_library")
+                            .addSource(sourceRoot("third_party/library/Library.java")))
+                    .build())
+            .setWorkspaceLanguageSettings(
+                LanguageSupport.createWorkspaceLanguageSettings(projectViewSet))
+            .build();
+
+    Set<File> filesToPrefetch = new HashSet<>();
+    new CPrefetchFileSource()
+        .addFilesToPrefetch(
+            getProject(),
+            projectViewSet,
+            getImportRoots(projectViewSet),
+            projectData,
+            filesToPrefetch);
+
+    assertThat(filesToPrefetch).isEmpty();
+  }
+
+  private ProjectViewSet parseProjectView(String... contents) {
+    ProjectViewParser projectViewParser =
+        new ProjectViewParser(new BlazeContext(), new WorkspacePathResolverImpl(workspaceRoot));
+    projectViewParser.parseProjectView(Joiner.on("\n").join(contents));
+    return projectViewParser.getResult();
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+
+  private File workspaceFile(String workspacePath) {
+    return workspaceRoot.fileForPath(new WorkspacePath(workspacePath));
+  }
+
+  private ImportRoots getImportRoots(ProjectViewSet projectViewSet) {
+    BlazeImportSettings importSettings =
+        BlazeImportSettingsManager.getInstance(getProject()).getImportSettings();
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+    return ImportRoots.builder(workspaceRoot, importSettings.getBuildSystem())
+        .add(projectViewSet)
+        .build();
+  }
+}
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
index 0d9cba6..0df7e0c 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
@@ -42,6 +42,7 @@
   @Rule
   public final AndroidIntegrationTestSetupRule androidSetupRule =
       new AndroidIntegrationTestSetupRule();
+
   private BlazeAndroidRunConfigurationCommonState state;
 
   @Before
@@ -58,6 +59,7 @@
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exe1", "--exe2"));
     state.setNativeDebuggingEnabled(true);
 
     Element element = new Element("test");
@@ -69,6 +71,9 @@
     assertThat(readState.getBlazeFlagsState().getRawFlags())
         .containsExactly("--flag1", "--flag2")
         .inOrder();
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .containsExactly("--exe1", "--exe2")
+        .inOrder();
     assertThat(readState.isNativeDebuggingEnabled()).isTrue();
   }
 
@@ -82,6 +87,8 @@
 
     assertThat(readState.getBlazeFlagsState().getRawFlags())
         .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
     assertThat(readState.isNativeDebuggingEnabled()).isEqualTo(state.isNativeDebuggingEnabled());
   }
 
@@ -90,6 +97,9 @@
     state
         .getBlazeFlagsState()
         .setRawFlags(ImmutableList.of("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    state
+        .getExeFlagsState()
+        .setRawFlags(ImmutableList.of("one ", "", "two", " ", "\t", "three\r\n", "\n"));
 
     Element element = new Element("test");
     state.writeExternal(element);
@@ -100,6 +110,9 @@
     assertThat(readState.getBlazeFlagsState().getRawFlags())
         .containsExactly("hi", "I'm", "Josh")
         .inOrder();
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .containsExactly("one", "two", "three")
+        .inOrder();
   }
 
   @Test
@@ -107,6 +120,7 @@
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
 
     state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exe1", "--exe2"));
     state.setNativeDebuggingEnabled(true);
 
     Element firstWrite = new Element("test");
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
index e72c5de..51f8f49 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
@@ -22,6 +22,7 @@
 import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryLaunchMethodsProvider.AndroidBinaryLaunchMethod;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
@@ -45,6 +46,7 @@
   @Rule
   public final AndroidIntegrationTestSetupRule androidSetupRule =
       new AndroidIntegrationTestSetupRule();
+
   private BlazeAndroidBinaryRunConfigurationState state;
 
   @Before
@@ -66,7 +68,7 @@
 
     state.setActivityClass("com.example.TestActivity");
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-    state.setMobileInstall(true);
+    state.setLaunchMethod(AndroidBinaryLaunchMethod.MOBILE_INSTALL);
     state.setUseSplitApksIfPossible(false);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
@@ -87,7 +89,7 @@
     assertThat(readState.getActivityClass()).isEqualTo("com.example.TestActivity");
     assertThat(readState.getMode())
         .isEqualTo(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-    assertThat(readState.mobileInstall()).isTrue();
+    assertThat(readState.getLaunchMethod()).isEqualTo(AndroidBinaryLaunchMethod.MOBILE_INSTALL);
     assertThat(readState.useSplitApksIfPossible()).isFalse();
     assertThat(readState.useWorkProfileIfPresent()).isTrue();
     assertThat(readState.getUserId()).isEqualTo(2);
@@ -111,7 +113,7 @@
 
     assertThat(readState.getActivityClass()).isEqualTo(state.getActivityClass());
     assertThat(readState.getMode()).isEqualTo(state.getMode());
-    assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
@@ -128,7 +130,7 @@
 
     state.setActivityClass("com.example.TestActivity");
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-    state.setMobileInstall(true);
+    state.setLaunchMethod(AndroidBinaryLaunchMethod.MOBILE_INSTALL);
     state.setUseSplitApksIfPossible(false);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
@@ -153,12 +155,12 @@
 
     state.setActivityClass("com.example.TestActivity");
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-    state.setMobileInstall(true);
+    state.setLaunchMethod(AndroidBinaryLaunchMethod.MOBILE_INSTALL);
     state.setUseSplitApksIfPossible(false);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
     // We don't test DeepLink because it is not exposed in the editor.
-    //state.setDeepLink("http://deeplink");
+    // state.setDeepLink("http://deeplink");
 
     editor.resetEditorFrom(state);
     BlazeAndroidBinaryRunConfigurationState readState =
@@ -173,12 +175,12 @@
 
     assertThat(readState.getActivityClass()).isEqualTo(state.getActivityClass());
     assertThat(readState.getMode()).isEqualTo(state.getMode());
-    assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
     // We don't test DeepLink because it is not exposed in the editor.
-    //assertThat(readState.getDeepLink()).isEqualTo(state.getDeepLink());
+    // assertThat(readState.getDeepLink()).isEqualTo(state.getDeepLink());
   }
 
   @Test
@@ -199,11 +201,11 @@
 
     assertThat(readState.getActivityClass()).isEqualTo(state.getActivityClass());
     assertThat(readState.getMode()).isEqualTo(state.getMode());
-    assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
     // We don't test DeepLink because it is not exposed in the editor.
-    //assertThat(readState.getDeepLink()).isEqualTo(state.getDeepLink());
+    // assertThat(readState.getDeepLink()).isEqualTo(state.getDeepLink());
   }
 }
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
index 4944264..800ea7c 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.test.BlazeAndroidTestLaunchMethodsProvider.AndroidTestLaunchMethod;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
@@ -44,6 +45,7 @@
   @Rule
   public final AndroidIntegrationTestSetupRule androidSetupRule =
       new AndroidIntegrationTestSetupRule();
+
   private BlazeAndroidTestRunConfigurationState state;
 
   @Before
@@ -61,6 +63,7 @@
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getExeFlagsState().setRawFlags(ImmutableList.of("--exe1", "--exe2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setTestingType(BlazeAndroidTestRunConfigurationState.TEST_METHOD);
@@ -68,7 +71,7 @@
     state.setMethodName("fooMethod");
     state.setClassName("BarClass");
     state.setPackageName("com.test.package.name");
-    state.setRunThroughBlaze(true);
+    state.setLaunchMethod(AndroidTestLaunchMethod.BLAZE_TEST);
     state.setExtraOptions("--option");
 
     Element element = new Element("test");
@@ -81,6 +84,9 @@
     assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
         .containsExactly("--flag1", "--flag2")
         .inOrder();
+    assertThat(readCommonState.getExeFlagsState().getRawFlags())
+        .containsExactly("--exe1", "--exe2")
+        .inOrder();
     assertThat(readCommonState.isNativeDebuggingEnabled()).isTrue();
 
     assertThat(readState.getTestingType())
@@ -89,7 +95,7 @@
     assertThat(readState.getMethodName()).isEqualTo("fooMethod");
     assertThat(readState.getClassName()).isEqualTo("BarClass");
     assertThat(readState.getPackageName()).isEqualTo("com.test.package.name");
-    assertThat(readState.isRunThroughBlaze()).isTrue();
+    assertThat(readState.getLaunchMethod()).isEqualTo(AndroidTestLaunchMethod.BLAZE_TEST);
     assertThat(readState.getExtraOptions()).isEqualTo("--option");
   }
 
@@ -105,6 +111,8 @@
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
     assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
         .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
+    assertThat(readCommonState.getExeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getExeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -114,7 +122,7 @@
     assertThat(readState.getMethodName()).isEqualTo(state.getMethodName());
     assertThat(readState.getClassName()).isEqualTo(state.getClassName());
     assertThat(readState.getPackageName()).isEqualTo(state.getPackageName());
-    assertThat(readState.isRunThroughBlaze()).isEqualTo(state.isRunThroughBlaze());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     assertThat(readState.getExtraOptions()).isEqualTo(state.getExtraOptions());
   }
 
@@ -131,7 +139,7 @@
     state.setMethodName("fooMethod");
     state.setClassName("BarClass");
     state.setPackageName("com.test.package.name");
-    state.setRunThroughBlaze(true);
+    state.setLaunchMethod(AndroidTestLaunchMethod.MOBILE_INSTALL);
     state.setExtraOptions("--option");
 
     Element firstWrite = new Element("test");
@@ -149,6 +157,7 @@
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getExeFlagsState().setRawFlags(ImmutableList.of("--exe1", "--exe2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setTestingType(BlazeAndroidTestRunConfigurationState.TEST_METHOD);
@@ -156,9 +165,9 @@
     state.setMethodName("fooMethod");
     state.setClassName("BarClass");
     state.setPackageName("com.test.package.name");
-    state.setRunThroughBlaze(true);
+    state.setLaunchMethod(AndroidTestLaunchMethod.BLAZE_TEST);
     // We don't test ExtraOptions because it is not exposed in the editor.
-    //state.setExtraOptions("--option");
+    // state.setExtraOptions("--option");
 
     editor.resetEditorFrom(state);
     BlazeAndroidTestRunConfigurationState readState =
@@ -168,6 +177,8 @@
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
     assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
         .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
+    assertThat(readCommonState.getExeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getExeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -177,9 +188,9 @@
     assertThat(readState.getMethodName()).isEqualTo(state.getMethodName());
     assertThat(readState.getClassName()).isEqualTo(state.getClassName());
     assertThat(readState.getPackageName()).isEqualTo(state.getPackageName());
-    assertThat(readState.isRunThroughBlaze()).isEqualTo(state.isRunThroughBlaze());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     // We don't test ExtraOptions because it is not exposed in the editor.
-    //assertThat(readState.getExtraOptions()).isEqualTo(state.getExtraOptions());
+    // assertThat(readState.getExtraOptions()).isEqualTo(state.getExtraOptions());
   }
 
   @Test
@@ -195,6 +206,8 @@
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
     assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
         .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
+    assertThat(readCommonState.getExeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getExeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -204,8 +217,8 @@
     assertThat(readState.getMethodName()).isEqualTo(state.getMethodName());
     assertThat(readState.getClassName()).isEqualTo(state.getClassName());
     assertThat(readState.getPackageName()).isEqualTo(state.getPackageName());
-    assertThat(readState.isRunThroughBlaze()).isEqualTo(state.isRunThroughBlaze());
+    assertThat(readState.getLaunchMethod()).isEqualTo(state.getLaunchMethod());
     // We don't test ExtraOptions because it is not exposed in the editor.
-    //assertThat(readState.getExtraOptions()).isEqualTo(state.getExtraOptions());
+    // assertThat(readState.getExtraOptions()).isEqualTo(state.getExtraOptions());
   }
 }
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
index a712648..f1f048a 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
@@ -42,7 +42,10 @@
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleFinder;
 import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.google.idea.blaze.cpp.CompilerVersionChecker;
+import com.google.idea.blaze.cpp.MockCompilerVersionChecker;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.sdkcompat.cidr.CidrCompilerSwitchesAdapter;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.projectRoots.SdkTypeId;
@@ -74,6 +77,8 @@
   public void setup() {
     mockSdk("android-25", "Android 25 SDK");
     registerProjectService(OCWorkspaceManager.class, new MockOCWorkspaceManager());
+    registerApplicationService(
+        CompilerVersionChecker.class, new MockCompilerVersionChecker("1234"));
   }
 
   private void mockSdk(String targetHash, String sdkName) {
@@ -288,7 +293,8 @@
                             .addTransitiveSystemIncludeDirectories(
                                 ImmutableList.of(
                                     new ExecutionRootPath("third_party/stl/gcc3"),
-                                    new ExecutionRootPath("third_party/java/jdk/include"))))
+                                    new ExecutionRootPath("third_party/java/jdk/include")))
+                            .addSource(sourceRoot("java/com/google/jni/native.cc")))
                     .addSource(sourceRoot("java/com/google/jni/native.cc"))
                     .addDependency("//android_ndk_linux/toolchains:aarch64"))
             .addTarget(
@@ -307,7 +313,8 @@
                             .addTransitiveSystemIncludeDirectories(
                                 ImmutableList.of(
                                     new ExecutionRootPath("third_party/stl/gcc3"),
-                                    new ExecutionRootPath("third_party/java/jdk/include"))))
+                                    new ExecutionRootPath("third_party/java/jdk/include")))
+                            .addSource(sourceRoot("java/com/google/jni/native2.cc")))
                     .addSource(sourceRoot("java/com/google/jni/native2.cc"))
                     .addDependency("//java/com/google:native_lib")
                     .addDependency("//android_ndk_linux/toolchains:armv7a"))
@@ -374,7 +381,8 @@
     assertThat(resolveConfigurations).hasSize(1);
     OCCompilerSettings compilerSettings = resolveConfigurations.get(0).getCompilerSettings();
     List<String> compilerSwitches =
-        compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc).getCommandLineArgs();
+        CidrCompilerSwitchesAdapter.getCommandLineArgs(
+            compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc));
     assertThat(compilerSwitches)
         .contains("--sysroot=android_ndk_linux/platforms/android-21/arch-arm64");
 
@@ -383,7 +391,8 @@
     assertThat(resolveConfigurations).hasSize(1);
     compilerSettings = resolveConfigurations.get(0).getCompilerSettings();
     compilerSwitches =
-        compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc).getCommandLineArgs();
+        CidrCompilerSwitchesAdapter.getCommandLineArgs(
+            compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc));
     assertThat(compilerSwitches)
         .contains("--sysroot=android_ndk_linux/platforms/android-18/arch-arm");
   }
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
index e784f6a..f55ad08 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
@@ -39,6 +39,7 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.sdkcompat.android.res.AppResourceRepositoryAdapter;
 import com.intellij.facet.FacetManager;
 import com.intellij.facet.ModifiableFacetModel;
 import com.intellij.openapi.application.ApplicationManager;
@@ -84,75 +85,165 @@
     createClassesInJars();
 
     // Make sure we can find classes in the main resource module.
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MainActivity", module))
+    VirtualFile found =
+        classJarProvider.findModuleClassFile("com.google.example.main.MainActivity", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
         .isEqualTo(
             fileSystem.findFile(
                 BLAZE_BIN
-                    + "/com/google/example/main.jar!"
+                    + "/com/google/example/libmain.jar!"
                     + "/com/google/example/main/MainActivity.class"));
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R", module))
+    found = classJarProvider.findModuleClassFile("com.google.example.main.R", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
         .isEqualTo(
             fileSystem.findFile(
                 BLAZE_BIN
                     + "/com/google/example/main_resources.jar!"
                     + "/com/google/example/main/R.class"));
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$string", module))
+
+    found = classJarProvider.findModuleClassFile("com.google.example.main.R$string", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
         .isEqualTo(
             fileSystem.findFile(
                 BLAZE_BIN
                     + "/com/google/example/main_resources.jar!"
                     + "/com/google/example/main/R$string.class"));
 
+    // And dependencies.
+    found = classJarProvider.findModuleClassFile("com.google.example.java.Java", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libjava.jar!"
+                    + "/com/google/example/java/Java.class"));
+    found = classJarProvider.findModuleClassFile("com.google.example.shared.Shared", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libshared.jar!"
+                    + "/com/google/example/shared/Shared.class"));
+    found = classJarProvider.findModuleClassFile("com.google.example.shared2.Shared2", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libshared2.jar!"
+                    + "/com/google/example/shared2/Shared2.class"));
+    found =
+        classJarProvider.findModuleClassFile("com.google.example.transitive.Transitive", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libtransitive.jar!"
+                    + "/com/google/example/transitive/Transitive.class"));
+
+    // But not resource classes in dependencies.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.resource.R", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.resource.R$style", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.resource.R$layout", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.resource2.R", module))
+        .isNull();
+
     // And not classes that are missing.
     assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MissingClass", module))
         .isNull();
     assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$missing", module))
         .isNull();
 
-    // And not classes in other libraries.
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.java.CustomView", module))
-        .isNull();
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.android_res.R", module))
-        .isNull();
-    assertThat(
-            classJarProvider.findModuleClassFile("com.google.example.android_res.R$style", module))
+    // And not imported libraries.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.import.Import", module))
         .isNull();
     assertThat(
             classJarProvider.findModuleClassFile(
-                "com.google.unrelated.android_res.R$layout", module))
+                "com.google.example.transitive.import.TransitiveImport", module))
         .isNull();
+
+    // And not unrelated libraries.
+    assertThat(classJarProvider.findModuleClassFile("com.google.unrelated.Java", module)).isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.unrelated.Android", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.unrelated.Resource", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.unrelated.R", module)).isNull();
   }
 
   @Test
-  public void testMissingMainJar() {
+  public void testMissingClassJars() {
     createClassesInJars();
 
     ApplicationManager.getApplication()
         .runWriteAction(
             () -> {
               try {
-                // Let's pretend that this hasn't been built yet.
-                fileSystem.findFile(BLAZE_BIN + "/com/google/example/main.jar").delete(this);
+                // Let's pretend that these haven't been built yet.
+                fileSystem.findFile(BLAZE_BIN + "/com/google/example/libmain.jar").delete(this);
+                fileSystem.findFile(BLAZE_BIN + "/com/google/example/libjava.jar").delete(this);
+                fileSystem.findFile(BLAZE_BIN + "/com/google/example/libshared.jar").delete(this);
+                fileSystem
+                    .findFile(BLAZE_BIN + "/com/google/example/libtransitive.jar")
+                    .delete(this);
               } catch (IOException ignored) {
                 // ignored
               }
             });
-    // This hasn't been built yet, and shouldn't be found.
+    // These hasn't been built yet, and shouldn't be found.
     assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MainActivity", module))
         .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.java.Java", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.shared.Shared", module))
+        .isNull();
+    assertThat(
+            classJarProvider.findModuleClassFile(
+                "com.google.example.transitive.Transitive", module))
+        .isNull();
+
     // But these should still be found.
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R", module))
+    VirtualFile found = classJarProvider.findModuleClassFile("com.google.example.main.R", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
         .isEqualTo(
             fileSystem.findFile(
                 BLAZE_BIN
                     + "/com/google/example/main_resources.jar!"
                     + "/com/google/example/main/R.class"));
-    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$string", module))
+    found = classJarProvider.findModuleClassFile("com.google.example.main.R$string", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
         .isEqualTo(
             fileSystem.findFile(
                 BLAZE_BIN
                     + "/com/google/example/main_resources.jar!"
                     + "/com/google/example/main/R$string.class"));
+    found = classJarProvider.findModuleClassFile("com.google.example.android.Android", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libandroid.jar!"
+                    + "/com/google/example/android/Android.class"));
+    found = classJarProvider.findModuleClassFile("com.google.example.shared2.Shared2", module);
+    assertThat(found).isNotNull();
+    assertThat(found)
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/libshared2.jar!"
+                    + "/com/google/example/shared2/Shared2.class"));
   }
 
   @Test
@@ -169,53 +260,40 @@
     List<VirtualFile> externalLibraries = classJarProvider.getModuleExternalLibraries(module);
     assertThat(externalLibraries)
         .containsExactly(
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_lib.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res2.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/android_res.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/java.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared/java.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar"),
-            fileSystem.findFile("com/google/example/import.jar"),
-            fileSystem.findFile("com/google/example/transitive/import.jar"),
-            fileSystem.findFile("com/google/example/transitive/import2.jar"));
+            fileSystem.findFile("com/google/example/libimport.jar"),
+            fileSystem.findFile("com/google/example/transitive/libimport.jar"),
+            fileSystem.findFile("com/google/example/transitive/libimport2.jar"));
 
     // Make sure we can generate dynamic classes from all resource packages in dependencies.
     ResourceClassRegistry registry = ResourceClassRegistry.get(getProject());
-    AppResourceRepository repository = AppResourceRepository.getAppResources(module, false);
+    AppResourceRepository repository = AppResourceRepositoryAdapter.findExistingInstance(module);
     assertThat(repository).isNotNull();
-    assertThat(registry.findClassDefinition("com.google.example.android_res.R", repository))
+    assertThat(registry.findClassDefinition("com.google.example.resource.R", repository))
         .isNotNull();
-    assertThat(registry.findClassDefinition("com.google.example.android_res.R$string", repository))
+    assertThat(registry.findClassDefinition("com.google.example.resource.R$string", repository))
         .isNotNull();
-    assertThat(registry.findClassDefinition("com.google.example.android_res2.R", repository))
+    assertThat(registry.findClassDefinition("com.google.example.resource2.R", repository))
         .isNotNull();
-    assertThat(registry.findClassDefinition("com.google.example.android_res2.R$layout", repository))
+    assertThat(registry.findClassDefinition("com.google.example.resource2.R$layout", repository))
         .isNotNull();
-    assertThat(
-            registry.findClassDefinition("com.google.example.transitive.android_res.R", repository))
+    assertThat(registry.findClassDefinition("com.google.example.transitive.resource.R", repository))
         .isNotNull();
     assertThat(
             registry.findClassDefinition(
-                "com.google.example.transitive.android_res.R$style", repository))
+                "com.google.example.transitive.resource.R$style", repository))
         .isNotNull();
 
     // And nothing else.
     assertThat(registry.findClassDefinition("com.google.example.main.MainActivity", repository))
         .isNull();
-    assertThat(registry.findClassDefinition("com.google.example.android_res.Bogus", repository))
+    assertThat(registry.findClassDefinition("com.google.example.resource.Bogus", repository))
         .isNull();
     assertThat(registry.findClassDefinition("com.google.example.main.R", repository)).isNull();
     assertThat(registry.findClassDefinition("com.google.example.main.R$string", repository))
         .isNull();
-    assertThat(registry.findClassDefinition("com.google.example.java.CustomView", repository))
-        .isNull();
-    assertThat(registry.findClassDefinition("com.google.unrelated.android_res.R", repository))
-        .isNull();
-    assertThat(
-            registry.findClassDefinition("com.google.unrelated.android_res.R$layout", repository))
-        .isNull();
+    assertThat(registry.findClassDefinition("com.google.example.java.Java", repository)).isNull();
+    assertThat(registry.findClassDefinition("com.google.unrelated.R", repository)).isNull();
+    assertThat(registry.findClassDefinition("com.google.unrelated.R$layout", repository)).isNull();
   }
 
   @Test
@@ -224,14 +302,9 @@
         .runWriteAction(
             () -> {
               try {
-                // Let's pretend that these haven't been built yet.
-                fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar").delete(this);
-                fileSystem
-                    .findFile(BLAZE_BIN + "/com/google/example/android_res2.jar")
-                    .delete(this);
-                fileSystem
-                    .findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar")
-                    .delete(this);
+                // Let's pretend that these were deleted.
+                fileSystem.findFile("com/google/example/libimport.jar").delete(this);
+                fileSystem.findFile("com/google/example/transitive/libimport.jar").delete(this);
               } catch (IOException ignored) {
                 // ignored
               }
@@ -239,26 +312,16 @@
     List<VirtualFile> externalLibraries = classJarProvider.getModuleExternalLibraries(module);
     assertThat(externalLibraries)
         .containsExactly(
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_lib.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res.jar"),
-            // This should be missing.
-            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res2.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/android_res.jar"),
-            // This should be missing.
-            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/java.jar"),
-            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared/java.jar"),
-            // This should be missing.
-            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar"),
-            fileSystem.findFile("com/google/example/import.jar"),
-            fileSystem.findFile("com/google/example/transitive/import.jar"),
-            fileSystem.findFile("com/google/example/transitive/import2.jar"));
+            // These should be missing.
+            // fileSystem.findFile("com/google/example/libimport.jar"),
+            // fileSystem.findFile("com/google/example/transitive/libimport.jar"),
+            fileSystem.findFile("com/google/example/transitive/libimport2.jar"));
   }
 
   private void createClassesInJars() {
     fileSystem.createFile(
         BLAZE_BIN
-            + "/com/google/example/main.jar!"
+            + "/com/google/example/libmain.jar!"
             + "/com/google/example/main/MainActivity.class");
     fileSystem.createFile(
         BLAZE_BIN + "/com/google/example/main_resources.jar!" + "/com/google/example/main/R.class");
@@ -267,37 +330,81 @@
             + "/com/google/example/main_resources.jar!"
             + "/com/google/example/main/R$string.class");
     fileSystem.createFile(
-        BLAZE_BIN + "/com/google/example/java.jar!" + "/com/google/example/java/CustomView.class");
+        BLAZE_BIN
+            + "/com/google/example/libandroid.jar!"
+            + "/com/google/example/android/Android.class");
     fileSystem.createFile(
         BLAZE_BIN
-            + "/com/google/example/android_res_resources.jar!"
-            + "/com/google/example/android_res/R.class");
+            + "/com/google/example/resource_resources.jar!"
+            + "/com/google/example/resource/R.class");
     fileSystem.createFile(
         BLAZE_BIN
-            + "/com/google/example/android_res_resources.jar!"
-            + "/com/google/example/android_res/R$style.class");
+            + "/com/google/example/resource_resources.jar!"
+            + "/com/google/example/resource/R$style.class");
     fileSystem.createFile(
         BLAZE_BIN
-            + "/com/google/unrelated/android_res_resources.jar!"
-            + "/com/google/unrelated/android_res/R$layout.class");
+            + "/com/google/unrelated/resource_resources.jar!"
+            + "/com/google/unrelated/resource/R$layout.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/resource2_resources.jar!"
+            + "/com/google/example/resource2/R.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/transitive/resource_resources.jar!"
+            + "/com/google/example/transitive/R.class");
+    fileSystem.createFile(
+        BLAZE_BIN + "/com/google/example/libjava.jar!" + "/com/google/example/java/Java.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/libtransitive.jar!"
+            + "/com/google/example/transitive/Transitive.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/libshared.jar!"
+            + "/com/google/example/shared/Shared.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/libshared2.jar!"
+            + "/com/google/example/shared2/Shared2.class");
+    fileSystem.createFile(
+        "com/google/example/libimport.jar!" + "/com/google/example/import/Import.class");
+    fileSystem.createFile(
+        "com/google/example/transitive/libimport.jar!"
+            + "/com/google/example/transitive/import/TransitiveImport.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/unrelated/libjava.jar!"
+            + "/com/google/example/unrelated/Java.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/unrelated/libandroid.jar!"
+            + "/com/google/example/unrelated/Android.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/unrelated/libresource.jar!"
+            + "/com/google/example/unrelated/Resource.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/unrelated/resource_resources.jar!"
+            + "/com/google/example/unrelated/R.class");
   }
 
   private TargetMap buildTargetMap() {
     Label mainResourceLibrary = Label.create("//com/google/example:main");
-    Label androidLibraryDependency = Label.create("//com/google/example:android_lib");
-    Label androidResourceDependency = Label.create("//com/google/example:android_res");
-    Label androidResourceDependency2 = Label.create("//com/google/example:android_res2");
-    Label transitiveResourceDependency =
-        Label.create("//com/google/example/transitive:android_res");
+    Label androidDependency = Label.create("//com/google/example:android");
+    Label resourceDependency = Label.create("//com/google/example:resource");
+    Label resourceDependency2 = Label.create("//com/google/example:resource2");
+    Label transitiveResourceDependency = Label.create("//com/google/example/transitive:resource");
     Label javaDependency = Label.create("//com/google/example:java");
-    Label transitiveJavaDependency = Label.create("//com/google/example/transitive:java");
-    Label sharedJavaDependency = Label.create("//com/google/example/shared:java");
-    Label sharedJavaDependency2 = Label.create("//com/google/example/shared2:java");
+    Label transitiveJavaDependency = Label.create("//com/google/example:transitive");
+    Label sharedJavaDependency = Label.create("//com/google/example:shared");
+    Label sharedJavaDependency2 = Label.create("//com/google/example:shared2");
     Label importDependency = Label.create("//com/google/example:import");
     Label transitiveImportDependency = Label.create("//com/google/example/transitive:import");
     Label unrelatedJava = Label.create("//com/google/unrelated:java");
-    Label unrelatedAndroidLibrary = Label.create("//com/google/unrelated:android_lib");
-    Label unrelatedAndroidResource = Label.create("//com/google/unrelated:android_res");
+    Label unrelatedAndroid = Label.create("//com/google/unrelated:android");
+    Label unrelatedResource = Label.create("//com/google/unrelated:resource");
 
     AndroidResourceModuleRegistry registry = new AndroidResourceModuleRegistry();
     registry.put(
@@ -306,18 +413,17 @@
     // Not using these, but they should be in the registry.
     registry.put(
         mock(Module.class),
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(androidResourceDependency)).build());
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(resourceDependency)).build());
     registry.put(
         mock(Module.class),
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(androidResourceDependency2))
-            .build());
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(resourceDependency2)).build());
     registry.put(
         mock(Module.class),
         AndroidResourceModule.builder(TargetKey.forPlainTarget(transitiveResourceDependency))
             .build());
     registry.put(
         mock(Module.class),
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(unrelatedAndroidResource)).build());
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(unrelatedResource)).build());
     registerProjectService(AndroidResourceModuleRegistry.class, registry);
 
     return TargetMapBuilder.builder()
@@ -327,67 +433,67 @@
                 .setKind(Kind.ANDROID_LIBRARY)
                 .setJavaInfo(
                     javaInfoWithJars(
-                        "com/google/example/main.jar", "com/google/example/main_resources.jar"))
+                        "com/google/example/libmain.jar", "com/google/example/main_resources.jar"))
                 .setAndroidInfo(
                     androidInfoWithResourceAndJar(
                         "com.google.example.main",
                         "com/google/example/main/res",
                         "com/google/example/main_resources.jar"))
-                .addDependency(androidLibraryDependency)
-                .addDependency(androidResourceDependency)
-                .addDependency(androidResourceDependency2)
+                .addDependency(androidDependency)
+                .addDependency(resourceDependency)
+                .addDependency(resourceDependency2)
                 .addDependency(javaDependency)
                 .addDependency(importDependency))
         .addTarget(
             TargetIdeInfo.builder()
-                .setLabel(androidLibraryDependency)
+                .setLabel(androidDependency)
                 .setKind(Kind.ANDROID_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/example/android_lib.jar"))
+                .setJavaInfo(javaInfoWithJars("com/google/example/libandroid.jar"))
                 .addDependency(transitiveResourceDependency))
         .addTarget(
             TargetIdeInfo.builder()
-                .setLabel(androidResourceDependency)
+                .setLabel(resourceDependency)
                 .setKind(Kind.ANDROID_LIBRARY)
                 .setJavaInfo(
                     javaInfoWithJars(
-                        "com/google/example/android_res.jar",
-                        "com/google/example/android_res_resources.jar"))
+                        "com/google/example/resource.jar",
+                        "com/google/example/resource_resources.jar"))
                 .setAndroidInfo(
                     androidInfoWithResourceAndJar(
-                        "com.google.example.android_res",
-                        "com/google/example/android_res/res",
-                        "com/google/example/android_res_resources.jar")))
+                        "com.google.example.resource",
+                        "com/google/example/resource/res",
+                        "com/google/example/resource_resources.jar")))
         .addTarget(
             TargetIdeInfo.builder()
-                .setLabel(androidResourceDependency2)
+                .setLabel(resourceDependency2)
                 .setKind(Kind.ANDROID_LIBRARY)
                 .setJavaInfo(
                     javaInfoWithJars(
-                        "com/google/example/android_res2.jar",
-                        "com/google/example/android_res2_resources.jar"))
+                        "com/google/example/resource2.jar",
+                        "com/google/example/resource2_resources.jar"))
                 .setAndroidInfo(
                     androidInfoWithResourceAndJar(
-                        "com.google.example.android_res2",
-                        "com/google/example/android_res2/res",
-                        "com/google/example/android_res2_resources.jar")))
+                        "com.google.example.resource2",
+                        "com/google/example/resource2/res",
+                        "com/google/example/resource2_resources.jar")))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(transitiveResourceDependency)
                 .setKind(Kind.ANDROID_LIBRARY)
                 .setJavaInfo(
                     javaInfoWithJars(
-                        "com/google/example/transitive/android_res.jar",
-                        "com/google/example/transitive/android_res_resources.jar"))
+                        "com/google/example/transitive/resource.jar",
+                        "com/google/example/transitive/resource_resources.jar"))
                 .setAndroidInfo(
                     androidInfoWithResourceAndJar(
-                        "com.google.example.transitive.android_res",
-                        "com/google/example/transitive/android_res/res",
-                        "com/google/example/transitive/android_res_resources.jar")))
+                        "com.google.example.transitive.resource",
+                        "com/google/example/transitive/resource/res",
+                        "com/google/example/transitive/resource_resources.jar")))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(javaDependency)
                 .setKind(Kind.JAVA_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/example/java.jar"))
+                .setJavaInfo(javaInfoWithJars("com/google/example/libjava.jar"))
                 .addDependency(transitiveJavaDependency)
                 .addDependency(sharedJavaDependency)
                 .addDependency(sharedJavaDependency2)
@@ -396,56 +502,56 @@
             TargetIdeInfo.builder()
                 .setLabel(transitiveJavaDependency)
                 .setKind(Kind.JAVA_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/example/transitive/java.jar"))
+                .setJavaInfo(javaInfoWithJars("com/google/example/libtransitive.jar"))
                 .addDependency(sharedJavaDependency)
                 .addDependency(sharedJavaDependency2))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(sharedJavaDependency)
                 .setKind(Kind.JAVA_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/example/shared/java.jar"))
+                .setJavaInfo(javaInfoWithJars("com/google/example/libshared.jar"))
                 .addDependency(sharedJavaDependency2))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(sharedJavaDependency2)
                 .setKind(Kind.JAVA_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/example/shared2/java.jar")))
+                .setJavaInfo(javaInfoWithJars("com/google/example/libshared2.jar")))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(importDependency)
                 .setKind(Kind.JAVA_IMPORT)
-                .setJavaInfo(javaInfoWithCheckedInJars("com/google/example/import.jar")))
+                .setJavaInfo(javaInfoWithCheckedInJars("com/google/example/libimport.jar")))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(transitiveImportDependency)
                 .setKind(Kind.JAVA_IMPORT)
                 .setJavaInfo(
                     javaInfoWithCheckedInJars(
-                        "com/google/example/transitive/import.jar",
-                        "com/google/example/transitive/import2.jar")))
+                        "com/google/example/transitive/libimport.jar",
+                        "com/google/example/transitive/libimport2.jar")))
         .addTarget(
             TargetIdeInfo.builder()
                 .setLabel(unrelatedJava)
                 .setKind(Kind.JAVA_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/unrelated/java.jar")))
+                .setJavaInfo(javaInfoWithJars("com/google/unrelated/libjava.jar")))
         .addTarget(
             TargetIdeInfo.builder()
-                .setLabel(unrelatedAndroidLibrary)
+                .setLabel(unrelatedAndroid)
                 .setKind(Kind.ANDROID_LIBRARY)
-                .setJavaInfo(javaInfoWithJars("com/google/unrelated/android_lib.jar")))
+                .setJavaInfo(javaInfoWithJars("com/google/unrelated/libandroid.jar")))
         .addTarget(
             TargetIdeInfo.builder()
-                .setLabel(unrelatedAndroidResource)
+                .setLabel(unrelatedResource)
                 .setKind(Kind.ANDROID_LIBRARY)
                 .setJavaInfo(
                     javaInfoWithJars(
-                        "com/google/unrelated/android_res.jar",
-                        "com/google/unrelated/android_res_resources.jar"))
+                        "com/google/unrelated/libresource.jar",
+                        "com/google/unrelated/resource_resources.jar"))
                 .setAndroidInfo(
                     androidInfoWithResourceAndJar(
-                        "com.google.unrelated.android_res",
-                        "com/google/unrelated/android_res/res",
-                        "com/google/unrelated/android_res_resources.jar")))
+                        "com.google.unrelated.resource",
+                        "com/google/unrelated/resource/res",
+                        "com/google/unrelated/resource_resources.jar")))
         .build();
   }
 
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
index 2b49d8c..3163f63 100644
--- a/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
@@ -17,6 +17,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.android.tools.idea.rendering.RenderErrorContributor;
 import com.android.tools.idea.rendering.RenderErrorModelFactory;
@@ -59,6 +60,7 @@
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleUtilCore;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.JavaPsiFacade;
@@ -70,7 +72,6 @@
 import com.intellij.psi.search.GlobalSearchScope;
 import com.intellij.psi.search.ProjectScopeBuilder;
 import com.intellij.psi.search.ProjectScopeBuilderImpl;
-import com.intellij.psi.util.PsiUtil.NullPsiClass;
 import java.io.File;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
@@ -96,6 +97,7 @@
     super.initTest(applicationServices, projectServices);
     applicationServices.register(FileTypeManager.class, new MockFileTypeManager());
 
+    projectServices.register(ProjectFileIndex.class, mock(ProjectFileIndex.class));
     projectServices.register(BuildReferenceManager.class, new MockBuildReferenceManager(project));
     projectServices.register(TransitiveDependencyMap.class, new TransitiveDependencyMap(project));
     projectServices.register(ProjectScopeBuilder.class, new ProjectScopeBuilderImpl(project));
@@ -651,15 +653,15 @@
     ImmutableMap<String, PsiClass> classes =
         ImmutableMap.of(
             "com.google.example.independent.LibraryView",
-            new MockPsiClass(psiManager, independentLibraryView),
+            mockPsiClass(independentLibraryView),
             "com.google.example.independent.LibraryView2",
-            new MockPsiClass(psiManager, independentLibraryView2),
+            mockPsiClass(independentLibraryView2),
             "com.google.example.independent.Library2View",
-            new MockPsiClass(psiManager, independentLibrary2View),
+            mockPsiClass(independentLibrary2View),
             "com.google.example.dependent.LibraryView",
-            new MockPsiClass(psiManager, dependentLibraryView),
+            mockPsiClass(dependentLibraryView),
             "com.google.example.ResourceView",
-            new MockPsiClass(psiManager, resourceView));
+            mockPsiClass(resourceView));
 
     ImmutableMap<File, TargetKey> sourceToTarget =
         ImmutableMap.of(
@@ -679,6 +681,14 @@
     projectServices.register(SourceToTargetMap.class, new MockSourceToTargetMap(sourceToTarget));
   }
 
+  private static PsiClass mockPsiClass(VirtualFile virtualFile) {
+    PsiFile psiFile = mock(PsiFile.class);
+    when(psiFile.getVirtualFile()).thenReturn(virtualFile);
+    PsiClass psiClass = mock(PsiClass.class);
+    when(psiClass.getContainingFile()).thenReturn(psiFile);
+    return psiClass;
+  }
+
   private static class MockBlazeProjectDataManager implements BlazeProjectDataManager {
     private BlazeProjectData blazeProjectData;
 
@@ -711,25 +721,6 @@
     }
   }
 
-  private static class MockPsiClass extends NullPsiClass {
-    private PsiFile psiFile;
-
-    public MockPsiClass(PsiManager psiManager, VirtualFile virtualFile) {
-      psiFile =
-          new MockPsiFile(psiManager) {
-            @Override
-            public VirtualFile getVirtualFile() {
-              return virtualFile;
-            }
-          };
-    }
-
-    @Override
-    public PsiFile getContainingFile() {
-      return psiFile;
-    }
-  }
-
   private static class MockJavaPsiFacade extends JavaPsiFacadeImpl {
     private ImmutableMap<String, PsiClass> classes;
 
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
index b48fe02..c9114dd 100644
--- a/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
@@ -21,7 +21,6 @@
 import com.android.tools.idea.run.editor.AndroidJavaDebugger;
 import com.android.tools.idea.run.editor.DeployTargetProvider;
 import com.android.tools.idea.run.editor.ShowChooserTargetProvider;
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxy;
 import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxyProvider;
@@ -68,6 +67,7 @@
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.List;
+import java.util.function.Predicate;
 import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -278,6 +278,11 @@
     }
 
     @Override
+    public String getLanguageSupportDocumentationUrl(String relativeDocName) {
+      return null;
+    }
+
+    @Override
     public boolean isBuildFile(String fileName) {
       return false;
     }
diff --git a/base/BUILD b/base/BUILD
index 7afe714..e51bb7f 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -3,16 +3,20 @@
 java_library(
     name = "base",
     srcs = glob(["src/**/*.java"]),
+    javacopts = ["-Xep:FutureReturnValueIgnored:OFF"],
     resources = glob(["resources/**/*"]),
     visibility = ["//visibility:public"],
     deps = [
         "//common/actionhelper",
         "//common/binaryhelper",
+        "//common/concurrency",
         "//common/experiments",
         "//common/formatter",
+        "//common/guava",
         "//intellij_platform_sdk:plugin_api",
         "//proto:proto_deps",
         "//sdkcompat",
+        "//third_party/auto_value",
         "@jsr305_annotations//jar",
     ],
 )
@@ -90,6 +94,7 @@
         ":unit_test_utils",
         "//common/experiments",
         "//common/experiments:unit_test_utils",
+        "//common/guava",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//proto:proto_deps",
         "@jsr305_annotations//jar",
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index f472193..5755e9e 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -175,8 +175,6 @@
                         serviceImplementation="com.google.idea.blaze.base.io.VirtualFileSystemProviderImpl"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.buildmodifier.BuildFileModifier"
                         serviceImplementation="com.google.idea.blaze.base.lang.buildfile.actions.BuildFileModifierImpl"/>
-    <projectService serviceInterface="com.google.idea.blaze.base.buildmodifier.FileSystemModifier"
-                    serviceImplementation="com.google.idea.blaze.base.buildmodifier.FileSystemModifierImpl"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.run.targetfinder.TargetFinder"
                         serviceImplementation="com.google.idea.blaze.base.run.targetfinder.TargetFinderImpl"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.command.info.BlazeInfoRunner"
@@ -350,11 +348,14 @@
     <extensionPoint qualifiedName="com.google.idea.blaze.AspectStrategyProvider" interface="com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.DistributedExecutorSupport" interface="com.google.idea.blaze.base.run.DistributedExecutorSupport"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.FileStringParser" interface="com.google.idea.blaze.base.run.filter.FileResolver"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestXmlFinderStrategy" interface="com.google.idea.blaze.base.run.testlogs.BlazeTestResultFinderStrategy"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestEventsHandler" interface="com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.AttributeSpecificStringLiteralReferenceProvider" interface="com.google.idea.blaze.base.lang.buildfile.references.AttributeSpecificStringLiteralReferenceProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.EventLogger" interface="com.google.idea.blaze.base.logging.EventLogger"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.ProjectViewDefaultValueProvider" interface="com.google.idea.blaze.base.projectview.section.ProjectViewDefaultValueProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.TestUiSessionProvider" interface="com.google.idea.blaze.base.run.smrunner.TestUiSessionProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeLibrarySorter" interface="com.google.idea.blaze.base.sync.libraries.BlazeLibrarySorter"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BuildSystemVersionChecker" interface="com.google.idea.blaze.base.plugin.BuildSystemVersionChecker"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeIssueParserProvider" interface="com.google.idea.blaze.base.issueparser.BlazeIssueParserProvider"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
@@ -368,17 +369,19 @@
     <BuildSystemProvider implementation="com.google.idea.blaze.base.bazel.BazelBuildSystemProvider" order="last"/>
     <BuildifierBinaryProvider implementation="com.google.idea.blaze.base.buildmodifier.BazelBuildifierBinaryProvider"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider" order="last"/>
-    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TargetNameHeuristic" order="first" id="TargetNameHeuristic"/>
-    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestTargetSourcesHeuristic"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TargetNameHeuristic" id="TargetNameHeuristic"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestTargetSourcesHeuristic" order="after TargetNameHeuristic"/>
     <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestSizeHeuristic" order="last" id="TestSizeHeuristic"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.base.run.BlazeBuildTargetRunConfigurationFactory" order="last"/>
     <AspectStrategyProvider implementation="com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProviderBazel" order="last"/>
     <FileStringParser implementation="com.google.idea.blaze.base.run.filter.StandardFileResolver" order="last"/>
-    <BlazeTestXmlFinderStrategy implementation="com.google.idea.blaze.base.run.testlogs.TargetPathTestResultFinderStrategy"/>
-    <BlazeTestEventsHandler implementation="com.google.idea.blaze.base.run.smrunner.BlazeCompositeTestEventsHandler" order="last"/>
+    <BlazeTestEventsHandler implementation="com.google.idea.blaze.base.run.smrunner.BlazeGenericTestEventsHandler" order="last"/>
     <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.DirectorySection$DirectoriesProjectViewDefaultValueProvider"/>
     <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.TargetSection$TargetsProjectViewDefaultValueProvider"/>
     <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection$AdditionalLanguagesDefaultValueProvider"/>
+    <PrefetchFileSource implementation="com.google.idea.blaze.base.prefetch.ProtoPrefetchFileSource"/>
+    <TestUiSessionProvider implementation="com.google.idea.blaze.base.run.smrunner.BazelTestUiSessionProvider"/>
+    <BuildSystemVersionChecker implementation="com.google.idea.blaze.base.plugin.BazelVersionChecker"/>
   </extensions>
 
 </idea-plugin>
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
index 36a23e9..50abc6d 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
@@ -44,10 +44,14 @@
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
 import java.util.List;
 
 /** Utility to build various collections of targets. */
 public class BlazeBuildService {
+  private static final Key<Long> PROJECT_LAST_BUILD_TIMESTAMP_KEY =
+      Key.create("blaze.project.last.build.timestamp");
+
   public static BlazeBuildService getInstance() {
     return ServiceManager.getService(BlazeBuildService.class);
   }
@@ -86,6 +90,15 @@
             "Make project",
             "Make project completed successfully",
             "Make project failed"));
+
+    // In case the user touched a file, but didn't change its content. The user will get a false
+    // positive for class file out of date. We need a way for the user to suppress the false
+    // message. Clicking the "build project" link should at least make the message go away.
+    project.putUserData(PROJECT_LAST_BUILD_TIMESTAMP_KEY, System.currentTimeMillis());
+  }
+
+  public static Long getLastBuildTimeStamp(Project project) {
+    return project.getUserData(PROJECT_LAST_BUILD_TIMESTAMP_KEY);
   }
 
   @VisibleForTesting
@@ -137,6 +150,7 @@
                         workspaceRoot,
                         projectViewSet,
                         blazeProjectData.blazeVersionData,
+                        blazeProjectData.workspaceLanguageSettings,
                         shardedTargets.shardedTargets);
             FileCaches.refresh(project);
 
diff --git a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java
index 92b0223..b40667c 100644
--- a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java
+++ b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java
@@ -18,6 +18,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.idea.common.concurrency.ConcurrencyUtil;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executors;
 
@@ -25,7 +26,9 @@
 public class BlazeExecutorImpl extends BlazeExecutor {
 
   private final ListeningExecutorService executorService =
-      MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(16));
+      MoreExecutors.listeningDecorator(
+          Executors.newFixedThreadPool(
+              16, ConcurrencyUtil.namedDaemonThreadPoolFactory(BlazeExecutorImpl.class)));
 
   @Override
   public <T> ListenableFuture<T> submit(Callable<T> callable) {
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
index 85296be..4e01815 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -32,6 +32,8 @@
 /** Provides the bazel build system name string. */
 public class BazelBuildSystemProvider implements BuildSystemProvider {
 
+  private static final String BAZEL_DOC_SITE = "https://ij.bazel.build/docs";
+
   @Override
   public BuildSystem buildSystem() {
     return BuildSystem.Bazel;
@@ -64,7 +66,12 @@
 
   @Override
   public String getProjectViewDocumentationUrl() {
-    return "https://ij.bazel.build/docs/project-views.html";
+    return BAZEL_DOC_SITE + "/project-views.html";
+  }
+
+  @Override
+  public String getLanguageSupportDocumentationUrl(String relativeDocName) {
+    return String.format("%s/%s.html", BAZEL_DOC_SITE, relativeDocName);
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
index 19be022..ec82ddb 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
@@ -29,14 +29,14 @@
 public class BazelVersion implements Serializable {
   private static final long serialVersionUID = 1L;
 
-  public static final BazelVersion UNKNOWN = new BazelVersion(0, 0, 0);
+  static final BazelVersion DEVELOPMENT = new BazelVersion(999, 999, 999);
   private static final Pattern PATTERN = Pattern.compile("([[0-9]\\.]+)");
 
-  public final int major;
-  public final int minor;
-  public final int bugfix;
+  final int major;
+  final int minor;
+  final int bugfix;
 
-  BazelVersion(int major, int minor, int bugfix) {
+  public BazelVersion(int major, int minor, int bugfix) {
     this.bugfix = bugfix;
     this.minor = minor;
     this.major = major;
@@ -44,21 +44,22 @@
 
   @VisibleForTesting
   static BazelVersion parseVersion(@Nullable String string) {
+    // treat all unknown / development versions as the very latest version
     if (string == null) {
-      return UNKNOWN;
+      return DEVELOPMENT;
     }
     Matcher matcher = PATTERN.matcher(string);
     if (!matcher.find()) {
-      return UNKNOWN;
+      return DEVELOPMENT;
     }
     try {
       BazelVersion version = parseVersion(matcher.group(1).split("\\."));
       if (version == null) {
-        return UNKNOWN;
+        return DEVELOPMENT;
       }
       return version;
     } catch (Exception e) {
-      return UNKNOWN;
+      return DEVELOPMENT;
     }
   }
 
@@ -76,11 +77,16 @@
     return new BazelVersion(major, minor, bugfix);
   }
 
-  public static BazelVersion parseVersion(BlazeInfo blazeInfo) {
+  static BazelVersion parseVersion(BlazeInfo blazeInfo) {
     return parseVersion(blazeInfo.get(BlazeInfo.RELEASE));
   }
 
   @Override
+  public String toString() {
+    return String.format("%s.%s.%s", major, minor, bugfix);
+  }
+
+  @Override
   public boolean equals(Object o) {
     if (this == o) {
       return true;
@@ -97,6 +103,10 @@
     return Objects.hashCode(major, minor, bugfix);
   }
 
+  public boolean isAtLeast(BazelVersion version) {
+    return isAtLeast(version.major, version.minor, version.bugfix);
+  }
+
   public boolean isAtLeast(int major, int minor, int bugfix) {
     return ComparisonChain.start()
             .compare(this.major, major)
diff --git a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
index 22392d4..214bc96 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
@@ -94,6 +94,15 @@
   @Nullable
   String getProjectViewDocumentationUrl();
 
+  /**
+   * The URL providing documentation for language support, if one can be found.
+   *
+   * @param relativeDocName the path to the language doc, relative to the plugin documentation
+   *     site's base URL, without the webpage's file extension.
+   */
+  @Nullable
+  String getLanguageSupportDocumentationUrl(String relativeDocName);
+
   /** Check if the given filename is a valid BUILD file name. */
   boolean isBuildFile(String fileName);
 
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java b/base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java
index 5875466..52690ec 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java
@@ -15,17 +15,27 @@
  */
 package com.google.idea.blaze.base.buildmodifier;
 
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.intellij.openapi.diagnostic.Logger;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import javax.annotation.Nullable;
 
 /** Formats BUILD files using 'buildifier' */
 public class BuildFileFormatter {
 
+  private static final Logger logger = Logger.getInstance(BuildFileFormatter.class);
+  // 10 seconds ought to be enough for formatting a BUILD file...
+  private static final long TIMEOUT_SECS = 10;
+
   @Nullable
   private static File getBuildifierBinary() {
     for (BuildifierBinaryProvider provider : BuildifierBinaryProvider.EP_NAME.getExtensions()) {
@@ -37,7 +47,29 @@
     return null;
   }
 
-  static String formatText(String text) {
+  /**
+   * Format the BUILD file with a timeout. The tool may be fetched from a network filesystem, and we
+   * don't want to block the UI if there is a network issue.
+   *
+   * @return formatted text, or null if there is an error or timeout
+   */
+  @Nullable
+  static String formatTextWithTimeout(String text) {
+    BlazeExecutor executor = BlazeExecutor.getInstance();
+    ListenableFuture<String> result = executor.submit(() -> formatText(text));
+    try {
+      return result.get(TIMEOUT_SECS, TimeUnit.SECONDS);
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      return null;
+    } catch (ExecutionException | TimeoutException e) {
+      logger.warn(e);
+      return null;
+    }
+  }
+
+  @Nullable
+  private static String formatText(String text) {
     try {
       File buildifierBinary = getBuildifierBinary();
       if (buildifierBinary == null) {
@@ -49,9 +81,9 @@
       file.delete();
       return formattedFile;
     } catch (IOException e) {
-      e.printStackTrace();
+      logger.warn(e);
     }
-    return text;
+    return null;
   }
 
   private static void formatFile(File file, String buildifierBinaryPath) throws IOException {
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
index ca0827d..d00fcd1 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
@@ -101,7 +101,7 @@
       String text, Collection<TextRange> ranges) {
     ImmutableMap.Builder<TextRange, String> output = ImmutableMap.builder();
     for (TextRange range : ranges) {
-      String result = BuildFileFormatter.formatText(range.substring(text));
+      String result = BuildFileFormatter.formatTextWithTimeout(range.substring(text));
       if (result == null) {
         return ImmutableMap.of();
       }
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
index daeae84..0b2e14b 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
@@ -45,7 +45,10 @@
     int lines = document.getLineCount();
     if (lines > 0) {
       String text = document.getText();
-      String formattedText = BuildFileFormatter.formatText(text);
+      String formattedText = BuildFileFormatter.formatTextWithTimeout(text);
+      if (formattedText == null) {
+        return;
+      }
       updateDocument(document, formattedText);
     }
   }
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
deleted file mode 100644
index 96719c1..0000000
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2016 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.idea.blaze.base.buildmodifier;
-
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
-import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
-
-/** Modifies the file system. Interface so we can mock it in tests */
-public abstract class FileSystemModifier {
-
-  public static FileSystemModifier getInstance(@NotNull Project project) {
-    return ServiceManager.getService(project, FileSystemModifier.class);
-  }
-
-  @Nullable
-  public abstract File makeWorkspacePathDirs(@NotNull WorkspacePath workspacePath);
-
-  @Nullable
-  public abstract File createFile(@NotNull WorkspacePath parentDir, @NotNull String name);
-}
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
deleted file mode 100644
index 7e873ee..0000000
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2016 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.idea.blaze.base.buildmodifier;
-
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import java.io.IOException;
-import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
-
-class FileSystemModifierImpl extends FileSystemModifier {
-
-  private final Project project;
-
-  public FileSystemModifierImpl(@NotNull Project project) {
-    this.project = project;
-  }
-
-  @Nullable
-  @Override
-  public File makeWorkspacePathDirs(@NotNull WorkspacePath workspacePath) {
-    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-    File dir = workspaceRoot.fileForPath(workspacePath);
-    boolean success = dir.mkdirs();
-    return success ? dir : null;
-  }
-
-  @Nullable
-  @Override
-  public File createFile(@NotNull WorkspacePath parentDirectory, @NotNull String name) {
-    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-    File dir = workspaceRoot.fileForPath(parentDirectory);
-    File f = new File(dir, name);
-    boolean success;
-    try {
-      success = f.createNewFile();
-    } catch (IOException e) {
-      success = false;
-    }
-    return success ? f : null;
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
index a44b056..6cb3b09 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
@@ -15,12 +15,11 @@
  */
 package com.google.idea.blaze.base.command;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.section.sections.BuildFlagsSection;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.projectview.section.sections.SyncFlagsSection;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.configurations.ParametersList;
 import com.intellij.openapi.project.Project;
@@ -47,39 +46,50 @@
   public static final String DISABLE_TEST_SHARDING = "--test_sharding_strategy=disabled";
   // Filters the unit tests that are run (used with regexp for Java/Robolectric tests).
   public static final String TEST_FILTER = "--test_filter";
-  // When used with mobile-install, deploys the an app incrementally.
-  public static final String INCREMENTAL = "--incremental";
-  // When used with mobile-install, deploys the an app incrementally
-  // can be used for API 23 or higher, for which it is preferred to --incremental
-  public static final String SPLIT_APKS = "--split_apks";
   // Re-run the test even if the results are cached.
   public static final String NO_CACHE_TEST_RESULTS = "--nocache_test_results";
+  public static final String LOCAL_TEST_EXECUTION = "--test_strategy=local";
 
   public static final String EXPERIMENTAL_SHOW_ARTIFACTS = "--experimental_show_artifacts";
 
-  public static List<String> buildFlags(Project project, ProjectViewSet projectViewSet) {
-    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+  public static final String DELETED_PACKAGES = "--deleted_packages";
+
+  public static List<String> blazeFlags(
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeCommandName command,
+      BlazeInvocationContext context) {
     List<String> flags = Lists.newArrayList();
     for (BuildFlagsProvider buildFlagsProvider : BuildFlagsProvider.EP_NAME.getExtensions()) {
-      buildFlagsProvider.addBuildFlags(buildSystem, projectViewSet, flags);
+      buildFlagsProvider.addBuildFlags(project, projectViewSet, command, flags);
     }
     flags.addAll(expandBuildFlags(projectViewSet.listItems(BuildFlagsSection.KEY)));
+    if (context == BlazeInvocationContext.Sync) {
+      flags.addAll(expandBuildFlags(projectViewSet.listItems(SyncFlagsSection.KEY)));
+    }
     return flags;
   }
 
-  // Pass-through arg for sending adb options during mobile-install.
-  public static final String ADB_ARG = "--adb_arg=";
-
-  public static ImmutableList<String> adbSerialFlags(String serial) {
-    return ImmutableList.of(ADB_ARG + "-s ", ADB_ARG + serial);
-  }
+  public static final String ADB_PATH = "--adb_path";
+  public static final String DEVICE = "--device";
 
   // Pass-through arg for sending test arguments.
   public static final String TEST_ARG = "--test_arg=";
 
   private static final String TOOL_TAG = "--tool_tag=ijwb:";
 
+  // TODO: remove these when mobile-install V1 is obsolete
+  // When used with mobile-install, deploys the an app incrementally.
+  public static final String INCREMENTAL = "--incremental";
+  // When used with mobile-install, deploys the an app incrementally
+  // can be used for API 23 or higher, for which it is preferred to --incremental
+  public static final String SPLIT_APKS = "--split_apks";
+  // Pass-through arg for sending adb options during mobile-install.
+  public static final String ADB_ARG = "--adb_arg=";
+  public static final String ADB = "--adb";
+
   // We add this to every single BlazeCommand instance. It's for tracking usage.
+  @VisibleForTesting
   public static String getToolTagFlag() {
     String platformPrefix = PlatformUtils.getPlatformPrefix();
 
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeInvocationContext.java b/base/src/com/google/idea/blaze/base/command/BlazeInvocationContext.java
new file mode 100644
index 0000000..b1517b8
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/BlazeInvocationContext.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.command;
+
+/** The context in which a blaze command is invoked. */
+public enum BlazeInvocationContext {
+  RunConfiguration,
+  Sync,
+}
diff --git a/base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java b/base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java
index 469d663..3d1e133 100644
--- a/base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java
+++ b/base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java
@@ -16,15 +16,17 @@
 package com.google.idea.blaze.base.command;
 
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
 import java.util.List;
 
-/** Provides additional build flags for bazel/blaze commands. */
+/** Provides additional flags for bazel/blaze commands. */
 public interface BuildFlagsProvider {
 
   ExtensionPointName<BuildFlagsProvider> EP_NAME =
       ExtensionPointName.create("com.google.idea.blaze.BuildFlagsProvider");
 
-  void addBuildFlags(BuildSystem buildSystem, ProjectViewSet projectViewSet, List<String> flags);
+  /** Flags to add to blaze/bazel invocations of the given type. */
+  void addBuildFlags(
+      Project project, ProjectViewSet projectViewSet, BlazeCommandName command, List<String> flags);
 }
diff --git a/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java b/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
index fcdfc70..afb4157 100644
--- a/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
+++ b/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.base.command;
 
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.project.Project;
 import java.util.List;
 
 /** Flags added to blaze/bazel build commands. */
@@ -24,7 +24,10 @@
 
   @Override
   public void addBuildFlags(
-      BuildSystem buildSystem, ProjectViewSet projectViewSet, List<String> flags) {
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeCommandName command,
+      List<String> flags) {
     flags.add("--curses=no");
     flags.add("--color=no");
     flags.add("--noexperimental_ui");
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReader.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReader.java
new file mode 100644
index 0000000..0320715
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReader.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.command.buildresult;
+
+import static com.google.idea.common.guava.GuavaHelper.toImmutableList;
+import static com.google.idea.common.guava.GuavaHelper.toImmutableSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResult;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResult.TestStatus;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResults;
+import com.google.idea.blaze.base.util.UrlUtil;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.NamedSetOfFilesId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.OutputGroup;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+
+/** Utility methods for reading Blaze's build event procotol output, in proto form. */
+public final class BuildEventProtocolOutputReader {
+
+  private BuildEventProtocolOutputReader() {}
+
+  /**
+   * Reads all test results from a BEP-formatted {@link InputStream}.
+   *
+   * @throws IOException if the BEP {@link InputStream} is incorrectly formatted
+   */
+  public static BlazeTestResults parseTestResults(InputStream inputStream) throws IOException {
+    Map<String, Kind> labelToTargetKind = new HashMap<>();
+    ImmutableList.Builder<BlazeTestResult> results = ImmutableList.builder();
+    BuildEventStreamProtos.BuildEvent event;
+    while ((event = BuildEventStreamProtos.BuildEvent.parseDelimitedFrom(inputStream)) != null) {
+      switch (event.getId().getIdCase()) {
+        case TARGET_COMPLETED:
+          String label = event.getId().getTargetCompleted().getLabel();
+          Kind kind = parseTargetKind(event.getCompleted().getTargetKind());
+          if (kind != null) {
+            labelToTargetKind.put(label, kind);
+          }
+          continue;
+        case TARGET_CONFIGURED:
+          label = event.getId().getTargetConfigured().getLabel();
+          kind = parseTargetKind(event.getConfigured().getTargetKind());
+          if (kind != null) {
+            labelToTargetKind.put(label, kind);
+          }
+          continue;
+        case TEST_RESULT:
+          label = event.getId().getTestResult().getLabel();
+          results.add(parseTestResult(label, labelToTargetKind.get(label), event.getTestResult()));
+          continue;
+        default: // continue
+      }
+    }
+    return BlazeTestResults.fromFlatList(results.build());
+  }
+
+  /** Convert BEP 'target_kind' to our internal format */
+  @Nullable
+  private static Kind parseTargetKind(String kind) {
+    return kind.endsWith(" rule")
+        ? Kind.fromString(kind.substring(0, kind.length() - " rule".length()))
+        : null;
+  }
+
+  private static BlazeTestResult parseTestResult(
+      String label, @Nullable Kind kind, BuildEventStreamProtos.TestResult testResult) {
+    ImmutableSet<File> files =
+        testResult
+            .getTestActionOutputList()
+            .stream()
+            .map(file -> parseFile(file, path -> path.endsWith(".xml")))
+            .filter(Objects::nonNull)
+            .collect(toImmutableSet());
+    return BlazeTestResult.create(
+        Label.create(label), kind, convertTestStatus(testResult.getStatus()), files);
+  }
+
+  private static TestStatus convertTestStatus(BuildEventStreamProtos.TestStatus protoStatus) {
+    if (protoStatus == BuildEventStreamProtos.TestStatus.UNRECOGNIZED) {
+      // for forward-compatibility
+      return TestStatus.NO_STATUS;
+    }
+    return TestStatus.valueOf(protoStatus.name());
+  }
+
+  /**
+   * Reads all output files listed in the BEP output that satisfy the specified predicate.
+   *
+   * @throws IOException if the BEP output file is incorrectly formatted
+   */
+  static ImmutableList<File> parseAllOutputFilenames(
+      InputStream inputStream, Predicate<String> fileFilter) throws IOException {
+    ImmutableList.Builder<File> files = ImmutableList.builder();
+    BuildEventStreamProtos.BuildEvent event;
+    while ((event = BuildEventStreamProtos.BuildEvent.parseDelimitedFrom(inputStream)) != null) {
+      files.addAll(parseFilenames(event, fileFilter));
+    }
+    return files.build();
+  }
+
+  /**
+   * Reads all artifacts associated with the given target that satisfy the specified predicate.
+   *
+   * @throws IOException if the BEP output file is incorrectly formatted
+   */
+  static ImmutableList<File> parseArtifactsForTarget(
+      InputStream inputStream, Label label, Predicate<String> fileFilter) throws IOException {
+    Map<String, List<BuildEventStreamProtos.File>> fileSets = new HashMap<>();
+    List<String> fileSetsForLabel = new ArrayList<>();
+    BuildEventStreamProtos.BuildEvent event;
+    while ((event = BuildEventStreamProtos.BuildEvent.parseDelimitedFrom(inputStream)) != null) {
+      if (event.getId().hasNamedSet() && event.hasNamedSetOfFiles()) {
+        fileSets.put(
+            event.getId().getNamedSet().getId(), event.getNamedSetOfFiles().getFilesList());
+      } else if (isTargetCompletedEvent(event, label)) {
+        fileSetsForLabel.addAll(getTargetFileSets(event));
+      }
+    }
+    return fileSetsForLabel
+        .stream()
+        .map(fileSets::get)
+        .flatMap(List::stream)
+        .map(file -> parseFile(file, fileFilter))
+        .collect(toImmutableList());
+  }
+
+  private static boolean isTargetCompletedEvent(
+      BuildEventStreamProtos.BuildEvent event, Label label) {
+    return event.getId().hasTargetCompleted()
+        && event.hasCompleted()
+        && label.toString().equals(event.getId().getTargetCompleted().getLabel());
+  }
+
+  /** Returns all file set IDs associated with the given target completed event. */
+  private static ImmutableList<String> getTargetFileSets(BuildEventStreamProtos.BuildEvent event) {
+    if (!event.hasCompleted()) {
+      return ImmutableList.of();
+    }
+    return event
+        .getCompleted()
+        .getOutputGroupList()
+        .stream()
+        .map(OutputGroup::getFileSetsList)
+        .flatMap(List::stream)
+        .map(NamedSetOfFilesId::getId)
+        .collect(toImmutableList());
+  }
+
+  /**
+   * If this is a NamedSetOfFiles event, reads all associated output files. Otherwise returns an
+   * empty list.
+   */
+  private static ImmutableList<File> parseFilenames(
+      BuildEventStreamProtos.BuildEvent event, Predicate<String> fileFilter) {
+    if (!event.hasNamedSetOfFiles()) {
+      return ImmutableList.of();
+    }
+    return event
+        .getNamedSetOfFiles()
+        .getFilesList()
+        .stream()
+        .map(f -> parseFile(f, fileFilter))
+        .filter(Objects::nonNull)
+        .collect(toImmutableList());
+  }
+
+  @Nullable
+  private static File parseFile(BuildEventStreamProtos.File file, Predicate<String> fileFilter) {
+    String filePath = UrlUtil.urlToFilePath(file.getUri());
+    return filePath != null && fileFilter.test(filePath) ? new File(filePath) : null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolUtils.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolUtils.java
new file mode 100644
index 0000000..6cdf791
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.util.UUID;
+
+/**
+ * Utility methods for accessing blaze build event data via the build event protocol (BEP for
+ * short).
+ */
+public final class BuildEventProtocolUtils {
+
+  // Instructs BEP to use local file paths (file://...) rather than objfs blobids.
+  private static final String LOCAL_FILE_PATHS =
+      "--noexperimental_build_event_binary_file_path_conversion";
+
+  private BuildEventProtocolUtils() {}
+
+  /**
+   * Creates a temporary output file to write the BEP data to. Callers are responsible for deleting
+   * this file after use.
+   */
+  public static File createTempOutputFile() {
+    File tempDir = new File(System.getProperty("java.io.tmpdir"));
+    String suffix = UUID.randomUUID().toString();
+    String fileName = "intellij-bep-" + suffix;
+    File tempFile = new File(tempDir, fileName);
+    // Callers should delete this file immediately after use. Add a shutdown hook as well, in case
+    // the application exits before then.
+    tempFile.deleteOnExit();
+    return tempFile;
+  }
+
+  /** Returns a build flag instructing blaze to write build events to the given output file. */
+  public static ImmutableList<String> getBuildFlags(File outputFile) {
+    return ImmutableList.of(
+        "--experimental_build_event_binary_file=" + outputFile.getPath(), LOCAL_FILE_PATHS);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
index 9adf5e2..cce2e00 100644
--- a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
@@ -17,26 +17,25 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
-import com.google.idea.common.experiments.BoolExperiment;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import java.io.Closeable;
 import java.io.File;
 import java.io.OutputStream;
 import java.util.List;
 import java.util.function.Predicate;
 
 /** Assists in getting build artifacts from a build operation. */
-public interface BuildResultHelper {
-  // This experiment does *not* work yet and should remain off
-  BoolExperiment USE_BEP = new BoolExperiment("use.bep", false);
+public interface BuildResultHelper extends Closeable {
 
   /**
    * Constructs a new build result helper.
    *
+   * @param blazeVersion The blaze version used for the build invocation.
    * @param files A filter for the output artifacts you are interested in.
    */
-  static BuildResultHelper forFiles(Predicate<String> files) {
-    return USE_BEP.getValue()
-        ? new BuildResultHelperBep(files)
-        : new BuildResultHelperStderr(files);
+  static BuildResultHelper forFiles(BlazeVersionData blazeVersion, Predicate<String> files) {
+    return new BuildResultHelperBep(files);
   }
 
   /**
@@ -62,4 +61,15 @@
    * @return The build artifacts from the build operation.
    */
   ImmutableList<File> getBuildArtifacts();
+
+  /**
+   * Returns the build artifacts, attempting to filter out all artifacts not directly produced by
+   * the specified target. Some implementations may return artifacts produced by other targets.
+   *
+   * <p>May only be called once the build is complete, or no artifacts will be returned.
+   */
+  ImmutableList<File> getBuildArtifactsForTarget(Label target);
+
+  @Override
+  void close();
 }
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
index 8a32e85..691e08d 100644
--- a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
@@ -18,9 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
-import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent;
-import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
-import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.IdCase;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.openapi.diagnostic.Logger;
 import java.io.BufferedInputStream;
 import java.io.File;
@@ -29,7 +27,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.List;
-import java.util.UUID;
+import java.util.Optional;
 import java.util.function.Predicate;
 
 /**
@@ -39,6 +37,7 @@
  * build events.
  */
 class BuildResultHelperBep implements BuildResultHelper {
+
   private static final Logger logger = Logger.getInstance(BuildResultHelperBep.class);
   private final File outputFile;
   private final Predicate<String> fileFilter;
@@ -46,15 +45,12 @@
 
   BuildResultHelperBep(Predicate<String> fileFilter) {
     this.fileFilter = fileFilter;
-    File tempDir = new File(System.getProperty("java.io.tmpdir"));
-    String suffix = UUID.randomUUID().toString();
-    String fileName = "intellij-bep-" + suffix;
-    this.outputFile = new File(tempDir, fileName);
+    outputFile = BuildEventProtocolUtils.createTempOutputFile();
   }
 
   @Override
   public List<String> getBuildFlags() {
-    return ImmutableList.of("--experimental_build_event_binary_file=" + outputFile.getPath());
+    return BuildEventProtocolUtils.getBuildFlags(outputFile);
   }
 
   @Override
@@ -65,34 +61,47 @@
   @Override
   public ImmutableList<File> getBuildArtifacts() {
     if (result == null) {
-      result = readResult();
+      result =
+          readResult(
+                  input ->
+                      BuildEventProtocolOutputReader.parseAllOutputFilenames(input, fileFilter))
+              .orElse(ImmutableList.of());
     }
     return result;
   }
 
-  private ImmutableList<File> readResult() {
-    ImmutableList.Builder<File> result = ImmutableList.builder();
+  @Override
+  public ImmutableList<File> getBuildArtifactsForTarget(Label target) {
+    if (result == null) {
+      result =
+          readResult(
+                  input ->
+                      BuildEventProtocolOutputReader.parseArtifactsForTarget(
+                          input, target, fileFilter))
+              .orElse(ImmutableList.of());
+    }
+    return result;
+  }
+
+  private <V> Optional<V> readResult(BepReader<V> readAction) {
     try (InputStream inputStream = new BufferedInputStream(new FileInputStream(outputFile))) {
-      BuildEvent buildEvent;
-      while ((buildEvent = BuildEvent.parseDelimitedFrom(inputStream)) != null) {
-        BuildEventId buildEventId = buildEvent.getId();
-        // Note: This doesn't actually work. BEP does not issue these for actions
-        // that don't execute during the build, so we can't find the files
-        // for a no-op build the way we can for --experimental_show_artifacts
-        if (buildEventId.getIdCase() == IdCase.ACTION_COMPLETED) {
-          String output = buildEventId.getActionCompleted().getPrimaryOutput();
-          if (fileFilter.test(output)) {
-            result.add(new File(output));
-          }
-        }
-      }
+      return Optional.of(readAction.read(inputStream));
     } catch (IOException e) {
       logger.error(e);
-      return ImmutableList.of();
+      return Optional.empty();
+    } finally {
+      close();
     }
+  }
+
+  @Override
+  public void close() {
     if (!outputFile.delete()) {
       logger.warn("Could not delete BEP output file: " + outputFile);
     }
-    return result.build();
+  }
+
+  private interface BepReader<V> {
+    V read(InputStream inputStream) throws IOException;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
deleted file mode 100644
index ef34dd6..0000000
--- a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2017 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.idea.blaze.base.command.buildresult;
-
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
-import com.google.idea.blaze.base.command.BlazeFlags;
-import java.io.File;
-import java.io.OutputStream;
-import java.util.List;
-import java.util.function.Predicate;
-
-class BuildResultHelperStderr implements BuildResultHelper {
-  private final ImmutableList.Builder<File> buildArtifacts = ImmutableList.builder();
-  private final ExperimentalShowArtifactsLineProcessor experimentalShowArtifactsLineProcessor;
-  private ImmutableList<File> result;
-
-  BuildResultHelperStderr(Predicate<String> fileFilter) {
-    experimentalShowArtifactsLineProcessor =
-        new ExperimentalShowArtifactsLineProcessor(buildArtifacts, fileFilter);
-  }
-
-  @Override
-  public List<String> getBuildFlags() {
-    return ImmutableList.of(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-  }
-
-  @Override
-  public OutputStream stderr(LineProcessor... lineProcessors) {
-    return LineProcessingOutputStream.of(
-        ImmutableList.<LineProcessor>builder()
-            .add(experimentalShowArtifactsLineProcessor)
-            .add(lineProcessors)
-            .build());
-  }
-
-  @Override
-  public ImmutableList<File> getBuildArtifacts() {
-    if (result == null) {
-      result = buildArtifacts.build();
-    }
-    return result;
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeConfigurationHandler.java b/base/src/com/google/idea/blaze/base/command/info/BlazeConfigurationHandler.java
new file mode 100644
index 0000000..ea99577
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeConfigurationHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.command.info;
+
+import java.io.File;
+import javax.annotation.Nullable;
+
+/**
+ * Encodes the default configuration for a given Blaze sync, and provides configuration-related
+ * helper methods.
+ */
+public class BlazeConfigurationHandler {
+
+  public final String defaultConfigurationPathComponent;
+  private final String blazeOutPath;
+
+  public BlazeConfigurationHandler(BlazeInfo blazeInfo) {
+    // Would be simpler to use 'output_path' instead, but there's a Bazel-side bug causing that to
+    // point to the wrong place. Instead derive 'output_path' from 'blaze-out'.
+    File blazeOutDir = blazeInfo.getBlazeBinDirectory().getParentFile().getParentFile();
+    blazeOutPath = blazeOutDir + File.separator;
+    defaultConfigurationPathComponent =
+        getConfigurationPathComponent(blazeInfo.getBlazeBinDirectory());
+    assert (defaultConfigurationPathComponent != null);
+  }
+
+  @Nullable
+  public String getConfigurationPathComponent(File artifact) {
+    if (!artifact.getPath().startsWith(blazeOutPath)) {
+      return null;
+    }
+    String relativePath = artifact.getPath().substring(blazeOutPath.length());
+    int endIndex = relativePath.indexOf(File.separatorChar);
+    return endIndex == -1 ? relativePath : relativePath.substring(0, endIndex);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
index bf9d694..479f373 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
@@ -30,6 +30,7 @@
   public static final String PACKAGE_PATH_KEY = "package_path";
   public static final String BUILD_LANGUAGE = "build-language";
   public static final String OUTPUT_BASE_KEY = "output_base";
+  public static final String OUTPUT_PATH_KEY = "output_path";
   public static final String MASTER_LOG = "master-log";
   public static final String COMMAND_LOG = "command_log";
   public static final String RELEASE = "release";
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
index 58162c5..84a54f4 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
@@ -37,10 +37,8 @@
 import com.intellij.openapi.wm.ToolWindow;
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
-import java.awt.BorderLayout;
 import javax.annotation.Nullable;
 import javax.swing.JComponent;
-import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
 
 class BlazeConsoleView implements Disposable {
@@ -55,14 +53,12 @@
   @NotNull private final Project myProject;
   @NotNull private final ConsoleViewImpl myConsoleView;
 
-  private JPanel myConsolePanel;
   private volatile Runnable myStopHandler;
 
   public BlazeConsoleView(@NotNull Project project) {
     myProject = project;
     myConsoleView = new ConsoleViewImpl(myProject, false);
     Disposer.register(this, myConsoleView);
-    setupUI();
   }
 
   public static BlazeConsoleView getInstance(@NotNull Project project) {
@@ -106,7 +102,6 @@
     group.add(new StopAction());
 
     JComponent layoutComponent = layoutUi.getComponent();
-    myConsolePanel.add(layoutComponent, BorderLayout.CENTER);
 
     //noinspection ConstantConditions
     Content content =
@@ -126,11 +121,6 @@
   @Override
   public void dispose() {}
 
-  private void setupUI() {
-    myConsolePanel = new JPanel();
-    myConsolePanel.setLayout(new BorderLayout(0, 0));
-  }
-
   private class StopAction extends DumbAwareAction {
     public StopAction() {
       super(IdeBundle.message("action.stop"), null, AllIcons.Actions.Suspend);
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
index 19c2b10..361f0ff 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
@@ -20,7 +20,6 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
-import com.google.idea.blaze.base.buildmodifier.FileSystemModifier;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -29,7 +28,6 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
-import com.google.idea.blaze.base.scope.ScopedOperation;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.output.StatusOutput;
 import com.google.idea.blaze.base.settings.Blaze;
@@ -42,6 +40,8 @@
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.LangDataKeys;
 import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.application.Result;
+import com.intellij.openapi.command.WriteCommandAction;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.DumbAware;
 import com.intellij.openapi.project.Project;
@@ -52,7 +52,9 @@
 import com.intellij.psi.PsiManager;
 import com.intellij.util.PlatformIcons;
 import java.io.File;
+import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
@@ -70,91 +72,81 @@
   protected void actionPerformedInBlazeProject(Project project, AnActionEvent event) {
     final IdeView view = event.getData(LangDataKeys.IDE_VIEW);
     Scope.root(
-        new ScopedOperation() {
-          @Override
-          public void execute(@NotNull final BlazeContext context) {
-            if (view == null || project == null) {
-              return;
-            }
-            PsiDirectory directory = getOrChooseDirectory(project, view);
-
-            if (directory == null) {
-              return;
-            }
-
-            NewBlazePackageDialog newBlazePackageDialog =
-                new NewBlazePackageDialog(project, directory);
-            boolean isOk = newBlazePackageDialog.showAndGet();
-            if (!isOk) {
-              return;
-            }
-
-            final Label newRule = newBlazePackageDialog.getNewRule();
-            final Kind newRuleKind = newBlazePackageDialog.getNewRuleKind();
-            // If we returned OK, we should have a non null result
-            logger.assertTrue(newRule != null);
-            logger.assertTrue(newRuleKind != null);
-
-            context.output(
-                new StatusOutput(
-                    String.format("Setting up a new %s package", Blaze.buildSystemName(project))));
-
-            boolean success = createPackageOnDisk(project, context, newRule, newRuleKind);
-
-            if (!success) {
-              return;
-            }
-
-            File newDirectory =
-                WorkspaceRoot.fromProject(project).fileForPath(newRule.blazePackage());
-            VirtualFile virtualFile = VfsUtil.findFileByIoFile(newDirectory, true);
-            // We just created this file, it should exist
-            logger.assertTrue(virtualFile != null);
-            PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
-            view.selectElement(psiFile);
+        context -> {
+          if (view == null || project == null) {
+            return;
           }
+          PsiDirectory directory = getOrChooseDirectory(project, view);
+
+          if (directory == null) {
+            return;
+          }
+
+          NewBlazePackageDialog newBlazePackageDialog =
+              new NewBlazePackageDialog(project, directory);
+          boolean isOk = newBlazePackageDialog.showAndGet();
+          if (!isOk) {
+            return;
+          }
+
+          final Label newRule = newBlazePackageDialog.getNewRule();
+          final Kind newRuleKind = newBlazePackageDialog.getNewRuleKind();
+          // If we returned OK, we should have a non null result
+          logger.assertTrue(newRule != null);
+          logger.assertTrue(newRuleKind != null);
+
+          context.output(
+              new StatusOutput(
+                  String.format("Setting up a new %s package", Blaze.buildSystemName(project))));
+
+          Optional<VirtualFile> virtualFile =
+              createPackageOnDisk(project, context, newRule, newRuleKind);
+          if (!virtualFile.isPresent()) {
+            return;
+          }
+          PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile.get());
+          view.selectElement(psiFile);
         });
   }
 
-  private static Boolean createPackageOnDisk(
-      @NotNull Project project,
-      @NotNull BlazeContext context,
-      @NotNull Label newRule,
-      @NotNull Kind ruleKind) {
-    LocalHistoryAction action;
+  private Optional<VirtualFile> createPackageOnDisk(
+      Project project, BlazeContext context, Label newRule, Kind ruleKind) {
 
-    String actionName =
+    String commandName =
         String.format(
             "Creating %s package: %s", Blaze.buildSystemName(project), newRule.toString());
-    LocalHistory localHistory = LocalHistory.getInstance();
-    action = localHistory.startAction(actionName);
 
-    // Create the package + BUILD file + rule
-    FileSystemModifier fileSystemModifier = FileSystemModifier.getInstance(project);
-    WorkspacePath newWorkspacePath = newRule.blazePackage();
-    File newDirectory = fileSystemModifier.makeWorkspacePathDirs(newWorkspacePath);
-    if (newDirectory == null) {
-      String errorMessage =
-          "Could not create new package directory: " + newWorkspacePath.toString();
-      context.output(PrintOutput.error(errorMessage));
-      return false;
-    }
-    File buildFile = fileSystemModifier.createFile(newWorkspacePath, BUILD_FILE_NAME);
-    if (buildFile == null) {
-      String errorMessage =
-          "Could not create new BUILD file in package: " + newWorkspacePath.toString();
-      context.output(PrintOutput.error(errorMessage));
-      return false;
-    }
-    BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
-    buildFileModifier.addRule(project, context, newRule, ruleKind);
-    action.finish();
+    return new WriteCommandAction<Optional<VirtualFile>>(project, commandName) {
 
-    return true;
+      @Override
+      protected void run(@NotNull Result<Optional<VirtualFile>> result) throws Throwable {
+        LocalHistory localHistory = LocalHistory.getInstance();
+        LocalHistoryAction action = localHistory.startAction(commandName);
+
+        try {
+          WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+          File dir = workspaceRoot.fileForPath(newRule.blazePackage());
+          try {
+            VirtualFile newDirectory = VfsUtil.createDirectories(dir.getPath());
+            VirtualFile newFile = newDirectory.createChildData(this, BUILD_FILE_NAME);
+            BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
+            buildFileModifier.addRule(project, context, newRule, ruleKind);
+            result.setResult(Optional.of(newFile));
+          } catch (IOException e) {
+            String errorMessage = "Error creating new package: " + e.getMessage();
+            context.output(PrintOutput.error(errorMessage));
+            logger.warn("Error creating new package", e);
+            result.setResult(Optional.empty());
+          }
+        } finally {
+          action.finish();
+        }
+      }
+    }.execute().getResultObject();
   }
 
   @Override
-  protected void updateForBlazeProject(Project project, @NotNull AnActionEvent event) {
+  protected void updateForBlazeProject(Project project, AnActionEvent event) {
     Presentation presentation = event.getPresentation();
     if (isEnabled(event)) {
       String text = String.format("New %s Package", Blaze.buildSystemName(project));
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
index 7317f31..9615339 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
@@ -42,25 +42,24 @@
 import javax.swing.Icon;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
-import org.jetbrains.annotations.NotNull;
 
 class NewBlazePackageDialog extends DialogWrapper {
   private static final Logger logger = Logger.getInstance(NewBlazePackageDialog.class);
 
-  @NotNull private final Project project;
-  @NotNull private final PsiDirectory parentDirectory;
+  private final Project project;
+  private final PsiDirectory parentDirectory;
 
   @Nullable private Label newRule;
   @Nullable private Kind newRuleKind;
 
   private static final int UI_INDENT_LEVEL = 0;
   private static final int TEXT_FIELD_LENGTH = 40;
-  @NotNull private final JPanel component = new JPanel(new GridBagLayout());
-  @NotNull private final JBLabel packageLabel = new JBLabel("Package name:");
-  @NotNull private final JBTextField packageNameField = new JBTextField(TEXT_FIELD_LENGTH);
-  @NotNull private final NewRuleUI newRuleUI = new NewRuleUI(TEXT_FIELD_LENGTH);
+  private final JPanel component = new JPanel(new GridBagLayout());
+  private final JBLabel packageLabel = new JBLabel("Package name:");
+  private final JBTextField packageNameField = new JBTextField(TEXT_FIELD_LENGTH);
+  private final NewRuleUI newRuleUI = new NewRuleUI(TEXT_FIELD_LENGTH);
 
-  public NewBlazePackageDialog(@NotNull Project project, @NotNull PsiDirectory currentDirectory) {
+  public NewBlazePackageDialog(Project project, PsiDirectory currentDirectory) {
     super(project);
     this.project = project;
     this.parentDirectory = currentDirectory;
@@ -72,16 +71,21 @@
     component.add(packageLabel);
     component.add(packageNameField, UiUtil.getFillLineConstraints(UI_INDENT_LEVEL));
     newRuleUI.fillUI(component, UI_INDENT_LEVEL);
+    newRuleUI.syncRuleNameTo(packageNameField);
     UiUtil.fillBottom(component);
     init();
   }
 
-  @Nullable
   @Override
   protected JComponent createCenterPanel() {
     return component;
   }
 
+  @Override
+  public JComponent getPreferredFocusedComponent() {
+    return packageNameField;
+  }
+
   @Nullable
   @Override
   protected ValidationInfo doValidate() {
@@ -126,7 +130,7 @@
     super.doOKAction();
   }
 
-  private void showErrorDialog(@NotNull String message) {
+  private void showErrorDialog(String message) {
     String title = CommonBundle.getErrorTitle();
     Icon icon = Messages.getErrorIcon();
     Messages.showMessageDialog(component, message, title, icon);
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
index ae43540..30c3f37 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
@@ -24,16 +24,20 @@
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.history.LocalHistory;
+import com.intellij.history.LocalHistoryAction;
+import com.intellij.openapi.application.Result;
+import com.intellij.openapi.command.WriteCommandAction;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.DialogWrapper;
 import com.intellij.openapi.ui.ValidationInfo;
 import com.intellij.openapi.vfs.VirtualFile;
-import java.awt.Dimension;
 import java.awt.GridBagLayout;
 import java.io.File;
 import javax.annotation.Nullable;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
+import org.jetbrains.annotations.NotNull;
 
 class NewBlazeRuleDialog extends DialogWrapper {
   private static final int UI_INDENT = 0;
@@ -46,7 +50,6 @@
 
   private JPanel component = new JPanel(new GridBagLayout());
   private final NewRuleUI newRuleUI = new NewRuleUI(TEXT_BOX_WIDTH);
-  private static final Dimension componentSize = new Dimension(500, 500);
 
   public NewBlazeRuleDialog(BlazeContext context, Project project, VirtualFile buildFile) {
     super(project);
@@ -62,9 +65,6 @@
     setOKButtonText("Create");
     setCancelButtonText("Cancel");
 
-    component.setPreferredSize(componentSize);
-    component.setMinimumSize(componentSize);
-
     newRuleUI.fillUI(component, UI_INDENT);
     UiUtil.fillBottom(component);
 
@@ -93,7 +93,22 @@
         workspaceRoot.workspacePathFor(new File(buildFile.getParent().getPath()));
     Label newRule = Label.create(workspacePath, targetName);
     BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
-    boolean success = buildFileModifier.addRule(project, context, newRule, ruleKind);
+
+    String commandName = String.format("Add %s %s rule '%s'", buildSystemName, ruleKind, newRule);
+
+    boolean success =
+        new WriteCommandAction<Boolean>(project, commandName) {
+          @Override
+          protected void run(@NotNull Result<Boolean> result) throws Throwable {
+            LocalHistory localHistory = LocalHistory.getInstance();
+            LocalHistoryAction action = localHistory.startAction(commandName);
+            try {
+              result.setResult(buildFileModifier.addRule(project, context, newRule, ruleKind));
+            } finally {
+              action.finish();
+            }
+          }
+        }.execute().getResultObject();
 
     if (success) {
       super.doOKAction();
diff --git a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
index bf0c26a..a2d95bc 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
@@ -15,54 +15,103 @@
  */
 package com.google.idea.blaze.base.ide;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.TargetName;
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.ide.IdeBundle;
+import com.intellij.ide.util.PropertiesComponent;
 import com.intellij.openapi.ui.ComboBox;
 import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.ui.DocumentAdapter;
 import com.intellij.ui.components.JBLabel;
 import com.intellij.ui.components.JBTextField;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
 import javax.swing.JPanel;
-import org.jetbrains.annotations.NotNull;
+import javax.swing.event.DocumentEvent;
 
 final class NewRuleUI {
 
-  private static final String[] POSSIBLE_RULES = {
-    "android_library", "java_library", "cc_library", "cc_binary", "proto_library"
-  };
+  private static final ImmutableSet<Kind> HANDLED_RULES =
+      ImmutableSet.of(
+          Kind.ANDROID_LIBRARY,
+          Kind.JAVA_LIBRARY,
+          Kind.CC_LIBRARY,
+          Kind.CC_BINARY,
+          Kind.PROTO_LIBRARY);
 
-  @NotNull private final ComboBox ruleComboBox = new ComboBox(POSSIBLE_RULES);
-  @NotNull private final JBLabel ruleNameLabel = new JBLabel("Rule name:");
-  @NotNull private final JBTextField ruleNameField;
+  private static final String LAST_SELECTED_KIND = "Blaze.Rule.Kind";
+
+  private final ComboBox ruleComboBox = new ComboBox(HANDLED_RULES.toArray(new Kind[0]));
+  private final JBLabel ruleNameLabel = new JBLabel("Rule name:");
+  private final JBTextField ruleNameField;
+
+  private boolean ruleNameEditedByUser = false;
 
   public NewRuleUI(int textFieldLength) {
     this.ruleNameField = new JBTextField(textFieldLength);
+    Kind lastValue =
+        Kind.fromString(PropertiesComponent.getInstance().getValue(LAST_SELECTED_KIND));
+    if (HANDLED_RULES.contains(lastValue)) {
+      ruleComboBox.setSelectedItem(lastValue);
+    }
   }
 
-  public void fillUI(@NotNull JPanel component, int indentLevel) {
+  public void fillUI(JPanel component, int indentLevel) {
     component.add(ruleNameLabel);
     component.add(ruleNameField, UiUtil.getFillLineConstraints(indentLevel));
     component.add(ruleComboBox, UiUtil.getFillLineConstraints(indentLevel));
   }
 
-  @NotNull
   public Kind getSelectedRuleKind() {
-    return Kind.fromString((String) ruleComboBox.getSelectedItem());
+    Kind kind = (Kind) ruleComboBox.getSelectedItem();
+    PropertiesComponent.getInstance().setValue(LAST_SELECTED_KIND, kind.toString());
+    return kind;
   }
 
-  @NotNull
   public TargetName getRuleName() {
     return TargetName.create(ruleNameField.getText());
   }
 
+  void syncRuleNameTo(JBTextField textField) {
+    ruleNameField
+        .getDocument()
+        .addDocumentListener(
+            new DocumentAdapter() {
+              @Override
+              protected void textChanged(DocumentEvent e) {
+                ruleNameEditedByUser = true;
+              }
+            });
+
+    textField
+        .getDocument()
+        .addDocumentListener(
+            new DocumentAdapter() {
+              @Override
+              protected void textChanged(DocumentEvent e) {
+                if (!ruleNameEditedByUser) {
+                  syncRuleName(textField.getText());
+                }
+              }
+            });
+  }
+
+  private void syncRuleName(String text) {
+    ruleNameField.setText(text);
+    // setText triggers an event which flips the field, so we'll set it back to false
+    this.ruleNameEditedByUser = false;
+  }
+
   @Nullable
   public ValidationInfo validate() {
+    if (ruleComboBox.getSelectedItem() == null) {
+      return new ValidationInfo("Select a rule type", ruleComboBox);
+    }
     String ruleName = ruleNameField.getText();
     List<BlazeValidationError> errors = Lists.newArrayList();
     if (!validateRuleName(ruleName, errors)) {
@@ -73,7 +122,7 @@
   }
 
   private static boolean validateRuleName(
-      @NotNull String inputString, @Nullable Collection<BlazeValidationError> errors) {
+      String inputString, @Nullable Collection<BlazeValidationError> errors) {
     if (inputString.length() == 0) {
       BlazeValidationError.collect(
           errors, new BlazeValidationError(IdeBundle.message("error.name.should.be.specified")));
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
index 215033d..7a993a5 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
@@ -21,9 +21,11 @@
 
 /** Sister class to {@link JavaIdeInfo} */
 public class CIdeInfo implements Serializable {
-  private static final long serialVersionUID = 7L;
+  private static final long serialVersionUID = 8L;
 
   public final ImmutableList<ArtifactLocation> sources;
+  public final ImmutableList<ArtifactLocation> headers;
+  public final ImmutableList<ArtifactLocation> textualHeaders;
 
   public final ImmutableList<String> localDefines;
   public final ImmutableList<ExecutionRootPath> localIncludeDirectories;
@@ -36,6 +38,8 @@
 
   public CIdeInfo(
       ImmutableList<ArtifactLocation> sources,
+      ImmutableList<ArtifactLocation> headers,
+      ImmutableList<ArtifactLocation> textualHeaders,
       ImmutableList<String> localDefines,
       ImmutableList<ExecutionRootPath> localIncludeDirectories,
       ImmutableList<ExecutionRootPath> transitiveIncludeDirectories,
@@ -43,6 +47,8 @@
       ImmutableList<String> transitiveDefines,
       ImmutableList<ExecutionRootPath> transitiveSystemIncludeDirectories) {
     this.sources = sources;
+    this.headers = headers;
+    this.textualHeaders = textualHeaders;
     this.localDefines = localDefines;
     this.localIncludeDirectories = localIncludeDirectories;
     this.transitiveIncludeDirectories = transitiveIncludeDirectories;
@@ -58,6 +64,8 @@
   /** Builder for c rule info */
   public static class Builder {
     private final ImmutableList.Builder<ArtifactLocation> sources = ImmutableList.builder();
+    private final ImmutableList.Builder<ArtifactLocation> headers = ImmutableList.builder();
+    private final ImmutableList.Builder<ArtifactLocation> textualHeaders = ImmutableList.builder();
 
     private final ImmutableList.Builder<String> localDefines = ImmutableList.builder();
     private final ImmutableList.Builder<ExecutionRootPath> localIncludeDirectories =
@@ -75,6 +83,31 @@
       return this;
     }
 
+    public Builder addSource(ArtifactLocation source) {
+      this.sources.add(source);
+      return this;
+    }
+
+    public Builder addHeaders(Iterable<ArtifactLocation> headers) {
+      this.headers.addAll(headers);
+      return this;
+    }
+
+    public Builder addHeader(ArtifactLocation header) {
+      this.headers.add(header);
+      return this;
+    }
+
+    public Builder addTextualHeaders(Iterable<ArtifactLocation> textualHeaders) {
+      this.textualHeaders.addAll(textualHeaders);
+      return this;
+    }
+
+    public Builder addTextualHeader(ArtifactLocation textualHeader) {
+      this.textualHeaders.add(textualHeader);
+      return this;
+    }
+
     public Builder addLocalDefines(Iterable<String> localDefines) {
       this.localDefines.addAll(localDefines);
       return this;
@@ -111,6 +144,8 @@
     public CIdeInfo build() {
       return new CIdeInfo(
           sources.build(),
+          headers.build(),
+          textualHeaders.build(),
           localDefines.build(),
           localIncludeDirectories.build(),
           transitiveIncludeDirectories.build(),
@@ -127,6 +162,12 @@
         + "  sources="
         + sources
         + "\n"
+        + "  headers="
+        + headers
+        + "\n"
+        + "  textualHeaders="
+        + textualHeaders
+        + "\n"
         + "  localDefines="
         + localDefines
         + "\n"
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/GoIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/GoIdeInfo.java
new file mode 100644
index 0000000..61689f3
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ideinfo/GoIdeInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+import java.io.Serializable;
+
+/** Ide info specific to go rules. */
+public class GoIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableList<ArtifactLocation> generatedSources;
+
+  public GoIdeInfo(ImmutableList<ArtifactLocation> generatedSources) {
+    this.generatedSources = generatedSources;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for go rule info */
+  public static class Builder {
+    private final ImmutableList.Builder<ArtifactLocation> generatedSources =
+        ImmutableList.builder();
+
+    public Builder addGeneratedSources(Iterable<ArtifactLocation> generatedSources) {
+      this.generatedSources.addAll(generatedSources);
+      return this;
+    }
+
+    public GoIdeInfo build() {
+      return new GoIdeInfo(generatedSources.build());
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "GoIdeInfo{" + "\n" + "  generatedSources=" + generatedSources + "\n" + '}';
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java
index b9a9dca..c7faade 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java
@@ -16,17 +16,21 @@
 package com.google.idea.blaze.base.ideinfo;
 
 import java.io.Serializable;
+import javax.annotation.Nullable;
 
 /** Represents the java_toolchain class */
 public class JavaToolchainIdeInfo implements Serializable {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
   public final String sourceVersion;
   public final String targetVersion;
+  @Nullable public final ArtifactLocation javacJar;
 
-  public JavaToolchainIdeInfo(String sourceVersion, String targetVersion) {
+  public JavaToolchainIdeInfo(
+      String sourceVersion, String targetVersion, @Nullable ArtifactLocation javacJar) {
     this.sourceVersion = sourceVersion;
     this.targetVersion = targetVersion;
+    this.javacJar = javacJar;
   }
 
   @Override
@@ -39,6 +43,9 @@
         + "  targetVersion="
         + targetVersion
         + "\n"
+        + "  javacJar="
+        + javacJar
+        + "\n"
         + '}';
   }
 
@@ -50,6 +57,7 @@
   public static class Builder {
     String sourceVersion;
     String targetVersion;
+    ArtifactLocation javacJar;
 
     public Builder setSourceVersion(String sourceVersion) {
       this.sourceVersion = sourceVersion;
@@ -61,8 +69,13 @@
       return this;
     }
 
+    public Builder setJavacJar(ArtifactLocation javacJar) {
+      this.javacJar = javacJar;
+      return this;
+    }
+
     public JavaToolchainIdeInfo build() {
-      return new JavaToolchainIdeInfo(sourceVersion, targetVersion);
+      return new JavaToolchainIdeInfo(sourceVersion, targetVersion, javacJar);
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/JsIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/JsIdeInfo.java
new file mode 100644
index 0000000..b191d22
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ideinfo/JsIdeInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+import java.io.Serializable;
+
+/** Ide info specific to js rules. */
+public class JsIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableList<ArtifactLocation> sources;
+
+  public JsIdeInfo(ImmutableList<ArtifactLocation> sources) {
+    this.sources = sources;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for js rule info */
+  public static class Builder {
+    private final ImmutableList.Builder<ArtifactLocation> sources = ImmutableList.builder();
+
+    public Builder addSources(Iterable<ArtifactLocation> sources) {
+      this.sources.addAll(sources);
+      return this;
+    }
+
+    public JsIdeInfo build() {
+      return new JsIdeInfo(sources.build());
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "JsIdeInfo{" + "\n" + "  sources=" + sources + "\n" + '}';
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
index ecd44c5..5e93271 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.ideinfo;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.ideinfo.Dependency.DependencyType;
 import com.google.idea.blaze.base.model.primitives.Kind;
@@ -41,6 +43,9 @@
   @Nullable public final AndroidIdeInfo androidIdeInfo;
   @Nullable public final AndroidSdkIdeInfo androidSdkIdeInfo;
   @Nullable public final PyIdeInfo pyIdeInfo;
+  @Nullable public final GoIdeInfo goIdeInfo;
+  @Nullable public final JsIdeInfo jsIdeInfo;
+  @Nullable public final TsIdeInfo tsIdeInfo;
   @Nullable public final TestIdeInfo testIdeInfo;
   @Nullable public final ProtoLibraryLegacyInfo protoLibraryLegacyInfo;
   @Nullable public final JavaToolchainIdeInfo javaToolchainIdeInfo;
@@ -58,6 +63,9 @@
       @Nullable AndroidIdeInfo androidIdeInfo,
       @Nullable AndroidSdkIdeInfo androidSdkIdeInfo,
       @Nullable PyIdeInfo pyIdeInfo,
+      @Nullable GoIdeInfo goIdeInfo,
+      @Nullable JsIdeInfo jsIdeInfo,
+      @Nullable TsIdeInfo tsIdeInfo,
       @Nullable TestIdeInfo testIdeInfo,
       @Nullable ProtoLibraryLegacyInfo protoLibraryLegacyInfo,
       @Nullable JavaToolchainIdeInfo javaToolchainIdeInfo) {
@@ -73,6 +81,9 @@
     this.androidIdeInfo = androidIdeInfo;
     this.androidSdkIdeInfo = androidSdkIdeInfo;
     this.pyIdeInfo = pyIdeInfo;
+    this.goIdeInfo = goIdeInfo;
+    this.jsIdeInfo = jsIdeInfo;
+    this.tsIdeInfo = tsIdeInfo;
     this.testIdeInfo = testIdeInfo;
     this.protoLibraryLegacyInfo = protoLibraryLegacyInfo;
     this.javaToolchainIdeInfo = javaToolchainIdeInfo;
@@ -117,6 +128,9 @@
     private JavaIdeInfo javaIdeInfo;
     private AndroidIdeInfo androidIdeInfo;
     private PyIdeInfo pyIdeInfo;
+    private GoIdeInfo goIdeInfo;
+    private JsIdeInfo jsIdeInfo;
+    private TsIdeInfo tsIdeInfo;
     private TestIdeInfo testIdeInfo;
     private ProtoLibraryLegacyInfo protoLibraryLegacyInfo;
     private JavaToolchainIdeInfo javaToolchainIdeInfo;
@@ -135,8 +149,10 @@
       return this;
     }
 
-    public Builder setKind(String kind) {
-      return setKind(Kind.fromString(kind));
+    @VisibleForTesting
+    public Builder setKind(String kindString) {
+      Kind kind = Preconditions.checkNotNull(Kind.fromString(kindString));
+      return setKind(kind);
     }
 
     public Builder setKind(Kind kind) {
@@ -160,6 +176,9 @@
 
     public Builder setCInfo(CIdeInfo cInfo) {
       this.cIdeInfo = cInfo;
+      this.sources.addAll(cInfo.sources);
+      this.sources.addAll(cInfo.headers);
+      this.sources.addAll(cInfo.textualHeaders);
       return this;
     }
 
@@ -190,6 +209,21 @@
       return this;
     }
 
+    public Builder setGoInfo(GoIdeInfo.Builder goInfo) {
+      this.goIdeInfo = goInfo.build();
+      return this;
+    }
+
+    public Builder setJsInfo(JsIdeInfo.Builder jsInfo) {
+      this.jsIdeInfo = jsInfo.build();
+      return this;
+    }
+
+    public Builder setTsInfo(TsIdeInfo.Builder tsInfo) {
+      this.tsIdeInfo = tsInfo.build();
+      return this;
+    }
+
     public Builder setTestInfo(TestIdeInfo.Builder testInfo) {
       this.testIdeInfo = testInfo.build();
       return this;
@@ -245,6 +279,9 @@
           androidIdeInfo,
           null,
           pyIdeInfo,
+          goIdeInfo,
+          jsIdeInfo,
+          tsIdeInfo,
           testIdeInfo,
           protoLibraryLegacyInfo,
           javaToolchainIdeInfo);
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/TsIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/TsIdeInfo.java
new file mode 100644
index 0000000..49e054d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TsIdeInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+import java.io.Serializable;
+
+/** Ide info specific to typescript rules. */
+public class TsIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableList<ArtifactLocation> sources;
+
+  public TsIdeInfo(ImmutableList<ArtifactLocation> sources) {
+    this.sources = sources;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for js rule info */
+  public static class Builder {
+    private final ImmutableList.Builder<ArtifactLocation> sources = ImmutableList.builder();
+
+    public Builder addSources(Iterable<ArtifactLocation> sources) {
+      this.sources.addAll(sources);
+      return this;
+    }
+
+    public TsIdeInfo build() {
+      return new TsIdeInfo(sources.build());
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "TsIdeInfo{" + "\n" + "  sources=" + sources + "\n" + '}';
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
index 55755ec..f743525 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
@@ -147,7 +147,7 @@
     private final WorkspaceRoot workspaceRoot;
 
     CompileParser(WorkspaceRoot workspaceRoot) {
-      super("^([^/].*?):([0-9]+):(?:([0-9]+):)? (error|warning): (.*)$");
+      super("^([^/].*?):([0-9]+):(?:([0-9]+):)? (fatal error|error|warning): (.*)$");
       this.workspaceRoot = workspaceRoot;
     }
 
@@ -155,9 +155,9 @@
     protected IssueOutput createIssue(Matcher matcher) {
       final File file = fileFromRelativePath(workspaceRoot, matcher.group(1));
       IssueOutput.Category type =
-          matcher.group(4).equals("error")
-              ? IssueOutput.Category.ERROR
-              : IssueOutput.Category.WARNING;
+          matcher.group(4).equals("warning")
+              ? IssueOutput.Category.WARNING
+              : IssueOutput.Category.ERROR;
       return IssueOutput.issue(type, matcher.group(5))
           .inFile(file)
           .onLine(Integer.parseInt(matcher.group(2)))
diff --git a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParserProvider.java b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParserProvider.java
new file mode 100644
index 0000000..513dc7e
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParserProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.issueparser;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParser.Parser;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Extension point for providing {@link
+ * com.google.idea.blaze.base.issueparser.BlazeIssueParser.Parser}s.
+ */
+public interface BlazeIssueParserProvider {
+
+  ExtensionPointName<BlazeIssueParserProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BlazeIssueParserProvider");
+
+  static List<Parser> getAllIssueParsers(Project project) {
+    return Arrays.stream(EP_NAME.getExtensions())
+        .map(provider -> provider.getIssueParsers(project))
+        .flatMap(Collection::stream)
+        .collect(Collectors.toList());
+  }
+
+  ImmutableList<BlazeIssueParser.Parser> getIssueParsers(Project project);
+}
diff --git a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
index a5588dc..0b3dc57 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
@@ -25,7 +25,6 @@
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.output.PrintOutput.OutputType;
 import com.intellij.openapi.project.Project;
-import javax.annotation.Nullable;
 
 /**
  * Forwards output to PrintOutputs, colored by whether or not an issue is found per-line.
@@ -39,26 +38,28 @@
   private final BlazeIssueParser blazeIssueParser;
 
   public IssueOutputLineProcessor(
-      @Nullable Project project, BlazeContext context, WorkspaceRoot workspaceRoot) {
+      Project project, BlazeContext context, WorkspaceRoot workspaceRoot) {
     this.context = context;
-    ProjectViewSet projectViewSet =
-        project != null ? ProjectViewManager.getInstance(project).getProjectViewSet() : null;
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
 
     ImmutableList<BlazeIssueParser.Parser> parsers =
-        ImmutableList.of(
-            new BlazeIssueParser.CompileParser(workspaceRoot),
-            new BlazeIssueParser.TracebackParser(),
-            new BlazeIssueParser.BuildParser(),
-            new BlazeIssueParser.SkylarkErrorParser(),
-            new BlazeIssueParser.LinelessBuildParser(),
-            new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
-            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
-                projectViewSet, "no such package '(.*)': BUILD file not found on package path"),
-            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
-                projectViewSet, "no targets found beneath '(.*)'"),
-            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
-                projectViewSet, "ERROR: invalid target format '(.*)'"),
-            new BlazeIssueParser.FileNotFoundBuildParser(workspaceRoot));
+        ImmutableList.<BlazeIssueParser.Parser>builder()
+            .add(
+                new BlazeIssueParser.CompileParser(workspaceRoot),
+                new BlazeIssueParser.TracebackParser(),
+                new BlazeIssueParser.BuildParser(),
+                new BlazeIssueParser.SkylarkErrorParser(),
+                new BlazeIssueParser.LinelessBuildParser(),
+                new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
+                new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                    projectViewSet, "no such package '(.*)': BUILD file not found on package path"),
+                new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                    projectViewSet, "no targets found beneath '(.*)'"),
+                new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                    projectViewSet, "ERROR: invalid target format '(.*)'"),
+                new BlazeIssueParser.FileNotFoundBuildParser(workspaceRoot))
+            .addAll(BlazeIssueParserProvider.getAllIssueParsers(project))
+            .build();
     this.blazeIssueParser = new BlazeIssueParser(parsers);
   }
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java
index dd9178c..a8dc5b5 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java
@@ -26,10 +26,8 @@
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.scope.BlazeContext;
-import com.intellij.openapi.command.WriteCommandAction;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Computable;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.psi.PsiElement;
 import java.io.File;
@@ -41,24 +39,19 @@
 
   @Override
   public boolean addRule(Project project, BlazeContext context, Label newRule, Kind ruleKind) {
-    return WriteCommandAction.runWriteCommandAction(
-        project,
-        (Computable<Boolean>)
-            () -> {
-              BuildReferenceManager manager = BuildReferenceManager.getInstance(project);
-              File file = manager.resolvePackage(newRule.blazePackage());
-              if (file == null) {
-                return null;
-              }
-              LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file));
-              BuildFile buildFile = manager.resolveBlazePackage(newRule.blazePackage());
-              if (buildFile == null) {
-                logger.error("No BUILD file found at location: " + newRule.blazePackage());
-                return false;
-              }
-              buildFile.add(createRule(project, ruleKind, newRule.targetName().toString()));
-              return true;
-            });
+    BuildReferenceManager manager = BuildReferenceManager.getInstance(project);
+    File file = manager.resolvePackage(newRule.blazePackage());
+    if (file == null) {
+      return false;
+    }
+    LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file));
+    BuildFile buildFile = manager.resolveBlazePackage(newRule.blazePackage());
+    if (buildFile == null) {
+      logger.error("No BUILD file found at location: " + newRule.blazePackage());
+      return false;
+    }
+    buildFile.add(createRule(project, ruleKind, newRule.targetName().toString()));
+    return true;
   }
 
   private PsiElement createRule(Project project, Kind ruleKind, String ruleName) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java
index 84d16cb..a085253 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java
@@ -21,11 +21,13 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.usages.Usage;
 import com.intellij.usages.UsageGroup;
+import com.intellij.usages.UsageTarget;
 import com.intellij.usages.UsageView;
 import com.intellij.usages.impl.FileStructureGroupRuleProvider;
 import com.intellij.usages.impl.rules.FileGroupingRule;
 import com.intellij.usages.rules.UsageGroupingRule;
 import com.intellij.usages.rules.UsageInFile;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
 
 /**
@@ -52,8 +54,9 @@
       this.project = project;
     }
 
-    @Override
-    public UsageGroup groupUsage(Usage usage) {
+    @SuppressWarnings("MissingOverride") // #api171: added in 2017.2
+    @Nullable
+    public UsageGroup getParentGroupFor(Usage usage, UsageTarget[] targets) {
       if (!(usage instanceof UsageInFile)) {
         return null;
       }
@@ -88,5 +91,10 @@
         }
       };
     }
+
+    @Override
+    public UsageGroup groupUsage(Usage usage) {
+      return getParentGroupFor(usage, UsageTarget.EMPTY_ARRAY);
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
index 7cfd64b..58bb3a1 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
@@ -32,6 +32,12 @@
 
   String getPresentableText();
 
+  /** See {@link com.intellij.navigation.ItemPresentation#getLocationString}. */
+  @Nullable
+  default String getLocationString() {
+    return null;
+  }
+
   @Nullable
   PsiElement getReferencedElement();
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
index 700f4dc..d6270b9 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
@@ -147,7 +147,7 @@
 
       @Override
       public String getLocationString() {
-        return null;
+        return element.getLocationString();
       }
 
       @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
index 46701da..41f68fc 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.FuncallReference;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.lang.ASTNode;
 import com.intellij.openapi.util.TextRange;
@@ -53,6 +54,12 @@
     return node != null ? node.getText() : null;
   }
 
+  @Nullable
+  public Kind getRuleKind() {
+    String functionName = getFunctionName();
+    return functionName != null ? Kind.fromString(functionName) : null;
+  }
+
   @Override
   @Nullable
   public String getName() {
@@ -161,12 +168,15 @@
 
   @Override
   public String getPresentableText() {
-    String name = getFunctionName();
-    if (name == null) {
+    String functionName = getFunctionName();
+    if (functionName == null) {
       return super.getPresentableText();
     }
     String targetName = getNameArgumentValue();
-    return targetName != null ? name + "(\"" + targetName + "\")" : name;
+    if (targetName == null) {
+      return functionName;
+    }
+    return String.format("%s : %s", targetName, functionName);
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java
index 31dd222..3cd68f8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java
@@ -86,7 +86,12 @@
 
   @Override
   public String getPresentableText() {
-    String path = LabelUtils.getNiceSkylarkFileName(getImportedPath());
-    return path != null ? "load: " + path : "load";
+    return "load";
+  }
+
+  @Nullable
+  @Override
+  public String getLocationString() {
+    return LabelUtils.getNiceSkylarkFileName(getImportedPath());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java
index 521f432..51d8750 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java
@@ -65,7 +65,7 @@
 
   @Nullable
   private static BuildFile resolveProjectWorkspaceFile(Project project) {
-    WorkspaceRoot projectRoot = WorkspaceRoot.fromProject(project);
+    WorkspaceRoot projectRoot = WorkspaceRoot.fromProjectSafe(project);
     if (projectRoot == null) {
       return null;
     }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
index 4a0f12b..526a8cb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
@@ -16,7 +16,9 @@
 package com.google.idea.blaze.base.lang.buildfile.sync;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.BlazeInvocationContext;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
@@ -102,15 +104,14 @@
       ProjectViewSet projectViewSet,
       BlazeContext context) {
     try {
-      // it's wasteful converting to a string and back, but uses existing code,
-      // and has a very minor cost (this is only run once per workspace)
       ListenableFuture<byte[]> future =
           BlazeInfoRunner.getInstance()
               .runBlazeInfoGetBytes(
                   context,
                   Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
                   workspace,
-                  BlazeFlags.buildFlags(project, projectViewSet),
+                  BlazeFlags.blazeFlags(
+                      project, projectViewSet, BlazeCommandName.INFO, BlazeInvocationContext.Sync),
                   BlazeInfo.BUILD_LANGUAGE);
 
       return BuildLanguageSpec.fromProto(Build.BuildLanguage.parseFrom(future.get()));
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
index 7ae6217..6800f53 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
@@ -53,4 +53,9 @@
   public String getPresentableText() {
     return element.getPresentableText();
   }
+
+  @Override
+  public String getLocationString() {
+    return element.getLocationString();
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
index ded37ad..3356e8b 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.model;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.idea.blaze.base.bazel.BazelVersion;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
@@ -35,11 +34,6 @@
   @Nullable private final Long clientCl;
   @Nullable private final BazelVersion bazelVersion;
 
-  @VisibleForTesting
-  public BlazeVersionData() {
-    this(null, null, null);
-  }
-
   private BlazeVersionData(
       @Nullable Long blazeCl, @Nullable Long clientCl, @Nullable BazelVersion bazelVersion) {
     this.blazeCl = blazeCl;
@@ -51,6 +45,10 @@
     return blazeCl != null && blazeCl >= cl;
   }
 
+  public boolean blazeClientIsKnown() {
+    return clientCl != null;
+  }
+
   public boolean blazeClientIsAtLeastCl(long cl) {
     return clientCl != null && clientCl >= cl;
   }
@@ -59,24 +57,40 @@
     return bazelVersion != null && bazelVersion.isAtLeast(major, minor, bugfix);
   }
 
+  public boolean bazelIsAtLeastVersion(BazelVersion version) {
+    return bazelVersion != null && bazelVersion.isAtLeast(version);
+  }
+
   public BuildSystem buildSystem() {
     return bazelVersion != null ? BuildSystem.Bazel : BuildSystem.Blaze;
   }
 
+  @Override
+  public String toString() {
+    if (bazelVersion != null) {
+      return bazelVersion.toString();
+    }
+    return String.format("Blaze CL: %s, Client CL: %s", blazeCl, clientCl);
+  }
+
   public static BlazeVersionData build(
       BuildSystem buildSystem, WorkspaceRoot workspaceRoot, BlazeInfo blazeInfo) {
-    Builder builder = new Builder();
+    Builder builder = builder();
     for (BuildSystemProvider provider : BuildSystemProvider.EP_NAME.getExtensions()) {
       provider.populateBlazeVersionData(buildSystem, workspaceRoot, blazeInfo, builder);
     }
     return builder.build();
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   /** Builder class for constructing the blaze version data */
   public static class Builder {
-    public Long blazeCl;
-    public Long clientCl;
-    public BazelVersion bazelVersion;
+    private Long blazeCl;
+    private Long clientCl;
+    private BazelVersion bazelVersion;
 
     public Builder setBlazeCl(Long blazeCl) {
       this.blazeCl = blazeCl;
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
index 5e060b7..e299cbd 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
@@ -90,7 +90,9 @@
     if (!isAncestor(root.getPath(), path.getPath(), /* strict */ false)) {
       return null;
     }
-    String relativePath = FileUtil.getRelativePath(root, path);
+    String relativePath =
+        FileUtil.getRelativePath(
+            root.getAbsolutePath(), path.getAbsolutePath(), File.separatorChar);
     if (relativePath == null) {
       return null;
     }
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/Kind.java b/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
index b5fbda8..7a041b8 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
@@ -20,50 +20,69 @@
 import com.google.common.collect.ImmutableMultimap;
 import java.util.Arrays;
 import java.util.Collection;
+import javax.annotation.Nullable;
 
 /** Wrapper around a string for a blaze kind (android_library, android_test...) */
 public enum Kind {
-  ANDROID_BINARY("android_binary", LanguageClass.ANDROID),
-  ANDROID_LIBRARY("android_library", LanguageClass.ANDROID),
-  ANDROID_TEST("android_test", LanguageClass.ANDROID),
-  ANDROID_ROBOLECTRIC_TEST("android_robolectric_test", LanguageClass.ANDROID),
-  ANDROID_SDK("android_sdk", LanguageClass.ANDROID),
-  JAVA_LIBRARY("java_library", LanguageClass.JAVA),
-  JAVA_TEST("java_test", LanguageClass.JAVA),
-  JAVA_BINARY("java_binary", LanguageClass.JAVA),
-  JAVA_IMPORT("java_import", LanguageClass.JAVA),
-  JAVA_TOOLCHAIN("java_toolchain", LanguageClass.JAVA),
-  PROTO_LIBRARY("proto_library", LanguageClass.GENERIC),
-  JAVA_PLUGIN("java_plugin", LanguageClass.JAVA),
-  ANDROID_RESOURCES("android_resources", LanguageClass.ANDROID),
-  CC_LIBRARY("cc_library", LanguageClass.C),
-  CC_BINARY("cc_binary", LanguageClass.C),
-  CC_TEST("cc_test", LanguageClass.C),
-  CC_INC_LIBRARY("cc_inc_library", LanguageClass.C),
-  CC_TOOLCHAIN("cc_toolchain", LanguageClass.C),
-  JAVA_WRAP_CC("java_wrap_cc", LanguageClass.JAVA),
-  GWT_APPLICATION("gwt_application", LanguageClass.JAVA),
-  GWT_HOST("gwt_host", LanguageClass.JAVA),
-  GWT_MODULE("gwt_module", LanguageClass.JAVA),
-  GWT_TEST("gwt_test", LanguageClass.JAVA),
-  TEST_SUITE("test_suite", LanguageClass.GENERIC),
-  PY_LIBRARY("py_library", LanguageClass.PYTHON),
-  PY_BINARY("py_binary", LanguageClass.PYTHON),
-  PY_TEST("py_test", LanguageClass.PYTHON),
-  PY_APPENGINE_BINARY("py_appengine_binary", LanguageClass.PYTHON),
-  PY_WRAP_CC("py_wrap_cc", LanguageClass.PYTHON),
-  GO_TEST("go_test", LanguageClass.GO),
-  GO_APPENGINE_TEST("go_appengine_test", LanguageClass.GO),
-  GO_BINARY("go_binary", LanguageClass.GO),
-  GO_APPENGINE_BINARY("go_appengine_binary", LanguageClass.GO),
-  GO_LIBRARY("go_library", LanguageClass.GO),
-  GO_APPENGINE_LIBRARY("go_appengine_library", LanguageClass.GO),
-  GO_WRAP_CC("go_wrap_cc", LanguageClass.GO),
-  INTELLIJ_PLUGIN_DEBUG_TARGET("intellij_plugin_debug_target", LanguageClass.JAVA),
-  SCALA_BINARY("scala_binary", LanguageClass.SCALA),
-  SCALA_LIBRARY("scala_library", LanguageClass.SCALA),
-  SCALA_MACRO_LIBRARY("scala_macro_library", LanguageClass.SCALA),
-  SCALA_TEST("scala_test", LanguageClass.SCALA),
+  ANDROID_BINARY("android_binary", LanguageClass.ANDROID, RuleType.BINARY),
+  ANDROID_LIBRARY("android_library", LanguageClass.ANDROID, RuleType.UNKNOWN),
+  ANDROID_TEST("android_test", LanguageClass.ANDROID, RuleType.TEST),
+  ANDROID_ROBOLECTRIC_TEST("android_robolectric_test", LanguageClass.ANDROID, RuleType.TEST),
+  ANDROID_SDK("android_sdk", LanguageClass.ANDROID, RuleType.UNKNOWN),
+  JAVA_LIBRARY("java_library", LanguageClass.JAVA, RuleType.UNKNOWN),
+  JAVA_TEST("java_test", LanguageClass.JAVA, RuleType.TEST),
+  JAVA_BINARY("java_binary", LanguageClass.JAVA, RuleType.BINARY),
+  JAVA_IMPORT("java_import", LanguageClass.JAVA, RuleType.UNKNOWN),
+  JAVA_TOOLCHAIN("java_toolchain", LanguageClass.JAVA, RuleType.UNKNOWN),
+  JAVA_PROTO_LIBRARY("java_proto_library", LanguageClass.JAVA, RuleType.UNKNOWN),
+  JAVA_PLUGIN("java_plugin", LanguageClass.JAVA, RuleType.UNKNOWN),
+  PROTO_LIBRARY("proto_library", LanguageClass.GENERIC, RuleType.UNKNOWN),
+  ANDROID_RESOURCES("android_resources", LanguageClass.ANDROID, RuleType.UNKNOWN),
+  CC_LIBRARY("cc_library", LanguageClass.C, RuleType.UNKNOWN),
+  CC_BINARY("cc_binary", LanguageClass.C, RuleType.BINARY),
+  CC_TEST("cc_test", LanguageClass.C, RuleType.TEST),
+  CC_INC_LIBRARY("cc_inc_library", LanguageClass.C, RuleType.UNKNOWN),
+  CC_TOOLCHAIN("cc_toolchain", LanguageClass.C, RuleType.UNKNOWN),
+  JAVA_WRAP_CC("java_wrap_cc", LanguageClass.JAVA, RuleType.UNKNOWN),
+  GWT_APPLICATION("gwt_application", LanguageClass.JAVA, RuleType.UNKNOWN),
+  GWT_HOST("gwt_host", LanguageClass.JAVA, RuleType.UNKNOWN),
+  GWT_MODULE("gwt_module", LanguageClass.JAVA, RuleType.UNKNOWN),
+  GWT_TEST("gwt_test", LanguageClass.JAVA, RuleType.TEST),
+  TEST_SUITE("test_suite", LanguageClass.GENERIC, RuleType.TEST),
+  PY_LIBRARY("py_library", LanguageClass.PYTHON, RuleType.UNKNOWN),
+  PY_BINARY("py_binary", LanguageClass.PYTHON, RuleType.BINARY),
+  PY_TEST("py_test", LanguageClass.PYTHON, RuleType.TEST),
+  PY_APPENGINE_BINARY("py_appengine_binary", LanguageClass.PYTHON, RuleType.BINARY),
+  PY_WRAP_CC("py_wrap_cc", LanguageClass.PYTHON, RuleType.UNKNOWN),
+  GO_TEST("go_test", LanguageClass.GO, RuleType.TEST),
+  GO_APPENGINE_TEST("go_appengine_test", LanguageClass.GO, RuleType.TEST),
+  GO_BINARY("go_binary", LanguageClass.GO, RuleType.BINARY),
+  GO_APPENGINE_BINARY("go_appengine_binary", LanguageClass.GO, RuleType.BINARY),
+  GO_LIBRARY("go_library", LanguageClass.GO, RuleType.UNKNOWN),
+  GO_APPENGINE_LIBRARY("go_appengine_library", LanguageClass.GO, RuleType.UNKNOWN),
+  GO_PROTO_LIBRARY("go_proto_library", LanguageClass.GO, RuleType.UNKNOWN),
+  GO_WRAP_CC("go_wrap_cc", LanguageClass.GO, RuleType.UNKNOWN),
+  INTELLIJ_PLUGIN_DEBUG_TARGET(
+      "intellij_plugin_debug_target", LanguageClass.JAVA, RuleType.UNKNOWN),
+  SCALA_BINARY("scala_binary", LanguageClass.SCALA, RuleType.BINARY),
+  SCALA_IMPORT("scala_import", LanguageClass.SCALA, RuleType.UNKNOWN),
+  SCALA_LIBRARY("scala_library", LanguageClass.SCALA, RuleType.UNKNOWN),
+  SCALA_MACRO_LIBRARY("scala_macro_library", LanguageClass.SCALA, RuleType.UNKNOWN),
+  SCALA_TEST("scala_test", LanguageClass.SCALA, RuleType.TEST),
+  SCALA_JUNIT_TEST("scala_junit_test", LanguageClass.SCALA, RuleType.TEST),
+  SH_TEST("sh_test", LanguageClass.GENERIC, RuleType.TEST),
+  SH_LIBRARY("sh_library", LanguageClass.GENERIC, RuleType.UNKNOWN),
+  SH_BINARY("sh_binary", LanguageClass.GENERIC, RuleType.BINARY),
+  JS_BINARY("js_binary", LanguageClass.JAVASCRIPT, RuleType.BINARY),
+  JS_MODULE_BINARY("js_module_binary", LanguageClass.JAVASCRIPT, RuleType.BINARY),
+  JS_LIBRARY("js_library", LanguageClass.JAVASCRIPT, RuleType.UNKNOWN),
+  JS_UNIT_TEST("jsunit_test", LanguageClass.JAVASCRIPT, RuleType.TEST),
+  JS_PUPPET_TEST("js_puppet_test", LanguageClass.JAVASCRIPT, RuleType.TEST),
+  PINTO_LIBRARY("pinto_library", LanguageClass.JAVASCRIPT, RuleType.UNKNOWN),
+  PINTO_LIBRARY_MOD("pinto_library_mod", LanguageClass.JAVASCRIPT, RuleType.UNKNOWN),
+  PINTO_MODULE("pinto_module", LanguageClass.JAVASCRIPT, RuleType.UNKNOWN),
+  TS_LIBRARY("ts_library", LanguageClass.TYPESCRIPT, RuleType.UNKNOWN),
+  TS_CONFIG("ts_config", LanguageClass.TYPESCRIPT, RuleType.BINARY),
   ;
 
   static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
@@ -86,6 +105,7 @@
     return result.build();
   }
 
+  @Nullable
   public static Kind fromString(String kindString) {
     return STRING_TO_KIND.get(kindString);
   }
@@ -95,11 +115,13 @@
   }
 
   private final String kind;
-  private final LanguageClass languageClass;
+  public final LanguageClass languageClass;
+  public final RuleType ruleType;
 
-  Kind(String kind, LanguageClass languageClass) {
+  Kind(String kind, LanguageClass languageClass, RuleType ruleType) {
     this.kind = kind;
     this.languageClass = languageClass;
+    this.ruleType = ruleType;
   }
 
   @Override
@@ -107,10 +129,6 @@
     return kind;
   }
 
-  public LanguageClass getLanguageClass() {
-    return languageClass;
-  }
-
   public boolean isOneOf(Kind... kinds) {
     return isOneOf(Arrays.asList(kinds));
   }
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/RuleType.java b/base/src/com/google/idea/blaze/base/model/primitives/RuleType.java
new file mode 100644
index 0000000..97e950c
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/model/primitives/RuleType.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.model.primitives;
+
+/** The general type of a rule (e.g. test, binary, etc.). */
+public enum RuleType {
+  TEST,
+  BINARY,
+  UNKNOWN,
+}
diff --git a/base/src/com/google/idea/blaze/base/plugin/BazelVersionChecker.java b/base/src/com/google/idea/blaze/base/plugin/BazelVersionChecker.java
new file mode 100644
index 0000000..78e146b
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/plugin/BazelVersionChecker.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.plugin;
+
+import com.google.idea.blaze.base.bazel.BazelVersion;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+/** Verifies that the available Bazel version is supported by this plugin. */
+public class BazelVersionChecker implements BuildSystemVersionChecker {
+
+  // prior to version 0.5 there was no BEP support
+  private static final BazelVersion OLDEST_SUPPORTED_VERSION = new BazelVersion(0, 5, 0);
+
+  @Override
+  public boolean versionSupported(BlazeContext context, BlazeVersionData version) {
+    if (version.buildSystem() != BuildSystem.Bazel) {
+      return true;
+    }
+    if (version.bazelIsAtLeastVersion(OLDEST_SUPPORTED_VERSION)) {
+      return true;
+    }
+    IssueOutput.error(
+            String.format(
+                "Bazel version %s is not supported by this version of the Bazel plugin. "
+                    + "Please upgrade to Bazel version %s+.\n"
+                    + "Upgrade instructions are available at https://bazel.build",
+                version, OLDEST_SUPPORTED_VERSION))
+        .submit(context);
+    return false;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/plugin/BuildSystemVersionChecker.java b/base/src/com/google/idea/blaze/base/plugin/BuildSystemVersionChecker.java
new file mode 100644
index 0000000..6667b91
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/plugin/BuildSystemVersionChecker.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.plugin;
+
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+/**
+ * Verifies that the available Blaze version is supported by this plugin.
+ *
+ * <p>Notifies the user if they're using an unsupported version of Blaze.
+ */
+public interface BuildSystemVersionChecker {
+
+  ExtensionPointName<BuildSystemVersionChecker> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BuildSystemVersionChecker");
+
+  static boolean verifyVersionSupported(BlazeContext context, BlazeVersionData version) {
+    for (BuildSystemVersionChecker checker : EP_NAME.getExtensions()) {
+      if (!checker.versionSupported(context, version)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Returns false if the blaze version is unsupported.
+   *
+   * <p>Also displays corresponding errors via the {@link BlazeContext}.
+   */
+  boolean versionSupported(BlazeContext context, BlazeVersionData version);
+}
diff --git a/base/src/com/google/idea/blaze/base/prefetch/FetchExecutor.java b/base/src/com/google/idea/blaze/base/prefetch/FetchExecutor.java
index c89cc4f..5011172 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/FetchExecutor.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/FetchExecutor.java
@@ -17,6 +17,7 @@
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.idea.common.concurrency.ConcurrencyUtil;
 import com.intellij.util.concurrency.BoundedTaskExecutor;
 import java.util.concurrent.Executors;
 
@@ -25,5 +26,10 @@
   private static final int THREAD_COUNT = 32;
   public static final ListeningExecutorService EXECUTOR =
       MoreExecutors.listeningDecorator(
-          new BoundedTaskExecutor(Executors.newFixedThreadPool(THREAD_COUNT), THREAD_COUNT));
+          new BoundedTaskExecutor(
+              // #api171 add this argument, the form without a name is deprecated
+              // FetchExecutor.class.getSimpleName(),
+              Executors.newFixedThreadPool(
+                  THREAD_COUNT, ConcurrencyUtil.namedDaemonThreadPoolFactory(FetchExecutor.class)),
+              THREAD_COUNT));
 }
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
index 26c7491..1b6a3c2 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
@@ -15,25 +15,41 @@
  */
 package com.google.idea.blaze.base.prefetch;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.Collection;
 import java.util.Set;
 
 /** Provides a source of files to prefetch */
 public interface PrefetchFileSource {
+
   ExtensionPointName<PrefetchFileSource> EP_NAME =
       ExtensionPointName.create("com.google.idea.blaze.PrefetchFileSource");
-  /** Adds any files or directories that we would be interested in prefetching. */
+
+  /** Returns all file extensions provided by available PrefetchFileSource implementations. */
+  static ImmutableSet<String> getAllPrefetchFileExtensions() {
+    ImmutableSet.Builder<String> extensionsToFetchContent = ImmutableSet.builder();
+    for (PrefetchFileSource fileSource : PrefetchFileSource.EP_NAME.getExtensions()) {
+      extensionsToFetchContent.addAll(fileSource.prefetchFileExtensions());
+    }
+    return extensionsToFetchContent.build();
+  }
+
+  /**
+   * Adds any files or directories that we would be interested in prefetching. Project source files
+   * should not be added here, as they're always prefetched.
+   */
   void addFilesToPrefetch(
       Project project,
       ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
       BlazeProjectData blazeProjectData,
-      Collection<File> files);
+      Set<File> files);
 
-  /** Returns any source file extensions that are a good candidate for the {@link Prefetcher}. */
-  Set<String> prefetchSrcFileExtensions();
+  /** Returns any file extensions that are a good candidate for the {@link Prefetcher}. */
+  Set<String> prefetchFileExtensions();
 }
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
index 726f970..c7ab12b 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
@@ -29,8 +29,14 @@
     return ServiceManager.getService(PrefetchService.class);
   }
 
-  /** Instructs all prefetchers to prefetch these files. */
-  ListenableFuture<?> prefetchFiles(Project project, Collection<File> files);
+  /**
+   * Instructs all prefetchers to prefetch these files.
+   *
+   * @param refetchCachedFiles True if all files should be fetched, regardless of whether they were
+   *     recently fetched.
+   */
+  ListenableFuture<?> prefetchFiles(
+      Project project, Collection<File> files, boolean refetchCachedFiles);
 
   ListenableFuture<?> prefetchProjectFiles(
       Project project, ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData);
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
index 05e02a6..27d3c03 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
@@ -16,9 +16,11 @@
 package com.google.idea.blaze.base.prefetch;
 
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -26,24 +28,86 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import java.io.File;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 
 /** Implementation for prefetcher. */
 public class PrefetchServiceImpl implements PrefetchService {
 
+  private static final Logger logger = Logger.getInstance(PrefetchServiceImpl.class);
+
+  private static final long REFETCH_PERIOD_MILLIS = 24 * 60 * 60 * 1000;
+  private final Map<Integer, Long> fileToLastFetchTimeMillis = Maps.newConcurrentMap();
+
   @Override
-  public ListenableFuture<?> prefetchFiles(Project project, Collection<File> files) {
+  public ListenableFuture<?> prefetchFiles(
+      Project project, Collection<File> files, boolean refetchCachedFiles) {
+    if (files.isEmpty() || !enabled(project)) {
+      return Futures.immediateFuture(null);
+    }
+    if (!refetchCachedFiles) {
+      long startTime = System.currentTimeMillis();
+      // ignore recently fetched files
+      files =
+          files
+              .stream()
+              .filter(file -> shouldPrefetch(file, startTime))
+              .collect(Collectors.toList());
+    }
+    FileAttributeProvider provider = FileAttributeProvider.getInstance();
+    List<ListenableFuture<File>> canonicalFiles =
+        files
+            .stream()
+            .map(file -> FetchExecutor.EXECUTOR.submit(() -> toCanonicalFile(provider, file)))
+            .collect(Collectors.toList());
     List<ListenableFuture<?>> futures = Lists.newArrayList();
     for (Prefetcher prefetcher : Prefetcher.EP_NAME.getExtensions()) {
-      futures.add(prefetcher.prefetchFiles(project, files, FetchExecutor.EXECUTOR));
+      futures.add(prefetcher.prefetchFiles(project, canonicalFiles, FetchExecutor.EXECUTOR));
     }
     return Futures.allAsList(futures);
   }
 
+  private static boolean enabled(Project project) {
+    for (Prefetcher prefetcher : Prefetcher.EP_NAME.getExtensions()) {
+      if (prefetcher.enabled(project)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Nullable
+  private static File toCanonicalFile(FileAttributeProvider provider, File file) {
+    try {
+      File canonicalFile = file.getCanonicalFile();
+      if (provider.exists(canonicalFile)) {
+        return canonicalFile;
+      }
+    } catch (IOException e) {
+      logger.warn(e);
+    }
+    return null;
+  }
+
+  /** Returns false if this file has been recently prefetched. */
+  private boolean shouldPrefetch(File file, long startTime) {
+    // Filter files that have been recently fetched
+    Long lastFetchTime = fileToLastFetchTimeMillis.get(file.hashCode());
+    if (lastFetchTime != null && (startTime - lastFetchTime < REFETCH_PERIOD_MILLIS)) {
+      return false;
+    }
+    fileToLastFetchTimeMillis.put(file.hashCode(), startTime);
+    return true;
+  }
+
   @Override
   public ListenableFuture<?> prefetchProjectFiles(
       Project project, ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
@@ -53,6 +117,10 @@
       return Futures.immediateFuture(null);
     }
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+    if (!FileAttributeProvider.getInstance().exists(workspaceRoot.directory())) {
+      // quick sanity check before trying to prefetch each individual file
+      return Futures.immediateFuture(null);
+    }
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, importSettings.getBuildSystem())
             .add(projectViewSet)
@@ -63,8 +131,8 @@
       files.add(workspaceRoot.fileForPath(workspacePath));
     }
     for (PrefetchFileSource fileSource : PrefetchFileSource.EP_NAME.getExtensions()) {
-      fileSource.addFilesToPrefetch(project, projectViewSet, blazeProjectData, files);
+      fileSource.addFilesToPrefetch(project, projectViewSet, importRoots, blazeProjectData, files);
     }
-    return prefetchFiles(project, files);
+    return prefetchFiles(project, files, false);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java b/base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java
index 64503e0..55b30c2 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java
@@ -17,6 +17,7 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import java.io.File;
@@ -28,10 +29,16 @@
       ExtensionPointName.create("com.google.idea.blaze.Prefetcher");
 
   /**
-   * Prefetches the given list of files.
+   * Prefetches the given list of canonical files.
    *
    * <p>It is the responsibility of the prefetcher to filter out any files it isn't interested in.
    */
   ListenableFuture<?> prefetchFiles(
-      Project project, Collection<File> file, ListeningExecutorService executor);
+      Project project,
+      Collection<ListenableFuture<File>> fileFutures,
+      ListeningExecutorService executor);
+
+  default boolean enabled(Project project) {
+    return Blaze.isBlazeProject(project);
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/prefetch/ProtoPrefetchFileSource.java b/base/src/com/google/idea/blaze/base/prefetch/ProtoPrefetchFileSource.java
new file mode 100644
index 0000000..efc4876
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/prefetch/ProtoPrefetchFileSource.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.prefetch;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.Set;
+
+/** Requests that proto source files be prefetched during sync. */
+public class ProtoPrefetchFileSource implements PrefetchFileSource {
+
+  @Override
+  public void addFilesToPrefetch(
+      Project project,
+      ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
+      BlazeProjectData blazeProjectData,
+      Set<File> files) {}
+
+  @Override
+  public Set<String> prefetchFileExtensions() {
+    return ImmutableSet.of("proto");
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
index e2e1211..29cc456 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
@@ -26,6 +26,7 @@
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import javax.annotation.Nullable;
 
 /** A collection of project views and their file names. */
@@ -57,20 +58,12 @@
   }
 
   /** Gets the last value from any scalar sections */
-  @Nullable
-  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key) {
-    return getScalarValue(key, null);
-  }
-
-  /** Gets the last value from any scalar sections */
-  @Nullable
-  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key, @Nullable T defaultValue) {
+  public <T> Optional<T> getScalarValue(SectionKey<T, ScalarSection<T>> key) {
     Collection<ScalarSection<T>> sections = getSections(key);
     if (sections.isEmpty()) {
-      return defaultValue;
-    } else {
-      return Iterables.getLast(sections).getValue();
+      return Optional.empty();
     }
+    return Optional.of(Iterables.getLast(sections).getValue());
   }
 
   public <T, SectionType extends Section<T>> Collection<SectionType> getSections(
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
index 9e05e64..68ca00a 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
@@ -100,8 +100,9 @@
 
     private static Set<LanguageClass> availableAdditionalLanguages(ProjectViewSet projectView) {
       WorkspaceType workspaceType =
-          projectView.getScalarValue(
-              WorkspaceTypeSection.KEY, LanguageSupport.getDefaultWorkspaceType());
+          projectView
+              .getScalarValue(WorkspaceTypeSection.KEY)
+              .orElse(LanguageSupport.getDefaultWorkspaceType());
       return LanguageSupport.availableAdditionalLanguages(workspaceType);
     }
   }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
index 51f0709..1cde7d7 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.base.projectview.section.ListSectionParser;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze;
 import javax.annotation.Nullable;
 
 /** Section for blaze_flags */
@@ -29,7 +30,7 @@
   public static final SectionParser PARSER = new BuildFlagsSectionParser();
 
   static class BuildFlagsSectionParser extends ListSectionParser<String> {
-    protected BuildFlagsSectionParser() {
+    BuildFlagsSectionParser() {
       super(KEY);
     }
 
@@ -51,7 +52,10 @@
 
     @Override
     public String quickDocs() {
-      return "A set of flags that get passed to all build command invocations as arguments";
+      return String.format(
+          "A set of flags that get passed to all %s build command invocations as arguments. This"
+              + "includes both sync and run configuration actions.",
+          Blaze.guessBuildSystemName());
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
index eda173f..3889951 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
@@ -33,11 +33,13 @@
           AdditionalLanguagesSection.PARSER,
           TestSourceSection.PARSER,
           BuildFlagsSection.PARSER,
+          SyncFlagsSection.PARSER,
           ImportTargetOutputSection.PARSER,
           ExcludeTargetSection.PARSER,
           ExcludedSourceSection.PARSER,
           RunConfigurationsSection.PARSER,
-          ShardBlazeBuildsSection.PARSER);
+          ShardBlazeBuildsSection.PARSER,
+          TargetShardSizeSection.PARSER);
 
   public static List<SectionParser> getParsers() {
     List<SectionParser> parsers = Lists.newArrayList(PARSERS);
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/SyncFlagsSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/SyncFlagsSection.java
new file mode 100644
index 0000000..b2c4b11
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/SyncFlagsSection.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.projectview.section.sections;
+
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ListSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze;
+import javax.annotation.Nullable;
+
+/** Section for blaze_flags */
+public class SyncFlagsSection {
+  public static final SectionKey<String, ListSection<String>> KEY = SectionKey.of("sync_flags");
+  public static final SectionParser PARSER = new SyncFlagsSectionParser();
+
+  static class SyncFlagsSectionParser extends ListSectionParser<String> {
+    SyncFlagsSectionParser() {
+      super(KEY);
+    }
+
+    @Nullable
+    @Override
+    protected String parseItem(ProjectViewParser parser, ParseContext parseContext) {
+      return parseContext.current().text;
+    }
+
+    @Override
+    protected void printItem(String item, StringBuilder sb) {
+      sb.append(item);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+
+    @Override
+    public String quickDocs() {
+      return String.format(
+          "A set of flags that get passed to %s build during all sync actions. Unlike"
+              + "'build_flags', these are not used for run configurations, so use 'sync_flags' "
+              + "only when necessary, as they can defeat %<s caching.",
+          Blaze.guessBuildSystemName());
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetShardSizeSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetShardSizeSection.java
new file mode 100644
index 0000000..7d1b354
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetShardSizeSection.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.projectview.section.sections;
+
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import javax.annotation.Nullable;
+
+/** Allows the user to tune the maximum number of targets in each blaze build shard. */
+public class TargetShardSizeSection {
+  public static final SectionKey<Integer, ScalarSection<Integer>> KEY =
+      SectionKey.of("target_shard_size");
+  public static final SectionParser PARSER = new TargetShardSizeSectionParser();
+
+  private static class TargetShardSizeSectionParser extends ScalarSectionParser<Integer> {
+    TargetShardSizeSectionParser() {
+      super(KEY, ':');
+    }
+
+    @Nullable
+    @Override
+    protected Integer parseItem(ProjectViewParser parser, ParseContext parseContext, String rest) {
+      try {
+        return Integer.parseInt(rest);
+      } catch (NumberFormatException e) {
+        parseContext.addError(
+            String.format("Invalid shard size '%s': Shard size must be an integer", rest));
+        return null;
+      }
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, Integer value) {
+      sb.append(value.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+
+    @Override
+    public String quickDocs() {
+      return "Sets the maximum number of targets per shard, when sharding build invocations during "
+          + "sync. Only relevant if 'shard_sync: true' is also set";
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java b/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java
index afb6e79..f6fe1fb 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java
@@ -15,9 +15,15 @@
  */
 package com.google.idea.blaze.base.run;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.run.producers.BlazeBuildFileRunConfigurationProducer;
+import com.google.idea.blaze.base.model.primitives.RuleType;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.openapi.project.Project;
@@ -28,9 +34,14 @@
  */
 public class BlazeBuildTargetRunConfigurationFactory extends BlazeRunConfigurationFactory {
 
+  // The rule types we auto-create run configurations for during sync.
+  private static final ImmutableSet<RuleType> HANDLED_RULE_TYPES =
+      ImmutableSet.of(RuleType.TEST, RuleType.BINARY);
+
   @Override
   public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
-    return BlazeBuildFileRunConfigurationProducer.handlesTarget(project, label);
+    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
+    return target != null && HANDLED_RULE_TYPES.contains(target.kind.ruleType);
   }
 
   @Override
@@ -40,6 +51,26 @@
 
   @Override
   public void setupConfiguration(RunConfiguration configuration, Label target) {
-    BlazeBuildFileRunConfigurationProducer.setupConfiguration(configuration, target);
+    BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
+    blazeConfig.setTarget(target);
+
+    BlazeCommandRunConfigurationCommonState state =
+        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    Kind kind = blazeConfig.getKindForTarget();
+    if (state != null && kind != null) {
+      state.getCommandState().setCommand(commandForRuleType(kind.ruleType));
+    }
+    blazeConfig.setGeneratedName();
+  }
+
+  private static BlazeCommandName commandForRuleType(RuleType ruleType) {
+    switch (ruleType) {
+      case BINARY:
+        return BlazeCommandName.RUN;
+      case TEST:
+        return BlazeCommandName.TEST;
+      default:
+        return BlazeCommandName.BUILD;
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
index 6a8d573..9e40076 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
@@ -33,13 +33,8 @@
   public abstract boolean handlesTarget(
       Project project, BlazeProjectData blazeProjectData, Label label);
 
-  /**
-   * Returns whether this factory can initialize a configuration. <br>
-   * The default implementation simply checks that the configuration has the same {@link
-   * com.intellij.execution.configurations.ConfigurationType} as the type of {@link
-   * #getConfigurationFactory()}.
-   */
-  public boolean handlesConfiguration(RunConfiguration configuration) {
+  /** Returns whether this factory is compatible with the given run configuration type. */
+  public final boolean handlesConfiguration(RunConfiguration configuration) {
     return getConfigurationFactory().getType().equals(configuration.getType());
   }
 
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
index 96be88d..f8a3b01 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -122,7 +122,6 @@
             configurationFactory.createForTarget(project, runManager, label);
         runManager.addConfiguration(settings, /* isShared */ false);
         if (runManager.getSelectedConfiguration() == null) {
-          // TODO(joshgiles): Better strategy for picking initially selected config.
           runManager.setSelectedConfiguration(settings);
         }
         break;
diff --git a/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java b/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java
index c6991ca..a5ff002 100644
--- a/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java
+++ b/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java
@@ -23,7 +23,11 @@
 import java.util.List;
 import javax.annotation.Nullable;
 
-/** Information about any distributed executor available to the build system. */
+/**
+ * Information about any distributed executor available to the build system.
+ *
+ * <p>TODO(brendandouglas): Temporary migration code. Remove in 2017.09.XX+
+ */
 public interface DistributedExecutorSupport {
 
   ExtensionPointName<DistributedExecutorSupport> EP_NAME =
diff --git a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
index 74a46e4..f063415 100644
--- a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
@@ -24,7 +24,10 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Heuristic to match test targets to source files. */
@@ -64,22 +67,28 @@
       File sourceFile,
       Collection<TargetIdeInfo> targets,
       @Nullable TestSize testSize) {
-
+    if (targets.isEmpty()) {
+      return null;
+    }
+    List<TargetIdeInfo> filteredTargets = new ArrayList<>(targets);
     for (TestTargetHeuristic filter : EP_NAME.getExtensions()) {
-      TargetIdeInfo match =
-          targets
+      List<TargetIdeInfo> matches =
+          filteredTargets
               .stream()
               .filter(
                   target ->
                       filter.matchesSource(project, target, sourcePsiFile, sourceFile, testSize))
-              .findFirst()
-              .orElse(null);
-
-      if (match != null) {
-        return match.key.label;
+              .collect(Collectors.toList());
+      if (matches.size() == 1) {
+        return matches.get(0).key.label;
+      }
+      if (!matches.isEmpty()) {
+        // A higher-priority filter found more than one match -- subsequent filters will only
+        // consider these matches.
+        filteredTargets = matches;
       }
     }
-    return targets.isEmpty() ? null : targets.iterator().next().key.label;
+    return filteredTargets.iterator().next().key.label;
   }
 
   /** Returns true if the rule and source file match, according to this heuristic. */
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
index 4605c17..5e77bfe 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
@@ -20,17 +20,18 @@
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.BlazeInvocationContext;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.DistributedExecutorSupport;
 import com.google.idea.blaze.base.run.filter.BlazeTargetFilter;
 import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
 import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
-import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestUiSession;
 import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.smrunner.TestUiSessionProvider;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
@@ -48,6 +49,7 @@
 import com.intellij.execution.configurations.WrappingRunConfiguration;
 import com.intellij.execution.filters.Filter;
 import com.intellij.execution.filters.TextConsoleBuilderImpl;
+import com.intellij.execution.filters.UrlFilter;
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.execution.process.ProcessListener;
 import com.intellij.execution.runners.ExecutionEnvironment;
@@ -91,6 +93,7 @@
           ImmutableList.<Filter>builder()
               .addAll(consoleFilters)
               .add(new BlazeTargetFilter(environment.getProject()))
+              .add(new UrlFilter())
               .build();
     }
 
@@ -121,18 +124,18 @@
       assert projectViewSet != null;
 
       ImmutableList<String> testHandlerFlags = ImmutableList.of();
-      BlazeTestEventsHandler testEventsHandler =
+      BlazeTestUiSession testUiSession =
           canUseTestUi()
-              ? BlazeTestEventsHandler.getHandlerForTarget(project, configuration.getTarget())
+              ? TestUiSessionProvider.createForTarget(project, configuration.getTarget())
               : null;
-      if (testEventsHandler != null) {
-        testHandlerFlags = BlazeTestEventsHandler.getBlazeFlags(project);
+      if (testUiSession != null) {
+        testHandlerFlags = testUiSession.getBlazeFlags();
         setConsoleBuilder(
             new TextConsoleBuilderImpl(project) {
               @Override
               protected ConsoleView createConsole() {
                 return SmRunnerUtils.getConsoleView(
-                    project, configuration, getEnvironment().getExecutor(), testEventsHandler);
+                    project, configuration, getEnvironment().getExecutor(), testUiSession);
               }
             });
       }
@@ -170,23 +173,23 @@
               ? handlerState.getBlazeBinaryState().getBlazeBinary()
               : Blaze.getBuildSystemProvider(project).getBinaryPath();
 
-      BlazeCommand.Builder command =
-          BlazeCommand.builder(binaryPath, handlerState.getCommandState().getCommand())
-              .addTargets(configuration.getTarget())
-              .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-              .addBlazeFlags(testHandlerFlags)
-              .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
-              .addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
+      return BlazeCommand.builder(binaryPath, getCommand())
+          .addTargets(configuration.getTarget())
+          .addBlazeFlags(
+              BlazeFlags.blazeFlags(
+                  project, projectViewSet, getCommand(), BlazeInvocationContext.RunConfiguration))
+          .addBlazeFlags(testHandlerFlags)
+          .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
+          .addExeFlags(handlerState.getExeFlagsState().getExpandedFlags())
+          .build();
+    }
 
-      command.addBlazeFlags(
-          DistributedExecutorSupport.getBlazeFlags(
-              project, handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor));
-      return command.build();
+    private BlazeCommandName getCommand() {
+      return handlerState.getCommandState().getCommand();
     }
 
     private boolean canUseTestUi() {
-      return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())
-          && !handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
+      return BlazeCommandName.TEST.equals(getCommand());
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java b/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java
index aed9db4..a52a756 100644
--- a/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java
+++ b/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java
@@ -17,6 +17,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.sdkcompat.run.RunnerAndConfigurationSettingsCompatUtils;
 import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.impl.RunManagerImpl;
@@ -89,7 +90,7 @@
       throws InvalidDataException {
     RunManagerImpl manager = RunManagerImpl.getInstanceImpl(project);
     RunnerAndConfigurationSettingsImpl settings = new RunnerAndConfigurationSettingsImpl(manager);
-    settings.readExternal(element);
+    RunnerAndConfigurationSettingsCompatUtils.readConfiguration(settings, element);
     RunConfiguration config = settings.getConfiguration();
     if (config == null) {
       return null;
diff --git a/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
index 9a838ed..6ce791c 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
@@ -111,14 +111,11 @@
     setupConfiguration(
         configuration.getProject(), blazeProjectData, generatedConfiguration, target);
 
-    // TODO This check should be removed once isTestRule is in a RuleFactory and
-    // test rules' suggestedName is modified to account for test filter flags.
-    if (Kind.isTestRule(target.ruleType)) {
-      BlazeCommandRunConfigurationCommonState handlerState =
-          configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-      if (handlerState != null && handlerState.getTestFilterFlag() != null) {
-        return false;
-      }
+    // ignore filtered test configs, produced by other configuration producers.
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState != null && handlerState.getTestFilterFlag() != null) {
+      return false;
     }
 
     return Objects.equals(configuration.suggestedName(), generatedConfiguration.suggestedName())
diff --git a/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
index 92089e4..d1941a1 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
@@ -31,7 +31,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import java.util.Optional;
 
 /**
  * Handles the specific case where the user creates a run configuration by selecting test suites /
@@ -53,8 +53,8 @@
       BlazeCommandRunConfiguration configuration,
       ConfigurationContext context,
       Ref<PsiElement> sourceElement) {
-    String testFilter = getTestFilter(context);
-    if (testFilter == null) {
+    Optional<String> testFilter = getTestFilter(context);
+    if (!testFilter.isPresent()) {
       return false;
     }
     BlazeCommandRunConfigurationCommonState handlerState =
@@ -66,7 +66,7 @@
     // replace old test filter flag if present
     List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
     flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
-    flags.add(testFilter);
+    flags.add(testFilter.get());
 
     if (SmRunnerUtils.countSelectedTestCases(context) == 1
         && !flags.contains(BlazeFlags.DISABLE_TEST_SHARDING)) {
@@ -81,8 +81,8 @@
   @Override
   protected boolean doIsConfigFromContext(
       BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
-    String testFilter = getTestFilter(context);
-    if (testFilter == null) {
+    Optional<String> testFilter = getTestFilter(context);
+    if (!testFilter.isPresent()) {
       return false;
     }
     BlazeCommandRunConfigurationCommonState handlerState =
@@ -90,28 +90,25 @@
 
     return handlerState != null
         && Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)
-        && Objects.equals(testFilter, handlerState.getTestFilterFlag());
+        && Objects.equals(testFilter.get(), handlerState.getTestFilterFlag());
   }
 
-  @Nullable
-  private static String getTestFilter(ConfigurationContext context) {
+  private static Optional<String> getTestFilter(ConfigurationContext context) {
     RunConfiguration base = context.getOriginalConfiguration(null);
     if (!(base instanceof BlazeCommandRunConfiguration)) {
-      return null;
+      return Optional.empty();
     }
     TargetExpression target = ((BlazeCommandRunConfiguration) base).getTarget();
     if (target == null) {
-      return null;
-    }
-    BlazeTestEventsHandler testEventsHandler =
-        BlazeTestEventsHandler.getHandlerForTarget(context.getProject(), target);
-    if (testEventsHandler == null) {
-      return null;
+      return Optional.empty();
     }
     List<Location<?>> selectedElements = SmRunnerUtils.getSelectedSmRunnerTreeElements(context);
     if (selectedElements.isEmpty()) {
       return null;
     }
-    return testEventsHandler.getTestFilter(context.getProject(), selectedElements);
+    Optional<BlazeTestEventsHandler> testEventsHandler =
+        BlazeTestEventsHandler.getHandlerForTarget(context.getProject(), target);
+    return testEventsHandler.map(
+        handler -> handler.getTestFilter(context.getProject(), selectedElements));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BazelTestUiSessionProvider.java b/base/src/com/google/idea/blaze/base/run/smrunner/BazelTestUiSessionProvider.java
new file mode 100644
index 0000000..d9ec034
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BazelTestUiSessionProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.run.smrunner;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.buildresult.BuildEventProtocolUtils;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.run.testlogs.BuildEventProtocolTestFinderStrategy;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Provides a {@link BlazeTestUiSession} for Bazel projects. */
+public class BazelTestUiSessionProvider implements TestUiSessionProvider {
+
+  @Nullable
+  @Override
+  public BlazeTestUiSession getTestUiSession(BlazeVersionData blazeVersion) {
+    if (blazeVersion.buildSystem() != BuildSystem.Bazel) {
+      return null;
+    }
+
+    File bepOutputFile = BuildEventProtocolUtils.createTempOutputFile();
+    ImmutableList<String> flags =
+        ImmutableList.<String>builder()
+            .add("--runs_per_test=1", "--flaky_test_attempts=1")
+            .addAll(BuildEventProtocolUtils.getBuildFlags(bepOutputFile))
+            .build();
+
+    return BlazeTestUiSession.create(
+        flags, new BuildEventProtocolTestFinderStrategy(bepOutputFile));
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
deleted file mode 100644
index 21bf76a..0000000
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright 2017 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.idea.blaze.base.run.smrunner;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
-import com.google.idea.blaze.base.model.primitives.Kind;
-import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
-import com.intellij.execution.Location;
-import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
-import com.intellij.execution.testframework.sm.runner.SMTestLocator;
-import com.intellij.execution.ui.ConsoleView;
-import com.intellij.openapi.project.Project;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import javax.annotation.Nullable;
-
-/** Combines multiple language-specific handlers (e.g. to handle test_suite targets). */
-public class BlazeCompositeTestEventsHandler extends BlazeTestEventsHandler {
-
-  private static ImmutableMap<Kind, BlazeTestEventsHandler> collectHandlers() {
-    Map<Kind, BlazeTestEventsHandler> map = new HashMap<>();
-    for (BlazeTestEventsHandler handler : BlazeTestEventsHandler.EP_NAME.getExtensions()) {
-      if (handler instanceof BlazeCompositeTestEventsHandler) {
-        continue;
-      }
-      for (Kind kind : handler.handledKinds()) {
-        // earlier handlers get priority.
-        map.putIfAbsent(kind, handler);
-      }
-    }
-    return Maps.immutableEnumMap(map);
-  }
-
-  private static ImmutableMap<Kind, BlazeTestEventsHandler> handlers;
-
-  private static ImmutableMap<Kind, BlazeTestEventsHandler> getHandlers() {
-    if (handlers == null) {
-      handlers = collectHandlers();
-    }
-    return handlers;
-  }
-
-  @Override
-  public boolean handlesTargetKind(@Nullable Kind kind) {
-    // composite handler specifically exists to handle test-suites and multi-target blaze
-    // invocations, so must handle targets without a kind.
-    return kind == null || kind == Kind.TEST_SUITE || handledKinds().contains(kind);
-  }
-
-  @Override
-  protected EnumSet<Kind> handledKinds() {
-    ImmutableSet<Kind> handledKinds = getHandlers().keySet();
-    return !handledKinds.isEmpty() ? EnumSet.copyOf(handledKinds) : EnumSet.noneOf(Kind.class);
-  }
-
-  @Override
-  public SMTestLocator getTestLocator() {
-    return new CompositeSMTestLocator(
-        ImmutableList.copyOf(
-            getHandlers()
-                .values()
-                .stream()
-                .map(BlazeTestEventsHandler::getTestLocator)
-                .collect(Collectors.toList())));
-  }
-
-  @Nullable
-  @Override
-  public String getTestFilter(Project project, List<Location<?>> testLocations) {
-    // We make no attempt to support re-running a subset of tests for test_suites or target patterns
-    return null;
-  }
-
-  @Nullable
-  @Override
-  public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
-    return null;
-  }
-
-  @Override
-  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
-    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
-    return handler != null ? handler.ignoreSuite(kind, suite) : super.ignoreSuite(kind, suite);
-  }
-
-  /** Converts the testsuite name in the blaze test XML to a user-friendly format */
-  @Override
-  public String suiteDisplayName(@Nullable Kind kind, String rawName) {
-    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
-    return handler != null
-        ? handler.suiteDisplayName(kind, rawName)
-        : super.suiteDisplayName(kind, rawName);
-  }
-
-  /** Converts the testcase name in the blaze test XML to a user-friendly format */
-  @Override
-  public String testDisplayName(@Nullable Kind kind, String rawName) {
-    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
-    return handler != null
-        ? handler.testDisplayName(kind, rawName)
-        : super.testDisplayName(kind, rawName);
-  }
-
-  @Override
-  public String suiteLocationUrl(@Nullable Kind kind, String name) {
-    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
-    return handler != null
-        ? handler.suiteLocationUrl(kind, name)
-        : super.suiteLocationUrl(kind, name);
-  }
-
-  @Override
-  public String testLocationUrl(
-      @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
-    BlazeTestEventsHandler handler = getHandlers().get(kind);
-    return handler != null
-        ? handler.testLocationUrl(kind, parentSuite, name, className)
-        : super.testLocationUrl(kind, parentSuite, name, className);
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeGenericTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeGenericTestEventsHandler.java
new file mode 100644
index 0000000..996b756
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeGenericTestEventsHandler.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 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.idea.blaze.base.run.smrunner;
+
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Fallback handler for otherwise unsupported targets. Normally it's undesirable to have a test UI
+ * for such targets, but if they're part of a test_suite or multi-target Blaze invocation, we handle
+ * them in a best-effort way.
+ */
+public class BlazeGenericTestEventsHandler implements BlazeTestEventsHandler {
+
+  @Override
+  public boolean handlesKind(@Nullable Kind kind) {
+    // Generic handler specifically exists to handle test-suites and multi-target blaze
+    // invocations, so must handle any targets without a (known) kind.
+    return kind == null || kind == Kind.TEST_SUITE;
+  }
+
+  @Override
+  @Nullable
+  public SMTestLocator getTestLocator() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<Location<?>> testLocations) {
+    // Test filters are language-specific, and don't work properly for multi-target invocations.
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
index 578bb9b..4fc47bb 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
@@ -42,7 +42,7 @@
 
   private final BlazeTestEventsHandler eventsHandler;
 
-  public BlazeRerunFailedTestsAction(
+  BlazeRerunFailedTestsAction(
       BlazeTestEventsHandler eventsHandler, ComponentContainer componentContainer) {
     super(componentContainer);
     this.eventsHandler = eventsHandler;
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java
index f4c95b9..7e55569 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java
@@ -15,8 +15,9 @@
  */
 package com.google.idea.blaze.base.run.smrunner;
 
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.intellij.execution.Executor;
-import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.testframework.TestConsoleProperties;
 import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
 import com.intellij.execution.testframework.sm.SMCustomMessagesParsing;
@@ -24,34 +25,50 @@
 import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
 import com.intellij.execution.ui.ConsoleView;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Integrates blaze test results with the SM-runner test UI. */
 public class BlazeTestConsoleProperties extends SMTRunnerConsoleProperties
     implements SMCustomMessagesParsing {
 
-  private final BlazeTestEventsHandler eventsHandler;
+  private final BlazeCommandRunConfiguration runConfiguration;
+  private final BlazeTestUiSession testUiSession;
 
   public BlazeTestConsoleProperties(
-      RunConfiguration runConfiguration, Executor executor, BlazeTestEventsHandler eventsHandler) {
+      BlazeCommandRunConfiguration runConfiguration,
+      Executor executor,
+      BlazeTestUiSession testUiSession) {
     super(runConfiguration, SmRunnerUtils.BLAZE_FRAMEWORK, executor);
-    this.eventsHandler = eventsHandler;
+    this.runConfiguration = runConfiguration;
+    this.testUiSession = testUiSession;
   }
 
   @Override
   public OutputToGeneralTestEventsConverter createTestEventsConverter(
       String framework, TestConsoleProperties consoleProperties) {
-    return new BlazeXmlToTestEventsConverter(framework, consoleProperties, eventsHandler);
+    return new BlazeXmlToTestEventsConverter(
+        framework, consoleProperties, testUiSession.getTestResultFinderStrategy());
   }
 
   @Override
   public SMTestLocator getTestLocator() {
-    return eventsHandler.getTestLocator();
+    return new CompositeSMTestLocator(
+        ImmutableList.copyOf(
+            Arrays.stream(BlazeTestEventsHandler.EP_NAME.getExtensions())
+                .map(BlazeTestEventsHandler::getTestLocator)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList())));
   }
 
   @Nullable
   @Override
   public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
-    return eventsHandler.createRerunFailedTestsAction(consoleView);
+    return BlazeTestEventsHandler.getHandlerForTarget(
+            runConfiguration.getProject(), runConfiguration.getTarget())
+        .map(handler -> handler.createRerunFailedTestsAction(consoleView))
+        .orElse(null);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
index 3ecdeea..47f229c 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
@@ -16,15 +16,12 @@
 package com.google.idea.blaze.base.run.smrunner;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.Location;
 import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
@@ -32,46 +29,60 @@
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import com.intellij.util.io.URLUtil;
-import java.util.EnumSet;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import javax.annotation.Nullable;
 
-/** Language-specific handling of SM runner test protocol */
-public abstract class BlazeTestEventsHandler {
+/** Stateless language-specific handling of SM runner test protocol */
+public interface BlazeTestEventsHandler {
 
-  static final ExtensionPointName<BlazeTestEventsHandler> EP_NAME =
+  ExtensionPointName<BlazeTestEventsHandler> EP_NAME =
       ExtensionPointName.create("com.google.idea.blaze.BlazeTestEventsHandler");
 
   /**
-   * Blaze/Bazel flags required for test UI.<br>
-   * Forces local test execution, without retries.
+   * Whether there's a {@link BlazeTestEventsHandler} applicable to the given target.
+   *
+   * <p>Test results will still be displayed for unhandled kinds if they're included in a test_suite
+   * or multi-target Blaze invocation, where we don't know up front the languages involved.
    */
-  public static ImmutableList<String> getBlazeFlags(Project project) {
-    ImmutableList.Builder<String> flags =
-        ImmutableList.<String>builder().add("--runs_per_test=1", "--flaky_test_attempts=1");
-    if (Blaze.getBuildSystem(project) == BuildSystem.Blaze) {
-      flags.add("--test_strategy=local");
-    }
-    if (Blaze.getBuildSystem(project) == BuildSystem.Bazel) {
-      flags.add("--test_sharding_strategy=disabled");
-    }
-    return flags.build();
-  }
-
-  @Nullable
-  public static BlazeTestEventsHandler getHandlerForTarget(
-      Project project, TargetExpression target) {
+  static boolean targetSupported(Project project, TargetExpression target) {
     Kind kind = getKindForTarget(project, target);
-    for (BlazeTestEventsHandler handler : EP_NAME.getExtensions()) {
-      if (handler.handlesTargetKind(kind)) {
-        return handler;
-      }
-    }
-    return null;
+    return Arrays.stream(EP_NAME.getExtensions()).anyMatch(handler -> handler.handlesKind(kind));
+  }
+
+  /**
+   * Returns a {@link BlazeTestEventsHandler} applicable to the given target.
+   *
+   * <p>If no such handler exists, falls back to returning {@link BlazeGenericTestEventsHandler}.
+   * This adds support for test suites / multi-target invocations, which can mix supported and
+   * unsupported target kinds.
+   */
+  static BlazeTestEventsHandler getHandlerForTargetKindOrFallback(@Nullable Kind kind) {
+    return getHandlerForTargetKind(kind).orElse(new BlazeGenericTestEventsHandler());
+  }
+
+  /**
+   * Returns a {@link BlazeTestEventsHandler} applicable to the given target or {@link
+   * Optional#empty()} if no such handler can be found.
+   */
+  static Optional<BlazeTestEventsHandler>