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> getHandlerForTarget(
+      Project project, TargetExpression target) {
+    return getHandlerForTargetKind(getKindForTarget(project, target));
+  }
+
+  /**
+   * Returns a {@link BlazeTestEventsHandler} applicable to the given target kind, or {@link
+   * Optional#empty()} if no such handler can be found.
+   */
+  static Optional<BlazeTestEventsHandler> getHandlerForTargetKind(@Nullable Kind kind) {
+    return Arrays.stream(EP_NAME.getExtensions())
+        .filter(handler -> handler.handlesKind(kind))
+        .findFirst();
   }
 
   @Nullable
-  private static Kind getKindForTarget(Project project, TargetExpression target) {
+  static Kind getKindForTarget(Project project, TargetExpression target) {
     if (!(target instanceof Label)) {
       return null;
     }
@@ -79,42 +90,46 @@
     return targetInfo != null ? targetInfo.kind : null;
   }
 
-  public boolean handlesTargetKind(@Nullable Kind kind) {
-    return handledKinds().contains(kind);
-  }
+  boolean handlesKind(@Nullable Kind kind);
 
-  protected abstract EnumSet<Kind> handledKinds();
-
-  public abstract SMTestLocator getTestLocator();
+  /**
+   * A {@link SMTestLocator} to convert location URLs provided by this event handler to project PSI
+   * elements. Returns {@code null} if no such conversion is available.
+   */
+  @Nullable
+  SMTestLocator getTestLocator();
 
   /**
    * The --test_filter flag passed to blaze to rerun the given tests.
    *
-   * @return null if no filter can be constructed for these tests.
+   * @return {@code null} if no filter can be constructed for these tests
    */
   @Nullable
-  public abstract String getTestFilter(Project project, List<Location<?>> testLocations);
+  String getTestFilter(Project project, List<Location<?>> testLocations);
 
+  /** Returns {@code null} if this test events handler doesn't support test filtering. */
   @Nullable
-  public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
+  default AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
     return new BlazeRerunFailedTestsAction(this, consoleView);
   }
 
-  /** Converts the testsuite name in the blaze test XML to a user-friendly format */
-  public String suiteDisplayName(@Nullable Kind kind, String rawName) {
+  /** Converts the testsuite name in the blaze test XML to a user-friendly format. */
+  default String suiteDisplayName(@Nullable Kind kind, String rawName) {
     return rawName;
   }
 
-  /** Converts the testcase name in the blaze test XML to a user-friendly format */
-  public String testDisplayName(@Nullable Kind kind, String rawName) {
+  /** Converts the testcase name in the blaze test XML to a user-friendly format. */
+  default String testDisplayName(@Nullable Kind kind, String rawName) {
     return rawName;
   }
 
-  public String suiteLocationUrl(@Nullable Kind kind, String name) {
+  /** Converts the suite name to a parsable location URL. */
+  default String suiteLocationUrl(@Nullable Kind kind, String name) {
     return SmRunnerUtils.GENERIC_SUITE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
   }
 
-  public String testLocationUrl(
+  /** Converts the test case and suite names to a parsable location URL. */
+  default String testLocationUrl(
       @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
     String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR;
     if (Strings.isNullOrEmpty(className)) {
@@ -124,7 +139,7 @@
   }
 
   /** Whether to skip logging a {@link TestSuite}. */
-  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
+  default boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
     // by default only include innermost 'testsuite' elements
     return !suite.testSuites.isEmpty();
   }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestUiSession.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestUiSession.java
new file mode 100644
index 0000000..bf9a6c6
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestUiSession.java
@@ -0,0 +1,45 @@
+/*
+ * 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.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResultFinderStrategy;
+
+/**
+ * Created during a single blaze test invocation, to manage test result finding and UI.
+ *
+ * <p>Unlike {@link BlazeTestEventsHandler}, this can be stateful, retaining information shared
+ * between all stages of the test (e.g. an output file path used for both the initial blaze
+ * invocation and required when parsing test results).
+ */
+@AutoValue
+public abstract class BlazeTestUiSession {
+
+  public static BlazeTestUiSession create(
+      ImmutableList<String> blazeFlags, BlazeTestResultFinderStrategy testResultFinderStrategy) {
+    return new AutoValue_BlazeTestUiSession(blazeFlags, testResultFinderStrategy);
+  }
+
+  /**
+   * Blaze flags required for test UI.<br>
+   * Forces local test execution, without retries.
+   */
+  public abstract ImmutableList<String> getBlazeFlags();
+
+  /** Returns a {@link BlazeTestResultFinderStrategy} for this blaze test invocation. */
+  public abstract BlazeTestResultFinderStrategy getTestResultFinderStrategy();
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
index 0cdee3b..c450d90 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
@@ -15,13 +15,14 @@
  */
 package com.google.idea.blaze.base.run.smrunner;
 
-import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.common.collect.ImmutableMap;
 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.smrunner.BlazeXmlSchema.ErrorOrFailureOrSkipped;
 import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestCase;
 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.run.testlogs.BlazeTestResult;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResult.TestStatus;
 import com.google.idea.blaze.base.run.testlogs.BlazeTestResultFinderStrategy;
 import com.google.idea.blaze.base.run.testlogs.BlazeTestResults;
 import com.google.idea.sdkcompat.smrunner.SmRunnerCompatUtils;
@@ -35,17 +36,15 @@
 import com.intellij.execution.testframework.sm.runner.events.TestStartedEvent;
 import com.intellij.execution.testframework.sm.runner.events.TestSuiteFinishedEvent;
 import com.intellij.execution.testframework.sm.runner.events.TestSuiteStartedEvent;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Key;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
 import javax.annotation.Nullable;
-import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor;
 import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted;
 
 /** Converts blaze test runner xml logs to smRunner events. */
@@ -57,32 +56,14 @@
     NO_ERROR.message = "No message"; // cannot be null
   }
 
-  private final Project project;
-  private final BlazeTestEventsHandler eventsHandler;
+  private final BlazeTestResultFinderStrategy testResultFinderStrategy;
 
   public BlazeXmlToTestEventsConverter(
       String testFrameworkName,
       TestConsoleProperties testConsoleProperties,
-      BlazeTestEventsHandler eventsHandler) {
+      BlazeTestResultFinderStrategy testResultFinderStrategy) {
     super(testFrameworkName, testConsoleProperties);
-    this.project = testConsoleProperties.getProject();
-    this.eventsHandler = eventsHandler;
-  }
-
-  @Override
-  protected boolean processServiceMessages(
-      String s, Key key, ServiceMessageVisitor serviceMessageVisitor) throws ParseException {
-    return super.processServiceMessages(s, key, serviceMessageVisitor);
-  }
-
-  @Override
-  public void process(String text, Key outputType) {
-    super.process(text, outputType);
-  }
-
-  @Override
-  public void dispose() {
-    super.dispose();
+    this.testResultFinderStrategy = testResultFinderStrategy;
   }
 
   @Override
@@ -91,33 +72,29 @@
     onStartTesting();
     getProcessor().onTestsReporterAttached();
 
-    BlazeTestResults testResults = BlazeTestResultFinderStrategy.locateTestResults(project);
-    for (Label target : testResults.failedTargets) {
-      reportFailedTarget(target);
+    BlazeTestResults testResults = testResultFinderStrategy.findTestResults();
+    if (testResults == null) {
+      return;
     }
-    for (Label label : testResults.testXmlFiles.keySet()) {
-      processTestSuites(label, testResults.testXmlFiles.get(label));
+    for (Label label : testResults.perTargetResults.keySet()) {
+      processTestSuites(label, testResults.perTargetResults.get(label));
     }
   }
 
-  private void reportFailedTarget(Label label) {
-    GeneralTestEventsProcessor processor = getProcessor();
-    TestSuiteStarted suiteStarted = new TestSuiteStarted(label.toString());
-    processor.onSuiteStarted(new TestSuiteStartedEvent(suiteStarted, null));
-    String targetName = label.targetName().toString();
-    processor.onTestStarted(new TestStartedEvent(targetName, null));
-    processor.onTestFailure(
-        SmRunnerCompatUtils.getTestFailedEvent(
-            targetName, "Target failed to build. See console output for details", null, 0));
-    processor.onTestFinished(new TestFinishedEvent(targetName, 0L));
-    processor.onSuiteFinished(new TestSuiteFinishedEvent(label.toString()));
-  }
-
   /** Process all test XML files from a single test target. */
-  private void processTestSuites(Label label, Collection<File> files) {
-    Kind kind = getKind(project, label);
+  private void processTestSuites(Label label, Collection<BlazeTestResult> results) {
+    List<File> outputFiles = new ArrayList<>();
+    results.forEach(result -> outputFiles.addAll(result.getOutputXmlFiles()));
+
+    if (noUsefulOutput(results, outputFiles)) {
+      Optional<TestStatus> status =
+          results.stream().map(BlazeTestResult::getTestStatus).findFirst();
+      status.ifPresent(testStatus -> reportTargetWithoutOutputFiles(label, testStatus));
+      return;
+    }
+
     List<TestSuite> targetSuites = new ArrayList<>();
-    for (File file : files) {
+    for (File file : outputFiles) {
       try (InputStream input = new FileInputStream(file)) {
         targetSuites.add(BlazeXmlSchema.parse(input));
       } catch (Exception e) {
@@ -128,19 +105,71 @@
     if (targetSuites.isEmpty()) {
       return;
     }
+    Kind kind =
+        results
+            .stream()
+            .map(BlazeTestResult::getTargetKind)
+            .filter(Objects::nonNull)
+            .findFirst()
+            .orElse(null);
+    BlazeTestEventsHandler eventsHandler =
+        BlazeTestEventsHandler.getHandlerForTargetKindOrFallback(kind);
     TestSuite suite =
         targetSuites.size() == 1 ? targetSuites.get(0) : BlazeXmlSchema.mergeSuites(targetSuites);
-    processTestSuite(getProcessor(), kind, suite);
+    processTestSuite(getProcessor(), eventsHandler, kind, suite);
   }
 
-  @Nullable
-  private static Kind getKind(Project project, Label label) {
-    TargetIdeInfo target = TargetFinder.getInstance().targetForLabel(project, label);
-    return target != null ? target.kind : null;
+  /** Return false if there's output XML which should be parsed. */
+  private static boolean noUsefulOutput(
+      Collection<BlazeTestResult> results, List<File> outputFiles) {
+    if (outputFiles.isEmpty()) {
+      return true;
+    }
+    TestStatus status =
+        results.stream().map(BlazeTestResult::getTestStatus).findFirst().orElse(null);
+    return status != null && BlazeTestResult.NO_USEFUL_OUTPUT.contains(status);
   }
 
-  private void processTestSuite(
-      GeneralTestEventsProcessor processor, @Nullable Kind kind, TestSuite suite) {
+  /**
+   * If there are no output files, the test may have failed to build, or timed out. Provide a
+   * suitable message in the test UI.
+   */
+  private void reportTargetWithoutOutputFiles(Label label, TestStatus status) {
+    if (status == TestStatus.PASSED) {
+      // Empty test targets do not produce output XML, yet technically pass. Ignore them.
+      return;
+    }
+    GeneralTestEventsProcessor processor = getProcessor();
+    TestSuiteStarted suiteStarted = new TestSuiteStarted(label.toString());
+    processor.onSuiteStarted(new TestSuiteStartedEvent(suiteStarted, /*locationUrl=*/ null));
+    String targetName = label.targetName().toString();
+    processor.onTestStarted(new TestStartedEvent(targetName, /*locationUrl=*/ null));
+    processor.onTestFailure(
+        SmRunnerCompatUtils.getTestFailedEvent(
+            targetName,
+            STATUS_EXPLANATIONS.get(status) + " See console output for details",
+            /*content=*/ null,
+            /*duration=*/ 0));
+    processor.onTestFinished(new TestFinishedEvent(targetName, /*duration=*/ 0L));
+    processor.onSuiteFinished(new TestSuiteFinishedEvent(label.toString()));
+  }
+
+  /** Status explanations for tests without output XML. */
+  private static final ImmutableMap<TestStatus, String> STATUS_EXPLANATIONS =
+      new ImmutableMap.Builder<TestStatus, String>()
+          .put(TestStatus.TIMEOUT, "Test target timed out.")
+          .put(TestStatus.INCOMPLETE, "Test output was incomplete.")
+          .put(TestStatus.REMOTE_FAILURE, "Remote failure during test execution.")
+          .put(TestStatus.FAILED_TO_BUILD, "Test target failed to build.")
+          .put(TestStatus.BLAZE_HALTED_BEFORE_TESTING, "Test target failed to build.")
+          .put(TestStatus.NO_STATUS, "No output found for test target.")
+          .build();
+
+  private static void processTestSuite(
+      GeneralTestEventsProcessor processor,
+      BlazeTestEventsHandler eventsHandler,
+      @Nullable Kind kind,
+      TestSuite suite) {
     if (!hasRunChild(suite)) {
       return;
     }
@@ -154,13 +183,13 @@
     }
 
     for (TestSuite child : suite.testSuites) {
-      processTestSuite(processor, kind, child);
+      processTestSuite(processor, eventsHandler, kind, child);
     }
     for (TestSuite decorator : suite.testDecorators) {
-      processTestSuite(processor, kind, decorator);
+      processTestSuite(processor, eventsHandler, kind, decorator);
     }
     for (TestCase test : suite.testCases) {
-      processTestCase(processor, kind, suite, test);
+      processTestCase(processor, eventsHandler, kind, suite, test);
     }
 
     if (suite.sysOut != null) {
@@ -224,8 +253,12 @@
     return test.failure != null || test.error != null;
   }
 
-  private void processTestCase(
-      GeneralTestEventsProcessor processor, @Nullable Kind kind, TestSuite parent, TestCase test) {
+  private static void processTestCase(
+      GeneralTestEventsProcessor processor,
+      BlazeTestEventsHandler eventsHandler,
+      @Nullable Kind kind,
+      TestSuite parent,
+      TestCase test) {
     if (test.name == null || !wasRun(test) || isCancelled(test)) {
       return;
     }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
index 54f6595..2c42915 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
@@ -16,11 +16,11 @@
 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.DefaultExecutionResult;
 import com.intellij.execution.Executor;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
-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.SMTestRunnerConnectionUtil;
@@ -52,11 +52,11 @@
 
   public static SMTRunnerConsoleView getConsoleView(
       Project project,
-      RunConfiguration configuration,
+      BlazeCommandRunConfiguration configuration,
       Executor executor,
-      BlazeTestEventsHandler eventsHandler) {
+      BlazeTestUiSession testUiSession) {
     SMTRunnerConsoleProperties properties =
-        new BlazeTestConsoleProperties(configuration, executor, eventsHandler);
+        new BlazeTestConsoleProperties(configuration, executor, testUiSession);
     SMTRunnerConsoleView console =
         (SMTRunnerConsoleView)
             SMTestRunnerConnectionUtil.createConsole(BLAZE_FRAMEWORK, properties);
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/TestUiSessionProvider.java b/base/src/com/google/idea/blaze/base/run/smrunner/TestUiSessionProvider.java
new file mode 100644
index 0000000..86b7b91
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/TestUiSessionProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Provides a {@link BlazeTestUiSession} for a given project. */
+public interface TestUiSessionProvider {
+  ExtensionPointName<TestUiSessionProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.TestUiSessionProvider");
+
+  /** Returns a {@link BlazeTestUiSession} for the given project and blaze target. */
+  @Nullable
+  static BlazeTestUiSession createForTarget(Project project, TargetExpression target) {
+    if (!BlazeTestEventsHandler.targetSupported(project, target)) {
+      return null;
+    }
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    return Arrays.stream(EP_NAME.getExtensions())
+        .map(provider -> provider.getTestUiSession(projectData.blazeVersionData))
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse(null);
+  }
+
+  /**
+   * Returns a {@link BlazeTestUiSession}, or {@code null} if this provider doesn't handle the given
+   * project.
+   */
+  @Nullable
+  BlazeTestUiSession getTestUiSession(BlazeVersionData blazeVersion);
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
index 082f4be..267f715 100644
--- a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
@@ -15,9 +15,7 @@
  */
 package com.google.idea.blaze.base.run.state;
 
-import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.run.state.BlazeRunOnDistributedExecutorState.RunOnExecutorStateEditor;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
@@ -37,15 +35,18 @@
   private final RunConfigurationFlagsState blazeFlags;
   private final RunConfigurationFlagsState exeFlags;
   private final BlazeBinaryState blazeBinary;
-  private final BlazeRunOnDistributedExecutorState runOnDistributedExecutor;
 
   public BlazeCommandRunConfigurationCommonState(BuildSystem buildSystem) {
     command = new BlazeCommandState();
     blazeFlags = new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystem + " flags:");
     exeFlags = new RunConfigurationFlagsState(USER_EXE_FLAG_TAG, "Executable flags:");
     blazeBinary = new BlazeBinaryState();
-    runOnDistributedExecutor = new BlazeRunOnDistributedExecutorState(buildSystem);
-    addStates(command, blazeFlags, exeFlags, blazeBinary, runOnDistributedExecutor);
+    addStates(command, blazeFlags, exeFlags, blazeBinary);
+
+    // no need to migrate Bazel, which at this time doesn't support distributed execution
+    if (buildSystem == BuildSystem.Blaze) {
+      addStates(new BlazeRunOnDistributedExecutorStateMigrator(buildSystem, blazeFlags));
+    }
   }
 
   /** @return The list of blaze flags that the user specified manually. */
@@ -66,10 +67,6 @@
     return command;
   }
 
-  public BlazeRunOnDistributedExecutorState getRunOnDistributedExecutorState() {
-    return runOnDistributedExecutor;
-  }
-
   /** Searches through all blaze flags for the first one beginning with '--test_filter' */
   @Nullable
   public String getTestFilterFlag() {
@@ -93,29 +90,6 @@
 
   @Override
   public RunConfigurationStateEditor getEditor(Project project) {
-    return new RunConfigurationCompositeStateEditor(project, getStates()) {
-
-      @Nullable
-      private final RunOnExecutorStateEditor runOnExecutorEditor =
-          (RunOnExecutorStateEditor)
-              editors
-                  .stream()
-                  .filter(editor -> editor instanceof RunOnExecutorStateEditor)
-                  .findFirst()
-                  .orElse(null);
-
-      @Override
-      public void applyEditorTo(RunConfigurationState genericState) {
-        BlazeCommandRunConfigurationCommonState state =
-            (BlazeCommandRunConfigurationCommonState) genericState;
-        super.applyEditorTo(genericState);
-
-        // this editor needs to update based on state provided by other children.
-        if (runOnExecutorEditor != null) {
-          boolean isTest = BlazeCommandName.TEST.equals(state.getCommandState().getCommand());
-          runOnExecutorEditor.updateVisibility(isTest);
-        }
-      }
-    };
+    return new RunConfigurationCompositeStateEditor(project, getStates());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java
deleted file mode 100644
index 556b3f3..0000000
--- a/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java
+++ /dev/null
@@ -1,124 +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.run.state;
-
-import com.google.idea.blaze.base.run.DistributedExecutorSupport;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.blaze.base.ui.UiUtil;
-import com.intellij.icons.AllIcons;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.WriteExternalException;
-import com.intellij.ui.components.JBCheckBox;
-import javax.annotation.Nullable;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import org.jdom.Element;
-
-/**
- * Provides an option to run blaze/bazel on a distributed executor, if available. If unchecked, we
- * fall back to whatever the default is.
- */
-public class BlazeRunOnDistributedExecutorState implements RunConfigurationState {
-
-  private static final String RUN_ON_DISTRIBUTED_EXECUTOR_ATTR =
-      "blaze-run-on-distributed-executor";
-
-  @Nullable private final DistributedExecutorSupport executorInfo;
-
-  public boolean runOnDistributedExecutor;
-
-  BlazeRunOnDistributedExecutorState(BuildSystem buildSystem) {
-    executorInfo = DistributedExecutorSupport.getAvailableExecutor(buildSystem);
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    String string = element.getAttributeValue(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
-    if (string != null) {
-      runOnDistributedExecutor = Boolean.parseBoolean(string);
-    }
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    if (executorInfo != null && runOnDistributedExecutor) {
-      element.setAttribute(
-          RUN_ON_DISTRIBUTED_EXECUTOR_ATTR, Boolean.toString(runOnDistributedExecutor));
-    } else {
-      element.removeAttribute(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
-    }
-  }
-
-  @Override
-  public RunOnExecutorStateEditor getEditor(Project project) {
-    return new RunOnExecutorStateEditor();
-  }
-
-  /** Editor for {@link BlazeRunOnDistributedExecutorState} */
-  class RunOnExecutorStateEditor implements RunConfigurationStateEditor {
-
-    private final String executorName =
-        executorInfo != null ? executorInfo.executorName() : "distributed executor";
-    private final JBCheckBox checkBox = new JBCheckBox("Run on " + executorName);
-    private final JLabel warning =
-        new JLabel("Warning: test UI integration is not available when running on " + executorName);
-
-    private boolean componentVisible = executorInfo != null;
-    private boolean isTest = false;
-
-    private RunOnExecutorStateEditor() {
-      warning.setIcon(AllIcons.RunConfigurations.ConfigurationWarning);
-      checkBox.addItemListener(e -> setVisibility());
-      setVisibility();
-    }
-
-    @Override
-    public void resetEditorFrom(RunConfigurationState genericState) {
-      BlazeRunOnDistributedExecutorState state = (BlazeRunOnDistributedExecutorState) genericState;
-      checkBox.setSelected(state.runOnDistributedExecutor);
-    }
-
-    @Override
-    public void applyEditorTo(RunConfigurationState genericState) {
-      BlazeRunOnDistributedExecutorState state = (BlazeRunOnDistributedExecutorState) genericState;
-      if (checkBox.isVisible()) {
-        state.runOnDistributedExecutor = checkBox.isSelected();
-      }
-    }
-
-    @Override
-    public JComponent createComponent() {
-      return UiUtil.createBox(checkBox, warning);
-    }
-
-    @Override
-    public void setComponentEnabled(boolean enabled) {
-      checkBox.setEnabled(enabled);
-    }
-
-    void updateVisibility(boolean isTest) {
-      this.componentVisible = executorInfo != null;
-      this.isTest = isTest;
-      setVisibility();
-    }
-
-    private void setVisibility() {
-      warning.setVisible(componentVisible && isTest && checkBox.isSelected());
-      checkBox.setVisible(componentVisible);
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorStateMigrator.java b/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorStateMigrator.java
new file mode 100644
index 0000000..610dce4
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorStateMigrator.java
@@ -0,0 +1,106 @@
+/*
+ * 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.run.state;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.jdom.Element;
+
+/**
+ * A temporary class to handle migrating from the previous run configuration settings.
+ *
+ * <p>In particular, migrates from setting the local execution flag at runtime to including it in
+ * the user-visible blaze flags UI field.
+ *
+ * <p>TODO(brendandouglas): Temporary migration code. Remove in 2017.09.XX+
+ */
+public class BlazeRunOnDistributedExecutorStateMigrator implements RunConfigurationState {
+
+  private static final String RUN_ON_DISTRIBUTED_EXECUTOR_ATTR =
+      "blaze-run-on-distributed-executor";
+
+  private static final String ALREADY_MIGRATED_ATTR = "blaze-dist-executor-migrated";
+
+  private final RunConfigurationFlagsState blazeFlags;
+  @Nullable private final DistributedExecutorSupport executorInfo;
+
+  @VisibleForTesting
+  public BlazeRunOnDistributedExecutorStateMigrator(
+      BuildSystem buildSystem, RunConfigurationFlagsState blazeFlags) {
+    this.blazeFlags = blazeFlags;
+    executorInfo = DistributedExecutorSupport.getAvailableExecutor(buildSystem);
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    if (Boolean.parseBoolean(element.getAttributeValue(ALREADY_MIGRATED_ATTR))) {
+      return;
+    }
+    String string = element.getAttributeValue(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
+    boolean runOnDistributedExecutor = Boolean.parseBoolean(string);
+    if (runOnDistributedExecutor) {
+      setDistributedTestExecution();
+    }
+  }
+
+  private void setDistributedTestExecution() {
+    if (executorInfo == null) {
+      return;
+    }
+    List<String> flags = new ArrayList<>(blazeFlags.getRawFlags());
+    for (String flag : executorInfo.getBlazeFlags(true)) {
+      if (!flags.contains(flag)) {
+        flags.add(flag);
+      }
+    }
+    blazeFlags.setRawFlags(flags);
+  }
+
+  @Override
+  public void writeExternal(Element element) {
+    element.removeAttribute(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
+    element.setAttribute(ALREADY_MIGRATED_ATTR, Boolean.toString(true));
+  }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    // a dummy implementation of RunConfigurationStateEditor.
+    return new RunConfigurationStateEditor() {
+
+      @Override
+      public void resetEditorFrom(RunConfigurationState state) {}
+
+      @Override
+      public void applyEditorTo(RunConfigurationState state) {}
+
+      @Override
+      public JComponent createComponent() {
+        return new JPanel();
+      }
+
+      @Override
+      public void setComponentEnabled(boolean enabled) {}
+    };
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
index 9cb8a3c..63f6de0 100644
--- a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
@@ -86,7 +86,7 @@
 
   private static class RunConfigurationFlagsStateEditor implements RunConfigurationStateEditor {
 
-    private final JTextArea flagsField = new JTextArea(5, 1);
+    private final JTextArea flagsField = new JTextArea(10, 1);
     private final String fieldLabel;
 
     RunConfigurationFlagsStateEditor(String fieldLabel) {
diff --git a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
index 23a03a2..37cf1ee 100644
--- a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
+++ b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
@@ -15,16 +15,13 @@
  */
 package com.google.idea.blaze.base.run.targetfinder;
 
-import com.google.common.base.Predicate;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 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.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
-import java.util.Arrays;
 import java.util.List;
+import java.util.function.Predicate;
 import javax.annotation.Nullable;
 
 /** Searches BlazeProjectData for matching rules. */
@@ -38,22 +35,9 @@
     return findTarget(project, target -> target.key.label.equals(label) && target.isPlainTarget());
   }
 
-  public ImmutableList<TargetIdeInfo> targetsOfKinds(Project project, final Kind... kinds) {
-    return targetsOfKinds(project, Arrays.asList(kinds));
-  }
-
-  public ImmutableList<TargetIdeInfo> targetsOfKinds(Project project, final List<Kind> kinds) {
-    return ImmutableList.copyOf(findTargets(project, target -> target.kindIsOneOf(kinds)));
-  }
-
   @Nullable
-  public TargetIdeInfo firstTargetOfKinds(Project project, Kind... kinds) {
-    return Iterables.getFirst(targetsOfKinds(project, kinds), null);
-  }
-
-  @Nullable
-  public TargetIdeInfo firstTargetOfKinds(Project project, List<Kind> kinds) {
-    return Iterables.getFirst(targetsOfKinds(project, kinds), null);
+  public TargetIdeInfo findFirstTarget(Project project, Predicate<TargetIdeInfo> predicate) {
+    return Iterables.getFirst(findTargets(project, predicate), null);
   }
 
   @Nullable
@@ -63,11 +47,6 @@
     return Iterables.getFirst(results, null);
   }
 
-  @Nullable
-  public TargetIdeInfo findFirstTarget(Project project, Predicate<TargetIdeInfo> predicate) {
-    return Iterables.getFirst(findTargets(project, predicate), null);
-  }
-
   public abstract List<TargetIdeInfo> findTargets(
       Project project, Predicate<TargetIdeInfo> predicate);
 }
diff --git a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinderImpl.java b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinderImpl.java
index 46f58c2..02e7e43 100644
--- a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinderImpl.java
+++ b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinderImpl.java
@@ -15,13 +15,13 @@
  */
 package com.google.idea.blaze.base.run.targetfinder;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.intellij.openapi.project.Project;
 import java.util.List;
+import java.util.function.Predicate;
 import org.jetbrains.annotations.NotNull;
 
 /** Implementation of RuleFinder. */
@@ -37,7 +37,7 @@
 
     ImmutableList.Builder<TargetIdeInfo> resultList = ImmutableList.builder();
     for (TargetIdeInfo target : projectData.targetMap.targets()) {
-      if (predicate.apply(target)) {
+      if (predicate.test(target)) {
         resultList.add(target);
       }
     }
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
index 6920313..06f5291 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
@@ -52,7 +52,7 @@
   /** Finds the targets which failed to build */
   public static ImmutableSet<Label> parseFailedTargets(File commandLog) {
     try (Stream<String> stream = Files.lines(Paths.get(commandLog.getPath()))) {
-      return parseTestTargets(stream);
+      return parseFailedTargets(stream);
     } catch (IOException e) {
       logger.warn("Error parsing master log", e);
       return ImmutableSet.of();
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResult.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResult.java
new file mode 100644
index 0000000..1ebe080
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResult.java
@@ -0,0 +1,66 @@
+/*
+ * 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.testlogs;
+
+import com.google.auto.value.AutoValue;
+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 java.io.File;
+import javax.annotation.Nullable;
+
+/** The result of a single blaze test action. */
+@AutoValue
+public abstract class BlazeTestResult {
+
+  /** The set of statuses for which no useful output XML is written. */
+  public static final ImmutableSet<TestStatus> NO_USEFUL_OUTPUT =
+      ImmutableSet.of(
+          TestStatus.TIMEOUT,
+          TestStatus.REMOTE_FAILURE,
+          TestStatus.FAILED_TO_BUILD,
+          TestStatus.BLAZE_HALTED_BEFORE_TESTING);
+
+  /** Status for a single blaze test action. */
+  public enum TestStatus {
+    NO_STATUS,
+    PASSED,
+    FLAKY,
+    TIMEOUT,
+    FAILED,
+    INCOMPLETE,
+    REMOTE_FAILURE,
+    FAILED_TO_BUILD,
+    BLAZE_HALTED_BEFORE_TESTING,
+  }
+
+  public static BlazeTestResult create(
+      Label label,
+      @Nullable Kind targetKind,
+      TestStatus testStatus,
+      ImmutableSet<File> outputXmlFiles) {
+    return new AutoValue_BlazeTestResult(label, targetKind, testStatus, outputXmlFiles);
+  }
+
+  public abstract Label getLabel();
+
+  @Nullable
+  public abstract Kind getTargetKind();
+
+  public abstract TestStatus getTestStatus();
+
+  public abstract ImmutableSet<File> getOutputXmlFiles();
+}
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java
index cff8bd4..7270f54 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java
@@ -5,7 +5,7 @@
  * 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
+ *   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,
@@ -15,40 +15,15 @@
  */
 package com.google.idea.blaze.base.run.testlogs;
 
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.extensions.ExtensionPointName;
-import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
 
 /** A strategy for locating results from 'blaze test' invocation (e.g. output XML files). */
 public interface BlazeTestResultFinderStrategy {
 
-  ExtensionPointName<BlazeTestResultFinderStrategy> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.BlazeTestXmlFinderStrategy");
-
-  /**
-   * Attempt to find all output test XML files produced by the most recent blaze invocation, grouped
-   * by target label.
-   */
-  @Nullable
-  static BlazeTestResults locateTestResults(Project project) {
-    BuildSystem buildSystem = Blaze.getBuildSystem(project);
-    for (BlazeTestResultFinderStrategy strategy : EP_NAME.getExtensions()) {
-      if (strategy.handlesBuildSystem(buildSystem)) {
-        return strategy.findTestResults(project);
-      }
-    }
-    return null;
-  }
-
   /**
    * Attempt to find test results corresponding to the most recent blaze invocation. Called after
    * the 'blaze test' process completes.
    */
   @Nullable
-  BlazeTestResults findTestResults(Project project);
-
-  /** Results are taken from the first strategy handling a given build system */
-  boolean handlesBuildSystem(BuildSystem buildSystem);
+  BlazeTestResults findTestResults();
 }
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java
index 7d50bf2..091014e 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java
@@ -15,22 +15,24 @@
  */
 package com.google.idea.blaze.base.run.testlogs;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.primitives.Label;
-import java.io.File;
 
 /** Results from a 'blaze test' invocation. */
 public class BlazeTestResults {
 
-  /** Output test XML files, grouped by target label. */
-  public final ImmutableMultimap<Label, File> testXmlFiles;
-  /** Targets which failed to build */
-  public final ImmutableSet<Label> failedTargets;
+  public static final BlazeTestResults NO_RESULTS = fromFlatList(ImmutableList.of());
 
-  public BlazeTestResults(
-      ImmutableMultimap<Label, File> testXmlFiles, ImmutableSet<Label> failedTargets) {
-    this.testXmlFiles = testXmlFiles;
-    this.failedTargets = failedTargets;
+  public static BlazeTestResults fromFlatList(Iterable<BlazeTestResult> results) {
+    ImmutableMultimap.Builder<Label, BlazeTestResult> map = ImmutableMultimap.builder();
+    results.forEach(result -> map.put(result.getLabel(), result));
+    return new BlazeTestResults(map.build());
+  }
+
+  public final ImmutableMultimap<Label, BlazeTestResult> perTargetResults;
+
+  private BlazeTestResults(ImmutableMultimap<Label, BlazeTestResult> perTargetResults) {
+    this.perTargetResults = perTargetResults;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategy.java
new file mode 100644
index 0000000..e6e37eb
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategy.java
@@ -0,0 +1,56 @@
+/*
+ * 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.testlogs;
+
+import com.google.idea.blaze.base.command.buildresult.BuildEventProtocolOutputReader;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A strategy for locating results from a single 'blaze test' invocation (e.g. output XML files).
+ *
+ * <p>Parses the output BEP proto written by blaze to locate the test XML files.
+ */
+public final class BuildEventProtocolTestFinderStrategy implements BlazeTestResultFinderStrategy {
+
+  private static final Logger logger =
+      Logger.getInstance(BuildEventProtocolTestFinderStrategy.class);
+
+  private final File outputFile;
+
+  public BuildEventProtocolTestFinderStrategy(File bepOutputFile) {
+    this.outputFile = bepOutputFile;
+  }
+
+  @Override
+  public BlazeTestResults findTestResults() {
+    try (InputStream inputStream =
+        new BufferedInputStream(InputStreamProvider.getInstance().getFile(outputFile))) {
+      return BuildEventProtocolOutputReader.parseTestResults(inputStream);
+    } catch (IOException e) {
+      logger.warn(e);
+      return BlazeTestResults.NO_RESULTS;
+    } finally {
+      if (!outputFile.delete()) {
+        logger.warn("Could not delete BEP output file: " + outputFile);
+      }
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java
deleted file mode 100644
index c18cc65..0000000
--- a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java
+++ /dev/null
@@ -1,89 +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.testlogs;
-
-import com.google.common.collect.ImmutableMultimap;
-import com.google.idea.blaze.base.command.info.BlazeInfo;
-import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import javax.annotation.Nullable;
-
-/**
- * Attempts to parse the list of test targets from the command log, then searches the corresponding
- * path in the bazel-testlogs output tree.
- */
-public class TargetPathTestResultFinderStrategy implements BlazeTestResultFinderStrategy {
-
-  @Override
-  public boolean handlesBuildSystem(BuildSystem buildSystem) {
-    return buildSystem == BuildSystem.Bazel;
-  }
-
-  @Override
-  public BlazeTestResults findTestResults(Project project) {
-    File testLogsDir = getTestLogsTree(project);
-    if (testLogsDir == null) {
-      return null;
-    }
-    File commandLog = getCommandLog(project);
-    if (commandLog == null) {
-      return null;
-    }
-    ImmutableMultimap.Builder<Label, File> output = ImmutableMultimap.builder();
-    for (Label label : BlazeCommandLogParser.parseTestTargets(commandLog)) {
-      File testXml = findTestXml(testLogsDir, label);
-      if (testXml != null) {
-        output.put(label, testXml);
-      }
-    }
-    return new BlazeTestResults(
-        output.build(), BlazeCommandLogParser.parseFailedTargets(commandLog));
-  }
-
-  @Nullable
-  private static File findTestXml(File testLogsDir, Label label) {
-    String labelPath = label.blazePackage() + File.separator + label.targetName();
-    return new File(testLogsDir, labelPath + File.separator + "test.xml");
-  }
-
-  @Nullable
-  private static File getTestLogsTree(Project project) {
-    BlazeProjectData projectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (projectData == null) {
-      return null;
-    }
-    String testLogsLocation =
-        projectData.blazeInfo.get(BlazeInfo.blazeTestlogsKey(Blaze.getBuildSystem(project)));
-    return testLogsLocation != null ? new File(testLogsLocation) : null;
-  }
-
-  @Nullable
-  private static File getCommandLog(Project project) {
-    BlazeProjectData projectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (projectData == null) {
-      return null;
-    }
-    String commandLogLocation = projectData.blazeInfo.get(BlazeInfo.COMMAND_LOG);
-    return commandLogLocation != null ? new File(commandLogLocation) : null;
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
index 3a74582..d2027f3 100644
--- a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
+++ b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
@@ -73,6 +73,8 @@
             Kind.GWT_TEST,
             Kind.CC_TEST,
             Kind.PY_TEST,
-            Kind.GO_TEST);
+            Kind.GO_TEST,
+            Kind.SCALA_TEST,
+            Kind.SCALA_JUNIT_TEST);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/scope/Scope.java b/base/src/com/google/idea/blaze/base/scope/Scope.java
index e288d8a..26104fb 100644
--- a/base/src/com/google/idea/blaze/base/scope/Scope.java
+++ b/base/src/com/google/idea/blaze/base/scope/Scope.java
@@ -38,7 +38,7 @@
     } catch (ProcessCanceledException e) {
       context.setCancelled();
       throw e;
-    } catch (RuntimeException e) {
+    } catch (Throwable e) {
       context.setHasError();
       logger.error(e);
       throw e;
@@ -61,7 +61,7 @@
     } catch (ProcessCanceledException e) {
       context.setCancelled();
       throw e;
-    } catch (RuntimeException e) {
+    } catch (Throwable e) {
       context.setHasError();
       logger.error(e);
       throw e;
diff --git a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
index 8690be2..f9fdd98 100644
--- a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
+++ b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
@@ -20,7 +20,6 @@
 import com.intellij.pom.Navigatable;
 import java.io.File;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
 /** An issue in a blaze operation. */
 public class IssueOutput implements Output {
@@ -31,8 +30,8 @@
   @Nullable private final File file;
   private final int line;
   private final int column;
-  @NotNull private final Category category;
-  @NotNull private final String message;
+  private final Category category;
+  private final String message;
   @Nullable Navigatable navigatable;
   @Nullable IssueData issueData;
 
@@ -47,61 +46,53 @@
     INFORMATION
   }
 
-  @NotNull
-  public static Builder issue(@NotNull Category category, @NotNull String message) {
+  public static Builder issue(Category category, String message) {
     return new Builder(category, message);
   }
 
-  @NotNull
-  public static Builder error(@NotNull String message) {
+  public static Builder error(String message) {
     return new Builder(Category.ERROR, message);
   }
 
-  @NotNull
-  public static Builder warn(@NotNull String message) {
+  public static Builder warn(String message) {
     return new Builder(Category.WARNING, message);
   }
 
   /** Builder for an issue */
   public static class Builder {
-    @NotNull private final Category category;
-    @NotNull private final String message;
+    private final Category category;
+    private final String message;
     @Nullable private File file;
     private int line = NO_LINE;
     private int column = NO_COLUMN;
     @Nullable Navigatable navigatable;
     @Nullable IssueData issueData;
 
-    public Builder(@NotNull Category category, @NotNull String message) {
+    public Builder(Category category, String message) {
       this.category = category;
       this.message = message;
     }
 
-    @NotNull
     public Builder inFile(@Nullable File file) {
       this.file = file;
       return this;
     }
 
-    @NotNull
     public Builder onLine(int line) {
       this.line = line;
       return this;
     }
 
-    @NotNull
     public Builder inColumn(int column) {
       this.column = column;
       return this;
     }
 
-    @NotNull
     public Builder withData(@Nullable IssueData issueData) {
       this.issueData = issueData;
       return this;
     }
 
-    @NotNull
     public Builder navigatable(@Nullable Navigatable navigatable) {
       this.navigatable = navigatable;
       return this;
@@ -111,7 +102,7 @@
       return new IssueOutput(file, line, column, navigatable, category, message, issueData);
     }
 
-    public void submit(@NotNull BlazeContext context) {
+    public void submit(BlazeContext context) {
       context.output(build());
       if (category == Category.ERROR) {
         context.setHasError();
@@ -124,8 +115,8 @@
       int line,
       int column,
       @Nullable Navigatable navigatable,
-      @NotNull Category category,
-      @NotNull String message,
+      Category category,
+      String message,
       @Nullable IssueData issueData) {
     this.file = file;
     this.line = line;
@@ -154,12 +145,10 @@
     return navigatable;
   }
 
-  @NotNull
   public Category getCategory() {
     return category;
   }
 
-  @NotNull
   public String getMessage() {
     return message;
   }
diff --git a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
index 7128856..2b9c295 100644
--- a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
+++ b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
@@ -24,12 +24,12 @@
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.output.PrintOutput.OutputType;
 import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
 import com.intellij.execution.ui.ConsoleViewContentType;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
 /** Moves print output to the blaze console. */
 public class BlazeConsoleScope implements BlazeScope {
@@ -38,20 +38,20 @@
   public static class Builder {
     private Project project;
     private ProgressIndicator progressIndicator;
-    private boolean suppressConsole = false;
+    private BlazeConsolePopupBehavior popupBehavior;
     private boolean escapeAnsiColorCodes = false;
 
-    public Builder(@NotNull Project project) {
+    public Builder(Project project) {
       this(project, null);
     }
 
-    public Builder(@NotNull Project project, ProgressIndicator progressIndicator) {
+    public Builder(Project project, ProgressIndicator progressIndicator) {
       this.project = project;
       this.progressIndicator = progressIndicator;
     }
 
-    public Builder setSuppressConsole(boolean suppressConsole) {
-      this.suppressConsole = suppressConsole;
+    public Builder setPopupBehavior(BlazeConsolePopupBehavior popupBehavior) {
+      this.popupBehavior = popupBehavior;
       return this;
     }
 
@@ -61,24 +61,23 @@
     }
 
     public BlazeConsoleScope build() {
-      return new BlazeConsoleScope(
-          project, progressIndicator, suppressConsole, escapeAnsiColorCodes);
+      return new BlazeConsoleScope(project, progressIndicator, popupBehavior, escapeAnsiColorCodes);
     }
   }
 
-  @NotNull private final BlazeConsoleService blazeConsoleService;
+  private final BlazeConsoleService blazeConsoleService;
 
   @Nullable private final ProgressIndicator progressIndicator;
 
-  private final boolean showDialogOnChange;
+  private final BlazeConsolePopupBehavior popupBehavior;
   private boolean activated;
 
   private final ConsoleStream consoleStream;
 
   private OutputSink<PrintOutput> printSink =
       (output) -> {
-        @NotNull String text = output.getText();
-        @NotNull
+        String text = output.getText();
+
         ConsoleViewContentType contentType =
             output.getOutputType() == OutputType.ERROR
                 ? ConsoleViewContentType.ERROR_OUTPUT
@@ -89,20 +88,20 @@
 
   private OutputSink<StatusOutput> statusSink =
       (output) -> {
-        @NotNull String text = output.getStatus();
-        @NotNull ConsoleViewContentType contentType = ConsoleViewContentType.NORMAL_OUTPUT;
+        String text = output.getStatus();
+        ConsoleViewContentType contentType = ConsoleViewContentType.NORMAL_OUTPUT;
         print(text, contentType);
         return OutputSink.Propagation.Continue;
       };
 
   private BlazeConsoleScope(
-      @NotNull Project project,
+      Project project,
       @Nullable ProgressIndicator progressIndicator,
-      boolean suppressConsole,
+      BlazeConsolePopupBehavior popupBehavior,
       boolean escapeAnsiColorCodes) {
     this.blazeConsoleService = BlazeConsoleService.getInstance(project);
     this.progressIndicator = progressIndicator;
-    this.showDialogOnChange = !suppressConsole;
+    this.popupBehavior = popupBehavior;
     ConsoleStream sinkConsoleStream = blazeConsoleService::print;
     this.consoleStream =
         escapeAnsiColorCodes ? new ColoredConsoleStream(sinkConsoleStream) : sinkConsoleStream;
@@ -112,14 +111,21 @@
     consoleStream.print(text, contentType);
     consoleStream.print("\n", contentType);
 
-    if (showDialogOnChange && !activated) {
+    if (activated) {
+      return;
+    }
+    boolean activate =
+        popupBehavior == BlazeConsolePopupBehavior.ALWAYS
+            || (popupBehavior == BlazeConsolePopupBehavior.ON_ERROR
+                && contentType == ConsoleViewContentType.ERROR_OUTPUT);
+    if (activate) {
       activated = true;
       ApplicationManager.getApplication().invokeLater(blazeConsoleService::activateConsoleWindow);
     }
   }
 
   @Override
-  public void onScopeBegin(@NotNull final BlazeContext context) {
+  public void onScopeBegin(final BlazeContext context) {
     context.addOutputSink(PrintOutput.class, printSink);
     context.addOutputSink(StatusOutput.class, statusSink);
     blazeConsoleService.clear();
@@ -133,7 +139,7 @@
   }
 
   @Override
-  public void onScopeEnd(@NotNull BlazeContext context) {
+  public void onScopeEnd(BlazeContext context) {
     blazeConsoleService.setStopHandler(null);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/settings/Blaze.java b/base/src/com/google/idea/blaze/base/settings/Blaze.java
index 65d5b93..86954db 100644
--- a/base/src/com/google/idea/blaze/base/settings/Blaze.java
+++ b/base/src/com/google/idea/blaze/base/settings/Blaze.java
@@ -35,7 +35,7 @@
       return name();
     }
 
-    /** The build system name, capitalized. */
+    /** The build system name, lower case. */
     public String getLowerCaseName() {
       return name().toLowerCase();
     }
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java b/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
index 6b7298e..e106aa9 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
@@ -36,13 +36,30 @@
 )
 public class BlazeUserSettings implements PersistentStateComponent<BlazeUserSettings> {
 
+  /** A setting to control whether the Blaze Console view is activated for a given operation. */
+  public enum BlazeConsolePopupBehavior {
+    ALWAYS("Always"),
+    ON_ERROR("On error"),
+    NEVER("Never");
+
+    private final String uiName;
+
+    BlazeConsolePopupBehavior(String uiName) {
+      this.uiName = uiName;
+    }
+
+    @Override
+    public String toString() {
+      return uiName;
+    }
+  }
+
+  private BlazeConsolePopupBehavior showBlazeConsoleOnSync = BlazeConsolePopupBehavior.ALWAYS;
   private boolean suppressConsoleForRunAction = false;
   private boolean resyncAutomatically = false;
   private boolean syncStatusPopupShown = false;
   private boolean expandSyncToWorkingSet = true;
   private boolean showPerformanceWarnings = false;
-  private boolean attachSourcesByDefault = false;
-  private boolean attachSourcesOnDemand = false;
   private boolean collapseProjectView = true;
   private boolean formatBuildFilesOnSave = true;
   private String blazeBinaryPath = "/usr/bin/blaze";
@@ -85,6 +102,14 @@
     return resyncAutomatically;
   }
 
+  public BlazeConsolePopupBehavior getShowBlazeConsoleOnSync() {
+    return showBlazeConsoleOnSync;
+  }
+
+  public void setShowBlazeConsoleOnSync(BlazeConsolePopupBehavior showBlazeConsoleOnSync) {
+    this.showBlazeConsoleOnSync = showBlazeConsoleOnSync;
+  }
+
   public boolean getSuppressConsoleForRunAction() {
     return suppressConsoleForRunAction;
   }
@@ -149,32 +174,4 @@
   public void setFormatBuildFilesOnSave(boolean formatBuildFilesOnSave) {
     this.formatBuildFilesOnSave = formatBuildFilesOnSave;
   }
-
-  // Deprecated -- use BlazeJavaUserSettings
-  @Deprecated
-  @SuppressWarnings("unused") // Used by bean serialization
-  public boolean getAttachSourcesByDefault() {
-    return attachSourcesByDefault;
-  }
-
-  // Deprecated -- use BlazeJavaUserSettings
-  @Deprecated
-  @SuppressWarnings("unused") // Used by bean serialization
-  public void setAttachSourcesByDefault(boolean attachSourcesByDefault) {
-    this.attachSourcesByDefault = attachSourcesByDefault;
-  }
-
-  // Deprecated -- use BlazeJavaUserSettings
-  @Deprecated
-  @SuppressWarnings("unused") // Used by bean serialization
-  public boolean getAttachSourcesOnDemand() {
-    return attachSourcesOnDemand;
-  }
-
-  // Deprecated -- use BlazeJavaUserSettings
-  @Deprecated
-  @SuppressWarnings("unused") // Used by bean serialization
-  public void setAttachSourcesOnDemand(boolean attachSourcesOnDemand) {
-    this.attachSourcesOnDemand = attachSourcesOnDemand;
-  }
 }
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java b/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
index f8a7210..2b0514d 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
@@ -22,6 +22,7 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
 import com.google.idea.blaze.base.ui.FileSelectorWithStoredHistory;
 import com.intellij.openapi.options.BaseConfigurable;
 import com.intellij.openapi.options.ConfigurationException;
@@ -33,6 +34,7 @@
 import java.util.Collection;
 import javax.annotation.Nullable;
 import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
@@ -49,6 +51,7 @@
   private final Collection<BlazeUserSettingsContributor> settingsContributors;
 
   private JPanel myMainPanel;
+  private JComboBox<BlazeConsolePopupBehavior> showBlazeConsoleOnSync;
   private JCheckBox suppressConsoleForRunAction;
   private JCheckBox resyncAutomatically;
   private JCheckBox collapseProjectView;
@@ -81,6 +84,8 @@
   @Override
   public void apply() throws ConfigurationException {
     BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    settings.setShowBlazeConsoleOnSync(
+        (BlazeConsolePopupBehavior) showBlazeConsoleOnSync.getSelectedItem());
     settings.setSuppressConsoleForRunAction(suppressConsoleForRunAction.isSelected());
     settings.setResyncAutomatically(resyncAutomatically.isSelected());
     settings.setCollapseProjectView(collapseProjectView.isSelected());
@@ -96,6 +101,7 @@
   @Override
   public void reset() {
     BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    showBlazeConsoleOnSync.setSelectedItem(settings.getShowBlazeConsoleOnSync());
     suppressConsoleForRunAction.setSelected(settings.getSuppressConsoleForRunAction());
     resyncAutomatically.setSelected(settings.getResyncAutomatically());
     collapseProjectView.setSelected(settings.getCollapseProjectView());
@@ -118,7 +124,8 @@
   public boolean isModified() {
     BlazeUserSettings settings = BlazeUserSettings.getInstance();
     boolean isModified =
-        suppressConsoleForRunAction.isSelected() != settings.getSuppressConsoleForRunAction()
+        showBlazeConsoleOnSync.getSelectedItem() != settings.getShowBlazeConsoleOnSync()
+            || suppressConsoleForRunAction.isSelected() != settings.getSuppressConsoleForRunAction()
             || resyncAutomatically.isSelected() != settings.getResyncAutomatically()
             || collapseProjectView.isSelected() != settings.getCollapseProjectView()
             || formatBuildFilesOnSave.isSelected() != settings.getFormatBuildFilesOnSave()
@@ -156,11 +163,46 @@
       contributorRowCount += contributor.getRowCount();
     }
 
-    final int totalRowSize = 7 + contributorRowCount;
+    final int totalRowSize = 8 + contributorRowCount;
     int rowi = 0;
 
     myMainPanel = new JPanel();
     myMainPanel.setLayout(new GridLayoutManager(totalRowSize, 2, new Insets(0, 0, 0, 0), -1, -1));
+
+    JLabel label = new JLabel(String.format("Show %s console on sync:", defaultBuildSystem));
+    myMainPanel.add(
+        label,
+        new GridConstraints(
+            rowi,
+            0,
+            1,
+            1,
+            GridConstraints.ANCHOR_WEST,
+            GridConstraints.FILL_NONE,
+            GridConstraints.SIZEPOLICY_FIXED,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            0,
+            false));
+    showBlazeConsoleOnSync = new JComboBox<>(BlazeConsolePopupBehavior.values());
+    myMainPanel.add(
+        showBlazeConsoleOnSync,
+        new GridConstraints(
+            rowi++,
+            1,
+            1,
+            1,
+            GridConstraints.ANCHOR_WEST,
+            GridConstraints.FILL_HORIZONTAL,
+            GridConstraints.SIZEPOLICY_CAN_GROW,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            0,
+            false));
     suppressConsoleForRunAction = new JCheckBox();
     suppressConsoleForRunAction.setText(
         String.format("Suppress %s console for Run/Debug actions", defaultBuildSystem));
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
index ae6a917..0172f69 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
@@ -93,10 +93,16 @@
     return ProjectManager.getInstance().getDefaultProject();
   }
 
-  public static Dimension getMinimumSize() {
+  private static Dimension getEditorSize() {
     return new Dimension(1000, 550);
   }
 
+  public static Dimension getContainerSize() {
+    // Add pixels so we have room for our extra fields
+    Dimension dimension = getEditorSize();
+    return new Dimension(dimension.width, dimension.height + 200);
+  }
+
   private static EditorEx createEditor(String tooltip) {
     Project project = getProject();
     LightVirtualFile virtualFile =
@@ -125,8 +131,8 @@
     settings.setFoldingOutlineShown(false);
     settings.setRightMarginShown(false);
     settings.setAdditionalPageAtBottom(false);
-    editor.getComponent().setMinimumSize(getMinimumSize());
-    editor.getComponent().setPreferredSize(getMinimumSize());
+    editor.getComponent().setMinimumSize(getEditorSize());
+    editor.getComponent().setPreferredSize(getEditorSize());
     editor.getComponent().setToolTipText(tooltip);
     editor.getComponent().setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null);
     editor.getComponent().setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null);
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
index a019ffd..d5858f9 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -24,7 +24,10 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+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.BlazeConfigurationHandler;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
@@ -41,6 +44,7 @@
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.plugin.BuildSystemVersionChecker;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -82,7 +86,7 @@
 import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder;
 import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder.ShardedTargetsResult;
 import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
-import com.google.idea.blaze.base.sync.sharding.SuggestEnablingShardingNotification;
+import com.google.idea.blaze.base.sync.sharding.SuggestBuildShardingNotification;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
@@ -146,7 +150,11 @@
 
           if (!syncParams.backgroundSync) {
             context
-                .push(new BlazeConsoleScope.Builder(project, indicator).build())
+                .push(
+                    new BlazeConsoleScope.Builder(project, indicator)
+                        .setPopupBehavior(
+                            BlazeUserSettings.getInstance().getShowBlazeConsoleOnSync())
+                        .build())
                 .push(new IssuesScope(project))
                 .push(new IdeaLogScope())
                 .push(
@@ -199,7 +207,7 @@
         updateInMemoryState(project, context, projectViewSet, blazeProjectData);
         onSyncComplete(project, context, projectViewSet, blazeProjectData, syncMode, syncResult);
       }
-    } catch (AssertionError | Exception e) {
+    } catch (Throwable e) {
       Throwable rootCause = e;
       while (rootCause.getCause() != null) {
         rootCause = rootCause.getCause();
@@ -246,7 +254,8 @@
                 importSettings.getBuildSystem(),
                 Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
                 workspaceRoot,
-                BlazeFlags.buildFlags(project, projectViewSet));
+                BlazeFlags.blazeFlags(
+                    project, projectViewSet, BlazeCommandName.INFO, BlazeInvocationContext.Sync));
 
     ListenableFuture<WorkingSet> workingSetFuture =
         vcsHandler.getWorkingSet(project, context, workspaceRoot, executor);
@@ -265,6 +274,10 @@
     BlazeVersionData blazeVersionData =
         BlazeVersionData.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
 
+    if (!BuildSystemVersionChecker.verifyVersionSupported(context, blazeVersionData)) {
+      return SyncResult.FAILURE;
+    }
+
     WorkspacePathResolver workspacePathResolver =
         workspacePathResolverAndProjectView.workspacePathResolver;
     ArtifactLocationDecoder artifactLocationDecoder =
@@ -272,9 +285,6 @@
 
     WorkspaceLanguageSettings workspaceLanguageSettings =
         LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
-    if (workspaceLanguageSettings == null) {
-      return SyncResult.FAILURE;
-    }
 
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
       syncPlugin.installSdks(context);
@@ -338,6 +348,7 @@
     }
     ShardedTargetList shardedTargets = shardedTargetsResult.shardedTargets;
 
+    BlazeConfigurationHandler configHandler = new BlazeConfigurationHandler(blazeInfo);
     boolean mergeWithOldState = !syncParams.addProjectViewTargets;
     BlazeIdeInterface.IdeResult ideQueryResult =
         getIdeQueryResult(
@@ -345,6 +356,7 @@
             context,
             projectViewSet,
             blazeVersionData,
+            configHandler,
             shardedTargets,
             workspaceLanguageSettings,
             artifactLocationDecoder,
@@ -358,7 +370,7 @@
         || ideQueryResult.buildResult.status == BuildResult.Status.FATAL_ERROR) {
       context.setHasError();
       if (ideQueryResult.buildResult.outOfMemory()) {
-        SuggestEnablingShardingNotification.suggestSharding(project, context);
+        SuggestBuildShardingNotification.syncOutOfMemoryError(project, context);
       }
       return SyncResult.FAILURE;
     }
@@ -371,11 +383,17 @@
 
     BuildResult ideResolveResult =
         resolveIdeArtifacts(
-            project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets);
+            project,
+            context,
+            workspaceRoot,
+            projectViewSet,
+            blazeVersionData,
+            workspaceLanguageSettings,
+            shardedTargets);
     if (ideResolveResult.status == BuildResult.Status.FATAL_ERROR) {
       context.setHasError();
       if (ideResolveResult.outOfMemory()) {
-        SuggestEnablingShardingNotification.suggestSharding(project, context);
+        SuggestBuildShardingNotification.syncOutOfMemoryError(project, context);
       }
       return SyncResult.FAILURE;
     }
@@ -490,21 +508,16 @@
 
   private static void refreshVirtualFileSystem(
       BlazeContext context, BlazeProjectData blazeProjectData) {
-    Transactions.submitTransactionAndWait(
+    Transactions.submitWriteActionTransactionAndWait(
         () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(
-                    (Runnable)
-                        () ->
-                            Scope.push(
-                                context,
-                                (childContext) -> {
-                                  childContext.push(new TimingScope("RefreshVirtualFileSystem"));
-                                  for (BlazeSyncPlugin syncPlugin :
-                                      BlazeSyncPlugin.EP_NAME.getExtensions()) {
-                                    syncPlugin.refreshVirtualFileSystem(blazeProjectData);
-                                  }
-                                })));
+            Scope.push(
+                context,
+                (childContext) -> {
+                  childContext.push(new TimingScope("RefreshVirtualFileSystem"));
+                  for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+                    syncPlugin.refreshVirtualFileSystem(blazeProjectData);
+                  }
+                }));
   }
 
   static class WorkspacePathResolverAndProjectView {
@@ -639,6 +652,7 @@
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      BlazeConfigurationHandler configHandler,
       ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -660,6 +674,7 @@
               workspaceRoot,
               projectViewSet,
               blazeVersionData,
+              configHandler,
               shardedTargets,
               workspaceLanguageSettings,
               artifactLocationDecoder,
@@ -675,6 +690,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets) {
     return Scope.push(
         parentContext,
@@ -690,7 +706,13 @@
           }
           BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
           return blazeIdeInterface.resolveIdeArtifacts(
-              project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets);
+              project,
+              context,
+              workspaceRoot,
+              projectViewSet,
+              blazeVersionData,
+              workspaceLanguageSettings,
+              shardedTargets);
         });
   }
 
@@ -708,23 +730,22 @@
           context.output(new StatusOutput("Committing project structure..."));
 
           try {
-            Transactions.submitTransactionAndWait(
+            Transactions.submitWriteActionTransactionAndWait(
                 () ->
-                    ApplicationManager.getApplication()
-                        .runWriteAction(
-                            (Runnable)
-                                () ->
-                                    ProjectRootManagerEx.getInstanceEx(this.project)
-                                        .mergeRootsChangesDuring(
-                                            () ->
-                                                updateProjectStructure(
-                                                    context,
-                                                    importSettings,
-                                                    projectViewSet,
-                                                    blazeVersionData,
-                                                    directoryStructure,
-                                                    newBlazeProjectData,
-                                                    oldBlazeProjectData))));
+                    ProjectRootManagerEx.getInstanceEx(this.project)
+                        .mergeRootsChangesDuring(
+                            () ->
+                                updateProjectStructure(
+                                    context,
+                                    importSettings,
+                                    projectViewSet,
+                                    blazeVersionData,
+                                    directoryStructure,
+                                    newBlazeProjectData,
+                                    oldBlazeProjectData)));
+          } catch (ProcessCanceledException e) {
+            context.setCancelled();
+            throw e;
           } catch (Throwable e) {
             IssueOutput.error("Internal error. Error: " + e).submit(context);
             logger.error(e);
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
index 34f952a..b28f13b 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.sync.aspects;
 
+import com.google.idea.blaze.base.command.info.BlazeConfigurationHandler;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
@@ -58,6 +59,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      BlazeConfigurationHandler configHandler,
       ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -76,6 +78,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets);
 
   /**
@@ -89,5 +92,6 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets);
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
index 18a884c..5f03475 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
@@ -16,7 +16,9 @@
 package com.google.idea.blaze.base.sync.aspects;
 
 import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
@@ -32,7 +34,9 @@
 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.command.buildresult.BuildResultHelper;
+import com.google.idea.blaze.base.command.info.BlazeConfigurationHandler;
 import com.google.idea.blaze.base.filecache.FileDiffer;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
@@ -42,13 +46,12 @@
 import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 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.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Result;
 import com.google.idea.blaze.base.scope.Scope;
@@ -59,17 +62,19 @@
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.aspects.BuildResult.Status;
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategy;
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider;
+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.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
-import com.google.idea.blaze.base.sync.sharding.WildcardTargetPattern;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.pom.NavigatableAdapter;
-import com.intellij.util.PathUtil;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -77,6 +82,7 @@
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -86,7 +92,6 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
 import javax.annotation.Nullable;
 
@@ -111,6 +116,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      BlazeConfigurationHandler configHandler,
       ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -126,14 +132,22 @@
     }
 
     // If the aspect strategy has changed, redo everything from scratch
-    final AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
+    final AspectStrategy aspectStrategy = getAspectStrategy(blazeVersionData);
     if (prevState != null
         && !Objects.equals(prevState.aspectStrategyName, aspectStrategy.getName())) {
       prevState = null;
     }
 
     IdeInfoResult ideInfoResult =
-        getIdeInfo(project, context, workspaceRoot, projectViewSet, shardedTargets, aspectStrategy);
+        getIdeInfo(
+            project,
+            context,
+            workspaceRoot,
+            projectViewSet,
+            blazeVersionData,
+            workspaceLanguageSettings.activeLanguages,
+            shardedTargets,
+            aspectStrategy);
     if (ideInfoResult.buildResult.status == BuildResult.Status.FATAL_ERROR) {
       return new IdeResult(
           prevState != null ? prevState.targetMap : null, ideInfoResult.buildResult);
@@ -161,7 +175,7 @@
                 fileList.size(), updatedFiles.size(), removedFiles.size())));
 
     ListenableFuture<?> prefetchFuture =
-        PrefetchService.getInstance().prefetchFiles(project, updatedFiles);
+        PrefetchService.getInstance().prefetchFiles(project, updatedFiles, true);
     if (!FutureUtil.waitForFuture(context, prefetchFuture)
         .timed("FetchAspectOutput")
         .withProgressMessage("Reading IDE info result...")
@@ -170,7 +184,10 @@
       return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
     }
 
-    Set<Label> targets = getNonWildcardProjectViewTargets(projectViewSet);
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
+            .add(projectViewSet)
+            .build();
 
     State state =
         updateState(
@@ -178,8 +195,9 @@
             context,
             prevState,
             fileState,
+            configHandler,
             workspaceLanguageSettings,
-            targets,
+            importRoots,
             aspectStrategy,
             updatedFiles,
             removedFiles,
@@ -191,33 +209,6 @@
     return new IdeResult(state.targetMap, ideInfoResult.buildResult);
   }
 
-  private static Set<Label> getNonWildcardProjectViewTargets(ProjectViewSet projectViewSet) {
-    return projectViewSet
-        .listItems(TargetSection.KEY)
-        .stream()
-        .map(BlazeIdeInterfaceAspectsImpl::singleTargetLabel)
-        .filter(Objects::nonNull)
-        .collect(Collectors.toSet());
-  }
-
-  @Nullable
-  private static Label singleTargetLabel(TargetExpression expression) {
-    if (WildcardTargetPattern.fromExpression(expression) != null) {
-      return null;
-    }
-    // convert to a valid Label format
-    String pattern = expression.toString();
-    if (!pattern.startsWith("//")) {
-      pattern = "//" + pattern;
-    }
-    int colonIndex = pattern.indexOf(':');
-    if (colonIndex == -1) {
-      // add the implicit rule name
-      pattern += ":" + PathUtil.getFileName(pattern);
-    }
-    return Label.createIfValid(pattern);
-  }
-
   private static class IdeInfoResult {
     final Collection<File> files;
     final BuildResult buildResult;
@@ -233,6 +224,8 @@
       BlazeContext parentContext,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      ImmutableSet<LanguageClass> activeLanguages,
       ShardedTargetList shardedTargets,
       AspectStrategy aspectStrategy) {
     return Scope.push(
@@ -250,7 +243,14 @@
               targets -> {
                 IdeInfoResult result =
                     getIdeInfoForTargets(
-                        project, context, workspaceRoot, projectViewSet, targets, aspectStrategy);
+                        project,
+                        context,
+                        workspaceRoot,
+                        projectViewSet,
+                        blazeVersionData,
+                        activeLanguages,
+                        targets,
+                        aspectStrategy);
                 ideInfoFiles.addAll(result.files);
                 return result.buildResult;
               };
@@ -266,22 +266,26 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      ImmutableSet<LanguageClass> activeLanguages,
       List<TargetExpression> targets,
       AspectStrategy aspectStrategy) {
     String fileExtension = aspectStrategy.getAspectOutputFileExtension();
     String gzFileExtension = fileExtension + ".gz";
     Predicate<String> fileFilter =
         fileName -> fileName.endsWith(fileExtension) || fileName.endsWith(gzFileExtension);
-    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(fileFilter);
+    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(blazeVersionData, fileFilter);
 
     BlazeCommand.Builder builder =
         BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
             .addTargets(targets)
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
             .addBlazeFlags(buildResultHelper.getBuildFlags())
-            .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
+            .addBlazeFlags(
+                BlazeFlags.blazeFlags(
+                    project, projectViewSet, BlazeCommandName.BUILD, BlazeInvocationContext.Sync));
 
-    aspectStrategy.modifyIdeInfoCommand(builder);
+    aspectStrategy.modifyIdeInfoCommand(builder, activeLanguages);
 
     int retVal =
         ExternalTask.builder(workspaceRoot)
@@ -294,6 +298,10 @@
             .run();
 
     BuildResult buildResult = BuildResult.fromExitCode(retVal);
+    if (buildResult.status == Status.FATAL_ERROR) {
+      buildResultHelper.close();
+      return new IdeInfoResult(ImmutableList.of(), buildResult);
+    }
     return new IdeInfoResult(buildResultHelper.getBuildArtifacts(), buildResult);
   }
 
@@ -313,8 +321,9 @@
       BlazeContext parentContext,
       @Nullable State prevState,
       ImmutableMap<File, Long> fileState,
+      BlazeConfigurationHandler configHandler,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      Set<Label> nonWildcardProjectTargets,
+      ImportRoots importRoots,
       AspectStrategy aspectStrategy,
       List<File> newFiles,
       List<File> removedFiles,
@@ -346,7 +355,6 @@
                   state.aspectStrategyName = aspectStrategy.getName();
 
                   Map<TargetKey, TargetIdeInfo> targetMap = Maps.newHashMap();
-                  Map<TargetKey, TargetIdeInfo> updatedTargets = Maps.newHashMap();
                   if (prevState != null) {
                     targetMap.putAll(prevState.targetMap.map());
                     state.fileToTargetMapKey.putAll(prevState.fileToTargetMapKey);
@@ -380,7 +388,7 @@
                                 TargetIdeInfo target =
                                     protoToTarget(
                                         workspaceLanguageSettings,
-                                        nonWildcardProjectTargets,
+                                        importRoots,
                                         message,
                                         ignoredLanguages);
                                 return new TargetFilePair(file, target);
@@ -388,19 +396,31 @@
                             }));
                   }
 
+                  Set<TargetKey> newTargets = new HashSet<>();
+                  Set<String> configurations = new LinkedHashSet<>();
+                  configurations.add(configHandler.defaultConfigurationPathComponent);
+
                   // Update state with result from proto files
                   int duplicateTargetLabels = 0;
                   try {
-                    for (TargetFilePair targetFilePairs : Futures.allAsList(futures).get()) {
-                      if (targetFilePairs.target != null) {
-                        File file = targetFilePairs.file;
-                        TargetKey key = targetFilePairs.target.key;
-                        TargetIdeInfo previousTarget =
-                            updatedTargets.putIfAbsent(key, targetFilePairs.target);
-                        if (previousTarget == null) {
+                    for (TargetFilePair targetFilePair : Futures.allAsList(futures).get()) {
+                      if (targetFilePair.target != null) {
+                        File file = targetFilePair.file;
+                        String config = configHandler.getConfigurationPathComponent(file);
+                        configurations.add(config);
+                        TargetKey key = targetFilePair.target.key;
+                        if (targetMap.putIfAbsent(key, targetFilePair.target) == null) {
                           state.fileToTargetMapKey.put(file, key);
                         } else {
-                          duplicateTargetLabels++;
+                          if (!newTargets.add(key)) {
+                            duplicateTargetLabels++;
+                          }
+                          // prioritize the default configuration over build order
+                          if (Objects.equals(
+                              config, configHandler.defaultConfigurationPathComponent)) {
+                            targetMap.put(key, targetFilePair.target);
+                            state.fileToTargetMapKey.put(file, key);
+                          }
                         }
                       }
                     }
@@ -410,7 +430,6 @@
                   } catch (ExecutionException e) {
                     return Result.error(e);
                   }
-                  targetMap.putAll(updatedTargets);
 
                   context.output(
                       PrintOutput.log(
@@ -421,13 +440,16 @@
                     context.output(
                         new PerformanceWarning(
                             String.format(
-                                "There were %d duplicate rules. "
-                                    + "You may be including multiple configurations in your build. "
-                                    + "Your IDE sync is slowed down by ~%d%%.",
+                                "There were %d duplicate rules, built with the following "
+                                    + "configurations: %s.\nYour IDE sync is slowed down by ~%d%%.",
                                 duplicateTargetLabels,
+                                configurations,
                                 (100 * duplicateTargetLabels / targetMap.size()))));
                   }
 
+                  ignoredLanguages.retainAll(
+                      LanguageSupport.availableAdditionalLanguages(
+                          workspaceLanguageSettings.getWorkspaceType()));
                   warnIgnoredLanguages(project, context, ignoredLanguages);
 
                   state.targetMap = new TargetMap(ImmutableMap.copyOf(targetMap));
@@ -444,18 +466,18 @@
   @Nullable
   private static TargetIdeInfo protoToTarget(
       WorkspaceLanguageSettings languageSettings,
-      Set<Label> nonWildcardProjectTargets,
+      ImportRoots importRoots,
       IntellijIdeInfo.TargetIdeInfo message,
       Set<LanguageClass> ignoredLanguages) {
     Kind kind = IdeInfoFromProtobuf.getKind(message);
     if (kind == null) {
       return null;
     }
-    if (languageSettings.isLanguageActive(kind.getLanguageClass())) {
+    if (languageSettings.isLanguageActive(kind.languageClass)) {
       return IdeInfoFromProtobuf.makeTargetIdeInfo(message);
     }
-    if (nonWildcardProjectTargets.contains(IdeInfoFromProtobuf.getKey(message).label)) {
-      ignoredLanguages.add(kind.getLanguageClass());
+    if (importRoots.importAsSource(IdeInfoFromProtobuf.getKey(message).label)) {
+      ignoredLanguages.add(kind.languageClass);
     }
     return null;
   }
@@ -498,9 +520,17 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets) {
     return resolveIdeArtifacts(
-        project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets, false);
+        project,
+        context,
+        workspaceRoot,
+        projectViewSet,
+        blazeVersionData,
+        workspaceLanguageSettings,
+        shardedTargets,
+        false);
   }
 
   @Override
@@ -510,6 +540,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets) {
     boolean ideCompile = hasIdeCompileOutputGroup(blazeVersionData);
     return resolveIdeArtifacts(
@@ -518,6 +549,7 @@
         workspaceRoot,
         projectViewSet,
         blazeVersionData,
+        workspaceLanguageSettings,
         shardedTargets,
         ideCompile);
   }
@@ -533,6 +565,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       ShardedTargetList shardedTargets,
       boolean useIdeCompileOutputGroup) {
 
@@ -543,45 +576,100 @@
                 count, shardedTargets.shardedTargets.size());
     Function<List<TargetExpression>, BuildResult> invocation =
         targets ->
-            doResolveIdeArtifacts(
-                project,
-                context,
-                workspaceRoot,
-                projectViewSet,
-                blazeVersionData,
-                targets,
-                useIdeCompileOutputGroup);
+            useIdeCompileOutputGroup
+                ? doCompileIdeArtifacts(
+                    project,
+                    context,
+                    workspaceRoot,
+                    projectViewSet,
+                    blazeVersionData,
+                    workspaceLanguageSettings,
+                    targets)
+                : doResolveIdeArtifacts(
+                    project,
+                    context,
+                    workspaceRoot,
+                    projectViewSet,
+                    blazeVersionData,
+                    workspaceLanguageSettings,
+                    targets);
     return shardedTargets.runShardedCommand(project, context, progressMessage, invocation);
   }
 
+  /**
+   * Blaze build invocation requesting the 'intellij-resolve' aspect output group.
+   *
+   * <p>Prefetches the output artifacts built by this invocation.
+   */
   private static BuildResult doResolveIdeArtifacts(
       Project project,
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets,
-      boolean useIdeCompileOutputGroup) {
-    AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
+      WorkspaceLanguageSettings workspaceLanguageSettings,
+      List<TargetExpression> targets) {
+    BuildResultHelper buildResultHelper =
+        BuildResultHelper.forFiles(blazeVersionData, getGenfilePrefetchFilter());
 
     BlazeCommand.Builder blazeCommandBuilder =
         BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
             .addTargets(targets)
+            .addBlazeFlags(BlazeFlags.KEEP_GOING)
+            .addBlazeFlags(buildResultHelper.getBuildFlags())
+            .addBlazeFlags(
+                BlazeFlags.blazeFlags(
+                    project, projectViewSet, BlazeCommandName.BUILD, BlazeInvocationContext.Sync));
+
+    // Request the 'intellij-resolve' aspect output group.
+    getAspectStrategy(blazeVersionData)
+        .modifyIdeResolveCommand(blazeCommandBuilder, workspaceLanguageSettings.activeLanguages);
+
+    // Run the blaze build command, parsing any output artifacts produced.
+    int retVal =
+        ExternalTask.builder(workspaceRoot)
+            .addBlazeCommand(blazeCommandBuilder.build())
+            .context(context)
+            .stderr(
+                buildResultHelper.stderr(
+                    new IssueOutputLineProcessor(project, context, workspaceRoot)))
+            .build()
+            .run(new TimingScope("ExecuteBlazeCommand"));
+
+    BuildResult result = BuildResult.fromExitCode(retVal);
+    if (result.status != BuildResult.Status.FATAL_ERROR) {
+      prefetchGenfiles(project, context, buildResultHelper.getBuildArtifacts());
+    } else {
+      buildResultHelper.close();
+    }
+    return result;
+  }
+
+  /** Blaze build invocation requesting the 'intellij-compile' aspect output group. */
+  private static BuildResult doCompileIdeArtifacts(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
+      List<TargetExpression> targets) {
+    BlazeCommand.Builder blazeCommandBuilder =
+        BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
+            .addTargets(targets)
             .addBlazeFlags()
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
-            .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
+            .addBlazeFlags(
+                BlazeFlags.blazeFlags(
+                    project, projectViewSet, BlazeCommandName.BUILD, BlazeInvocationContext.Sync));
 
-    if (useIdeCompileOutputGroup) {
-      aspectStrategy.modifyIdeCompileCommand(blazeCommandBuilder);
-    } else {
-      aspectStrategy.modifyIdeResolveCommand(blazeCommandBuilder);
-    }
+    getAspectStrategy(blazeVersionData)
+        .modifyIdeCompileCommand(blazeCommandBuilder, workspaceLanguageSettings.activeLanguages);
 
-    BlazeCommand blazeCommand = blazeCommandBuilder.build();
-
+    // Run the blaze build command.
     int retVal =
         ExternalTask.builder(workspaceRoot)
-            .addBlazeCommand(blazeCommand)
+            .addBlazeCommand(blazeCommandBuilder.build())
             .context(context)
             .stderr(
                 LineProcessingOutputStream.of(
@@ -592,10 +680,26 @@
     return BuildResult.fromExitCode(retVal);
   }
 
-  private static AspectStrategy getAspectStrategy(
-      Project project, BlazeVersionData blazeVersionData) {
+  /** A filename filter for blaze output artifacts to prefetch. */
+  private static Predicate<String> getGenfilePrefetchFilter() {
+    ImmutableSet<String> extensions = PrefetchFileSource.getAllPrefetchFileExtensions();
+    return fileName -> extensions.contains(FileUtil.getExtension(fileName));
+  }
+
+  /** Prefetch a list of blaze output artifacts, blocking until complete. */
+  private static void prefetchGenfiles(
+      Project project, BlazeContext context, ImmutableList<File> artifacts) {
+    ListenableFuture<?> prefetchFuture =
+        PrefetchService.getInstance().prefetchFiles(project, artifacts, false);
+    FutureUtil.waitForFuture(context, prefetchFuture)
+        .timed("PrefetchGenfiles")
+        .withProgressMessage("Prefetching genfiles...")
+        .run();
+  }
+
+  private static AspectStrategy getAspectStrategy(BlazeVersionData blazeVersionData) {
     for (AspectStrategyProvider provider : AspectStrategyProvider.EP_NAME.getExtensions()) {
-      AspectStrategy aspectStrategy = provider.getAspectStrategy(project, blazeVersionData);
+      AspectStrategy aspectStrategy = provider.getAspectStrategy(blazeVersionData);
       if (aspectStrategy != null) {
         return aspectStrategy;
       }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
index a37991f..443dcbc 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
@@ -30,14 +30,17 @@
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.Dependency;
 import com.google.idea.blaze.base.ideinfo.Dependency.DependencyType;
+import com.google.idea.blaze.base.ideinfo.GoIdeInfo;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.JavaToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.JsIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.ProtoLibraryLegacyInfo;
 import com.google.idea.blaze.base.ideinfo.PyIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TsIdeInfo;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -81,6 +84,8 @@
     if (message.hasCIdeInfo()) {
       cIdeInfo = makeCIdeInfo(message.getCIdeInfo());
       sources.addAll(cIdeInfo.sources);
+      sources.addAll(cIdeInfo.headers);
+      sources.addAll(cIdeInfo.textualHeaders);
     }
     CToolchainIdeInfo cToolchainIdeInfo = null;
     if (message.hasCToolchainIdeInfo()) {
@@ -106,6 +111,21 @@
       pyIdeInfo = makePyIdeInfo(message.getPyIdeInfo());
       sources.addAll(pyIdeInfo.sources);
     }
+    GoIdeInfo goIdeInfo = null;
+    if (message.hasGoIdeInfo()) {
+      goIdeInfo = makeGoIdeInfo(message.getGoIdeInfo());
+      sources.addAll(goIdeInfo.generatedSources);
+    }
+    JsIdeInfo jsIdeInfo = null;
+    if (message.hasJsIdeInfo()) {
+      jsIdeInfo = makeJsIdeInfo(message.getJsIdeInfo());
+      sources.addAll(jsIdeInfo.sources);
+    }
+    TsIdeInfo tsIdeInfo = null;
+    if (message.hasTsIdeInfo()) {
+      tsIdeInfo = makeTsIdeInfo(message.getTsIdeInfo());
+      sources.addAll(tsIdeInfo.sources);
+    }
     TestIdeInfo testIdeInfo = null;
     if (message.hasTestInfo()) {
       testIdeInfo = makeTestIdeInfo(message.getTestInfo());
@@ -133,6 +153,9 @@
         androidIdeInfo,
         androidSdkIdeInfo,
         pyIdeInfo,
+        goIdeInfo,
+        jsIdeInfo,
+        tsIdeInfo,
         testIdeInfo,
         protoLibraryLegacyInfo,
         javaToolchainIdeInfo);
@@ -177,6 +200,9 @@
 
   private static CIdeInfo makeCIdeInfo(IntellijIdeInfo.CIdeInfo cIdeInfo) {
     List<ArtifactLocation> sources = makeArtifactLocationList(cIdeInfo.getSourceList());
+    List<ArtifactLocation> headers = makeArtifactLocationList(cIdeInfo.getHeaderList());
+    List<ArtifactLocation> textualHeaders =
+        makeArtifactLocationList(cIdeInfo.getTextualHeaderList());
     List<ExecutionRootPath> transitiveIncludeDirectories =
         makeExecutionRootPathList(cIdeInfo.getTransitiveIncludeDirectoryList());
     List<ExecutionRootPath> transitiveQuoteIncludeDirectories =
@@ -202,6 +228,8 @@
     CIdeInfo.Builder builder =
         CIdeInfo.builder()
             .addSources(sources)
+            .addHeaders(headers)
+            .addTextualHeaders(textualHeaders)
             .addLocalDefines(coptDefines)
             .addLocalIncludeDirectories(coptIncludeDirectories)
             .addTransitiveIncludeDirectories(transitiveIncludeDirectories)
@@ -289,6 +317,20 @@
     return PyIdeInfo.builder().addSources(makeArtifactLocationList(info.getSourcesList())).build();
   }
 
+  private static GoIdeInfo makeGoIdeInfo(IntellijIdeInfo.GoIdeInfo info) {
+    return GoIdeInfo.builder()
+        .addGeneratedSources(makeArtifactLocationList(info.getGeneratedSourcesList()))
+        .build();
+  }
+
+  private static JsIdeInfo makeJsIdeInfo(IntellijIdeInfo.JsIdeInfo info) {
+    return JsIdeInfo.builder().addSources(makeArtifactLocationList(info.getSourcesList())).build();
+  }
+
+  private static TsIdeInfo makeTsIdeInfo(IntellijIdeInfo.TsIdeInfo info) {
+    return TsIdeInfo.builder().addSources(makeArtifactLocationList(info.getSourcesList())).build();
+  }
+
   private static TestIdeInfo makeTestIdeInfo(IntellijIdeInfo.TestInfo testInfo) {
     String size = testInfo.getSize();
     TestIdeInfo.TestSize testSize = TestIdeInfo.DEFAULT_RULE_TEST_SIZE;
@@ -343,8 +385,12 @@
 
   private static JavaToolchainIdeInfo makeJavaToolchainIdeInfo(
       IntellijIdeInfo.JavaToolchainIdeInfo javaToolchainIdeInfo) {
+    ArtifactLocation javacJar =
+        javaToolchainIdeInfo.hasJavacJar()
+            ? makeArtifactLocation(javaToolchainIdeInfo.getJavacJar())
+            : null;
     return new JavaToolchainIdeInfo(
-        javaToolchainIdeInfo.getSourceVersion(), javaToolchainIdeInfo.getTargetVersion());
+        javaToolchainIdeInfo.getSourceVersion(), javaToolchainIdeInfo.getTargetVersion(), javacJar);
   }
 
   private static Collection<LibraryArtifact> makeLibraryArtifactList(
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
index d63630e..cb98797 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
@@ -15,22 +15,96 @@
  */
 package com.google.idea.blaze.base.sync.aspects.strategy;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommand.Builder;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.common.experiments.BoolExperiment;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
+import com.google.repackaged.protobuf.TextFormat;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
-/** Indirection for our various ways of calling the aspect. */
-public interface AspectStrategy {
-  String getName();
+/** Aspect strategy for Skylark. */
+public abstract class AspectStrategy {
 
-  void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder);
+  private static final BoolExperiment usePerLanguageOutputGroups =
+      new BoolExperiment("blaze.use.per.language.output.groups", true);
 
-  void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder);
+  public abstract String getName();
 
-  void modifyIdeCompileCommand(BlazeCommand.Builder blazeCommandBuilder);
+  protected abstract List<String> getAspectFlags();
 
-  String getAspectOutputFileExtension();
+  protected abstract boolean hasPerLanguageOutputGroups();
 
-  IntellijIdeInfo.TargetIdeInfo readAspectFile(InputStream inputStream) throws IOException;
+  private boolean usePerLanguageOutputGroups() {
+    return usePerLanguageOutputGroups.getValue() && hasPerLanguageOutputGroups();
+  }
+
+  public final void modifyIdeInfoCommand(
+      BlazeCommand.Builder blazeCommandBuilder, Set<LanguageClass> activeLanguages) {
+    blazeCommandBuilder.addBlazeFlags(getAspectFlags());
+    if (!usePerLanguageOutputGroups()) {
+      blazeCommandBuilder.addBlazeFlags("--output_groups=intellij-info-text");
+      return;
+    }
+    List<String> outputGroups = getOutputGroups(activeLanguages, "intellij-info-");
+    outputGroups.add("intellij-info-generic");
+    String flag = "--output_groups=" + Joiner.on(',').join(outputGroups);
+    blazeCommandBuilder.addBlazeFlags(flag);
+  }
+
+  public final void modifyIdeResolveCommand(
+      BlazeCommand.Builder blazeCommandBuilder, Set<LanguageClass> activeLanguages) {
+    blazeCommandBuilder.addBlazeFlags(getAspectFlags());
+    if (!usePerLanguageOutputGroups()) {
+      blazeCommandBuilder.addBlazeFlags("--output_groups=intellij-resolve");
+      return;
+    }
+    List<String> outputGroups = getOutputGroups(activeLanguages, "intellij-resolve-");
+    String flag = "--output_groups=" + Joiner.on(',').join(outputGroups);
+    blazeCommandBuilder.addBlazeFlags(flag);
+  }
+
+  public final void modifyIdeCompileCommand(
+      Builder blazeCommandBuilder, Set<LanguageClass> activeLanguages) {
+    blazeCommandBuilder.addBlazeFlags(getAspectFlags());
+    if (!usePerLanguageOutputGroups()) {
+      blazeCommandBuilder.addBlazeFlags("--output_groups=intellij-compile");
+      return;
+    }
+    List<String> outputGroups = getOutputGroups(activeLanguages, "intellij-compile-");
+    String flag = "--output_groups=" + Joiner.on(',').join(outputGroups);
+    blazeCommandBuilder.addBlazeFlags(flag);
+  }
+
+  private static List<String> getOutputGroups(Set<LanguageClass> activeLanguages, String prefix) {
+    return activeLanguages
+        .stream()
+        .map(LanguageOutputGroup::forLanguage)
+        .filter(Objects::nonNull)
+        .map(lang -> prefix + lang.suffix)
+        .distinct()
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  public final String getAspectOutputFileExtension() {
+    return ".intellij-info.txt";
+  }
+
+  public final IntellijIdeInfo.TargetIdeInfo readAspectFile(InputStream inputStream)
+      throws IOException {
+    IntellijIdeInfo.TargetIdeInfo.Builder builder = IntellijIdeInfo.TargetIdeInfo.newBuilder();
+    TextFormat.Parser parser = TextFormat.Parser.newBuilder().setAllowUnknownFields(true).build();
+    parser.merge(new InputStreamReader(inputStream, UTF_8), builder);
+    return builder.build();
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
index 13c7586..268eaca 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
@@ -17,7 +17,6 @@
 
 import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.intellij.openapi.extensions.ExtensionPointName;
-import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
 
 /** Extension point for providing an aspect strategy */
@@ -26,5 +25,5 @@
       ExtensionPointName.create("com.google.idea.blaze.AspectStrategyProvider");
 
   @Nullable
-  AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData);
+  AspectStrategy getAspectStrategy(BlazeVersionData blazeVersionData);
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
index 9c0faa4..191ae61 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
@@ -15,12 +15,66 @@
  */
 package com.google.idea.blaze.base.sync.aspects.strategy;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.BlazeVersionData;
-import com.intellij.openapi.project.Project;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import java.io.File;
+import java.util.List;
 
 class AspectStrategyProviderBazel implements AspectStrategyProvider {
   @Override
-  public AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData) {
-    return new AspectStrategySkylark();
+  public AspectStrategy getAspectStrategy(BlazeVersionData blazeVersionData) {
+    if (blazeVersionData.buildSystem() != BuildSystem.Bazel) {
+      return null;
+    }
+    return new AspectStrategyBazel(blazeVersionData);
+  }
+
+  private static class AspectStrategyBazel extends AspectStrategy {
+
+    private final BlazeVersionData blazeVersionData;
+
+    private AspectStrategyBazel(BlazeVersionData blazeVersionData) {
+      this.blazeVersionData = blazeVersionData;
+    }
+
+    @Override
+    public String getName() {
+      return "AspectStrategySkylarkBazel";
+    }
+
+    @Override
+    protected boolean hasPerLanguageOutputGroups() {
+      return useBundledAspect();
+    }
+
+    @Override
+    protected List<String> getAspectFlags() {
+      if (useBundledAspect()) {
+        return ImmutableList.of(
+            "--aspects=@intellij_aspect//:intellij_info.bzl%intellij_info_aspect",
+            getAspectRepositoryOverrideFlag());
+      }
+      return ImmutableList.of(
+          "--aspects=@bazel_tools//tools/ide:intellij_info.bzl%intellij_info_aspect");
+    }
+
+    private boolean useBundledAspect() {
+      return blazeVersionData.bazelIsAtLeastVersion(0, 5, 0);
+    }
+
+    private static File findAspectDirectory() {
+      IdeaPluginDescriptor plugin =
+          PluginManager.getPlugin(
+              PluginManager.getPluginByClassName(AspectStrategy.class.getName()));
+      return new File(plugin.getPath(), "aspect");
+    }
+
+    private static String getAspectRepositoryOverrideFlag() {
+      return String.format(
+          "--override_repository=intellij_aspect=%s", findAspectDirectory().getPath());
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java
deleted file mode 100644
index aa3eadc..0000000
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java
+++ /dev/null
@@ -1,73 +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.sync.aspects.strategy;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.idea.blaze.base.command.BlazeCommand;
-import com.google.idea.blaze.base.command.BlazeCommand.Builder;
-import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
-import com.google.repackaged.protobuf.TextFormat;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-/** Aspect strategy for Skylark. */
-public class AspectStrategySkylark implements AspectStrategy {
-
-  @Override
-  public String getName() {
-    return "SkylarkAspect";
-  }
-
-  protected String getAspectFlag() {
-    return "--aspects=@bazel_tools//tools/ide:intellij_info.bzl%intellij_info_aspect";
-  }
-
-  @Override
-  public void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags(getAspectFlag())
-        .addBlazeFlags("--output_groups=intellij-info-text");
-  }
-
-  @Override
-  public void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags(getAspectFlag())
-        .addBlazeFlags("--output_groups=intellij-resolve");
-  }
-
-  @Override
-  public void modifyIdeCompileCommand(Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags(getAspectFlag())
-        .addBlazeFlags("--output_groups=intellij-compile");
-  }
-
-  @Override
-  public String getAspectOutputFileExtension() {
-    return ".intellij-info.txt";
-  }
-
-  @Override
-  public IntellijIdeInfo.TargetIdeInfo readAspectFile(InputStream inputStream) throws IOException {
-    IntellijIdeInfo.TargetIdeInfo.Builder builder = IntellijIdeInfo.TargetIdeInfo.newBuilder();
-    TextFormat.Parser parser = TextFormat.Parser.newBuilder().setAllowUnknownFields(true).build();
-    parser.merge(new InputStreamReader(inputStream, UTF_8), builder);
-    return builder.build();
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/LanguageOutputGroup.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/LanguageOutputGroup.java
new file mode 100644
index 0000000..6f60207
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/LanguageOutputGroup.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sync.aspects.strategy;
+
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import javax.annotation.Nullable;
+
+/** Enumerates the sets of aspect output groups corresponding to each language */
+public enum LanguageOutputGroup {
+  ANDROID(LanguageClass.ANDROID, "android"),
+  C(LanguageClass.C, "cpp"),
+  JAVA(LanguageClass.JAVA, "java"),
+  PYTHON(LanguageClass.PYTHON, "py"),
+  GO(LanguageClass.GO, "go"),
+  JAVASCRIPT(LanguageClass.JAVASCRIPT, "js"),
+  TYPESCRIPT(LanguageClass.TYPESCRIPT, "ts");
+
+  public final LanguageClass languageClass;
+  public final String suffix;
+
+  LanguageOutputGroup(LanguageClass languageClass, String suffix) {
+    this.languageClass = languageClass;
+    this.suffix = suffix;
+  }
+
+  @Nullable
+  public static LanguageOutputGroup forLanguage(LanguageClass languageClass) {
+    for (LanguageOutputGroup group : values()) {
+      if (group.languageClass == languageClass) {
+        return group;
+      }
+    }
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
index c8e38db..c5d4c79 100644
--- a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
+++ b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
@@ -50,6 +50,8 @@
             .filter(Objects::nonNull)
             .reduce(Predicate::and)
             .orElse(o -> true);
-    return result.stream().filter(libraryFilter).collect(Collectors.toList());
+
+    return BlazeLibrarySorter.sortLibraries(
+        result.stream().filter(libraryFilter).collect(Collectors.toList()));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibrarySorter.java b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibrarySorter.java
new file mode 100644
index 0000000..e6fca95
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibrarySorter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.sync.libraries;
+
+import com.google.idea.blaze.base.model.BlazeLibrary;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import java.util.List;
+
+/**
+ * Sorts project libraries found during sync. Sorters are invoked in reverse EP order, so the
+ * highest-priority sorter should appear first in the EP list.
+ */
+public interface BlazeLibrarySorter {
+
+  ExtensionPointName<BlazeLibrarySorter> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BlazeLibrarySorter");
+
+  static List<BlazeLibrary> sortLibraries(List<BlazeLibrary> libraries) {
+    BlazeLibrarySorter[] sorters = EP_NAME.getExtensions();
+    for (int i = sorters.length - 1; i >= 0; i--) {
+      libraries = sorters[i].sort(libraries);
+    }
+    return libraries;
+  }
+
+  List<BlazeLibrary> sort(List<BlazeLibrary> libraries);
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/LibrarySource.java b/base/src/com/google/idea/blaze/base/sync/libraries/LibrarySource.java
index 5d15a8d..ef1f275 100644
--- a/base/src/com/google/idea/blaze/base/sync/libraries/LibrarySource.java
+++ b/base/src/com/google/idea/blaze/base/sync/libraries/LibrarySource.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.intellij.openapi.roots.libraries.Library;
-import java.util.Collection;
+import java.util.List;
 import java.util.function.Predicate;
 import javax.annotation.Nullable;
 
@@ -26,7 +26,7 @@
 public interface LibrarySource {
 
   /** Called during the project structure phase to get libraries. */
-  Collection<? extends BlazeLibrary> getLibraries();
+  List<? extends BlazeLibrary> getLibraries();
 
   /**
    * Returns a filter on libraries.
@@ -49,7 +49,7 @@
   /** Adapter class */
   abstract class Adapter implements LibrarySource {
     @Override
-    public Collection<? extends BlazeLibrary> getLibraries() {
+    public List<? extends BlazeLibrary> getLibraries() {
       return ImmutableList.of();
     }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
index bae9239..aecac37 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.sync.projectview;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
@@ -24,18 +26,13 @@
 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.intellij.openapi.diagnostic.Logger;
 import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
-import javax.annotation.Nullable;
 
 /** Reads the user's language preferences from the project view. */
 public class LanguageSupport {
 
-  private static final Logger logger = Logger.getInstance(LanguageSupport.class);
-
-  @Nullable
   public static WorkspaceType getDefaultWorkspaceType() {
     WorkspaceType workspaceType = null;
     // prioritize by enum ordinal.
@@ -46,24 +43,19 @@
         workspaceType = recommendedType;
       }
     }
+    // Should never happen, outside of tests without proper set up.
+    checkState(
+        workspaceType != null, "No SyncPlugin present which provides a default workspace type.");
     return workspaceType;
   }
 
   /**
    * Derives {@link WorkspaceLanguageSettings} from the {@link ProjectViewSet}. Does no validation.
    */
-  @Nullable
   public static WorkspaceLanguageSettings createWorkspaceLanguageSettings(
       ProjectViewSet projectViewSet) {
-    WorkspaceType workspaceType = projectViewSet.getScalarValue(WorkspaceTypeSection.KEY);
-    if (workspaceType == null) {
-      workspaceType = getDefaultWorkspaceType();
-    }
-
-    if (workspaceType == null) {
-      logger.error("Could not find workspace type."); // Should never happen
-      return null;
-    }
+    WorkspaceType workspaceType =
+        projectViewSet.getScalarValue(WorkspaceTypeSection.KEY).orElse(getDefaultWorkspaceType());
 
     ImmutableSet.Builder<LanguageClass> activeLanguages =
         ImmutableSet.<LanguageClass>builder()
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java b/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
index 5fdd67f..0b52104 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
@@ -30,7 +30,7 @@
   private static final long serialVersionUID = 1L;
 
   private final WorkspaceType workspaceType;
-  final ImmutableSet<LanguageClass> activeLanguages;
+  public final ImmutableSet<LanguageClass> activeLanguages;
 
   public WorkspaceLanguageSettings(
       WorkspaceType workspaceType, ImmutableSet<LanguageClass> activeLanguages) {
@@ -61,7 +61,7 @@
 
   public EnumSet<Kind> getAvailableTargetKinds() {
     EnumSet<Kind> kinds = EnumSet.allOf(Kind.class);
-    kinds.removeIf(kind -> !activeLanguages.contains(kind.getLanguageClass()));
+    kinds.removeIf(kind -> !activeLanguages.contains(kind.languageClass));
     return kinds;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java b/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java
index 8f2279c..01cf684 100644
--- a/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java
@@ -21,11 +21,12 @@
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.section.sections.ShardBlazeBuildsSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetShardSizeSection;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.aspects.BuildResult;
 import com.google.idea.blaze.base.sync.sharding.WildcardTargetExpander.ExpandedTargetsResult;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
-import com.google.idea.common.experiments.BoolExperiment;
+import com.google.idea.common.experiments.IntExperiment;
 import com.intellij.openapi.project.Project;
 import java.util.ArrayList;
 import java.util.List;
@@ -36,15 +37,13 @@
 /** Utility methods for sharding blaze build invocations. */
 public class BlazeBuildTargetSharder {
 
-  private static final BoolExperiment allowSharding =
-      new BoolExperiment("blaze.build.sharding.allowed", true);
+  /** Default number of individual targets per blaze build shard. Can be overridden by the user. */
+  private static final IntExperiment targetShardSize =
+      new IntExperiment("blaze.target.shard.size", 1000);
 
   // number of packages per blaze query shard
   static final int PACKAGE_SHARD_SIZE = 500;
 
-  // number of individual targets per blaze build shard
-  private static final int TARGET_SHARD_SIZE = 1000;
-
   /** Result of expanding then sharding wildcard target patterns */
   public static class ShardedTargetsResult {
     public final ShardedTargetList shardedTargets;
@@ -56,20 +55,21 @@
     }
   }
 
-  /** Returns true if sharding can be enabled for this project, and is not already enabled */
-  static boolean canEnableSharding(Project project) {
-    if (!allowSharding.getValue()) {
-      return false;
-    }
+  /** Returns true if sharding is already enabled for this project. */
+  static boolean shardingEnabled(Project project) {
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
-    return projectViewSet != null && !shardingEnabled(projectViewSet);
+    return projectViewSet != null && shardingEnabled(projectViewSet);
   }
 
   private static boolean shardingEnabled(ProjectViewSet projectViewSet) {
-    if (!allowSharding.getValue()) {
-      return false;
-    }
-    return projectViewSet.getScalarValue(ShardBlazeBuildsSection.KEY, false);
+    return projectViewSet.getScalarValue(ShardBlazeBuildsSection.KEY).orElse(false);
+  }
+
+  /** Number of individual targets per blaze build shard */
+  static int getTargetShardSize(ProjectViewSet projectViewSet) {
+    return projectViewSet
+        .getScalarValue(TargetShardSizeSection.KEY)
+        .orElse(targetShardSize.getValue());
   }
 
   /** Expand wildcard target patterns and partition the resulting target list. */
@@ -98,7 +98,7 @@
           new ShardedTargetList(ImmutableList.of()), expandedTargets.buildResult);
     }
     return new ShardedTargetsResult(
-        shardTargets(expandedTargets.singleTargets, TARGET_SHARD_SIZE),
+        shardTargets(expandedTargets.singleTargets, getTargetShardSize(projectViewSet)),
         expandedTargets.buildResult);
   }
 
@@ -150,6 +150,9 @@
     for (int index = 0; index < targets.size(); index += shardSize) {
       int endIndex = Math.min(targets.size(), index + shardSize);
       List<TargetExpression> shard = new ArrayList<>(targets.subList(index, endIndex));
+      if (shard.stream().filter(TargetExpression::isExcluded).count() == shard.size()) {
+        continue;
+      }
       List<TargetExpression> remainingExcludes =
           targets
               .subList(endIndex, targets.size())
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java b/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java
index d87d205..5531c21 100644
--- a/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java
@@ -129,12 +129,15 @@
     if (provider.findBuildFileInDirectory(dir) != null) {
       output.add(TargetExpression.allFromPackageNonRecursive(path));
     }
-    File[] children = FileAttributeProvider.getInstance().listFiles(dir);
+    FileAttributeProvider attributeProvider = FileAttributeProvider.getInstance();
+    File[] children = attributeProvider.listFiles(dir);
     if (children == null) {
       return;
     }
     for (File child : children) {
-      traversePackageRecursively(provider, pathResolver, child, output);
+      if (attributeProvider.isDirectory(child)) {
+        traversePackageRecursively(provider, pathResolver, child, output);
+      }
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/SuggestBuildShardingNotification.java b/base/src/com/google/idea/blaze/base/sync/sharding/SuggestBuildShardingNotification.java
new file mode 100644
index 0000000..0fe13e2
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/SuggestBuildShardingNotification.java
@@ -0,0 +1,163 @@
+/*
+ * 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.sync.sharding;
+
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit.ProjectViewEditor;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.ShardBlazeBuildsSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetShardSizeSection;
+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;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationListener;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.util.Consumer;
+import com.intellij.xml.util.XmlStringUtil;
+import javax.swing.event.HyperlinkEvent;
+
+/**
+ * If blaze runs out of memory during sync, suggest that the user enables build sharding, or tweaks
+ * the shard sizes if sharding is already enabled.
+ */
+public class SuggestBuildShardingNotification {
+
+  /** Displays any sharding-related notifications (with quick fixes) appropriate to the OOME. */
+  public static void syncOutOfMemoryError(Project project, BlazeContext context) {
+    if (BlazeBuildTargetSharder.shardingEnabled(project)) {
+      suggestReducingShardSize(project, context);
+    } else {
+      suggestSharding(project, context);
+    }
+  }
+
+  private static void suggestReducingShardSize(Project project, BlazeContext context) {
+    String buildSystem = Blaze.buildSystemName(project);
+    String message =
+        String.format(
+            "The %1$s server ran out of memory during sync. You can workaround this by "
+                + "<a href='fix'>reducing the shard size</a> in the project view file, "
+                + "or alternatively allocate more memory to %1$s",
+            buildSystem);
+    IssueOutput.error(
+            StringUtil.stripHtml(message, true) + ". Click here to reduce the shard " + "size.")
+        .navigatable(
+            new NavigatableAdapter() {
+              @Override
+              public void navigate(boolean requestFocus) {
+                reduceShardSizeAndResync(project);
+              }
+            })
+        .submit(context);
+
+    showNotification(project, message, SuggestBuildShardingNotification::reduceShardSizeAndResync);
+  }
+
+  private static void suggestSharding(Project project, BlazeContext context) {
+    String buildSystem = Blaze.buildSystemName(project);
+    String message =
+        String.format(
+            "The %1$s server ran out of memory during sync. This can occur for large projects. You "
+                + "can workaround this by <a href='fix'>sharding the %1$s build during sync</a>, "
+                + "or alternatively allocate more memory to %1$s",
+            buildSystem);
+    IssueOutput.error(StringUtil.stripHtml(message, true) + ". Click here to set up sync sharding.")
+        .navigatable(
+            new NavigatableAdapter() {
+              @Override
+              public void navigate(boolean requestFocus) {
+                enableShardingAndResync(project);
+              }
+            })
+        .submit(context);
+
+    showNotification(project, message, SuggestBuildShardingNotification::enableShardingAndResync);
+  }
+
+  private static void showNotification(
+      Project project, String message, Consumer<Project> projectViewEditor) {
+    Notification notification =
+        new Notification(
+            "Out of memory during sync",
+            Blaze.buildSystemName(project) + " ran out of memory during sync",
+            XmlStringUtil.wrapInHtml(message),
+            NotificationType.ERROR,
+            new NotificationListener.Adapter() {
+              @Override
+              protected void hyperlinkActivated(
+                  Notification notification, HyperlinkEvent hyperlinkEvent) {
+                notification.expire();
+                projectViewEditor.consume(project);
+              }
+            });
+    notification.setImportant(true);
+    ApplicationManager.getApplication().invokeLater(() -> notification.notify(project));
+  }
+
+  /** Halve the previous shard size and resync. */
+  private static void reduceShardSizeAndResync(Project project) {
+    int previousShardSize =
+        BlazeBuildTargetSharder.getTargetShardSize(
+            ProjectViewManager.getInstance(project).getProjectViewSet());
+    editProjectViewAndResync(
+        project,
+        builder -> {
+          ScalarSection<Integer> existingSection = builder.getLast(TargetShardSizeSection.KEY);
+          builder.replace(
+              existingSection,
+              ScalarSection.builder(TargetShardSizeSection.KEY).set(previousShardSize / 2));
+          return true;
+        });
+  }
+
+  private static void enableShardingAndResync(Project project) {
+    editProjectViewAndResync(
+        project,
+        builder -> {
+          ScalarSection<Boolean> existingSection = builder.getLast(ShardBlazeBuildsSection.KEY);
+          builder.replace(
+              existingSection, ScalarSection.builder(ShardBlazeBuildsSection.KEY).set(true));
+          return true;
+        });
+  }
+
+  private static void editProjectViewAndResync(Project project, ProjectViewEditor editor) {
+    ProjectViewEdit edit = ProjectViewEdit.editLocalProjectView(project, editor);
+    if (edit == null) {
+      Messages.showErrorDialog(
+          "Could not modify project view. Check for errors in your project view and try again",
+          "Error");
+      return;
+    }
+    edit.apply();
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java b/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java
deleted file mode 100644
index 71f69c0..0000000
--- a/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java
+++ /dev/null
@@ -1,103 +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.sync.sharding;
-
-import com.google.idea.blaze.base.projectview.ProjectViewEdit;
-import com.google.idea.blaze.base.projectview.section.ScalarSection;
-import com.google.idea.blaze.base.projectview.section.sections.ShardBlazeBuildsSection;
-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;
-import com.google.idea.blaze.base.settings.BlazeUserSettings;
-import com.google.idea.blaze.base.sync.BlazeSyncManager;
-import com.google.idea.blaze.base.sync.BlazeSyncParams;
-import com.intellij.notification.Notification;
-import com.intellij.notification.NotificationListener;
-import com.intellij.notification.NotificationType;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.Messages;
-import com.intellij.pom.NavigatableAdapter;
-import com.intellij.xml.util.XmlStringUtil;
-import javax.swing.event.HyperlinkEvent;
-
-/** If blaze runs out of memory during sync, suggest that the user enables build sharding. */
-public class SuggestEnablingShardingNotification {
-
-  public static void suggestSharding(Project project, BlazeContext context) {
-    if (!BlazeBuildTargetSharder.canEnableSharding(project)) {
-      return;
-    }
-    String buildSystem = Blaze.buildSystemName(project);
-    String message =
-        String.format(
-            "The %1$s server ran out of memory during sync. This can occur for large projects. You "
-                + "can workaround this by <a href='fix'>sharding the %1$s build during sync</a>, "
-                + "or alternatively allocate more memory to %1$s",
-            buildSystem);
-    IssueOutput.error(message)
-        .navigatable(
-            new NavigatableAdapter() {
-              @Override
-              public void navigate(boolean requestFocus) {
-                enableShardingAndResync(project);
-              }
-            })
-        .submit(context);
-
-    Notification notification =
-        new Notification(
-            "Out of memory during sync",
-            buildSystem + " ran out of memory during sync",
-            XmlStringUtil.wrapInHtml(message),
-            NotificationType.ERROR,
-            new NotificationListener.Adapter() {
-              @Override
-              protected void hyperlinkActivated(
-                  Notification notification, HyperlinkEvent hyperlinkEvent) {
-                notification.expire();
-                enableShardingAndResync(project);
-              }
-            });
-    notification.setImportant(true);
-    ApplicationManager.getApplication().invokeLater(() -> notification.notify(project));
-  }
-
-  private static void enableShardingAndResync(Project project) {
-    ProjectViewEdit edit =
-        ProjectViewEdit.editLocalProjectView(
-            project,
-            builder -> {
-              ScalarSection<Boolean> existingSection = builder.getLast(ShardBlazeBuildsSection.KEY);
-              builder.replace(
-                  existingSection, ScalarSection.builder(ShardBlazeBuildsSection.KEY).set(true));
-              return true;
-            });
-    if (edit == null) {
-      Messages.showErrorDialog(
-          "Could not modify project view. Check for errors in your project view and try again",
-          "Error");
-      return;
-    }
-    edit.apply();
-    BlazeSyncManager.getInstance(project)
-        .requestProjectSync(
-            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
-                .addProjectViewTargets(true)
-                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
-                .build());
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java
index 396c1cf..10ab8aa 100644
--- a/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java
@@ -21,11 +21,11 @@
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.async.process.ExternalTask;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import com.google.idea.blaze.base.async.process.PrintOutputLineProcessor;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 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.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -36,6 +36,7 @@
 import com.google.idea.blaze.base.scope.output.StatusOutput;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.aspects.BuildResult;
+import com.google.idea.blaze.base.sync.aspects.BuildResult.Status;
 import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
 import com.google.idea.blaze.base.sync.sharding.QueryResultLineProcessor.RuleTypeAndLabel;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
@@ -108,7 +109,7 @@
         PackageLister.getDirectoriesToPrefetch(pathResolver, includes, excludePredicate);
 
     ListenableFuture<?> prefetchFuture =
-        PrefetchService.getInstance().prefetchFiles(project, toPrefetch);
+        PrefetchService.getInstance().prefetchFiles(project, toPrefetch, false);
     if (!FutureUtil.waitForFuture(context, prefetchFuture)
         .withProgressMessage("Prefetching wildcard target pattern directories...")
         .timed("PrefetchingWildcardTargetDirectories")
@@ -144,7 +145,7 @@
       ExpandedTargetsResult result =
           queryIndividualTargets(project, context, workspaceRoot, handledRuleTypes, shard);
       output = output == null ? result : ExpandedTargetsResult.merge(output, result);
-      if (output.buildResult == BuildResult.FATAL_ERROR) {
+      if (output.buildResult.status == Status.FATAL_ERROR) {
         return output;
       }
     }
@@ -158,6 +159,11 @@
       WorkspaceRoot workspaceRoot,
       ImmutableSet<String> handledRuleTypes,
       List<TargetExpression> targetPatterns) {
+    String query = queryString(targetPatterns);
+    if (query.isEmpty()) {
+      // will be empty if there are no non-excluded targets
+      return new ExpandedTargetsResult(ImmutableList.of(), BuildResult.SUCCESS);
+    }
     BlazeCommand.Builder builder =
         BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.QUERY)
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
@@ -179,7 +185,9 @@
             .addBlazeCommand(builder.build())
             .context(context)
             .stdout(LineProcessingOutputStream.of(new QueryResultLineProcessor(output, filter)))
-            .stderr(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
+            .stderr(
+                LineProcessingOutputStream.of(
+                    new IssueOutputLineProcessor(project, context, workspaceRoot)))
             .build()
             .run();
 
diff --git a/base/src/com/google/idea/blaze/base/ui/UiUtil.java b/base/src/com/google/idea/blaze/base/ui/UiUtil.java
index 9f28ccb..ca4f65e 100644
--- a/base/src/com/google/idea/blaze/base/ui/UiUtil.java
+++ b/base/src/com/google/idea/blaze/base/ui/UiUtil.java
@@ -38,7 +38,7 @@
   }
 
   /** Puts all the given components in order in a box, aligned left. */
-  public static Box createBox(@NotNull Iterable<Component> components) {
+  public static Box createBox(@NotNull Iterable<? extends Component> components) {
     Box box = Box.createVerticalBox();
     box.setAlignmentX(0);
     for (Component component : components) {
diff --git a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
index ea689da..ca03904 100644
--- a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
@@ -26,13 +26,16 @@
 import java.io.ObjectStreamClass;
 import java.io.Serializable;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
 /** Utils for serialization. */
 public class SerializationUtil {
 
-  public static void saveToDisk(@NotNull File file, @NotNull Serializable serializable)
-      throws IOException {
+  /**
+   * Write {@link Serializable} to disk.
+   *
+   * @throws IOException if serialization fails.
+   */
+  public static void saveToDisk(File file, Serializable serializable) throws IOException {
     ensureExists(file.getParentFile());
     FileOutputStream fos = null;
     try {
@@ -48,48 +51,46 @@
     }
   }
 
+  /**
+   * Read the serialized objects from disk. Returns null if the file doesn't exist or is empty.
+   *
+   * @throws IOException if deserialization fails.
+   */
   @Nullable
-  public static Object loadFromDisk(
-      @NotNull File file, @NotNull final Iterable<ClassLoader> classLoaders) throws IOException {
-    try {
-      FileInputStream fin = null;
-      try {
-        if (!file.exists()) {
-          return null;
-        }
-        fin = new FileInputStream(file);
-        ObjectInputStream ois =
-            new ObjectInputStream(fin) {
-              @Override
-              protected Class<?> resolveClass(ObjectStreamClass desc)
-                  throws IOException, ClassNotFoundException {
-                String name = desc.getName();
-                for (ClassLoader loader : classLoaders) {
-                  try {
-                    return Class.forName(name, false, loader);
-                  } catch (ClassNotFoundException e) {
-                    // Ignore - will throw eventually in super
-                  }
+  public static Object loadFromDisk(File file, final Iterable<ClassLoader> classLoaders)
+      throws IOException {
+    if (!file.exists()) {
+      return null;
+    }
+    try (FileInputStream fin = new FileInputStream(file)) {
+      ObjectInputStream ois =
+          new ObjectInputStream(fin) {
+            @Override
+            protected Class<?> resolveClass(ObjectStreamClass desc)
+                throws IOException, ClassNotFoundException {
+              String name = desc.getName();
+              for (ClassLoader loader : classLoaders) {
+                try {
+                  return Class.forName(name, false, loader);
+                } catch (ClassNotFoundException e) {
+                  // Ignore - will throw eventually in super
                 }
-                return super.resolveClass(desc);
               }
-            };
-        try {
-          return (Object) ois.readObject();
-        } finally {
-          Closeables.close(ois, false);
-        }
+              return super.resolveClass(desc);
+            }
+          };
+      try {
+        return ois.readObject();
       } finally {
-        Closeables.close(fin, false);
+        Closeables.close(ois, false);
       }
-    } catch (ClassNotFoundException e) {
-      throw new IOException(e);
-    } catch (ClassCastException e) {
+    } catch (ClassNotFoundException | ClassCastException | IllegalStateException e) {
+      // rethrow as an IOException, handled by callers
       throw new IOException(e);
     }
   }
 
-  private static void ensureExists(@NotNull File dir) throws IOException {
+  private static void ensureExists(File dir) throws IOException {
     if (!dir.exists() && !dir.mkdirs()) {
       throw new IOException(
           CommonBundle.message("exception.directory.can.not.create", dir.getPath()));
diff --git a/base/src/com/google/idea/blaze/base/util/UrlUtil.java b/base/src/com/google/idea/blaze/base/util/UrlUtil.java
index a2cc7b2..0567319 100644
--- a/base/src/com/google/idea/blaze/base/util/UrlUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/UrlUtil.java
@@ -17,10 +17,12 @@
 
 import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
 import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFileManager;
 import com.intellij.util.io.URLUtil;
 import java.io.File;
+import javax.annotation.Nullable;
 
 /** Utility methods for converting between URLs and file paths. */
 public class UrlUtil {
@@ -30,7 +32,7 @@
   }
 
   public static String fileToIdeaUrl(File path) {
-    return pathToUrl(toSystemIndependentName(path.getPath()));
+    return pathToUrl(FileUtil.toSystemIndependentName(path.getPath()));
   }
 
   public static String pathToUrl(String filePath) {
@@ -45,7 +47,16 @@
     }
   }
 
-  private static String toSystemIndependentName(String aFileName) {
-    return FileUtilRt.toSystemIndependentName(aFileName);
+  /**
+   * Returns the local file path associated with the given URL, or null if it doesn't refer to a
+   * local file.
+   */
+  @Nullable
+  public static String urlToFilePath(@Nullable String url) {
+    if (url == null || !url.startsWith(LocalFileSystem.PROTOCOL_PREFIX)) {
+      return null;
+    }
+    return FileUtil.toSystemDependentName(
+        StringUtil.trimStart(url, LocalFileSystem.PROTOCOL_PREFIX));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java b/base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java
index 4e58d0d..9743cad 100644
--- a/base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java
+++ b/base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java
@@ -61,7 +61,7 @@
           if (upstreamSha == null) {
             return null;
           }
-          return GitWorkingSetProvider.calculateWorkingSet(workspaceRoot, upstreamSha);
+          return GitWorkingSetProvider.calculateWorkingSet(workspaceRoot, upstreamSha, context);
         });
   }
 
diff --git a/base/src/com/google/idea/blaze/base/vcs/git/GitWorkingSetProvider.java b/base/src/com/google/idea/blaze/base/vcs/git/GitWorkingSetProvider.java
index 465c6ac..1fd70f4 100644
--- a/base/src/com/google/idea/blaze/base/vcs/git/GitWorkingSetProvider.java
+++ b/base/src/com/google/idea/blaze/base/vcs/git/GitWorkingSetProvider.java
@@ -21,6 +21,8 @@
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.text.StringUtil;
@@ -40,7 +42,8 @@
    * Returns null if an error occurred.
    */
   @Nullable
-  public static WorkingSet calculateWorkingSet(WorkspaceRoot workspaceRoot, String upstreamSha) {
+  public static WorkingSet calculateWorkingSet(
+      WorkspaceRoot workspaceRoot, String upstreamSha, BlazeContext context) {
 
     String gitRoot = getConsoleOutput(workspaceRoot, "git", "rev-parse", "--show-toplevel");
     if (gitRoot == null) {
@@ -53,10 +56,11 @@
     int retVal =
         ExternalTask.builder(workspaceRoot)
             .args("git", "diff", "--name-status", "--no-renames", upstreamSha)
+            .context(context)
             .stdout(LineProcessingOutputStream.of(processor))
             .stderr(stderr)
             .build()
-            .run();
+            .run(new TimingScope("GitDiff"));
     if (retVal != 0) {
       logger.error(stderr);
       return null;
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java b/base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java
index 9e62e78..99a6b1d 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.wizard2;
 
 import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.Disposable;
 import java.util.Collection;
 
 /** Provides bazel options for the wizard. */
@@ -23,7 +24,7 @@
 
   @Override
   public Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(
-      BlazeNewProjectBuilder builder) {
+      BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     return ImmutableList.of(new UseExistingBazelWorkspaceOption(builder));
   }
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
index 1739755..904e0e8 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
@@ -15,12 +15,14 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
+import com.intellij.openapi.Disposable;
 import com.intellij.openapi.components.ServiceManager;
 import java.util.Collection;
 
 /** Provides options during the import process. */
 public interface BlazeWizardOptionProvider {
-  Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(BlazeNewProjectBuilder builder);
+  Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(
+      BlazeNewProjectBuilder builder, Disposable parentDisposable);
 
   Collection<BlazeSelectProjectViewOption> getSelectProjectViewOptions(
       BlazeNewProjectBuilder builder);
diff --git a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
index ae00949..25a63e3 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
@@ -86,6 +86,9 @@
     if (!file.exists()) {
       return BlazeValidationResult.failure("Project view file does not exist.");
     }
+    if (file.isDirectory()) {
+      return BlazeValidationResult.failure("Specified path is a directory, not a file");
+    }
     return BlazeValidationResult.success();
   }
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
index 577f290..f445e44 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -110,6 +110,9 @@
     if (!file.exists()) {
       return BlazeValidationResult.failure("BUILD file does not exist.");
     }
+    if (file.isDirectory()) {
+      return BlazeValidationResult.failure("Specified path is a directory, not a file");
+    }
     BuildSystemProvider buildSystemProvider =
         BuildSystemProvider.getBuildSystemProvider(builder.getBuildSystem());
     checkState(buildSystemProvider != null);
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
index 14a07e0..c727a45 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -98,6 +98,9 @@
     if (!file.exists()) {
       return BlazeValidationResult.failure("Project view file does not exist.");
     }
+    if (file.isDirectory()) {
+      return BlazeValidationResult.failure("Specified path is a directory, not a file");
+    }
 
     return BlazeValidationResult.success();
   }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
index 5b652d8..e5d0bc7 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
@@ -70,18 +70,20 @@
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.ui.components.JBLabel;
 import com.intellij.util.SystemProperties;
+import java.awt.BorderLayout;
 import java.awt.Component;
-import java.awt.Dimension;
 import java.awt.GridBagLayout;
 import java.io.File;
 import java.io.IOException;
 import java.util.Comparator;
 import java.util.List;
 import javax.annotation.Nullable;
+import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
 import javax.swing.JTextField;
 import org.jetbrains.annotations.NotNull;
 
@@ -122,10 +124,14 @@
 
   public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     this.projectViewUi = new ProjectViewUi(parentDisposable);
-    JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
-    fillUi(component);
+    JPanel content = new JPanel(new GridBagLayout());
+    fillUi(content);
     update(builder);
-    UiUtil.fillBottom(component);
+    UiUtil.fillBottom(content);
+    JScrollPane scrollPane = new JScrollPane(content);
+    scrollPane.setBorder(BorderFactory.createEmptyBorder());
+    JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new BorderLayout());
+    component.add(scrollPane);
     this.component = component;
     this.buildSystemName = builder.getBuildSystemName();
   }
@@ -137,11 +143,7 @@
   private void fillUi(JPanel canvas) {
     JLabel projectDataDirLabel = new JBLabel("Project data directory:");
 
-    Dimension minSize = ProjectViewUi.getMinimumSize();
-    // Add pixels so we have room for our extra fields
-    minSize.setSize(minSize.width, minSize.height + 180);
-    canvas.setMinimumSize(minSize);
-    canvas.setPreferredSize(minSize);
+    canvas.setPreferredSize(ProjectViewUi.getContainerSize());
 
     projectDataDirField = new TextFieldWithBrowseButton();
     projectDataDirField.addBrowseFolderListener(
@@ -491,7 +493,6 @@
       return BlazeValidationResult.failure(msg);
     }
 
-
     return BlazeValidationResult.success();
   }
 
@@ -528,9 +529,6 @@
       }
       WorkspaceLanguageSettings workspaceLanguageSettings =
           LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
-      if (workspaceLanguageSettings == null) {
-        return false;
-      }
       return ProjectViewVerifier.verifyProjectView(
           null, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java
index 69a36e4..132f2dd 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java
@@ -25,13 +25,15 @@
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.ui.components.panels.HorizontalLayout;
 import com.intellij.ui.components.panels.VerticalLayout;
-import java.awt.Dimension;
+import java.awt.BorderLayout;
 import java.util.Collection;
+import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
+import javax.swing.JScrollPane;
 import javax.swing.JSeparator;
 import javax.swing.border.EmptyBorder;
 
@@ -61,10 +63,9 @@
 
     this.userSettings = builder.getUserSettings();
 
-    JPanel canvas = new JPanel(new VerticalLayout(4));
+    JPanel canvas = new JPanel(new BorderLayout(0, 4));
 
-    Dimension minSize = ProjectViewUi.getMinimumSize();
-    canvas.setPreferredSize(minSize);
+    canvas.setPreferredSize(ProjectViewUi.getContainerSize());
 
     titleLabel = new JLabel(getTitle());
     canvas.add(titleLabel);
@@ -72,7 +73,9 @@
 
     JPanel content = new JPanel(new VerticalLayout(12));
     content.setBorder(new EmptyBorder(20, 100, 0, 0));
-    canvas.add(content);
+    JScrollPane scrollPane = new JScrollPane(content);
+    scrollPane.setBorder(BorderFactory.createEmptyBorder());
+    canvas.add(scrollPane);
 
     ButtonGroup buttonGroup = new ButtonGroup();
     Collection<OptionUiEntry<T>> optionUiEntryList = Lists.newArrayList();
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
index e24b446..62b2a7c 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
 import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
 import com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider;
+import com.intellij.openapi.Disposable;
 import java.util.Collection;
 import javax.swing.JComponent;
 
@@ -26,9 +27,10 @@
 public class BlazeSelectWorkspaceControl {
   BlazeSelectOptionControl<BlazeSelectWorkspaceOption> selectOptionControl;
 
-  public BlazeSelectWorkspaceControl(BlazeNewProjectBuilder builder) {
+  public BlazeSelectWorkspaceControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     Collection<BlazeSelectWorkspaceOption> options =
-        BlazeWizardOptionProvider.getInstance().getSelectWorkspaceOptions(builder);
+        BlazeWizardOptionProvider.getInstance()
+            .getSelectWorkspaceOptions(builder, parentDisposable);
 
     this.selectOptionControl =
         new BlazeSelectOptionControl<BlazeSelectWorkspaceOption>(builder, options) {
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
index 691dc74..2790cea 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
@@ -22,6 +22,8 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.util.Computable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -34,8 +36,16 @@
   public void testAddNewTarget() {
     BuildFile buildFile =
         createBuildFile(new WorkspacePath("BUILD"), "java_library(name = 'existing')", "");
-    BuildFileModifier.getInstance()
-        .addRule(getProject(), new BlazeContext(), Label.create("//:new_target"), Kind.JAVA_TEST);
+    WriteCommandAction.runWriteCommandAction(
+        getProject(),
+        (Computable<Boolean>)
+            () ->
+                BuildFileModifier.getInstance()
+                    .addRule(
+                        getProject(),
+                        new BlazeContext(),
+                        Label.create("//:new_target"),
+                        Kind.JAVA_TEST));
     assertFileContents(
         buildFile,
         "java_library(name = 'existing')",
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
index dcf3e6b..3e67be8 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
@@ -17,14 +17,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.producers.BlazeBuildFileRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.actions.ConfigurationFromContext;
 import com.intellij.psi.PsiFile;
@@ -87,4 +91,68 @@
         .isEqualTo(TargetExpression.fromString("//java/com/google/test:unit_tests"));
     assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
   }
+
+  @Test
+  public void testConfigFromContextRecognizesItsOwnConfig() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    StringLiteral nameString =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, StringLiteral.class);
+    ConfigurationContext context = createContextFromPsi(nameString);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+
+    assertThat(
+            new BlazeBuildFileRunConfigurationProducer()
+                .isConfigurationFromContext(config, context))
+        .isTrue();
+  }
+
+  @Test
+  public void testConfigWithDifferentLabelIgnored() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    StringLiteral nameString =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, StringLiteral.class);
+    ConfigurationContext context = createContextFromPsi(nameString);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+
+    // modify the label, and check that is enough for the producer to class it as different.
+    config.setTarget(Label.create("//java/com/google/test:integration_tests"));
+
+    assertThat(
+            new BlazeBuildFileRunConfigurationProducer()
+                .isConfigurationFromContext(config, context))
+        .isFalse();
+  }
+
+  @Test
+  public void testConfigWithTestFilterIgnored() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    StringLiteral nameString =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, StringLiteral.class);
+    ConfigurationContext context = createContextFromPsi(nameString);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    handlerState
+        .getBlazeFlagsState()
+        .setRawFlags(
+            ImmutableList.of(BlazeFlags.TEST_FILTER + "=com.google.test.SingleTestClass#"));
+
+    assertThat(
+            new BlazeBuildFileRunConfigurationProducer()
+                .isConfigurationFromContext(config, context))
+        .isFalse();
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
index 23e40c8..18fed93 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
@@ -107,6 +107,22 @@
     verify(service).buildTargetExpressions(eq(project), eq(targets), eq(viewSet), any());
   }
 
+  @Test
+  public void testLastBuildProjectTimestampIsNull() {
+    assertThat(BlazeBuildService.getLastBuildTimeStamp(project)).isNull();
+  }
+
+  @Test
+  public void testGetLastBuildProjectTimestamp() {
+    long beforeTime = System.currentTimeMillis() - 1;
+    service.buildProject(project);
+    long afterTime = System.currentTimeMillis() + 1;
+    Long timestamp = BlazeBuildService.getLastBuildTimeStamp(project);
+    assertThat(timestamp).isNotNull();
+    assertThat(timestamp).isGreaterThan(beforeTime);
+    assertThat(timestamp).isLessThan(afterTime);
+  }
+
   private static class MockProjectViewManager extends ProjectViewManager {
     private final ProjectViewSet viewSet;
 
diff --git a/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java b/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java
index 5860e40..673f810 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java
@@ -53,9 +53,10 @@
   }
 
   @Test
-  public void testParseVersionFormatManualOld() {
+  public void testParseDevelopmentVersion() {
     BazelVersion version = BazelVersion.parseVersion("development version");
-    assertThat(version).isEqualTo(BazelVersion.UNKNOWN);
+    assertThat(version).isEqualTo(BazelVersion.DEVELOPMENT);
+    assertThat(version.isAtLeast(9, 9, 9)).isTrue();
   }
 
   @Test
diff --git a/base/tests/unittests/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReaderTest.java b/base/tests/unittests/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReaderTest.java
new file mode 100644
index 0000000..7026b1d
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/command/buildresult/BuildEventProtocolOutputReaderTest.java
@@ -0,0 +1,412 @@
+/*
+ * 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.common.truth.Truth.assertThat;
+import static com.google.idea.common.guava.GuavaHelper.toImmutableList;
+
+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.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+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.TargetCompletedId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TargetConfiguredId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TestResultId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.NamedSetOfFiles;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TargetComplete;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TargetConfigured;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.TestResult;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+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 BuildEventProtocolOutputReader}. */
+@RunWith(JUnit4.class)
+public class BuildEventProtocolOutputReaderTest {
+
+  @Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+  @Test
+  public void parseAllOutputFilenames_singleFileEvent_returnsAllFilenames() throws IOException {
+    ImmutableList<String> filePaths =
+        ImmutableList.of(
+            "/usr/local/lib/File.py", "/usr/bin/python2.7", "/usr/local/home/script.sh");
+    BuildEvent.Builder event = BuildEvent.newBuilder().setNamedSetOfFiles(setOfFiles(filePaths));
+
+    ImmutableList<File> parsedFilenames =
+        BuildEventProtocolOutputReader.parseAllOutputFilenames(asInputStream(event), path -> true);
+
+    assertThat(parsedFilenames)
+        .containsExactly(filePaths.stream().map(File::new).toArray())
+        .inOrder();
+  }
+
+  @Test
+  public void parseAllOutputFilenamesWithFilter_singleFileEvent_returnsFilteredFilenames()
+      throws IOException {
+    Predicate<String> filter = path -> path.endsWith(".py");
+    ImmutableList<String> filePaths =
+        ImmutableList.of(
+            "/usr/local/lib/File.py", "/usr/bin/python2.7", "/usr/local/home/script.sh");
+    BuildEvent.Builder event = BuildEvent.newBuilder().setNamedSetOfFiles(setOfFiles(filePaths));
+
+    ImmutableList<File> parsedFilenames =
+        BuildEventProtocolOutputReader.parseAllOutputFilenames(asInputStream(event), filter);
+
+    assertThat(parsedFilenames).containsExactly(new File("/usr/local/lib/File.py"));
+  }
+
+  @Test
+  public void parseAllOutputFilenames_nonFileEvent_returnsEmptyList() throws IOException {
+    BuildEvent.Builder targetFinishedEvent =
+        BuildEvent.newBuilder()
+            .setCompleted(BuildEventStreamProtos.TargetComplete.getDefaultInstance());
+
+    ImmutableList<File> parsedFilenames =
+        BuildEventProtocolOutputReader.parseAllOutputFilenames(
+            asInputStream(targetFinishedEvent), path -> true);
+
+    assertThat(parsedFilenames).isEmpty();
+  }
+
+  @Test
+  public void parseAllOutputFilenames_streamWithOneFileEvent_returnsAllFilenames()
+      throws IOException {
+    ImmutableList<String> filePaths =
+        ImmutableList.of(
+            "/usr/local/lib/Provider.java",
+            "/usr/local/home/Executor.java",
+            "/google/code/script.sh");
+    List<BuildEvent.Builder> events =
+        ImmutableList.of(
+            BuildEvent.newBuilder()
+                .setStarted(BuildEventStreamProtos.BuildStarted.getDefaultInstance()),
+            BuildEvent.newBuilder()
+                .setProgress(BuildEventStreamProtos.Progress.getDefaultInstance()),
+            BuildEvent.newBuilder().setNamedSetOfFiles(setOfFiles(filePaths)));
+
+    ImmutableList<File> parsedFilenames =
+        BuildEventProtocolOutputReader.parseAllOutputFilenames(asInputStream(events), path -> true);
+
+    assertThat(parsedFilenames)
+        .containsExactly(filePaths.stream().map(File::new).toArray())
+        .inOrder();
+  }
+
+  @Test
+  public void parseAllOutputFilenames_streamWithMultipleFileEvents_returnsAllFilenames()
+      throws IOException {
+    ImmutableList<String> fileSet1 =
+        ImmutableList.of(
+            "/usr/local/lib/Provider.java",
+            "/usr/local/home/Executor.java",
+            "/google/code/script.sh");
+
+    ImmutableList<String> fileSet2 =
+        ImmutableList.of(
+            "/usr/local/code/ParserTest.java",
+            "/usr/local/code/action_output.bzl",
+            "/usr/genfiles/BUILD.bazel");
+
+    List<BuildEvent.Builder> events =
+        ImmutableList.of(
+            BuildEvent.newBuilder()
+                .setStarted(BuildEventStreamProtos.BuildStarted.getDefaultInstance()),
+            BuildEvent.newBuilder()
+                .setProgress(BuildEventStreamProtos.Progress.getDefaultInstance()),
+            BuildEvent.newBuilder().setNamedSetOfFiles(setOfFiles(fileSet1)),
+            BuildEvent.newBuilder()
+                .setProgress(BuildEventStreamProtos.Progress.getDefaultInstance()),
+            BuildEvent.newBuilder().setNamedSetOfFiles(setOfFiles(fileSet2)),
+            BuildEvent.newBuilder()
+                .setCompleted(BuildEventStreamProtos.TargetComplete.getDefaultInstance()));
+
+    ImmutableList<File> allFiles =
+        ImmutableList.<String>builder()
+            .addAll(fileSet1)
+            .addAll(fileSet2)
+            .build()
+            .stream()
+            .map(File::new)
+            .collect(toImmutableList());
+
+    ImmutableList<File> parsedFilenames =
+        BuildEventProtocolOutputReader.parseAllOutputFilenames(asInputStream(events), path -> true);
+
+    assertThat(parsedFilenames).containsExactlyElementsIn(allFiles).inOrder();
+  }
+
+  @Test
+  public void testStatusEnum_handlesAllProtoEnumValues() {
+    Set<String> protoValues =
+        EnumSet.allOf(BuildEventStreamProtos.TestStatus.class)
+            .stream()
+            .map(Enum::name)
+            .collect(Collectors.toSet());
+    protoValues.remove(BuildEventStreamProtos.TestStatus.UNRECOGNIZED.name());
+    Set<String> handledValues =
+        EnumSet.allOf(TestStatus.class).stream().map(Enum::name).collect(Collectors.toSet());
+
+    assertThat(protoValues).containsExactlyElementsIn(handledValues);
+  }
+
+  @Test
+  public void parseTestResults_singleEvent_returnsTestResults() throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.FAILED;
+    ImmutableList<String> filePaths = ImmutableList.of("/usr/local/tmp/_cache/test_result.xml");
+    BuildEvent.Builder event = testResultEvent(label.toString(), status, filePaths);
+
+    BlazeTestResults results =
+        BuildEventProtocolOutputReader.parseTestResults(asInputStream(event));
+
+    assertThat(results.perTargetResults.keySet()).containsExactly(label);
+    assertThat(results.perTargetResults.get(label)).hasSize(1);
+    BlazeTestResult result = results.perTargetResults.get(label).iterator().next();
+    assertThat(result.getTestStatus()).isEqualTo(TestStatus.FAILED);
+    assertThat(result.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+  }
+
+  @Test
+  public void parseTestResults_singleTestEventWithTargetConfigured_resultsIncludeTargetKind()
+      throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.FAILED;
+    ImmutableList<String> filePaths = ImmutableList.of("/usr/local/tmp/_cache/test_result.xml");
+    InputStream events =
+        asInputStream(
+            targetConfiguredEvent(label.toString(), "java_test rule"),
+            testResultEvent(label.toString(), status, filePaths));
+
+    BlazeTestResults results = BuildEventProtocolOutputReader.parseTestResults(events);
+
+    assertThat(results.perTargetResults.keySet()).containsExactly(label);
+    assertThat(results.perTargetResults.get(label)).hasSize(1);
+    BlazeTestResult result = results.perTargetResults.get(label).iterator().next();
+    assertThat(result.getTargetKind()).isEqualTo(Kind.JAVA_TEST);
+    assertThat(result.getTestStatus()).isEqualTo(TestStatus.FAILED);
+    assertThat(result.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+  }
+
+  @Test
+  public void parseTestResults_singleTestEventWithTargetCompleted_resultsIncludeTargetKind()
+      throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.FAILED;
+    ImmutableList<String> filePaths = ImmutableList.of("/usr/local/tmp/_cache/test_result.xml");
+    InputStream events =
+        asInputStream(
+            targetCompletedEvent(label.toString(), "java_test rule"),
+            testResultEvent(label.toString(), status, filePaths));
+
+    BlazeTestResults results = BuildEventProtocolOutputReader.parseTestResults(events);
+
+    assertThat(results.perTargetResults.keySet()).containsExactly(label);
+    assertThat(results.perTargetResults.get(label)).hasSize(1);
+    BlazeTestResult result = results.perTargetResults.get(label).iterator().next();
+    assertThat(result.getTargetKind()).isEqualTo(Kind.JAVA_TEST);
+    assertThat(result.getTestStatus()).isEqualTo(TestStatus.FAILED);
+    assertThat(result.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+  }
+
+  @Test
+  public void parseTestResults_multipleTargetKindSources_resultsIncludeCorrectTargetKind()
+      throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.FAILED;
+    ImmutableList<String> filePaths = ImmutableList.of("/usr/local/tmp/_cache/test_result.xml");
+    InputStream events =
+        asInputStream(
+            targetConfiguredEvent(label.toString(), "java_test rule"),
+            targetCompletedEvent(label.toString(), "java_test rule"),
+            testResultEvent(label.toString(), status, filePaths));
+
+    BlazeTestResults results = BuildEventProtocolOutputReader.parseTestResults(events);
+
+    assertThat(results.perTargetResults.keySet()).containsExactly(label);
+    assertThat(results.perTargetResults.get(label)).hasSize(1);
+    BlazeTestResult result = results.perTargetResults.get(label).iterator().next();
+    assertThat(result.getTargetKind()).isEqualTo(Kind.JAVA_TEST);
+    assertThat(result.getTestStatus()).isEqualTo(TestStatus.FAILED);
+    assertThat(result.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+  }
+
+  @Test
+  public void parseTestResults_singleEvent_ignoresNonXmlOutputFiles() throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.FAILED;
+    ImmutableList<String> filePaths =
+        ImmutableList.of(
+            "/usr/local/tmp/_cache/test_result.xml",
+            "/usr/local/tmp/_cache/test_result.log",
+            "/usr/local/tmp/other_output_file");
+    BuildEvent.Builder event = testResultEvent(label.toString(), status, filePaths);
+
+    BlazeTestResults results =
+        BuildEventProtocolOutputReader.parseTestResults(asInputStream(event));
+
+    BlazeTestResult result = results.perTargetResults.get(label).iterator().next();
+    assertThat(result.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+  }
+
+  @Test
+  public void parseTestResults_singleTargetWithMultipleEvents_returnsTestResults()
+      throws IOException {
+    Label label = Label.create("//java/com/google:unit_tests");
+    BuildEventStreamProtos.TestStatus status = BuildEventStreamProtos.TestStatus.PASSED;
+    BuildEvent.Builder shard1 =
+        testResultEvent(
+            label.toString(), status, ImmutableList.of("/usr/local/tmp/_cache/shard1_of_2.xml"));
+    BuildEvent.Builder shard2 =
+        testResultEvent(
+            label.toString(), status, ImmutableList.of("/usr/local/tmp/_cache/shard2_of_2.xml"));
+
+    BlazeTestResults results =
+        BuildEventProtocolOutputReader.parseTestResults(asInputStream(shard1, shard2));
+
+    assertThat(results.perTargetResults).hasSize(2);
+    Collection<BlazeTestResult> targetResults = results.perTargetResults.get(label);
+    assertThat(targetResults)
+        .containsExactly(
+            BlazeTestResult.create(
+                label,
+                null,
+                TestStatus.PASSED,
+                ImmutableSet.of(new File("/usr/local/tmp/_cache/shard1_of_2.xml"))),
+            BlazeTestResult.create(
+                label,
+                null,
+                TestStatus.PASSED,
+                ImmutableSet.of(new File("/usr/local/tmp/_cache/shard2_of_2.xml"))));
+  }
+
+  @Test
+  public void parseTestResults_multipleEvents_returnsAllResults() throws IOException {
+    BuildEvent.Builder test1 =
+        testResultEvent(
+            "//java/com/google:Test1",
+            BuildEventStreamProtos.TestStatus.PASSED,
+            ImmutableList.of("/usr/local/tmp/_cache/test_result.xml"));
+    BuildEvent.Builder test2 =
+        testResultEvent(
+            "//java/com/google:Test2",
+            BuildEventStreamProtos.TestStatus.INCOMPLETE,
+            ImmutableList.of("/usr/local/tmp/_cache/second_result.xml"));
+
+    BlazeTestResults results =
+        BuildEventProtocolOutputReader.parseTestResults(asInputStream(test1, test2));
+
+    assertThat(results.perTargetResults).hasSize(2);
+    assertThat(results.perTargetResults.get(Label.create("//java/com/google:Test1"))).hasSize(1);
+    assertThat(results.perTargetResults.get(Label.create("//java/com/google:Test2"))).hasSize(1);
+    BlazeTestResult result1 =
+        results.perTargetResults.get(Label.create("//java/com/google:Test1")).iterator().next();
+    assertThat(result1.getTestStatus()).isEqualTo(TestStatus.PASSED);
+    assertThat(result1.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/test_result.xml"));
+    BlazeTestResult result2 =
+        results.perTargetResults.get(Label.create("//java/com/google:Test2")).iterator().next();
+    assertThat(result2.getTestStatus()).isEqualTo(TestStatus.INCOMPLETE);
+    assertThat(result2.getOutputXmlFiles())
+        .containsExactly(new File("/usr/local/tmp/_cache/second_result.xml"));
+  }
+
+  private static InputStream asInputStream(BuildEvent.Builder... events) throws IOException {
+    return asInputStream(Arrays.asList(events));
+  }
+
+  private static InputStream asInputStream(Iterable<BuildEvent.Builder> events) throws IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    for (BuildEvent.Builder event : events) {
+      event.build().writeDelimitedTo(output);
+    }
+    return new ByteArrayInputStream(output.toByteArray());
+  }
+
+  private static BuildEvent.Builder targetConfiguredEvent(String label, String targetKind) {
+    return BuildEvent.newBuilder()
+        .setId(
+            BuildEventId.newBuilder()
+                .setTargetConfigured(TargetConfiguredId.newBuilder().setLabel(label)))
+        .setConfigured(TargetConfigured.newBuilder().setTargetKind(targetKind));
+  }
+
+  private static BuildEvent.Builder targetCompletedEvent(String label, String targetKind) {
+    return BuildEvent.newBuilder()
+        .setId(
+            BuildEventId.newBuilder()
+                .setTargetCompleted(TargetCompletedId.newBuilder().setLabel(label)))
+        .setCompleted(TargetComplete.newBuilder().setTargetKind(targetKind));
+  }
+
+  private static BuildEvent.Builder testResultEvent(
+      String label, BuildEventStreamProtos.TestStatus status, List<String> filePaths) {
+    return BuildEvent.newBuilder()
+        .setId(BuildEventId.newBuilder().setTestResult(TestResultId.newBuilder().setLabel(label)))
+        .setTestResult(
+            TestResult.newBuilder()
+                .setStatus(status)
+                .addAllTestActionOutput(
+                    filePaths
+                        .stream()
+                        .map(BuildEventProtocolOutputReaderTest::toEventFile)
+                        .collect(toImmutableList())));
+  }
+
+  private static NamedSetOfFiles setOfFiles(List<String> filePaths) {
+    return NamedSetOfFiles.newBuilder()
+        .addAllFiles(
+            filePaths
+                .stream()
+                .map(BuildEventProtocolOutputReaderTest::toEventFile)
+                .collect(toImmutableList()))
+        .build();
+  }
+
+  private static BuildEventStreamProtos.File toEventFile(String filePath) {
+    return BuildEventStreamProtos.File.newBuilder().setUri(fileUrl(filePath)).build();
+  }
+
+  private static String fileUrl(String filePath) {
+    return LocalFileSystem.PROTOCOL_PREFIX + filePath;
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
index 6ed7193..f3a7dae 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
@@ -135,6 +135,20 @@
   }
 
   @Test
+  public void testParseCompileFatalErrorWithColumn() {
+    // Clang also has a 'fatal error' category.
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
+    IssueOutput issue =
+        blazeIssueParser.parseIssue(
+            "net/something/foo_bar.cc:29:10: fatal error: 'util/ptr_util2.h' file not found");
+    assertNotNull(issue);
+    assertThat(issue.getLine()).isEqualTo(29);
+    assertThat(issue.getColumn()).isEqualTo(10);
+    assertThat(issue.getMessage()).isEqualTo("'util/ptr_util2.h' file not found");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
   public void testParseBuildError() {
     BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
diff --git a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
index 9f1ad20..bb3063d 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
@@ -41,7 +41,9 @@
 import com.google.idea.blaze.base.projectview.section.sections.RunConfigurationsSection;
 import com.google.idea.blaze.base.projectview.section.sections.Sections;
 import com.google.idea.blaze.base.projectview.section.sections.ShardBlazeBuildsSection;
+import com.google.idea.blaze.base.projectview.section.sections.SyncFlagsSection;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetShardSizeSection;
 import com.google.idea.blaze.base.projectview.section.sections.TestSourceSection;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlockSection;
@@ -82,6 +84,7 @@
                     .add(ListSection.builder(TestSourceSection.KEY).add(new Glob("javatests/*")))
                     .add(ListSection.builder(ExcludedSourceSection.KEY).add(new Glob("*.java")))
                     .add(ListSection.builder(BuildFlagsSection.KEY).add("--android_sdk=abcd"))
+                    .add(ListSection.builder(SyncFlagsSection.KEY).add("--config=arm"))
                     .add(
                         ListSection.builder(ImportTargetOutputSection.KEY)
                             .add(Label.create("//test:test")))
@@ -96,6 +99,7 @@
                         ListSection.builder(RunConfigurationsSection.KEY)
                             .add(new WorkspacePath("test")))
                     .add(ScalarSection.builder(ShardBlazeBuildsSection.KEY).set(false))
+                    .add(ScalarSection.builder(TargetShardSizeSection.KEY).set(500))
                     .build())
             .build();
 
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
index f0047bd..3ed4fd1 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
@@ -17,7 +17,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
@@ -33,6 +32,7 @@
 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
 import com.intellij.openapi.project.Project;
 import java.util.List;
+import java.util.function.Predicate;
 import org.jdom.Element;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategyTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategyTest.java
new file mode 100644
index 0000000..a90660c
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BuildEventProtocolTestFinderStrategyTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.testlogs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.idea.common.guava.GuavaHelper.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.buildresult.BuildEventProtocolOutputReader;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.io.MockInputStreamProvider;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.TestResultId;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link BuildEventProtocolTestFinderStrategy}. */
+@RunWith(JUnit4.class)
+public class BuildEventProtocolTestFinderStrategyTest extends BlazeTestCase {
+
+  private MockInputStreamProvider inputStreamProvider;
+  private final Set<File> deletedFiles = new HashSet<>();
+
+  @After
+  public void clearState() {
+    deletedFiles.clear();
+  }
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    inputStreamProvider = new MockInputStreamProvider();
+    applicationServices.register(InputStreamProvider.class, inputStreamProvider);
+  }
+
+  @Test
+  public void findTestResults_fileDeletedAfterReading() throws IOException {
+    File file = createMockFile("/tmp/bep_output.txt", new byte[0]);
+
+    new BuildEventProtocolTestFinderStrategy(file).findTestResults();
+
+    assertThat(deletedFiles).contains(file);
+  }
+
+  @Test
+  public void findTestResults_shouldMatchBuildEventProtocolOutputReader() throws IOException {
+    BuildEventStreamProtos.BuildEvent.Builder test1 =
+        testResultEvent(
+            "//java/com/google:Test1",
+            BuildEventStreamProtos.TestStatus.PASSED,
+            ImmutableList.of("/usr/local/tmp/_cache/test_result.xml"));
+    BuildEventStreamProtos.BuildEvent.Builder test2 =
+        testResultEvent(
+            "//java/com/google:Test2",
+            BuildEventStreamProtos.TestStatus.INCOMPLETE,
+            ImmutableList.of("/usr/local/tmp/_cache/second_result.xml"));
+    File bepOutputFile =
+        createMockFile("/tmp/bep_output.txt", asByteArray(ImmutableList.of(test1, test2)));
+    BuildEventProtocolTestFinderStrategy strategy =
+        new BuildEventProtocolTestFinderStrategy(bepOutputFile);
+
+    BlazeTestResults results =
+        BuildEventProtocolOutputReader.parseTestResults(inputStreamProvider.getFile(bepOutputFile));
+    BlazeTestResults finderStrategyResults = strategy.findTestResults();
+
+    assertThat(finderStrategyResults.perTargetResults).isEqualTo(results.perTargetResults);
+  }
+
+  private File createMockFile(String path, byte[] contents) {
+    File org = new File(path);
+    File spy = Mockito.spy(org);
+    inputStreamProvider.addFile(path, contents);
+    Mockito.when(spy.delete())
+        .then(
+            invocationOnMock -> {
+              deletedFiles.add(spy);
+              return true;
+            });
+    return spy;
+  }
+
+  private static byte[] asByteArray(Iterable<BuildEventStreamProtos.BuildEvent.Builder> events)
+      throws IOException {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    for (BuildEventStreamProtos.BuildEvent.Builder event : events) {
+      event.build().writeDelimitedTo(output);
+    }
+    return output.toByteArray();
+  }
+
+  private static BuildEventStreamProtos.BuildEvent.Builder testResultEvent(
+      String label, BuildEventStreamProtos.TestStatus status, List<String> filePaths) {
+    return BuildEventStreamProtos.BuildEvent.newBuilder()
+        .setId(
+            BuildEventStreamProtos.BuildEventId.newBuilder()
+                .setTestResult(TestResultId.newBuilder().setLabel(label)))
+        .setTestResult(
+            BuildEventStreamProtos.TestResult.newBuilder()
+                .setStatus(status)
+                .addAllTestActionOutput(
+                    filePaths
+                        .stream()
+                        .map(BuildEventProtocolTestFinderStrategyTest::toEventFile)
+                        .collect(toImmutableList())));
+  }
+
+  private static BuildEventStreamProtos.File toEventFile(String filePath) {
+    return BuildEventStreamProtos.File.newBuilder().setUri(fileUrl(filePath)).build();
+  }
+
+  private static String fileUrl(String filePath) {
+    return LocalFileSystem.PROTOCOL_PREFIX + filePath;
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
index fdc880e..5adae26 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
@@ -71,6 +71,11 @@
           public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
             return ImmutableList.of(WorkspaceType.C);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.C;
+          }
         });
 
     ProjectViewSet projectViewSet =
@@ -91,6 +96,13 @@
 
   @Test
   public void testFailWithUnsupportedWorkspaceType() {
+    syncPlugins.registerExtension(
+        new BlazeSyncPlugin.Adapter() {
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
+        });
     ProjectViewSet projectViewSet =
         ProjectViewSet.builder()
             .add(
@@ -117,6 +129,11 @@
           public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
             return ImmutableList.of(WorkspaceType.C);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.C;
+          }
         });
 
     ProjectViewSet projectViewSet =
@@ -149,6 +166,11 @@
           public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
             return ImmutableList.of(WorkspaceType.ANDROID);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.ANDROID;
+          }
         });
 
     ProjectViewSet projectViewSet =
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyTest.java
new file mode 100644
index 0000000..147a547
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.sync.aspects.strategy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.common.experiments.ExperimentService;
+import com.google.idea.common.experiments.MockExperimentService;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AspectStrategy}. */
+@RunWith(JUnit4.class)
+public class AspectStrategyTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+  }
+
+  @Test
+  public void testLegacyOutputGroupsUnchanged() {
+    AspectStrategy strategy = MockAspectStrategy.noPerLanguageOutputGroups();
+    Set<LanguageClass> activeLanguages = ImmutableSet.of(LanguageClass.JAVA, LanguageClass.ANDROID);
+
+    BlazeCommand.Builder builder = emptyBuilder();
+    strategy.modifyIdeInfoCommand(builder, activeLanguages);
+    assertThat(getBlazeFlags(builder)).containsExactly("--output_groups=intellij-info-text");
+
+    builder = emptyBuilder();
+    strategy.modifyIdeResolveCommand(builder, activeLanguages);
+    assertThat(getBlazeFlags(builder)).containsExactly("--output_groups=intellij-resolve");
+
+    builder = emptyBuilder();
+    strategy.modifyIdeCompileCommand(builder, activeLanguages);
+    assertThat(getBlazeFlags(builder)).containsExactly("--output_groups=intellij-compile");
+  }
+
+  @Test
+  public void testGenericOutputGroupAlwaysPresent() {
+    AspectStrategy strategy = MockAspectStrategy.withPerLanguageOutputGroups();
+    Set<LanguageClass> activeLanguages = ImmutableSet.of();
+
+    BlazeCommand.Builder builder = emptyBuilder();
+    strategy.modifyIdeInfoCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder)).containsExactly("intellij-info-generic");
+  }
+
+  @Test
+  public void testNoGenericOutputGroupInResolveOrCompile() {
+    AspectStrategy strategy = MockAspectStrategy.withPerLanguageOutputGroups();
+    Set<LanguageClass> activeLanguages = ImmutableSet.of(LanguageClass.JAVA);
+
+    BlazeCommand.Builder builder = emptyBuilder();
+    strategy.modifyIdeResolveCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder)).containsExactly("intellij-resolve-java");
+
+    builder = emptyBuilder();
+    strategy.modifyIdeCompileCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder)).containsExactly("intellij-compile-java");
+  }
+
+  @Test
+  public void testAllPerLanguageOutputGroupsRecognized() {
+    AspectStrategy strategy = MockAspectStrategy.withPerLanguageOutputGroups();
+    Set<LanguageClass> activeLanguages =
+        Arrays.stream(LanguageOutputGroup.values())
+            .map(lang -> lang.languageClass)
+            .collect(Collectors.toSet());
+
+    BlazeCommand.Builder builder = emptyBuilder();
+    strategy.modifyIdeInfoCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder))
+        .containsExactly(
+            "intellij-info-generic",
+            "intellij-info-java",
+            "intellij-info-cpp",
+            "intellij-info-android",
+            "intellij-info-py",
+            "intellij-info-go",
+            "intellij-info-js",
+            "intellij-info-ts");
+
+    builder = emptyBuilder();
+    strategy.modifyIdeResolveCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder))
+        .containsExactly(
+            "intellij-resolve-java",
+            "intellij-resolve-cpp",
+            "intellij-resolve-android",
+            "intellij-resolve-py",
+            "intellij-resolve-go",
+            "intellij-resolve-js",
+            "intellij-resolve-ts");
+
+    builder = emptyBuilder();
+    strategy.modifyIdeCompileCommand(builder, activeLanguages);
+    assertThat(getOutputGroups(builder))
+        .containsExactly(
+            "intellij-compile-java",
+            "intellij-compile-cpp",
+            "intellij-compile-android",
+            "intellij-compile-py",
+            "intellij-compile-go",
+            "intellij-compile-js",
+            "intellij-compile-ts");
+  }
+
+  private static BlazeCommand.Builder emptyBuilder() {
+    return BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.BUILD);
+  }
+
+  private static ImmutableList<String> getBlazeFlags(BlazeCommand.Builder builder) {
+    ImmutableList<String> args = builder.build().toList();
+    return args.subList(3, args.indexOf("--"));
+  }
+
+  private static ImmutableList<String> getOutputGroups(BlazeCommand.Builder builder) {
+    List<String> blazeFlags = getBlazeFlags(builder);
+    assertThat(blazeFlags).hasSize(1);
+    String groups = blazeFlags.get(0).substring("--output_groups=".length());
+    return ImmutableList.copyOf(groups.split(","));
+  }
+
+  private static class MockAspectStrategy extends AspectStrategy {
+
+    static MockAspectStrategy withPerLanguageOutputGroups() {
+      return new MockAspectStrategy(true);
+    }
+
+    static MockAspectStrategy noPerLanguageOutputGroups() {
+      return new MockAspectStrategy(false);
+    }
+
+    final boolean hasPerLanguageOutputGroups;
+
+    private MockAspectStrategy(boolean hasPerLanguageOutputGroups) {
+      this.hasPerLanguageOutputGroups = hasPerLanguageOutputGroups;
+    }
+
+    @Override
+    public String getName() {
+      return "MockAspectStrategy";
+    }
+
+    @Override
+    protected List<String> getAspectFlags() {
+      return ImmutableList.of();
+    }
+
+    @Override
+    protected boolean hasPerLanguageOutputGroups() {
+      return hasPerLanguageOutputGroups;
+    }
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java
index 5f2051f..35c24f6 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java
@@ -17,6 +17,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -24,6 +26,8 @@
 /** Unit tests for {@link SourceTestConfig} */
 @RunWith(JUnit4.class)
 public class SourceTestConfigTest {
+  @Rule
+  public final BlazeTestCase.IgnoreOnWindowsRule rule = new BlazeTestCase.IgnoreOnWindowsRule();
 
   @Test
   public void testGlobModification() {
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java
index 7da1a7a..dfa2957 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java
@@ -83,7 +83,7 @@
         .inOrder();
 
     shards = BlazeBuildTargetSharder.shardTargets(targets, 1);
-    assertThat(shards.shardedTargets).hasSize(6);
+    assertThat(shards.shardedTargets).hasSize(3);
     assertThat(shards.shardedTargets.get(0))
         .containsExactly(
             TargetExpression.fromString("//java/com/google:one"),
@@ -92,4 +92,21 @@
             TargetExpression.fromString("-//java/com/google:six"))
         .inOrder();
   }
+
+  @Test
+  public void testShardWithOnlyExcludedTargetsIsDropped() {
+    List<TargetExpression> targets =
+        ImmutableList.of(
+            TargetExpression.fromString("//java/com/google:one"),
+            TargetExpression.fromString("//java/com/google:two"),
+            TargetExpression.fromString("//java/com/google:three"),
+            TargetExpression.fromString("-//java/com/google:four"),
+            TargetExpression.fromString("-//java/com/google:five"),
+            TargetExpression.fromString("-//java/com/google:six"));
+
+    ShardedTargetList shards = BlazeBuildTargetSharder.shardTargets(targets, 3);
+
+    assertThat(shards.shardedTargets).hasSize(1);
+    assertThat(shards.shardedTargets.get(0)).hasSize(6);
+  }
 }
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
index 9080df7..af3969a 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
@@ -27,6 +27,7 @@
 import com.google.idea.testing.EdtRule;
 import com.google.idea.testing.IntellijTestSetupRule;
 import com.google.idea.testing.ServiceHelper;
+import com.google.idea.testing.VerifyRequiredPluginsEnabled;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.extensions.ExtensionPointName;
@@ -132,6 +133,11 @@
         });
     registerApplicationService(
         VirtualFileSystemProvider.class, new TestFileSystem.TempVirtualFileSystemProvider());
+
+    String requiredPlugins = System.getProperty("idea.required.plugins.id");
+    if (requiredPlugins != null) {
+      VerifyRequiredPluginsEnabled.runCheck(requiredPlugins.split(","));
+    }
   }
 
   @After
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
index a227d50..9a435b4 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
@@ -26,6 +26,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.command.info.BlazeConfigurationHandler;
 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.ArtifactLocation;
@@ -77,10 +78,11 @@
   // blaze-info data
   private static final String OUTPUT_BASE = "/output_base";
   private static final String EXECUTION_ROOT = "/execroot/root";
+  private static final String OUTPUT_PATH = EXECUTION_ROOT + "/blaze-out";
   private static final String BLAZE_BIN =
-      EXECUTION_ROOT + "/blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
+      OUTPUT_PATH + "/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
   private static final String BLAZE_GENFILES =
-      EXECUTION_ROOT + "/blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/genfiles";
+      OUTPUT_PATH + "/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/genfiles";
 
   private MockProjectViewManager projectViewManager;
   private MockBlazeVcsHandler vcsHandler;
@@ -147,17 +149,14 @@
     fileSystem.createDirectory(projectDataDirectory.getPath() + "/.blaze/modules");
 
     blazeInfoData.setResults(
-        ImmutableMap.of(
-            BlazeInfo.blazeBinKey(Blaze.getBuildSystem(getProject())),
-            BLAZE_BIN,
-            BlazeInfo.blazeGenfilesKey(Blaze.getBuildSystem(getProject())),
-            BLAZE_GENFILES,
-            BlazeInfo.EXECUTION_ROOT_KEY,
-            EXECUTION_ROOT,
-            BlazeInfo.OUTPUT_BASE_KEY,
-            OUTPUT_BASE,
-            BlazeInfo.PACKAGE_PATH_KEY,
-            workspaceRoot.toString()));
+        ImmutableMap.<String, String>builder()
+            .put(BlazeInfo.blazeBinKey(Blaze.getBuildSystem(getProject())), BLAZE_BIN)
+            .put(BlazeInfo.blazeGenfilesKey(Blaze.getBuildSystem(getProject())), BLAZE_GENFILES)
+            .put(BlazeInfo.EXECUTION_ROOT_KEY, EXECUTION_ROOT)
+            .put(BlazeInfo.OUTPUT_BASE_KEY, OUTPUT_BASE)
+            .put(BlazeInfo.OUTPUT_PATH_KEY, OUTPUT_PATH)
+            .put(BlazeInfo.PACKAGE_PATH_KEY, workspaceRoot.toString())
+            .build());
   }
 
   /** The workspace content entries created during sync */
@@ -337,6 +336,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
+        BlazeConfigurationHandler configHandler,
         ShardedTargetList shardedTargets,
         WorkspaceLanguageSettings workspaceLanguageSettings,
         ArtifactLocationDecoder artifactLocationDecoder,
@@ -353,6 +353,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
+        WorkspaceLanguageSettings workspaceLanguageSettings,
         ShardedTargetList shardedTargets) {
       return BuildResult.SUCCESS;
     }
@@ -364,6 +365,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
+        WorkspaceLanguageSettings workspaceLanguageSettings,
         ShardedTargetList shardedTargets) {
       return BuildResult.SUCCESS;
     }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java b/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
index 020ee9f..52246ab 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
@@ -28,7 +28,6 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Disposer;
 import com.intellij.openapi.util.SystemInfo;
-import org.jetbrains.annotations.NotNull;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -40,14 +39,11 @@
 /**
  * Test base class.
  *
- * <p>
- *
  * <p>Provides a mock application and a mock project.
  */
 public class BlazeTestCase {
   /** Test rule that ensures tests do not run on Windows (see http://b.android.com/222904) */
   public static class IgnoreOnWindowsRule implements TestRule {
-    @NotNull
     @Override
     public Statement apply(Statement base, Description description) {
       if (SystemInfo.isWindows) {
@@ -80,7 +76,7 @@
   public static class Container {
     private final MutablePicoContainer container;
 
-    Container(@NotNull MutablePicoContainer container) {
+    Container(MutablePicoContainer container) {
       this.container = container;
     }
 
@@ -115,11 +111,10 @@
     return project;
   }
 
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {}
+  protected void initTest(Container applicationServices, Container projectServices) {}
 
   protected <T> ExtensionPointImpl<T> registerExtensionPoint(
-      @NotNull ExtensionPointName<T> name, @NotNull Class<T> type) {
+      ExtensionPointName<T> name, Class<T> type) {
     PluginDescriptor pluginDescriptor =
         new DefaultPluginDescriptor(PluginId.getId(type.getName()), type.getClassLoader());
     extensionsArea.registerExtensionPoint(name.getName(), type.getName(), pluginDescriptor);
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/io/MockInputStreamProvider.java b/base/tests/utils/unit/com/google/idea/blaze/base/io/MockInputStreamProvider.java
new file mode 100644
index 0000000..8f6a723
--- /dev/null
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/io/MockInputStreamProvider.java
@@ -0,0 +1,60 @@
+/*
+ * 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.io;
+
+import static org.junit.Assert.fail;
+
+import com.intellij.util.containers.HashMap;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+/**
+ * Mocks {@link InputStreamProvider} for tests, providing an input stream corresponding to given
+ * file contents without IO operations.
+ */
+public class MockInputStreamProvider implements InputStreamProvider {
+
+  private final Map<String, byte[]> files = new HashMap<>();
+
+  /** Add a file to provide an {@link InputStream} for, with specified contents. */
+  public MockInputStreamProvider addFile(String filePath, String src) {
+    try {
+      addFile(filePath, src.getBytes("UTF-8"));
+    } catch (UnsupportedEncodingException e) {
+      fail(e.getMessage());
+    }
+    return this;
+  }
+
+  /** Add a file to provide an {@link InputStream} for, with specified contents. */
+  public MockInputStreamProvider addFile(String filePath, byte[] contents) {
+    files.put(filePath, contents);
+    return this;
+  }
+
+  @Override
+  public InputStream getFile(File path) throws FileNotFoundException {
+    final byte[] contents = files.get(path.getPath());
+    if (contents == null) {
+      throw new FileNotFoundException(path + " has not been mapped into MockInputStreamProvider.");
+    }
+    return new ByteArrayInputStream(contents);
+  }
+}
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
index 8e104a7..9f9dedc 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
@@ -125,7 +125,7 @@
               outputBase + "/execroot/gen");
     }
     BlazeVersionData blazeVersionData =
-        this.blazeVersionData != null ? this.blazeVersionData : new BlazeVersionData();
+        this.blazeVersionData != null ? this.blazeVersionData : BlazeVersionData.builder().build();
     WorkspacePathResolver workspacePathResolver =
         this.workspacePathResolver != null
             ? this.workspacePathResolver
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java b/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
index bedd5d9..309ba63 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
@@ -27,7 +27,8 @@
 public class MockPrefetchService implements PrefetchService {
 
   @Override
-  public ListenableFuture<?> prefetchFiles(Project project, Collection<File> files) {
+  public ListenableFuture<?> prefetchFiles(
+      Project project, Collection<File> files, boolean refetchCachedFiles) {
     return Futures.immediateFuture(null);
   }
 
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java b/base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java
index 3362bae..eab80bb 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java
@@ -20,14 +20,13 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
 /** Test class that collects issues. */
 public class ErrorCollector implements OutputSink<IssueOutput> {
   List<IssueOutput> issues = Lists.newArrayList();
 
   @Override
-  public Propagation onOutput(@NotNull IssueOutput output) {
+  public Propagation onOutput(IssueOutput output) {
     issues.add(output);
     return Propagation.Continue;
   }
@@ -36,7 +35,7 @@
     assertThat(issues).isEmpty();
   }
 
-  public void assertIssues(@NotNull String... requiredMessages) {
+  public void assertIssues(String... requiredMessages) {
     List<String> messages = Lists.newArrayList();
     for (IssueOutput issue : issues) {
       messages.add(issue.getMessage());
@@ -44,7 +43,7 @@
     assertThat(messages).containsExactly((Object[]) requiredMessages);
   }
 
-  public void assertIssueContaining(@NotNull String s) {
+  public void assertIssueContaining(String s) {
     assertThat(issues.stream().anyMatch((issue) -> issue.getMessage().contains(s)))
         .named("Issues must contain: " + s)
         .isTrue();
diff --git a/build_defs/BUILD b/build_defs/BUILD
index 19ad33f..fd3fe33 100644
--- a/build_defs/BUILD
+++ b/build_defs/BUILD
@@ -35,3 +35,8 @@
     name = "package_meta_inf_files",
     srcs = ["package_meta_inf_files.py"],
 )
+
+py_binary(
+    name = "zip_plugin_files",
+    srcs = ["zip_plugin_files.py"],
+)
diff --git a/build_defs/append_optional_xml_elements.py b/build_defs/append_optional_xml_elements.py
index c987fe1..c6761af 100755
--- a/build_defs/append_optional_xml_elements.py
+++ b/build_defs/append_optional_xml_elements.py
@@ -37,9 +37,9 @@
 
   if args.output:
     with file(args.output, "w") as f:
-      f.write(dom.toxml())
+      f.write(dom.toxml(encoding="utf-8"))
   else:
-    print dom.toxml()
+    print dom.toxml(encoding="utf-8")
 
 
 if __name__ == "__main__":
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
index 5325028..a4e2c6d 100644
--- a/build_defs/build_defs.bzl
+++ b/build_defs/build_defs.bzl
@@ -1,7 +1,6 @@
 """Custom build macros for IntelliJ plugin handling.
 """
 
-load(":intellij_plugin_debug_target.bzl", "intellij_plugin_debug_target")
 load(":intellij_plugin.bzl", "intellij_plugin", "optional_plugin_xml")
 
 def merged_plugin_xml(name, srcs, **kwargs):
@@ -179,61 +178,123 @@
       tools = [api_version_txt_tool],
       **kwargs)
 
-def repackaged_jar(name, deps, rules, **kwargs):
-  """Repackages classes in a jar, to avoid collisions in the classpath.
-
-  Args:
-    name: the name of this target
-    deps: The dependencies repackage
-    rules: the rules to apply in the repackaging
-        Do not repackage:
-        - com.google.net.** because that has JNI files which use
-          FindClass(JNIEnv *, const char *) with hard-coded native string
-          literals that jarjar doesn't rewrite.
-        - com.google.errorprone packages (rewriting will throw off blaze build).
-    **kwargs: Any additional arguments to pass to the final target.
-  """
-  java_binary_name = name + "_orig"
-  out = name + ".jar"
-  native.java_binary(
-      name = java_binary_name,
-      create_executable = 0,
-      stamp = 0,
-      runtime_deps = deps)
-  _repackaged_jar(name, java_binary_name, out, rules, **kwargs)
-
-def _repackaged_jar(name, src_rule, out, rules, **kwargs):
-  """Repackages classes in a jar, to avoid collisions in the classpath."""
-  repackage_tool = "@jarjar//jar"
-  deploy_jar = "{src_rule}_deploy.jar".format(src_rule=src_rule)
-  script_lines = []
-  script_lines.append("echo >> /tmp/repackaged_rule.txt")
-  for rule in rules:
-    script_lines.append("echo 'rule {rule}' >> /tmp/repackaged_rule.txt;".format(rule=rule))
-  script_lines.append(" ".join([
-      "$(location {repackage_tool})",
-      "process /tmp/repackaged_rule.txt",
-      "$(location {deploy_jar})",
-      "$@",
-  ]).format(
-      repackage_tool = repackage_tool,
-      deploy_jar = deploy_jar,
-  ))
-  genrule_name = name + "_repackaged"
-  native.genrule(
-      name = genrule_name,
-      srcs = [deploy_jar],
-      outs = [out],
-      tools = [repackage_tool],
-      cmd = "\n".join(script_lines),
-  )
-  native.java_import(
-      name = name,
-      jars = [out],
-      **kwargs)
-
 def beta_gensignature(name, srcs, stable, stable_version, beta_version):
   if stable_version == beta_version:
     native.alias(name = name, actual = stable)
   else:
     native.gensignature(name = name, srcs = srcs)
+
+repackaged_files_data = provider()
+
+def _repackaged_files_impl(ctx):
+  prefix = ctx.attr.prefix
+  if prefix.startswith("/"):
+    fail("'prefix' must be a relative path")
+  input_files = depset()
+  for target in ctx.attr.srcs:
+    input_files = input_files | target.files
+
+  return [
+      # TODO(brendandouglas): Only valid for Bazel 0.5 onwards. Uncomment when
+      # 0.5 used more widely.
+      # DefaultInfo(files = input_files),
+      repackaged_files_data(
+          files = input_files,
+          prefix = prefix,
+      )
+  ]
+
+_repackaged_files = rule(
+    implementation = _repackaged_files_impl,
+    attrs = {
+        "srcs": attr.label_list(mandatory = True, allow_files = True),
+        "prefix": attr.string(mandatory = True),
+    },
+)
+
+def repackaged_files(name, srcs, prefix, **kwargs):
+  """Assembles files together so that they can be packaged as an IntelliJ plugin.
+
+  A cut-down version of the internal 'pkgfilegroup' rule.
+
+  Args:
+    name: The name of this target
+    srcs: A list of targets which are dependencies of this rule. All output files of each of these
+        targets will be repackaged.
+    prefix: Where the package should install these files, relative to the 'plugins' directory. The
+        input file path is stripped prior to applying this prefix.
+    **kwargs: Any further arguments to be passed to the target
+  """
+  _repackaged_files(name = name, srcs = srcs, prefix = prefix, **kwargs)
+
+def _plugin_deploy_zip_impl(ctx):
+  zip_name = ctx.attr.zip_filename
+  zip_file = ctx.new_file(zip_name)
+
+  input_files = depset()
+  exec_path_to_zip_path = {}
+  for target in ctx.attr.srcs:
+    data = target[repackaged_files_data]
+    input_files = input_files | data.files
+    for f in data.files:
+      exec_path_to_zip_path[f.path] = data.prefix + "/" + f.basename
+
+  args = []
+  args.extend(["--output", zip_file.path])
+  for exec_path, zip_path in exec_path_to_zip_path.items():
+    args.extend([exec_path, zip_path])
+  ctx.action(
+      executable = ctx.executable._zip_plugin_files,
+      arguments = args,
+      inputs = list(input_files),
+      outputs = [zip_file],
+      mnemonic = "ZipPluginFiles",
+      progress_message = "Creating final plugin zip archive",
+  )
+  files = depset([zip_file])
+  return struct(
+      files = files,
+  )
+
+_plugin_deploy_zip = rule(
+    implementation = _plugin_deploy_zip_impl,
+    attrs = {
+        "srcs": attr.label_list(mandatory = True, providers = []),
+        "zip_filename": attr.string(mandatory = True),
+        "_zip_plugin_files": attr.label(
+            default = Label("//build_defs:zip_plugin_files"),
+            executable = True,
+            cfg = "host",
+        ),
+    }
+)
+
+def plugin_deploy_zip(name, srcs, zip_filename):
+  """Packages up plugin files into a zip archive.
+
+  Args:
+    name: The name of this target
+    srcs: A list of targets of type 'repackaged_files', specifying the input files and relative
+        paths to include in the output zip archive.
+    zip_filename: The output zip filename.
+  """
+  _plugin_deploy_zip(name = name, zip_filename = zip_filename, srcs = srcs)
+
+def unescape_filenames(name, srcs):
+  """Macro to generate files with spaces in their names instead of underscores.
+
+  For each file in the srcs, a file will be generated with the same name but with all underscores
+  replaced with spaces.
+
+  Args:
+    name: The name of the generator rule
+    srcs: A list of source files to process
+  """
+  outs = [s.replace("_", " ") for s in srcs]
+  cmd = "&&".join(["cp \"{}\" $(@D)/\"{}\"".format(s, d) for (s,d) in zip(srcs, outs)])
+  native.genrule(
+      name = name,
+      srcs = srcs,
+      outs = outs,
+      cmd = cmd,
+  )
diff --git a/build_defs/intellij_plugin.bzl b/build_defs/intellij_plugin.bzl
index 207435d..3a827e4 100644
--- a/build_defs/intellij_plugin.bzl
+++ b/build_defs/intellij_plugin.bzl
@@ -131,7 +131,7 @@
   module_to_merged_xmls = _merge_optional_plugin_xmls(ctx)
   final_plugin_xml_file = _add_optional_dependencies_to_plugin_xml(ctx, module_to_merged_xmls.keys())
   jar_file = _package_meta_inf_files(ctx, final_plugin_xml_file, module_to_merged_xmls)
-  files = set([jar_file])
+  files = depset([jar_file])
   return struct(
       files = files,
   )
diff --git a/build_defs/intellij_plugin_debug_target.bzl b/build_defs/intellij_plugin_debug_target.bzl
index 37ec6f6..bbf21f4 100644
--- a/build_defs/intellij_plugin_debug_target.bzl
+++ b/build_defs/intellij_plugin_debug_target.bzl
@@ -5,12 +5,11 @@
 
 Any files are stripped of their prefix and installed into
 <sandbox>/plugins. If you need structure, first put the files
-into a pkgfilegroup. The files will be installed relative to the
-'plugins' directory if present in the pkgfilegroup prefix.
+into //build_defs:build_defs%repackage_files.
 
 intellij_plugin_debug_targets can be nested.
 
-pkgfilegroup(
+repackaged_files(
   name = "foo_files",
   srcs = [
     ":my_plugin_jar",
@@ -28,35 +27,17 @@
 
 """
 
+load("//build_defs:build_defs.bzl", "repackaged_files_data")
+
 SUFFIX = ".intellij-plugin-debug-target-deploy-info"
 
 def _trim_start(path, prefix):
   return path[len(prefix):] if path.startswith(prefix) else path
 
-def _pkgfilegroup_deploy_file(ctx, f):
-  strip_prefix = ctx.rule.attr.strip_prefix
-  prefix = ctx.rule.attr.prefix
-  if strip_prefix == ".":
-    stripped_relative_path = f.basename
-  elif strip_prefix.startswith("/"):
-    stripped_relative_path = _trim_start(f.short_path, strip_prefix[1:])
-  else:
-    stripped_relative_path = _trim_start(f.short_path, PACKAGE_NAME)
-    stripped_relative_path = _trim_start(stripped_relative_path, strip_prefix)
-  stripped_relative_path = _trim_start(stripped_relative_path, "/")
-
-  # If there's a 'plugins' directory, make destination relative to that
-  plugini = prefix.find("plugins/")
-  plugins_prefix = prefix[plugini + len("plugins/"):] if plugini >= 0 else prefix
-
-  # If the install location is still absolute, fail
-  if plugins_prefix.startswith("/"):
-    fail("Cannot compute plugins-relative install directory for pkgfilegroup")
-
-  dest = plugins_prefix + "/" + stripped_relative_path if plugins_prefix else stripped_relative_path
+def _repackaged_deploy_file(f, prefix):
   return struct(
       src = f,
-      deploy_location = dest,
+      deploy_location = prefix + "/" + f.basename,
   )
 
 def _flat_deploy_file(f):
@@ -68,19 +49,24 @@
 def _intellij_plugin_debug_target_aspect_impl(target, ctx):
   aspect_intellij_plugin_deploy_info = None
 
+  files = target.files
   if ctx.rule.kind == "intellij_plugin_debug_target":
     aspect_intellij_plugin_deploy_info = target.intellij_plugin_deploy_info
-  elif ctx.rule.kind == "pkgfilegroup":
+  elif ctx.rule.kind == "_repackaged_files":
+    data = target[repackaged_files_data]
+    prefix = data.prefix
     aspect_intellij_plugin_deploy_info = struct(
-        deploy_files = [_pkgfilegroup_deploy_file(ctx, f) for f in target.files],
+        deploy_files = [_repackaged_deploy_file(f, prefix) for f in data.files],
     )
+    # TODO(brendandouglas): Remove when migrating to Bazel 0.5, when DefaultInfo
+    # provider can be populated by '_repackaged_files' directly
+    files = files | data.files
   else:
     aspect_intellij_plugin_deploy_info = struct(
         deploy_files = [_flat_deploy_file(f) for f in target.files],
     )
-
   return struct(
-      files = target.files,
+      input_files = files,
       aspect_intellij_plugin_deploy_info = aspect_intellij_plugin_deploy_info,
   )
 
@@ -95,10 +81,10 @@
   )
 
 def _intellij_plugin_debug_target_impl(ctx):
-  files = set()
+  files = depset()
   deploy_files = []
   for target in ctx.attr.deps:
-    files = files | target.files
+    files = files | target.input_files
     deploy_files.extend(target.aspect_intellij_plugin_deploy_info.deploy_files)
   deploy_info = struct(
       deploy_files = [_build_deploy_info_file(f) for f in deploy_files]
@@ -108,8 +94,8 @@
 
   # We've already consumed any dependent intellij_plugin_debug_targets into our own,
   # do not build or report these
-  files = set([f for f in files if not f.path.endswith(SUFFIX)])
-  files = files | set([output])
+  files = depset([f for f in files if not f.path.endswith(SUFFIX)])
+  files = files | depset([output])
 
   return struct(
       files = files,
diff --git a/build_defs/stamp_plugin_xml.py b/build_defs/stamp_plugin_xml.py
index 72baa66..a59c426 100755
--- a/build_defs/stamp_plugin_xml.py
+++ b/build_defs/stamp_plugin_xml.py
@@ -1,6 +1,7 @@
 """Stamps a plugin xml with build information."""
 
 import argparse
+import io
 import re
 from xml.dom.minidom import parse
 
@@ -62,7 +63,7 @@
 
 def _read_changelog(changelog_file):
   """Reads the changelog and transforms it into trivial HTML."""
-  with open(changelog_file) as f:
+  with io.open(changelog_file, encoding="utf-8") as f:
     return "\n".join("<p>" + line + "</p>" for line in f.readlines())
 
 
@@ -189,7 +190,7 @@
   for new_element in new_elements:
     idea_plugin.appendChild(new_element)
 
-  print dom.toxml()
+  print dom.toxml(encoding="utf-8")
 
 
 if __name__ == "__main__":
diff --git a/build_defs/zip_plugin_files.py b/build_defs/zip_plugin_files.py
new file mode 100644
index 0000000..c86dcf3
--- /dev/null
+++ b/build_defs/zip_plugin_files.py
@@ -0,0 +1,35 @@
+"""Packages plugin files into a zip archive."""
+
+import argparse
+from itertools import izip
+import time
+import zipfile
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument("--output", help="The output filename.", required=True)
+parser.add_argument(
+    "files_to_zip", nargs="+", help="Sequence of exec_path, zip_path... pairs")
+
+
+def pairwise(t):
+  it = iter(t)
+  return izip(it, it)
+
+
+def main():
+  args = parser.parse_args()
+
+  # zipfile cannot be coaxed into putting a custom timestamp into the zip files,
+  # and we cannot modify the timestamp of the file itself (because it's a CAS
+  # entry on Forge). Therefore, we replace time.localtime().
+  time.localtime = lambda _: [2000, 1, 1, 0, 0, 0, 0, 0, 0]
+
+  outfile = zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED)
+  for exec_path, zip_path in pairwise(args.files_to_zip):
+    outfile.write(exec_path, zip_path)
+  outfile.close()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/clwb/BUILD b/clwb/BUILD
index e420e93..bfa5d6e 100644
--- a/clwb/BUILD
+++ b/clwb/BUILD
@@ -8,9 +8,14 @@
     "//build_defs:build_defs.bzl",
     "intellij_plugin",
     "merged_plugin_xml",
+    "plugin_deploy_zip",
+    "repackaged_files",
     "stamped_plugin_xml",
 )
-load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+load(
+    "//build_defs:intellij_plugin_debug_target.bzl",
+    "intellij_plugin_debug_target",
+)
 load("//:version.bzl", "VERSION")
 
 merged_plugin_xml(
@@ -36,7 +41,7 @@
     changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.clwb",
-    plugin_name = "CLion with Bazel",
+    plugin_name = "Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
     version = VERSION,
@@ -46,11 +51,10 @@
     name = "clwb_lib",
     srcs = glob(["src/**/*.java"]),
     runtime_deps = [
+        "//golang",
+        "//python",
         "//terminal",
-    ] + select_for_plugin_api({
-        "clion-2016.3.2": [],
-        "default": ["//python"],
-    }),
+    ],
     deps = [
         "//base",
         "//common/experiments",
@@ -62,6 +66,7 @@
 )
 
 OPTIONAL_PLUGIN_XMLS = [
+    "//golang:optional_xml",
     "//python:optional_xml",
     "//terminal:optional_xml",
 ]
@@ -74,3 +79,32 @@
         ":clwb_lib",
     ],
 )
+
+repackaged_files(
+    name = "plugin_jar",
+    srcs = [":clwb_bazel"],
+    prefix = "clwb/lib",
+)
+
+repackaged_files(
+    name = "aspect_directory",
+    srcs = ["//aspect:aspect_files"],
+    prefix = "clwb/aspect",
+)
+
+intellij_plugin_debug_target(
+    name = "clwb_bazel_dev",
+    deps = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+)
+
+plugin_deploy_zip(
+    name = "clwb_bazel_zip",
+    srcs = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+    zip_filename = "clwb_bazel.zip",
+)
diff --git a/clwb/src/META-INF/clwb.xml b/clwb/src/META-INF/clwb.xml
index 1b79d3c..186ca5d 100644
--- a/clwb/src/META-INF/clwb.xml
+++ b/clwb/src/META-INF/clwb.xml
@@ -49,7 +49,6 @@
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <SyncPlugin implementation="com.google.idea.blaze.clwb.sync.BlazeCLionSyncPlugin"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.clwb.run.BlazeCidrRunConfigurationHandlerProvider" order="first"/>
-    <RunConfigurationFactory implementation="com.google.idea.blaze.clwb.run.BlazeCidrDebuggableConfigurationFactory"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.clwb.run.test.BlazeCidrTestEventsHandler"/>
   </extensions>
 
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
deleted file mode 100644
index 55b0592..0000000
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
+++ /dev/null
@@ -1,65 +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.clwb.run;
-
-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.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
-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;
-
-/**
- * Creates run configurations for debuggable cc targets. Non-debuggable targets are handled by the
- * default factory.
- */
-public class BlazeCidrDebuggableConfigurationFactory extends BlazeRunConfigurationFactory {
-  @Override
-  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
-    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
-    return target != null && RunConfigurationUtils.canUseClionHandler(target.kind);
-  }
-
-  @Override
-  protected ConfigurationFactory getConfigurationFactory() {
-    return BlazeCommandRunConfigurationType.getInstance().getFactory();
-  }
-
-  @Override
-  public void setupConfiguration(RunConfiguration configuration, Label target) {
-    final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(target);
-
-    BlazeCommandRunConfigurationCommonState state =
-        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    Kind kind = blazeConfig.getKindForTarget();
-    if (state != null) {
-      if (kind != null && Kind.isTestRule(kind.toString())) {
-        state.getCommandState().setCommand(BlazeCommandName.TEST);
-      } else {
-        state.getCommandState().setCommand(BlazeCommandName.RUN);
-      }
-    }
-    blazeConfig.setGeneratedName();
-  }
-}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
index 473400b..ba964cf 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
@@ -20,17 +20,20 @@
 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.Kind;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 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.IssuesScope;
@@ -41,6 +44,8 @@
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.filters.Filter;
+import com.intellij.execution.filters.UrlFilter;
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.execution.process.ProcessListener;
 import com.intellij.execution.runners.ExecutionEnvironment;
@@ -54,6 +59,7 @@
 import com.jetbrains.cidr.execution.debugger.CidrDebugProcess;
 import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess;
 import com.jetbrains.cidr.execution.testing.CidrLauncher;
+import com.jetbrains.cidr.execution.testing.google.CidrGoogleTestConsoleProperties;
 import java.io.File;
 import javax.annotation.Nullable;
 
@@ -92,12 +98,12 @@
     LOG.assertTrue(projectViewSet != null);
 
     ImmutableList<String> testHandlerFlags = ImmutableList.of();
-    BlazeTestEventsHandler testEventsHandler =
+    BlazeTestUiSession testUiSession =
         useTestUi()
-            ? BlazeTestEventsHandler.getHandlerForTarget(project, configuration.getTarget())
+            ? TestUiSessionProvider.createForTarget(project, configuration.getTarget())
             : null;
-    if (testEventsHandler != null) {
-      testHandlerFlags = BlazeTestEventsHandler.getBlazeFlags(project);
+    if (testUiSession != null) {
+      testHandlerFlags = testUiSession.getBlazeFlags();
     }
 
     BlazeCommand.Builder command =
@@ -105,16 +111,18 @@
                 Blaze.getBuildSystemProvider(project).getBinaryPath(),
                 handlerState.getCommandState().getCommand())
             .addTargets(configuration.getTarget())
-            .addBlazeFlags(BlazeFlags.buildFlags(project, ProjectViewSet.builder().build()))
+            .addBlazeFlags(
+                BlazeFlags.blazeFlags(
+                    project,
+                    ProjectViewSet.builder().build(),
+                    handlerState.getCommandState().getCommand(),
+                    BlazeInvocationContext.RunConfiguration))
             .addBlazeFlags(testHandlerFlags)
             .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
             .addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
 
-    command.addBlazeFlags(
-        DistributedExecutorSupport.getBlazeFlags(
-            project, handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor));
-
-    state.setConsoleBuilder(createConsoleBuilder(testEventsHandler));
+    state.setConsoleBuilder(createConsoleBuilder(testUiSession));
+    state.addConsoleFilters(getConsoleFilters().toArray(new Filter[0]));
 
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
     return new ScopedBlazeProcessHandler(
@@ -155,6 +163,16 @@
     File workingDir = workspaceRoot.directory();
     commandLine.setWorkDirectory(workingDir);
     commandLine.addParameters(handlerState.getExeFlagsState().getExpandedFlags());
+    // Disable colored output, to workaround parsing bug (CPP-10054)
+    // Note: cc_test runner currently only supports GUnit tests.
+    if (Kind.CC_TEST.equals(configuration.getKindForTarget())) {
+      commandLine.addParameter("--gunit_color=no");
+    }
+
+    String testFilter = convertToGUnitTestFilter(handlerState.getTestFilterFlag());
+    if (testFilter != null) {
+      commandLine.addParameter(testFilter);
+    }
 
     TrivialInstaller installer = new TrivialInstaller(commandLine);
     ImmutableList<String> startupCommands = getGdbStartupCommands(workingDir);
@@ -162,18 +180,33 @@
         new CLionRunParameters(
             new BlazeGDBDriverConfiguration(project, startupCommands, workspaceRoot), installer);
 
+    state.setConsoleBuilder(createConsoleBuilder(null));
+    state.addConsoleFilters(getConsoleFilters().toArray(new Filter[0]));
     return new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
   }
 
+  /** Convert Blaze test filter to gunit test filter */
+  @Nullable
+  private static String convertToGUnitTestFilter(@Nullable String blazeTestFilter) {
+    if (blazeTestFilter == null || !blazeTestFilter.startsWith(BlazeFlags.TEST_FILTER)) {
+      return null;
+    }
+    return "--gunit_filter" + blazeTestFilter.substring(BlazeFlags.TEST_FILTER.length());
+  }
+
   @Override
   protected Project getProject() {
     return project;
   }
 
-  private CidrConsoleBuilder createConsoleBuilder(
-      @Nullable BlazeTestEventsHandler testEventsHandler) {
-    if (testEventsHandler != null) {
-      return new GoogleTestConsoleBuilder(configuration.getProject(), testEventsHandler);
+  private ImmutableList<Filter> getConsoleFilters() {
+    return ImmutableList.of(new BlazeTargetFilter(project), new UrlFilter());
+  }
+
+  private CidrConsoleBuilder createConsoleBuilder(@Nullable BlazeTestUiSession testUiSession) {
+    if (BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())) {
+      // hook up the test tree UI
+      return new GoogleTestConsoleBuilder(configuration.getProject(), testUiSession);
     }
     return new CidrConsoleBuilderAdapter(configuration.getProject());
   }
@@ -190,26 +223,35 @@
   }
 
   private boolean useTestUi() {
-    return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())
-        && !handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
+    return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand());
   }
 
   private final class GoogleTestConsoleBuilder extends CidrConsoleBuilderAdapter {
-    private final BlazeTestEventsHandler testEventsHandler;
+    @Nullable private final BlazeTestUiSession testUiSession;
 
-    private GoogleTestConsoleBuilder(Project project, BlazeTestEventsHandler testEventsHandler) {
+    private GoogleTestConsoleBuilder(Project project, @Nullable BlazeTestUiSession testUiSession) {
       super(project);
-      this.testEventsHandler = testEventsHandler;
+      this.testUiSession = testUiSession;
       addFilter(new BlazeCidrTestOutputFilter(project));
     }
 
     @Override
     protected ConsoleView createConsole() {
-      return SmRunnerUtils.getConsoleView(
-          configuration.getProject(),
-          configuration,
-          executionEnvironment.getExecutor(),
-          testEventsHandler);
+      if (testUiSession != null) {
+        return SmRunnerUtils.getConsoleView(
+            configuration.getProject(),
+            configuration,
+            executionEnvironment.getExecutor(),
+            testUiSession);
+      }
+      // When debugging, we run gdb manually on the debug binary, so the blaze test runners aren't
+      // involved.
+      CidrGoogleTestConsoleProperties consoleProperties =
+          new CidrGoogleTestConsoleProperties(
+              configuration,
+              executionEnvironment.getExecutor(),
+              executionEnvironment.getExecutionTarget());
+      return createConsole(configuration.getType(), consoleProperties);
     }
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
index 42a78c7..a64e9d0 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
@@ -16,15 +16,17 @@
 package com.google.idea.blaze.clwb.run;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
 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.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
 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;
@@ -37,6 +39,7 @@
 import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
@@ -47,8 +50,12 @@
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.util.PathUtil;
 import com.jetbrains.cidr.execution.CidrCommandLineState;
 import java.io.File;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 
 /** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
 public class BlazeCidrRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
@@ -74,6 +81,7 @@
 
   @Override
   public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
+    executableToDebug = null;
     if (!isDebugging(environment)) {
       return true;
     }
@@ -107,7 +115,10 @@
     final ProjectViewSet projectViewSet =
         ProjectViewManager.getInstance(project).getProjectViewSet();
 
-    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    BuildResultHelper buildResultHelper =
+        BuildResultHelper.forFiles(projectData.blazeVersionData, file -> true);
 
     final ListenableFuture<Void> buildOperation =
         BlazeExecutor.submitTask(
@@ -126,14 +137,19 @@
                             Blaze.getBuildSystemProvider(project).getBinaryPath(),
                             BlazeCommandName.BUILD)
                         .addTargets(configuration.getTarget())
-                        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                        .addBlazeFlags(
+                            BlazeFlags.blazeFlags(
+                                project,
+                                projectViewSet,
+                                BlazeCommandName.BUILD,
+                                BlazeInvocationContext.RunConfiguration))
                         .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
                         .addBlazeFlags(buildResultHelper.getBuildFlags());
 
                 // If we are trying to debug, make sure we are building in debug mode.
                 // This can cause a rebuild, so it is a heavyweight setting.
                 if (FORCE_DEBUG_BUILD_FOR_DEBUGGING_TEST.getValue()) {
-                  command.addBlazeFlags("-c", "dbg");
+                  command.addBlazeFlags("-c", "dbg", "--copt=-g", "--strip=never");
                 }
 
                 ExternalTask.builder(workspaceRoot)
@@ -153,18 +169,42 @@
     } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
       throw new ExecutionException(e);
     }
-    ImmutableList<File> outputArtifacts = buildResultHelper.getBuildArtifacts();
-    if (outputArtifacts.isEmpty()) {
+    List<File> candidateFiles =
+        buildResultHelper
+            .getBuildArtifactsForTarget((Label) configuration.getTarget())
+            .stream()
+            .filter(File::canExecute)
+            .collect(Collectors.toList());
+    if (candidateFiles.isEmpty()) {
       throw new ExecutionException(
           String.format("No output artifacts found when building %s", configuration.getTarget()));
     }
-    if (outputArtifacts.size() > 1) {
+    File file = findExecutable((Label) configuration.getTarget(), candidateFiles);
+    if (file == null) {
       throw new ExecutionException(
           String.format(
               "More than 1 executable was produced when building %s; don't know which one to debug",
               configuration.getTarget()));
     }
-    LocalFileSystem.getInstance().refreshIoFiles(outputArtifacts);
-    return Iterables.getOnlyElement(outputArtifacts);
+    LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file));
+    return file;
+  }
+
+  /**
+   * Basic heuristic for choosing between multiple output files. Currently just looks for a filename
+   * matching the target name.
+   */
+  @Nullable
+  private static File findExecutable(Label target, List<File> outputs) {
+    if (outputs.size() == 1) {
+      return outputs.get(0);
+    }
+    String name = PathUtil.getFileName(target.targetName().toString());
+    for (File file : outputs) {
+      if (file.getName().equals(name)) {
+        return file;
+      }
+    }
+    return null;
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
index f117615..839bd7c 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
@@ -23,16 +23,15 @@
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
 import com.intellij.openapi.project.Project;
 import java.util.ArrayList;
-import java.util.EnumSet;
 import java.util.List;
 import javax.annotation.Nullable;
 
 /** Provides C/C++ specific methods needed by the SM-runner test UI. */
-public class BlazeCidrTestEventsHandler extends BlazeTestEventsHandler {
+public class BlazeCidrTestEventsHandler implements BlazeTestEventsHandler {
 
   @Override
-  protected EnumSet<Kind> handledKinds() {
-    return EnumSet.of(Kind.CC_TEST);
+  public boolean handlesKind(@Nullable Kind kind) {
+    return kind == Kind.CC_TEST;
   }
 
   @Override
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
index 3d719bb..1b95745 100644
--- a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
@@ -48,7 +48,8 @@
   }
 
   private void init() {
-    control = new BlazeSelectWorkspaceControl(getProjectBuilder());
+    control =
+        new BlazeSelectWorkspaceControl(getProjectBuilder(), getWizardContext().getDisposable());
     this.component.add(control.getUiComponent());
     settingsInitialised = true;
   }
diff --git a/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java b/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java
index 7a2c2ca..ed1fc04 100644
--- a/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java
+++ b/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java
@@ -35,9 +35,11 @@
 public class CMakeWorkspaceOverride extends CMakeWorkspace {
 
   private final boolean isBlazeProject;
+  private final Project projectOverride;
 
   public CMakeWorkspaceOverride(Project project) {
     super(project);
+    projectOverride = project;
     isBlazeProject = Blaze.isBlazeProject(project);
   }
 
@@ -47,7 +49,7 @@
       super.projectOpened();
       return;
     }
-    removeClasspathStorageFromModules(myProject);
+    removeClasspathStorageFromModules(projectOverride);
   }
 
   /**
diff --git a/common/concurrency/BUILD b/common/concurrency/BUILD
new file mode 100644
index 0000000..a1d7730
--- /dev/null
+++ b/common/concurrency/BUILD
@@ -0,0 +1,11 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "concurrency",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/common/concurrency/src/com/google/idea/common/concurrency/ConcurrencyUtil.java b/common/concurrency/src/com/google/idea/common/concurrency/ConcurrencyUtil.java
new file mode 100644
index 0000000..16d4a46
--- /dev/null
+++ b/common/concurrency/src/com/google/idea/common/concurrency/ConcurrencyUtil.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.common.concurrency;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.intellij.util.concurrency.AppExecutorUtil;
+import java.util.concurrent.ThreadFactory;
+
+/** Utils for concurrency. */
+public final class ConcurrencyUtil {
+
+  private static final ListeningScheduledExecutorService executor =
+      MoreExecutors.listeningDecorator(AppExecutorUtil.getAppScheduledExecutorService());
+
+  private ConcurrencyUtil() {}
+
+  public static ListeningScheduledExecutorService getAppExecutorService() {
+    return executor;
+  }
+
+  public static ThreadFactory namedDaemonThreadPoolFactory(Class<?> klass) {
+    return new ThreadFactoryBuilder()
+        .setNameFormat(klass.getSimpleName() + "-%d")
+        .setDaemon(true)
+        .build();
+  }
+}
diff --git a/common/experiments/tests/utils/unit/com/google/idea/common/experiments/MockExperimentService.java b/common/experiments/tests/utils/unit/com/google/idea/common/experiments/MockExperimentService.java
index c144c70..144f178 100644
--- a/common/experiments/tests/utils/unit/com/google/idea/common/experiments/MockExperimentService.java
+++ b/common/experiments/tests/utils/unit/com/google/idea/common/experiments/MockExperimentService.java
@@ -51,6 +51,10 @@
     return defaultValue;
   }
 
+  public void setExperimentInt(IntExperiment experiment, int value) {
+    experiments.put(experiment.getKey(), value);
+  }
+
   @Override
   public int getExperimentInt(String key, int defaultValue) {
     if (experiments.containsKey(key)) {
diff --git a/common/guava/BUILD b/common/guava/BUILD
new file mode 100644
index 0000000..aa0735d
--- /dev/null
+++ b/common/guava/BUILD
@@ -0,0 +1,8 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "guava",
+    srcs = glob(["**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = ["//intellij_platform_sdk:plugin_api"],
+)
diff --git a/common/guava/src/com/google/idea/common/guava/GuavaHelper.java b/common/guava/src/com/google/idea/common/guava/GuavaHelper.java
new file mode 100644
index 0000000..0d4aa6c
--- /dev/null
+++ b/common/guava/src/com/google/idea/common/guava/GuavaHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.common.guava;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+/** Adds a few methods that aren't available until future versions of Guava. */
+public final class GuavaHelper {
+
+  private static final Collector<Object, ?, ImmutableSet<Object>> TO_IMMUTABLE_SET =
+      Collector.of(
+          ImmutableSet::<Object>builder,
+          ImmutableSet.Builder::add,
+          GuavaHelper::combineSet,
+          ImmutableSet.Builder::build);
+
+  private static final Collector<Object, ?, ImmutableList<Object>> TO_IMMUTABLE_LIST =
+      Collector.of(
+          ImmutableList::<Object>builder,
+          ImmutableList.Builder::add,
+          GuavaHelper::combineList,
+          ImmutableList.Builder::build);
+
+  /** Replaces {@code ImmutableSet#toImmutableSet}, which isn't available until Guava 21. */
+  @SuppressWarnings("unchecked") // so we can use a singleton
+  public static <E> Collector<E, ?, ImmutableSet<E>> toImmutableSet() {
+    return (Collector) TO_IMMUTABLE_SET;
+  }
+
+  /** Replaces {@code ImmutableList#toImmutableList}, which isn't available until Guava 21. */
+  @SuppressWarnings("unchecked") // so we can use a singleton
+  public static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+    return (Collector) TO_IMMUTABLE_LIST;
+  }
+
+  /** Replaces {@code ImmutableMap#toImmutableMap}, which isn't available until Guava 21. */
+  public static <T, K, V> Collector<T, ?, ImmutableMap<K, V>> toImmutableMap(
+      Function<? super T, ? extends K> keyFunction,
+      Function<? super T, ? extends V> valueFunction) {
+    return Collector.of(
+        ImmutableMap.Builder<K, V>::new,
+        (builder, input) -> builder.put(keyFunction.apply(input), valueFunction.apply(input)),
+        GuavaHelper::combineMap,
+        ImmutableMap.Builder::build);
+  }
+
+  private static <T> ImmutableSet.Builder<T> combineSet(
+      ImmutableSet.Builder<T> one, ImmutableSet.Builder<T> two) {
+    return one.addAll(two.build());
+  }
+
+  private static <T> ImmutableList.Builder<T> combineList(
+      ImmutableList.Builder<T> one, ImmutableList.Builder<T> two) {
+    return one.addAll(two.build());
+  }
+
+  private static <K, V> ImmutableMap.Builder<K, V> combineMap(
+      ImmutableMap.Builder<K, V> one, ImmutableMap.Builder<K, V> two) {
+    return one.putAll(two.build());
+  }
+
+  public static <T> Stream<T> stream(Optional<T> optional) {
+    return optional.isPresent() ? Stream.of(optional.get()) : Stream.of();
+  }
+}
diff --git a/cpp/BUILD b/cpp/BUILD
index bbf1c7e..e1f64c1 100644
--- a/cpp/BUILD
+++ b/cpp/BUILD
@@ -1,5 +1,17 @@
 licenses(["notice"])  # Apache 2.0
 
+load(
+    "//build_defs:build_defs.bzl",
+    "intellij_plugin",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+)
+load(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
+
 java_library(
     name = "cpp",
     srcs = glob(
@@ -11,6 +23,7 @@
         "//common/experiments",
         "//intellij_platform_sdk:plugin_api",
         "//sdkcompat",
+        "//third_party/auto_value",
         "@jsr305_annotations//jar",
     ],
 )
@@ -21,17 +34,58 @@
     visibility = ["//visibility:public"],
 )
 
-load(
-    "//testing:test_defs.bzl",
-    "intellij_unit_test_suite",
-)
-
 intellij_unit_test_suite(
     name = "unit_tests",
     srcs = glob(["tests/unittests/**/*.java"]),
     test_package_root = "com.google.idea.blaze.cpp",
     deps = [
         ":cpp",
+        "//base",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//sdkcompat",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml",
+    srcs = [
+        "//base:plugin_xml",
+    ] + [
+        ":plugin_xml",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "cpp_plugin_xml",
+    plugin_id = "com.google.idea.bazel.cpp",
+    plugin_name = "com.google.idea.bazel.cpp",
+    plugin_xml = ":merged_plugin_xml",
+)
+
+intellij_plugin(
+    name = "cpp_integration_test_plugin",
+    testonly = 1,
+    plugin_xml = ":cpp_plugin_xml",
+    deps = [
+        ":cpp",
+    ],
+)
+
+intellij_integration_test_suite(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    required_plugins = "com.google.idea.bazel.cpp",
+    test_package_root = "com.google.idea.blaze.cpp",
+    runtime_deps = [
+        ":cpp_integration_test_plugin",
+    ],
+    deps = [
+        ":cpp",
+        "//base",
+        "//base:integration_test_utils",
         "//base:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//sdkcompat",
diff --git a/cpp/src/META-INF/blaze-cpp.xml b/cpp/src/META-INF/blaze-cpp.xml
index 158f6d8..34f7f67 100644
--- a/cpp/src/META-INF/blaze-cpp.xml
+++ b/cpp/src/META-INF/blaze-cpp.xml
@@ -38,5 +38,11 @@
   </extensions>
   <extensions defaultExtensionNs="com.intellij">
     <projectService serviceImplementation="com.google.idea.blaze.cpp.BlazeCWorkspace"/>
+    <projectViewNodeDecorator implementation="com.google.idea.blaze.cpp.syncstatus.BlazeCppSyncStatusFileNodeDecorator"/>
+    <editorTabColorProvider implementation="com.google.idea.blaze.cpp.syncstatus.BlazeCppSyncStatusEditorTabColorProvider"/>
+    <editorTabTitleProvider implementation="com.google.idea.blaze.cpp.syncstatus.BlazeCppSyncStatusEditorTabTitleProvider"/>
+
+    <applicationService serviceInterface="com.google.idea.blaze.cpp.CompilerVersionChecker"
+                        serviceImplementation="com.google.idea.blaze.cpp.CompilerVersionCheckerImpl"/>
   </extensions>
 </idea-plugin>
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
index aa1736e..d26722d 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
@@ -32,7 +32,6 @@
 import com.intellij.openapi.vfs.VfsUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 import java.util.Set;
 
 final class BlazeCSyncPlugin extends BlazeSyncPlugin.Adapter {
@@ -65,10 +64,10 @@
         childContext -> {
           childContext.push(new TimingScope("Setup C Workspace"));
 
-          OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
+          OCWorkspace workspace = OCWorkspaceProvider.getWorkspace(project);
           if (workspace instanceof BlazeCWorkspace) {
             BlazeCWorkspace blazeCWorkspace = (BlazeCWorkspace) workspace;
-            blazeCWorkspace.update(childContext, blazeProjectData);
+            blazeCWorkspace.update(childContext, workspaceRoot, projectViewSet, blazeProjectData);
           }
         });
   }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
index c0f5b94..835aa1e 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -18,10 +18,11 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.sdkcompat.cidr.OCWorkspaceAdapter;
 import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.symbols.OCSymbol;
@@ -32,26 +33,28 @@
 
 /** Main entry point for C/CPP configuration data. */
 public final class BlazeCWorkspace extends OCWorkspaceAdapter {
-  private static final Logger logger = Logger.getInstance(BlazeCWorkspace.class);
-
   private final BlazeConfigurationResolver configurationResolver;
+  private BlazeConfigurationResolverResult resolverResult;
 
   private BlazeCWorkspace(Project project) {
     super(project);
     this.configurationResolver = new BlazeConfigurationResolver(project);
+    this.resolverResult = BlazeConfigurationResolverResult.empty(project);
   }
 
   public static BlazeCWorkspace getInstance(Project project) {
     return ServiceManager.getService(project, BlazeCWorkspace.class);
   }
 
-  public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
-    // Non-incremental update to our c configurations.
-    long start = System.currentTimeMillis();
-    configurationResolver.update(context, blazeProjectData);
-    long end = System.currentTimeMillis();
-
-    logger.info(String.format("Blaze OCWorkspace update took: %d ms", (end - start)));
+  public void update(
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData) {
+    BlazeConfigurationResolverResult oldResult = resolverResult;
+    resolverResult =
+        configurationResolver.update(
+            context, workspaceRoot, projectViewSet, blazeProjectData, oldResult);
   }
 
   @Override
@@ -82,8 +85,8 @@
   }
 
   @Override
-  public List<? extends OCResolveConfiguration> getConfigurations() {
-    return configurationResolver.getAllConfigurations();
+  public List<BlazeResolveConfiguration> getConfigurations() {
+    return resolverResult.getAllConfigurations();
   }
 
   @Override
@@ -92,7 +95,12 @@
     if (sourceFile == null || !sourceFile.isValid()) {
       return ImmutableList.of();
     }
-    OCResolveConfiguration config = configurationResolver.getConfigurationForFile(sourceFile);
+    OCResolveConfiguration config = resolverResult.getConfigurationForFile(sourceFile);
     return config == null ? ImmutableList.of() : ImmutableList.of(config);
   }
+
+  @Nullable
+  BlazeConfigurationResolverDiff getConfigurationDiff() {
+    return resolverResult.getConfigurationDiff();
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
index 654a8e2..b68aa1b 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
@@ -15,29 +15,33 @@
  */
 package com.google.idea.blaze.cpp;
 
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
+import com.google.idea.sdkcompat.cidr.OCCompilerMacrosAdapter;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.preprocessor.OCInclusionContext;
 import com.jetbrains.cidr.lang.preprocessor.OCInclusionContextUtil;
 import com.jetbrains.cidr.lang.workspace.compiler.CidrCompilerResult;
-import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
 import com.jetbrains.cidr.toolchains.CompilerInfoCache;
 import java.util.Map;
 
-final class BlazeCompilerMacros extends OCCompilerMacros {
-  private final CompilerInfoCache compilerInfoCache;
-  private final ImmutableCollection<String> globalDefines;
-  private final ImmutableMap<String, String> globalFeatures;
-  private final OCCompilerSettings compilerSettings;
+final class BlazeCompilerMacros extends OCCompilerMacrosAdapter {
   private final Project project;
 
-  public BlazeCompilerMacros(
+  private final CompilerInfoCache compilerInfoCache;
+  private final OCCompilerSettings compilerSettings;
+
+  private final ImmutableCollection<String> globalDefines;
+  private final ImmutableMap<String, String> globalFeatures;
+
+  BlazeCompilerMacros(
       Project project,
       CompilerInfoCache compilerInfoCache,
       OCCompilerSettings compilerSettings,
@@ -51,14 +55,10 @@
   }
 
   @Override
-  protected void fillFileMacros(OCInclusionContext context, PsiFile sourceFile) {
-    // Get the default compiler info for this file.
-    VirtualFile vf = OCInclusionContextUtil.getVirtualFile(sourceFile);
+  public String getAllDefines(OCLanguageKind kind, VirtualFile vf) {
     CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoProvider =
-        compilerInfoCache.getCompilerInfoCache(
-            project, compilerSettings, context.getLanguageKind(), vf);
+        compilerInfoCache.getCompilerInfoCache(project, compilerSettings, kind, vf);
     CompilerInfoCache.Entry compilerInfo = compilerInfoProvider.getResult();
-
     // Combine the info we got from Blaze with the info we get from IntelliJ's methods.
     ImmutableSet.Builder<String> allDefinesBuilder = ImmutableSet.builder();
     // IntelliJ expects a string of "#define [VAR_NAME] [VALUE]\n#define [VAR_NAME2] [VALUE]\n...",
@@ -76,14 +76,45 @@
       allDefines += "\n" + compilerInfo.defines;
     }
 
+    return allDefines;
+  }
+
+  @Override
+  protected void fillFileMacros(OCInclusionContext context, PsiFile sourceFile) {
+    // Get the default compiler info for this file.
+    VirtualFile vf = OCInclusionContextUtil.getVirtualFile(sourceFile);
+
+    CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoProvider =
+        compilerInfoCache.getCompilerInfoCache(
+            project, compilerSettings, context.getLanguageKind(), vf);
+    CompilerInfoCache.Entry compilerInfo = compilerInfoProvider.getResult();
+
     Map<String, String> allFeatures = Maps.newHashMap();
     allFeatures.putAll(globalFeatures);
     if (compilerInfo != null) {
-      allFeatures.putAll(compilerInfo.features);
+      addAllFeatures(allFeatures, compilerInfo.features);
     }
 
-    fillSubstitutions(context, allDefines);
+    fillSubstitutions(context, getAllDefines(context.getLanguageKind(), vf));
     enableClangFeatures(context, allFeatures);
     enableClangExtensions(context, allFeatures);
   }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof BlazeCompilerMacros)) {
+      return false;
+    }
+    BlazeCompilerMacros other = (BlazeCompilerMacros) obj;
+    return this.globalDefines.equals(other.globalDefines)
+        && this.globalFeatures.equals(other.globalFeatures);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(globalDefines, globalFeatures);
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
index ccc178d..679d19e 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
@@ -18,19 +18,22 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.sdkcompat.cidr.CidrSwitchBuilderAdapter;
+import com.google.idea.sdkcompat.cidr.OCCompilerSettingsAdapter;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
 import com.jetbrains.cidr.lang.toolchains.CidrToolEnvironment;
 import com.jetbrains.cidr.lang.toolchains.DefaultCidrToolEnvironment;
+import com.jetbrains.cidr.lang.workspace.compiler.CidrCompilerResult;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerKind;
-import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache.Entry;
 import java.io.File;
 import java.util.List;
 import javax.annotation.Nullable;
 
-final class BlazeCompilerSettings extends OCCompilerSettings {
+final class BlazeCompilerSettings extends OCCompilerSettingsAdapter {
   private final CidrToolEnvironment toolEnvironment = new DefaultCidrToolEnvironment();
 
   private final Project project;
@@ -38,18 +41,24 @@
   @Nullable private final File cppCompiler;
   private final CidrCompilerSwitches cCompilerSwitches;
   private final CidrCompilerSwitches cppCompilerSwitches;
+  private final String compilerVersion;
+  private final CompilerInfoCache compilerInfoCache;
 
   BlazeCompilerSettings(
       Project project,
       @Nullable File cCompiler,
       @Nullable File cppCompiler,
       ImmutableList<String> cFlags,
-      ImmutableList<String> cppFlags) {
+      ImmutableList<String> cppFlags,
+      String compilerVersion,
+      CompilerInfoCache compilerInfoCache) {
     this.project = project;
     this.cCompiler = cCompiler;
     this.cppCompiler = cppCompiler;
     this.cCompilerSwitches = getCompilerSwitches(cFlags);
     this.cppCompilerSwitches = getCompilerSwitches(cppFlags);
+    this.compilerVersion = compilerVersion;
+    this.compilerInfoCache = compilerInfoCache;
   }
 
   @Override
@@ -93,7 +102,21 @@
     return new CidrSwitchBuilderAdapter().build();
   }
 
+  String getCompilerVersion() {
+    return compilerVersion;
+  }
+
   private static CidrCompilerSwitches getCompilerSwitches(List<String> allCompilerFlags) {
     return new CidrSwitchBuilderAdapter().addAllRaw(allCompilerFlags).build();
   }
+
+  public CidrCompilerResult<Entry> getCompilerInfo(
+      OCLanguageKind ocLanguageKind, @Nullable VirtualFile virtualFile) {
+    return compilerInfoCache.getCompilerInfoCache(project, this, ocLanguageKind, virtualFile);
+  }
+
+  @Override
+  public String getCompilerKey(OCLanguageKind ocLanguageKind, @Nullable VirtualFile virtualFile) {
+    return getCompiler(ocLanguageKind).toString();
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index d01798e..5708f9b 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -15,104 +15,134 @@
  */
 package com.google.idea.blaze.cpp;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 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.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 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.io.FileAttributeProvider;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 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;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.ScopedFunction;
+import com.google.idea.blaze.base.scope.ScopedOperation;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewTargetImportFilter;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
-import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.io.FileUtilRt;
 import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
-import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
 import com.jetbrains.cidr.toolchains.CompilerInfoCache;
 import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 final class BlazeConfigurationResolver {
-  private static final class MapEntry {
-    public final TargetKey targetKey;
-    public final BlazeResolveConfiguration configuration;
+  private static final Logger logger = Logger.getInstance(BlazeConfigurationResolver.class);
+  // Don't recursively check too many directories, in case the root is just too big.
+  // Sometimes genfiles/java is considered a header search root.
+  private static final int GEN_HEADER_ROOT_SEARCH_LIMIT = 50;
 
-    public MapEntry(TargetKey targetKey, BlazeResolveConfiguration configuration) {
-      this.targetKey = targetKey;
-      this.configuration = configuration;
-    }
-  }
-
-  private static final Logger LOG = Logger.getInstance(BlazeConfigurationResolver.class);
   private final Project project;
 
-  private ImmutableMap<TargetKey, BlazeResolveConfiguration> resolveConfigurations =
-      ImmutableMap.of();
-
-  public BlazeConfigurationResolver(Project project) {
+  BlazeConfigurationResolver(Project project) {
     this.project = project;
   }
 
-  public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
+  public BlazeConfigurationResolverResult update(
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      BlazeConfigurationResolverResult oldResult) {
+    ExecutionRootPathResolver executionRootPathResolver =
+        new ExecutionRootPathResolver(
+            Blaze.getBuildSystem(project),
+            WorkspaceRoot.fromProject(project),
+            blazeProjectData.blazeInfo.getExecutionRoot(),
+            blazeProjectData.workspacePathResolver);
     ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap =
-        BlazeResolveConfiguration.buildToolchainLookupMap(context, blazeProjectData.targetMap);
+        BlazeConfigurationToolchainResolver.buildToolchainLookupMap(
+            context, blazeProjectData.targetMap);
     ImmutableMap<File, VirtualFile> headerRoots =
-        collectHeaderRoots(context, blazeProjectData, toolchainLookupMap);
-    ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings =
-        buildCompilerSettingsMap(
-            context, project, toolchainLookupMap, blazeProjectData.workspacePathResolver);
+        collectHeaderRoots(
+            context, blazeProjectData, toolchainLookupMap, executionRootPathResolver);
     CompilerInfoCache compilerInfoCache = new CompilerInfoCache();
-    resolveConfigurations =
-        buildBlazeConfigurationMap(
+    ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings =
+        BlazeConfigurationToolchainResolver.buildCompilerSettingsMap(
             context,
-            blazeProjectData,
+            project,
+            workspaceRoot,
             toolchainLookupMap,
-            headerRoots,
-            compilerSettings,
-            compilerInfoCache);
+            blazeProjectData.workspacePathResolver,
+            compilerInfoCache,
+            oldResult.compilerSettings);
+    BlazeConfigurationResolverResult.Builder builder =
+        BlazeConfigurationResolverResult.builder(project);
+    buildBlazeConfigurationData(
+        context,
+        workspaceRoot,
+        projectViewSet,
+        blazeProjectData,
+        toolchainLookupMap,
+        headerRoots,
+        compilerSettings,
+        compilerInfoCache,
+        executionRootPathResolver,
+        oldResult,
+        builder);
+    builder.setCompilerSettings(compilerSettings);
+    builder.setResolveDiff(
+        computeConfigurationDiff(
+            blazeProjectData, builder.configurationMap, oldResult.configurationMap));
+    return builder.build();
   }
 
-  private ImmutableMap<File, VirtualFile> collectHeaderRoots(
+  private static ImmutableMap<File, VirtualFile> collectHeaderRoots(
       BlazeContext parentContext,
       BlazeProjectData blazeProjectData,
-      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
+      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
+      ExecutionRootPathResolver executionRootPathResolver) {
     // Type specification needed to avoid incorrect type inference during command line build.
     return Scope.push(
         parentContext,
@@ -121,20 +151,20 @@
               context.push(new TimingScope("Resolve header include roots"));
               Set<ExecutionRootPath> paths =
                   collectExecutionRootPaths(blazeProjectData.targetMap, toolchainLookupMap);
-              return doCollectHeaderRoots(context, blazeProjectData, paths);
+              return doCollectHeaderRoots(
+                  context, blazeProjectData, paths, executionRootPathResolver);
             });
   }
 
-  private ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
-      BlazeContext context, BlazeProjectData projectData, Set<ExecutionRootPath> rootPaths) {
-    ExecutionRootPathResolver pathResolver =
-        new ExecutionRootPathResolver(
-            Blaze.getBuildSystem(project),
-            WorkspaceRoot.fromProject(project),
-            projectData.blazeInfo.getExecutionRoot(),
-            projectData.workspacePathResolver);
+  private static ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
+      BlazeContext context,
+      BlazeProjectData projectData,
+      Set<ExecutionRootPath> rootPaths,
+      ExecutionRootPathResolver pathResolver) {
     ConcurrentMap<File, VirtualFile> rootsMap = Maps.newConcurrentMap();
     List<ListenableFuture<Void>> futures = Lists.newArrayListWithCapacity(rootPaths.size());
+    AtomicInteger genRootsWithHeaders = new AtomicInteger();
+    AtomicInteger genRootsWithoutHeaders = new AtomicInteger();
     for (ExecutionRootPath path : rootPaths) {
       futures.add(
           submit(
@@ -144,11 +174,20 @@
                 for (File file : possibleDirectories) {
                   VirtualFile vf = getVirtualFile(file);
                   if (vf != null) {
-                    rootsMap.put(file, vf);
+                    // Check gen directories to see if they actually contain headers and not just
+                    // other random generated files (like .s, .cc, or module maps).
+                    if (!isOutputArtifact(projectData.blazeInfo, path)) {
+                      rootsMap.put(file, vf);
+                    } else if (genRootMayContainHeaders(vf)) {
+                      genRootsWithHeaders.incrementAndGet();
+                      rootsMap.put(file, vf);
+                    } else {
+                      genRootsWithoutHeaders.incrementAndGet();
+                    }
                   } else if (!isOutputArtifact(projectData.blazeInfo, path)
                       && FileAttributeProvider.getInstance().exists(file)) {
                     // If it's not a blaze output file, we expect it to always resolve.
-                    LOG.info(String.format("Unresolved header root %s", file.getAbsolutePath()));
+                    logger.info(String.format("Unresolved header root %s", file.getAbsolutePath()));
                   }
                 }
                 return null;
@@ -156,17 +195,46 @@
     }
     try {
       Futures.allAsList(futures).get();
-      return ImmutableMap.copyOf(rootsMap);
+      ImmutableMap<File, VirtualFile> result = ImmutableMap.copyOf(rootsMap);
+      logger.info(
+          String.format(
+              "CollectHeaderRoots: %s roots, (%s, %s) genroots with/without headers",
+              result.size(), genRootsWithHeaders.get(), genRootsWithoutHeaders.get()));
+      return result;
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
       context.setCancelled();
     } catch (ExecutionException e) {
       IssueOutput.error("Error resolving header include roots: " + e).submit(context);
-      LOG.error("Error resolving header include roots", e);
+      logger.error("Error resolving header include roots", e);
     }
     return ImmutableMap.of();
   }
 
+  private static boolean genRootMayContainHeaders(VirtualFile directory) {
+    int totalDirectoriesChecked = 0;
+    Queue<VirtualFile> worklist = new ArrayDeque<>();
+    worklist.add(directory);
+    while (!worklist.isEmpty()) {
+      totalDirectoriesChecked++;
+      if (totalDirectoriesChecked > GEN_HEADER_ROOT_SEARCH_LIMIT) {
+        return true;
+      }
+      VirtualFile dir = worklist.poll();
+      for (VirtualFile child : dir.getChildren()) {
+        if (child.isDirectory()) {
+          worklist.add(child);
+          continue;
+        }
+        String fileExtension = child.getExtension();
+        if (CFileExtensions.HEADER_EXTENSIONS.contains(fileExtension)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   private static boolean isOutputArtifact(BlazeInfo blazeInfo, ExecutionRootPath path) {
     return ExecutionRootPath.isAncestor(blazeInfo.getBlazeGenfilesExecutionRootPath(), path, false)
         || ExecutionRootPath.isAncestor(blazeInfo.getBlazeBinExecutionRootPath(), path, false);
@@ -193,7 +261,7 @@
 
   @Nullable
   private static VirtualFile getVirtualFile(File file) {
-    LocalFileSystem fileSystem = LocalFileSystem.getInstance();
+    LocalFileSystem fileSystem = VirtualFileSystemProvider.getInstance().getSystem();
     VirtualFile vf = fileSystem.findFileByPathIfCached(file.getPath());
     if (vf == null) {
       vf = fileSystem.findFileByIoFile(file);
@@ -201,77 +269,155 @@
     return vf;
   }
 
-  private ImmutableMap<TargetKey, BlazeResolveConfiguration> buildBlazeConfigurationMap(
+  private static boolean containsCompiledSources(TargetIdeInfo target) {
+    Predicate<ArtifactLocation> isCompiled =
+        location -> {
+          String locationExtension = FileUtilRt.getExtension(location.getRelativePath());
+          return CFileExtensions.SOURCE_EXTENSIONS.contains(locationExtension);
+        };
+    return target.cIdeInfo != null
+        && target.cIdeInfo.sources.stream().filter(ArtifactLocation::isSource).anyMatch(isCompiled);
+  }
+
+  private void buildBlazeConfigurationData(
       BlazeContext parentContext,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
       ImmutableMap<File, VirtualFile> headerRoots,
       ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings,
-      CompilerInfoCache compilerInfoCache) {
+      CompilerInfoCache compilerInfoCache,
+      ExecutionRootPathResolver executionRootPathResolver,
+      BlazeConfigurationResolverResult oldConfigurationData,
+      BlazeConfigurationResolverResult.Builder builder) {
     // Type specification needed to avoid incorrect type inference during command line build.
-    return Scope.push(
+    Scope.push(
         parentContext,
-        (ScopedFunction<ImmutableMap<TargetKey, BlazeResolveConfiguration>>)
+        (ScopedOperation)
             context -> {
               context.push(new TimingScope("Build C configuration map"));
 
-              List<ListenableFuture<MapEntry>> mapEntryFutures = Lists.newArrayList();
+              ProjectViewTargetImportFilter filter =
+                  new ProjectViewTargetImportFilter(project, workspaceRoot, projectViewSet);
 
-              for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
-                if (target.kind.getLanguageClass() == LanguageClass.C
-                    && target.kind != Kind.CC_TOOLCHAIN) {
-                  ListenableFuture<MapEntry> future =
-                      submit(
-                          () ->
-                              createResolveConfiguration(
-                                  target,
-                                  toolchainLookupMap,
-                                  headerRoots,
-                                  compilerSettings,
-                                  blazeProjectData,
-                                  compilerInfoCache));
-                  mapEntryFutures.add(future);
-                }
-              }
-
-              ImmutableMap.Builder<TargetKey, BlazeResolveConfiguration> newResolveConfigurations =
-                  ImmutableMap.builder();
-              List<MapEntry> mapEntries;
+              ConcurrentMap<TargetKey, BlazeResolveConfigurationData> targetToData =
+                  Maps.newConcurrentMap();
+              List<ListenableFuture<?>> targetToDataFutures =
+                  blazeProjectData
+                      .targetMap
+                      .targets()
+                      .stream()
+                      .filter(target -> target.kind.languageClass == LanguageClass.C)
+                      .filter(target -> target.kind != Kind.CC_TOOLCHAIN)
+                      .filter(filter::isSourceTarget)
+                      .filter(BlazeConfigurationResolver::containsCompiledSources)
+                      .map(
+                          target ->
+                              submit(
+                                  () -> {
+                                    BlazeResolveConfigurationData data =
+                                        createResolveConfiguration(
+                                            target,
+                                            toolchainLookupMap,
+                                            headerRoots,
+                                            compilerSettings,
+                                            compilerInfoCache,
+                                            executionRootPathResolver);
+                                    if (data != null) {
+                                      targetToData.put(target.key, data);
+                                    }
+                                    return null;
+                                  }))
+                      .collect(Collectors.toList());
               try {
-                mapEntries = Futures.allAsList(mapEntryFutures).get();
+                Futures.allAsList(targetToDataFutures).get();
               } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
                 context.setCancelled();
-                return ImmutableMap.of();
+                return;
               } catch (ExecutionException e) {
                 IssueOutput.error("Could not build C resolve configurations: " + e).submit(context);
-                LOG.error("Could not build C resolve configurations", e);
-                return ImmutableMap.of();
+                logger.error("Could not build C resolve configurations", e);
+                return;
               }
-
-              for (MapEntry mapEntry : mapEntries) {
-                // Skip over labels that don't have C configuration data.
-                if (mapEntry != null) {
-                  newResolveConfigurations.put(mapEntry.targetKey, mapEntry.configuration);
-                }
-              }
-              return newResolveConfigurations.build();
+              findEquivalenceClasses(
+                  context,
+                  project,
+                  blazeProjectData.workspacePathResolver,
+                  targetToData,
+                  oldConfigurationData,
+                  builder);
             });
   }
 
+  private static void findEquivalenceClasses(
+      BlazeContext context,
+      Project project,
+      WorkspacePathResolver workspacePathResolver,
+      Map<TargetKey, BlazeResolveConfigurationData> targetToData,
+      BlazeConfigurationResolverResult oldConfigurationData,
+      BlazeConfigurationResolverResult.Builder builder) {
+    Map<BlazeResolveConfigurationData, BlazeResolveConfiguration> dataToConfiguration =
+        new HashMap<>();
+    Multimap<BlazeResolveConfigurationData, TargetKey> dataEquivalenceClasses =
+        ArrayListMultimap.create();
+    int reused = 0;
+    for (Map.Entry<TargetKey, BlazeResolveConfigurationData> entry : targetToData.entrySet()) {
+      TargetKey target = entry.getKey();
+      BlazeResolveConfigurationData data = entry.getValue();
+      if (!dataToConfiguration.containsKey(data)) {
+        BlazeResolveConfiguration configuration;
+        if (oldConfigurationData.uniqueResolveConfigurations.containsKey(data)) {
+          configuration = oldConfigurationData.uniqueResolveConfigurations.get(data);
+          reused++;
+        } else {
+          configuration =
+              BlazeResolveConfiguration.createForTargets(
+                  project, workspacePathResolver, data, ImmutableList.of(target));
+        }
+        dataToConfiguration.put(data, configuration);
+      }
+      dataEquivalenceClasses.put(data, target);
+    }
+    ImmutableMap.Builder<TargetKey, BlazeResolveConfiguration> targetToConfiguration =
+        ImmutableMap.builder();
+    for (Map.Entry<BlazeResolveConfigurationData, Collection<TargetKey>> entry :
+        dataEquivalenceClasses.asMap().entrySet()) {
+      BlazeResolveConfigurationData data = entry.getKey();
+      Collection<TargetKey> targets = entry.getValue();
+      BlazeResolveConfiguration configuration = dataToConfiguration.get(data);
+      configuration.representMultipleTargets(targets);
+      for (TargetKey targetKey : targets) {
+        targetToConfiguration.put(targetKey, configuration);
+      }
+    }
+    context.output(
+        PrintOutput.log(
+            String.format(
+                "%s unique C configurations (%s reused), %s C targets",
+                dataEquivalenceClasses.keySet().size(), reused, dataEquivalenceClasses.size())));
+    builder.setConfigurationMap(targetToConfiguration.build());
+    builder.setUniqueConfigurations(ImmutableMap.copyOf(dataToConfiguration));
+  }
+
   private static <T> ListenableFuture<T> submit(Callable<T> callable) {
     return BlazeExecutor.getInstance().submit(callable);
   }
 
   @Nullable
-  private MapEntry createResolveConfiguration(
+  private BlazeResolveConfigurationData createResolveConfiguration(
       TargetIdeInfo target,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
       ImmutableMap<File, VirtualFile> headerRoots,
       ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettingsMap,
-      BlazeProjectData blazeProjectData,
-      CompilerInfoCache compilerInfoCache) {
+      CompilerInfoCache compilerInfoCache,
+      ExecutionRootPathResolver executionRootPathResolver) {
     TargetKey targetKey = target.key;
+    CIdeInfo cIdeInfo = target.cIdeInfo;
+    if (cIdeInfo == null) {
+      return null;
+    }
     CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(targetKey);
     if (toolchainIdeInfo == null) {
       return null;
@@ -280,177 +426,76 @@
     if (compilerSettings == null) {
       return null;
     }
-    BlazeResolveConfiguration config =
-        BlazeResolveConfiguration.createConfigurationForTarget(
-            project,
-            new ExecutionRootPathResolver(
-                Blaze.getBuildSystem(project),
-                WorkspaceRoot.fromProject(project),
-                blazeProjectData.blazeInfo.getExecutionRoot(),
-                blazeProjectData.workspacePathResolver),
-            blazeProjectData.workspacePathResolver,
-            headerRoots,
-            blazeProjectData.targetMap.get(targetKey),
-            toolchainIdeInfo,
-            compilerSettings,
-            compilerInfoCache);
-    if (config == null) {
-      return null;
-    }
-    return new MapEntry(targetKey, config);
+    return BlazeResolveConfigurationData.create(
+        project,
+        executionRootPathResolver,
+        headerRoots,
+        cIdeInfo,
+        toolchainIdeInfo,
+        compilerSettings,
+        compilerInfoCache);
   }
 
-  private static ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> buildCompilerSettingsMap(
-      BlazeContext context,
-      Project project,
-      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
-      WorkspacePathResolver workspacePathResolver) {
-    Set<CToolchainIdeInfo> toolchains =
-        toolchainLookupMap.values().stream().distinct().collect(Collectors.toSet());
-    List<ListenableFuture<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>>>
-        compilerSettingsFutures = new ArrayList<>();
-    for (CToolchainIdeInfo toolchain : toolchains) {
-      compilerSettingsFutures.add(
-          submit(
-              () -> {
-                BlazeCompilerSettings settings =
-                    createBlazeCompilerSettings(project, toolchain, workspacePathResolver);
-                if (settings == null) {
-                  return null;
-                }
-                return new SimpleImmutableEntry<>(toolchain, settings);
-              }));
+  @Nullable
+  private static BlazeConfigurationResolverDiff computeConfigurationDiff(
+      BlazeProjectData blazeProjectData,
+      ImmutableMap<TargetKey, BlazeResolveConfiguration> newConfigs,
+      ImmutableMap<TargetKey, BlazeResolveConfiguration> oldConfigs) {
+    if (oldConfigs.isEmpty()) {
+      return null;
     }
-    ImmutableMap.Builder<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettingsMap =
-        ImmutableMap.builder();
+    List<ListenableFuture<List<VirtualFile>>> fileResolveFutures = new ArrayList<>();
+    for (Map.Entry<TargetKey, BlazeResolveConfiguration> entry : newConfigs.entrySet()) {
+      TargetKey targetKey = entry.getKey();
+      BlazeResolveConfiguration newConfiguration = entry.getValue();
+      BlazeResolveConfiguration oldConfiguration = oldConfigs.get(targetKey);
+      if (newConfiguration != oldConfiguration) {
+        fileResolveFutures.add(
+            submit(
+                () ->
+                    changedFilesForTarget(
+                        blazeProjectData.targetMap,
+                        blazeProjectData.artifactLocationDecoder,
+                        targetKey)));
+      }
+    }
+    ImmutableSet.Builder<VirtualFile> changedFiles = ImmutableSet.builder();
     try {
-      List<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>> createdSettings =
-          Futures.allAsList(compilerSettingsFutures).get();
-      for (Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings> createdSetting : createdSettings) {
-        if (createdSetting != null) {
-          compilerSettingsMap.put(createdSetting);
-        }
+      for (List<VirtualFile> changedFilesForTarget : Futures.allAsList(fileResolveFutures).get()) {
+        changedFiles.addAll(changedFilesForTarget);
       }
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
-      context.setCancelled();
+      return null;
     } catch (ExecutionException e) {
-      IssueOutput.error("Could not build C compiler settings map: " + e).submit(context);
-      LOG.error("Could not build C compiler settings map", e);
-    }
-    return compilerSettingsMap.build();
-  }
-
-  @Nullable
-  private static BlazeCompilerSettings createBlazeCompilerSettings(
-      Project project,
-      CToolchainIdeInfo toolchainIdeInfo,
-      WorkspacePathResolver workspacePathResolver) {
-    File compilerWrapper = getCompilerWrapper(toolchainIdeInfo, workspacePathResolver);
-    if (compilerWrapper == null) {
+      logger.error("Error getting changed files", e);
       return null;
     }
-    ImmutableList.Builder<String> cFlagsBuilder = ImmutableList.builder();
-    cFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
-    cFlagsBuilder.addAll(toolchainIdeInfo.cCompilerOptions);
-    cFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
-
-    ImmutableList.Builder<String> cppFlagsBuilder = ImmutableList.builder();
-    cppFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
-    cppFlagsBuilder.addAll(toolchainIdeInfo.cppCompilerOptions);
-    cppFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
-    return new BlazeCompilerSettings(
-        project, compilerWrapper, compilerWrapper, cFlagsBuilder.build(), cppFlagsBuilder.build());
+    return new BlazeConfigurationResolverDiff(
+        changedFiles.build(), hasRemovedTargets(newConfigs, oldConfigs));
   }
 
-  @Nullable
-  private static File getCompilerWrapper(
-      CToolchainIdeInfo toolchainIdeInfo, WorkspacePathResolver workspacePathResolver) {
-    File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
-    if (cppExecutable != null && !cppExecutable.isAbsolute()) {
-      cppExecutable = workspacePathResolver.resolveToFile(cppExecutable.getPath());
-    }
-    if (cppExecutable == null) {
-      LOG.warn(
-          String.format(
-              "Unable to find compiler executable: %s for toolchain %s",
-              toolchainIdeInfo.cppExecutable.toString(), toolchainIdeInfo));
-      return null;
-    }
-    return createCompilerExecutableWrapper(cppExecutable);
-  }
-
-  /**
-   * Create a wrapper script that transforms the CLion compiler invocation into a safe invocation of
-   * the compiler script that blaze uses.
-   *
-   * <p>CLion passes arguments to the compiler in an arguments file. The c toolchain compiler
-   * wrapper script doesn't handle arguments files, so we need to move the compiler arguments from
-   * the file to the command line.
-   *
-   * @param blazeCompilerExecutableFile blaze compiler wrapper
-   * @return The wrapper script that CLion can call.
-   */
-  @Nullable
-  private static File createCompilerExecutableWrapper(File blazeCompilerExecutableFile) {
-    try {
-      File blazeCompilerWrapper =
-          FileUtil.createTempFile("blaze_compiler", ".sh", true /* deleteOnExit */);
-      if (!blazeCompilerWrapper.setExecutable(true)) {
-        return null;
+  private static List<VirtualFile> changedFilesForTarget(
+      TargetMap targetMap, ArtifactLocationDecoder locationDecoder, TargetKey targetKey) {
+    List<VirtualFile> changedFilesForTarget = new ArrayList<>();
+    for (ArtifactLocation sourceLocation : targetMap.get(targetKey).sources) {
+      File sourceFile = locationDecoder.decode(sourceLocation);
+      VirtualFile virtualFile = getVirtualFile(sourceFile);
+      if (virtualFile != null) {
+        changedFilesForTarget.add(virtualFile);
       }
-      ImmutableList<String> compilerWrapperScriptLines =
-          ImmutableList.of(
-              "#!/bin/bash",
-              "",
-              "# The c toolchain compiler wrapper script doesn't handle arguments files, so we",
-              "# need to move the compiler arguments from the file to the command line.",
-              "",
-              "if [ $# -ne 2 ]; then",
-              "  echo \"Usage: $0 @arg-file compile-file\"",
-              "  exit 2;",
-              "fi",
-              "",
-              "if [[ $1 != @* ]]; then",
-              "  echo \"Usage: $0 @arg-file compile-file\"",
-              "  exit 3;",
-              "fi",
-              "",
-              " # Remove the @ before the arguments file path",
-              "ARG_FILE=${1#@}",
-              "# The actual compiler wrapper script we get from blaze",
-              "EXE=" + blazeCompilerExecutableFile.getPath(),
-              "# Read in the arguments file so we can pass the arguments on the command line.",
-              "ARGS=`cat $ARG_FILE`",
-              "$EXE $ARGS $2");
+    }
+    return changedFilesForTarget;
+  }
 
-      try (PrintWriter pw = new PrintWriter(blazeCompilerWrapper, UTF_8.name())) {
-        compilerWrapperScriptLines.forEach(pw::println);
+  private static boolean hasRemovedTargets(
+      Map<TargetKey, BlazeResolveConfiguration> newConfigs,
+      Map<TargetKey, BlazeResolveConfiguration> oldConfigs) {
+    for (TargetKey oldKey : oldConfigs.keySet()) {
+      if (!newConfigs.containsKey(oldKey)) {
+        return true;
       }
-      return blazeCompilerWrapper;
-    } catch (IOException e) {
-      return null;
     }
-  }
-
-  @Nullable
-  public OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
-    SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
-    ImmutableCollection<TargetKey> targetsForSourceFile =
-        sourceToTargetMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile));
-    if (targetsForSourceFile.isEmpty()) {
-      return null;
-    }
-
-    // If a source file is in two different targets, we can't possibly show how it will be
-    // interpreted in both contexts at the same time in the IDE, so just pick the "first" target.
-    TargetKey targetKey = targetsForSourceFile.stream().min(TargetKey::compareTo).orElse(null);
-    assert (targetKey != null);
-
-    return resolveConfigurations.get(targetKey);
-  }
-
-  ImmutableList<? extends OCResolveConfiguration> getAllConfigurations() {
-    return resolveConfigurations.values().asList();
+    return false;
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverDiff.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverDiff.java
new file mode 100644
index 0000000..bb1e0df
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverDiff.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.cpp;
+
+import com.google.common.collect.ImmutableSet;
+import com.intellij.openapi.vfs.VirtualFile;
+import javax.annotation.concurrent.Immutable;
+
+/** Summary of differences between two {@link BlazeConfigurationResolver}s. */
+@Immutable
+final class BlazeConfigurationResolverDiff {
+
+  private final ImmutableSet<VirtualFile> changedFiles;
+  private final boolean hasRemovedTargets;
+
+  BlazeConfigurationResolverDiff(
+      ImmutableSet<VirtualFile> changedFiles, boolean hasRemovedTargets) {
+    this.changedFiles = changedFiles;
+    this.hasRemovedTargets = hasRemovedTargets;
+  }
+
+  ImmutableSet<VirtualFile> getChangedFiles() {
+    return changedFiles;
+  }
+
+  boolean hasChanges() {
+    return hasRemovedTargets || !changedFiles.isEmpty();
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverResult.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverResult.java
new file mode 100644
index 0000000..6d95662
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolverResult.java
@@ -0,0 +1,135 @@
+/*
+ * 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.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Resolve configuration maps, etc. obtained from running the {@link BlazeConfigurationResolver}.
+ */
+@Immutable
+final class BlazeConfigurationResolverResult {
+
+  private final Project project;
+
+  // Multiple target keys may map to the same resolve configuration.
+  final ImmutableMap<TargetKey, BlazeResolveConfiguration> configurationMap;
+  final ImmutableMap<BlazeResolveConfigurationData, BlazeResolveConfiguration>
+      uniqueResolveConfigurations;
+  final ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings;
+  @Nullable final BlazeConfigurationResolverDiff resolverDiff;
+
+  BlazeConfigurationResolverResult(
+      Project project,
+      ImmutableMap<TargetKey, BlazeResolveConfiguration> configurationMap,
+      ImmutableMap<BlazeResolveConfigurationData, BlazeResolveConfiguration>
+          uniqueResolveConfigurations,
+      ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings,
+      @Nullable BlazeConfigurationResolverDiff resolverDiff) {
+    this.project = project;
+    this.configurationMap = configurationMap;
+    this.uniqueResolveConfigurations = uniqueResolveConfigurations;
+    this.compilerSettings = compilerSettings;
+    this.resolverDiff = resolverDiff;
+  }
+
+  static Builder builder(Project project) {
+    return new Builder(project);
+  }
+
+  static BlazeConfigurationResolverResult empty(Project project) {
+    return builder(project).build();
+  }
+
+  @Nullable
+  OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
+    SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
+    ImmutableCollection<TargetKey> targetsForSourceFile =
+        sourceToTargetMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile));
+    if (targetsForSourceFile.isEmpty()) {
+      return null;
+    }
+
+    // If a source file is in two different targets, we can't possibly show how it will be
+    // interpreted in both contexts at the same time in the IDE, so just pick the "first" target.
+    TargetKey targetKey = targetsForSourceFile.stream().min(TargetKey::compareTo).orElse(null);
+    Preconditions.checkNotNull(targetKey);
+
+    return configurationMap.get(targetKey);
+  }
+
+  ImmutableList<BlazeResolveConfiguration> getAllConfigurations() {
+    return uniqueResolveConfigurations.values().asList();
+  }
+
+  /**
+   * The difference between the latest resolver result and the previous one, if known. Returns null
+   * if unknown (or there is no previous result).
+   */
+  @Nullable
+  BlazeConfigurationResolverDiff getConfigurationDiff() {
+    return resolverDiff;
+  }
+
+  static class Builder {
+    final Project project;
+    ImmutableMap<TargetKey, BlazeResolveConfiguration> configurationMap = ImmutableMap.of();
+    ImmutableMap<BlazeResolveConfigurationData, BlazeResolveConfiguration> uniqueConfigurations =
+        ImmutableMap.of();
+    ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings = ImmutableMap.of();
+    @Nullable BlazeConfigurationResolverDiff resolverDiff;
+
+    public Builder(Project project) {
+      this.project = project;
+    }
+
+    BlazeConfigurationResolverResult build() {
+      return new BlazeConfigurationResolverResult(
+          project, configurationMap, uniqueConfigurations, compilerSettings, resolverDiff);
+    }
+
+    void setConfigurationMap(ImmutableMap<TargetKey, BlazeResolveConfiguration> configurationMap) {
+      this.configurationMap = configurationMap;
+    }
+
+    void setUniqueConfigurations(
+        ImmutableMap<BlazeResolveConfigurationData, BlazeResolveConfiguration>
+            uniqueConfigurations) {
+      this.uniqueConfigurations = uniqueConfigurations;
+    }
+
+    void setCompilerSettings(
+        ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings) {
+      this.compilerSettings = compilerSettings;
+    }
+
+    void setResolveDiff(@Nullable BlazeConfigurationResolverDiff resolverDiff) {
+      this.resolverDiff = resolverDiff;
+    }
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationToolchainResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationToolchainResolver.java
new file mode 100644
index 0000000..9ba2448
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationToolchainResolver.java
@@ -0,0 +1,323 @@
+/*
+ * 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.cpp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+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.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Converts {@link CToolchainIdeInfo} to interfaces used by {@link
+ * com.jetbrains.cidr.lang.workspace.OCResolveConfiguration}
+ */
+final class BlazeConfigurationToolchainResolver {
+  private static final Logger logger =
+      Logger.getInstance(BlazeConfigurationToolchainResolver.class);
+
+  private BlazeConfigurationToolchainResolver() {}
+
+  /** Returns the toolchain used by each target */
+  static ImmutableMap<TargetKey, CToolchainIdeInfo> buildToolchainLookupMap(
+      BlazeContext context, TargetMap targetMap) {
+    return Scope.push(
+        context,
+        childContext -> {
+          childContext.push(new TimingScope("Build toolchain lookup map"));
+
+          Map<TargetKey, CToolchainIdeInfo> toolchains = Maps.newLinkedHashMap();
+          for (TargetIdeInfo target : targetMap.targets()) {
+            CToolchainIdeInfo cToolchainIdeInfo = target.cToolchainIdeInfo;
+            if (cToolchainIdeInfo != null) {
+              toolchains.put(target.key, cToolchainIdeInfo);
+            }
+          }
+
+          ImmutableMap.Builder<TargetKey, CToolchainIdeInfo> lookupTable = ImmutableMap.builder();
+          for (TargetIdeInfo target : targetMap.targets()) {
+            if (target.kind.languageClass != LanguageClass.C || target.kind == Kind.CC_TOOLCHAIN) {
+              continue;
+            }
+            List<TargetKey> toolchainDeps =
+                target
+                    .dependencies
+                    .stream()
+                    .map(dep -> dep.targetKey)
+                    .filter(toolchains::containsKey)
+                    .collect(Collectors.toList());
+            if (toolchainDeps.size() != 1) {
+              issueToolchainWarning(context, target, toolchainDeps);
+            }
+            if (!toolchainDeps.isEmpty()) {
+              TargetKey toolchainKey = toolchainDeps.get(0);
+              CToolchainIdeInfo toolchainInfo = toolchains.get(toolchainKey);
+              lookupTable.put(target.key, toolchainInfo);
+            } else {
+              CToolchainIdeInfo arbitraryToolchain = Iterables.getFirst(toolchains.values(), null);
+              if (arbitraryToolchain != null) {
+                lookupTable.put(target.key, arbitraryToolchain);
+              }
+            }
+          }
+          return lookupTable.build();
+        });
+  }
+
+  private static void issueToolchainWarning(
+      BlazeContext context, TargetIdeInfo target, List<TargetKey> toolchainDeps) {
+    String warningMessage =
+        String.format(
+            "cc target %s does not depend on exactly 1 cc toolchain. " + " Found %d toolchains.",
+            target.key, toolchainDeps.size());
+    if (usesAppleCcToolchain(target)) {
+      logger.warn(warningMessage + " (apple_cc_toolchain)");
+    } else {
+      IssueOutput.warn(warningMessage).submit(context);
+    }
+  }
+
+  private static boolean usesAppleCcToolchain(TargetIdeInfo target) {
+    return target
+        .dependencies
+        .stream()
+        .anyMatch(dep -> dep.targetKey.label.toString().startsWith("//tools/osx/crosstool"));
+  }
+
+  /** Returns the compiler settings for each toolchain. */
+  static ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> buildCompilerSettingsMap(
+      BlazeContext context,
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
+      WorkspacePathResolver workspacePathResolver,
+      CompilerInfoCache compilerInfoCache,
+      ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> oldCompilerSettings) {
+    return Scope.push(
+        context,
+        childContext -> {
+          childContext.push(new TimingScope("Build compiler settings map"));
+          return doBuildCompilerSettingsMap(
+              context,
+              project,
+              workspaceRoot,
+              toolchainLookupMap,
+              workspacePathResolver,
+              compilerInfoCache,
+              oldCompilerSettings);
+        });
+  }
+
+  private static ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> doBuildCompilerSettingsMap(
+      BlazeContext context,
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
+      WorkspacePathResolver workspacePathResolver,
+      CompilerInfoCache compilerInfoCache,
+      ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> oldCompilerSettings) {
+    Set<CToolchainIdeInfo> toolchains =
+        toolchainLookupMap.values().stream().distinct().collect(Collectors.toSet());
+    List<ListenableFuture<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>>>
+        compilerSettingsFutures = new ArrayList<>();
+    for (CToolchainIdeInfo toolchain : toolchains) {
+      compilerSettingsFutures.add(
+          submit(
+              () -> {
+                File cppExecutable = resolveCompilerExecutable(toolchain, workspacePathResolver);
+                if (cppExecutable == null) {
+                  logger.warn(
+                      String.format(
+                          "Unable to find compiler executable: %s for toolchain %s",
+                          toolchain.cppExecutable.toString(), toolchain));
+                  return null;
+                }
+                String compilerVersion =
+                    CompilerVersionChecker.getInstance()
+                        .checkCompilerVersion(workspaceRoot, cppExecutable);
+                if (compilerVersion == null) {
+                  logger.warn(
+                      String.format(
+                          "Unable to determine version of compiler: %s for toolchain %s",
+                          cppExecutable, toolchain));
+                  return null;
+                }
+                BlazeCompilerSettings oldSettings = oldCompilerSettings.get(toolchain);
+                if (oldSettings != null
+                    && oldSettings.getCompilerVersion().equals(compilerVersion)) {
+                  return new SimpleImmutableEntry<>(toolchain, oldSettings);
+                }
+                BlazeCompilerSettings settings =
+                    createBlazeCompilerSettings(
+                        project, toolchain, cppExecutable, compilerVersion, compilerInfoCache);
+                if (settings == null) {
+                  return null;
+                }
+                return new SimpleImmutableEntry<>(toolchain, settings);
+              }));
+    }
+    ImmutableMap.Builder<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettingsMap =
+        ImmutableMap.builder();
+    try {
+      List<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>> createdSettings =
+          Futures.allAsList(compilerSettingsFutures).get();
+      for (Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings> createdSetting : createdSettings) {
+        if (createdSetting != null) {
+          compilerSettingsMap.put(createdSetting);
+        }
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      context.setCancelled();
+    } catch (ExecutionException e) {
+      IssueOutput.error("Could not build C compiler settings map: " + e).submit(context);
+      logger.error("Could not build C compiler settings map", e);
+    }
+    return compilerSettingsMap.build();
+  }
+
+  @Nullable
+  private static BlazeCompilerSettings createBlazeCompilerSettings(
+      Project project,
+      CToolchainIdeInfo toolchainIdeInfo,
+      File cppExecutable,
+      String compilerVersion,
+      CompilerInfoCache compilerInfoCache) {
+    File compilerWrapper = createCompilerExecutableWrapper(cppExecutable);
+    if (compilerWrapper == null) {
+      return null;
+    }
+    ImmutableList.Builder<String> cFlagsBuilder = ImmutableList.builder();
+    cFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.cCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+
+    ImmutableList.Builder<String> cppFlagsBuilder = ImmutableList.builder();
+    cppFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.cppCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+    return new BlazeCompilerSettings(
+        project,
+        compilerWrapper,
+        compilerWrapper,
+        cFlagsBuilder.build(),
+        cppFlagsBuilder.build(),
+        compilerVersion,
+        compilerInfoCache);
+  }
+
+  @Nullable
+  private static File resolveCompilerExecutable(
+      CToolchainIdeInfo toolchainIdeInfo, WorkspacePathResolver workspacePathResolver) {
+    File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
+    if (cppExecutable != null && !cppExecutable.isAbsolute()) {
+      cppExecutable = workspacePathResolver.resolveToFile(cppExecutable.getPath());
+    }
+    return cppExecutable;
+  }
+
+  /**
+   * Create a wrapper script that transforms the CLion compiler invocation into a safe invocation of
+   * the compiler script that blaze uses.
+   *
+   * <p>CLion passes arguments to the compiler in an arguments file. The c toolchain compiler
+   * wrapper script doesn't handle arguments files, so we need to move the compiler arguments from
+   * the file to the command line.
+   *
+   * @param blazeCompilerExecutableFile blaze compiler wrapper
+   * @return The wrapper script that CLion can call.
+   */
+  @Nullable
+  private static File createCompilerExecutableWrapper(File blazeCompilerExecutableFile) {
+    try {
+      File blazeCompilerWrapper =
+          FileUtil.createTempFile("blaze_compiler", ".sh", true /* deleteOnExit */);
+      if (!blazeCompilerWrapper.setExecutable(true)) {
+        logger.warn("Unable to make compiler wrapper script executable: " + blazeCompilerWrapper);
+        return null;
+      }
+      ImmutableList<String> compilerWrapperScriptLines =
+          ImmutableList.of(
+              "#!/bin/bash",
+              "",
+              "# The c toolchain compiler wrapper script doesn't handle arguments files, so we",
+              "# need to move the compiler arguments from the file to the command line.",
+              "",
+              "if [ $# -ne 2 ]; then",
+              "  echo \"Usage: $0 @arg-file compile-file\"",
+              "  exit 2;",
+              "fi",
+              "",
+              "if [[ $1 != @* ]]; then",
+              "  echo \"Usage: $0 @arg-file compile-file\"",
+              "  exit 3;",
+              "fi",
+              "",
+              " # Remove the @ before the arguments file path",
+              "ARG_FILE=${1#@}",
+              "# The actual compiler wrapper script we get from blaze",
+              "EXE=" + blazeCompilerExecutableFile.getPath(),
+              "# Read in the arguments file so we can pass the arguments on the command line.",
+              "ARGS=`cat $ARG_FILE`",
+              "$EXE $ARGS $2");
+
+      try (PrintWriter pw = new PrintWriter(blazeCompilerWrapper, UTF_8.name())) {
+        compilerWrapperScriptLines.forEach(pw::println);
+      }
+      return blazeCompilerWrapper;
+    } catch (IOException e) {
+      logger.warn(
+          "Unable to write compiler wrapper script executable: " + blazeCompilerExecutableFile, e);
+      return null;
+    }
+  }
+
+  private static <T> ListenableFuture<T> submit(Callable<T> callable) {
+    return BlazeExecutor.getInstance().submit(callable);
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCppAutoImportHelper.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCppAutoImportHelper.java
index 4a63ee9..562615c 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCppAutoImportHelper.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCppAutoImportHelper.java
@@ -18,64 +18,78 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.PsiFileSystemItem;
 import com.intellij.util.Processor;
 import com.jetbrains.cidr.lang.autoImport.OCDefaultAutoImportHelper;
 import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeadersSearchRoot;
 import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
+import java.util.List;
 import javax.annotation.Nullable;
 
 /**
- * CLion's auto-import suggestions result in include paths relative to the current file (CPP-7593).
- * Instead, we want paths relative to the header search root (e.g. the relevant blaze/bazel package
- * path). Presumably this will be fixed in a future CLwB release, but in the meantime, fix it
- * ourselves.
+ * CLion's auto-import suggestions result in include paths relative to the current file (CPP-7593,
+ * CPP-6369). Instead, we want paths relative to the header search root (e.g. the relevant
+ * blaze/bazel package path). Presumably this will be fixed in a future CLion release, but in the
+ * meantime, fix it ourselves.
  */
 public class BlazeCppAutoImportHelper extends OCDefaultAutoImportHelper {
 
   @Override
   public boolean supports(OCResolveRootAndConfiguration rootAndConfiguration) {
-    return rootAndConfiguration.getConfiguration()
-        instanceof com.google.idea.blaze.cpp.BlazeResolveConfiguration;
+    return rootAndConfiguration.getConfiguration() instanceof BlazeResolveConfiguration;
   }
 
   /**
-   * Search in project header roots only. All other cases are covered by CLion's default
+   * Search in the configuration's header roots only. All other cases are covered by CLion's default
    * implementation.
    */
   @Override
   public boolean processPathSpecificationToInclude(
       Project project,
       @Nullable VirtualFile targetFile,
-      final VirtualFile fileToImport,
+      VirtualFile fileToImport,
       OCResolveRootAndConfiguration rootAndConfiguration,
       Processor<ImportSpecification> processor) {
-    String name = fileToImport.getName();
-    String path = fileToImport.getPath();
-
-    VirtualFile targetFileParent = targetFile != null ? targetFile.getParent() : null;
-
-    if (targetFileParent != null && targetFileParent.equals(fileToImport.getParent())) {
-      if (!processor.process(
-          new ImportSpecification(name, ImportSpecification.Kind.PROJECT_HEADER))) {
-        return false;
-      }
+    // Check system headers of library roots first. Project roots may include the workspace root,
+    // and the system headers might be under the workspace root as well.
+    ImportSpecification specification =
+        findMatchingRoot(
+            fileToImport,
+            rootAndConfiguration.getLibraryHeadersRoots().getRoots(),
+            /* asUserHeader= */ false);
+    if (specification != null && !processor.process(specification)) {
+      return false;
     }
+    specification =
+        findMatchingRoot(
+            fileToImport,
+            rootAndConfiguration.getProjectHeadersRoots().getRoots(),
+            /* asUserHeader= */ true);
+    return specification == null || processor.process(specification);
+  }
 
-    for (PsiFileSystemItem root : rootAndConfiguration.getProjectHeadersRoots().getRoots()) {
+  @Nullable
+  private static ImportSpecification findMatchingRoot(
+      VirtualFile fileToImport, List<HeadersSearchRoot> roots, boolean asUserHeader) {
+    for (HeadersSearchRoot root : roots) {
       if (!(root instanceof IncludedHeadersRoot)) {
         continue;
       }
+      IncludedHeadersRoot includedHeadersRoot = (IncludedHeadersRoot) root;
+      if (asUserHeader != includedHeadersRoot.isUserHeaders()) {
+        continue;
+      }
       VirtualFile rootBase = root.getVirtualFile();
       String relativePath = VfsUtilCore.getRelativePath(fileToImport, rootBase);
       if (relativePath == null) {
         continue;
       }
-      if (!processor.process(
-          new ImportSpecification(relativePath, ImportSpecification.Kind.PROJECT_HEADER))) {
-        return false;
-      }
+      return new ImportSpecification(
+          relativePath,
+          asUserHeader
+              ? ImportSpecification.Kind.USER_HEADER_SEARCH_PATH
+              : ImportSpecification.Kind.SYSTEM_HEADER_SEARCH_PATH);
     }
-    return true;
+    return null;
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
index fb9e190..f49311a 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
@@ -21,16 +21,20 @@
 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.common.experiments.BoolExperiment;
 import com.google.idea.sdkcompat.cidr.OCWorkspaceModificationTrackersCompatUtils;
 import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.symbols.symtable.OCSymbolTablesBuildingActivity;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 
 /** Runs after sync, triggering a rebuild of the symbol tables. */
 public class BlazeCppSymbolRebuildSyncListener extends SyncListener.Adapter {
 
+  private static final BoolExperiment prefillCidrCaches =
+      new BoolExperiment("prefill.cidr.caches.on.startup", true);
+
   @Override
   public void onSyncComplete(
       Project project,
@@ -40,15 +44,25 @@
       BlazeProjectData blazeProjectData,
       SyncMode syncMode,
       SyncResult syncResult) {
-
-    OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
+    OCWorkspace workspace = OCWorkspaceProvider.getWorkspace(project);
     if (!(workspace instanceof BlazeCWorkspace)) {
       return;
     }
-    rebuildSymbolTables(project);
+    if (syncMode == SyncMode.INCREMENTAL || syncMode == SyncMode.PARTIAL) {
+      BlazeConfigurationResolverDiff resolverDiff =
+          ((BlazeCWorkspace) workspace).getConfigurationDiff();
+      if (resolverDiff != null) {
+        incrementallyUpdateSymbolTables(project, resolverDiff);
+        return;
+      }
+    }
+    loadOrRebuildSymbolTables(project);
+    if (syncMode == SyncMode.STARTUP && prefillCidrCaches.getValue()) {
+      CidrCacheFiller.prefillCaches(project);
+    }
   }
 
-  private static void rebuildSymbolTables(Project project) {
+  private static void loadOrRebuildSymbolTables(Project project) {
     Transactions.submitTransactionAndWait(
         () ->
             ApplicationManager.getApplication()
@@ -57,4 +71,24 @@
                         OCWorkspaceModificationTrackersCompatUtils.incrementModificationCounts(
                             project)));
   }
+
+  private static void incrementallyUpdateSymbolTables(
+      Project project, BlazeConfigurationResolverDiff resolverDiff) {
+    Transactions.submitTransactionAndWait(
+        () -> {
+          ApplicationManager.getApplication()
+              .runWriteAction(
+                  () -> {
+                    if (resolverDiff.hasChanges()) {
+                      OCWorkspaceModificationTrackersCompatUtils.partialIncModificationCounts(
+                          project);
+                    }
+                    OCSymbolTablesBuildingActivity.getInstance(project)
+                        .getModificationTracker()
+                        .incModificationCount();
+                  });
+          OCSymbolTablesBuildingActivity.getInstance(project)
+              .buildSymbolsForFiles(resolverDiff.getChangedFiles());
+        });
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java b/cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java
index 254c94a..9ad6514 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java
@@ -35,10 +35,13 @@
   public OCLanguageKind getLanguageByExtension(Project project, String name) {
     if (Blaze.isBlazeProject(project)) {
       String extension = FileUtilRt.getExtension(name);
-      if (extension.equalsIgnoreCase("c")) {
+      if (CFileExtensions.C_FILE_EXTENSIONS.contains(extension)) {
         return OCLanguageKind.C;
       }
-      if (extension.equalsIgnoreCase("cc")) {
+      if (CFileExtensions.CXX_FILE_EXTENSIONS.contains(extension)) {
+        return OCLanguageKind.CPP;
+      }
+      if (CFileExtensions.CXX_ONLY_HEADER_EXTENSIONS.contains(extension)) {
         return OCLanguageKind.CPP;
       }
     }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
index 6581b12..78c4890 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
@@ -15,31 +15,15 @@
  */
 package com.google.idea.blaze.cpp;
 
-import com.google.common.collect.ImmutableCollection;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
-import com.google.idea.blaze.base.ideinfo.CIdeInfo;
-import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
-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.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.Kind;
-import com.google.idea.blaze.base.model.primitives.LanguageClass;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.Scope;
-import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.google.idea.blaze.base.scope.scopes.TimingScope;
-import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.sdkcompat.cidr.OCCompilerMacrosAdapter;
 import com.google.idea.sdkcompat.cidr.OCResolveConfigurationAdapter;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Pair;
-import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCFileTypeHelpers;
 import com.jetbrains.cidr.lang.OCLanguageKind;
@@ -49,206 +33,65 @@
 import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
 import com.jetbrains.cidr.lang.workspace.OCWorkspaceUtil;
 import com.jetbrains.cidr.lang.workspace.compiler.CidrCompilerResult;
-import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
 import com.jetbrains.cidr.lang.workspace.headerRoots.HeaderRoots;
 import com.jetbrains.cidr.lang.workspace.headerRoots.HeadersSearchRoot;
-import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
 import com.jetbrains.cidr.toolchains.CompilerInfoCache;
-import java.io.File;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
-final class BlazeResolveConfiguration extends UserDataHolderBase
-    implements OCResolveConfigurationAdapter {
+/** Blaze implementation of {@link OCResolveConfiguration}. */
+final class BlazeResolveConfiguration extends OCResolveConfigurationAdapter {
 
-  private static final Logger logger = Logger.getInstance(BlazeResolveConfiguration.class);
-  private final ExecutionRootPathResolver executionRootPathResolver;
+  private final Project project;
+  private final ConcurrentMap<Pair<OCLanguageKind, VirtualFile>, HeaderRoots>
+      libraryIncludeRootsCache = new ConcurrentHashMap<>();
   private final WorkspacePathResolver workspacePathResolver;
 
-  /* project, label are protected instead of private just so v145 can access */
-  protected final Project project;
-  protected final TargetKey targetKey;
+  private final BlazeResolveConfigurationData configurationData;
 
-  private final ImmutableList<HeadersSearchRoot> cLibraryIncludeRoots;
-  private final ImmutableList<HeadersSearchRoot> cppLibraryIncludeRoots;
-  private final HeaderRoots projectIncludeRoots;
-  private final ConcurrentMap<Pair<OCLanguageKind, VirtualFile>, HeaderRoots> libraryIncludeRoots =
-      new ConcurrentHashMap<>();
+  private String displayNameIdentifier;
 
-  private final CompilerInfoCache compilerInfoCache;
-  private final BlazeCompilerMacros compilerMacros;
-  private final BlazeCompilerSettings compilerSettings;
-  private final CToolchainIdeInfo toolchainIdeInfo;
-
-  @Nullable
-  public static BlazeResolveConfiguration createConfigurationForTarget(
+  private BlazeResolveConfiguration(
       Project project,
-      ExecutionRootPathResolver executionRootPathResolver,
       WorkspacePathResolver workspacePathResolver,
-      ImmutableMap<File, VirtualFile> headerRoots,
-      TargetIdeInfo target,
-      CToolchainIdeInfo toolchainIdeInfo,
-      BlazeCompilerSettings compilerSettings,
-      CompilerInfoCache compilerInfoCache) {
-    CIdeInfo cIdeInfo = target.cIdeInfo;
-    if (cIdeInfo == null) {
-      return null;
-    }
-
-    ImmutableSet.Builder<ExecutionRootPath> systemIncludesBuilder = ImmutableSet.builder();
-    systemIncludesBuilder.addAll(cIdeInfo.transitiveSystemIncludeDirectories);
-    systemIncludesBuilder.addAll(toolchainIdeInfo.builtInIncludeDirectories);
-    systemIncludesBuilder.addAll(toolchainIdeInfo.unfilteredToolchainSystemIncludes);
-
-    ImmutableSet.Builder<ExecutionRootPath> userIncludesBuilder = ImmutableSet.builder();
-    userIncludesBuilder.addAll(cIdeInfo.transitiveIncludeDirectories);
-    userIncludesBuilder.addAll(cIdeInfo.localIncludeDirectories);
-
-    ImmutableSet.Builder<ExecutionRootPath> userQuoteIncludesBuilder = ImmutableSet.builder();
-    userQuoteIncludesBuilder.addAll(cIdeInfo.transitiveQuoteIncludeDirectories);
-
-    ImmutableList.Builder<String> defines = ImmutableList.builder();
-    defines.addAll(cIdeInfo.transitiveDefines);
-    defines.addAll(cIdeInfo.localDefines);
-
-    ImmutableMap<String, String> features = ImmutableMap.of();
-
-    return new BlazeResolveConfiguration(
-        project,
-        executionRootPathResolver,
-        workspacePathResolver,
-        headerRoots,
-        target.key,
-        systemIncludesBuilder.build(),
-        systemIncludesBuilder.build(),
-        userQuoteIncludesBuilder.build(),
-        userIncludesBuilder.build(),
-        userIncludesBuilder.build(),
-        defines.build(),
-        features,
-        compilerSettings,
-        compilerInfoCache,
-        toolchainIdeInfo);
-  }
-
-  static ImmutableMap<TargetKey, CToolchainIdeInfo> buildToolchainLookupMap(
-      BlazeContext context, TargetMap targetMap) {
-    return Scope.push(
-        context,
-        childContext -> {
-          childContext.push(new TimingScope("Build toolchain lookup map"));
-
-          Map<TargetKey, CToolchainIdeInfo> toolchains = Maps.newLinkedHashMap();
-          for (TargetIdeInfo target : targetMap.targets()) {
-            CToolchainIdeInfo cToolchainIdeInfo = target.cToolchainIdeInfo;
-            if (cToolchainIdeInfo != null) {
-              toolchains.put(target.key, cToolchainIdeInfo);
-            }
-          }
-
-          ImmutableMap.Builder<TargetKey, CToolchainIdeInfo> lookupTable = ImmutableMap.builder();
-          for (TargetIdeInfo target : targetMap.targets()) {
-            if (target.kind.getLanguageClass() != LanguageClass.C
-                || target.kind == Kind.CC_TOOLCHAIN) {
-              continue;
-            }
-            List<TargetKey> toolchainDeps =
-                target
-                    .dependencies
-                    .stream()
-                    .map(dep -> dep.targetKey)
-                    .filter(toolchains::containsKey)
-                    .collect(Collectors.toList());
-            if (toolchainDeps.size() != 1) {
-              issueToolchainWarning(context, target, toolchainDeps);
-            }
-            if (!toolchainDeps.isEmpty()) {
-              TargetKey toolchainKey = toolchainDeps.get(0);
-              CToolchainIdeInfo toolchainInfo = toolchains.get(toolchainKey);
-              lookupTable.put(target.key, toolchainInfo);
-            } else {
-              CToolchainIdeInfo arbitraryToolchain = Iterables.getFirst(toolchains.values(), null);
-              if (arbitraryToolchain != null) {
-                lookupTable.put(target.key, arbitraryToolchain);
-              }
-            }
-          }
-          return lookupTable.build();
-        });
-  }
-
-  private static void issueToolchainWarning(
-      BlazeContext context, TargetIdeInfo target, List<TargetKey> toolchainDeps) {
-    String warningMessage =
-        String.format(
-            "cc target %s does not depend on exactly 1 cc toolchain. " + " Found %d toolchains.",
-            target.key, toolchainDeps.size());
-    if (usesAppleCcToolchain(target)) {
-      logger.warn(warningMessage + " (apple_cc_toolchain)");
-    } else {
-      IssueOutput.warn(warningMessage).submit(context);
-    }
-  }
-
-  private static boolean usesAppleCcToolchain(TargetIdeInfo target) {
-    return target
-        .dependencies
-        .stream()
-        .anyMatch(dep -> dep.targetKey.label.toString().startsWith("//tools/osx/crosstool"));
-  }
-
-  public BlazeResolveConfiguration(
-      Project project,
-      ExecutionRootPathResolver executionRootPathResolver,
-      WorkspacePathResolver workspacePathResolver,
-      ImmutableMap<File, VirtualFile> headerRoots,
-      TargetKey targetKey,
-      ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppIncludeDirs,
-      ImmutableCollection<String> defines,
-      ImmutableMap<String, String> features,
-      BlazeCompilerSettings compilerSettings,
-      CompilerInfoCache compilerInfoCache,
-      CToolchainIdeInfo toolchainIdeInfo) {
-    this.executionRootPathResolver = executionRootPathResolver;
-    this.workspacePathResolver = workspacePathResolver;
+      BlazeResolveConfigurationData configurationData) {
     this.project = project;
-    this.targetKey = targetKey;
-    this.toolchainIdeInfo = toolchainIdeInfo;
+    this.workspacePathResolver = workspacePathResolver;
+    this.configurationData = configurationData;
+  }
 
-    ImmutableList.Builder<HeadersSearchRoot> cIncludeRootsBuilder = ImmutableList.builder();
-    collectHeaderRoots(headerRoots, cIncludeRootsBuilder, cIncludeDirs, true /* isUserHeader */);
-    collectHeaderRoots(
-        headerRoots, cIncludeRootsBuilder, cSystemIncludeDirs, false /* isUserHeader */);
-    this.cLibraryIncludeRoots = cIncludeRootsBuilder.build();
+  static BlazeResolveConfiguration createForTargets(
+      Project project,
+      WorkspacePathResolver workspacePathResolver,
+      BlazeResolveConfigurationData configurationData,
+      Collection<TargetKey> targets) {
+    BlazeResolveConfiguration result =
+        new BlazeResolveConfiguration(project, workspacePathResolver, configurationData);
+    result.representMultipleTargets(targets);
+    return result;
+  }
 
-    ImmutableList.Builder<HeadersSearchRoot> cppIncludeRootsBuilder = ImmutableList.builder();
-    collectHeaderRoots(
-        headerRoots, cppIncludeRootsBuilder, cppIncludeDirs, true /* isUserHeader */);
-    collectHeaderRoots(
-        headerRoots, cppIncludeRootsBuilder, cppSystemIncludeDirs, false /* isUserHeader */);
-    this.cppLibraryIncludeRoots = cppIncludeRootsBuilder.build();
-
-    ImmutableList.Builder<HeadersSearchRoot> quoteIncludeRootsBuilder = ImmutableList.builder();
-    collectHeaderRoots(
-        headerRoots, quoteIncludeRootsBuilder, quoteIncludeDirs, true /* isUserHeader */);
-    this.projectIncludeRoots = new HeaderRoots(quoteIncludeRootsBuilder.build());
-
-    this.compilerSettings = compilerSettings;
-    this.compilerInfoCache = compilerInfoCache;
-    this.compilerMacros =
-        new BlazeCompilerMacros(project, compilerInfoCache, compilerSettings, defines, features);
+  /**
+   * Indicate that this single configuration represents N other targets. NOTE: this changes the
+   * identifier used by {@link #compareTo}, so any data structures using compareTo must be
+   * invalidated when this changes.
+   */
+  void representMultipleTargets(Collection<TargetKey> targets) {
+    TargetKey minTargetKey = targets.stream().min(TargetKey::compareTo).orElse(null);
+    Preconditions.checkNotNull(minTargetKey);
+    String minTarget = minTargetKey.toString();
+    if (targets.size() == 1) {
+      displayNameIdentifier = minTarget;
+    } else {
+      displayNameIdentifier =
+          String.format("%s and %d other target(s)", minTarget, targets.size() - 1);
+    }
   }
 
   @Override
@@ -262,7 +105,36 @@
 
   @Override
   public String getDisplayName(boolean shorten) {
-    return targetKey.toString();
+    return displayNameIdentifier;
+  }
+
+  @Override
+  public int compareTo(OCResolveConfiguration other) {
+    // This is a bit of a weak comparison -- it just uses the display name (ignoring case)
+    // and doesn't compare the actual fields of the configuration.
+    // It should only be used for simple things like sorting for the UI.
+    return OCWorkspaceUtil.compareConfigurations(this, other);
+  }
+
+  @Override
+  public int hashCode() {
+    // There should only be one configuration per target, and the display name is derived
+    // from a target
+    return Objects.hashCode(displayNameIdentifier);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!(obj instanceof BlazeResolveConfiguration)) {
+      return false;
+    }
+
+    BlazeResolveConfiguration that = (BlazeResolveConfiguration) obj;
+    return compareTo(that) == 0;
   }
 
   @Nullable
@@ -305,7 +177,7 @@
 
   @Override
   public HeaderRoots getProjectHeadersRoots() {
-    return projectIncludeRoots;
+    return configurationData.projectIncludeRoots;
   }
 
   @Override
@@ -316,20 +188,20 @@
       languageKind = getLanguageKind(sourceFile);
     }
     Pair<OCLanguageKind, VirtualFile> cacheKey = Pair.create(languageKind, sourceFile);
-    return libraryIncludeRoots.computeIfAbsent(
+    return libraryIncludeRootsCache.computeIfAbsent(
         cacheKey,
         key -> {
           OCLanguageKind lang = key.first;
           VirtualFile source = key.second;
           ImmutableSet.Builder<HeadersSearchRoot> roots = ImmutableSet.builder();
           if (lang == OCLanguageKind.C) {
-            roots.addAll(cLibraryIncludeRoots);
+            roots.addAll(configurationData.cLibraryIncludeRoots);
           } else {
-            roots.addAll(cppLibraryIncludeRoots);
+            roots.addAll(configurationData.cppLibraryIncludeRoots);
           }
 
           CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoCacheHolder =
-              compilerInfoCache.getCompilerInfoCache(project, compilerSettings, lang, source);
+              configurationData.compilerSettings.getCompilerInfo(lang, source);
           CompilerInfoCache.Entry compilerInfo = compilerInfoCacheHolder.getResult();
           if (compilerInfo != null) {
             roots.addAll(compilerInfo.headerSearchPaths);
@@ -338,91 +210,55 @@
         });
   }
 
-  private void collectHeaderRoots(
-      ImmutableMap<File, VirtualFile> virtualFileCache,
-      ImmutableList.Builder<HeadersSearchRoot> roots,
-      ImmutableCollection<ExecutionRootPath> paths,
-      boolean isUserHeader) {
-    for (ExecutionRootPath executionRootPath : paths) {
-      ImmutableList<File> possibleDirectories =
-          executionRootPathResolver.resolveToIncludeDirectories(executionRootPath);
-      for (File f : possibleDirectories) {
-        VirtualFile vf = virtualFileCache.get(f);
-        if (vf != null) {
-          roots.add(new IncludedHeadersRoot(project, vf, false /* recursive */, isUserHeader));
-        }
-      }
-    }
-  }
-
   @Override
-  public OCCompilerMacros getCompilerMacros() {
-    return compilerMacros;
+  public OCCompilerMacrosAdapter getCompilerMacros() {
+    return configurationData.compilerMacros;
   }
 
   @Override
   public OCCompilerSettings getCompilerSettings() {
-    return compilerSettings;
+    return configurationData.compilerSettings;
   }
 
   @Override
   public Object getIndexingCluster() {
-    return toolchainIdeInfo;
+    return configurationData.toolchainIdeInfo;
   }
 
-  @Override
-  public int compareTo(OCResolveConfiguration other) {
-    return OCWorkspaceUtil.compareConfigurations(this, other);
-  }
-
-  @Override
-  public int hashCode() {
-    // There should only be one configuration per target.
-    return Objects.hash(targetKey);
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (this == obj) {
-      return true;
-    }
-
-    if (!(obj instanceof BlazeResolveConfiguration)) {
-      return false;
-    }
-
-    BlazeResolveConfiguration that = (BlazeResolveConfiguration) obj;
-    return compareTo(that) == 0;
-  }
-
-  /* This function is part of the v162/v163 plugin APIs. */
+  /* #api163 */
   @Nullable
   @Override
   public VirtualFile getPrecompiledHeader() {
     return null;
   }
 
-  /* This function is part of the v162/v163 plugin APIs. */
+  /* #api163 */
   @Override
   public OCLanguageKind getPrecompiledLanguageKind() {
     return getMaximumLanguageKind();
   }
 
-  /* This function is part of the v171 plugin API. */
+  /* #api171 */
   @Override
   public Set<VirtualFile> getPrecompiledHeaders() {
     return ImmutableSet.of();
   }
 
-  /* This function is part of the v171 plugin API. */
+  /* #api171 */
   @Override
   public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile) {
     return ImmutableList.of();
   }
 
-  /* This function is part of the v171 plugin API. */
+  /* #api171 */
   @Override
   public Collection<VirtualFile> getSources() {
     return ImmutableList.of();
   }
+
+  /* #api172 */
+  @Override
+  public String getPreprocessorDefines(OCLanguageKind kind, VirtualFile virtualFile) {
+    return configurationData.compilerMacros.getAllDefines(kind, virtualFile);
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationData.java b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationData.java
new file mode 100644
index 0000000..df63c2d
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationData.java
@@ -0,0 +1,190 @@
+/*
+ * 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.cpp;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeaderRoots;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeadersSearchRoot;
+import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
+import java.io.File;
+import java.util.Objects;
+
+/** Data used by a {@link BlazeResolveConfiguration}. */
+final class BlazeResolveConfigurationData {
+
+  final BlazeCompilerSettings compilerSettings;
+
+  final ImmutableList<HeadersSearchRoot> cLibraryIncludeRoots;
+  final ImmutableList<HeadersSearchRoot> cppLibraryIncludeRoots;
+  final HeaderRoots projectIncludeRoots;
+  final BlazeCompilerMacros compilerMacros;
+  final CToolchainIdeInfo toolchainIdeInfo;
+
+  static BlazeResolveConfigurationData create(
+      Project project,
+      ExecutionRootPathResolver executionRootPathResolver,
+      ImmutableMap<File, VirtualFile> headerRoots,
+      CIdeInfo cIdeInfo,
+      CToolchainIdeInfo toolchainIdeInfo,
+      BlazeCompilerSettings compilerSettings,
+      CompilerInfoCache compilerInfoCache) {
+    ImmutableSet.Builder<ExecutionRootPath> systemIncludesBuilder = ImmutableSet.builder();
+    systemIncludesBuilder.addAll(cIdeInfo.transitiveSystemIncludeDirectories);
+    systemIncludesBuilder.addAll(toolchainIdeInfo.builtInIncludeDirectories);
+    systemIncludesBuilder.addAll(toolchainIdeInfo.unfilteredToolchainSystemIncludes);
+
+    ImmutableSet.Builder<ExecutionRootPath> userIncludesBuilder = ImmutableSet.builder();
+    userIncludesBuilder.addAll(cIdeInfo.transitiveIncludeDirectories);
+    userIncludesBuilder.addAll(cIdeInfo.localIncludeDirectories);
+
+    ImmutableSet.Builder<ExecutionRootPath> userQuoteIncludesBuilder = ImmutableSet.builder();
+    userQuoteIncludesBuilder.addAll(cIdeInfo.transitiveQuoteIncludeDirectories);
+
+    ImmutableList.Builder<String> defines = ImmutableList.builder();
+    defines.addAll(cIdeInfo.transitiveDefines);
+    defines.addAll(cIdeInfo.localDefines);
+
+    ImmutableMap<String, String> features = ImmutableMap.of();
+
+    return new BlazeResolveConfigurationData(
+        project,
+        executionRootPathResolver,
+        headerRoots,
+        systemIncludesBuilder.build(),
+        systemIncludesBuilder.build(),
+        userQuoteIncludesBuilder.build(),
+        userIncludesBuilder.build(),
+        userIncludesBuilder.build(),
+        defines.build(),
+        features,
+        compilerSettings,
+        compilerInfoCache,
+        toolchainIdeInfo);
+  }
+
+  private BlazeResolveConfigurationData(
+      Project project,
+      ExecutionRootPathResolver executionRootPathResolver,
+      ImmutableMap<File, VirtualFile> headerRoots,
+      ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
+      ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
+      ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
+      ImmutableCollection<ExecutionRootPath> cIncludeDirs,
+      ImmutableCollection<ExecutionRootPath> cppIncludeDirs,
+      ImmutableCollection<String> defines,
+      ImmutableMap<String, String> features,
+      BlazeCompilerSettings compilerSettings,
+      CompilerInfoCache compilerInfoCache,
+      CToolchainIdeInfo toolchainIdeInfo) {
+    this.toolchainIdeInfo = toolchainIdeInfo;
+
+    HeaderRootsCollector headerRootsCollector =
+        new HeaderRootsCollector(project, executionRootPathResolver, headerRoots);
+    ImmutableList.Builder<HeadersSearchRoot> cIncludeRootsBuilder = ImmutableList.builder();
+    headerRootsCollector.collectHeaderRoots(
+        cIncludeRootsBuilder, cIncludeDirs, true /* isUserHeader */);
+    headerRootsCollector.collectHeaderRoots(
+        cIncludeRootsBuilder, cSystemIncludeDirs, false /* isUserHeader */);
+    this.cLibraryIncludeRoots = cIncludeRootsBuilder.build();
+
+    ImmutableList.Builder<HeadersSearchRoot> cppIncludeRootsBuilder = ImmutableList.builder();
+    headerRootsCollector.collectHeaderRoots(
+        cppIncludeRootsBuilder, cppIncludeDirs, true /* isUserHeader */);
+    headerRootsCollector.collectHeaderRoots(
+        cppIncludeRootsBuilder, cppSystemIncludeDirs, false /* isUserHeader */);
+    this.cppLibraryIncludeRoots = cppIncludeRootsBuilder.build();
+
+    ImmutableList.Builder<HeadersSearchRoot> quoteIncludeRootsBuilder = ImmutableList.builder();
+    headerRootsCollector.collectHeaderRoots(
+        quoteIncludeRootsBuilder, quoteIncludeDirs, true /* isUserHeader */);
+    this.projectIncludeRoots = new HeaderRoots(quoteIncludeRootsBuilder.build());
+
+    this.compilerSettings = compilerSettings;
+    this.compilerMacros =
+        new BlazeCompilerMacros(project, compilerInfoCache, compilerSettings, defines, features);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof BlazeResolveConfigurationData)) {
+      return false;
+    }
+    BlazeResolveConfigurationData otherData = (BlazeResolveConfigurationData) other;
+    return this.cLibraryIncludeRoots.equals(otherData.cLibraryIncludeRoots)
+        && this.cppLibraryIncludeRoots.equals(otherData.cppLibraryIncludeRoots)
+        && this.projectIncludeRoots.equals(otherData.projectIncludeRoots)
+        && this.compilerMacros.equals(otherData.compilerMacros)
+        && this.toolchainIdeInfo.equals(otherData.toolchainIdeInfo)
+        && this.compilerSettings
+            .getCompilerVersion()
+            .equals(otherData.compilerSettings.getCompilerVersion());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        cLibraryIncludeRoots,
+        cppLibraryIncludeRoots,
+        projectIncludeRoots,
+        compilerMacros,
+        toolchainIdeInfo,
+        compilerSettings.getCompilerVersion());
+  }
+
+  private static class HeaderRootsCollector {
+    private final Project project;
+    private final ExecutionRootPathResolver executionRootPathResolver;
+    private final ImmutableMap<File, VirtualFile> virtualFileCache;
+
+    HeaderRootsCollector(
+        Project project,
+        ExecutionRootPathResolver executionRootPathResolver,
+        ImmutableMap<File, VirtualFile> virtualFileCache) {
+      this.project = project;
+      this.executionRootPathResolver = executionRootPathResolver;
+      this.virtualFileCache = virtualFileCache;
+    }
+
+    void collectHeaderRoots(
+        ImmutableList.Builder<HeadersSearchRoot> roots,
+        ImmutableCollection<ExecutionRootPath> paths,
+        boolean isUserHeader) {
+      for (ExecutionRootPath executionRootPath : paths) {
+        ImmutableList<File> possibleDirectories =
+            executionRootPathResolver.resolveToIncludeDirectories(executionRootPath);
+        for (File f : possibleDirectories) {
+          VirtualFile vf = virtualFileCache.get(f);
+          if (vf != null) {
+            roots.add(new IncludedHeadersRoot(project, vf, false /* recursive */, isUserHeader));
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/CFileExtensions.java b/cpp/src/com/google/idea/blaze/cpp/CFileExtensions.java
new file mode 100644
index 0000000..7b9287c
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/CFileExtensions.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.cpp;
+
+import com.google.common.collect.ImmutableSet;
+
+/** C/C++ file extensions categorized. */
+final class CFileExtensions {
+
+  // See https://bazel.build/versions/master/docs/be/c-cpp.html#cc_binary.srcs
+  static final ImmutableSet<String> C_FILE_EXTENSIONS = ImmutableSet.of("c");
+  static final ImmutableSet<String> CXX_FILE_EXTENSIONS =
+      ImmutableSet.of("cc", "cpp", "cxx", "c++", "C");
+
+  static final ImmutableSet<String> CXX_ONLY_HEADER_EXTENSIONS =
+      ImmutableSet.of("hh", "hpp", "hxx");
+  private static final ImmutableSet<String> SHARED_HEADER_EXTENSIONS = ImmutableSet.of("h", "inc");
+
+  static final ImmutableSet<String> SOURCE_EXTENSIONS =
+      ImmutableSet.<String>builder().addAll(C_FILE_EXTENSIONS).addAll(CXX_FILE_EXTENSIONS).build();
+  static final ImmutableSet<String> HEADER_EXTENSIONS =
+      ImmutableSet.<String>builder()
+          .addAll(SHARED_HEADER_EXTENSIONS)
+          .addAll(CXX_ONLY_HEADER_EXTENSIONS)
+          .build();
+
+  private CFileExtensions() {}
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java b/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
index 4e63e71..911c714 100644
--- a/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
+++ b/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
@@ -16,25 +16,66 @@
 package com.google.idea.blaze.cpp;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 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.google.idea.common.experiments.BoolExperiment;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
 import java.io.File;
-import java.util.Collection;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /** Causes C files to become prefetched. */
 public class CPrefetchFileSource implements PrefetchFileSource {
+
+  private static final BoolExperiment prefetchAllCppSources =
+      new BoolExperiment("prefetch.all.cpp.sources", true);
+
   @Override
   public void addFilesToPrefetch(
       Project project,
       ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
       BlazeProjectData blazeProjectData,
-      Collection<File> files) {}
+      Set<File> files) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C)
+        || !prefetchAllCppSources.getValue()) {
+      return;
+    }
+    // Prefetch all non-project CPP header files encountered during sync
+    Predicate<ArtifactLocation> shouldPrefetch =
+        location -> {
+          if (!location.isSource || location.isExternal) {
+            return false;
+          }
+          WorkspacePath path = WorkspacePath.createIfValid(location.relativePath);
+          if (path == null || importRoots.containsWorkspacePath(path)) {
+            return false;
+          }
+          String extension = FileUtil.getExtension(path.relativePath());
+          return CFileExtensions.HEADER_EXTENSIONS.contains(extension);
+        };
+    ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
+    for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
+      if (target.cIdeInfo == null) {
+        continue;
+      }
+      target.sources.stream().filter(shouldPrefetch).map(decoder::decode).forEach(files::add);
+    }
+  }
 
   @Override
-  public Set<String> prefetchSrcFileExtensions() {
-    return ImmutableSet.of("c", "cc", "cpp", "h", "hh", "hpp");
+  public Set<String> prefetchFileExtensions() {
+    return ImmutableSet.<String>builder()
+        .addAll(CFileExtensions.SOURCE_EXTENSIONS)
+        .addAll(CFileExtensions.HEADER_EXTENSIONS)
+        .build();
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/CidrCacheFiller.java b/cpp/src/com/google/idea/blaze/cpp/CidrCacheFiller.java
new file mode 100644
index 0000000..982b389
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/CidrCacheFiller.java
@@ -0,0 +1,186 @@
+/*
+ * 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.cpp;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+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.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewTargetImportFilter;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
+import com.intellij.openapi.project.DumbModeTask;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.EmptyRunnable;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.preprocessor.OCImportGraph;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContextUtil;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Fills some cidr caches after loading/rebuilding symbol tables.
+ *
+ * <p>Namely, some of the code for determining the {@link OCResolveConfiguration} of files depend on
+ * the {@link OCImportGraph}. If the cidr code becomes less dependent on {@link OCImportGraph} we
+ * can remove this step. See upstream bug for potential freezes due to depenence on OCImportGraph
+ * (https://youtrack.jetbrains.com/issue/CPP-10557).
+ */
+class CidrCacheFiller extends DumbModeTask {
+  private static final Logger logger = Logger.getInstance(CidrCacheFiller.class);
+
+  private final Project project;
+  private final ListeningExecutorService executor;
+
+  private CidrCacheFiller(Project project, ListeningExecutorService executor) {
+    this.project = project;
+    this.executor = executor;
+  }
+
+  static void prefillCaches(Project project) {
+    DumbService.getInstance(project)
+        .queueTask(new CidrCacheFiller(project, BlazeExecutor.getInstance().getExecutor()));
+  }
+
+  @Override
+  public void performInDumbMode(ProgressIndicator indicator) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (blazeProjectData == null || projectViewSet == null) {
+      return;
+    }
+    OCWorkspace ocWorkspace = OCWorkspaceManager.getWorkspace(project);
+    if (!(ocWorkspace instanceof BlazeCWorkspace)) {
+      return;
+    }
+    Stopwatch timer = Stopwatch.createStarted();
+    indicator.setText("Pre-filling C++ caches");
+    indicator.setIndeterminate(false);
+    indicator.setFraction(0);
+    ProjectViewTargetImportFilter filter =
+        new ProjectViewTargetImportFilter(
+            project, WorkspaceRoot.fromProject(project), projectViewSet);
+    ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
+    Set<File> cSourceFiles = new HashSet<>();
+    for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
+      if (target.cIdeInfo == null || !filter.isSourceTarget(target)) {
+        continue;
+      }
+      for (ArtifactLocation sourceLocation : target.sources) {
+        cSourceFiles.add(decoder.decode(sourceLocation));
+      }
+    }
+    int numSources = cSourceFiles.size();
+    AtomicInteger numDone = new AtomicInteger();
+    indicator.setText(String.format("Pre-filling C++ caches (%s files)", numSources));
+    VirtualFileSystemProvider vfsProvider = VirtualFileSystemProvider.getInstance();
+    List<ListenableFuture<?>> futures = new ArrayList<>();
+    for (File sourceFile : cSourceFiles) {
+      futures.add(
+          executor.submit(
+              () -> {
+                VirtualFile sourceRoot = vfsProvider.getSystem().findFileByIoFile(sourceFile);
+                if (sourceRoot == null
+                    || OCInclusionContextUtil.isNeedToFindRoot(sourceRoot, project)) {
+                  updateIndicator(numDone, indicator, numSources);
+                  return;
+                }
+                List<? extends OCResolveConfiguration> configurations =
+                    ocWorkspace.getConfigurationsForFile(sourceRoot);
+                if (configurations.isEmpty()) {
+                  updateIndicator(numDone, indicator, numSources);
+                  return;
+                }
+                OCResolveConfiguration someConfiguration = configurations.get(0);
+                runInReadActionWithWriteActionPriorityWithRetries(
+                    () ->
+                        OCImportGraph.getAllRootHeaders(someConfiguration, sourceRoot, indicator));
+                updateIndicator(numDone, indicator, numSources);
+              }));
+    }
+    try {
+      Futures.allAsList(futures).get();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      return;
+    } catch (ProcessCanceledException e) {
+      return;
+    } catch (ExecutionException e) {
+      logger.warn(e);
+    }
+    logger.info(
+        String.format(
+            "Pre-fill C++ caches took: %s ms for %s files",
+            timer.elapsed(TimeUnit.MILLISECONDS), numSources));
+  }
+
+  private static void updateIndicator(
+      AtomicInteger numDone, ProgressIndicator indicator, int total) {
+    int currentDone = numDone.incrementAndGet();
+    indicator.setFraction((double) currentDone / total);
+  }
+
+  // Use a friendlier read action which will allow write actions to jump in while still
+  // running multiple read actions in parallel.
+  // This is essentially DebuggerUtilImpl#runInReadActionWithWriteActionPriorityWithRetries
+  // but avoid depending on an Impl class for this simple loop.
+  private static void runInReadActionWithWriteActionPriorityWithRetries(Runnable runnable) {
+    while (true) {
+      if (runInReadActionWithWriteActionPriority(runnable)) {
+        return;
+      }
+    }
+  }
+
+  // In #api_171: we can just use ProgressManager#runInReadActionWithWriteActionPriority
+  // instead of these two following methods.
+  private static boolean runInReadActionWithWriteActionPriority(Runnable runnable) {
+    boolean success = ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(runnable);
+    if (!success) {
+      yieldToPendingWriteActions();
+    }
+    return success;
+  }
+
+  private static void yieldToPendingWriteActions() {
+    ApplicationManager.getApplication().invokeAndWait(EmptyRunnable.INSTANCE, ModalityState.any());
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/CompilerVersionChecker.java b/cpp/src/com/google/idea/blaze/cpp/CompilerVersionChecker.java
new file mode 100644
index 0000000..5599ff3
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/CompilerVersionChecker.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.blaze.cpp;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.components.ServiceManager;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Runs a compiler to check its version. */
+public interface CompilerVersionChecker {
+
+  static CompilerVersionChecker getInstance() {
+    return ServiceManager.getService(CompilerVersionChecker.class);
+  }
+
+  /** Returns the compiler's version string, or null on failure */
+  @Nullable
+  String checkCompilerVersion(WorkspaceRoot workspaceRoot, File cppExecutable);
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/CompilerVersionCheckerImpl.java b/cpp/src/com/google/idea/blaze/cpp/CompilerVersionCheckerImpl.java
new file mode 100644
index 0000000..3d7e94c
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/CompilerVersionCheckerImpl.java
@@ -0,0 +1,48 @@
+/*
+ * 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.cpp;
+
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+
+/** Runs a compiler to check its version. */
+public class CompilerVersionCheckerImpl implements CompilerVersionChecker {
+
+  private static final Logger logger = Logger.getInstance(CompilerVersionCheckerImpl.class);
+
+  @Override
+  public String checkCompilerVersion(WorkspaceRoot workspaceRoot, File cppExecutable) {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    ByteArrayOutputStream errStream = new ByteArrayOutputStream();
+    int result =
+        ExternalTask.builder(workspaceRoot)
+            .args(cppExecutable.toString())
+            // NOTE: this won't work with MSVC if we ever support that (check CToolchainIdeInfo?)
+            .args("--version")
+            .stdout(outputStream)
+            .stderr(errStream)
+            .build()
+            .run();
+    if (result != 0) {
+      logger.warn(String.format("Error getting compiler version: \"%s\"", errStream));
+      return null;
+    }
+    return outputStream.toString();
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/MockCompilerVersionChecker.java b/cpp/src/com/google/idea/blaze/cpp/MockCompilerVersionChecker.java
new file mode 100644
index 0000000..e00fcb2
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/MockCompilerVersionChecker.java
@@ -0,0 +1,40 @@
+/*
+ * 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.cpp;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** {@link CompilerVersionChecker} for tests. */
+public class MockCompilerVersionChecker implements CompilerVersionChecker {
+
+  private String compilerVersion;
+
+  public MockCompilerVersionChecker(String compilerVersion) {
+    this.compilerVersion = compilerVersion;
+  }
+
+  @Nullable
+  @Override
+  public String checkCompilerVersion(WorkspaceRoot workspaceRoot, File cppExecutable) {
+    return compilerVersion;
+  }
+
+  public void setCompilerVersion(String compilerVersion) {
+    this.compilerVersion = compilerVersion;
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/OCWorkspaceProvider.java b/cpp/src/com/google/idea/blaze/cpp/OCWorkspaceProvider.java
new file mode 100644
index 0000000..f2e90e3
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/OCWorkspaceProvider.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.cpp;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+import javax.annotation.Nullable;
+
+/**
+ * An Android-Studio-safe version of {@link OCWorkspaceManager}, which handles the case where no
+ * such manager is available (e.g. because the 'NDK WorkspaceManager Support' plugin isn't enabled).
+ */
+public final class OCWorkspaceProvider {
+
+  private OCWorkspaceProvider() {}
+
+  @Nullable
+  public static OCWorkspace getWorkspace(Project project) {
+    OCWorkspaceManager manager = ServiceManager.getService(project, OCWorkspaceManager.class);
+    return manager != null ? manager.getWorkspace() : null;
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/includes/IncludePath.java b/cpp/src/com/google/idea/blaze/cpp/includes/IncludePath.java
new file mode 100644
index 0000000..c223349
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/includes/IncludePath.java
@@ -0,0 +1,52 @@
+/*
+ * 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.cpp.includes;
+
+import com.google.auto.value.AutoValue;
+import com.intellij.openapi.util.text.StringUtil;
+import com.jetbrains.cidr.lang.psi.OCIncludeDirective.Delimiters;
+
+/** Information extracted from a #include directive. */
+@AutoValue
+abstract class IncludePath {
+  abstract String headerPath();
+
+  abstract Delimiters headerDelim();
+
+  static IncludePath create(String headerPath, Delimiters headerDelim) {
+    return new AutoValue_IncludePath(headerPath, headerDelim);
+  }
+
+  static IncludePath create(String rawHeaderPath) {
+    if (rawHeaderPath.startsWith(Delimiters.QUOTES.getBeforeText())
+        && rawHeaderPath.endsWith(Delimiters.QUOTES.getAfterText())) {
+      return new AutoValue_IncludePath(
+          StringUtil.trimEnd(
+              StringUtil.trimStart(rawHeaderPath, Delimiters.QUOTES.getBeforeText()),
+              Delimiters.QUOTES.getAfterText()),
+          Delimiters.QUOTES);
+    }
+    if (rawHeaderPath.startsWith(Delimiters.ANGLE_BRACKETS.getBeforeText())
+        && rawHeaderPath.endsWith(Delimiters.ANGLE_BRACKETS.getAfterText())) {
+      return new AutoValue_IncludePath(
+          StringUtil.trimEnd(
+              StringUtil.trimStart(rawHeaderPath, Delimiters.ANGLE_BRACKETS.getBeforeText()),
+              Delimiters.ANGLE_BRACKETS.getAfterText()),
+          Delimiters.ANGLE_BRACKETS);
+    }
+    return new AutoValue_IncludePath(rawHeaderPath, Delimiters.NONE);
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/includes/IwyuPragmas.java b/cpp/src/com/google/idea/blaze/cpp/includes/IwyuPragmas.java
new file mode 100644
index 0000000..497efdc
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/includes/IwyuPragmas.java
@@ -0,0 +1,333 @@
+/*
+ * 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.cpp.includes;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiComment;
+import com.jetbrains.cidr.lang.editor.OCCommenter;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCIncludeDirective;
+import com.jetbrains.cidr.lang.psi.visitors.OCRecursiveVisitor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses IWYU pragmas specified in a given file.
+ *
+ * <p>See:
+ * https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUPragmas.md
+ */
+final class IwyuPragmas {
+  private static final String IWYU_PREFIX = "IWYU pragma:";
+
+  public final OCFile file;
+  public final Optional<PrivatePragma> privatePragma;
+  public final ImmutableSet<KeepPragma> keeps;
+  public final ImmutableSet<ExportPragma> exports;
+  public final Optional<IncludePath> associatedHeader;
+
+  private IwyuPragmas(
+      OCFile file,
+      Optional<PrivatePragma> privatePragma,
+      ImmutableSet<KeepPragma> keeps,
+      ImmutableSet<ExportPragma> exports,
+      Optional<IncludePath> associatedHeader) {
+    this.file = file;
+    this.privatePragma = privatePragma;
+    this.keeps = keeps;
+    this.exports = exports;
+    this.associatedHeader = associatedHeader;
+  }
+
+  public static IwyuPragmas parse(OCFile file) {
+    BuildingVisitor builder = new BuildingVisitor(file);
+    file.accept(builder);
+    return builder.build();
+  }
+
+  /** Parse pragmas that are trailing comments */
+  interface TrailingPragmaParser {
+    /**
+     * Checks if the pragmaContent is parsed by this parser, and updates the builder state if it is
+     * actually parsed.
+     *
+     * @param builder builder to update on success
+     * @param directive include directive with a trailing pragma comment
+     * @param pragmaContent content from the pragma comment to parse
+     * @return true if handled by this parser
+     */
+    boolean tryParse(BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent);
+  }
+
+  /** Parse pragmas are standalone comments */
+  interface StandalonePragmaParser {
+    /**
+     * Checks if the pragmaContent is parsed by this parser, and updates the builder state if it is
+     * actually parsed.
+     *
+     * @param builder builder to update on success
+     * @param pragmaContent content from the pragma comment to parse
+     * @return true if handled by this parser
+     */
+    boolean tryParse(BuildingVisitor builder, String pragmaContent);
+  }
+
+  /** Represents a "keep" pragma */
+  @AutoValue
+  public abstract static class KeepPragma {
+    /* TODO: keep pragmas can also be attached to a forward include. We don't yet handle that */
+    private static final TrailingPragmaParser PARSER = new Parser();
+
+    abstract IncludePath includePath();
+
+    static KeepPragma create(IncludePath includePath) {
+      return new AutoValue_IwyuPragmas_KeepPragma(includePath);
+    }
+
+    private static class Parser implements TrailingPragmaParser {
+      private static final Pattern KEEP_PATTERN = Pattern.compile("^\\s*keep\\s*$");
+
+      @Override
+      public boolean tryParse(
+          BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
+        Matcher matcher = KEEP_PATTERN.matcher(pragmaContent);
+        if (!matcher.find()) {
+          return false;
+        }
+        builder.keeps.add(
+            KeepPragma.create(
+                IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
+        return true;
+      }
+    }
+  }
+
+  /** Represents an "export" pragma */
+  @AutoValue
+  public abstract static class ExportPragma {
+    private static final TrailingPragmaParser TRAIL_PARSER = new TrailParser();
+    private static final StandalonePragmaParser RANGE_PARSER = new RangeParser();
+
+    abstract IncludePath includePath();
+
+    static ExportPragma create(IncludePath includePath) {
+      return new AutoValue_IwyuPragmas_ExportPragma(includePath);
+    }
+
+    private static class TrailParser implements TrailingPragmaParser {
+
+      private static final Pattern EXPORT_PATTERN = Pattern.compile("^\\s*export\\s*$");
+
+      @Override
+      public boolean tryParse(
+          BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
+        Matcher matcher = EXPORT_PATTERN.matcher(pragmaContent);
+        if (!matcher.find()) {
+          return false;
+        }
+        builder.exports.add(
+            ExportPragma.create(
+                IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
+        return true;
+      }
+    }
+
+    private static class RangeParser implements StandalonePragmaParser {
+
+      private static final Pattern BEGIN_PATTERN = Pattern.compile("^\\s*begin_exports\\s*$");
+      private static final Pattern END_PATTERN = Pattern.compile("^\\s*end_exports\\s*$");
+
+      @Override
+      public boolean tryParse(BuildingVisitor builder, String pragmaContent) {
+        Matcher matcher = BEGIN_PATTERN.matcher(pragmaContent);
+        if (matcher.matches()) {
+          builder.includesInRange.clear();
+          builder.collectRange = true;
+          return true;
+        }
+        matcher = END_PATTERN.matcher(pragmaContent);
+        if (matcher.matches()) {
+          builder.collectRange = false;
+          for (OCIncludeDirective directive : builder.includesInRange) {
+            builder.exports.add(
+                ExportPragma.create(
+                    IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
+          }
+          builder.includesInRange.clear();
+          return true;
+        }
+        return false;
+      }
+    }
+  }
+
+  /** Represents the "private" pragma */
+  public static class PrivatePragma {
+    private static final StandalonePragmaParser PARSER = new Parser();
+
+    public final Optional<IncludePath> includeOther;
+
+    PrivatePragma(IncludePath includeOther) {
+      this.includeOther = Optional.of(includeOther);
+    }
+
+    PrivatePragma() {
+      this.includeOther = Optional.empty();
+    }
+
+    private static class Parser implements StandalonePragmaParser {
+      private static final Pattern PRIVATE_PATTERN =
+          Pattern.compile("^\\s*private\\s*(,\\s*include\\s*(?<includename>.*)\\s*)?$");
+
+      @Override
+      public boolean tryParse(BuildingVisitor builder, String pragmaContent) {
+        Matcher matcher = PRIVATE_PATTERN.matcher(pragmaContent);
+        if (!matcher.find()) {
+          return false;
+        }
+        String alternateInclude = matcher.group("includename");
+        if (alternateInclude != null) {
+          builder.privatePragma =
+              Optional.of(new PrivatePragma(IncludePath.create(alternateInclude)));
+        } else {
+          builder.privatePragma = Optional.of(new PrivatePragma());
+        }
+        return true;
+      }
+    }
+  }
+
+  /** Represents the "associated" pragma */
+  public static class AssociatedPragma {
+    private static final TrailingPragmaParser PARSER = new Parser();
+
+    private static class Parser implements TrailingPragmaParser {
+      private static final Pattern ASSOCIATED_PATTERN = Pattern.compile("^\\s*associated\\s*$");
+
+      @Override
+      public boolean tryParse(
+          BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
+        Matcher matcher = ASSOCIATED_PATTERN.matcher(pragmaContent);
+        if (!matcher.find()) {
+          return false;
+        }
+        builder.associatedHeader =
+            Optional.of(
+                IncludePath.create(directive.getReferenceText(), directive.getDelimiters()));
+        return true;
+      }
+    }
+  }
+
+  private static final ImmutableList<TrailingPragmaParser> TRAILING_PARSERS =
+      ImmutableList.of(KeepPragma.PARSER, ExportPragma.TRAIL_PARSER, AssociatedPragma.PARSER);
+  private static final ImmutableList<StandalonePragmaParser> STANDALONE_PARSERS =
+      ImmutableList.of(ExportPragma.RANGE_PARSER, PrivatePragma.PARSER);
+
+  private static class BuildingVisitor extends OCRecursiveVisitor {
+
+    final OCFile file;
+    final OCCommenter commenter;
+
+    Optional<PrivatePragma> privatePragma = Optional.empty();
+    ImmutableSet.Builder<KeepPragma> keeps = ImmutableSet.builder();
+    ImmutableSet.Builder<ExportPragma> exports = ImmutableSet.builder();
+    Optional<IncludePath> associatedHeader = Optional.empty();
+
+    List<OCIncludeDirective> includesInRange = new ArrayList<>();
+    boolean collectRange;
+
+    BuildingVisitor(OCFile file) {
+      this.file = file;
+      this.commenter = new OCCommenter();
+    }
+
+    IwyuPragmas build() {
+      return new IwyuPragmas(file, privatePragma, keeps.build(), exports.build(), associatedHeader);
+    }
+
+    @Override
+    public void visitImportDirective(OCIncludeDirective directive) {
+      if (collectRange) {
+        includesInRange.add(directive);
+      }
+      visitTrailingComments(directive);
+      super.visitImportDirective(directive);
+    }
+
+    @Override
+    public void visitComment(PsiComment comment) {
+      String text = trimCommentContent(comment.getText());
+      if (text.startsWith(IWYU_PREFIX)) {
+        String pragmaContent = StringUtil.trimStart(text, IWYU_PREFIX);
+        for (StandalonePragmaParser parser : STANDALONE_PARSERS) {
+          if (parser.tryParse(this, pragmaContent)) {
+            break;
+          }
+        }
+      }
+      super.visitComment(comment);
+    }
+
+    // In older CIDR implementations, trailing comments are not separate PsiComment nodes. They
+    // are simply part of the "directive content" PsiElement (which also has #include path).
+    // Thus, we have to handle it at the OCIncludeDirective level instead of waiting for
+    // visitComment() to run. In newer CIDR implementations, trailing comments are a separate
+    // PsiComment node, but they are still a child of the OCIncludeDirective, so pragma should be
+    // found in the directive's getText().
+    private void visitTrailingComments(OCIncludeDirective directive) {
+      String fullText = directive.getText();
+      String pathText = directive.getReferenceText();
+      if (pathText.isEmpty()) {
+        return;
+      }
+      String afterPath = fullText.substring(fullText.indexOf(pathText) + pathText.length());
+      OCIncludeDirective.Delimiters delimiters = directive.getDelimiters();
+      int delimIndex = afterPath.indexOf(delimiters.getAfterText());
+      if (delimIndex == -1) {
+        return;
+      }
+      afterPath = afterPath.substring(delimIndex + delimiters.getAfterText().length()).trim();
+      String trimmed = trimCommentContent(afterPath);
+      if (trimmed.startsWith(IWYU_PREFIX)) {
+        String pragmaContent = StringUtil.trimStart(trimmed, IWYU_PREFIX);
+        for (TrailingPragmaParser parser : TRAILING_PARSERS) {
+          if (parser.tryParse(this, directive, pragmaContent)) {
+            break;
+          }
+        }
+      }
+    }
+
+    private String trimCommentContent(String text) {
+      if (text.startsWith(commenter.getLineCommentPrefix())) {
+        return StringUtil.trimStart(text, commenter.getLineCommentPrefix()).trim();
+      } else if (text.startsWith(commenter.getBlockCommentPrefix())) {
+        return StringUtil.trimEnd(
+                StringUtil.trimStart(text, commenter.getBlockCommentPrefix()),
+                commenter.getBlockCommentSuffix())
+            .trim();
+      }
+      return text.trim();
+    }
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabColorProvider.java b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabColorProvider.java
new file mode 100644
index 0000000..53fbaf9
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabColorProvider.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.cpp.syncstatus;
+
+import com.intellij.openapi.fileEditor.impl.EditorTabColorProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.JBColor;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import java.awt.Color;
+import javax.annotation.Nullable;
+
+/** Changes the color for unsynced files. */
+public class BlazeCppSyncStatusEditorTabColorProvider implements EditorTabColorProvider {
+  private static final JBColor UNSYNCED_COLOR =
+      new JBColor(new Color(252, 234, 234), new Color(121, 105, 105));
+
+  @Nullable
+  @Override
+  public Color getEditorTabColor(Project project, VirtualFile file) {
+    PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+    if (psiFile instanceof OCFile && SyncStatusHelper.isUnsynced(project, file)) {
+      return UNSYNCED_COLOR;
+    }
+    return null;
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabTitleProvider.java b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabTitleProvider.java
new file mode 100644
index 0000000..c3f7d2a
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusEditorTabTitleProvider.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.cpp.syncstatus;
+
+import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import javax.annotation.Nullable;
+
+/** Changes the tab title for unsynced files. */
+public class BlazeCppSyncStatusEditorTabTitleProvider implements EditorTabTitleProvider {
+  @Nullable
+  @Override
+  public String getEditorTabTitle(Project project, VirtualFile file) {
+    PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+    if (psiFile instanceof OCFile && SyncStatusHelper.isUnsynced(project, file)) {
+      return file.getPresentableName() + " (unsynced)";
+    }
+    return null;
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusFileNodeDecorator.java b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusFileNodeDecorator.java
new file mode 100644
index 0000000..db3172c
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/syncstatus/BlazeCppSyncStatusFileNodeDecorator.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cpp.syncstatus;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ProjectViewNode;
+import com.intellij.ide.projectView.ProjectViewNodeDecorator;
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.packageDependencies.ui.PackageDependenciesNode;
+import com.intellij.psi.PsiFile;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import com.jetbrains.cidr.lang.psi.OCFile;
+
+/** Grays out any unreachable (from project view targets) C++ files. */
+public class BlazeCppSyncStatusFileNodeDecorator implements ProjectViewNodeDecorator {
+
+  @Override
+  public void decorate(ProjectViewNode node, PresentationData data) {
+    if (!(node instanceof PsiFileNode)) {
+      return;
+    }
+    PsiFile psiFile = ((PsiFileNode) node).getValue();
+    if (!(psiFile instanceof OCFile)) {
+      return;
+    }
+    VirtualFile virtualFile = psiFile.getVirtualFile();
+    if (virtualFile == null) {
+      return;
+    }
+    Project project = node.getProject();
+    if (SyncStatusHelper.isUnsynced(project, virtualFile)) {
+      data.clearText();
+      data.addText(psiFile.getName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
+      data.addText(" (unsynced)", SimpleTextAttributes.GRAY_ATTRIBUTES);
+    }
+  }
+
+  @Override
+  public void decorate(PackageDependenciesNode node, ColoredTreeCellRenderer cellRenderer) {}
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/syncstatus/SyncStatusHelper.java b/cpp/src/com/google/idea/blaze/cpp/syncstatus/SyncStatusHelper.java
new file mode 100644
index 0000000..1aa61e6
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/syncstatus/SyncStatusHelper.java
@@ -0,0 +1,52 @@
+/*
+ * 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.cpp.syncstatus;
+
+import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
+import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.google.idea.blaze.cpp.OCWorkspaceProvider;
+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.jetbrains.cidr.lang.workspace.OCWorkspace;
+
+/** Checks if we have sync data for the given C++ file. */
+final class SyncStatusHelper {
+  private SyncStatusHelper() {}
+
+  static boolean isUnsynced(Project project, VirtualFile virtualFile) {
+    if (!virtualFile.isInLocalFileSystem()) {
+      return false;
+    }
+    if (ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(virtualFile) == null) {
+      return false;
+    }
+    OCWorkspace workspace = OCWorkspaceProvider.getWorkspace(project);
+    if (!(workspace instanceof BlazeCWorkspace)) {
+      // Skip if the project isn't a Blaze project or doesn't have C support enabled anyway.
+      return false;
+    }
+    if (workspace.getConfigurations().isEmpty()) {
+      // The workspace configurations may not have been loaded yet.
+      return false;
+    }
+    SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
+    return sourceToTargetMap
+        .getRulesForSourceFile(VfsUtilCore.virtualToIoFile(virtualFile))
+        .isEmpty();
+  }
+}
diff --git a/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppAutoImportHelperTest.java b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppAutoImportHelperTest.java
new file mode 100644
index 0000000..7750a32
--- /dev/null
+++ b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppAutoImportHelperTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.cpp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+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.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCReferenceElement;
+import com.jetbrains.cidr.lang.quickfixes.OCImportSymbolFix;
+import com.jetbrains.cidr.lang.symbols.symtable.FileSymbolTablesCache;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests that {@link BlazeCppAutoImportHelper} is able to get the correct form of #include for the
+ * {@link OCImportSymbolFix} quickfix, given typical workspace layouts / location of system headers.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCppAutoImportHelperTest extends BlazeCppIntegrationTestCase {
+
+  @Before
+  public void setup() {
+    createHeaderRoots();
+    registerApplicationService(
+        CompilerVersionChecker.class, new MockCompilerVersionChecker("1234"));
+  }
+
+  @Test
+  public void stlPathsUnderWorkspaceRoot_importStlHeader() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:bar"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:bar", Kind.CC_LIBRARY, sources("foo/bar/bar.cc"), sources()))
+            .addTarget(
+                createCcTarget(
+                    "//third_party/stl:stl",
+                    Kind.CC_LIBRARY,
+                    sources(),
+                    sources("third_party/stl/vector.h")))
+            .build();
+    // Normally this is <vector> without .h, but we need to trick the file type detector into
+    // realizing that this is an OCFile.
+    OCFile header =
+        createFile(
+            "third_party/stl/vector.h",
+            "namespace std {",
+            "template<typename T> class vector {};",
+            "}");
+    OCFile file = createFile("foo/bar/bar.cc", "std::vector<int> my_vector;");
+
+    resolve(projectView, targetMap, file, header);
+
+    testFixture.openFileInEditor(file.getVirtualFile());
+    OCReferenceElement referenceElement =
+        testFixture.findElementByText("std::vector<int>", OCReferenceElement.class);
+    OCImportSymbolFix fix = new OCImportSymbolFix(referenceElement);
+    assertThat(fix.isAvailable(getProject(), testFixture.getEditor(), file)).isTrue();
+    assertThat(fix.getAutoImportItems()).hasSize(1);
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getFirst())
+        .isEqualTo("class 'std::vector'");
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getSecond())
+        .isEqualTo("<vector.h>");
+  }
+
+  @Test
+  public void sameDirectory_importUserHeader() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:bar"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:bar",
+                    Kind.CC_LIBRARY,
+                    sources("foo/bar/bar.cc"),
+                    sources("foo/bar/test.h")))
+            .build();
+    OCFile header = createFile("foo/bar/test.h", "class SomeClass {};");
+    OCFile file = createFile("foo/bar/bar.cc", "SomeClass* my_class = new SomeClass();");
+
+    resolve(projectView, targetMap, file, header);
+
+    testFixture.openFileInEditor(file.getVirtualFile());
+    OCReferenceElement referenceElement =
+        testFixture.findElementByText("SomeClass*", OCReferenceElement.class);
+    OCImportSymbolFix fix = new OCImportSymbolFix(referenceElement);
+    assertThat(fix.isAvailable(getProject(), testFixture.getEditor(), file)).isTrue();
+    assertThat(fix.getAutoImportItems()).hasSize(1);
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getFirst())
+        .isEqualTo("class 'SomeClass'");
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getSecond())
+        .isEqualTo("\"foo/bar/test.h\"");
+  }
+
+  @Test
+  public void differentDirectory_importUserHeader() {
+    ProjectView projectView =
+        projectView(directories("foo/bar", "baz"), targets("//foo/bar", "//baz"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:bar", Kind.CC_LIBRARY, sources("foo/bar/bar.cc"), sources()))
+            .addTarget(
+                createCcTarget("//baz:baz", Kind.CC_LIBRARY, sources(""), sources("baz/test.h")))
+            .build();
+    OCFile header = createFile("baz/test.h", "class SomeClass {};");
+    OCFile file = createFile("foo/bar/bar.cc", "SomeClass* my_class = new SomeClass();");
+
+    resolve(projectView, targetMap, file, header);
+
+    testFixture.openFileInEditor(file.getVirtualFile());
+    OCReferenceElement referenceElement =
+        testFixture.findElementByText("SomeClass*", OCReferenceElement.class);
+    OCImportSymbolFix fix = new OCImportSymbolFix(referenceElement);
+    assertThat(fix.isAvailable(getProject(), testFixture.getEditor(), file)).isTrue();
+    assertThat(fix.getAutoImportItems()).hasSize(1);
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getFirst())
+        .isEqualTo("class 'SomeClass'");
+    assertThat(fix.getAutoImportItems().get(0).getTitleAndLocation().getSecond())
+        .isEqualTo("\"baz/test.h\"");
+  }
+
+  private static List<ArtifactLocation> sources(String... paths) {
+    return Arrays.stream(paths)
+        .map(path -> ArtifactLocation.builder().setRelativePath(path).setIsSource(true).build())
+        .collect(Collectors.toList());
+  }
+
+  private void createHeaderRoots() {
+    workspace.createDirectory(new WorkspacePath("output/genfiles"));
+    workspace.createDirectory(new WorkspacePath("include/third_party/libxml/_/libxml"));
+    workspace.createDirectory(new WorkspacePath("third_party/stl"));
+    workspace.createDirectory(new WorkspacePath("third_party/lib_that_expects_angle_include"));
+    workspace.createDirectory(new WorkspacePath("third_party/toolchain/include/c++/4.9"));
+  }
+
+  private TargetIdeInfo.Builder createCcTarget(
+      String label, Kind kind, List<ArtifactLocation> sources, List<ArtifactLocation> headers) {
+    TargetIdeInfo.Builder targetInfo =
+        TargetIdeInfo.builder().setLabel(label).setKind(kind).addDependency("//:toolchain");
+    sources.forEach(targetInfo::addSource);
+    return targetInfo.setCInfo(
+        CIdeInfo.builder()
+            .addSources(sources)
+            .addHeaders(headers)
+            .addTransitiveIncludeDirectories(
+                ImmutableList.of(new ExecutionRootPath("include/third_party/libxml/_/libxml")))
+            .addTransitiveQuoteIncludeDirectories(
+                ImmutableList.of(
+                    new ExecutionRootPath("."), new ExecutionRootPath("output/genfiles")))
+            .addTransitiveSystemIncludeDirectories(
+                ImmutableList.of(
+                    new ExecutionRootPath("third_party/stl"),
+                    new ExecutionRootPath("third_party/lib_that_expects_angle_include"))));
+  }
+
+  private static TargetIdeInfo.Builder createCcToolchain() {
+    return TargetIdeInfo.builder()
+        .setLabel("//:toolchain")
+        .setKind(Kind.CC_TOOLCHAIN)
+        .setCToolchainInfo(
+            CToolchainIdeInfo.builder()
+                .setCppExecutable(new ExecutionRootPath("cc"))
+                .addBuiltInIncludeDirectories(
+                    ImmutableList.of(
+                        new ExecutionRootPath("third_party/toolchain/include/c++/4.9"))));
+  }
+
+  private static ListSection<DirectoryEntry> directories(String... directories) {
+    return ListSection.builder(DirectorySection.KEY)
+        .addAll(
+            Arrays.stream(directories)
+                .map(directory -> DirectoryEntry.include(WorkspacePath.createIfValid(directory)))
+                .collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ListSection<TargetExpression> targets(String... targets) {
+    return ListSection.builder(TargetSection.KEY)
+        .addAll(
+            Arrays.stream(targets).map(TargetExpression::fromString).collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ProjectView projectView(
+      ListSection<DirectoryEntry> directories, ListSection<TargetExpression> targets) {
+    return ProjectView.builder().add(directories).add(targets).build();
+  }
+
+  private MockBlazeProjectDataBuilder projectDataBuilder() {
+    return MockBlazeProjectDataBuilder.builder(workspaceRoot)
+        .setOutputBase(fileSystem.getRootDir() + "/output");
+  }
+
+  private void resolve(ProjectView projectView, TargetMap targetMap, OCFile... files) {
+    BlazeProjectData blazeProjectData = projectDataBuilder().setTargetMap(targetMap).build();
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+    BlazeCWorkspace.getInstance(getProject())
+        .update(
+            new BlazeContext(),
+            workspaceRoot,
+            ProjectViewSet.builder().add(projectView).build(),
+            blazeProjectData);
+    for (OCFile file : files) {
+      resetFileSymbols(file);
+    }
+    FileSymbolTablesCache.getInstance(getProject()).ensurePendingFilesProcessed();
+  }
+
+  private void resetFileSymbols(OCFile file) {
+    FileSymbolTablesCache.getInstance(getProject()).handleFileChange(file, true);
+  }
+}
diff --git a/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppIntegrationTestCase.java b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppIntegrationTestCase.java
new file mode 100644
index 0000000..84b35c1
--- /dev/null
+++ b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/BlazeCppIntegrationTestCase.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.cpp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.jetbrains.cidr.lang.OCLanguage.LANGUAGE_SUPPORT_DISABLED;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+import org.junit.Before;
+
+/** Base C++ test class for integration tests. */
+public class BlazeCppIntegrationTestCase extends BlazeIntegrationTestCase {
+
+  @Before
+  public void enableCppLanguageSupport() throws Throwable {
+    registerProjectService(OCWorkspaceManager.class, new TestOCWorkspaceManager());
+    enableCSupportInIde(getProject());
+  }
+
+  protected OCFile createFile(String relativePath, String... contentLines) {
+    PsiFile file = workspace.createPsiFile(new WorkspacePath(relativePath), contentLines);
+    assertThat(file).isInstanceOf(OCFile.class);
+    return (OCFile) file;
+  }
+
+  private static void enableCSupportInIde(Project project) {
+    OCWorkspace workspace = OCWorkspaceProvider.getWorkspace(project);
+    assertThat(workspace).isNotNull();
+    if (LANGUAGE_SUPPORT_DISABLED.get(project, false)) {
+      LANGUAGE_SUPPORT_DISABLED.set(project, false);
+    }
+  }
+
+  private class TestOCWorkspaceManager extends OCWorkspaceManager {
+    @Override
+    public OCWorkspace getWorkspace() {
+      return BlazeCWorkspace.getInstance(getProject());
+    }
+  }
+}
diff --git a/cpp/tests/integrationtests/com/google/idea/blaze/cpp/includes/IwyuPragmasTest.java b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/includes/IwyuPragmasTest.java
new file mode 100644
index 0000000..b592636
--- /dev/null
+++ b/cpp/tests/integrationtests/com/google/idea/blaze/cpp/includes/IwyuPragmasTest.java
@@ -0,0 +1,452 @@
+/*
+ * 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.cpp.includes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.cpp.BlazeCppIntegrationTestCase;
+import com.google.idea.blaze.cpp.includes.IwyuPragmas.ExportPragma;
+import com.google.idea.blaze.cpp.includes.IwyuPragmas.KeepPragma;
+import com.google.idea.blaze.cpp.includes.IwyuPragmas.PrivatePragma;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCIncludeDirective.Delimiters;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for parsing {@link IwyuPragmas}. */
+@RunWith(JUnit4.class)
+public class IwyuPragmasTest extends BlazeCppIntegrationTestCase {
+
+  @Test
+  public void noPragmas() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"bar.h\"",
+            "",
+            "#include <memory>",
+            "#include <vector>",
+            "",
+            "#include \"f/foo1.h\"",
+            "#include \"f/foo2.h\"",
+            "#include \"f/foo3.h\"  // blah",
+            "",
+            "void bar() {}");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isFalse();
+    assertThat(pragmas.keeps).isEmpty();
+    assertThat(pragmas.exports).isEmpty();
+    assertThat(pragmas.associatedHeader.isPresent()).isFalse();
+  }
+
+  @Test
+  public void incompletePath_parse() {
+    OCFile file = createFile("bar.cc", "#include <memory>", "#include <vecto");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps).isEmpty();
+  }
+
+  @Test
+  public void noPathYet_parse() {
+    OCFile file = createFile("bar.cc", "#include <memory>", "#include ");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps).isEmpty();
+  }
+
+  @Test
+  public void lineComment_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"bar.h\"",
+            "",
+            "#include <memory>",
+            "#include <vector> // IWYU pragma: keep",
+            "",
+            "#include \"f/foo1.h\" // IWYU pragma: keep",
+            "#include \"f/foo2.h\"",
+            "#include \"f/foo3.h\"     // IWYU pragma: keep     ",
+            "",
+            "void bar() {}");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(
+            KeepPragma.create(IncludePath.create("vector", Delimiters.ANGLE_BRACKETS)),
+            KeepPragma.create(IncludePath.create("f/foo1.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("f/foo3.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void blockComment_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"bar.h\"",
+            "",
+            "#include <memory>",
+            "#include <vector> /* IWYU pragma: keep */",
+            "",
+            "#include \"f/foo1.h\" /* IWYU pragma: keep */",
+            "#include \"f/foo2.h\"",
+            "#include \"f/foo3.h\"     /* IWYU pragma: keep    */",
+            "",
+            "void bar() {}");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(
+            KeepPragma.create(IncludePath.create("vector", Delimiters.ANGLE_BRACKETS)),
+            KeepPragma.create(IncludePath.create("f/foo1.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("f/foo3.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void lineCommentIncludeGuardPrefixes_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.h",
+            "#ifndef BAR_H_",
+            "#define BAR_H_",
+            "#include <memory>",
+            "#include <vector> // IWYU pragma: keep",
+            "",
+            "#include \"f/foo1.h\" // IWYU pragma: keep",
+            "#include \"f/foo2.h\"",
+            "#include \"f/foo3.h\"     // IWYU pragma: keep     ",
+            "",
+            "void bar();",
+            "#endif");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(
+            KeepPragma.create(IncludePath.create("vector", Delimiters.ANGLE_BRACKETS)),
+            KeepPragma.create(IncludePath.create("f/foo1.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("f/foo3.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void incompletePragma_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"f/foo1.h\" // IWYU pragma: kee",
+            "#include \"f/foo2.h\" // IWYU pragma: keep");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(KeepPragma.create(IncludePath.create("f/foo2.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void hasSuffixSoNoMatch_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"f/foo1.h\" // IWYU pragma: keepaway",
+            "#include \"f/foo2.h\" // IWYU pragma: keep");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(KeepPragma.create(IncludePath.create("f/foo2.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void insideNamespace_parseKeep() {
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"f/foo1.h\" // IWYU pragma: keep",
+            "#include \"f/foo2.h\"",
+            "namespace change_namespace {",
+            "#include \"f/foo3.h\" // IWYU pragma: keep",
+            "}",
+            "class C {",
+            "#include \"f/foo4.h\" // IWYU pragma: keep",
+            "};");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(
+            KeepPragma.create(IncludePath.create("f/foo1.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("f/foo3.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("f/foo4.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void duplicate_parseKeep() {
+    // Probably need to disambiguate the PSI nodes somewhere (last one shouldn't be kept?)
+    OCFile file =
+        createFile(
+            "bar.cc",
+            "#include \"f/foo1.h\" // IWYU pragma: keep",
+            "#include \"f/foo1.h\" // IWYU pragma: keep",
+            "#include \"f/foo1.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(KeepPragma.create(IncludePath.create("f/foo1.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void parseExport() {
+    OCFile file =
+        createFile(
+            "public/foo.h",
+            "/** Stuff */",
+            "#include <private/memory> // IWYU pragma: export",
+            "#include <vector>",
+            "",
+            "#include \"private/foo1.h\" // IWYU pragma: export",
+            "#include \"private/foo2.h\"",
+            "#include \"private/foo3.h\"     // IWYU pragma: export",
+            "",
+            "void doFoo(Foo* f);");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.exports)
+        .containsExactly(
+            ExportPragma.create(IncludePath.create("private/memory", Delimiters.ANGLE_BRACKETS)),
+            ExportPragma.create(IncludePath.create("private/foo1.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo3.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void singeRange_parseExportBeginEnd() {
+    OCFile file =
+        createFile(
+            "public/foo.h",
+            "/** Stuff */",
+            "// IWYU pragma: begin_exports",
+            "#include <private/memory>",
+            "#include \"private/foo1.h\"",
+            "#include \"private/foo2.h\"",
+            "// IWYU pragma: end_exports",
+            "#include \"private/foo3.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.exports)
+        .containsExactly(
+            ExportPragma.create(IncludePath.create("private/memory", Delimiters.ANGLE_BRACKETS)),
+            ExportPragma.create(IncludePath.create("private/foo1.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo2.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void multipleRanges_parseExportBeginEnd() {
+    OCFile file =
+        createFile(
+            "public/foo.h",
+            "/** Stuff */",
+            "// IWYU pragma: begin_exports",
+            "#include <private/memory>",
+            "#include \"private/foo1.h\"",
+            "#include \"private/foo2.h\"",
+            "// IWYU pragma: end_exports",
+            "#include \"private/foo3.h\"",
+            "#include \"private/foo4.h\"",
+            "   // IWYU pragma: begin_exports",
+            "#include \"private/foo5.h\"",
+            "   // IWYU pragma: end_exports",
+            "#include \"private/foo6.h\"",
+            "",
+            "void doFoo(Foo& f);");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.exports)
+        .containsExactly(
+            ExportPragma.create(IncludePath.create("private/memory", Delimiters.ANGLE_BRACKETS)),
+            ExportPragma.create(IncludePath.create("private/foo1.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo2.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo5.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void exportRangePlusInlineKeep() {
+    // Probably need to disambiguate the PSI nodes somewhere (last one shouldn't be kept?)
+    OCFile file =
+        createFile(
+            "public/foo.h",
+            "// IWYU pragma: begin_exports",
+            "#include <memory>",
+            "#include \"private/foo1.h\" // IWYU pragma: keep",
+            "#include \"private/foo2.h\" // IWYU pragma: keep",
+            "#include \"private/foo3.h\"",
+            "// IWYU pragma: end_exports",
+            "#include \"private/foo4.h\"",
+            "",
+            "void doFoo(Foo& f);");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.keeps)
+        .containsExactly(
+            KeepPragma.create(IncludePath.create("private/foo1.h", Delimiters.QUOTES)),
+            KeepPragma.create(IncludePath.create("private/foo2.h", Delimiters.QUOTES)));
+    assertThat(pragmas.exports)
+        .containsExactly(
+            ExportPragma.create(IncludePath.create("memory", Delimiters.ANGLE_BRACKETS)),
+            ExportPragma.create(IncludePath.create("private/foo1.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo2.h", Delimiters.QUOTES)),
+            ExportPragma.create(IncludePath.create("private/foo3.h", Delimiters.QUOTES)));
+  }
+
+  @Test
+  public void parsePrivate() {
+    OCFile file =
+        createFile(
+            "private.h", "// Stuff", "// IWYU pragma: private", "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isFalse();
+  }
+
+  @Test
+  public void incomplete_parsePrivate() {
+    OCFile file =
+        createFile(
+            "private.h", "// Stuff", "// IWYU pragma: privat", "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isFalse();
+  }
+
+  @Test
+  public void parsePrivateIncludeOther() {
+    OCFile file =
+        createFile(
+            "private.h",
+            "// IWYU pragma: private, include \"f/public.h\"",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isTrue();
+    assertThat(privatePragma.includeOther.get())
+        .isEqualTo(IncludePath.create("f/public.h", Delimiters.QUOTES));
+  }
+
+  @Test
+  public void withWhiteSpace_parsePrivateIncludeOther() {
+    OCFile file =
+        createFile(
+            "private.h",
+            "//   IWYU pragma:private,include\"f/public.h\"   ",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isTrue();
+    assertThat(privatePragma.includeOther.get())
+        .isEqualTo(IncludePath.create("f/public.h", Delimiters.QUOTES));
+  }
+
+  @Test
+  public void parsePrivateIncludeOtherAngle() {
+    OCFile file =
+        createFile(
+            "private.h",
+            "// IWYU pragma: private, include <f/public.h>",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isTrue();
+    assertThat(privatePragma.includeOther.get())
+        .isEqualTo(IncludePath.create("f/public.h", Delimiters.ANGLE_BRACKETS));
+  }
+
+  @Test
+  public void parsePrivateIncludeOtherNoQuotes() {
+    OCFile file =
+        createFile(
+            "private.h",
+            "// IWYU pragma: private, include f/public.h",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isTrue();
+    assertThat(privatePragma.includeOther.get())
+        .isEqualTo(IncludePath.create("f/public.h", Delimiters.NONE));
+  }
+
+  @Test
+  public void hasMultiple_parsePrivate() {
+    // We shouldn't have multiple private pragmas, but we just take the last one.
+    // An alternative might be to take the most descriptive one (when there is a unique
+    // most descriptive one).
+    OCFile file =
+        createFile(
+            "private.h",
+            "// Stuff",
+            "// IWYU pragma: private",
+            "// IWYU pragma: private, include \"public.h\"",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isTrue();
+    assertThat(privatePragma.includeOther.get())
+        .isEqualTo(IncludePath.create("public.h", Delimiters.QUOTES));
+  }
+
+  @Test
+  public void hasMultipleOtherOrder_parsePrivate() {
+    // Like hasMultiple_parsePrivate, but with order swapped
+    OCFile file =
+        createFile(
+            "private.h",
+            "// Stuff",
+            "// IWYU pragma: private, include \"public.h\"",
+            "// IWYU pragma: private",
+            "#include \"private_details.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isFalse();
+  }
+
+  @Test
+  public void sortOfCommentViaIfdef_parsePrivate() {
+    OCFile file =
+        createFile("bar.cc", "#include <memory>", "#if 0", "IWYU pragma: private", "#endif");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    // We don't really support this, but #ifdef'ed out things may be parsed as PsiComment,
+    // so at least make sure we don't assert. Answer could go either way.
+    assertThat(pragmas.privatePragma.isPresent()).isTrue();
+    PrivatePragma privatePragma = pragmas.privatePragma.get();
+    assertThat(privatePragma.includeOther.isPresent()).isFalse();
+  }
+
+  @Test
+  public void parseAssociated() {
+    OCFile file =
+        createFile(
+            "implementation.cc",
+            "#include \"some/interface.h\" // IWYU pragma: associated",
+            "",
+            "#include \"other_stuff.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.associatedHeader.isPresent()).isTrue();
+    assertThat(pragmas.associatedHeader.get())
+        .isEqualTo(IncludePath.create("some/interface.h", Delimiters.QUOTES));
+  }
+
+  @Test
+  public void parseAssociatedAngle() {
+    OCFile file =
+        createFile(
+            "implementation.cc",
+            "#include <some/interface.h> // IWYU pragma: associated",
+            "",
+            "#include \"other_stuff.h\"");
+    IwyuPragmas pragmas = IwyuPragmas.parse(file);
+    assertThat(pragmas.associatedHeader.isPresent()).isTrue();
+    assertThat(pragmas.associatedHeader.get())
+        .isEqualTo(IncludePath.create("some/interface.h", Delimiters.ANGLE_BRACKETS));
+  }
+}
diff --git a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
index a1e748c..e1fbe27 100644
--- a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
+++ b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
@@ -22,6 +22,7 @@
 import com.google.idea.sdkcompat.cidr.CidrCompilerSwitchesAdapter;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
 import java.io.File;
 import java.util.List;
 import org.junit.Test;
@@ -36,8 +37,16 @@
   public void testCompilerSwitchesSimple() {
     File cppExe = new File("bin/cpp");
     ImmutableList<String> cFlags = ImmutableList.of("-fast", "-slow");
+    CompilerInfoCache compilerInfoCache = new CompilerInfoCache();
     BlazeCompilerSettings settings =
-        new BlazeCompilerSettings(getProject(), cppExe, cppExe, cFlags, cFlags);
+        new BlazeCompilerSettings(
+            getProject(),
+            cppExe,
+            cppExe,
+            cFlags,
+            cFlags,
+            "cc version (trunk r123456)",
+            compilerInfoCache);
 
     CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
     List<String> commandLineArgs = CidrCompilerSwitchesAdapter.getFileArgs(compilerSwitches);
diff --git a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java
new file mode 100644
index 0000000..612f87d
--- /dev/null
+++ b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeConfigurationResolverTest.java
@@ -0,0 +1,698 @@
+/*
+ * 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.cpp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.bazel.BazelBuildSystemProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.impl.ProgressManagerImpl;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link BlazeConfigurationResolver}. */
+@RunWith(JUnit4.class)
+public class BlazeConfigurationResolverTest extends BlazeTestCase {
+  private final BlazeContext context = new BlazeContext();
+  private final ErrorCollector errorCollector = new ErrorCollector();
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+
+  private BlazeConfigurationResolver resolver;
+  private BlazeConfigurationResolverResult resolverResult;
+  private MockCompilerVersionChecker compilerVersionChecker;
+  private LocalFileSystem mockFileSystem;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(BlazeExecutor.class, new MockBlazeExecutor());
+    compilerVersionChecker = new MockCompilerVersionChecker("1234");
+    applicationServices.register(CompilerVersionChecker.class, compilerVersionChecker);
+    applicationServices.register(ProgressManager.class, new ProgressManagerImpl());
+    applicationServices.register(VirtualFileManager.class, mock(VirtualFileManager.class));
+    mockFileSystem = mock(LocalFileSystem.class);
+    applicationServices.register(
+        VirtualFileSystemProvider.class, mock(VirtualFileSystemProvider.class));
+    when(VirtualFileSystemProvider.getInstance().getSystem()).thenReturn(mockFileSystem);
+
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
+    BuildSystemProvider buildSystemProvider = new BazelBuildSystemProvider();
+    registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class)
+        .registerExtension(buildSystemProvider);
+    BlazeImportSettingsManager.getInstance(getProject())
+        .setImportSettings(
+            new BlazeImportSettings("", "", "", "", buildSystemProvider.buildSystem()));
+
+    context.addOutputSink(IssueOutput.class, errorCollector);
+
+    resolver = new BlazeConfigurationResolver(project);
+    resolverResult = BlazeConfigurationResolverResult.empty(project);
+  }
+
+  @Test
+  public void testEmptyProject() {
+    ProjectView projectView = projectView(directories(), targets());
+    TargetMap targetMap = TargetMapBuilder.builder().build();
+    assertThatResolving(projectView, targetMap).producesNoConfigurations();
+  }
+
+  @Test
+  public void testTargetWithoutSources() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:library"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(createCcTarget("//foo/bar:library", Kind.CC_LIBRARY, ImmutableList.of()))
+            .build();
+    assertThatResolving(projectView, targetMap).producesNoConfigurations();
+  }
+
+  @Test
+  public void testTargetWithGeneratedSources() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:library"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(gen("foo/bar/library.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap).producesNoConfigurations();
+  }
+
+  @Test
+  public void testTargetWithMixedSources() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary",
+                    Kind.CC_BINARY,
+                    ImmutableList.of(src("foo/bar/binary.cc"), gen("foo/bar/generated.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+  }
+
+  @Test
+  public void testSingleSourceTarget() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+  }
+
+  @Test
+  public void testSingleSourceTargetWithLibraryDependencies() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                        "//foo/bar:binary",
+                        Kind.CC_BINARY,
+                        ImmutableList.of(src("foo/bar/binary.cc")))
+                    .addDependency("//bar/baz:library")
+                    .addDependency("//third_party:library"))
+            .addTarget(
+                createCcTarget(
+                    "//bar/baz:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("bar/baz/library.cc"))))
+            .addTarget(
+                createCcTarget(
+                    "//third_party:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("third_party/library.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+  }
+
+  @Test
+  public void testSingleSourceTargetWithSourceDependencies() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                        "//foo/bar:binary",
+                        Kind.CC_BINARY,
+                        ImmutableList.of(src("foo/bar/binary.cc")))
+                    .addDependency("//foo/bar:library")
+                    .addDependency("//third_party:library"))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/library.cc")),
+                    ImmutableList.of("SOME_DEFINE=1")))
+            .addTarget(
+                createCcTarget(
+                    "//third_party:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("third_party/library.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap)
+        .producesConfigurationsFor("//foo/bar:binary", "//foo/bar:library");
+  }
+
+  @Test
+  public void testComplexProject() {
+    ProjectView projectView =
+        projectView(
+            directories("foo/bar", "foo/baz"),
+            targets("//foo:test", "//foo/bar:binary", "//foo/baz:test"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget("//foo:test", Kind.CC_TEST, ImmutableList.of(src("foo/test.cc")))
+                    .addDependency("//foo:library")
+                    .addDependency("//foo/bar:library")
+                    .addDependency("//third_party:library"))
+            .addTarget(
+                createCcTarget(
+                    "//foo:library", Kind.CC_TEST, ImmutableList.of(src("foo/library.cc"))))
+            .addTarget(
+                createCcTarget(
+                        "//foo/bar:binary",
+                        Kind.CC_BINARY,
+                        ImmutableList.of(src("foo/bar/binary.cc")),
+                        ImmutableList.of("SOME_DEFINE=1"))
+                    .addDependency("//foo/bar:library")
+                    .addDependency("//foo/bar:empty")
+                    .addDependency("//foo/bar:generated")
+                    .addDependency("//foo/bar:mixed")
+                    .addDependency("//third_party:library"))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/library.cc")),
+                    ImmutableList.of("SOME_DEFINE=2")))
+            .addTarget(createCcTarget("//foo/bar:empty", Kind.CC_LIBRARY, ImmutableList.of()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:generated",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(gen("foo/bar/generated.cc"))))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:mixed",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/mixed_src.cc"), gen("foo/bar/mixed_gen.cc")),
+                    ImmutableList.of("SOME_DEFINE=3")))
+            .addTarget(
+                createCcTarget(
+                        "//foo/baz:test",
+                        Kind.CC_TEST,
+                        ImmutableList.of(src("foo/baz/test.cc")),
+                        ImmutableList.of("SOME_DEFINE=4"))
+                    .addDependency("//foo/baz:binary")
+                    .addDependency("//foo/baz:library")
+                    .addDependency("//foo/qux:library"))
+            .addTarget(
+                createCcTarget(
+                    "//foo/baz:binary",
+                    Kind.CC_BINARY,
+                    ImmutableList.of(src("foo/baz/binary.cc")),
+                    ImmutableList.of("SOME_DEFINE=5")))
+            .addTarget(
+                createCcTarget(
+                    "//foo/baz:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/baz/library.cc")),
+                    ImmutableList.of("SOME_DEFINE=6")))
+            .addTarget(
+                createCcTarget(
+                    "//foo/qux:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/qux/library.cc"))))
+            .addTarget(
+                createCcTarget(
+                    "//third_party:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("third_party/library.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap)
+        .producesConfigurationsFor(
+            "//foo/bar:binary",
+            "//foo/bar:library",
+            "//foo/bar:mixed",
+            "//foo/baz:test",
+            "//foo/baz:binary",
+            "//foo/baz:library");
+  }
+
+  @Test
+  public void firstResolve_testNotIncremental() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    ImmutableList<BlazeResolveConfiguration> noReusedConfigurations = ImmutableList.of();
+    assertThatResolving(projectView, targetMap)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:binary");
+  }
+
+  @Test
+  public void identicalTargets_testIncrementalUpdateFullReuse() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+    Collection<BlazeResolveConfiguration> initialConfigurations =
+        resolverResult.getAllConfigurations();
+
+    assertThatResolving(projectView, targetMap).reusedConfigurations(initialConfigurations);
+  }
+
+  @Test
+  public void newTarget_testIncrementalUpdatePartlyReused() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:*"));
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary",
+                    Kind.CC_BINARY,
+                    ImmutableList.of(src("foo/bar/binary.cc"))));
+    assertThatResolving(projectView, targetMapBuilder.build())
+        .producesConfigurationsFor("//foo/bar:binary");
+    Collection<BlazeResolveConfiguration> initialConfigurations =
+        resolverResult.getAllConfigurations();
+
+    targetMapBuilder.addTarget(
+        createCcTarget(
+            "//foo/bar:library",
+            Kind.CC_LIBRARY,
+            ImmutableList.of(src("foo/bar/library.cc")),
+            ImmutableList.of("OTHER=1")));
+
+    assertThatResolving(projectView, targetMapBuilder.build())
+        .reusedConfigurations(initialConfigurations, "//foo/bar:library");
+  }
+
+  @Test
+  public void afterQueryingConfiguration_newTarget_testIncrementalUpdatePartlyReused() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:*"));
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary",
+                    Kind.CC_BINARY,
+                    ImmutableList.of(src("foo/bar/binary.cc"))));
+    assertThatResolving(projectView, targetMapBuilder.build())
+        .producesConfigurationsFor("//foo/bar:binary");
+    Collection<BlazeResolveConfiguration> initialConfigurations =
+        resolverResult.getAllConfigurations();
+
+    // Make sure that if we *query* the configuration in some way, it doesn't affect its
+    // compatibility / reusability. There may be caches attached to the configuration and those
+    // should not be compared when checking equivalence.
+    OCResolveConfiguration firstConfiguration = initialConfigurations.iterator().next();
+    firstConfiguration.getLibraryHeadersRoots(
+        new OCResolveRootAndConfiguration(firstConfiguration, OCLanguageKind.CPP));
+
+    targetMapBuilder.addTarget(
+        createCcTarget(
+            "//foo/bar:library",
+            Kind.CC_LIBRARY,
+            ImmutableList.of(src("foo/bar/library.cc")),
+            ImmutableList.of("OTHER=1")));
+
+    assertThatResolving(projectView, targetMapBuilder.build())
+        .reusedConfigurations(initialConfigurations, "//foo/bar:library");
+  }
+
+  @Test
+  public void completelyDifferentTargetsSameProjectView_testIncrementalUpdateNoReuse() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:*"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    ImmutableList<BlazeResolveConfiguration> noReusedConfigurations = ImmutableList.of();
+    assertThatResolving(projectView, targetMap)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:binary");
+
+    TargetMap targetMap2 =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/library.cc")),
+                    ImmutableList.of("OTHER=1")))
+            .build();
+    assertThatResolving(projectView, targetMap2)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:library");
+  }
+
+  @Test
+  public void completelyDifferentTargetsDifferentProjectView_testIncrementalUpdateNoReuse() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    ImmutableList<BlazeResolveConfiguration> noReusedConfigurations = ImmutableList.of();
+    assertThatResolving(projectView, targetMap)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:binary");
+
+    ProjectView projectView2 = projectView(directories("foo/zoo"), targets("//foo/zoo:library"));
+    TargetMap targetMap2 =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/zoo:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/zoo/library.cc")),
+                    ImmutableList.of("OTHER=1")))
+            .build();
+    assertThatResolving(projectView2, targetMap2)
+        .reusedConfigurations(noReusedConfigurations, "//foo/zoo:library");
+  }
+
+  @Test
+  public void changeCompilerVersion_testIncrementalUpdateNoReuse() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+
+    ImmutableList<BlazeResolveConfiguration> noReusedConfigurations = ImmutableList.of();
+    assertThatResolving(projectView, targetMap)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:binary");
+
+    compilerVersionChecker.setCompilerVersion("cc modified version");
+    assertThatResolving(projectView, targetMap)
+        .reusedConfigurations(noReusedConfigurations, "//foo/bar:binary");
+  }
+
+  @Test
+  public void noChange_testIncrementalUpdateGetChangedFiles() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:binary"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+
+    assertThatResolving(projectView, targetMap).hasChangedRemovedFiles(ImmutableList.of(), false);
+  }
+
+  @Test
+  public void addFiles_testIncrementalUpdateGetChangedFiles() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:*"));
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary",
+                    Kind.CC_BINARY,
+                    ImmutableList.of(src("foo/bar/binary.cc"))));
+    TargetMap targetMap = targetMapBuilder.build();
+    createVirtualFile("/root/foo/bar/binary.cc");
+    assertThatResolving(projectView, targetMap).producesConfigurationsFor("//foo/bar:binary");
+
+    TargetMap targetMap2 =
+        targetMapBuilder
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/library.cc"))))
+            .build();
+    VirtualFile libraryCc = createVirtualFile("/root/foo/bar/library.cc");
+    assertThatResolving(projectView, targetMap2)
+        .hasChangedRemovedFiles(ImmutableList.of(libraryCc.getPath()), true);
+  }
+
+  @Test
+  public void removeFiles_testIncrementalUpdateGetChangedFiles() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:*"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:library",
+                    Kind.CC_LIBRARY,
+                    ImmutableList.of(src("foo/bar/library.cc")),
+                    ImmutableList.of("SOME_DEFINE=1")))
+            .build();
+    createVirtualFile("/root/foo/bar/binary.cc");
+    createVirtualFile("/root/foo/bar/library.cc");
+    assertThatResolving(projectView, targetMap)
+        .producesConfigurationsFor("//foo/bar:binary", "//foo/bar:library");
+
+    TargetMap targetMap2 =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:binary", Kind.CC_BINARY, ImmutableList.of(src("foo/bar/binary.cc"))))
+            .build();
+    assertThatResolving(projectView, targetMap2).hasChangedRemovedFiles(ImmutableList.of(), true);
+  }
+
+  private static ArtifactLocation src(String path) {
+    return ArtifactLocation.builder().setRelativePath(path).setIsSource(true).build();
+  }
+
+  private static ArtifactLocation gen(String path) {
+    return ArtifactLocation.builder().setRelativePath(path).setIsSource(false).build();
+  }
+
+  private static TargetIdeInfo.Builder createCcTarget(
+      String label, Kind kind, ImmutableList<ArtifactLocation> sources) {
+    return createCcTarget(label, kind, sources, ImmutableList.of());
+  }
+
+  private static TargetIdeInfo.Builder createCcTarget(
+      String label,
+      Kind kind,
+      ImmutableList<ArtifactLocation> sources,
+      ImmutableList<String> defines) {
+    TargetIdeInfo.Builder targetInfo =
+        TargetIdeInfo.builder().setLabel(label).setKind(kind).addDependency("//:toolchain");
+    sources.forEach(targetInfo::addSource);
+    return targetInfo.setCInfo(CIdeInfo.builder().addSources(sources).addLocalDefines(defines));
+  }
+
+  private static TargetIdeInfo.Builder createCcToolchain() {
+    return TargetIdeInfo.builder()
+        .setLabel("//:toolchain")
+        .setKind(Kind.CC_TOOLCHAIN)
+        .setCToolchainInfo(
+            CToolchainIdeInfo.builder().setCppExecutable(new ExecutionRootPath("cc")));
+  }
+
+  private static ListSection<DirectoryEntry> directories(String... directories) {
+    return ListSection.builder(DirectorySection.KEY)
+        .addAll(
+            Arrays.stream(directories)
+                .map(directory -> DirectoryEntry.include(WorkspacePath.createIfValid(directory)))
+                .collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ListSection<TargetExpression> targets(String... targets) {
+    return ListSection.builder(TargetSection.KEY)
+        .addAll(
+            Arrays.stream(targets).map(TargetExpression::fromString).collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ProjectView projectView(
+      ListSection<DirectoryEntry> directories, ListSection<TargetExpression> targets) {
+    return ProjectView.builder().add(directories).add(targets).build();
+  }
+
+  private VirtualFile createVirtualFile(String path) {
+    VirtualFile mockFile = mock(VirtualFile.class);
+    when(mockFile.getPath()).thenReturn(path);
+    when(mockFileSystem.findFileByIoFile(new File(path))).thenReturn(mockFile);
+    return mockFile;
+  }
+
+  private Subject assertThatResolving(ProjectView projectView, TargetMap targetMap) {
+    BlazeProjectData blazeProjectData =
+        MockBlazeProjectDataBuilder.builder(workspaceRoot).setTargetMap(targetMap).build();
+    resolverResult =
+        resolver.update(
+            context,
+            workspaceRoot,
+            ProjectViewSet.builder().add(projectView).build(),
+            blazeProjectData,
+            resolverResult);
+    errorCollector.assertNoIssues();
+    return new Subject() {
+      @Override
+      public void producesConfigurationsFor(String... expected) {
+        List<String> targets =
+            resolverResult
+                .getAllConfigurations()
+                .stream()
+                .map(configuration -> configuration.getDisplayName(false))
+                .collect(Collectors.toList());
+        assertThat(targets).containsExactly((Object[]) expected);
+      }
+
+      @Override
+      public void producesNoConfigurations() {
+        assertThat(resolverResult.getAllConfigurations()).isEmpty();
+      }
+
+      @Override
+      public void reusedConfigurations(
+          Collection<BlazeResolveConfiguration> expectedReused, String... expectedNotReused) {
+        Collection<BlazeResolveConfiguration> currentConfigurations =
+            resolverResult.getAllConfigurations();
+        assertContainsAllInIdentity(expectedReused, currentConfigurations);
+        List<String> notReusedTargets =
+            currentConfigurations
+                .stream()
+                .filter(
+                    configuration ->
+                        expectedReused
+                            .stream()
+                            .noneMatch(reusedConfig -> configuration == reusedConfig))
+                .map(configuration -> configuration.getDisplayName(false))
+                .collect(Collectors.toList());
+        assertThat(notReusedTargets).containsExactly((Object[]) expectedNotReused);
+      }
+
+      @Override
+      public void hasChangedRemovedFiles(
+          ImmutableList<String> expectedChangedFiles, boolean expectedHasChanges) {
+        BlazeConfigurationResolverDiff diff = resolverResult.getConfigurationDiff();
+        assertThat(diff).isNotNull();
+        List<String> changedFileNames =
+            diff.getChangedFiles().stream().map(VirtualFile::getPath).collect(Collectors.toList());
+        assertThat(changedFileNames).containsExactlyElementsIn(expectedChangedFiles);
+        assertThat(diff.hasChanges()).isEqualTo(expectedHasChanges);
+      }
+
+      // In newer truth libraries, we could use:
+      // assertThat(actual).comparingElementsUsing(IdentityCorrespondence).containsAllIn(expected)
+      // but that isn't available in truth 0.30 from older plugin APIs.
+      private <T> void assertContainsAllInIdentity(Collection<T> expected, Collection<T> actual) {
+        for (T expectedItem : expected) {
+          assertThat(actual.stream().anyMatch(actualItem -> actualItem == expectedItem)).isTrue();
+        }
+      }
+    };
+  }
+
+  private interface Subject {
+    void producesConfigurationsFor(String... expected);
+
+    void producesNoConfigurations();
+
+    void reusedConfigurations(Collection<BlazeResolveConfiguration> reused, String... notReused);
+
+    void hasChangedRemovedFiles(
+        ImmutableList<String> expectedChangedFiles, boolean hasRemovedFiles);
+  }
+}
diff --git a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeResolveConfigurationEquivalenceTest.java b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeResolveConfigurationEquivalenceTest.java
new file mode 100644
index 0000000..a08bfc9
--- /dev/null
+++ b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeResolveConfigurationEquivalenceTest.java
@@ -0,0 +1,661 @@
+/*
+ * 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.cpp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.bazel.BazelBuildSystemProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.mock.MockPsiManager;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.impl.ProgressManagerImpl;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.newvfs.impl.StubVirtualFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeadersSearchRoot;
+import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests that we group equivalent {@link BlazeResolveConfiguration}s. */
+@RunWith(JUnit4.class)
+public class BlazeResolveConfigurationEquivalenceTest extends BlazeTestCase {
+  private final BlazeContext context = new BlazeContext();
+  private final ErrorCollector errorCollector = new ErrorCollector();
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+
+  private BlazeConfigurationResolver resolver;
+  private BlazeConfigurationResolverResult resolverResult;
+  private LocalFileSystem mockFileSystem;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(BlazeExecutor.class, new MockBlazeExecutor());
+    applicationServices.register(
+        CompilerVersionChecker.class, new MockCompilerVersionChecker("1234"));
+
+    applicationServices.register(ProgressManager.class, new ProgressManagerImpl());
+    applicationServices.register(VirtualFileManager.class, mock(VirtualFileManager.class));
+    mockFileSystem = mock(LocalFileSystem.class);
+    applicationServices.register(
+        VirtualFileSystemProvider.class, mock(VirtualFileSystemProvider.class));
+    when(VirtualFileSystemProvider.getInstance().getSystem()).thenReturn(mockFileSystem);
+
+    projectServices.register(PsiManager.class, new MockPsiManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
+
+    BuildSystemProvider buildSystemProvider = new BazelBuildSystemProvider();
+    registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class)
+        .registerExtension(buildSystemProvider);
+    BlazeImportSettingsManager.getInstance(getProject())
+        .setImportSettings(
+            new BlazeImportSettings("", "", "", "", buildSystemProvider.buildSystem()));
+
+    context.addOutputSink(IssueOutput.class, errorCollector);
+
+    resolver = new BlazeConfigurationResolver(project);
+    resolverResult = BlazeConfigurationResolverResult.empty(project);
+  }
+
+  @Test
+  public void testEmptyConfigurations() {
+    ProjectView projectView =
+        projectView(
+            directories("foo/bar"), targets("//foo/bar:one", "//foo/bar:two", "//foo/bar:three"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:one",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/one.cc"),
+                    defines(),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:two",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/two.cc"),
+                    defines(),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:three",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/three.cc"),
+                    defines(),
+                    includes()))
+            .build();
+    List<BlazeResolveConfiguration> configurations = resolve(projectView, targetMap);
+    assertThat(configurations).hasSize(1);
+    assertThat(get(configurations, "//foo/bar:one and 2 other target(s)")).isNotNull();
+    for (BlazeResolveConfiguration configuration : configurations) {
+      assertThat(configuration.getProjectHeadersRoots().getRoots()).isEmpty();
+      assertThat(getHeaders(configuration, OCLanguageKind.CPP)).isEmpty();
+      assertThat(configuration.getCompilerMacros()).isEqualTo(macros());
+    }
+  }
+
+  @Test
+  public void testDefines() {
+    ProjectView projectView =
+        projectView(
+            directories("foo/bar"), targets("//foo/bar:one", "//foo/bar:two", "//foo/bar:three"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:one",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/one.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:two",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/two.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:three",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/three.cc"),
+                    defines("DIFFERENT=1"),
+                    includes()))
+            .build();
+    List<BlazeResolveConfiguration> configurations = resolve(projectView, targetMap);
+    assertThat(configurations).hasSize(2);
+    assertThat(get(configurations, "//foo/bar:one and 1 other target(s)").getCompilerMacros())
+        .isEqualTo(macros("SAME=1"));
+    assertThat(get(configurations, "//foo/bar:three").getCompilerMacros())
+        .isEqualTo(macros("DIFFERENT=1"));
+    for (BlazeResolveConfiguration configuration : configurations) {
+      assertThat(configuration.getProjectHeadersRoots().getRoots()).isEmpty();
+      assertThat(getHeaders(configuration, OCLanguageKind.CPP)).isEmpty();
+    }
+  }
+
+  @Test
+  public void testIncludes() {
+    ProjectView projectView =
+        projectView(
+            directories("foo/bar"), targets("//foo/bar:one", "//foo/bar:two", "//foo/bar:three"));
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:one",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/one.cc"),
+                    defines(),
+                    includes("foo/same")))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:two",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/two.cc"),
+                    defines(),
+                    includes("foo/same")))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:three",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/three.cc"),
+                    defines(),
+                    includes("foo/different")))
+            .build();
+    VirtualFile includeSame = createVirtualFile("/root/foo/same");
+    VirtualFile includeDifferent = createVirtualFile("/root/foo/different");
+    List<BlazeResolveConfiguration> configurations = resolve(projectView, targetMap);
+    assertThat(configurations).hasSize(2);
+    assertThat(
+            getHeaders(
+                get(configurations, "//foo/bar:one and 1 other target(s)"), OCLanguageKind.CPP))
+        .containsExactly(header(includeSame));
+    assertThat(getHeaders(get(configurations, "//foo/bar:three"), OCLanguageKind.CPP))
+        .containsExactly(header(includeDifferent));
+    for (BlazeResolveConfiguration configuration : configurations) {
+      assertThat(configuration.getProjectHeadersRoots().getRoots()).isEmpty();
+      assertThat(configuration.getCompilerMacros()).isEqualTo(macros());
+    }
+  }
+
+  // Test a series of permutations of labels a, b, c, d.
+  // Initial state is {a=1, b=1, c=1, d=0}, and we flip some of the 1 to 0.
+  private TargetMap incrementalUpdateTestCaseInitialTargetMap() {
+    return TargetMapBuilder.builder()
+        .addTarget(createCcToolchain())
+        .addTarget(
+            createCcTarget(
+                "//foo/bar:a",
+                Kind.CC_BINARY,
+                sources("foo/bar/a.cc"),
+                defines("SAME=1"),
+                includes()))
+        .addTarget(
+            createCcTarget(
+                "//foo/bar:b",
+                Kind.CC_BINARY,
+                sources("foo/bar/b.cc"),
+                defines("SAME=1"),
+                includes()))
+        .addTarget(
+            createCcTarget(
+                "//foo/bar:c",
+                Kind.CC_BINARY,
+                sources("foo/bar/c.cc"),
+                defines("SAME=1"),
+                includes()))
+        .addTarget(
+            createCcTarget(
+                "//foo/bar:d",
+                Kind.CC_BINARY,
+                sources("foo/bar/d.cc"),
+                defines("DIFFERENT=1"),
+                includes()))
+        .build();
+  }
+
+  // TODO(jvoung): This could be a separate Parameterized test.
+  private static final Map<List<String>, ReusedConfigurationExpectations>
+      permutationsAndExpectations =
+          ImmutableMap.<List<String>, ReusedConfigurationExpectations>builder()
+              .put(
+                  ImmutableList.of("a"),
+                  // Since we already had a config at 1 and one at 0, flipping any 1 to 0 will
+                  // always
+                  // result in reuse. The old configurations will get renamed.
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of(
+                          "//foo/bar:a and 1 other target(s)", "//foo/bar:b and 1 other target(s)"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("b"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of(
+                          "//foo/bar:a and 1 other target(s)", "//foo/bar:b and 1 other target(s)"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("c"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of(
+                          "//foo/bar:a and 1 other target(s)", "//foo/bar:c and 1 other target(s)"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("a", "b"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of("//foo/bar:a and 2 other target(s)", "//foo/bar:c"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("b", "c"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of("//foo/bar:a", "//foo/bar:b and 2 other target(s)"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("a", "c"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of("//foo/bar:a and 2 other target(s)", "//foo/bar:b"),
+                      ImmutableList.of()))
+              .put(
+                  ImmutableList.of("a", "b", "c"),
+                  new ReusedConfigurationExpectations(
+                      ImmutableList.of("//foo/bar:a and 3 other target(s)"), ImmutableList.of()))
+              .build();
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_0() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 0);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_1() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 1);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_2() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 2);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_3() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 3);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_4() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 4);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_5() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 5);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+  }
+
+  @Test
+  public void changeDefines_testIncrementalUpdate_6() {
+    Map.Entry<List<String>, ReusedConfigurationExpectations> testCase =
+        Iterables.get(permutationsAndExpectations.entrySet(), 6);
+    do_changeDefines_testIncrementalUpdate(testCase.getKey(), testCase.getValue());
+    assertThat(permutationsAndExpectations.size()).isEqualTo(7);
+  }
+
+  private void do_changeDefines_testIncrementalUpdate(
+      List<String> labelsToFlip, ReusedConfigurationExpectations expectation) {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:...:all"));
+    List<BlazeResolveConfiguration> configurations =
+        resolve(projectView, incrementalUpdateTestCaseInitialTargetMap());
+    assertThat(configurations).hasSize(2);
+    assertThat(get(configurations, "//foo/bar:a and 2 other target(s)")).isNotNull();
+    assertThat(get(configurations, "//foo/bar:d")).isNotNull();
+
+    TargetMapBuilder targetMapBuilder = TargetMapBuilder.builder().addTarget(createCcToolchain());
+    for (String target : ImmutableList.of("a", "b", "c")) {
+      if (labelsToFlip.contains(target)) {
+        targetMapBuilder.addTarget(
+            createCcTarget(
+                String.format("//foo/bar:%s", target),
+                Kind.CC_BINARY,
+                sources(String.format("foo/bar/%s.cc", target)),
+                defines("DIFFERENT=1"),
+                includes()));
+      } else {
+        targetMapBuilder.addTarget(
+            createCcTarget(
+                String.format("//foo/bar:%s", target),
+                Kind.CC_BINARY,
+                sources(String.format("foo/bar/%s.cc", target)),
+                defines("SAME=1"),
+                includes()));
+      }
+    }
+    targetMapBuilder.addTarget(
+        createCcTarget(
+            "//foo/bar:d",
+            Kind.CC_BINARY,
+            sources("foo/bar/d.cc"),
+            defines("DIFFERENT=1"),
+            includes()));
+    List<BlazeResolveConfiguration> newConfigurations =
+        resolve(projectView, targetMapBuilder.build());
+    assertReusedConfigs(configurations, newConfigurations, expectation);
+  }
+
+  @Test
+  public void changeDefinesWithSameStructure_testIncrementalUpdate() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:...:all"));
+    TargetMap targetMap = incrementalUpdateTestCaseInitialTargetMap();
+    List<BlazeResolveConfiguration> configurations = resolve(projectView, targetMap);
+    assertThat(configurations).hasSize(2);
+    assertThat(get(configurations, "//foo/bar:a and 2 other target(s)")).isNotNull();
+    assertThat(get(configurations, "//foo/bar:d")).isNotNull();
+
+    targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:a",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/a.cc"),
+                    defines("CHANGED=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:b",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/b.cc"),
+                    defines("CHANGED=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:c",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/c.cc"),
+                    defines("CHANGED=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:d",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/d.cc"),
+                    defines("DIFFERENT=1"),
+                    includes()))
+            .build();
+    List<BlazeResolveConfiguration> newConfigurations = resolve(projectView, targetMap);
+    assertThat(newConfigurations).hasSize(2);
+    assertReusedConfigs(
+        configurations,
+        newConfigurations,
+        new ReusedConfigurationExpectations(
+            ImmutableList.of("//foo/bar:d"),
+            ImmutableList.of("//foo/bar:a and 2 other target(s)")));
+  }
+
+  @Test
+  public void changeDefinesMakeAllSame_testIncrementalUpdate() {
+    ProjectView projectView = projectView(directories("foo/bar"), targets("//foo/bar:...:all"));
+    TargetMap targetMap = incrementalUpdateTestCaseInitialTargetMap();
+    List<BlazeResolveConfiguration> configurations = resolve(projectView, targetMap);
+    assertThat(configurations).hasSize(2);
+    assertThat(get(configurations, "//foo/bar:a and 2 other target(s)")).isNotNull();
+    assertThat(get(configurations, "//foo/bar:d")).isNotNull();
+
+    targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(createCcToolchain())
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:a",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/a.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:b",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/b.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:c",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/c.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .addTarget(
+                createCcTarget(
+                    "//foo/bar:d",
+                    Kind.CC_BINARY,
+                    sources("foo/bar/d.cc"),
+                    defines("SAME=1"),
+                    includes()))
+            .build();
+    List<BlazeResolveConfiguration> newConfigurations = resolve(projectView, targetMap);
+    assertThat(newConfigurations).hasSize(1);
+    // What used to be "//foo/bar:a and 2 other target(s)" will be renamed to
+    // "//foo/bar:a and 3 other target(s)" and reused.
+    assertReusedConfigs(
+        configurations,
+        newConfigurations,
+        new ReusedConfigurationExpectations(
+            ImmutableList.of("//foo/bar:a and 3 other target(s)"), ImmutableList.of()));
+  }
+
+  private static List<ArtifactLocation> sources(String... paths) {
+    return Arrays.stream(paths)
+        .map(path -> ArtifactLocation.builder().setRelativePath(path).setIsSource(true).build())
+        .collect(Collectors.toList());
+  }
+
+  private static List<String> defines(String... defines) {
+    return Arrays.asList(defines);
+  }
+
+  private static List<ExecutionRootPath> includes(String... paths) {
+    return Arrays.stream(paths).map(ExecutionRootPath::new).collect(Collectors.toList());
+  }
+
+  private static TargetIdeInfo.Builder createCcTarget(
+      String label,
+      Kind kind,
+      List<ArtifactLocation> sources,
+      List<String> defines,
+      List<ExecutionRootPath> includes) {
+    TargetIdeInfo.Builder targetInfo =
+        TargetIdeInfo.builder().setLabel(label).setKind(kind).addDependency("//:toolchain");
+    sources.forEach(targetInfo::addSource);
+    return targetInfo.setCInfo(
+        CIdeInfo.builder()
+            .addSources(sources)
+            .addLocalDefines(defines)
+            .addLocalIncludeDirectories(includes));
+  }
+
+  private static TargetIdeInfo.Builder createCcToolchain() {
+    return TargetIdeInfo.builder()
+        .setLabel("//:toolchain")
+        .setKind(Kind.CC_TOOLCHAIN)
+        .setCToolchainInfo(
+            CToolchainIdeInfo.builder().setCppExecutable(new ExecutionRootPath("cc")));
+  }
+
+  private static ListSection<DirectoryEntry> directories(String... directories) {
+    return ListSection.builder(DirectorySection.KEY)
+        .addAll(
+            Arrays.stream(directories)
+                .map(directory -> DirectoryEntry.include(WorkspacePath.createIfValid(directory)))
+                .collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ListSection<TargetExpression> targets(String... targets) {
+    return ListSection.builder(TargetSection.KEY)
+        .addAll(
+            Arrays.stream(targets).map(TargetExpression::fromString).collect(Collectors.toList()))
+        .build();
+  }
+
+  private static ProjectView projectView(
+      ListSection<DirectoryEntry> directories, ListSection<TargetExpression> targets) {
+    return ProjectView.builder().add(directories).add(targets).build();
+  }
+
+  private List<BlazeResolveConfiguration> resolve(ProjectView projectView, TargetMap targetMap) {
+    resolverResult =
+        resolver.update(
+            context,
+            workspaceRoot,
+            ProjectViewSet.builder().add(projectView).build(),
+            MockBlazeProjectDataBuilder.builder(workspaceRoot).setTargetMap(targetMap).build(),
+            resolverResult);
+    errorCollector.assertNoIssues();
+    return resolverResult.getAllConfigurations();
+  }
+
+  private static BlazeResolveConfiguration get(
+      List<BlazeResolveConfiguration> configurations, String name) {
+    List<BlazeResolveConfiguration> filteredConfigurations =
+        configurations
+            .stream()
+            .filter(c -> c.getDisplayName(false).equals(name))
+            .collect(Collectors.toList());
+    assertWithMessage(
+            String.format(
+                "%s contains %s",
+                configurations
+                    .stream()
+                    .map(c -> c.getDisplayName(false))
+                    .collect(Collectors.toList()),
+                name))
+        .that(filteredConfigurations)
+        .hasSize(1);
+    return filteredConfigurations.get(0);
+  }
+
+  private BlazeCompilerMacros macros(String... defines) {
+    return new BlazeCompilerMacros(
+        project, null, null, ImmutableList.copyOf(defines), ImmutableMap.of());
+  }
+
+  private HeadersSearchRoot header(VirtualFile include) {
+    return new IncludedHeadersRoot(project, include, false, true);
+  }
+
+  private static List<HeadersSearchRoot> getHeaders(
+      BlazeResolveConfiguration configuration, OCLanguageKind languageKind) {
+    return configuration
+        .getLibraryHeadersRoots(new OCResolveRootAndConfiguration(configuration, languageKind))
+        .getRoots();
+  }
+
+  private VirtualFile createVirtualFile(String path) {
+    VirtualFile stub = new StubVirtualFile();
+    when(mockFileSystem.findFileByIoFile(new File(path))).thenReturn(stub);
+    return stub;
+  }
+
+  private static void assertReusedConfigs(
+      List<BlazeResolveConfiguration> oldConfigurations,
+      List<BlazeResolveConfiguration> newConfigurations,
+      ReusedConfigurationExpectations expected) {
+    for (String label : expected.reusedLabels) {
+      assertWithMessage(String.format("Checking that %s is reused", label))
+          .that(get(newConfigurations, label))
+          .isSameAs(get(oldConfigurations, label));
+    }
+    for (String label : expected.notReusedLabels) {
+      assertWithMessage(String.format("Checking that %s is NOT reused", label))
+          .that(get(newConfigurations, label))
+          .isNotSameAs(get(oldConfigurations, label));
+    }
+  }
+
+  private static class ReusedConfigurationExpectations {
+    final ImmutableCollection<String> reusedLabels;
+    final ImmutableCollection<String> notReusedLabels;
+
+    ReusedConfigurationExpectations(
+        ImmutableCollection<String> reusedLabels, ImmutableCollection<String> notReusedLabels) {
+      this.reusedLabels = reusedLabels;
+      this.notReusedLabels = notReusedLabels;
+    }
+  }
+}
diff --git a/golang/BUILD b/golang/BUILD
index 16a486c..99181ab 100644
--- a/golang/BUILD
+++ b/golang/BUILD
@@ -1,26 +1,69 @@
 licenses(["notice"])  # Apache 2.0
 
+load(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
+load(
+    "//build_defs:build_defs.bzl",
+    "intellij_plugin",
+    "merged_plugin_xml",
+    "optional_plugin_xml",
+    "stamped_plugin_xml",
+)
+
 java_library(
     name = "golang",
     srcs = glob(["src/**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
         "//base",
+        "//common/experiments",
         "//intellij_platform_sdk:plugin_api",
         "//sdkcompat",
+        "//third_party/go",
         "@jsr305_annotations//jar",
     ],
 )
 
 filegroup(
     name = "plugin_xml",
-    srcs = ["src/META-INF/golang.xml"],
+    srcs = ["src/META-INF/blaze-go.xml"],
     visibility = ["//visibility:public"],
 )
 
-load(
-    "//testing:test_defs.bzl",
-    "intellij_unit_test_suite",
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "org.jetbrains.plugins.go",
+    plugin_xml = "src/META-INF/go-contents.xml",
+    visibility = ["//visibility:public"],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml",
+    srcs = [
+        "//base:plugin_xml",
+    ] + [
+        ":plugin_xml",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "golang_plugin_xml",
+    plugin_id = "com.google.idea.blaze.golang",
+    plugin_name = "com.google.idea.blaze.golang",
+    plugin_xml = "merged_plugin_xml",
+)
+
+intellij_plugin(
+    name = "golang_integration_test_plugin",
+    testonly = 1,
+    optional_plugin_xmls = [":optional_xml"],
+    plugin_xml = ":golang_plugin_xml",
+    deps = [
+        ":golang",
+    ],
 )
 
 intellij_unit_test_suite(
@@ -36,3 +79,26 @@
         "@junit//jar",
     ],
 )
+
+intellij_integration_test_suite(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    platform_prefix = "",
+    required_plugins = "com.google.idea.blaze.golang",
+    test_package_root = "com.google.idea.blaze.golang",
+    runtime_deps = [
+        ":golang_integration_test_plugin",
+    ],
+    deps = [
+        ":golang",
+        "//base",
+        "//base:integration_test_utils",
+        "//base:unit_test_utils",
+        "//common/experiments",
+        "//common/experiments:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//third_party/go:go_for_tests",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
diff --git a/golang/src/META-INF/blaze-go.xml b/golang/src/META-INF/blaze-go.xml
new file mode 100644
index 0000000..5219884
--- /dev/null
+++ b/golang/src/META-INF/blaze-go.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.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.golang.sync.AlwaysPresentGoSyncPlugin"/>
+  </extensions>
+</idea-plugin>
\ No newline at end of file
diff --git a/golang/src/META-INF/go-contents.xml b/golang/src/META-INF/go-contents.xml
new file mode 100644
index 0000000..079f1ed
--- /dev/null
+++ b/golang/src/META-INF/go-contents.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.golang.sync.BlazeGoSyncPlugin"/>
+    <SyncListener implementation="com.google.idea.blaze.golang.sync.BlazeGoSdkUpdater"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.goide">
+    <importResolver implementation="com.google.idea.blaze.golang.resolve.BlazeGoImportResolver"
+        order="first"/>
+  </extensions>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/golang/src/META-INF/golang.xml b/golang/src/META-INF/golang.xml
deleted file mode 100644
index 32948ee..0000000
--- a/golang/src/META-INF/golang.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<idea-plugin>
-
-  <extensions defaultExtensionNs="com.google.idea.blaze">
-    <SyncPlugin implementation="com.google.idea.blaze.golang.sync.BlazeGoSyncPlugin"/>
-  </extensions>
-
-</idea-plugin>
diff --git a/golang/src/com/google/idea/blaze/golang/BlazeGoSupport.java b/golang/src/com/google/idea/blaze/golang/BlazeGoSupport.java
new file mode 100644
index 0000000..fb6afe1
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/BlazeGoSupport.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.blaze.golang;
+
+import com.google.idea.common.experiments.BoolExperiment;
+
+/**
+ * Controls whether Blaze go-lang support is activated. If this is disabled, we make no attempt to
+ * resolve Blaze-specific import formats, etc.
+ *
+ * <p>If this is enabled, we override some default Go plugin behaviors.
+ */
+public class BlazeGoSupport {
+
+  public static final BoolExperiment blazeGoSupportEnabled =
+      new BoolExperiment("blaze.go.support.enabled", false);
+}
diff --git a/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoImportResolver.java b/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoImportResolver.java
new file mode 100644
index 0000000..1767d6b
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/resolve/BlazeGoImportResolver.java
@@ -0,0 +1,177 @@
+/*
+ * 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.golang.resolve;
+
+import com.goide.psi.GoImportSpec;
+import com.goide.psi.impl.imports.GoImportResolver;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+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.model.primitives.TargetName;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.golang.BlazeGoSupport;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/**
+ * Resolves go imports in a blaze workspace, of the form:
+ *
+ * <p>"[workspace_name]/path/to/blaze/package/[go_library target]"
+ *
+ * <p>Only the first non-null import candidate is considered, so all blaze-specific import handling
+ * is done in this {@link GoImportResolver}, to more easily manage priority.
+ */
+public class BlazeGoImportResolver implements GoImportResolver {
+
+  @Nullable
+  @Override
+  public PsiDirectory resolve(GoImportSpec goImportSpec) {
+    Project project = goImportSpec.getProject();
+    if (!Blaze.isBlazeProject(project) || !BlazeGoSupport.blazeGoSupportEnabled.getValue()) {
+      return null;
+    }
+    // TODO: Handle go packages whose sources are in multiple directories (requires upstream change)
+    String pathString = goImportSpec.getPath();
+    String packageName = getPackageName(pathString);
+
+    GoTarget target = findGoTarget(project, pathString, packageName);
+    if (target == null) {
+      return null;
+    }
+    switch (target.kind) {
+      case GO_LIBRARY:
+      case GO_APPENGINE_LIBRARY:
+        return resolveFile(PsiManager.getInstance(project), target.buildFile.getParentFile());
+      case PROTO_LIBRARY:
+      case GO_WRAP_CC:
+        return resolveGenfilesPath(project, target.label.blazePackage().relativePath());
+      default:
+        return null;
+    }
+  }
+
+  private static String getPackageName(String pathString) {
+    int ix = pathString.lastIndexOf('/');
+    return ix == -1 ? pathString : pathString.substring(ix + 1);
+  }
+
+  @Nullable
+  private static GoTarget findGoTarget(Project project, String importPath, String packageName) {
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    TargetName targetName = TargetName.createIfValid(packageName);
+    if (targetName == null) {
+      return null;
+    }
+    WorkspacePath workspacePath = blazePackageWorkspacePath(project, importPath);
+    if (workspacePath == null) {
+      return null;
+    }
+    Label label = Label.create(workspacePath, targetName);
+    GoTarget goTarget = GoTarget.fromTargetIdeInfo(projectData, label);
+    if (goTarget != null) {
+      return goTarget;
+    }
+    // if the target wasn't indexed, try parsing the BUILD file manually
+    return GoTarget.manuallyParseBuildFile(project, label);
+  }
+
+  @Nullable
+  private static PsiDirectory resolveGenfilesPath(Project project, String relativePath) {
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    File genfiles = projectData.blazeInfo.getGenfilesDirectory();
+    return resolveFile(PsiManager.getInstance(project), new File(genfiles, relativePath));
+  }
+
+  @Nullable
+  private static PsiDirectory resolveFile(PsiManager manager, File file) {
+    VirtualFile vf =
+        VirtualFileSystemProvider.getInstance().getSystem().findFileByPath(file.getPath());
+    return vf != null && vf.isDirectory() ? manager.findDirectory(vf) : null;
+  }
+
+  @Nullable
+  private static WorkspacePath blazePackageWorkspacePath(Project project, String importPath) {
+    String workspaceName = WorkspaceRoot.fromProject(project).directory().getName();
+    if (!importPath.startsWith(workspaceName + "/")) {
+      return null;
+    }
+    // strip first and last path components (workspace name, go package name)
+    importPath = importPath.substring(workspaceName.length() + 1);
+    int lastSeparator = importPath.lastIndexOf('/');
+    if (lastSeparator <= 0) {
+      return null;
+    }
+    return WorkspacePath.createIfValid(importPath.substring(0, lastSeparator));
+  }
+
+  private static class GoTarget {
+    final File buildFile;
+    final Kind kind;
+    final Label label;
+
+    GoTarget(File buildFile, Kind kind, Label label) {
+      this.buildFile = buildFile;
+      this.kind = kind;
+      this.label = label;
+    }
+
+    @Nullable
+    static GoTarget fromTargetIdeInfo(BlazeProjectData projectData, Label label) {
+      TargetIdeInfo target = projectData.targetMap.get(TargetKey.forPlainTarget(label));
+      if (target == null) {
+        return null;
+      }
+      File buildFile = projectData.artifactLocationDecoder.decode(target.buildFile);
+      return new GoTarget(buildFile, target.kind, target.key.label);
+    }
+
+    @Nullable
+    static GoTarget manuallyParseBuildFile(Project project, Label label) {
+      PsiElement psiElement = BuildReferenceManager.getInstance(project).resolveLabel(label);
+      if (!(psiElement instanceof FuncallExpression)) {
+        return null;
+      }
+      FuncallExpression funcall = (FuncallExpression) psiElement;
+      Kind kind = funcall.getRuleKind();
+      BuildFile parentFile = funcall.getContainingFile();
+      return kind == null || parentFile == null
+          ? null
+          : new GoTarget(parentFile.getFile(), kind, label);
+    }
+  }
+}
diff --git a/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java b/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java
deleted file mode 100644
index ba42571..0000000
--- a/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java
+++ /dev/null
@@ -1,59 +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.golang.sdk;
-
-import com.intellij.execution.configurations.PathEnvironmentVariableUtil;
-import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-import java.io.File;
-import java.io.IOException;
-import javax.annotation.Nullable;
-
-/**
- * Go-lang SDK utility methods.
- *
- * <p>TODO: Remove this, and reference go-lang plugin source code directly.
- */
-public class GoSdkUtil {
-
-  @Nullable
-  public static VirtualFile suggestSdkDirectory() {
-    String fromEnv = suggestSdkDirectoryPathFromEnv();
-    if (fromEnv != null) {
-      return LocalFileSystem.getInstance().findFileByPath(fromEnv);
-    }
-    return LocalFileSystem.getInstance().findFileByPath("/usr/local/go");
-  }
-
-  @Nullable
-  private static String suggestSdkDirectoryPathFromEnv() {
-    File fileFromPath = PathEnvironmentVariableUtil.findInPath("go");
-    if (fileFromPath != null) {
-      File canonicalFile;
-      try {
-        canonicalFile = fileFromPath.getCanonicalFile();
-        String path = canonicalFile.getPath();
-        if (path.endsWith("bin/go")) {
-          return StringUtil.trimEnd(path, "bin/go");
-        }
-      } catch (IOException e) {
-        // if it can't be found, just silently return null
-      }
-    }
-    return null;
-  }
-}
diff --git a/golang/src/com/google/idea/blaze/golang/sync/AlwaysPresentGoSyncPlugin.java b/golang/src/com/google/idea/blaze/golang/sync/AlwaysPresentGoSyncPlugin.java
new file mode 100644
index 0000000..a97a5f6
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/sync/AlwaysPresentGoSyncPlugin.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.golang.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection;
+import com.google.idea.blaze.base.projectview.section.sections.WorkspaceTypeSection;
+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;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.util.PlatformUtils;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Unlike most of the go-specific code, will be run even if the JetBrains Go plugin isn't enabled.
+ */
+public class AlwaysPresentGoSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  private static final String GO_PLUGIN_ID = "org.jetbrains.plugins.go";
+  private static final String OLD_GO_PLUGIN_ID = "ro.redeul.google.go";
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    return ImmutableSet.of(LanguageClass.GO);
+  }
+
+  @Override
+  public boolean validate(
+      Project project, BlazeContext context, BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)
+        || PluginUtils.isPluginEnabled(GO_PLUGIN_ID)) {
+      return true;
+    }
+    if (PlatformUtils.isIdeaCommunity() && !ApplicationManager.getApplication().isUnitTestMode()) {
+      IssueOutput.error(
+              String.format(
+                  "Go is no longer supported by the %s plugin with IntelliJ Community Edition.\n"
+                      + "Please install Ultimate Edition and upgrade to the JetBrains Go plugin",
+                  Blaze.defaultBuildSystemName()))
+          .submit(context);
+    }
+    if (PluginUtils.isPluginEnabled(OLD_GO_PLUGIN_ID)) {
+      String error =
+          String.format(
+              "The currently installed Go plugin is no longer supported by the %s plugin.\n"
+                  + "Click here to install the new JetBrains Go plugin and restart.",
+              Blaze.defaultBuildSystemName());
+      IssueOutput.error(error)
+          .navigatable(
+              new NavigatableAdapter() {
+                @Override
+                public void navigate(boolean requestFocus) {
+                  PluginManager.disablePlugin(OLD_GO_PLUGIN_ID);
+                  PluginUtils.installOrEnablePlugin(GO_PLUGIN_ID);
+                }
+              })
+          .submit(context);
+      return true;
+    }
+    IssueOutput.error(
+            "Go support requires the Go plugin. Click here to install/enable the JetBrains Go "
+                + "plugin, then restart the IDE")
+        .navigatable(PluginUtils.installOrEnablePluginNavigable(GO_PLUGIN_ID))
+        .submit(context);
+    return true;
+  }
+
+  @Override
+  public boolean validateProjectView(
+      @Nullable Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!workspaceLanguageSettings.isWorkspaceType(WorkspaceType.GO)) {
+      return true;
+    }
+    ProjectViewFile topLevelProjectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    String msg =
+        "Go workspace_type is no longer supported. Please add 'go' to "
+            + "additional_languages instead";
+    boolean fixable =
+        project != null
+            && topLevelProjectViewFile != null
+            && topLevelProjectViewFile.projectView.getScalarValue(WorkspaceTypeSection.KEY)
+                == WorkspaceType.GO;
+    msg += fixable ? ". Click here to fix your .blazeproject and resync." : ", then resync.";
+    IssueOutput.error(msg)
+        .navigatable(
+            !fixable
+                ? null
+                : new NavigatableAdapter() {
+                  @Override
+                  public void navigate(boolean requestFocus) {
+                    fixLanguageSupport(project);
+                  }
+                })
+        .submit(context);
+    return false;
+  }
+
+  private static void fixLanguageSupport(Project project) {
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              removeGoWorkspaceType(builder);
+              addToAdditionalLanguages(builder);
+              return true;
+            });
+    if (edit == null) {
+      Messages.showErrorDialog(
+          "Could not modify project view. Check for errors in your project view and try again",
+          "Error");
+      return;
+    }
+    edit.apply();
+
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
+  }
+
+  private static void removeGoWorkspaceType(ProjectView.Builder builder) {
+    ScalarSection<WorkspaceType> section = builder.getLast(WorkspaceTypeSection.KEY);
+    if (section != null && section.getValue() == WorkspaceType.GO) {
+      builder.remove(section);
+    }
+  }
+
+  private static void addToAdditionalLanguages(ProjectView.Builder builder) {
+    ListSection<LanguageClass> existingSection = builder.getLast(AdditionalLanguagesSection.KEY);
+    builder.replace(
+        existingSection,
+        ListSection.update(AdditionalLanguagesSection.KEY, existingSection).add(LanguageClass.GO));
+  }
+}
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java
index 693c5f7..bbc8f9d 100644
--- a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java
@@ -33,6 +33,14 @@
 
   static boolean isGoLibrary(Library library) {
     String name = library.getName();
-    return name != null && name.startsWith(BlazeGoSyncPlugin.GO_LIBRARY_PREFIX);
+    if (name == null) {
+      return false;
+    }
+    for (String prefix : BlazeGoSyncPlugin.GO_LIBRARY_PREFIXES) {
+      if (name.startsWith(prefix)) {
+        return true;
+      }
+    }
+    return false;
   }
 }
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSdkUpdater.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSdkUpdater.java
new file mode 100644
index 0000000..7b8d621
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSdkUpdater.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.golang.sync;
+
+import com.goide.project.GoModuleSettings;
+import com.goide.sdk.GoSdk;
+import com.goide.sdk.GoSdkService;
+import com.goide.sdk.GoSdkUtil;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+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.base.sync.data.BlazeDataStorage;
+import com.google.idea.sdkcompat.transactions.Transactions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import javax.annotation.Nullable;
+
+/**
+ * Runs after sync. Sets up a Go SDK library if Go-lang is active, and there's no existing library
+ * set up.
+ */
+public class BlazeGoSdkUpdater extends SyncListener.Adapter {
+
+  @Override
+  public void onSyncComplete(
+      Project project,
+      BlazeContext context,
+      BlazeImportSettings importSettings,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
+      SyncResult syncResult) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
+      return;
+    }
+    Module workspaceModule = getWorkspaceModule(project);
+    if (workspaceModule == null || GoSdkService.getInstance(project).isGoModule(workspaceModule)) {
+      return;
+    }
+    String sdkPath = getOrSuggestSdkPath(workspaceModule);
+    if (sdkPath != null) {
+      setSdkPath(project, workspaceModule, sdkPath);
+    }
+  }
+
+  private static void setSdkPath(Project project, Module workspaceModule, String path) {
+    Transactions.submitTransactionAndWait(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(
+                    () -> {
+                      GoSdkService.getInstance(project).setSdkHomePath(path);
+                      GoModuleSettings.getInstance(workspaceModule).setGoSupportEnabled(true);
+                    }));
+  }
+
+  @Nullable
+  private static String getOrSuggestSdkPath(Module module) {
+    GoSdk sdk = GoSdkService.getInstance(module.getProject()).getSdk(module);
+    if (sdk != GoSdk.NULL) {
+      return sdk.getHomePath();
+    }
+    VirtualFile defaultSdk = GoSdkUtil.suggestSdkDirectory();
+    return defaultSdk != null ? defaultSdk.getPath() : null;
+  }
+
+  @Nullable
+  private static Module getWorkspaceModule(Project project) {
+    return ReadAction.compute(
+        () ->
+            ModuleManager.getInstance(project)
+                .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME));
+  }
+}
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
index d65c8e6..0905ac1 100644
--- a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
@@ -15,40 +15,25 @@
  */
 package com.google.idea.blaze.golang.sync;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
-import com.google.idea.blaze.base.plugin.PluginUtils;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 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.blaze.base.sync.GenericSourceFolderProvider;
-import com.google.idea.blaze.base.sync.SourceFolderProvider;
-import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
-import com.google.idea.blaze.golang.sdk.GoSdkUtil;
-import com.google.idea.sdkcompat.transactions.Transactions;
-import com.intellij.openapi.application.ApplicationManager;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
-import com.intellij.openapi.module.ModuleType;
-import com.intellij.openapi.module.ModuleTypeManager;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.projectRoots.ProjectJdkTable;
-import com.intellij.openapi.projectRoots.Sdk;
-import com.intellij.openapi.projectRoots.SdkType;
-import com.intellij.openapi.projectRoots.SdkTypeId;
-import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
 import com.intellij.openapi.roots.ModifiableRootModel;
-import com.intellij.openapi.roots.ProjectRootManager;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
+import com.intellij.openapi.vfs.VfsUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.util.List;
 import java.util.Set;
@@ -57,46 +42,16 @@
 /** Supports golang. */
 public class BlazeGoSyncPlugin extends BlazeSyncPlugin.Adapter {
 
-  static final String GO_LIBRARY_PREFIX = "GOPATH";
-  private static final String GO_MODULE_TYPE_ID = "GO_MODULE";
-  private static final String GO_PLUGIN_ID = "ro.redeul.google.go";
-  private static final String GO_SDK_TYPE_ID = "Go SDK";
+  private static final Logger logger = Logger.getInstance(BlazeGoSyncPlugin.class);
 
-  @Nullable
-  @Override
-  public ModuleType<?> getWorkspaceModuleType(WorkspaceType workspaceType) {
-    if (workspaceType == WorkspaceType.GO) {
-      return ModuleTypeManager.getInstance().findByID(GO_MODULE_TYPE_ID);
-    }
-    return null;
-  }
+  private static final BoolExperiment refreshExecRoot =
+      new BoolExperiment("refresh.exec.root.golang", true);
 
-  @Override
-  public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
-    return ImmutableList.of(WorkspaceType.GO);
-  }
+  static final ImmutableSet<String> GO_LIBRARY_PREFIXES = ImmutableSet.of("GOPATH", "Go SDK");
 
   @Override
   public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
-    if (workspaceType == WorkspaceType.GO) {
-      return ImmutableSet.of(LanguageClass.GO);
-    }
-    return ImmutableSet.of();
-  }
-
-  @Nullable
-  @Override
-  public WorkspaceType getDefaultWorkspaceType() {
-    return WorkspaceType.GO;
-  }
-
-  @Nullable
-  @Override
-  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
-    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.GO)) {
-      return null;
-    }
-    return GenericSourceFolderProvider.INSTANCE;
+    return ImmutableSet.of(LanguageClass.GO);
   }
 
   @Override
@@ -129,25 +84,14 @@
       }
     }
 
-    String moduleLibraryName =
-        String.format("%s <%s>", GO_LIBRARY_PREFIX, BlazeDataStorage.WORKSPACE_MODULE_NAME);
-    Library goModuleLibrary =
-        registrar.getLibraryTable(project).getLibraryByName(moduleLibraryName);
-    if (goModuleLibrary != null) {
-      libraries.add(goModuleLibrary);
+    for (Library lib : registrar.getLibraryTable(project).getLibraries()) {
+      if (BlazeGoLibrarySource.isGoLibrary(lib)) {
+        libraries.add(lib);
+      }
     }
     return libraries;
   }
 
-  /**
-   * By default the Go plugin will create duplicate copies of project libraries, one for each
-   * module. We only care about library associated with the workspace module.
-   */
-  static boolean isGoLibraryForModule(Library library, String moduleName) {
-    String name = library.getName();
-    return name != null && name.equals("GOPATH <" + moduleName + ">");
-  }
-
   @Nullable
   @Override
   public LibrarySource getLibrarySource(
@@ -159,62 +103,28 @@
   }
 
   @Override
-  public boolean validateProjectView(
-      @Nullable Project project,
-      BlazeContext context,
-      ProjectViewSet projectViewSet,
-      WorkspaceLanguageSettings workspaceLanguageSettings) {
-    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
-      return true;
-    }
-    if (!PluginUtils.isPluginEnabled(GO_PLUGIN_ID)) {
-      IssueOutput.error("Go plugin needed for Go language support.")
-          .navigatable(PluginUtils.installOrEnablePluginNavigable(GO_PLUGIN_ID))
-          .submit(context);
-      return false;
-    }
-    return true;
-  }
-
-  @Override
-  public void updateProjectSdk(
-      Project project,
-      BlazeContext context,
-      ProjectViewSet projectViewSet,
-      BlazeVersionData blazeVersionData,
-      BlazeProjectData blazeProjectData) {
-    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.GO)) {
+  public void refreshVirtualFileSystem(BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
       return;
     }
-    Sdk currentSdk = ProjectRootManager.getInstance(project).getProjectSdk();
-    if (currentSdk != null && currentSdk.getSdkType().getName().equals(GO_SDK_TYPE_ID)) {
+    if (!refreshExecRoot.getValue()) {
       return;
     }
-    Sdk sdk = getOrCreateGoSdk();
-    if (sdk != null) {
-      setProjectSdk(project, sdk);
-    }
+    long start = System.currentTimeMillis();
+    refreshExecRoot(blazeProjectData);
+    long end = System.currentTimeMillis();
+    logger.info(String.format("Refreshing execution root took: %d ms", (end - start)));
   }
 
-  @Nullable
-  private static Sdk getOrCreateGoSdk() {
-    ProjectJdkTable sdkTable = ProjectJdkTable.getInstance();
-    SdkTypeId type = sdkTable.getSdkTypeByName(GO_SDK_TYPE_ID);
-    List<Sdk> sdk = sdkTable.getSdksOfType(type);
-    if (!sdk.isEmpty()) {
-      return sdk.get(0);
+  private static void refreshExecRoot(BlazeProjectData blazeProjectData) {
+    // recursive refresh of the blaze execution root. This is required because our blaze aspect
+    // can't yet tell us exactly which genfiles are required to resolve the project.
+    VirtualFile execRoot =
+        VirtualFileSystemProvider.getInstance()
+            .getSystem()
+            .refreshAndFindFileByIoFile(blazeProjectData.blazeInfo.getExecutionRoot());
+    if (execRoot != null) {
+      VfsUtil.markDirtyAndRefresh(false, true, true, execRoot);
     }
-    VirtualFile defaultSdk = GoSdkUtil.suggestSdkDirectory();
-    if (defaultSdk != null) {
-      return SdkConfigurationUtil.createAndAddSDK(defaultSdk.getPath(), (SdkType) type);
-    }
-    return null;
-  }
-
-  private static void setProjectSdk(Project project, Sdk sdk) {
-    Transactions.submitTransactionAndWait(
-        () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(() -> ProjectRootManager.getInstance(project).setProjectSdk(sdk)));
   }
 }
diff --git a/golang/tests/integrationtests/com/google/idea/blaze/golang/resolve/BlazeGoImportResolverTest.java b/golang/tests/integrationtests/com/google/idea/blaze/golang/resolve/BlazeGoImportResolverTest.java
new file mode 100644
index 0000000..1dce235
--- /dev/null
+++ b/golang/tests/integrationtests/com/google/idea/blaze/golang/resolve/BlazeGoImportResolverTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.golang.resolve;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.goide.psi.GoFile;
+import com.goide.psi.GoImportSpec;
+import com.goide.psi.GoTypeReferenceExpression;
+import com.goide.psi.GoTypeSpec;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.golang.BlazeGoSupport;
+import com.google.idea.common.experiments.ExperimentService;
+import com.google.idea.common.experiments.MockExperimentService;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeGoImportResolver}. */
+@RunWith(JUnit4.class)
+public class BlazeGoImportResolverTest extends BlazeIntegrationTestCase {
+
+  @Before
+  public void init() {
+    MockExperimentService experimentService = new MockExperimentService();
+    experimentService.setExperiment(BlazeGoSupport.blazeGoSupportEnabled, true);
+    registerApplicationComponent(ExperimentService.class, experimentService);
+  }
+
+  @Test
+  public void testGoPluginEnabled() {
+    assertThat(PluginUtils.isPluginEnabled("org.jetbrains.plugins.go")).isTrue();
+  }
+
+  @Test
+  public void testGoLibraryPackageResolves() {
+    setProjectTargets(
+        TargetIdeInfo.builder()
+            .setKind(Kind.GO_LIBRARY)
+            .setLabel("//package/path/foo:bar")
+            .setBuildFile(sourceRoot("package/path/foo/BUILD"))
+            .build());
+    GoFile barFile =
+        (GoFile)
+            workspace.createPsiFile(
+                new WorkspacePath("package/path/foo/baz.go"),
+                "package bar",
+                "type Struct struct {}");
+    GoFile referencingFile =
+        (GoFile)
+            workspace.createPsiFile(
+                new WorkspacePath("different/package/path/gofile.go"),
+                "package bar",
+                "import \"workspace/package/path/foo/bar\"",
+                "",
+                "type Context interface {",
+                "  Method() (x bar.Struct)",
+                "}");
+
+    GoImportSpec importSpec =
+        PsiUtils.findFirstChildOfClassRecursive(referencingFile, GoImportSpec.class);
+    assertThat(importSpec).isNotNull();
+    assertThat(importSpec.resolve()).isEqualTo(barFile.getParent());
+
+    GoTypeReferenceExpression typeReference =
+        PsiUtils.findLastChildOfClassRecursive(referencingFile, GoTypeReferenceExpression.class);
+    GoTypeSpec typeSpec = PsiUtils.findFirstChildOfClassRecursive(barFile, GoTypeSpec.class);
+    assertThat(typeReference).isNotNull();
+    assertThat(typeReference.resolve()).isEqualTo(typeSpec);
+  }
+
+  private void setProjectTargets(TargetIdeInfo... targets) {
+    TargetMapBuilder targetMap = TargetMapBuilder.builder();
+    Arrays.stream(targets).forEach(targetMap::addTarget);
+    BlazeProjectData projectData =
+        MockBlazeProjectDataBuilder.builder(workspaceRoot).setTargetMap(targetMap.build()).build();
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(projectData));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+}
diff --git a/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java b/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
index fe0cf37..f6ddb27 100644
--- a/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
+++ b/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
@@ -57,31 +57,8 @@
 
     syncPluginEp = registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
     syncPluginEp.registerExtension(new BlazeGoSyncPlugin());
-    context = new BlazeContext();
-    context.addOutputSink(IssueOutput.class, errorCollector);
-  }
-
-  @Test
-  public void testGoWorkspaceTypeSupported() {
-    ProjectViewSet projectViewSet =
-        ProjectViewSet.builder()
-            .add(
-                ProjectView.builder()
-                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.GO))
-                    .build())
-            .build();
-    WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
-    errorCollector.assertNoIssues();
-    assertThat(workspaceLanguageSettings)
-        .isEqualTo(
-            new WorkspaceLanguageSettings(
-                WorkspaceType.GO, ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.GO)));
-  }
-
-  @Test
-  public void testGoNotAValidAdditionalLanguage() {
-    // add a java sync plugin so we have another workspace type available
+    syncPluginEp.registerExtension(new AlwaysPresentGoSyncPlugin());
+    // At least one sync plugin providing a default workspace type must be present
     syncPluginEp.registerExtension(
         new BlazeSyncPlugin.Adapter() {
           @Override
@@ -100,7 +77,27 @@
             return WorkspaceType.JAVA;
           }
         });
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
 
+  @Test
+  public void testGoWorkspaceTypeError() {
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.GO))
+                    .build())
+            .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
+    errorCollector.assertIssueContaining("Workspace type 'go' is not supported by this plugin");
+  }
+
+  @Test
+  public void testGoAdditionalLanguageSupported() {
     ProjectViewSet projectViewSet =
         ProjectViewSet.builder()
             .add(
@@ -112,7 +109,7 @@
     WorkspaceLanguageSettings workspaceLanguageSettings =
         LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
-    errorCollector.assertIssueContaining(
-        "Language 'go' is not supported for this plugin with workspace type: 'java'");
+    errorCollector.assertNoIssues();
+    assertThat(workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)).isTrue();
   }
 }
diff --git a/ijwb/BUILD b/ijwb/BUILD
index ea9be4f..51cbc5d 100644
--- a/ijwb/BUILD
+++ b/ijwb/BUILD
@@ -8,9 +8,20 @@
     "//build_defs:build_defs.bzl",
     "intellij_plugin",
     "merged_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(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
@@ -19,7 +30,6 @@
         "//base:plugin_xml",
         "//golang:plugin_xml",
         "//java:plugin_xml",
-        "//plugin_dev:plugin_xml",
         "//python:plugin_xml",
         "//scala:plugin_xml",
     ],
@@ -38,7 +48,7 @@
     changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.ijwb",
-    plugin_name = "IntelliJ with Bazel",
+    plugin_name = "Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
     version = VERSION,
@@ -53,21 +63,25 @@
     runtime_deps = [
         "//golang",
         "//python",
+        "//scala",
         "//terminal",
     ],
     deps = [
         "//base",
+        "//common/experiments",
         "//intellij_platform_sdk:plugin_api",
         "//java",
-        "//scala",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
 
 OPTIONAL_PLUGIN_XMLS = [
-    "//scala:optional_xml",
+    "//golang:optional_xml",
+    "//java:optional_xml",
+    "//plugin_dev:optional_xml",
     "//python:optional_xml",
+    "//scala:optional_xml",
     "//terminal:optional_xml",
 ]
 
@@ -80,10 +94,33 @@
     ],
 )
 
-load(
-    "//testing:test_defs.bzl",
-    "intellij_integration_test_suite",
-    "intellij_unit_test_suite",
+repackaged_files(
+    name = "plugin_jar",
+    srcs = [":ijwb_bazel"],
+    prefix = "ijwb/lib",
+)
+
+repackaged_files(
+    name = "aspect_directory",
+    srcs = ["//aspect:aspect_files"],
+    prefix = "ijwb/aspect",
+)
+
+intellij_plugin_debug_target(
+    name = "ijwb_bazel_dev",
+    deps = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+)
+
+plugin_deploy_zip(
+    name = "ijwb_bazel_zip",
+    srcs = [
+        ":aspect_directory",
+        ":plugin_jar",
+    ],
+    zip_filename = "ijwb_bazel.zip",
 )
 
 intellij_unit_test_suite(
@@ -105,7 +142,7 @@
 intellij_integration_test_suite(
     name = "integration_tests",
     srcs = glob(["tests/integrationtests/**/*.java"]),
-    required_plugins = "com.google.idea.blaze.ijwb",
+    required_plugins = "com.google.idea.bazel.ijwb",
     test_package_root = "com.google.idea.blaze.ijwb",
     runtime_deps = [
         ":ijwb_bazel",
diff --git a/ijwb/src/META-INF/ijwb.xml b/ijwb/src/META-INF/ijwb.xml
index 805e46e..8e22848 100644
--- a/ijwb/src/META-INF/ijwb.xml
+++ b/ijwb/src/META-INF/ijwb.xml
@@ -27,6 +27,8 @@
     <SyncPlugin implementation="com.google.idea.blaze.ijwb.typescript.BlazeTypescriptSyncPlugin"/>
     <SyncPlugin implementation="com.google.idea.blaze.ijwb.dart.BlazeDartSyncPlugin"/>
     <JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.android.BlazeAndroidLiteJavaSyncAugmenter"/>
+    <PrefetchFileSource implementation="com.google.idea.blaze.ijwb.javascript.JavascriptPrefetchFileSource"/>
+    <PrefetchFileSource implementation="com.google.idea.blaze.ijwb.typescript.TypescriptPrefetchFileSource"/>
   </extensions>
 
 </idea-plugin>
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
index 5f3e545..606c768 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
@@ -29,7 +29,6 @@
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
-import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -62,7 +61,7 @@
     }
     return new LibrarySource.Adapter() {
       @Override
-      public Collection<? extends BlazeLibrary> getLibraries() {
+      public List<? extends BlazeLibrary> getLibraries() {
         return ImmutableList.of(sdkLibrary);
       }
     };
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/javascript/JavascriptPrefetchFileSource.java b/ijwb/src/com/google/idea/blaze/ijwb/javascript/JavascriptPrefetchFileSource.java
new file mode 100644
index 0000000..81d95e4
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/javascript/JavascriptPrefetchFileSource.java
@@ -0,0 +1,98 @@
+/*
+ * 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.ijwb.javascript;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+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.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/** Declare that js files should be prefetched. */
+public class JavascriptPrefetchFileSource implements PrefetchFileSource {
+
+  private static final BoolExperiment prefetchAllJsSources =
+      new BoolExperiment("prefetch.all.js.sources", true);
+
+  @Override
+  public void addFilesToPrefetch(
+      Project project,
+      ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
+      BlazeProjectData blazeProjectData,
+      Set<File> files) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVASCRIPT)
+        || !blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)) {
+      return;
+    }
+    if (!prefetchAllJsSources.getValue()) {
+      return;
+    }
+    // Prefetch all non-project js source files found during sync
+    Predicate<ArtifactLocation> shouldPrefetch =
+        location -> {
+          if (!location.isSource || location.isExternal) {
+            return false;
+          }
+          WorkspacePath path = WorkspacePath.createIfValid(location.relativePath);
+          if (path == null || importRoots.containsWorkspacePath(path)) {
+            return false;
+          }
+          String extension = FileUtil.getExtension(path.relativePath());
+          return prefetchFileExtensions().contains(extension);
+        };
+    List<File> sourceFiles =
+        blazeProjectData
+            .targetMap
+            .targets()
+            .stream()
+            .map(JavascriptPrefetchFileSource::getJsSources)
+            .flatMap(Collection::stream)
+            .filter(shouldPrefetch)
+            .map(blazeProjectData.artifactLocationDecoder::decode)
+            .collect(Collectors.toList());
+    files.addAll(sourceFiles);
+  }
+
+  @Override
+  public Set<String> prefetchFileExtensions() {
+    return ImmutableSet.of("js", "html", "css", "gss");
+  }
+
+  private static Collection<ArtifactLocation> getJsSources(TargetIdeInfo target) {
+    if (target.jsIdeInfo != null) {
+      return target.jsIdeInfo.sources;
+    }
+    if (target.kind.languageClass == LanguageClass.JAVASCRIPT) {
+      return target.sources;
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
index 8d8ab1a..016f0cb 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
@@ -19,9 +19,11 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.async.process.ExternalTask;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 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.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
@@ -54,6 +56,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
+import java.util.Optional;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -89,7 +92,7 @@
 
     Set<Label> tsConfigTargets = getTsConfigTargets(projectViewSet);
     if (tsConfigTargets.isEmpty()) {
-      invalidProjectViewError(context);
+      invalidProjectViewError(context, Blaze.getBuildSystemProvider(project));
       return;
     }
 
@@ -104,7 +107,12 @@
                       Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
                       BlazeCommandName.RUN)
                   .addTargets(new ArrayList<>(tsConfigTargets))
-                  .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                  .addBlazeFlags(
+                      BlazeFlags.blazeFlags(
+                          project,
+                          projectViewSet,
+                          BlazeCommandName.RUN,
+                          BlazeInvocationContext.Sync))
                   .build();
 
           int retVal =
@@ -162,26 +170,30 @@
 
     // Must have either both typescript and ts_config_rules or neither
     if (typescriptActive ^ !getTsConfigTargets(projectViewSet).isEmpty()) {
-      invalidProjectViewError(context);
+      invalidProjectViewError(context, Blaze.getBuildSystemProvider(project));
       return false;
     }
 
     return true;
   }
 
-  private void invalidProjectViewError(BlazeContext context) {
-    IssueOutput.error(
-            "For Typescript support you must add both `additional_languages`: "
-                + "typescript and the `ts_config_rules` attribute.")
-        .submit(context);
+  private void invalidProjectViewError(
+      BlazeContext context, BuildSystemProvider buildSystemProvider) {
+    String errorNote =
+        "For Typescript support you must add both `additional_languages: typescript`"
+            + " and the `ts_config_rules` attribute.";
+    String documentationUrl =
+        buildSystemProvider.getLanguageSupportDocumentationUrl("dynamic-languages-typescript");
+    if (documentationUrl != null) {
+      errorNote += String.format("<p>See <a href=\"%1$s\">%1$s</a>.", documentationUrl);
+    }
+    IssueOutput.error(errorNote).submit(context);
   }
 
   private static Set<Label> getTsConfigTargets(ProjectViewSet projectViewSet) {
-    Label oldSectionType = projectViewSet.getScalarValue(TsConfigRuleSection.KEY);
+    Optional<Label> oldSectionType = projectViewSet.getScalarValue(TsConfigRuleSection.KEY);
     Set<Label> labels = new LinkedHashSet<>(projectViewSet.listItems(TsConfigRulesSection.KEY));
-    if (oldSectionType != null) {
-      labels.add(oldSectionType);
-    }
+    oldSectionType.ifPresent(labels::add);
     return labels;
   }
 
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/TypescriptPrefetchFileSource.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TypescriptPrefetchFileSource.java
new file mode 100644
index 0000000..9a57919
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TypescriptPrefetchFileSource.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.ijwb.typescript;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+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.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/** Declare that ts files should be prefetched. */
+public class TypescriptPrefetchFileSource implements PrefetchFileSource {
+
+  private static final BoolExperiment prefetchAllTsSources =
+      new BoolExperiment("prefetch.all.ts.sources", true);
+
+  @Override
+  public void addFilesToPrefetch(
+      Project project,
+      ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
+      BlazeProjectData blazeProjectData,
+      Set<File> files) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)
+        || !prefetchAllTsSources.getValue()) {
+      return;
+    }
+    // Prefetch all non-project ts source files found during sync
+    Predicate<ArtifactLocation> shouldPrefetch =
+        location -> {
+          if (!location.isSource || location.isExternal) {
+            return false;
+          }
+          WorkspacePath path = WorkspacePath.createIfValid(location.relativePath);
+          if (path == null || importRoots.containsWorkspacePath(path)) {
+            return false;
+          }
+          String extension = FileUtil.getExtension(path.relativePath());
+          return prefetchFileExtensions().contains(extension);
+        };
+    List<File> sourceFiles =
+        blazeProjectData
+            .targetMap
+            .targets()
+            .stream()
+            .map(TypescriptPrefetchFileSource::getJsSources)
+            .flatMap(Collection::stream)
+            .filter(shouldPrefetch)
+            .map(blazeProjectData.artifactLocationDecoder::decode)
+            .collect(Collectors.toList());
+    files.addAll(sourceFiles);
+  }
+
+  @Override
+  public Set<String> prefetchFileExtensions() {
+    return ImmutableSet.of("ts", "tsx");
+  }
+
+  private static Collection<ArtifactLocation> getJsSources(TargetIdeInfo target) {
+    if (target.tsIdeInfo != null) {
+      return target.tsIdeInfo.sources;
+    }
+    if (target.kind.languageClass == LanguageClass.TYPESCRIPT) {
+      return target.sources;
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/prefetch/IntelliJBazelPrefetchFileSourceTest.java b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/prefetch/IntelliJBazelPrefetchFileSourceTest.java
new file mode 100644
index 0000000..6fee570
--- /dev/null
+++ b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/prefetch/IntelliJBazelPrefetchFileSourceTest.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.ijwb.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 the IntelliJ Bazel plugin. */
+@RunWith(JUnit4.class)
+public class IntelliJBazelPrefetchFileSourceTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testPrefetchedExtensions() {
+    assertThat(PrefetchFileSource.getAllPrefetchFileExtensions())
+        .containsExactly("java", "proto", "js", "html", "css", "gss", "ts", "tsx");
+  }
+}
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
index aa90aa5..7062bd1 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
@@ -68,6 +68,11 @@
           public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
             return ImmutableSet.of(LanguageClass.JAVA);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
         });
 
     context = new BlazeContext();
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
index d8a27e3..807f5a6 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
@@ -68,6 +68,11 @@
           public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
             return ImmutableSet.of(LanguageClass.JAVA);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
         });
 
     context = new BlazeContext();
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
index da42baf..de61e08 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
@@ -54,6 +54,13 @@
     ExtensionPointImpl<BlazeSyncPlugin> ep =
         registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
     ep.registerExtension(new BlazeJavascriptSyncPlugin());
+    ep.registerExtension(
+        new BlazeSyncPlugin.Adapter() {
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
+        });
 
     context = new BlazeContext();
     context.addOutputSink(IssueOutput.class, errorCollector);
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
index 1975ae8..1db8e99 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
@@ -70,6 +70,11 @@
           public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
             return ImmutableSet.of(LanguageClass.JAVA);
           }
+
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
         });
 
     context = new BlazeContext();
diff --git a/intellij_platform_sdk/BUILD b/intellij_platform_sdk/BUILD
index d20389e..fa127ea 100644
--- a/intellij_platform_sdk/BUILD
+++ b/intellij_platform_sdk/BUILD
@@ -17,25 +17,51 @@
 )
 
 config_setting(
+    name = "intellij-ue-latest",
+    values = {
+        "define": "ij_product=intellij-ue-latest",
+    },
+)
+
+config_setting(
     name = "intellij-beta",
     values = {
         "define": "ij_product=intellij-beta",
     },
 )
 
-# IntelliJ CE 2017.1.1
 config_setting(
-    name = "intellij-2017.1.1",
+    name = "intellij-ue-beta",
     values = {
-        "define": "ij_product=intellij-2017.1.1",
+        "define": "ij_product=intellij-ue-beta",
     },
 )
 
-# IntelliJ CE 2016.3.1
 config_setting(
-    name = "intellij-2016.3.1",
+    name = "intellij-2017.2.2",
     values = {
-        "define": "ij_product=intellij-2016.3.1",
+        "define": "ij_product=intellij-2017.2.2",
+    },
+)
+
+config_setting(
+    name = "intellij-ue-2017.2.2",
+    values = {
+        "define": "ij_product=intellij-ue-2017.2.2",
+    },
+)
+
+config_setting(
+    name = "intellij-2017.1.5",
+    values = {
+        "define": "ij_product=intellij-2017.1.5",
+    },
+)
+
+config_setting(
+    name = "intellij-ue-2017.1.5",
+    values = {
+        "define": "ij_product=intellij-ue-2017.1.5",
     },
 )
 
@@ -53,15 +79,13 @@
     },
 )
 
-# Android Studio 2.3.0.8
 config_setting(
-    name = "android-studio-2.3.0.8",
+    name = "android-studio-3.0.0.9",
     values = {
-        "define": "ij_product=android-studio-2.3.0.8",
+        "define": "ij_product=android-studio-3.0.0.9",
     },
 )
 
-# Android Studio 2.3.1.0
 config_setting(
     name = "android-studio-2.3.1.0",
     values = {
@@ -83,7 +107,6 @@
     },
 )
 
-# CLion 2017.1.1
 config_setting(
     name = "clion-2017.1.1",
     values = {
@@ -91,19 +114,10 @@
     },
 )
 
-# CLion 2016.3.2
 config_setting(
-    name = "clion-2016.3.2",
+    name = "clion-2017.2.1",
     values = {
-        "define": "ij_product=clion-2016.3.2",
-    },
-)
-
-# CLion 2016.2.2
-config_setting(
-    name = "clion-162.1967.7",
-    values = {
-        "define": "ij_product=clion-162.1967.7",
+        "define": "ij_product=clion-2017.2.1",
     },
 )
 
@@ -156,7 +170,35 @@
 java_library(
     name = "plugin_api_for_grammar_kit",
     visibility = ["//third_party/java/jetbrains/grammar_kit:__pkg__"],
-    exports = ["//intellij_platform_sdk/intellij_ce_2016_3_1:sdk"],
+    exports = ["//intellij_platform_sdk/intellij_ce_2017_2_2:sdk"],
+)
+
+# The version of guava bundled with the IntelliJ plugin API.
+java_library(
+    name = "guava",
+    exports = select_from_plugin_api_directory(
+        android_studio = [":guava"],
+        clion = [":guava"],
+        intellij = [":guava"],
+    ),
+)
+
+# The version of truth bundled with the IntelliJ plugin API.
+java_library(
+    name = "truth",
+    testonly = 1,
+    exports = ["@truth//jar"],
+)
+
+# IntelliJ Coverage plugin
+java_library(
+    name = "coverage",
+    neverlink = 1,
+    exports = select_from_plugin_api_directory(
+        android_studio = [],
+        clion = [],
+        intellij = [":coverage"],
+    ),
 )
 
 # Used to support IntelliJ plugin development in our plugin
@@ -231,9 +273,6 @@
 java_library(
     name = "missing_test_classes",
     srcs = select_for_plugin_api({
-        "android-studio-2.3.0.8": [
-            "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
-        ],
         "android-studio-2.3.1.0": [
             "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
         ],
@@ -259,5 +298,6 @@
         clion = ["clion_application_info_name.txt"],
         default = ["intellij_application_info_name.txt"],
         intellij = ["intellij_application_info_name.txt"],
+        intellij_ue = ["intellij_ue_application_info_name.txt"],
     ),
 )
diff --git a/intellij_platform_sdk/BUILD.android_studio b/intellij_platform_sdk/BUILD.android_studio
index 1a8a88c..8ba77c9 100644
--- a/intellij_platform_sdk/BUILD.android_studio
+++ b/intellij_platform_sdk/BUILD.android_studio
@@ -6,9 +6,8 @@
 
 java_import(
     name = "sdk",
-    jars = glob([
-        "android-studio/lib/*.jar",
-    ]),
+    jars = glob(["android-studio/lib/*.jar"]),
+    deps = ["@error_prone_annotations//jar"],
     tags = ["intellij-provided-by-sdk"],
 )
 
@@ -33,6 +32,11 @@
     jars = glob(["android-studio/plugins/junit/lib/*.jar"]),
 )
 
+java_import(
+    name = "guava",
+    jars = glob(["android-studio/lib/guava-*.jar"]),
+)
+
 # The plugins required by ASwB. We need to include them
 # when running integration tests.
 java_import(
@@ -45,6 +49,7 @@
             "android-studio/plugins/junit/lib/*.jar",
             "android-studio/plugins/ndk-workspace/lib/*.jar",
             "android-studio/plugins/properties/lib/*.jar",
+            "android-studio/plugins/smali/lib/*.jar",
         ],
         exclude = [
             # Conflict with lib/guava-*.jar
diff --git a/intellij_platform_sdk/BUILD.clion b/intellij_platform_sdk/BUILD.clion
index d43b8a1..fe2487f 100644
--- a/intellij_platform_sdk/BUILD.clion
+++ b/intellij_platform_sdk/BUILD.clion
@@ -7,6 +7,7 @@
 java_import(
     name = "sdk",
     jars = glob(["clion-*/lib/*.jar"]),
+    deps = ["@error_prone_annotations//jar"],
     tags = ["intellij-provided-by-sdk"],
 )
 
@@ -16,6 +17,11 @@
 )
 
 java_import(
+    name = "guava",
+    jars = glob(["clion-*/lib/guava-*.jar"]),
+)
+
+java_import(
     name = "terminal",
     jars = glob(["clion-*/plugins/terminal/lib/terminal.jar"]),
 )
@@ -36,4 +42,4 @@
 filegroup(
     name = "application_info_jar",
     srcs = glob(["clion-*/lib/clion.jar"]),
-)
\ No newline at end of file
+)
diff --git a/intellij_platform_sdk/BUILD.idea b/intellij_platform_sdk/BUILD.idea
index 4133e09..8755e7f 100644
--- a/intellij_platform_sdk/BUILD.idea
+++ b/intellij_platform_sdk/BUILD.idea
@@ -7,6 +7,7 @@
 java_import(
     name = "sdk",
     jars = glob(["lib/*.jar"]),
+    deps = ["@error_prone_annotations//jar"],
     tags = ["intellij-provided-by-sdk"],
 )
 
@@ -16,6 +17,11 @@
 )
 
 java_import(
+    name = "guava",
+    jars = glob(["lib/guava-*.jar"]),
+)
+
+java_import(
     name = "hg4idea",
     jars = ["plugins/hg4idea/lib/hg4idea.jar"],
 )
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
index 5c1bbda..1d1c314 100644
--- a/intellij_platform_sdk/build_defs.bzl
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -1,39 +1,55 @@
 """Convenience methods for plugin_api."""
 
+# BUILD_VARS for each IDE corresponding to indirect ij_products, eg. "intellij-latest"
+
+
+
+
+
+
+
+
+
 # The current indirect ij_product mapping (eg. "intellij-latest")
 INDIRECT_IJ_PRODUCTS = {
-    "intellij-latest": "intellij-2017.1.1",
-    "intellij-beta": "intellij-2017.1.1",
+    "intellij-latest": "intellij-2017.1.5",
+    "intellij-beta": "intellij-2017.2.2",
+    "intellij-ue-latest": "intellij-ue-2017.1.5",
+    "intellij-ue-beta": "intellij-ue-2017.2.2",
     "android-studio-latest": "android-studio-2.3.1.0",
-    "android-studio-beta": "android-studio-2.3.1.0",
+    "android-studio-beta": "android-studio-3.0.0.9",
     "clion-latest": "clion-2017.1.1",
-    "clion-beta": "clion-2017.1.1",
+    "clion-beta": "clion-2017.2.1",
 }
 
 DIRECT_IJ_PRODUCTS = {
-    "intellij-2017.1.1": struct(
+    "intellij-2017.2.2": struct(
         ide="intellij",
-        directory="intellij_ce_2017_1_1",
+        directory="intellij_ce_2017_2_2",
     ),
-    "intellij-2016.3.1": struct(
+    "intellij-ue-2017.2.2": struct(
+        ide="intellij-ue",
+        directory="intellij_ue_2017_2_2",
+    ),
+    "intellij-2017.1.5": struct(
         ide="intellij",
-        directory="intellij_ce_2016_3_1",
+        directory="intellij_ce_2017_1_5",
     ),
-    "android-studio-2.3.0.8": struct(
+    "intellij-ue-2017.1.5": struct(
+        ide="intellij-ue",
+        directory="intellij_ue_2017_1_5",
+    ),
+    "android-studio-3.0.0.9": struct(
         ide="android-studio",
-        directory="android_studio_2_3_0_8",
+        directory="android_studio_3_0_0_9",
     ),
     "android-studio-2.3.1.0": struct(
         ide="android-studio",
         directory="android_studio_2_3_1_0",
     ),
-    "clion-162.1967.7": struct(
+    "clion-2017.2.1": struct(
         ide="clion",
-        directory="CL_162_1967_7",
-    ),
-    "clion-2016.3.2": struct(
-        ide="clion",
-        directory="clion_2016_3_2",
+        directory="clion_2017_2_1",
     ),
     "clion-2017.1.1": struct(
         ide="clion",
@@ -94,11 +110,12 @@
 
   return select(select_params)
 
-def select_for_ide(intellij=None, android_studio=None, clion=None, default=[]):
+def select_for_ide(intellij=None, intellij_ue=None, android_studio=None, clion=None, default=[]):
   """Selects for the supported IDEs.
 
   Args:
       intellij: Files to use for IntelliJ. If None, will use default.
+      intellij_ue: Files to use for IntelliJ UE. If None, will use value chosen for 'intellij'.
       android_studio: Files to use for Android Studio. If None will use default.
       clion: Files to use for CLion. If None will use default.
       default: Files to use for any IDEs not passed.
@@ -115,11 +132,13 @@
     )
   """
   intellij = intellij if intellij != None else default
+  intellij_ue = intellij_ue if intellij_ue != None else intellij
   android_studio = android_studio if android_studio != None else default
   clion = clion if clion != None else default
 
   ide_to_value = {
       "intellij" : intellij,
+      "intellij-ue" : intellij_ue,
       "android-studio": android_studio,
       "clion": clion,
   }
@@ -135,11 +154,12 @@
 def _plugin_api_directory(value):
   return "@" + value.directory + "//"
 
-def select_from_plugin_api_directory(intellij, android_studio, clion):
+def select_from_plugin_api_directory(intellij, android_studio, clion, intellij_ue=None):
   """Internal convenience method to generate select statement from the IDE's plugin_api directories."""
 
   ide_to_value = {
       "intellij" : intellij,
+      "intellij-ue" : intellij_ue if intellij_ue else intellij,
       "android-studio": android_studio,
       "clion": clion,
   }
diff --git a/intellij_platform_sdk/intellij_ue_application_info_name.txt b/intellij_platform_sdk/intellij_ue_application_info_name.txt
new file mode 100644
index 0000000..c60c001
--- /dev/null
+++ b/intellij_platform_sdk/intellij_ue_application_info_name.txt
@@ -0,0 +1 @@
+idea/ApplicationInfo.xml
diff --git a/java/BUILD b/java/BUILD
index fb36558..1bd67b8 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -2,9 +2,10 @@
 
 load(
     "//build_defs:build_defs.bzl",
-    "merged_plugin_xml",
-    "stamped_plugin_xml",
     "intellij_plugin",
+    "merged_plugin_xml",
+    "optional_plugin_xml",
+    "stamped_plugin_xml",
 )
 load(
     "//testing:test_defs.bzl",
@@ -15,13 +16,12 @@
 java_library(
     name = "java",
     srcs = glob(["src/**/*.java"]),
+    javacopts = ["-Xep:FutureReturnValueIgnored:OFF"],
     visibility = ["//visibility:public"],
-    runtime_deps = [
-        "//common/actionhelper",
-        "//common/experiments",
-    ],
+    runtime_deps = ["//common/actionhelper"],
     deps = [
         "//base",
+        "//common/experiments",
         "//intellij_platform_sdk:junit",
         "//intellij_platform_sdk:plugin_api",
         "//proto:proto_deps",
@@ -52,9 +52,17 @@
     plugin_xml = "merged_plugin_xml",
 )
 
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "JUnit",
+    plugin_xml = "src/META-INF/java-contents.xml",
+    visibility = ["//visibility:public"],
+)
+
 intellij_plugin(
     name = "java_integration_test_plugin",
     testonly = 1,
+    optional_plugin_xmls = [":optional_xml"],
     plugin_xml = ":java_plugin_xml",
     deps = [
         ":java",
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index e22c895..08f7fd4 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -1,11 +1,11 @@
 <!--
-  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~ 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
+  ~   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,
@@ -14,119 +14,9 @@
   ~ limitations under the License.
   -->
 <idea-plugin>
-  <depends>JUnit</depends>
-  <depends>com.intellij.modules.java</depends>
-
-  <actions>
-    <action class="com.google.idea.blaze.java.libraries.ExcludeLibraryAction"
-      id="Blaze.ExcludeLibraryAction"
-      text="Exclude Library and Resync">
-    </action>
-    <action class="com.google.idea.blaze.java.libraries.AttachSourceJarAction"
-      id="Blaze.AttachSourceJarAction"
-      text="Attach Source Jar">
-    </action>
-    <action class="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAction"
-      id="Blaze.AddLibraryTargetDirectoryToProjectView"
-      text="Add Library Target Directory to Project View">
-    </action>
-    <action class="com.google.idea.blaze.java.libraries.DetachAllSourceJarsAction"
-      id="Blaze.DetachAllSourceJars"
-      text="Detach All Blaze Source Jars">
-    </action>
-
-    <group id="Blaze.Java.ProjectViewPopupMenu">
-      <add-to-group group-id="Blaze.PerFileContextMenu"/>
-      <reference id="Blaze.ExcludeLibraryAction"/>
-      <reference id="Blaze.AttachSourceJarAction"/>
-      <reference id="Blaze.AddLibraryTargetDirectoryToProjectView"/>
-    </group>
-
-    <group id="Blaze.JavaMenuGroup.Outer">
-      <add-to-group group-id="Blaze.MainMenuActionGroup" relative-to-action="Blaze.MenuFooter" anchor="after"/>
-      <group id="Blaze.JavaMenuGroup" text="Java">
-        <reference id="Blaze.DetachAllSourceJars"/>
-      </group>
-    </group>
-
-    <!-- IntelliJ specific actions -->
-
-    <action id="Blaze.ImportProject2" class="com.google.idea.blaze.java.wizard2.BlazeImportProjectAction" icon="BlazeIcons.Blaze">
-      <add-to-group group-id="WelcomeScreen.QuickStart" />
-      <add-to-group group-id="OpenProjectGroup" relative-to-action="ImportProject" anchor="after"/>
-    </action>
-
-    <!-- End IntelliJ specific actions -->
-
-  </actions>
-
-  <extensions defaultExtensionNs="com.google.idea.blaze">
-    <SyncPlugin implementation="com.google.idea.blaze.java.sync.BlazeJavaSyncPlugin"/>
-    <PsiFileProvider implementation="com.google.idea.blaze.java.psi.JavaPsiFileProvider" />
-    <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.java.run.BlazeJavaRunConfigurationHandlerProvider"/>
-    <RunConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaRunConfigurationFactory"/>
-    <RunConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaTestRunConfigurationFactory"/>
-    <BlazeUserSettingsContributor implementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettingsContributor$BlazeJavaUserSettingsProvider"/>
-    <FileCache implementation="com.google.idea.blaze.java.libraries.JarCache$FileCacheAdapter"/>
-    <PrefetchFileSource implementation="com.google.idea.blaze.java.sync.JavaPrefetchFileSource"/>
-    <BlazeTestEventsHandler implementation="com.google.idea.blaze.java.run.BlazeJavaTestEventsHandler"/>
-    <AttributeSpecificStringLiteralReferenceProvider implementation="com.google.idea.blaze.java.lang.build.references.JavaClassQualifiedNameReference"/>
-    <JavaLikeLanguage implementation="com.google.idea.blaze.java.sync.source.JavaLikeLanguage$Java"/>
-    <TestTargetHeuristic implementation="com.google.idea.blaze.java.run.JUnitTestHeuristic" order="before TestSizeHeuristic"/>
-  </extensions>
-
-  <extensions defaultExtensionNs="com.intellij">
-    <runConfigurationProducer
-        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaMainClassRunConfigurationProducer"
-        order="first"/>
-    <runConfigurationProducer
-        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestClassConfigurationProducer"
-        order="first"/>
-    <runConfigurationProducer
-        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestMethodConfigurationProducer"
-        order="first"/>
-    <runConfigurationProducer
-        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaAbstractTestCaseConfigurationProducer"
-        order="first"/>
-    <runConfigurationProducer
-        implementation="com.google.idea.blaze.java.run.producers.MultipleJavaClassesTestConfigurationProducer"
-        order="first"/>
-    <projectViewNodeDecorator implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusClassNodeDecorator"/>
-    <editorTabColorProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabColorProvider"/>
-    <editorTabTitleProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabTitleProvider"/>
-    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"
-                        serviceImplementation="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"/>
-    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.PackageManifestReader"
-                        serviceImplementation="com.google.idea.blaze.java.sync.source.PackageManifestReader"/>
-    <programRunner implementation="com.google.idea.blaze.java.run.BlazeJavaDebuggerRunner"/>
-    <projectService serviceInterface="com.google.idea.blaze.base.ui.BlazeProblemsView"
-                    serviceImplementation="com.google.idea.blaze.java.ui.BlazeIntelliJProblemsView"/>
-    <projectService serviceImplementation="com.google.idea.blaze.java.libraries.SourceJarManager"/>
-    <refactoring.safeDeleteProcessor id="build_file_safe_delete" order="before javaProcessor"
-                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
-    <!--duplicated here in case the Kotlin plugin is present, as it also tries to replace javaProcessor-->
-    <refactoring.safeDeleteProcessor id="build_file_safe_delete_copy" order="before kotlinProcessor"
-                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
-    <projectService serviceImplementation="com.google.idea.blaze.java.libraries.JarCache"/>
-
-    <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAttachSourcesProvider"/>
-    <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.BlazeAttachSourceProvider"/>
-    <applicationService serviceImplementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettings"/>
-    <psi.referenceContributor language="BUILD" implementation="com.google.idea.blaze.java.lang.build.references.JavaClassReferenceContributor"/>
-  </extensions>
-
-  <project-components>
+  <application-components>
     <component>
-      <implementation-class>com.google.idea.blaze.java.run.producers.NonBlazeProducerSuppressor</implementation-class>
+      <implementation-class>com.google.idea.blaze.java.plugin.JUnitPluginDependencyWarning</implementation-class>
     </component>
-  </project-components>
-
-  <extensionPoints>
-    <extensionPoint qualifiedName="com.google.idea.blaze.JavaSyncAugmenter"
-                    interface="com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.JavaLikeLanguage"
-                    interface="com.google.idea.blaze.java.sync.source.JavaLikeLanguage"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.JUnitParameterizedClassHeuristic"
-                    interface="com.google.idea.blaze.java.run.producers.JUnitParameterizedClassHeuristic"/>
-  </extensionPoints>
-</idea-plugin>
+  </application-components>
+</idea-plugin>
\ No newline at end of file
diff --git a/java/src/META-INF/java-contents.xml b/java/src/META-INF/java-contents.xml
new file mode 100644
index 0000000..a48e8f1
--- /dev/null
+++ b/java/src/META-INF/java-contents.xml
@@ -0,0 +1,135 @@
+<!--
+  ~ 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.
+  -->
+<idea-plugin>
+  <depends>JUnit</depends>
+  <depends>com.intellij.modules.java</depends>
+
+  <actions>
+    <action class="com.google.idea.blaze.java.libraries.ExcludeLibraryAction"
+      id="Blaze.ExcludeLibraryAction"
+      text="Exclude Library and Resync">
+    </action>
+    <action class="com.google.idea.blaze.java.libraries.AttachSourceJarAction"
+      id="Blaze.AttachSourceJarAction"
+      text="Attach Source Jar">
+    </action>
+    <action class="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAction"
+      id="Blaze.AddLibraryTargetDirectoryToProjectView"
+      text="Add Library Target Directory to Project View">
+    </action>
+    <action class="com.google.idea.blaze.java.libraries.DetachAllSourceJarsAction"
+      id="Blaze.DetachAllSourceJars"
+      text="Detach All Blaze Source Jars">
+    </action>
+
+    <group id="Blaze.Java.ProjectViewPopupMenu">
+      <add-to-group group-id="Blaze.PerFileContextMenu"/>
+      <reference id="Blaze.ExcludeLibraryAction"/>
+      <reference id="Blaze.AttachSourceJarAction"/>
+      <reference id="Blaze.AddLibraryTargetDirectoryToProjectView"/>
+    </group>
+
+    <group id="Blaze.JavaMenuGroup.Outer">
+      <add-to-group group-id="Blaze.MainMenuActionGroup" relative-to-action="Blaze.MenuFooter" anchor="after"/>
+      <group id="Blaze.JavaMenuGroup" text="Java">
+        <reference id="Blaze.DetachAllSourceJars"/>
+      </group>
+    </group>
+
+    <!-- IntelliJ specific actions -->
+
+    <action id="Blaze.ImportProject2" class="com.google.idea.blaze.java.wizard2.BlazeImportProjectAction" icon="BlazeIcons.Blaze">
+      <add-to-group group-id="WelcomeScreen.QuickStart" />
+      <add-to-group group-id="OpenProjectGroup" relative-to-action="ImportProject" anchor="after"/>
+    </action>
+
+    <!-- End IntelliJ specific actions -->
+
+  </actions>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.java.sync.BlazeJavaSyncPlugin"/>
+    <PsiFileProvider implementation="com.google.idea.blaze.java.psi.JavaPsiFileProvider" />
+    <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.java.run.BlazeJavaRunConfigurationHandlerProvider"/>
+    <BlazeUserSettingsContributor implementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettingsContributor$BlazeJavaUserSettingsProvider"/>
+    <FileCache implementation="com.google.idea.blaze.java.libraries.JarCache$FileCacheAdapter"/>
+    <PrefetchFileSource implementation="com.google.idea.blaze.java.sync.JavaPrefetchFileSource"/>
+    <BlazeTestEventsHandler implementation="com.google.idea.blaze.java.run.BlazeJavaTestEventsHandler"/>
+    <AttributeSpecificStringLiteralReferenceProvider implementation="com.google.idea.blaze.java.lang.build.references.JavaClassQualifiedNameReference"/>
+    <JavaLikeLanguage implementation="com.google.idea.blaze.java.sync.source.JavaLikeLanguage$Java"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.java.run.JUnitTestHeuristic" order="before TestSizeHeuristic"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.java.run.QualifiedClassNameHeuristic" order="before TargetNameHeuristic"/>
+    <SyncListener implementation="com.google.idea.blaze.java.libraries.BlazeSourceJarNavigationPolicy$SyncTrackerUpdater"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaMainClassRunConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestClassConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestMethodConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaAbstractTestCaseConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.MultipleJavaClassesTestConfigurationProducer"
+        order="first"/>
+    <projectViewNodeDecorator implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusClassNodeDecorator"/>
+    <editorTabColorProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabColorProvider"/>
+    <editorTabTitleProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabTitleProvider"/>
+    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"
+                        serviceImplementation="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"/>
+    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.PackageManifestReader"
+                        serviceImplementation="com.google.idea.blaze.java.sync.source.PackageManifestReader"/>
+    <programRunner implementation="com.google.idea.blaze.java.run.BlazeJavaDebuggerRunner"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.ui.BlazeProblemsView"
+                    serviceImplementation="com.google.idea.blaze.java.ui.BlazeIntelliJProblemsView"/>
+    <projectService serviceImplementation="com.google.idea.blaze.java.libraries.SourceJarManager"/>
+    <refactoring.safeDeleteProcessor id="build_file_safe_delete" order="before javaProcessor"
+                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
+    <!--duplicated here in case the Kotlin plugin is present, as it also tries to replace javaProcessor-->
+    <refactoring.safeDeleteProcessor id="build_file_safe_delete_copy" order="before kotlinProcessor"
+                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
+    <projectService serviceImplementation="com.google.idea.blaze.java.libraries.JarCache"/>
+
+    <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAttachSourcesProvider"/>
+    <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.BlazeAttachSourceProvider"/>
+    <applicationService serviceImplementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettings"/>
+    <psi.referenceContributor language="BUILD" implementation="com.google.idea.blaze.java.lang.build.references.JavaClassReferenceContributor"/>
+    <useScopeEnlarger implementation="com.google.idea.blaze.java.psi.AutoFactoryUseScopeEnlarger"/>
+    <implicitUsageProvider implementation="com.google.idea.blaze.java.psi.AutoFactoryImplicitUsageProvider"/>
+    <psi.clsCustomNavigationPolicy implementation="com.google.idea.blaze.java.libraries.BlazeSourceJarNavigationPolicy"/>
+  </extensions>
+
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.java.run.producers.NonBlazeProducerSuppressor</implementation-class>
+    </component>
+  </project-components>
+
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.JavaSyncAugmenter"
+                    interface="com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.JavaLikeLanguage"
+                    interface="com.google.idea.blaze.java.sync.source.JavaLikeLanguage"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.JUnitParameterizedClassHeuristic"
+                    interface="com.google.idea.blaze.java.run.producers.JUnitParameterizedClassHeuristic"/>
+  </extensionPoints>
+</idea-plugin>
diff --git a/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java b/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
index c5e45c6..a35c789 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
@@ -97,7 +97,7 @@
     }
     // To start with, we whitelist only library rules
     // It makes no sense to add directories for java_imports and the like
-    if (!target.kind.isOneOf(Kind.JAVA_LIBRARY, Kind.ANDROID_LIBRARY)) {
+    if (!target.kind.isOneOf(Kind.JAVA_LIBRARY, Kind.ANDROID_LIBRARY, Kind.PROTO_LIBRARY)) {
       return null;
     }
     if (target.buildFile == null) {
diff --git a/java/src/com/google/idea/blaze/java/libraries/BlazeSourceJarNavigationPolicy.java b/java/src/com/google/idea/blaze/java/libraries/BlazeSourceJarNavigationPolicy.java
new file mode 100644
index 0000000..8af9539
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/libraries/BlazeSourceJarNavigationPolicy.java
@@ -0,0 +1,221 @@
+/*
+ * 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.java.libraries;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.ProjectManagerAdapter;
+import com.intellij.openapi.roots.LibraryOrderEntry;
+import com.intellij.openapi.roots.OrderEntry;
+import com.intellij.openapi.roots.OrderRootType;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryUtil;
+import com.intellij.openapi.util.SimpleModificationTracker;
+import com.intellij.openapi.vfs.JarFileSystem;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiJavaFile;
+import com.intellij.psi.impl.compiled.ClsClassImpl;
+import com.intellij.psi.impl.compiled.ClsCustomNavigationPolicyEx;
+import com.intellij.psi.impl.compiled.ClsFileImpl;
+import com.intellij.psi.util.CachedValueProvider.Result;
+import com.intellij.psi.util.CachedValuesManager;
+import java.io.File;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.annotation.Nullable;
+
+/**
+ * A navigation policy that allows navigating to source (or reading javadocs) even when that source
+ * isn't officially attached to the library in the project.
+ *
+ * <p>Attaching sources has been shown to slow indexing time because IntelliJ indexes all the source
+ * files attached to project jars. This isn't a huge problem for non-Blaze projects, since the jars
+ * change infrequently. With blaze, however, the jars are reshuffled after every blaze build and so
+ * the indexing time increases dramatically if you attach too many of them.
+ *
+ * <p>This class attempts to work around that problem by providing a way to navigate to the source
+ * without having that source actually indexed.
+ */
+final class BlazeSourceJarNavigationPolicy extends ClsCustomNavigationPolicyEx {
+
+  private static final BoolExperiment enabled =
+      new BoolExperiment("blaze.source.jar.navigation.policy", true);
+  static final BoolExperiment cacheEnabled =
+      new BoolExperiment("blaze.source.jar.navigation.policy.cache", false);
+
+  private final ConcurrentMap<Project, SimpleModificationTracker> projectSyncTrackers =
+      new ConcurrentHashMap<>();
+
+  public BlazeSourceJarNavigationPolicy() {
+    ApplicationManager.getApplication()
+        .getMessageBus()
+        .connect()
+        .subscribe(ProjectManager.TOPIC, new RemoveSyncTrackerOnProjectClosing());
+  }
+
+  @Nullable
+  @Override
+  public PsiFile getFileNavigationElement(ClsFileImpl file) {
+    if (!enabled.getValue()) {
+      return null;
+    }
+
+    return CachedValuesManager.getCachedValue(
+        file,
+        () -> {
+          Result<PsiFile> result = getPsiFile(file);
+          if (result == null) {
+            result = notFound(file);
+          }
+          return result;
+        });
+  }
+
+  @Nullable
+  private Result<PsiFile> getPsiFile(ClsFileImpl file) {
+    Project project = file.getProject();
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+
+    VirtualFile root = getSourceJarRoot(project, blazeProjectData, file);
+    if (root == null) {
+      return null;
+    }
+
+    return getSourceFileResult(file, root);
+  }
+
+  @Nullable
+  private VirtualFile getSourceJarRoot(
+      Project project, BlazeProjectData blazeProjectData, PsiJavaFile clsFile) {
+
+    Library library = findLibrary(project, clsFile);
+    if (library == null || library.getFiles(OrderRootType.SOURCES).length != 0) {
+      // If the library already has sources attached, no need to hunt for them.
+      return null;
+    }
+
+    BlazeJarLibrary blazeLibrary =
+        LibraryActionHelper.findLibraryFromIntellijLibrary(project, blazeProjectData, library);
+    if (blazeLibrary == null) {
+      return null;
+    }
+
+    File sourceJar =
+        JarCache.getInstance(project)
+            .getCachedSourceJar(blazeProjectData.artifactLocationDecoder, blazeLibrary);
+
+    if (sourceJar == null && blazeLibrary.libraryArtifact.sourceJar != null) {
+      sourceJar =
+          blazeProjectData.artifactLocationDecoder.decode(blazeLibrary.libraryArtifact.sourceJar);
+    }
+
+    if (sourceJar == null) {
+      return null;
+    }
+
+    VirtualFile vfsFile = VfsUtil.findFileByIoFile(sourceJar, true);
+    if (vfsFile == null) {
+      return null;
+    }
+    return JarFileSystem.getInstance().getJarRootForLocalFile(vfsFile);
+  }
+
+  @Nullable
+  private Library findLibrary(Project project, PsiJavaFile clsFile) {
+    OrderEntry libraryEntry = LibraryUtil.findLibraryEntry(clsFile.getVirtualFile(), project);
+    if (!(libraryEntry instanceof LibraryOrderEntry)) {
+      return null;
+    }
+    return ((LibraryOrderEntry) libraryEntry).getLibrary();
+  }
+
+  @Nullable
+  private Result<PsiFile> getSourceFileResult(ClsFileImpl clsFile, VirtualFile root) {
+    // This code is adapted from JavaPsiImplementationHelperImpl#getClsFileNavigationElement
+    PsiClass[] classes = clsFile.getClasses();
+    if (classes.length == 0) {
+      return null;
+    }
+
+    String sourceFileName = ((ClsClassImpl) classes[0]).getSourceFileName();
+    String packageName = clsFile.getPackageName();
+    String relativePath =
+        packageName.isEmpty()
+            ? sourceFileName
+            : packageName.replace('.', '/') + '/' + sourceFileName;
+
+    VirtualFile source = root.findFileByRelativePath(relativePath);
+    if (source != null && source.isValid()) {
+      // Since we have an actual source jar tracked down, use that source jar as the modification
+      // tracker. This means the result will continue to be cached unless that source jar changes.
+      // If we didn't find a source jar, we use a modification tracker that invalidates on every
+      // Blaze sync, which is less efficient.
+      PsiFile psiSource = clsFile.getManager().findFile(source);
+      if (psiSource instanceof PsiClassOwner) {
+        return Result.create(psiSource, source);
+      }
+      return Result.create(null, source);
+    }
+
+    return null;
+  }
+
+  private Result<PsiFile> notFound(ClsFileImpl file) {
+    // A "not-found" result is null, but depends on the project sync tracker, so it will expire
+    // after the next blaze sync. This means we'll run this check again after every sync for files
+    // that don't have source jars, but it's not a huge deal because checking for the source jar
+    // only takes a few microseconds.
+    projectSyncTrackers.putIfAbsent(file.getProject(), new SimpleModificationTracker());
+    return Result.create(null, projectSyncTrackers.get(file.getProject()));
+  }
+
+  // In #api_163 and beyond, this can simply implement ProjectManagerListener
+  private class RemoveSyncTrackerOnProjectClosing extends ProjectManagerAdapter {
+
+    @Override
+    public void projectClosing(Project project) {
+      projectSyncTrackers.remove(project);
+    }
+  }
+
+  class SyncTrackerUpdater extends SyncListener.Adapter {
+
+    @Override
+    public void afterSync(
+        Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult) {
+      SimpleModificationTracker modificationTracker = projectSyncTrackers.get(project);
+      if (modificationTracker != null) {
+        modificationTracker.incModificationCount();
+      }
+    }
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/libraries/JarCache.java b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
index 66eafaa..a6fe08c 100644
--- a/java/src/com/google/idea/blaze/java/libraries/JarCache.java
+++ b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
@@ -112,9 +112,11 @@
           artifactLocationDecoder.decode(library.libraryArtifact.jarForIntellijLibrary());
       sourceFileToCacheKey.put(jarFile, cacheKeyForJar(jarFile));
 
-      boolean attachSourceJar =
-          attachAllSourceJars || sourceJarManager.hasSourceJarAttached(library.key);
-      if (attachSourceJar && library.libraryArtifact.sourceJar != null) {
+      boolean copySourceJar =
+          attachAllSourceJars
+              || BlazeSourceJarNavigationPolicy.cacheEnabled.getValue()
+              || sourceJarManager.hasSourceJarAttached(library.key);
+      if (copySourceJar && library.libraryArtifact.sourceJar != null) {
         File srcJarFile = artifactLocationDecoder.decode(library.libraryArtifact.sourceJar);
         sourceFileToCacheKey.put(srcJarFile, cacheKeyForSourceJar(srcJarFile));
       }
diff --git a/java/src/com/google/idea/blaze/java/plugin/JUnitPluginDependencyWarning.java b/java/src/com/google/idea/blaze/java/plugin/JUnitPluginDependencyWarning.java
new file mode 100644
index 0000000..57e3b1a
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/plugin/JUnitPluginDependencyWarning.java
@@ -0,0 +1,153 @@
+/*
+ * 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.java.plugin;
+
+import com.google.idea.blaze.base.plugin.PluginUtils;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.transactions.Transactions;
+import com.intellij.ide.AppLifecycleListener;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationListener;
+import com.intellij.notification.NotificationType;
+import com.intellij.notification.Notifications;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.ApplicationComponent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.ProjectManagerAdapter;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.ui.popup.Balloon;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.wm.WindowManager;
+import com.intellij.ui.HyperlinkAdapter;
+import com.intellij.ui.awt.RelativePoint;
+import com.intellij.util.messages.MessageBusConnection;
+import java.awt.Point;
+import java.awt.Rectangle;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.event.HyperlinkEvent;
+
+/**
+ * Runs on startup, displaying an error if the JUnit plugin (required for the blaze plugin to
+ * properly function) is not enabled.
+ */
+public class JUnitPluginDependencyWarning extends ApplicationComponent.Adapter {
+
+  private static final String JUNIT_PLUGIN_ID = "JUnit";
+
+  @Override
+  public void initComponent() {
+    if (!PluginUtils.isPluginEnabled(JUNIT_PLUGIN_ID)) {
+      notifyJUnitNotEnabled();
+    }
+  }
+
+  /**
+   * Pop up a notification asking user to enable the JUnit plugin, and also add an error item to the
+   * event log.
+   */
+  private static void notifyJUnitNotEnabled() {
+    String buildSystem = Blaze.defaultBuildSystemName();
+
+    String message =
+        String.format(
+            "<html>The JUnit plugin is disabled, but it's required for the %s plugin to function."
+                + "<br>Please <a href=\"fix\">enable the JUnit plugin</a> and restart the IDE",
+            buildSystem);
+
+    NotificationListener listener =
+        new NotificationListener.Adapter() {
+          @Override
+          protected void hyperlinkActivated(Notification notification, HyperlinkEvent e) {
+            if ("fix".equals(e.getDescription())) {
+              PluginUtils.installOrEnablePlugin(JUNIT_PLUGIN_ID);
+            }
+          }
+        };
+
+    Notification notification =
+        new Notification(
+            buildSystem + " Plugin Error",
+            buildSystem + " plugin dependencies are missing",
+            message,
+            NotificationType.ERROR,
+            listener);
+    notification.setImportant(true);
+
+    Application app = ApplicationManager.getApplication();
+    MessageBusConnection connection = app.getMessageBus().connect(app);
+    connection.subscribe(
+        AppLifecycleListener.TOPIC,
+        new AppLifecycleListener.Adapter() {
+          @Override
+          public void appStarting(@Nullable Project projectFromCommandLine) {
+            // Adds an error item to the 'Event Log' tab.
+            // Easy to ignore, but remains in event log until manually cleared.
+            Transactions.submitTransactionAndWait(() -> Notifications.Bus.notify(notification));
+          }
+
+          @Override
+          public void appFrameCreated(String[] commandLineArgs, Ref<Boolean> willOpenProject) {
+            // Popup dialog in welcome screen.
+            app.invokeLater(() -> showPopupNotification(message));
+          }
+        });
+    if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
+      connection.subscribe(
+          ProjectManager.TOPIC,
+          new ProjectManagerAdapter() {
+            @Override
+            public void projectOpened(Project project) {
+              // Popup dialog on project open, for users bypassing the welcome screen.
+              if (Blaze.isBlazeProject(project)) {
+                app.invokeLater(() -> showPopupNotification(message));
+              }
+            }
+          });
+    }
+  }
+
+  private static void showPopupNotification(String message) {
+    JComponent component = WindowManager.getInstance().findVisibleFrame().getRootPane();
+    if (component == null) {
+      return;
+    }
+    Rectangle rect = component.getVisibleRect();
+    JBPopupFactory.getInstance()
+        .createHtmlTextBalloonBuilder(
+            message,
+            MessageType.WARNING,
+            new HyperlinkAdapter() {
+              @Override
+              protected void hyperlinkActivated(HyperlinkEvent e) {
+                PluginUtils.installOrEnablePlugin(JUNIT_PLUGIN_ID);
+              }
+            })
+        .setFadeoutTime(-1)
+        .setHideOnLinkClick(true)
+        .setHideOnFrameResize(false)
+        .setHideOnClickOutside(false)
+        .setHideOnKeyOutside(false)
+        .setDisposable(ApplicationManager.getApplication())
+        .createBalloon()
+        .show(
+            new RelativePoint(component, new Point(rect.x + 30, rect.y + rect.height - 10)),
+            Balloon.Position.above);
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java b/java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java
index 09cd3b4..6035da6 100644
--- a/java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java
+++ b/java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java
@@ -33,11 +33,10 @@
 
   public static LanguageLevel getLanguageLevel(
       ProjectViewSet projectViewSet, LanguageLevel defaultValue) {
-    Integer level = projectViewSet.getScalarValue(KEY, null);
-    if (level == null) {
-      return defaultValue;
-    }
-    return getLanguageLevel(level, defaultValue);
+    return projectViewSet
+        .getScalarValue(KEY)
+        .map(i -> getLanguageLevel(i, defaultValue))
+        .orElse(defaultValue);
   }
 
   @Nullable
diff --git a/java/src/com/google/idea/blaze/java/psi/AutoFactoryImplicitUsageProvider.java b/java/src/com/google/idea/blaze/java/psi/AutoFactoryImplicitUsageProvider.java
new file mode 100644
index 0000000..84570a3
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/psi/AutoFactoryImplicitUsageProvider.java
@@ -0,0 +1,38 @@
+/*
+ * 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.java.psi;
+
+import com.intellij.codeInsight.daemon.ImplicitUsageProvider;
+import com.intellij.psi.PsiElement;
+
+/** Suppresses 'unused' warnings for @AutoFactory annotated classes / constructors. */
+public class AutoFactoryImplicitUsageProvider implements ImplicitUsageProvider {
+
+  @Override
+  public boolean isImplicitUsage(PsiElement element) {
+    return AutoFactoryUseScopeEnlarger.isAutoFactoryClass(element);
+  }
+
+  @Override
+  public boolean isImplicitRead(PsiElement element) {
+    return false;
+  }
+
+  @Override
+  public boolean isImplicitWrite(PsiElement element) {
+    return false;
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/psi/AutoFactoryUseScopeEnlarger.java b/java/src/com/google/idea/blaze/java/psi/AutoFactoryUseScopeEnlarger.java
new file mode 100644
index 0000000..02bdba5
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/psi/AutoFactoryUseScopeEnlarger.java
@@ -0,0 +1,66 @@
+/*
+ * 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.java.psi;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiModifierList;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.UseScopeEnlarger;
+import javax.annotation.Nullable;
+
+/**
+ * Find usages of @AutoFactory-annotated classes in project libraries.
+ *
+ * <p>Without implementing {@link UseScopeEnlarger}, IntelliJ will not search libraries for usages
+ * of symbols defined in the project.
+ */
+public class AutoFactoryUseScopeEnlarger extends UseScopeEnlarger {
+
+  private static final String AUTO_FACTORY_ANNOTATION = "com.google.auto.factory.AutoFactory";
+
+  @Nullable
+  @Override
+  public SearchScope getAdditionalUseScope(PsiElement element) {
+    if (isAutoFactoryClass(element)) {
+      return GlobalSearchScope.allScope(element.getProject());
+    }
+    return null;
+  }
+
+  @Nullable
+  private static PsiClass getPsiClass(PsiElement element) {
+    if (element instanceof PsiClass) {
+      return (PsiClass) element;
+    }
+    if (element instanceof PsiMethod && ((PsiMethod) element).isConstructor()) {
+      return ((PsiMethod) element).getContainingClass();
+    }
+    return null;
+  }
+
+  static boolean isAutoFactoryClass(PsiElement element) {
+    PsiClass psiClass = getPsiClass(element);
+    if (psiClass == null || !psiClass.hasModifierProperty(PsiModifier.PUBLIC)) {
+      return false;
+    }
+    PsiModifierList modifiers = psiClass.getModifierList();
+    return modifiers != null && modifiers.findAnnotation(AUTO_FACTORY_ANNOTATION) != null;
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
deleted file mode 100644
index 3daee7a..0000000
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.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.java.run;
-
-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.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
-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;
-
-/** Creates run configurations for java_binary. */
-public class BlazeJavaRunConfigurationFactory extends BlazeRunConfigurationFactory {
-  @Override
-  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
-    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
-    return target != null && target.kind == Kind.JAVA_BINARY;
-  }
-
-  @Override
-  protected ConfigurationFactory getConfigurationFactory() {
-    return BlazeCommandRunConfigurationType.getInstance().getFactory();
-  }
-
-  @Override
-  public void setupConfiguration(RunConfiguration configuration, Label target) {
-    final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(target);
-
-    BlazeCommandRunConfigurationCommonState state =
-        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    if (state != null) {
-      state.getCommandState().setCommand(BlazeCommandName.RUN);
-    }
-    blazeConfig.setGeneratedName();
-  }
-}
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
index ca6a022..fbfe454 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
@@ -26,7 +26,13 @@
     implements BlazeCommandRunConfigurationHandlerProvider {
 
   private static final ImmutableSet<Kind> RELEVANT_RULE_KINDS =
-      ImmutableSet.of(Kind.ANDROID_ROBOLECTRIC_TEST, Kind.JAVA_TEST, Kind.JAVA_BINARY);
+      ImmutableSet.of(
+          Kind.ANDROID_ROBOLECTRIC_TEST,
+          Kind.JAVA_TEST,
+          Kind.JAVA_BINARY,
+          Kind.SCALA_BINARY,
+          Kind.SCALA_TEST,
+          Kind.SCALA_JUNIT_TEST);
 
   static boolean supportsKind(Kind kind) {
     return RELEVANT_RULE_KINDS.contains(kind);
@@ -46,5 +52,4 @@
   public String getId() {
     return "BlazeJavaRunConfigurationHandlerProvider";
   }
-
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
index e7c4773..67a1cd5 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
@@ -21,18 +21,19 @@
 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.Kind;
 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;
@@ -97,23 +98,20 @@
     assert projectViewSet != null;
 
     BlazeCommand blazeCommand;
-    if (useTestUi()) {
-      BlazeTestEventsHandler eventsHandler =
-          BlazeTestEventsHandler.getHandlerForTarget(project, configuration.getTarget());
-      assert (eventsHandler != null);
+    BlazeTestUiSession testUiSession =
+        useTestUi()
+            ? TestUiSessionProvider.createForTarget(project, configuration.getTarget())
+            : null;
+    if (testUiSession != null) {
       blazeCommand =
           getBlazeCommand(
-              project,
-              configuration,
-              projectViewSet,
-              BlazeTestEventsHandler.getBlazeFlags(project),
-              debug);
+              project, configuration, projectViewSet, testUiSession.getBlazeFlags(), debug);
       setConsoleBuilder(
           new TextConsoleBuilderImpl(project) {
             @Override
             protected ConsoleView createConsole() {
               return SmRunnerUtils.getConsoleView(
-                  project, configuration, getEnvironment().getExecutor(), eventsHandler);
+                  project, configuration, getEnvironment().getExecutor(), testUiSession);
             }
           });
     } else {
@@ -153,9 +151,7 @@
   private boolean useTestUi() {
     BlazeCommandRunConfigurationCommonState state =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    return state != null
-        && BlazeCommandName.TEST.equals(state.getCommandState().getCommand())
-        && !state.getRunOnDistributedExecutorState().runOnDistributedExecutor;
+    return state != null && BlazeCommandName.TEST.equals(state.getCommandState().getCommand());
   }
 
   @Override
@@ -192,22 +188,20 @@
     BlazeCommand.Builder command =
         BlazeCommand.builder(binaryPath, blazeCommand)
             .addTargets(configuration.getTarget())
-            .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+            .addBlazeFlags(
+                BlazeFlags.blazeFlags(
+                    project, projectViewSet, blazeCommand, BlazeInvocationContext.RunConfiguration))
             .addBlazeFlags(extraBlazeFlags)
             .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags());
 
     if (debug) {
       Kind kind = configuration.getKindForTarget();
-      boolean isJavaBinary = kind == Kind.JAVA_BINARY;
-      if (isJavaBinary) {
+      boolean isBinary = kind != null && kind.isOneOf(Kind.JAVA_BINARY, Kind.SCALA_BINARY);
+      if (isBinary) {
         command.addExeFlags(BlazeFlags.JAVA_BINARY_DEBUG);
       } else {
         command.addBlazeFlags(BlazeFlags.JAVA_TEST_DEBUG);
       }
-    } else {
-      boolean runDistributed =
-          handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
-      command.addBlazeFlags(DistributedExecutorSupport.getBlazeFlags(project, runDistributed));
     }
 
     command.addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
index 8091b51..73b1ef8 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.java.run;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
@@ -30,7 +31,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 +38,19 @@
 import javax.annotation.Nullable;
 
 /** Provides java-specific methods needed by the SM-runner test UI. */
-public class BlazeJavaTestEventsHandler extends BlazeTestEventsHandler {
+public class BlazeJavaTestEventsHandler implements BlazeTestEventsHandler {
+
+  private static final ImmutableSet<Kind> HANDLED_KINDS =
+      ImmutableSet.of(
+          Kind.JAVA_TEST,
+          Kind.ANDROID_ROBOLECTRIC_TEST,
+          Kind.GWT_TEST,
+          Kind.SCALA_TEST,
+          Kind.SCALA_JUNIT_TEST);
 
   @Override
-  protected EnumSet<Kind> handledKinds() {
-    return EnumSet.of(Kind.JAVA_TEST, Kind.ANDROID_ROBOLECTRIC_TEST, Kind.GWT_TEST);
+  public boolean handlesKind(@Nullable Kind kind) {
+    return HANDLED_KINDS.contains(kind);
   }
 
   /** Overridden to support parameterized tests, which use nested test_suite XML elements. */
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
deleted file mode 100644
index 9b9aad8..0000000
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.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.java.run;
-
-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.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
-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;
-
-/** Creates run configurations for java_test and android_robolectric_test. */
-public class BlazeJavaTestRunConfigurationFactory 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.JAVA_TEST, Kind.ANDROID_ROBOLECTRIC_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);
-
-    BlazeCommandRunConfigurationCommonState state =
-        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    if (state != null) {
-      state.getCommandState().setCommand(BlazeCommandName.TEST);
-    }
-    blazeConfig.setGeneratedName();
-  }
-}
diff --git a/java/src/com/google/idea/blaze/java/run/QualifiedClassNameHeuristic.java b/java/src/com/google/idea/blaze/java/run/QualifiedClassNameHeuristic.java
new file mode 100644
index 0000000..164e704
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/QualifiedClassNameHeuristic.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.java.run;
+
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Matches test targets to source files based on fully qualified class names. */
+public class QualifiedClassNameHeuristic implements TestTargetHeuristic {
+
+  @Override
+  public boolean matchesSource(
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize) {
+    if (!(sourcePsiFile instanceof PsiClassOwner)) {
+      return false;
+    }
+    String targetName = target.key.label.targetName().toString();
+    if (!targetName.contains(".")) {
+      return false;
+    }
+    for (PsiClass psiClass : ((PsiClassOwner) sourcePsiFile).getClasses()) {
+      String fqcn = psiClass.getQualifiedName();
+      if (fqcn != null && fqcn.endsWith(targetName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
index 1609e30..12f0075 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+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.BlazeConfigurationNameBuilder;
@@ -34,7 +35,6 @@
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiModifier;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -44,7 +44,6 @@
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
 /** Producer for run configurations related to Java test classes in Blaze. */
 public class BlazeJavaTestClassConfigurationProducer
@@ -54,49 +53,70 @@
     super(BlazeCommandRunConfigurationType.getInstance());
   }
 
-  @Override
-  protected boolean doSetupConfigFromContext(
-      @NotNull BlazeCommandRunConfiguration configuration,
-      @NotNull ConfigurationContext context,
-      @NotNull Ref<PsiElement> sourceElement) {
+  private static class TestLocation {
+    final PsiClass testClass;
+    final Label blazeTarget;
 
-    final Location contextLocation = context.getLocation();
-    assert contextLocation != null;
-    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+    private TestLocation(PsiClass testClass, Label blazeTarget) {
+      this.testClass = testClass;
+      this.blazeTarget = blazeTarget;
+    }
+  }
+
+  /**
+   * Returns the {@link TestLocation} corresponding to the single selected JUnit test class, or
+   * {@code null} if something else is selected.
+   */
+  @Nullable
+  private static TestLocation getSingleJUnitTestClass(ConfigurationContext context) {
+    Location<?> location = context.getLocation();
     if (location == null) {
-      return false;
+      return null;
+    }
+    location = JavaExecutionUtil.stepIntoSingleClass(location);
+    if (location == null) {
+      return null;
     }
 
+    // check for contexts handled by a different producer
     if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
-      // handled by a different producer
-      return false;
+      return null;
     }
     if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
-      return false;
+      return null;
+    }
+    if (TestMethodSelectionUtil.getSelectedMethods(context) != null) {
+      return null;
     }
 
     PsiClass testClass = JUnitUtil.getTestClass(location);
-    if (testClass == null) {
-      return false;
+    if (testClass == null || testClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
+      return null;
     }
-    if (testClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
-      return false;
-    }
-    sourceElement.set(testClass);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(testClass);
     TargetIdeInfo target = RunUtil.targetForTestClass(testClass, testSize);
-    if (target == null) {
+    return target != null ? new TestLocation(testClass, target.key.label) : null;
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    TestLocation location = getSingleJUnitTestClass(context);
+    if (location == null) {
       return false;
     }
+    sourceElement.set(location.testClass);
+    configuration.setTarget(location.blazeTarget);
 
-    configuration.setTarget(target.key.label);
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState == null) {
       return false;
     }
-    String testFilter = getTestFilter(testClass);
+    String testFilter = getTestFilter(location.testClass);
     if (testFilter == null) {
       return false;
     }
@@ -108,47 +128,30 @@
     flags.add(BlazeFlags.TEST_FILTER + "=" + testFilter);
     handlerState.getBlazeFlagsState().setRawFlags(flags);
 
-    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
-    nameBuilder.setTargetString(testClass.getName());
-    configuration.setName(nameBuilder.build());
+    String name =
+        new BlazeConfigurationNameBuilder(configuration)
+            .setTargetString(location.testClass.getName())
+            .build();
+    configuration.setName(name);
     configuration.setNameChangedByUser(true); // don't revert to generated name
     return true;
   }
 
   @Override
   protected boolean doIsConfigFromContext(
-      @NotNull BlazeCommandRunConfiguration configuration, @NotNull ConfigurationContext context) {
-
-    final Location contextLocation = context.getLocation();
-    assert contextLocation != null;
-    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    TestLocation location = getSingleJUnitTestClass(context);
     if (location == null) {
       return false;
     }
-
-    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
-      // handled by a different producer
-      return false;
-    }
-    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
-      return false;
-    }
-
-    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
-    if (methodLocation != null) {
-      return false;
-    }
-
-    PsiClass testClass = JUnitUtil.getTestClass(location);
-    if (testClass == null) {
-      return false;
-    }
-
-    return checkIfAttributesAreTheSame(configuration, testClass);
+    return checkIfAttributesAreTheSame(configuration, location);
   }
 
   private boolean checkIfAttributesAreTheSame(
-      @NotNull BlazeCommandRunConfiguration configuration, @NotNull PsiClass testClass) {
+      BlazeCommandRunConfiguration configuration, TestLocation location) {
+    if (!location.blazeTarget.equals(configuration.getTarget())) {
+      return false;
+    }
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState == null) {
@@ -157,11 +160,9 @@
     if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
-    String filter = getTestFilter(testClass);
-    if (filter == null) {
-      return false;
-    }
-    return Objects.equals(BlazeFlags.TEST_FILTER + "=" + filter, handlerState.getTestFilterFlag());
+    String filter = getTestFilter(location.testClass);
+    return filter != null
+        && Objects.equals(BlazeFlags.TEST_FILTER + "=" + filter, handlerState.getTestFilterFlag());
   }
 
   @Nullable
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
index b1cabb3..e97ca5f 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+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.BlazeConfigurationNameBuilder;
@@ -41,21 +42,24 @@
 public class BlazeJavaTestMethodConfigurationProducer
     extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
 
-  private static class SelectedMethodInfo {
+  private static class TestMethodContext {
     private final PsiMethod firstMethod;
     private final PsiClass containingClass;
     private final List<String> methodNames;
     private final String testFilterFlag;
+    private final Label blazeTarget;
 
-    public SelectedMethodInfo(
+    TestMethodContext(
         PsiMethod firstMethod,
         PsiClass containingClass,
         List<String> methodNames,
-        String testFilterFlag) {
+        String testFilterFlag,
+        Label blazeTarget) {
       this.firstMethod = firstMethod;
       this.containingClass = containingClass;
       this.methodNames = methodNames;
       this.testFilterFlag = testFilterFlag;
+      this.blazeTarget = blazeTarget;
     }
   }
 
@@ -69,23 +73,17 @@
       ConfigurationContext context,
       Ref<PsiElement> sourceElement) {
 
-    SelectedMethodInfo methodInfo = getSelectedMethodInfo(context);
-    if (methodInfo == null) {
+    TestMethodContext methodContext = getSelectedMethodContext(context);
+    if (methodContext == null) {
       return false;
     }
 
     // PatternConfigurationProducer also chooses the first method as its source element.
     // As long as we choose an element at the same PSI hierarchy level,
     // PatternConfigurationProducer won't override our configuration.
-    sourceElement.set(methodInfo.firstMethod);
+    sourceElement.set(methodContext.firstMethod);
 
-    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(methodInfo.firstMethod);
-    TargetIdeInfo target = RunUtil.targetForTestClass(methodInfo.containingClass, testSize);
-    if (target == null) {
-      return false;
-    }
-
-    configuration.setTarget(target.key.label);
+    configuration.setTarget(methodContext.blazeTarget);
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState == null) {
@@ -96,7 +94,7 @@
     // remove old test filter flag if present
     List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
     flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
-    flags.add(methodInfo.testFilterFlag);
+    flags.add(methodContext.testFilterFlag);
     if (!flags.contains(BlazeFlags.DISABLE_TEST_SHARDING)) {
       flags.add(BlazeFlags.DISABLE_TEST_SHARDING);
     }
@@ -106,7 +104,7 @@
     nameBuilder.setTargetString(
         String.format(
             "%s.%s",
-            methodInfo.containingClass.getName(), String.join(",", methodInfo.methodNames)));
+            methodContext.containingClass.getName(), String.join(",", methodContext.methodNames)));
     configuration.setName(nameBuilder.build());
     configuration.setNameChangedByUser(true); // don't revert to generated name
     return true;
@@ -124,17 +122,14 @@
       return false;
     }
 
-    SelectedMethodInfo methodInfo = getSelectedMethodInfo(context);
-    if (methodInfo == null) {
-      return false;
-    }
-
-    List<String> flags = handlerState.getBlazeFlagsState().getRawFlags();
-    return flags.contains(methodInfo.testFilterFlag);
+    TestMethodContext methodContext = getSelectedMethodContext(context);
+    return methodContext != null
+        && handlerState.getBlazeFlagsState().getRawFlags().contains(methodContext.testFilterFlag)
+        && methodContext.blazeTarget.equals(configuration.getTarget());
   }
 
   @Nullable
-  private static SelectedMethodInfo getSelectedMethodInfo(ConfigurationContext context) {
+  private static TestMethodContext getSelectedMethodContext(ConfigurationContext context) {
     if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
       // handled by a different producer
       return null;
@@ -155,6 +150,13 @@
         return null;
       }
     }
+
+    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(firstMethod);
+    TargetIdeInfo target = RunUtil.targetForTestClass(containingClass, testSize);
+    if (target == null) {
+      return null;
+    }
+
     String testFilter =
         BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(containingClass, selectedMethods);
     if (testFilter == null) {
@@ -164,6 +166,7 @@
     List<String> methodNames =
         selectedMethods.stream().map(PsiMethod::getName).sorted().collect(Collectors.toList());
     final String testFilterFlag = BlazeFlags.TEST_FILTER + "=" + testFilter;
-    return new SelectedMethodInfo(firstMethod, containingClass, methodNames, testFilterFlag);
+    return new TestMethodContext(
+        firstMethod, containingClass, methodNames, testFilterFlag, target.key.label);
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
index 4260daa..73ec889 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.java.JavaConfigurationProducerList;
 import com.intellij.execution.RunConfigurationProducerService;
 import com.intellij.execution.actions.RunConfigurationProducer;
 import com.intellij.ide.plugins.IdeaPluginDescriptor;
@@ -32,15 +33,8 @@
 /** Suppresses certain non-Blaze configuration producers in Blaze projects. */
 public class NonBlazeProducerSuppressor extends AbstractProjectComponent {
 
-  private static final Collection<Class<? extends RunConfigurationProducer<?>>>
-      PRODUCERS_TO_SUPPRESS =
-          ImmutableList.of(
-              com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
-              com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
-              com.intellij.execution.junit.TestClassConfigurationProducer.class,
-              com.intellij.execution.junit.TestMethodConfigurationProducer.class,
-              com.intellij.execution.junit.PatternConfigurationProducer.class,
-              com.intellij.execution.application.ApplicationConfigurationProducer.class);
+  private static final String KOTLIN_PLUGIN_ID = "org.jetbrains.kotlin";
+  private static final String ANDROID_PLUGIN_ID = "org.jetbrains.android";
 
   private static final ImmutableList<String> KOTLIN_PRODUCERS =
       ImmutableList.of(
@@ -48,15 +42,26 @@
           "org.jetbrains.kotlin.idea.run.KotlinPatternConfigurationProducer",
           "org.jetbrains.kotlin.idea.run.KotlinRunConfigurationProducer");
 
-  private static Collection<Class<? extends RunConfigurationProducer<?>>> getKotlinProducers() {
-    // rather than compiling against the Kotlin plugin, and including a switch in the our
+  private static final ImmutableList<String> ANDROID_PRODUCERS =
+      ImmutableList.of(
+          "com.android.tools.idea.run.AndroidConfigurationProducer",
+          "com.android.tools.idea.testartifacts.instrumented.AndroidTestConfigurationProducer",
+          "com.android.tools.idea.testartifacts.junit.TestClassAndroidConfigurationProducer",
+          "com.android.tools.idea.testartifacts.junit.TestDirectoryAndroidConfigurationProducer",
+          "com.android.tools.idea.testartifacts.junit.TestMethodAndroidConfigurationProducer",
+          "com.android.tools.idea.testartifacts.junit.TestPackageAndroidConfigurationProducer",
+          "com.android.tools.idea.testartifacts.junit.TestPatternConfigurationProducer");
+
+  private static Collection<Class<? extends RunConfigurationProducer<?>>> getProducers(
+      String pluginId, Collection<String> qualifiedClassNames) {
+    // rather than compiling against additional plugins, and including a switch in the our
     // plugin.xml, just get the classes manually via the plugin class loader.
-    IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId("org.jetbrains.kotlin"));
+    IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(pluginId));
     if (plugin == null || !plugin.isEnabled()) {
       return ImmutableList.of();
     }
     ClassLoader loader = plugin.getPluginClassLoader();
-    return KOTLIN_PRODUCERS
+    return qualifiedClassNames
         .stream()
         .map((qualifiedName) -> loadClass(loader, qualifiedName))
         .filter(Objects::nonNull)
@@ -92,8 +97,9 @@
     RunConfigurationProducerService producerService =
         RunConfigurationProducerService.getInstance(project);
     ImmutableList.<Class<? extends RunConfigurationProducer<?>>>builder()
-        .addAll(PRODUCERS_TO_SUPPRESS)
-        .addAll(getKotlinProducers())
+        .addAll(JavaConfigurationProducerList.PRODUCERS_TO_SUPPRESS)
+        .addAll(getProducers(KOTLIN_PLUGIN_ID, KOTLIN_PRODUCERS))
+        .addAll(getProducers(ANDROID_PLUGIN_ID, ANDROID_PRODUCERS))
         .build()
         .forEach(producerService::addIgnoredProducer);
   }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
index 1bc5250..6ea06e0 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
@@ -47,7 +47,7 @@
     }
     PsiClassListCellRenderer renderer = new PsiClassListCellRenderer();
     classes.sort(renderer.getComparator());
-    // JBList has no generics in AS 2.2. TODO: Add generics here when we migrate to AS 2.3.
+    // #api171 add generics to JBList.
     JBList list = new JBList(classes);
     list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     list.setCellRenderer(renderer);
@@ -57,6 +57,7 @@
         .setMovable(false)
         .setResizable(false)
         .setRequestFocus(true)
+        .setCancelOnWindowDeactivation(false)
         .setItemChoosenCallback(
             () -> callbackOnClassSelection.accept((PsiClass) list.getSelectedValue()))
         .createPopup()
diff --git a/java/src/com/google/idea/blaze/java/settings/BlazeJavaUserSettings.java b/java/src/com/google/idea/blaze/java/settings/BlazeJavaUserSettings.java
index d9b9d57..a30b8b3 100644
--- a/java/src/com/google/idea/blaze/java/settings/BlazeJavaUserSettings.java
+++ b/java/src/com/google/idea/blaze/java/settings/BlazeJavaUserSettings.java
@@ -17,13 +17,11 @@
 
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.components.PersistentStateComponent;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.components.State;
 import com.intellij.openapi.components.Storage;
 import com.intellij.util.xmlb.XmlSerializerUtil;
-import org.jetbrains.annotations.NotNull;
 
 /** Java-specific user settings. */
 @State(name = "BlazeJavaUserSettings", storages = @Storage("blaze.java.user.settings.xml"))
@@ -31,12 +29,9 @@
   private boolean useJarCache = getDefaultJarCacheValue();
   private boolean attachSourcesByDefault = false;
   private boolean attachSourcesOnDemand = false;
-  private boolean migrated;
 
   public static BlazeJavaUserSettings getInstance() {
-    BlazeJavaUserSettings settings = ServiceManager.getService(BlazeJavaUserSettings.class);
-    settings.migrate();
-    return settings;
+    return ServiceManager.getService(BlazeJavaUserSettings.class);
   }
 
   private static boolean getDefaultJarCacheValue() {
@@ -44,7 +39,6 @@
   }
 
   @Override
-  @NotNull
   public BlazeJavaUserSettings getState() {
     return this;
   }
@@ -52,20 +46,6 @@
   @Override
   public void loadState(BlazeJavaUserSettings state) {
     XmlSerializerUtil.copyBean(state, this);
-    migrate();
-  }
-
-  /**
-   * Added in 1.8, can be removed ~2.2. When this is removed, java settings can no longer be
-   * migrated. (This is non-catastrophic though -- the settings will just reset)
-   */
-  private void migrate() {
-    if (!migrated) {
-      BlazeUserSettings userSettings = BlazeUserSettings.getInstance();
-      this.attachSourcesByDefault = userSettings.getAttachSourcesByDefault();
-      this.attachSourcesOnDemand = userSettings.getAttachSourcesOnDemand();
-      this.migrated = true;
-    }
   }
 
   public boolean getUseJarCache() {
@@ -91,14 +71,4 @@
   public void setAttachSourcesOnDemand(boolean attachSourcesOnDemand) {
     this.attachSourcesOnDemand = attachSourcesOnDemand;
   }
-
-  @SuppressWarnings("unused") // Used by bean serialization
-  public boolean getMigrated() {
-    return migrated;
-  }
-
-  @SuppressWarnings("unused") // Used by bean serialization
-  public void setMigrated(boolean migrated) {
-    this.migrated = migrated;
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaLibrarySource.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaLibrarySource.java
index 0fd68e1..fea7d94 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaLibrarySource.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaLibrarySource.java
@@ -20,7 +20,7 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
-import java.util.Collection;
+import java.util.List;
 import java.util.function.Predicate;
 import javax.annotation.Nullable;
 
@@ -33,12 +33,12 @@
   }
 
   @Override
-  public Collection<? extends BlazeLibrary> getLibraries() {
+  public List<? extends BlazeLibrary> getLibraries() {
     BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
       return ImmutableList.of();
     }
-    return syncData.importResult.libraries.values();
+    return syncData.importResult.libraries.values().asList();
   }
 
   @Nullable
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
index 1d31d91..8748288 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -58,7 +58,6 @@
 import com.google.idea.blaze.java.sync.projectstructure.Jdks;
 import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
 import com.google.idea.sdkcompat.transactions.Transactions;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
 import com.intellij.openapi.project.Project;
@@ -214,18 +213,13 @@
 
   private static void setProjectSdkAndLanguageLevel(
       final Project project, final Sdk sdk, final LanguageLevel javaLanguageLevel) {
-    Transactions.submitTransactionAndWait(
-        () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(
-                    () -> {
-                      ProjectRootManagerEx rootManager =
-                          ProjectRootManagerEx.getInstanceEx(project);
-                      rootManager.setProjectSdk(sdk);
-                      LanguageLevelProjectExtension ext =
-                          LanguageLevelProjectExtension.getInstance(project);
-                      ext.setLanguageLevel(javaLanguageLevel);
-                    }));
+    Transactions.submitWriteActionTransactionAndWait(
+        () -> {
+          ProjectRootManagerEx rootManager = ProjectRootManagerEx.getInstanceEx(project);
+          rootManager.setProjectSdk(sdk);
+          LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(project);
+          ext.setLanguageLevel(javaLanguageLevel);
+        });
   }
 
   @Override
diff --git a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
index c39eb64..a950fd4 100644
--- a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
+++ b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.libraries.BlazeLibraryCollector;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.libraries.JarCache;
 import com.google.idea.blaze.java.libraries.SourceJarManager;
@@ -38,8 +39,9 @@
   public void addFilesToPrefetch(
       Project project,
       ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
       BlazeProjectData blazeProjectData,
-      Collection<File> files) {
+      Set<File> files) {
     BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
       return;
@@ -70,7 +72,7 @@
   }
 
   @Override
-  public Set<String> prefetchSrcFileExtensions() {
+  public Set<String> prefetchFileExtensions() {
     return ImmutableSet.of("java");
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
index c089e53..63d0c08 100644
--- a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
+++ b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
@@ -34,6 +34,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.LibraryKey;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -222,9 +223,9 @@
       WorkspaceBuilder workspaceBuilder,
       TargetMap targetMap,
       Map<LibraryKey, BlazeJarLibrary> result) {
-    List<TargetKey> version1Roots = Lists.newArrayList();
-    List<TargetKey> immutableRoots = Lists.newArrayList();
-    List<TargetKey> mutableRoots = Lists.newArrayList();
+    List<TargetKey> version1Targets = Lists.newArrayList();
+    List<TargetKey> immutableTargets = Lists.newArrayList();
+    List<TargetKey> mutableTargets = Lists.newArrayList();
     for (TargetKey targetKey : workspaceBuilder.directDeps) {
       TargetIdeInfo target = targetMap.get(targetKey);
       if (target == null) {
@@ -236,17 +237,17 @@
       }
       switch (protoLibraryLegacyInfo.apiFlavor) {
         case VERSION_1:
-          version1Roots.add(targetKey);
+          version1Targets.add(targetKey);
           break;
         case IMMUTABLE:
-          immutableRoots.add(targetKey);
+          immutableTargets.add(targetKey);
           break;
         case MUTABLE:
-          mutableRoots.add(targetKey);
+          mutableTargets.add(targetKey);
           break;
         case BOTH:
-          mutableRoots.add(targetKey);
-          immutableRoots.add(targetKey);
+          mutableTargets.add(targetKey);
+          immutableTargets.add(targetKey);
           break;
         default:
           // Can't happen
@@ -255,25 +256,20 @@
     }
 
     addProtoLegacyLibrariesFromDirectDepsForFlavor(
-        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1, version1Roots, result);
+        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1, version1Targets, result);
     addProtoLegacyLibrariesFromDirectDepsForFlavor(
-        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE, immutableRoots, result);
+        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE, immutableTargets, result);
     addProtoLegacyLibrariesFromDirectDepsForFlavor(
-        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.MUTABLE, mutableRoots, result);
+        targetMap, ProtoLibraryLegacyInfo.ApiFlavor.MUTABLE, mutableTargets, result);
   }
 
   private void addProtoLegacyLibrariesFromDirectDepsForFlavor(
       TargetMap targetMap,
       ProtoLibraryLegacyInfo.ApiFlavor apiFlavor,
-      List<TargetKey> roots,
+      List<TargetKey> targetKeys,
       Map<LibraryKey, BlazeJarLibrary> result) {
-    Set<TargetKey> seen = Sets.newHashSet();
-    while (!roots.isEmpty()) {
-      TargetKey targetKey = roots.remove(roots.size() - 1);
-      if (!seen.add(targetKey)) {
-        continue;
-      }
-      TargetIdeInfo target = targetMap.get(targetKey);
+    for (TargetKey key : targetKeys) {
+      TargetIdeInfo target = targetMap.get(key);
       if (target == null) {
         continue;
       }
@@ -304,12 +300,6 @@
           result.put(library.key, library);
         }
       }
-
-      for (Dependency dep : target.dependencies) {
-        if (dep.dependencyType == DependencyType.COMPILE_TIME) {
-          roots.add(dep.targetKey);
-        }
-      }
     }
   }
 
@@ -346,7 +336,15 @@
       // Add self, so we pick up our own gen jars if in working set
       workspaceBuilder.directDeps.add(targetKey);
       for (Dependency dep : target.dependencies) {
-        if (dep.dependencyType == DependencyType.COMPILE_TIME) {
+        if (dep.dependencyType != DependencyType.COMPILE_TIME) {
+          continue;
+        }
+        // forward deps from java_proto_library
+        TargetIdeInfo depTarget = targetMap.get(dep.targetKey);
+        if (depTarget != null && depTarget.kind == Kind.JAVA_PROTO_LIBRARY) {
+          workspaceBuilder.directDeps.addAll(
+              depTarget.dependencies.stream().map(d -> d.targetKey).collect(Collectors.toList()));
+        } else {
           workspaceBuilder.directDeps.add(dep.targetKey);
         }
       }
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
index 474b729..5199ec9 100644
--- a/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
+++ b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
@@ -94,8 +94,8 @@
     return sourceTargets;
   }
 
-  private boolean canImportAsSource(TargetIdeInfo target) {
-    return !target.kindIsOneOf(Kind.JAVA_WRAP_CC, Kind.JAVA_IMPORT);
+  public static boolean canImportAsSource(TargetIdeInfo target) {
+    return !target.kindIsOneOf(Kind.JAVA_WRAP_CC, Kind.JAVA_IMPORT, Kind.SCALA_IMPORT);
   }
 
   private boolean anyNonGeneratedSources(Collection<ArtifactLocation> sources) {
diff --git a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
index 550c00c..1cd2cee 100644
--- a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
@@ -132,7 +132,7 @@
             removedFiles);
 
     ListenableFuture<?> fetchFuture =
-        PrefetchService.getInstance().prefetchFiles(project, updatedFiles);
+        PrefetchService.getInstance().prefetchFiles(project, updatedFiles, true);
     if (!FutureUtil.waitForFuture(context, fetchFuture)
         .timed("FetchJdeps")
         .withProgressMessage("Reading jdeps files...")
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
index 704f4c4..a6e59ce 100644
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
@@ -15,9 +15,7 @@
  */
 package com.google.idea.blaze.java.sync.projectstructure;
 
-import static com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil.createAndAddSDK;
 import static com.intellij.openapi.util.io.FileUtil.notNullize;
-import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
 import static java.util.Collections.emptyList;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -29,33 +27,35 @@
 import com.intellij.openapi.projectRoots.JavaSdkVersion;
 import com.intellij.openapi.projectRoots.ProjectJdkTable;
 import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
+import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.pom.java.LanguageLevel;
 import com.intellij.util.SystemProperties;
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NonNls;
-import org.jetbrains.annotations.NotNull;
 
 /** Utility methods related to IDEA JDKs. */
 public class Jdks {
-  @NonNls private static final LanguageLevel DEFAULT_LANG_LEVEL = LanguageLevel.JDK_1_7;
+
+  private static final Logger logger = Logger.getInstance(Jdks.class);
 
   @Nullable
   public static Sdk chooseOrCreateJavaSdk(LanguageLevel langLevel) {
-    for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
-      if (isApplicableJdk(sdk, langLevel)) {
-        return sdk;
-      }
+    Sdk existing = findClosestMatch(langLevel);
+    if (existing != null) {
+      return existing;
     }
     String jdkHomePath = null;
     for (DefaultSdkProvider defaultSdkProvider : DefaultSdkProvider.EP_NAME.getExtensions()) {
-      File sdk = defaultSdkProvider.provideSdkForLanguage(LanguageClass.JAVA);
-      if (sdk != null) {
-        jdkHomePath = sdk.getPath();
+      File sdkRoot = defaultSdkProvider.provideSdkForLanguage(LanguageClass.JAVA);
+      if (sdkRoot != null) {
+        jdkHomePath = sdkRoot.getPath();
         break;
       }
     }
@@ -63,39 +63,43 @@
     if (jdkHomePath == null) {
       jdkHomePath = getJdkHomePath(langLevel);
     }
-
-    if (jdkHomePath == null) {
-      return null;
-    }
-
-    return createJdk(jdkHomePath);
-  }
-
-  public static boolean isApplicableJdk(@NotNull Sdk jdk, @Nullable LanguageLevel langLevel) {
-    if (!(jdk.getSdkType() instanceof JavaSdk)) {
-      return false;
-    }
-    if (langLevel == null) {
-      langLevel = DEFAULT_LANG_LEVEL;
-    }
-    JavaSdkVersion version = JavaSdk.getInstance().getVersion(jdk);
-    if (version != null) {
-      //noinspection TestOnlyProblems
-      return hasMatchingLangLevel(version, langLevel);
-    }
-    return false;
+    return jdkHomePath != null ? createJdk(jdkHomePath) : null;
   }
 
   @Nullable
-  public static String getJdkHomePath(@NotNull LanguageLevel langLevel) {
-    Collection<String> jdkHomePaths =
-        new ArrayList<String>(JavaSdk.getInstance().suggestHomePaths());
+  @VisibleForTesting
+  static Sdk findClosestMatch(LanguageLevel langLevel) {
+    return Arrays.stream(ProjectJdkTable.getInstance().getAllJdks())
+        .filter(
+            sdk -> {
+              LanguageLevel level = getJavaLanguageLevel(sdk);
+              return level != null && level.isAtLeast(langLevel);
+            })
+        .min(Comparator.comparing(Jdks::getJavaLanguageLevel))
+        .orElse(null);
+  }
+
+  /**
+   * Returns null if the SDK is not a java JDK, or doesn't have a recognized java langauge level.
+   */
+  @Nullable
+  private static LanguageLevel getJavaLanguageLevel(Sdk sdk) {
+    if (!(sdk.getSdkType() instanceof JavaSdk)) {
+      return null;
+    }
+    JavaSdkVersion version = JavaSdk.getInstance().getVersion(sdk);
+    return version != null ? version.getMaxLanguageLevel() : null;
+  }
+
+  @Nullable
+  private static String getJdkHomePath(LanguageLevel langLevel) {
+    Collection<String> jdkHomePaths = new ArrayList<>(JavaSdk.getInstance().suggestHomePaths());
     if (jdkHomePaths.isEmpty()) {
       return null;
     }
     // prefer jdk path of getJavaHome(), since we have to allow access to it in tests
     // see AndroidProjectDataServiceTest#testImportData()
-    final List<String> list = new ArrayList<String>();
+    final List<String> list = new ArrayList<>();
     String javaHome = SystemProperties.getJavaHome();
 
     if (javaHome != null && !javaHome.isEmpty()) {
@@ -112,14 +116,12 @@
     return getBestJdkHomePath(list, langLevel);
   }
 
-  @VisibleForTesting
   @Nullable
-  static String getBestJdkHomePath(
-      @NotNull Collection<String> jdkHomePaths, @NotNull LanguageLevel langLevel) {
+  private static String getBestJdkHomePath(List<String> jdkHomePaths, LanguageLevel langLevel) {
     // Search for JDKs in both the suggest folder and all its sub folders.
     List<String> roots = Lists.newArrayList();
     for (String jdkHomePath : jdkHomePaths) {
-      if (isNotEmpty(jdkHomePath)) {
+      if (StringUtil.isNotEmpty(jdkHomePath)) {
         roots.add(jdkHomePath);
         roots.addAll(getChildrenPaths(jdkHomePath));
       }
@@ -127,8 +129,7 @@
     return getBestJdk(roots, langLevel);
   }
 
-  @NotNull
-  private static List<String> getChildrenPaths(@NotNull String dirPath) {
+  private static List<String> getChildrenPaths(String dirPath) {
     File dir = new File(dirPath);
     if (!dir.isDirectory()) {
       return emptyList();
@@ -144,48 +145,16 @@
   }
 
   @Nullable
-  private static String getBestJdk(
-      @NotNull List<String> jdkRoots, @NotNull LanguageLevel langLevel) {
-    String bestJdk = null;
-    for (String jdkRoot : jdkRoots) {
-      if (JavaSdk.getInstance().isValidSdkHome(jdkRoot)) {
-        if (bestJdk == null && hasMatchingLangLevel(jdkRoot, langLevel)) {
-          bestJdk = jdkRoot;
-        } else if (bestJdk != null) {
-          bestJdk = selectJdk(bestJdk, jdkRoot, langLevel);
-        }
-      }
-    }
-    return bestJdk;
+  private static String getBestJdk(List<String> jdkRoots, LanguageLevel langLevel) {
+    return jdkRoots
+        .stream()
+        .filter(root -> JavaSdk.getInstance().isValidSdkHome(root))
+        .filter(root -> getVersion(root).getMaxLanguageLevel().isAtLeast(langLevel))
+        .min(Comparator.comparing(o -> getVersion(o).getMaxLanguageLevel()))
+        .orElse(null);
   }
 
-  @Nullable
-  private static String selectJdk(
-      @NotNull String jdk1, @NotNull String jdk2, @NotNull LanguageLevel langLevel) {
-    if (hasMatchingLangLevel(jdk1, langLevel)) {
-      return jdk1;
-    }
-    if (hasMatchingLangLevel(jdk2, langLevel)) {
-      return jdk2;
-    }
-    return null;
-  }
-
-  private static boolean hasMatchingLangLevel(
-      @NotNull String jdkRoot, @NotNull LanguageLevel langLevel) {
-    JavaSdkVersion version = getVersion(jdkRoot);
-    return hasMatchingLangLevel(version, langLevel);
-  }
-
-  @VisibleForTesting
-  static boolean hasMatchingLangLevel(
-      @NotNull JavaSdkVersion jdkVersion, @NotNull LanguageLevel langLevel) {
-    LanguageLevel max = jdkVersion.getMaxLanguageLevel();
-    return max.isAtLeast(langLevel);
-  }
-
-  @NotNull
-  private static JavaSdkVersion getVersion(@NotNull String jdkRoot) {
+  private static JavaSdkVersion getVersion(String jdkRoot) {
     String version = JavaSdk.getInstance().getVersionString(jdkRoot);
     if (version == null) {
       return JavaSdkVersion.JDK_1_0;
@@ -195,11 +164,10 @@
   }
 
   @Nullable
-  public static Sdk createJdk(@NotNull String jdkHomePath) {
-    Sdk jdk = createAndAddSDK(jdkHomePath, JavaSdk.getInstance());
+  private static Sdk createJdk(String jdkHomePath) {
+    Sdk jdk = SdkConfigurationUtil.createAndAddSDK(jdkHomePath, JavaSdk.getInstance());
     if (jdk == null) {
-      String msg = String.format("Unable to create JDK from path '%1$s'", jdkHomePath);
-      Logger.getInstance(Jdks.class).error(msg);
+      logger.error(String.format("Unable to create JDK from path '%1$s'", jdkHomePath));
     }
     return jdk;
   }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
index cadef54..1ba5835 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
@@ -30,9 +30,9 @@
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.JavaSourcePackage;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.PackageManifest;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
@@ -78,7 +78,7 @@
         FileDiffer.updateFiles(fileDiffState, fileToLabelMap.keySet(), updatedFiles, removedFiles);
 
     ListenableFuture<?> fetchFuture =
-        PrefetchService.getInstance().prefetchFiles(project, updatedFiles);
+        PrefetchService.getInstance().prefetchFiles(project, updatedFiles, true);
     if (!FutureUtil.waitForFuture(context, fetchFuture)
         .timed("FetchPackageManifests")
         .withProgressMessage("Reading package manifests...")
@@ -132,7 +132,7 @@
     }
   }
 
-  private static ArtifactLocation fromProto(PackageManifestOuterClass.ArtifactLocation location) {
+  private static ArtifactLocation fromProto(IntellijIdeInfo.ArtifactLocation location) {
     String relativePath = location.getRelativePath();
     String rootExecutionPathFragment = location.getRootExecutionPathFragment();
     if (!location.getIsNewExternalVersion() && location.getIsExternal()) {
diff --git a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
index 14afec5..ff8f5f0 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
@@ -20,7 +20,6 @@
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.HashMultimap;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -51,6 +50,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -258,19 +258,21 @@
     }
 
     // Sort source roots into their respective directories
-    Multimap<WorkspacePath, SourceRoot> sourceDirectoryToSourceRoots = HashMultimap.create();
+    Map<WorkspacePath, Multiset<SourceRoot>> sourceDirectoryToSourceRoots = new HashMap<>();
     for (SourceRoot sourceRoot : sourceRootsPerFile) {
-      sourceDirectoryToSourceRoots.put(sourceRoot.workspacePath, sourceRoot);
+      sourceDirectoryToSourceRoots
+          .computeIfAbsent(sourceRoot.workspacePath, k -> HashMultiset.create())
+          .add(sourceRoot);
     }
 
     // Create a mapping from directory to package prefix
     Map<WorkspacePath, SourceRoot> workspacePathToSourceRoot = Maps.newHashMap();
     for (WorkspacePath workspacePath : sourceDirectoryToSourceRoots.keySet()) {
-      Collection<SourceRoot> sources = sourceDirectoryToSourceRoots.get(workspacePath);
+      Multiset<SourceRoot> sources = sourceDirectoryToSourceRoots.get(workspacePath);
       Multiset<String> packages = HashMultiset.create();
 
-      for (SourceRoot source : sources) {
-        packages.add(source.packagePrefix);
+      for (Multiset.Entry<SourceRoot> entry : sources.entrySet()) {
+        packages.setCount(entry.getElement().packagePrefix, entry.getCount());
       }
 
       final String directoryPackagePrefix;
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
index 26b97fa..7d76ce7 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
@@ -21,7 +21,6 @@
 import com.intellij.ui.JBColor;
 import java.awt.Color;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
 /** Changes the color for unsynced files. */
 public class BlazeJavaSyncStatusEditorTabColorProvider implements EditorTabColorProvider {
@@ -30,7 +29,7 @@
 
   @Nullable
   @Override
-  public Color getEditorTabColor(@NotNull Project project, @NotNull VirtualFile file) {
+  public Color getEditorTabColor(Project project, VirtualFile file) {
     if (file.getName().endsWith(".java") && SyncStatusHelper.isUnsynced(project, file)) {
       return UNSYNCED_COLOR;
     }
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
index c79888d..5692558 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -27,6 +27,7 @@
 import java.util.Set;
 
 class SyncStatusHelper {
+  private SyncStatusHelper() {}
 
   static boolean isUnsynced(Project project, VirtualFile virtualFile) {
     if (!virtualFile.isInLocalFileSystem()) {
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
index 3d57300..109d9e8 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -64,7 +64,8 @@
   public boolean validate() throws ConfigurationException {
     BlazeValidationResult validationResult = control.validate();
     if (validationResult.error != null) {
-      throw new ConfigurationException(validationResult.error.getError());
+      throw new ConfigurationException(
+          "<html><body>" + validationResult.error.getError() + "</body></html>");
     }
     return validationResult.success;
   }
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java
index b5b5b6a..d91566a 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java
@@ -50,7 +50,8 @@
   }
 
   private void init() {
-    control = new BlazeSelectWorkspaceControl(getProjectBuilder());
+    control =
+        new BlazeSelectWorkspaceControl(getProjectBuilder(), getWizardContext().getDisposable());
     this.component.add(control.getUiComponent());
     settingsInitialised = true;
   }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/CombinedTestHeuristicTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/CombinedTestHeuristicTest.java
new file mode 100644
index 0000000..3bf8255
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/CombinedTestHeuristicTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.java.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link TestTargetHeuristic} combinations. */
+@RunWith(JUnit4.class)
+public class CombinedTestHeuristicTest extends BlazeIntegrationTestCase {
+
+  @Before
+  public final void doSetup() {
+    BlazeProjectData blazeProjectData = MockBlazeProjectDataBuilder.builder(workspaceRoot).build();
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+
+    // required for IntelliJ to recognize annotations, JUnit version, etc.
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/runner/RunWith.java"),
+        "package org.junit.runner;"
+            + "public @interface RunWith {"
+            + "    Class<? extends Runner> value();"
+            + "}");
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/Test"), "package org.junit;", "public @interface Test {}");
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/runners/JUnit4"),
+        "package org.junit.runners;",
+        "public class JUnit4 {}");
+  }
+
+  @Test
+  public void testSizeAndJUnit4Combination() {
+    Collection<TargetIdeInfo> targets =
+        ImmutableList.of(
+            createTarget("//foo:SmallJUnit3Tests", TestSize.SMALL),
+            createTarget("//foo:MediumJUnit3Tests", TestSize.MEDIUM),
+            createTarget("//foo:LargeJUnit3Tests", TestSize.LARGE),
+            createTarget("//foo:SmallJUnit4Tests", TestSize.SMALL),
+            createTarget("//foo:MediumJUnit4Tests", TestSize.MEDIUM),
+            createTarget("//foo:LargeJUnit4Tests", TestSize.LARGE));
+
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/lib/JavaTest.java"),
+            "package com.google.lib;",
+            "import org.junit.Test;",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "@RunWith(JUnit4.class)",
+            "public class JavaTest {",
+            "  @Test",
+            "  public void testMethod1() {}",
+            "  @Test",
+            "  public void testMethod2() {}",
+            "}");
+    File source = new File(psiFile.getVirtualFile().getPath());
+
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), psiFile, source, targets, TestSize.LARGE);
+    assertThat(match).isEqualTo(Label.create("//foo:LargeJUnit4Tests"));
+
+    match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), psiFile, source, targets, TestSize.MEDIUM);
+    assertThat(match).isEqualTo(Label.create("//foo:MediumJUnit4Tests"));
+  }
+
+  private static TargetIdeInfo createTarget(String label, TestSize size) {
+    return TargetIdeInfo.builder()
+        .setLabel(label)
+        .setKind("java_test")
+        .setTestInfo(TestIdeInfo.builder().setTestSize(size))
+        .build();
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java
index 5470d05..7243dbf 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java
@@ -99,7 +99,7 @@
     PsiFile psiFile = workspace.createPsiFile(new WorkspacePath("foo/script_test.py"));
     File file = new File(psiFile.getVirtualFile().getPath());
     TargetIdeInfo target =
-        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("python_test").build();
+        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("py_test").build();
     assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, psiFile, file, null))
         .isFalse();
   }
@@ -108,7 +108,7 @@
   public void testNullPsiFileDoesNotMatch() {
     File file = new File("foo/script_test.py");
     TargetIdeInfo target =
-        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("python_test").build();
+        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("py_test").build();
     assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, null, file, null))
         .isFalse();
   }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/QualifiedClassNameHeuristicTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/QualifiedClassNameHeuristicTest.java
new file mode 100644
index 0000000..8b781b9
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/QualifiedClassNameHeuristicTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.java.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link QualifiedClassNameHeuristic}. */
+@RunWith(JUnit4.class)
+public class QualifiedClassNameHeuristicTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testMatchesQualifiedClassNames() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "public class JavaClass {}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:lib.JavaClass").setKind("java_test").build();
+    assertThat(
+            new QualifiedClassNameHeuristic()
+                .matchesSource(getProject(), target, psiFile, file, null))
+        .isTrue();
+
+    target =
+        TargetIdeInfo.builder().setLabel("//foo:google.lib.JavaClass").setKind("java_test").build();
+    assertThat(
+            new QualifiedClassNameHeuristic()
+                .matchesSource(getProject(), target, psiFile, file, null))
+        .isTrue();
+
+    target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:com.google.lib.JavaClass")
+            .setKind("java_test")
+            .build();
+    assertThat(
+            new QualifiedClassNameHeuristic()
+                .matchesSource(getProject(), target, psiFile, file, null))
+        .isTrue();
+  }
+
+  @Test
+  public void testDoesNotMatchUnqualifiedClassName() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "public class JavaClass {}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:JavaClass").setKind("java_test").build();
+    assertThat(
+            new QualifiedClassNameHeuristic()
+                .matchesSource(getProject(), target, psiFile, file, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testDoesNotMatchQualifiedClassNameWithAdditionalPathPrefix() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "public class JavaClass {}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+
+    TargetIdeInfo target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:foo.com.google.lib.JavaClass")
+            .setKind("java_test")
+            .build();
+    assertThat(
+            new QualifiedClassNameHeuristic()
+                .matchesSource(getProject(), target, psiFile, file, null))
+        .isFalse();
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
index 6c48f2f..5101f07 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
@@ -18,20 +18,24 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.actions.ConfigurationFromContext;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiClassOwner;
 import com.intellij.psi.PsiFile;
+import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -185,4 +189,116 @@
     assertThat(config.getName()).isEqualTo("Blaze test OuterClass");
     assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
   }
+
+  @Test
+  public void testConfigFromContextRecognizesItsOwnConfig() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(javaFile);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+    assertThat(config).isNotNull();
+
+    assertThat(new BlazeJavaTestClassConfigurationProducer().doIsConfigFromContext(config, context))
+        .isTrue();
+  }
+
+  @Test
+  public void testConfigWithDifferentLabelIgnored() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(javaFile);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+    assertThat(config).isNotNull();
+
+    // modify the label, and check that is enough for the producer to class it as different.
+    config.setTarget(Label.create("//java/com/google/test:TestClass2"));
+
+    assertThat(new BlazeJavaTestClassConfigurationProducer().doIsConfigFromContext(config, context))
+        .isFalse();
+  }
+
+  @Test
+  public void testConfigWithDifferentFilterIgnored() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(javaFile);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+
+    // modify the test filter, and check that is enough for the producer to class it as different.
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    flags.add(BlazeFlags.TEST_FILTER + "=com.google.test.OtherTestClass#");
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
+
+    assertThat(new BlazeJavaTestClassConfigurationProducer().doIsConfigFromContext(config, context))
+        .isFalse();
+  }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
index aa7d2a2..439ba6a 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
@@ -24,6 +24,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
@@ -34,6 +35,7 @@
 import com.intellij.execution.actions.ConfigurationFromContext;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiMethod;
+import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +48,116 @@
 
   @Test
   public void testProducedFromPsiMethod() {
+    // Arrange
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiMethod.class);
+
+    // Act
+    ConfigurationContext context = createContextFromPsi(method);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    ConfigurationFromContext fromContext = configurations.get(0);
+
+    // Assert
+    assertThat(configurations).hasSize(1);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestMethodConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo("--test_filter=com.google.test.TestClass#testMethod1$");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass.testMethod1");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+
+    BlazeCommandRunConfigurationCommonState state =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    assertThat(state.getBlazeFlagsState().getRawFlags()).contains(BlazeFlags.DISABLE_TEST_SHARDING);
+  }
+
+  @Test
+  public void testConfigFromContextRecognizesItsOwnConfig() {
+    PsiMethod method = setupGenericJunitTestClassAndBlazeTarget();
+    ConfigurationContext context = createContextFromPsi(method);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+
+    boolean isConfigFromContext =
+        new BlazeJavaTestMethodConfigurationProducer().doIsConfigFromContext(config, context);
+
+    assertThat(isConfigFromContext).isTrue();
+  }
+
+  @Test
+  public void testConfigWithDifferentLabelIsIgnored() {
+    // Arrange
+    PsiMethod method = setupGenericJunitTestClassAndBlazeTarget();
+    ConfigurationContext context = createContextFromPsi(method);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+    // modify the label, and check that is enough for the producer to class it as different.
+    config.setTarget(Label.create("//java/com/google/test:DifferentTestTarget"));
+
+    // Act
+    boolean isConfigFromContext =
+        new BlazeJavaTestMethodConfigurationProducer().doIsConfigFromContext(config, context);
+
+    // Assert
+    assertThat(isConfigFromContext).isFalse();
+  }
+
+  @Test
+  public void testConfigWithDifferentFilterIgnored() {
+    // Arrange
+    PsiMethod method = setupGenericJunitTestClassAndBlazeTarget();
+    ConfigurationContext context = createContextFromPsi(method);
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) context.getConfiguration().getConfiguration();
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+
+    // modify the test filter, and check that is enough for the producer to class it as different.
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    flags.add(BlazeFlags.TEST_FILTER + "=com.google.test.DifferentTestClass#");
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
+
+    // Act
+    boolean isConfigFromContext =
+        new BlazeJavaTestMethodConfigurationProducer().doIsConfigFromContext(config, context);
+
+    // Assert
+    assertThat(isConfigFromContext).isFalse();
+  }
+
+  /**
+   * Creates a JUnit test class and associated blaze target, and returns a PsiMethod from that
+   * class. Used when the implementation details (class name, target string, etc.) aren't relevant
+   * to the test.
+   */
+  private PsiMethod setupGenericJunitTestClassAndBlazeTarget() {
     PsiFile javaFile =
         createAndIndexFile(
             new WorkspacePath("java/com/google/test/TestClass.java"),
@@ -69,28 +181,6 @@
     registerProjectService(
         BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
-    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiMethod.class);
-    assertThat(method).isNotNull();
-
-    ConfigurationContext context = createContextFromPsi(method);
-    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
-    assertThat(configurations).hasSize(1);
-
-    ConfigurationFromContext fromContext = configurations.get(0);
-    assertThat(fromContext.isProducedBy(BlazeJavaTestMethodConfigurationProducer.class)).isTrue();
-    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
-
-    BlazeCommandRunConfiguration config =
-        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
-    assertThat(config.getTarget())
-        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
-    assertThat(getTestFilterContents(config))
-        .isEqualTo("--test_filter=com.google.test.TestClass#testMethod1$");
-    assertThat(config.getName()).isEqualTo("Blaze test TestClass.testMethod1");
-    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
-
-    BlazeCommandRunConfigurationCommonState state =
-        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    assertThat(state.getBlazeFlagsState().getRawFlags()).contains(BlazeFlags.DISABLE_TEST_SHARDING);
+    return PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiMethod.class);
   }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JdksTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JdksTest.java
new file mode 100644
index 0000000..aad4425
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JdksTest.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.java.sync.projectstructure;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.projectRoots.JavaSdk;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.pom.java.LanguageLevel;
+import com.intellij.testFramework.IdeaTestUtil;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link Jdks}. */
+@RunWith(JUnit4.class)
+public class JdksTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testLowerJdkIgnored() {
+    setJdks(ImmutableList.of(IdeaTestUtil.getMockJdk14()));
+    assertThat(Jdks.findClosestMatch(LanguageLevel.JDK_1_7)).isNull();
+  }
+
+  @Test
+  public void testEqualJdkChosen() {
+    Sdk jdk7 = IdeaTestUtil.getMockJdk17();
+    setJdks(ImmutableList.of(jdk7));
+    assertThat(Jdks.findClosestMatch(LanguageLevel.JDK_1_7)).isEqualTo(jdk7);
+  }
+
+  @Test
+  public void testHigherJdkChosen() {
+    Sdk jdk8 = IdeaTestUtil.getMockJdk18();
+    setJdks(ImmutableList.of(jdk8));
+    assertThat(Jdks.findClosestMatch(LanguageLevel.JDK_1_7)).isEqualTo(jdk8);
+  }
+
+  @Test
+  public void testClosestJdkOfAtLeastSpecifiedLevelChosen() {
+    Sdk jdk7 = IdeaTestUtil.getMockJdk17();
+    // Ordering retained in final list; add jdk7 last to ensure first Jdk of at least the specified
+    // language level isn't automatically chosen.
+    setJdks(ImmutableList.of(IdeaTestUtil.getMockJdk18(), IdeaTestUtil.getMockJdk14(), jdk7));
+    assertThat(Jdks.findClosestMatch(LanguageLevel.JDK_1_6)).isEqualTo(jdk7);
+  }
+
+  private void setJdks(List<Sdk> jdks) {
+    List<Sdk> currentJdks =
+        ReadAction.compute(
+            () -> ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance()));
+    WriteAction.run(
+        () -> {
+          currentJdks.forEach(jdk -> ProjectJdkTable.getInstance().removeJdk(jdk));
+          jdks.forEach(jdk -> ProjectJdkTable.getInstance().addJdk(jdk));
+        });
+  }
+}
diff --git a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
index 612b946..a2ff2e9 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
@@ -19,7 +19,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
@@ -46,6 +45,7 @@
 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
 import com.intellij.openapi.project.Project;
 import java.util.List;
+import java.util.function.Predicate;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
index c69b66b..a2a73db 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
@@ -77,13 +77,11 @@
 import com.google.idea.common.experiments.MockExperimentService;
 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
 import java.io.File;
-import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -127,14 +125,11 @@
   private JavaWorkingSet workingSet = null;
   private final WorkspaceLanguageSettings workspaceLanguageSettings =
       new WorkspaceLanguageSettings(WorkspaceType.JAVA, ImmutableSet.of(LanguageClass.JAVA));
-  private MockExperimentService experimentService;
 
   @Override
   @SuppressWarnings("FunctionalInterfaceClash") // False positive on getDeclaredPackageOfJavaFile.
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    experimentService = new MockExperimentService();
-    applicationServices.register(ExperimentService.class, experimentService);
+  protected void initTest(Container applicationServices, Container projectServices) {
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
 
     BlazeExecutor blazeExecutor = new MockBlazeExecutor();
     applicationServices.register(BlazeExecutor.class, blazeExecutor);
@@ -167,7 +162,7 @@
         .registerExtension(new JavaLikeLanguage.Java());
   }
 
-  BlazeJavaImportResult importWorkspace(
+  private BlazeJavaImportResult importWorkspace(
       WorkspaceRoot workspaceRoot, TargetMapBuilder targetMapBuilder, ProjectView projectView) {
 
     ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
@@ -1229,7 +1224,7 @@
     assertThat(findLibrary(result.libraries, "liba-ijar.jar")).isNotNull();
 
     // Second test
-    // Put everything in the working set, which should expand to the full transitive closure
+    // Put everything in the working set, which should expand to include the direct deps
     workingSet =
         new JavaWorkingSet(
             workspaceRoot,
@@ -1242,13 +1237,9 @@
     result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
     errorCollector.assertNoIssues();
 
-    assertThat(result.libraries).hasSize(6);
+    assertThat(result.libraries).hasSize(2);
     assertThat(findLibrary(result.libraries, "liba-ijar.jar")).isNotNull();
     assertThat(findLibrary(result.libraries, "libb-ijar.jar")).isNotNull();
-    assertThat(findLibrary(result.libraries, "libb-2-ijar.jar")).isNotNull();
-    assertThat(findLibrary(result.libraries, "libc-ijar.jar")).isNotNull();
-    assertThat(findLibrary(result.libraries, "libd-ijar.jar")).isNotNull();
-    assertThat(findLibrary(result.libraries, "libd-2-ijar.jar")).isNotNull();
   }
 
   /** Test that the non-android libraries can be imported. */
@@ -1325,19 +1316,11 @@
   @Test
   public void testSyncAugmenter() {
     augmenters.registerExtension(
-        new BlazeJavaSyncAugmenter() {
-          @Override
-          public void addJarsForSourceTarget(
-              WorkspaceLanguageSettings workspaceLanguageSettings,
-              ProjectViewSet projectViewSet,
-              TargetIdeInfo target,
-              Collection<BlazeJarLibrary> jars,
-              Collection<BlazeJarLibrary> genJars) {
-            if (target.key.label.equals(Label.create("//java/example:source"))) {
-              jars.add(
-                  new BlazeJarLibrary(
-                      LibraryArtifact.builder().setInterfaceJar(gen("source.jar")).build()));
-            }
+        (workspaceLanguageSettings, projectViewSet, target, jars, genJars) -> {
+          if (target.key.label.equals(Label.create("//java/example:source"))) {
+            jars.add(
+                new BlazeJarLibrary(
+                    LibraryArtifact.builder().setInterfaceJar(gen("source.jar")).build()));
           }
         });
 
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
index 0538155..38dd84c 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.java.sync.source;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -29,6 +28,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.io.MockInputStreamProvider;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -44,18 +44,12 @@
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
-import com.intellij.util.containers.HashMap;
-import java.io.ByteArrayInputStream;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.JavaSourcePackage;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo.PackageManifest;
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.util.List;
 import java.util.Map;
-import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -80,8 +74,7 @@
           artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
   @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
+  protected void initTest(Container applicationServices, Container projectServices) {
     super.initTest(applicationServices, projectServices);
 
     mockInputStreamProvider = new MockInputStreamProvider();
@@ -678,11 +671,15 @@
   @Test
   public void testCompetingPackageDeclarationPicksMajority() throws Exception {
     mockInputStreamProvider
+        .addFile("/root/java/com/google/Foo.java", "package com.google;\n public class Foo {}")
         .addFile(
-            "/root/java/com/google/Foo.java", "package com.google.different;\n public class Foo {}")
-        .addFile("/root/java/com/google/Bla.java", "package com.google;\n public class Bla {}")
-        .addFile("/root/java/com/google/Bla2.java", "package com.google;\n public class Bla2 {}")
-        .addFile("/root/java/com/google/Bla3.java", "package com.google;\n public class Bla3 {}");
+            "/root/java/com/google/Bla.java", "package com.google.different;\n public class Bla {}")
+        .addFile(
+            "/root/java/com/google/Bla2.java",
+            "package com.google.different;\n public class Bla2 {}")
+        .addFile(
+            "/root/java/com/google/Bla3.java",
+            "package com.google.different;\n public class Bla3 {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
             SourceArtifact.builder(TargetKey.forPlainTarget(LABEL))
@@ -724,6 +721,46 @@
             BlazeContentEntry.builder("/root/java/com/google")
                 .addSource(
                     BlazeSourceDirectory.builder("/root/java/com/google")
+                        .setPackagePrefix("com.google.different")
+                        .build())
+                .build());
+  }
+
+  @Test
+  public void testCompetingPackageDeclarationWithEqualCountsPicksDefault() throws Exception {
+    mockInputStreamProvider
+        .addFile(
+            "/root/java/com/google/Bla.java", "package com.google.different;\n public class Bla {}")
+        .addFile("/root/java/com/google/Foo.java", "package com.google;\n public class Foo {}");
+    List<SourceArtifact> sourceArtifacts =
+        ImmutableList.of(
+            SourceArtifact.builder(TargetKey.forPlainTarget(LABEL))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("java/com/google/Foo.java")
+                        .setIsSource(true))
+                .build(),
+            SourceArtifact.builder(TargetKey.forPlainTarget(LABEL))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("java/com/google/Bla.java")
+                        .setIsSource(true))
+                .build());
+    ImmutableList<BlazeContentEntry> result =
+        sourceDirectoryCalculator.calculateContentEntries(
+            project,
+            context,
+            workspaceRoot,
+            decoder,
+            ImmutableList.of(new WorkspacePath("java/com/google")),
+            sourceArtifacts,
+            NO_MANIFESTS);
+    issues.assertNoIssues();
+    assertThat(result)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/java/com/google")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/java/com/google")
                         .setPackagePrefix("com.google")
                         .build())
                 .build());
@@ -890,7 +927,7 @@
     setNewFormatPackageManifest(
         "/root/java/com/test.manifest",
         ImmutableList.of(
-            PackageManifestOuterClass.ArtifactLocation.newBuilder()
+            IntellijIdeInfo.ArtifactLocation.newBuilder()
                 .setRelativePath("java/com/google/Bla.java")
                 .setIsSource(true)
                 .build()),
@@ -1062,8 +1099,8 @@
     PackageManifest.Builder manifest = PackageManifest.newBuilder();
     for (int i = 0; i < sourceRelativePaths.size(); i++) {
       String sourceRelativePath = sourceRelativePaths.get(i);
-      PackageManifestOuterClass.ArtifactLocation source =
-          PackageManifestOuterClass.ArtifactLocation.newBuilder()
+      IntellijIdeInfo.ArtifactLocation source =
+          IntellijIdeInfo.ArtifactLocation.newBuilder()
               .setRelativePath(sourceRelativePath)
               .setIsSource(true)
               .build();
@@ -1076,9 +1113,7 @@
   }
 
   private void setNewFormatPackageManifest(
-      String manifestPath,
-      List<PackageManifestOuterClass.ArtifactLocation> sources,
-      List<String> packages) {
+      String manifestPath, List<IntellijIdeInfo.ArtifactLocation> sources, List<String> packages) {
     PackageManifest.Builder manifest = PackageManifest.newBuilder();
     for (int i = 0; i < sources.size(); i++) {
       manifest.addSources(
@@ -1098,35 +1133,6 @@
     return new ArtifactLocationDecoderImpl(roots, new WorkspacePathResolverImpl(workspaceRoot));
   }
 
-  private static class MockInputStreamProvider implements InputStreamProvider {
-
-    private final Map<String, InputStream> javaFiles = new HashMap<>();
-
-    public MockInputStreamProvider addFile(String filePath, String javaSrc) {
-      try {
-        addFile(filePath, javaSrc.getBytes("UTF-8"));
-      } catch (UnsupportedEncodingException e) {
-        fail(e.getMessage());
-      }
-      return this;
-    }
-
-    public MockInputStreamProvider addFile(String filePath, byte[] contents) {
-      javaFiles.put(filePath, new ByteArrayInputStream(contents));
-      return this;
-    }
-
-    @Override
-    public InputStream getFile(@NotNull File path) throws FileNotFoundException {
-      final InputStream inputStream = javaFiles.get(path.getPath());
-      if (inputStream == null) {
-        throw new FileNotFoundException(
-            path + " has not been mapped into MockInputStreamProvider.");
-      }
-      return inputStream;
-    }
-  }
-
   private Map<TargetKey, Map<ArtifactLocation, String>> readPackageManifestFiles(
       Map<TargetKey, ArtifactLocation> manifests, ArtifactLocationDecoder decoder) {
     return PackageManifestReader.getInstance()
@@ -1136,7 +1142,7 @@
 
   static class MockFileAttributeProvider extends FileAttributeProvider {
     @Override
-    public long getFileModifiedTime(@NotNull File file) {
+    public long getFileModifiedTime(File file) {
       return 1;
     }
   }
diff --git a/plugin_dev/BUILD b/plugin_dev/BUILD
index 1586378..1bb2c41 100644
--- a/plugin_dev/BUILD
+++ b/plugin_dev/BUILD
@@ -1,5 +1,13 @@
 licenses(["notice"])  # Apache 2.0
 
+load(
+    "//build_defs:build_defs.bzl",
+    "intellij_plugin",
+    "merged_plugin_xml",
+    "optional_plugin_xml",
+    "stamped_plugin_xml",
+)
+
 java_library(
     name = "plugin_dev",
     srcs = glob(["src/**/*.java"]),
@@ -15,19 +23,24 @@
     ],
 )
 
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "DevKit",
+    plugin_xml = "src/META-INF/blaze-plugin-dev.xml",
+    visibility = ["//visibility:public"],
+)
+
+OPTIONAL_PLUGIN_XMLS = [
+    ":optional_xml",
+    "//java:optional_xml",
+]
+
 filegroup(
     name = "plugin_xml",
     srcs = ["src/META-INF/blaze-plugin-dev.xml"],
     visibility = ["//visibility:public"],
 )
 
-load(
-    "//build_defs:build_defs.bzl",
-    "merged_plugin_xml",
-    "stamped_plugin_xml",
-    "intellij_plugin",
-)
-
 merged_plugin_xml(
     name = "merged_plugin_xml",
     srcs = [
@@ -48,6 +61,7 @@
 intellij_plugin(
     name = "plugin_dev_integration_test_plugin",
     testonly = 1,
+    optional_plugin_xmls = OPTIONAL_PLUGIN_XMLS,
     plugin_xml = ":plugin_dev_plugin_xml",
     deps = [
         ":plugin_dev",
@@ -64,7 +78,7 @@
 intellij_integration_test_suite(
     name = "integration_tests",
     srcs = glob(["tests/integrationtests/**/*.java"]),
-    required_plugins = "com.google.idea.blaze.ijwb",
+    required_plugins = "com.google.idea.blaze.plugin_dev,DevKit",
     test_package_root = "com.google.idea.blaze.plugin",
     runtime_deps = [
         ":plugin_dev_integration_test_plugin",
diff --git a/plugin_dev/src/META-INF/blaze-plugin-dev.xml b/plugin_dev/src/META-INF/blaze-plugin-dev.xml
index af9bc68..c3c4ac2 100644
--- a/plugin_dev/src/META-INF/blaze-plugin-dev.xml
+++ b/plugin_dev/src/META-INF/blaze-plugin-dev.xml
@@ -17,7 +17,7 @@
   <depends>DevKit</depends>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
-    <RunConfigurationFactory implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType$BlazeIntellijPluginRunConfigurationFactory"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType$BlazeIntellijPluginRunConfigurationFactory" order="first"/>
     <SyncPlugin implementation="com.google.idea.blaze.plugin.sync.IntellijPluginSyncPlugin"/>
   </extensions>
 
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
index 3621e36..f743cc9 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
@@ -188,54 +188,52 @@
     }
     String buildNumber = IdeaJdkHelper.getBuildNumber(ideaJdk);
     final BlazeIntellijPluginDeployer deployer =
-        new BlazeIntellijPluginDeployer(getProject(), sandboxHome, buildNumber);
-    deployer.addTarget(getTarget());
+        new BlazeIntellijPluginDeployer(sandboxHome, buildNumber, getTarget());
     env.putUserData(BlazeIntellijPluginDeployer.USER_DATA_KEY, deployer);
 
     // copy license from running instance of idea
     IdeaJdkHelper.copyIDEALicense(sandboxHome);
 
-    final JavaCommandLineState state =
-        new JavaCommandLineState(env) {
-          @Override
-          protected JavaParameters createJavaParameters() throws ExecutionException {
-            List<String> pluginIds = deployer.deploy();
+    return new JavaCommandLineState(env) {
+      @Override
+      protected JavaParameters createJavaParameters() throws ExecutionException {
+        List<String> pluginIds = deployer.deployNonBlocking();
 
-            final JavaParameters params = new JavaParameters();
+        final JavaParameters params = new JavaParameters();
 
-            ParametersList vm = params.getVMParametersList();
+        ParametersList vm = params.getVMParametersList();
 
-            fillParameterList(vm, vmParameters);
-            fillParameterList(params.getProgramParametersList(), programParameters);
+        fillParameterList(vm, vmParameters);
+        fillParameterList(params.getProgramParametersList(), programParameters);
 
-            IntellijWithPluginClasspathHelper.addRequiredVmParams(params, ideaJdk);
+        IntellijWithPluginClasspathHelper.addRequiredVmParams(params, ideaJdk);
 
-            vm.defineProperty(
-                JetBrainsProtocolHandler.REQUIRED_PLUGINS_KEY, Joiner.on(',').join(pluginIds));
+        vm.defineProperty(
+            JetBrainsProtocolHandler.REQUIRED_PLUGINS_KEY, Joiner.on(',').join(pluginIds));
 
-            if (!vm.hasProperty(PlatformUtils.PLATFORM_PREFIX_KEY) && buildNumber != null) {
-              String prefix = IdeaJdkHelper.getPlatformPrefix(buildNumber);
-              if (prefix != null) {
-                vm.defineProperty(PlatformUtils.PLATFORM_PREFIX_KEY, prefix);
+        if (!vm.hasProperty(PlatformUtils.PLATFORM_PREFIX_KEY) && buildNumber != null) {
+          String prefix = IdeaJdkHelper.getPlatformPrefix(buildNumber);
+          if (prefix != null) {
+            vm.defineProperty(PlatformUtils.PLATFORM_PREFIX_KEY, prefix);
+          }
+        }
+        return params;
+      }
+
+      @Override
+      protected OSProcessHandler startProcess() throws ExecutionException {
+        deployer.blockUntilDeployComplete();
+        final OSProcessHandler handler = super.startProcess();
+        handler.addProcessListener(
+            new ProcessAdapter() {
+              @Override
+              public void processTerminated(ProcessEvent event) {
+                deployer.deleteDeployment();
               }
-            }
-            return params;
-          }
-
-          @Override
-          protected OSProcessHandler startProcess() throws ExecutionException {
-            final OSProcessHandler handler = super.startProcess();
-            handler.addProcessListener(
-                new ProcessAdapter() {
-                  @Override
-                  public void processTerminated(ProcessEvent event) {
-                    deployer.deleteDeployment();
-                  }
-                });
-            return handler;
-          }
-        };
-    return state;
+            });
+        return handler;
+      }
+    };
   }
 
   private static void fillParameterList(ParametersList list, @Nullable String value) {
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
index 2c366f9..93e4854 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
@@ -21,23 +21,16 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
-import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
-import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
-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.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.plugin.IntellijPluginRule;
 import com.google.repackaged.devtools.intellij.plugin.IntellijPluginTargetDeployInfo.IntellijPluginDeployFile;
 import com.google.repackaged.devtools.intellij.plugin.IntellijPluginTargetDeployInfo.IntellijPluginDeployInfo;
 import com.google.repackaged.protobuf.TextFormat;
+import com.intellij.concurrency.AsyncUtil;
 import com.intellij.execution.ExecutionException;
 import com.intellij.ide.plugins.IdeaPluginDescriptor;
 import com.intellij.ide.plugins.PluginManagerCore;
-import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.BuildNumber;
 import com.intellij.openapi.util.Key;
 import java.io.BufferedInputStream;
@@ -52,6 +45,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Future;
 import javax.annotation.Nullable;
 
 /** Handles finding files to deploy and copying these into the sandbox. */
@@ -62,48 +56,35 @@
 
   private final String sandboxHome;
   private final String buildNumber;
-  private final TargetMap targetMap;
-  private final List<Label> targetsToDeploy = new ArrayList<>();
+  private final Label pluginTarget;
   private final List<File> deployInfoFiles = new ArrayList<>();
   private final Map<File, File> filesToDeploy = Maps.newHashMap();
   private File executionRoot;
 
-  BlazeIntellijPluginDeployer(Project project, String sandboxHome, String buildNumber)
-      throws ExecutionException {
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null) {
-      throw new ExecutionException("Not synced yet, please sync project");
-    }
+  private Future<Void> fileCopyingTask;
+
+  BlazeIntellijPluginDeployer(String sandboxHome, String buildNumber, Label pluginTarget) {
     this.sandboxHome = sandboxHome;
     this.buildNumber = buildNumber;
-    this.targetMap = blazeProjectData.targetMap;
-  }
-
-  /** Adds an intellij plugin target to deploy */
-  void addTarget(Label label) throws ExecutionException {
-    targetsToDeploy.add(label);
+    this.pluginTarget = pluginTarget;
   }
 
   void reportBuildComplete(File executionRoot, BuildResultHelper buildResultHelper) {
     this.executionRoot = executionRoot;
-    for (File file : buildResultHelper.getBuildArtifacts()) {
+    for (File file : buildResultHelper.getBuildArtifactsForTarget(pluginTarget)) {
       if (file.getName().endsWith(".intellij-plugin-debug-target-deploy-info")) {
         deployInfoFiles.add(file);
       }
     }
   }
 
-  List<String> deploy() throws ExecutionException {
+  /**
+   * Returns a list of plugin IDs, and asynchronously copies the corresponding files to the sandbox.
+   */
+  List<String> deployNonBlocking() throws ExecutionException {
     List<IntellijPluginDeployInfo> deployInfoList = Lists.newArrayList();
-    if (!deployInfoFiles.isEmpty()) {
-      for (File deployInfoFile : deployInfoFiles) {
-        deployInfoList.addAll(readDeployInfoFromFile(deployInfoFile));
-      }
-    } else {
-      for (Label label : targetsToDeploy) {
-        deployInfoList.addAll(findDeployInfoFromBareIntelliJPluginTargets(label));
-      }
+    for (File deployInfoFile : deployInfoFiles) {
+      deployInfoList.addAll(readDeployInfoFromFile(deployInfoFile));
     }
     ImmutableMap<File, File> filesToDeploy = getFilesToDeploy(executionRoot, deployInfoList);
     this.filesToDeploy.putAll(filesToDeploy);
@@ -114,11 +95,24 @@
             String.format("Plugin file '%s' not found. Did the build fail?", file.getName()));
       }
     }
-    List<String> pluginIds = readPluginIds(filesToDeploy.keySet());
-    for (Map.Entry<File, File> entry : filesToDeploy.entrySet()) {
-      copyFileToSandbox(entry.getKey(), entry.getValue());
-    }
-    return pluginIds;
+    // kick off file copying task asynchronously, so it doesn't block the EDT.
+    fileCopyingTask =
+        BlazeExecutor.getInstance()
+            .submit(
+                () -> {
+                  for (Map.Entry<File, File> entry : filesToDeploy.entrySet()) {
+                    copyFileToSandbox(entry.getKey(), entry.getValue());
+                  }
+                  return null;
+                });
+
+    return readPluginIds(filesToDeploy.keySet());
+  }
+
+  /** Blocks until the plugin files have been copied to the sandbox */
+  void blockUntilDeployComplete() {
+    AsyncUtil.get(fileCopyingTask);
+    fileCopyingTask = null;
   }
 
   void deleteDeployment() {
@@ -144,40 +138,6 @@
     return result.build();
   }
 
-  private ImmutableList<IntellijPluginDeployInfo> findDeployInfoFromBareIntelliJPluginTargets(
-      Label label) throws ExecutionException {
-    TargetIdeInfo target = targetMap.get(TargetKey.forPlainTarget(label));
-    if (target == null) {
-      throw new ExecutionException("Target '" + label + "' not imported during sync");
-    }
-    if (IntellijPluginRule.isSinglePluginTarget(target)) {
-      return ImmutableList.of(deployInfoForIntellijPlugin(target));
-    }
-    throw new ExecutionException("Target is not a supported intellij plugin type.");
-  }
-
-  private static IntellijPluginDeployInfo deployInfoForIntellijPlugin(TargetIdeInfo target)
-      throws ExecutionException {
-    JavaIdeInfo javaIdeInfo = target.javaIdeInfo;
-    if (!IntellijPluginRule.isSinglePluginTarget(target) || javaIdeInfo == null) {
-      throw new ExecutionException("Target '" + target + "' is not a valid intellij_plugin target");
-    }
-    Collection<LibraryArtifact> jars = javaIdeInfo.jars;
-    if (javaIdeInfo.jars.size() > 1) {
-      throw new ExecutionException("Invalid IntelliJ plugin target: it has multiple output jars");
-    }
-    LibraryArtifact artifact = jars.isEmpty() ? null : jars.iterator().next();
-    if (artifact == null || artifact.classJar == null) {
-      throw new ExecutionException("No output plugin jar found for '" + target + "'");
-    }
-    return IntellijPluginDeployInfo.newBuilder()
-        .addDeployFiles(
-            IntellijPluginDeployFile.newBuilder()
-                .setExecutionPath(artifact.classJar.getExecutionRootRelativePath())
-                .setDeployLocation(new File(artifact.classJar.relativePath).getName()))
-        .build();
-  }
-
   private ImmutableMap<File, File> getFilesToDeploy(
       File executionRoot, Collection<IntellijPluginDeployInfo> deployInfos) {
     ImmutableMap.Builder<File, File> result = ImmutableMap.builder();
@@ -229,6 +189,7 @@
     try {
       dest.getParentFile().mkdirs();
       Files.copy(src.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
+      dest.deleteOnExit();
     } catch (IOException e) {
       throw new ExecutionException("Error copying plugin file to sandbox", e);
     }
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
index 3c778fe..e2b9fa0 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
@@ -22,12 +22,14 @@
 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.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.experiments.ExperimentScope;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 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;
@@ -40,6 +42,8 @@
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.intellij.execution.BeforeRunTask;
 import com.intellij.execution.BeforeRunTaskProvider;
@@ -141,7 +145,10 @@
     if (!canExecuteTask(configuration, task)) {
       return false;
     }
-    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    BlazeConsolePopupBehavior consolePopupBehavior =
+        BlazeUserSettings.getInstance().getSuppressConsoleForRunAction()
+            ? BlazeConsolePopupBehavior.NEVER
+            : BlazeConsolePopupBehavior.ALWAYS;
     return Scope.root(
         context -> {
           context
@@ -149,7 +156,7 @@
               .push(new IssuesScope(project))
               .push(
                   new BlazeConsoleScope.Builder(project)
-                      .setSuppressConsole(suppressConsole)
+                      .setPopupBehavior(consolePopupBehavior)
                       .build())
               .push(new IdeaLogScope());
 
@@ -202,12 +209,24 @@
                     IssueOutput.error("Could not determine execution root").submit(context);
                     return;
                   }
+                  BlazeProjectData blazeProjectData =
+                      BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+                  if (blazeProjectData == null) {
+                    IssueOutput.error("Could not determine execution root").submit(context);
+                    return;
+                  }
 
-                  BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(f -> true);
+                  BuildResultHelper buildResultHelper =
+                      BuildResultHelper.forFiles(blazeProjectData.blazeVersionData, f -> true);
                   BlazeCommand command =
                       BlazeCommand.builder(binaryPath, BlazeCommandName.BUILD)
                           .addTargets(config.getTarget())
-                          .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                          .addBlazeFlags(
+                              BlazeFlags.blazeFlags(
+                                  project,
+                                  projectViewSet,
+                                  BlazeCommandName.BUILD,
+                                  BlazeInvocationContext.RunConfiguration))
                           .addBlazeFlags(config.getBlazeFlagsState().getExpandedFlags())
                           .addExeFlags(config.getExeFlagsState().getExpandedFlags())
                           .addBlazeFlags(buildResultHelper.getBuildFlags())
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
index e2eeb8e..fa13047 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
@@ -29,7 +29,6 @@
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
 import com.google.idea.sdkcompat.transactions.Transactions;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
 import com.intellij.openapi.project.Project;
@@ -92,14 +91,10 @@
             projectViewSet, blazeProjectData, LanguageLevel.JDK_1_7);
 
     // Leave the SDK, but set the language level
-    Transactions.submitTransactionAndWait(
-        () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(
-                    () -> {
-                      LanguageLevelProjectExtension ext =
-                          LanguageLevelProjectExtension.getInstance(project);
-                      ext.setLanguageLevel(javaLanguageLevel);
-                    }));
+    Transactions.submitWriteActionTransactionAndWait(
+        () -> {
+          LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(project);
+          ext.setLanguageLevel(javaLanguageLevel);
+        });
   }
 }
diff --git a/proto/BUILD b/proto/BUILD
index f90fbfa..1666e1c 100644
--- a/proto/BUILD
+++ b/proto/BUILD
@@ -10,3 +10,15 @@
     jars = ["proto_deps.jar"],
     visibility = ["//visibility:public"],
 )
+
+proto_library(
+    name = "intellij_ide_info_proto",
+    srcs = ["intellij_ide_info.proto"],
+    visibility = ["//visibility:public"],
+)
+
+java_proto_library(
+    name = "intellij_ide_info_java_proto",
+    visibility = ["//visibility:public"],
+    deps = [":intellij_ide_info_proto"],
+)
diff --git a/proto/intellij_ide_info.proto b/proto/intellij_ide_info.proto
new file mode 100644
index 0000000..ad73c68
--- /dev/null
+++ b/proto/intellij_ide_info.proto
@@ -0,0 +1,207 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package blaze;
+
+option java_api_version = 2;
+
+option java_package = "com.google.devtools.intellij.ideinfo";
+
+message ArtifactLocation {
+  string relative_path = 2;
+  bool is_source = 3;
+
+  // path from the execution root to the actual root:
+  // exec_root + root_execution_path_fragment + relative_path = absolute_path
+  string root_execution_path_fragment = 4;
+  // whether this artifact comes from an external repository (bazel only)
+  bool is_external = 5;
+
+  // The contents of relative_path and root_execution_path_fragment have changed
+  // for external workspaces. This is a temporary field to distinguish between
+  // the two versions.
+  bool is_new_external_version = 6;
+}
+
+message JavaSourcePackage {
+  string package_string = 2;
+  ArtifactLocation artifact_location = 3;
+}
+
+message PackageManifest {
+  repeated JavaSourcePackage sources = 1;
+}
+
+message LibraryArtifact {
+  ArtifactLocation jar = 1;
+  ArtifactLocation interface_jar = 2;
+  ArtifactLocation source_jar = 3;
+}
+
+message JavaIdeInfo {
+  repeated LibraryArtifact jars = 1;
+  repeated LibraryArtifact generated_jars = 2;
+  ArtifactLocation package_manifest = 3;
+  repeated ArtifactLocation sources = 4;
+  ArtifactLocation jdeps = 5;
+  LibraryArtifact filtered_gen_jar = 6;
+  string main_class = 7;
+}
+
+message CIdeInfo {
+  repeated ArtifactLocation source = 1;
+
+  repeated string transitive_include_directory = 3;
+  repeated string transitive_quote_include_directory = 4;
+  repeated string transitive_define = 5;
+  repeated string transitive_system_include_directory = 6;
+
+  repeated string target_copt = 7;
+  repeated string target_define = 8;
+  repeated string target_include = 9;
+  repeated ArtifactLocation header = 10;
+  repeated ArtifactLocation textual_header = 11;
+}
+
+message AndroidIdeInfo {
+  repeated ArtifactLocation resources = 1;
+  ArtifactLocation apk = 3;
+  repeated ArtifactLocation dependency_apk = 4;
+  ArtifactLocation manifest = 5;
+  string java_package = 7;
+  bool has_idl_sources = 8;
+  LibraryArtifact idl_jar = 9;
+  bool generate_resource_class = 10;
+  string legacy_resources = 11;
+  LibraryArtifact resource_jar = 12;
+  string idl_import_root = 13;
+}
+
+message AndroidSdkIdeInfo {
+  ArtifactLocation android_jar = 1;
+}
+
+message PyIdeInfo {
+  repeated ArtifactLocation sources = 1;
+}
+
+message GoIdeInfo {
+  repeated ArtifactLocation generated_sources = 1;
+}
+
+message JsIdeInfo {
+  repeated ArtifactLocation sources = 1;
+}
+
+message TsIdeInfo {
+  repeated ArtifactLocation sources = 1;
+}
+
+message CToolchainIdeInfo {
+  string target_name = 1;
+  repeated string base_compiler_option = 2;
+  repeated string cpp_option = 3;
+  repeated string c_option = 4;
+  string preprocessor_executable = 5;
+  string cpp_executable = 6;
+  repeated string link_option = 7;
+  repeated string built_in_include_directory = 8;
+  repeated string unfiltered_compiler_option = 9;
+}
+
+message ProtoLibraryLegacyJavaIdeInfo {
+  enum ApiFlavor {
+    NONE = 0;
+    IMMUTABLE = 1;
+    MUTABLE = 2;
+    BOTH = 3;
+  }
+
+  int32 api_version = 1;
+  ApiFlavor api_flavor = 2;
+  repeated LibraryArtifact jars1 = 3;
+  repeated LibraryArtifact jars_mutable = 4;
+  repeated LibraryArtifact jars_immutable = 5;
+}
+
+message TestInfo {
+  string size = 1;
+}
+
+message JavaToolchainIdeInfo {
+  string source_version = 1;
+  string target_version = 2;
+  ArtifactLocation javac_jar = 3;
+}
+
+message TargetKey {
+  string label = 1;
+  repeated string aspect_ids = 3;
+}
+
+message Dependency {
+  enum DependencyType {
+    COMPILE_TIME = 0;
+    RUNTIME = 1;
+  }
+
+  TargetKey target = 1;
+  DependencyType dependency_type = 2;
+}
+
+message TargetIdeInfo {
+  string label = 1 [deprecated = true];
+  repeated string dependencies = 4 [deprecated = true];
+
+  // kind is one of {JAVA,ANDROID}_{LIBRARY,BINARY,TEST} and JAVA_IMPORT
+  JavaIdeInfo java_ide_info = 7;
+  AndroidIdeInfo android_ide_info = 8;
+
+  repeated string tags = 9;
+  repeated string runtime_deps = 10 [deprecated = true];
+
+  ArtifactLocation build_file_artifact_location = 11;
+
+  CIdeInfo c_ide_info = 12;
+  CToolchainIdeInfo c_toolchain_ide_info = 13;
+
+  string kind_string = 14;
+
+  TestInfo test_info = 15;
+
+  ProtoLibraryLegacyJavaIdeInfo proto_library_legacy_java_ide_info = 16;
+  JavaToolchainIdeInfo java_toolchain_ide_info = 17;
+
+  PyIdeInfo py_ide_info = 18;
+
+  TargetKey key = 19;
+
+  repeated Dependency deps = 20;
+
+  reserved 21;
+
+  AndroidSdkIdeInfo android_sdk_ide_info = 22;
+
+  reserved 23;
+
+  repeated string features = 24;
+
+  GoIdeInfo go_ide_info = 25;
+  JsIdeInfo js_ide_info = 26;
+  TsIdeInfo ts_ide_info = 27;
+
+  // Next available: 28
+}
diff --git a/proto/proto_deps.jar b/proto/proto_deps.jar
index 2637779..5310fc0 100755
--- a/proto/proto_deps.jar
+++ b/proto/proto_deps.jar
Binary files differ
diff --git a/python/BUILD b/python/BUILD
index 51e2752..e2ac2dd 100644
--- a/python/BUILD
+++ b/python/BUILD
@@ -92,6 +92,7 @@
     deps = [
         ":python",
         "//base",
+        "//base:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "@jsr305_annotations//jar",
         "@junit//jar",
diff --git a/python/src/META-INF/python-contents.xml b/python/src/META-INF/python-contents.xml
index b2e1ab4..72b8b1f 100644
--- a/python/src/META-INF/python-contents.xml
+++ b/python/src/META-INF/python-contents.xml
@@ -19,8 +19,9 @@
     <SyncPlugin implementation="com.google.idea.blaze.python.sync.BlazePythonSyncPlugin"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.python.sync.PythonPrefetchFileSource"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.python.run.BlazePyRunConfigurationHandlerProvider" order="first"/>
-    <RunConfigurationFactory implementation="com.google.idea.blaze.python.run.BlazePyDebuggableRunConfigurationFactory"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.python.run.smrunner.BlazePythonTestEventsHandler"/>
+    <PyParameterizedNameConverter implementation="com.google.idea.blaze.python.run.smrunner.PyBaseParameterizedNameConverter" order="first"/>
+    <BlazeIssueParserProvider implementation="com.google.idea.blaze.python.issueparser.PyIssueParserProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij">
@@ -36,6 +37,8 @@
       interface="com.google.idea.blaze.python.run.filter.BlazePyFilterProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazePyDebugFlagsProvider"
       interface="com.google.idea.blaze.python.run.BlazePyDebugHelper"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.PyParameterizedNameConverter"
+      interface="com.google.idea.blaze.python.run.smrunner.PyParameterizedNameConverter"/>
   </extensionPoints>
 
   <project-components>
diff --git a/python/src/com/google/idea/blaze/python/issueparser/PyIssueParserProvider.java b/python/src/com/google/idea/blaze/python/issueparser/PyIssueParserProvider.java
new file mode 100644
index 0000000..a05aae4
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/issueparser/PyIssueParserProvider.java
@@ -0,0 +1,98 @@
+/*
+ * 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.python.issueparser;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParser.Parser;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParser.SingleLineParser;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParserProvider;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.pom.Navigatable;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.search.FilenameIndex;
+import com.intellij.psi.search.GlobalSearchScope;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import javax.annotation.Nullable;
+
+/** Finds python-specific errors in blaze build output. */
+public class PyIssueParserProvider implements BlazeIssueParserProvider {
+
+  @Override
+  public ImmutableList<Parser> getIssueParsers(Project project) {
+    return ImmutableList.of(new PyTracebackIssueParser(project));
+  }
+
+  private static class PyTracebackIssueParser extends SingleLineParser {
+
+    final Project project;
+
+    PyTracebackIssueParser(Project project) {
+      super("File \"(.*?)\", line ([0-9]+), in (.*)");
+      this.project = project;
+    }
+
+    @Nullable
+    @Override
+    protected IssueOutput createIssue(Matcher matcher) {
+      String fileName = matcher.group(1);
+      if (fileName == null) {
+        return null;
+      }
+      return IssueOutput.error(matcher.group(0))
+          .navigatable(openFileNavigatable(project, fileName, parseLineNumber(matcher.group(2))))
+          .build();
+    }
+
+    private static Navigatable openFileNavigatable(Project project, String fileName, int line) {
+      return new NavigatableAdapter() {
+        @Override
+        public void navigate(boolean requestFocus) {
+          openFile(project, fileName, line, requestFocus);
+        }
+      };
+    }
+
+    private static void openFile(Project project, String fileName, int line, boolean requestFocus) {
+      PsiFile file = findFile(project, fileName);
+      if (file == null) {
+        return;
+      }
+      new OpenFileDescriptor(project, file.getViewProvider().getVirtualFile(), line - 1, -1)
+          .navigate(requestFocus);
+    }
+
+    @Nullable
+    private static PsiFile findFile(Project project, String fileName) {
+      return Arrays.stream(
+              FilenameIndex.getFilesByName(project, fileName, GlobalSearchScope.allScope(project)))
+          .findFirst()
+          .orElse(null);
+    }
+
+    /** defaults to -1 if no line number can be parsed. */
+    private static int parseLineNumber(@Nullable String string) {
+      try {
+        return string != null ? Integer.parseInt(string) : -1;
+      } catch (NumberFormatException e) {
+        return -1;
+      }
+    }
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java b/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java
deleted file mode 100644
index 7536401..0000000
--- a/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java
+++ /dev/null
@@ -1,62 +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.python.run;
-
-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.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
-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;
-
-/** Creates run configurations for debuggable python targets. */
-public class BlazePyDebuggableRunConfigurationFactory extends BlazeRunConfigurationFactory {
-  @Override
-  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
-    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
-    return target != null && target.kind != null && PyDebugUtils.canUsePyDebugger(target.kind);
-  }
-
-  @Override
-  protected ConfigurationFactory getConfigurationFactory() {
-    return BlazeCommandRunConfigurationType.getInstance().getFactory();
-  }
-
-  @Override
-  public void setupConfiguration(RunConfiguration configuration, Label target) {
-    final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(target);
-
-    BlazeCommandRunConfigurationCommonState state =
-        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    Kind kind = blazeConfig.getKindForTarget();
-    if (state != null) {
-      BlazeCommandName command =
-          kind != null && Kind.isTestRule(kind.toString())
-              ? BlazeCommandName.TEST
-              : BlazeCommandName.RUN;
-      state.getCommandState().setCommand(command);
-    }
-    blazeConfig.setGeneratedName();
-  }
-}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java
index 08f2a22..adea754 100644
--- a/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java
@@ -25,6 +25,7 @@
 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.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
@@ -46,14 +47,15 @@
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
-import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.blaze.python.PySdkUtils;
 import com.google.idea.blaze.python.run.filter.BlazePyFilterProvider;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
 import com.intellij.execution.Executor;
+import com.intellij.execution.RunCanceledByUserException;
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.execution.configurations.RunProfile;
 import com.intellij.execution.configurations.RunProfileState;
@@ -68,11 +70,9 @@
 import com.intellij.execution.runners.ProgramRunner;
 import com.intellij.execution.ui.ConsoleView;
 import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.util.Key;
-import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.util.PathUtil;
@@ -85,7 +85,6 @@
 import java.io.File;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -98,13 +97,6 @@
 
   private static final Logger logger = Logger.getInstance(BlazePyRunConfigurationRunner.class);
 
-  // Filter executables instead of files in the bin directory
-  // This bin directory isn't the right one, because we don't know the blaze binary
-  // or the config flags used to execute the build command
-  // Introduced March 2017
-  private static final BoolExperiment filterExecutableFiles =
-      new BoolExperiment("filter.executable.files", true);
-
   /** Converts to the native python plugin debug configuration state */
   static class BlazePyDummyRunProfileState implements RunProfileState {
     final BlazeCommandRunConfiguration configuration;
@@ -130,15 +122,12 @@
           Strings.nullToEmpty(
               getRunfilesPath(executable, WorkspaceRoot.fromProjectSafe(env.getProject()))));
 
-      Module workspaceModule =
-          nativeConfig.getConfigurationModule().findModule(BlazeDataStorage.WORKSPACE_MODULE_NAME);
-      if (workspaceModule != null) {
-        nativeConfig.setModule(workspaceModule);
-        nativeConfig.setUseModuleSdk(true);
-      } else {
-        throw new ExecutionException(
-            "Can't find the workspace module when debugging a python target");
+      Sdk sdk = PySdkUtils.getPythonSdk(env.getProject());
+      if (sdk == null) {
+        throw new ExecutionException("Can't find a Python SDK when debugging a python target.");
       }
+      nativeConfig.setModule(null);
+      nativeConfig.setSdkHome(sdk.getHomePath());
 
       BlazeCommandRunConfigurationCommonState handlerState =
           configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
@@ -230,6 +219,7 @@
     if (!isDebugging(env)) {
       return true;
     }
+    env.getCopyableUserData(EXECUTABLE_KEY).set(null);
     try {
       File executable = getExecutableToDebug(env);
       env.getCopyableUserData(EXECUTABLE_KEY).set(executable);
@@ -297,8 +287,12 @@
     final ProjectViewSet projectViewSet =
         ProjectViewManager.getInstance(project).getProjectViewSet();
 
-    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
-    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    BuildResultHelper buildResultHelper =
+        BuildResultHelper.forFiles(blazeProjectData.blazeVersionData, file -> true);
+    BlazeConsolePopupBehavior consolePopupBehavior =
+        BlazeUserSettings.getInstance().getSuppressConsoleForRunAction()
+            ? BlazeConsolePopupBehavior.NEVER
+            : BlazeConsolePopupBehavior.ALWAYS;
     final ListenableFuture<Void> buildOperation =
         BlazeExecutor.submitTask(
             project,
@@ -309,7 +303,7 @@
                     .push(new IssuesScope(project))
                     .push(
                         new BlazeConsoleScope.Builder(project)
-                            .setSuppressConsole(suppressConsole)
+                            .setPopupBehavior(consolePopupBehavior)
                             .build());
 
                 context.output(new StatusOutput("Building debug binary"));
@@ -319,7 +313,12 @@
                             Blaze.getBuildSystemProvider(project).getBinaryPath(),
                             BlazeCommandName.BUILD)
                         .addTargets(configuration.getTarget())
-                        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                        .addBlazeFlags(
+                            BlazeFlags.blazeFlags(
+                                project,
+                                projectViewSet,
+                                BlazeCommandName.BUILD,
+                                BlazeInvocationContext.RunConfiguration))
                         .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
                         .addBlazeFlags(BlazePyDebugHelper.getAllBlazeDebugFlags())
                         .addBlazeFlags(buildResultHelper.getBuildFlags());
@@ -338,14 +337,16 @@
     try {
       SaveUtil.saveAllFiles();
       buildOperation.get();
-    } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
+    } catch (InterruptedException e) {
+      throw new RunCanceledByUserException();
+    } catch (java.util.concurrent.ExecutionException e) {
       throw new ExecutionException(e);
     }
     List<File> candidateFiles =
         buildResultHelper
-            .getBuildArtifacts()
+            .getBuildArtifactsForTarget((Label) configuration.getTarget())
             .stream()
-            .filter(fileFilter(blazeProjectData))
+            .filter(File::canExecute)
             .collect(Collectors.toList());
     if (candidateFiles.isEmpty()) {
       throw new ExecutionException(
@@ -362,12 +363,6 @@
     return file;
   }
 
-  private static Predicate<File> fileFilter(BlazeProjectData blazeProjectData) {
-    return filterExecutableFiles.getValue()
-        ? File::canExecute
-        : f -> FileUtil.isAncestor(blazeProjectData.blazeInfo.getBlazeBinDirectory(), f, true);
-  }
-
   /**
    * Basic heuristic for choosing between multiple output files. Currently just looks for a filename
    * matching the target name.
diff --git a/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java b/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java
index 5d21fa8..2bf5216 100644
--- a/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java
+++ b/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.python.run.producers;
 
-import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -121,16 +120,14 @@
     }
     handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
 
-    ImmutableList.Builder<String> flags = ImmutableList.builder();
+    // remove conflicting flags from initial configuration
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
     String filter = testLocation.testFilter();
     if (filter != null) {
       flags.add(BlazeFlags.TEST_FILTER + "=" + filter);
     }
-    // remove conflicting flags from initial configuration
-    List<String> oldFlags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
-    oldFlags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
-    flags.addAll(oldFlags);
-    handlerState.getBlazeFlagsState().setRawFlags(flags.build());
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
 
     BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
     nameBuilder.setTargetString(
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java
index f39eb76..339a1b8 100644
--- a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java
@@ -25,17 +25,17 @@
 import com.intellij.psi.PsiElement;
 import com.jetbrains.python.psi.PyClass;
 import com.jetbrains.python.psi.PyFunction;
-import java.util.ArrayList;
-import java.util.EnumSet;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import javax.annotation.Nullable;
 
 /** Provides python-specific methods needed by the SM-runner test UI. */
-public class BlazePythonTestEventsHandler extends BlazeTestEventsHandler {
+public class BlazePythonTestEventsHandler implements BlazeTestEventsHandler {
 
   @Override
-  protected EnumSet<Kind> handledKinds() {
-    return EnumSet.of(Kind.PY_TEST);
+  public boolean handlesKind(@Nullable Kind kind) {
+    return kind == Kind.PY_TEST;
   }
 
   @Override
@@ -53,7 +53,9 @@
   @Override
   public String getTestFilter(Project project, List<Location<?>> testLocations) {
     // python test runner parses filters of the form "class1.method1 class2.method2 ..."
-    List<String> filters = new ArrayList<>();
+    // parameterized test cases can cause the same class.method combination to be present
+    // multiple times, so we use a set
+    Set<String> filters = new LinkedHashSet<>();
     for (Location<?> location : testLocations) {
       String filter = getFilter(location.getPsiElement());
       if (filter != null) {
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java
index 5bcfdc2..1582b12 100644
--- a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java
@@ -29,7 +29,9 @@
 import com.jetbrains.python.psi.PyFunction;
 import com.jetbrains.python.psi.stubs.PyClassNameIndex;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import javax.annotation.Nullable;
 
 /** Locate python test classes / methods for test UI navigation. */
@@ -49,7 +51,7 @@
     }
     if (protocol.equals(SmRunnerUtils.GENERIC_TEST_PROTOCOL)) {
       path = StringUtil.trimStart(path, PY_TESTCASE_PREFIX);
-      String[] components = path.split("\\.");
+      String[] components = path.split("\\.|::");
       if (components.length < 2) {
         return ImmutableList.of();
       }
@@ -68,7 +70,7 @@
     for (PyClass pyClass : PyClassNameIndex.find(className, project, scope)) {
       ProgressManager.checkCanceled();
       if (PyTestUtils.isTestClass(pyClass)) {
-        PyFunction method = pyClass.findMethodByName(methodName, true, null);
+        PyFunction method = findMethod(pyClass, methodName);
         if (method != null && PyTestUtils.isTestFunction(method)) {
           results.add(new PsiLocation<>(project, method));
         }
@@ -89,4 +91,18 @@
     }
     return results;
   }
+
+  @Nullable
+  private static PyFunction findMethod(PyClass pyClass, String methodName) {
+    PyFunction method = pyClass.findMethodByName(methodName, true, null);
+    if (method != null) {
+      return method;
+    }
+    return Arrays.stream(PyParameterizedNameConverter.EP_NAME.getExtensions())
+        .map(converter -> converter.toFunctionName(methodName))
+        .map(name -> pyClass.findMethodByName(name, true, null))
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse(null);
+  }
 }
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverter.java b/python/src/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverter.java
new file mode 100644
index 0000000..e86487a
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverter.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.blaze.python.run.smrunner;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** Converts from pybase/parameterized parameterized output format to plain testcase names. */
+public class PyBaseParameterizedNameConverter implements PyParameterizedNameConverter {
+
+  private static final Pattern PATTERN = Pattern.compile("^(\\w+)\\(.*\\)$");
+
+  @Override
+  @Nullable
+  public String toFunctionName(String testCaseName) {
+    Matcher match = PATTERN.matcher(testCaseName);
+    return match.matches() ? match.group(1) : null;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/PyParameterizedNameConverter.java b/python/src/com/google/idea/blaze/python/run/smrunner/PyParameterizedNameConverter.java
new file mode 100644
index 0000000..a1fc58d
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/PyParameterizedNameConverter.java
@@ -0,0 +1,32 @@
+/*
+ * 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.python.run.smrunner;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import javax.annotation.Nullable;
+
+/**
+ * Converts a possibly parameterized testcase name from test output XML to an unparameterized python
+ * function name.
+ */
+public interface PyParameterizedNameConverter {
+
+  ExtensionPointName<PyParameterizedNameConverter> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.PyParameterizedNameConverter");
+
+  @Nullable
+  String toFunctionName(String testCaseName);
+}
diff --git a/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java b/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java
index 2354f3c..e45b0d6 100644
--- a/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java
+++ b/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java
@@ -41,11 +41,11 @@
 import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.common.experiments.BoolExperiment;
+import com.google.idea.sdkcompat.python.PythonFacetUtil;
 import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.facet.Facet;
 import com.intellij.facet.FacetManager;
 import com.intellij.facet.ModifiableFacetModel;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleType;
@@ -62,8 +62,7 @@
 import com.intellij.pom.NavigatableAdapter;
 import com.intellij.util.PlatformUtils;
 import com.jetbrains.python.PythonModuleTypeBase;
-import com.jetbrains.python.facet.PythonFacet;
-import com.jetbrains.python.facet.PythonFacetType;
+import com.jetbrains.python.facet.LibraryContributingFacet;
 import com.jetbrains.python.sdk.PythonSdkType;
 import java.util.List;
 import java.util.Set;
@@ -134,6 +133,9 @@
 
   @Override
   public void refreshVirtualFileSystem(BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.PYTHON)) {
+      return;
+    }
     if (!refreshExecRoot.getValue()) {
       return;
     }
@@ -168,7 +170,7 @@
     if (ModuleType.get(workspaceModule) instanceof PythonModuleTypeBase) {
       return;
     }
-    PythonFacet pythonFacet = getOrCreatePythonFacet(context, workspaceModule);
+    LibraryContributingFacet<?> pythonFacet = getOrCreatePythonFacet(context, workspaceModule);
     if (pythonFacet == null) {
       return;
     }
@@ -181,7 +183,7 @@
   private static void removeFacet(Module workspaceModule) {
     FacetManager manager = FacetManager.getInstance(workspaceModule);
     ModifiableFacetModel facetModel = manager.createModifiableModel();
-    PythonFacet facet = manager.findFacet(PythonFacet.ID, "Python");
+    LibraryContributingFacet<?> facet = manager.findFacet(PythonFacetUtil.getFacetId(), "Python");
     if (facet != null) {
       facetModel.removeFacet(facet);
       facetModel.commit();
@@ -189,8 +191,9 @@
   }
 
   @Nullable
-  private static PythonFacet getOrCreatePythonFacet(BlazeContext context, Module module) {
-    PythonFacet facet = findPythonFacet(module);
+  private static LibraryContributingFacet<?> getOrCreatePythonFacet(
+      BlazeContext context, Module module) {
+    LibraryContributingFacet<?> facet = findPythonFacet(module);
     if (facet != null && facetHasSdk(facet)) {
       return facet;
     }
@@ -210,35 +213,37 @@
       IssueOutput.error(msg).submit(context);
       return null;
     }
-    facet = manager.createFacet(PythonFacetType.getInstance(), "Python", null);
+    facet = manager.createFacet(PythonFacetUtil.getTypeInstance(), "Python", null);
     facetModel.addFacet(facet);
     facetModel.commit();
     return facet;
   }
 
-  private static boolean facetHasSdk(PythonFacet facet) {
+  private static boolean facetHasSdk(LibraryContributingFacet<?> facet) {
     // facets aren't properly updated when SDKs change (e.g. when they're deleted), so we need to
     // manually check against the full list.
-    Sdk sdk = facet.getConfiguration().getSdk();
+    Sdk sdk = PythonFacetUtil.getSdk(facet);
     return sdk != null && PythonSdkType.getAllSdks().contains(sdk);
   }
 
   @Nullable
-  private static Library getFacetLibrary(PythonFacet pythonFacet) {
-    Sdk sdk = pythonFacet.getConfiguration().getSdk();
+  private static Library getFacetLibrary(LibraryContributingFacet<?> facet) {
+    Sdk sdk = PythonFacetUtil.getSdk(facet);
     if (sdk == null) {
       return null;
     }
     return LibraryTablesRegistrar.getInstance()
         .getLibraryTable()
-        .getLibraryByName(sdk.getName() + PythonFacet.PYTHON_FACET_LIBRARY_NAME_SUFFIX);
+        .getLibraryByName(
+            sdk.getName() + LibraryContributingFacet.PYTHON_FACET_LIBRARY_NAME_SUFFIX);
   }
 
-  private static PythonFacet findPythonFacet(Module module) {
+  private static LibraryContributingFacet<?> findPythonFacet(Module module) {
     final Facet<?>[] allFacets = FacetManager.getInstance(module).getAllFacets();
     for (Facet<?> facet : allFacets) {
-      if (facet instanceof PythonFacet) {
-        return (PythonFacet) facet;
+      if ((facet instanceof LibraryContributingFacet)
+          && (facet.getTypeId() == PythonFacetUtil.getFacetId())) {
+        return (LibraryContributingFacet<?>) facet;
       }
     }
     return null;
@@ -274,10 +279,8 @@
   }
 
   private static void setProjectSdk(Project project, Sdk sdk) {
-    Transactions.submitTransactionAndWait(
-        () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(() -> ProjectRootManager.getInstance(project).setProjectSdk(sdk)));
+    Transactions.submitWriteActionTransactionAndWait(
+        () -> ProjectRootManager.getInstance(project).setProjectSdk(sdk));
   }
 
   @Override
diff --git a/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java b/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java
index 29bde5e..0e5ae17 100644
--- a/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java
+++ b/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java
@@ -19,9 +19,9 @@
 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.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.Collection;
 import java.util.Set;
 
 /** Causes python files to become prefetched. */
@@ -30,11 +30,12 @@
   public void addFilesToPrefetch(
       Project project,
       ProjectViewSet projectViewSet,
+      ImportRoots importRoots,
       BlazeProjectData blazeProjectData,
-      Collection<File> files) {}
+      Set<File> files) {}
 
   @Override
-  public Set<String> prefetchSrcFileExtensions() {
+  public Set<String> prefetchFileExtensions() {
     return ImmutableSet.of("py", "pyw", "pyi");
   }
 }
diff --git a/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java b/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java
index c87025c..d56f656 100644
--- a/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java
+++ b/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java
@@ -57,7 +57,7 @@
   }
 
   @Test
-  public void testFunctionLocationResolves() {
+  public void testFunctionLocationOldFormatResolves() {
     PsiFile file =
         workspace.createPsiFile(
             new WorkspacePath("lib/app_unittest.py"),
@@ -74,6 +74,23 @@
   }
 
   @Test
+  public void testFunctionLocationResolves() {
+    PsiFile file =
+        workspace.createPsiFile(
+            new WorkspacePath("lib/app_unittest.py"),
+            "class AppUnitTest:",
+            "  def testApp(self):",
+            "    return");
+    PyClass pyClass = PsiUtils.findFirstChildOfClassRecursive(file, PyClass.class);
+    PyFunction function = pyClass.findMethodByName("testApp", false, null);
+    assertThat(function).isNotNull();
+
+    String url = handler.testLocationUrl(null, null, "__main__.AppUnitTest::testApp", null);
+    Location<?> location = getLocation(url);
+    assertThat(location.getPsiElement()).isEqualTo(function);
+  }
+
+  @Test
   public void testFunctionWithoutMainPrefixResolves() {
     PsiFile file =
         workspace.createPsiFile(
diff --git a/python/tests/unittests/com/google/idea/blaze/python/issueparser/PyIssueParserProviderTest.java b/python/tests/unittests/com/google/idea/blaze/python/issueparser/PyIssueParserProviderTest.java
new file mode 100644
index 0000000..f09b741
--- /dev/null
+++ b/python/tests/unittests/com/google/idea/blaze/python/issueparser/PyIssueParserProviderTest.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.python.issueparser;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParser;
+import com.google.idea.blaze.base.issueparser.BlazeIssueParserProvider;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.IssueOutput.Category;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PyIssueParserProvider}. */
+@RunWith(JUnit4.class)
+public class PyIssueParserProviderTest extends BlazeTestCase {
+
+  private ImmutableList<BlazeIssueParser.Parser> parsers;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    ExtensionPointImpl<BlazeIssueParserProvider> ep =
+        registerExtensionPoint(BlazeIssueParserProvider.EP_NAME, BlazeIssueParserProvider.class);
+    ep.registerExtension(new PyIssueParserProvider());
+
+    parsers = ImmutableList.copyOf(BlazeIssueParserProvider.getAllIssueParsers(project));
+  }
+
+  @Test
+  public void testParsePyTypeError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
+    IssueOutput issue =
+        blazeIssueParser.parseIssue(
+            "File \"dataset.py\", line 109, in Dataset: "
+                + "Name 'function' is not defined [name-error]");
+    assertThat(issue).isNotNull();
+    assertThat(issue.getCategory()).isEqualTo(Category.ERROR);
+    assertThat(issue.getNavigatable()).isNotNull();
+  }
+}
diff --git a/python/tests/unittests/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverterTest.java b/python/tests/unittests/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverterTest.java
new file mode 100644
index 0000000..b4b13ba
--- /dev/null
+++ b/python/tests/unittests/com/google/idea/blaze/python/run/smrunner/PyBaseParameterizedNameConverterTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.python.run.smrunner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PyBaseParameterizedNameConverter}. */
+@RunWith(JUnit4.class)
+public class PyBaseParameterizedNameConverterTest {
+
+  private static final PyBaseParameterizedNameConverter CONVERTER =
+      new PyBaseParameterizedNameConverter();
+
+  @Test
+  public void testUnparameterizedTestCaseReturnsNull() {
+    assertThat(CONVERTER.toFunctionName("simpleName")).isNull();
+    assertThat(CONVERTER.toFunctionName("name_with_underscores")).isNull();
+    assertThat(CONVERTER.toFunctionName("name__with__double__underscores123")).isNull();
+  }
+
+  @Test
+  public void testSimpleParameterNames() {
+    assertThat(CONVERTER.toFunctionName("test(1)")).isEqualTo("test");
+    assertThat(CONVERTER.toFunctionName("test(param__name)")).isEqualTo("test");
+    assertThat(CONVERTER.toFunctionName("test(@#$%^&*,./)")).isEqualTo("test");
+  }
+
+  @Test
+  public void testSplitAtFirstOpenBracket() {
+    assertThat(CONVERTER.toFunctionName("test(param(1)")).isEqualTo("test");
+    assertThat(CONVERTER.toFunctionName("test(@#$;('(())))())")).isEqualTo("test");
+  }
+}
diff --git a/scala/BUILD b/scala/BUILD
index 1343c03..2f2dded 100644
--- a/scala/BUILD
+++ b/scala/BUILD
@@ -26,6 +26,11 @@
     visibility = ["//visibility:public"],
 )
 
+OPTIONAL_PLUGIN_XMLS = [
+    ":optional_xml",
+    "//java:optional_xml",
+]
+
 merged_plugin_xml(
     name = "merged_plugin_xml",
     srcs = [
@@ -58,7 +63,7 @@
 intellij_plugin(
     name = "scala_integration_test_plugin",
     testonly = 1,
-    optional_plugin_xmls = [":optional_xml"],
+    optional_plugin_xmls = OPTIONAL_PLUGIN_XMLS,
     plugin_xml = ":scala_plugin_xml",
     deps = [":scala"],
 )
diff --git a/scala/src/META-INF/scala-contents.xml b/scala/src/META-INF/scala-contents.xml
index 9c46cf1..d2fbd81 100644
--- a/scala/src/META-INF/scala-contents.xml
+++ b/scala/src/META-INF/scala-contents.xml
@@ -4,4 +4,28 @@
     <JavaLikeLanguage
         implementation="com.google.idea.blaze.scala.sync.source.ScalaJavaLikeLanguage"/>
   </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.scala.run.producers.BlazeScalaMainClassRunConfigurationProducer"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.scala.run.producers.BlazeScalaTestClassConfigurationProducer"/>
+    <!-- TODO: add configuration producer for infix expression test cases in scalatest and specs2. -->
+    <!-- Need to come before the one in the scala plugin to override the icon. -->
+    <runLineMarkerContributor
+        implementationClass="com.google.idea.blaze.scala.run.producers.BlazeScalaRunLineMarkerContributor"
+        language="Scala"
+        order="first"/>
+    <runLineMarkerContributor
+        implementationClass="com.google.idea.blaze.scala.run.producers.BlazeScalaTestRunLineMarkerContributor"
+        language="Scala"
+        order="first"/>
+  </extensions>
+
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.scala.run.producers.NonBlazeProducerSuppressor
+      </implementation-class>
+    </component>
+  </project-components>
 </idea-plugin>
diff --git a/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassRunConfigurationProducer.java b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassRunConfigurationProducer.java
new file mode 100644
index 0000000..933758e
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassRunConfigurationProducer.java
@@ -0,0 +1,224 @@
+/*
+ * 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.scala.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.run.testmap.FilteredTargetMap;
+import com.google.idea.blaze.base.sync.SyncCache;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import java.io.File;
+import java.util.Collection;
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile;
+import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.ScObject;
+import org.jetbrains.plugins.scala.runner.ScalaMainMethodUtil;
+import scala.Option;
+
+/** Creates run configurations for Scala main classes sourced by scala_binary targets. */
+public class BlazeScalaMainClassRunConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  private static final String SCALA_BINARY_MAP_KEY = "BlazeScalaBinaryMap";
+
+  public BlazeScalaMainClassRunConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    ScObject mainObject = getMainObject(context);
+    if (mainObject == null) {
+      return false;
+    }
+    Option<PsiMethod> mainMethod = ScalaMainMethodUtil.findMainMethod(mainObject);
+    if (mainMethod.isEmpty()) {
+      sourceElement.set(mainObject);
+    } else {
+      sourceElement.set(mainMethod.get());
+    }
+    TargetIdeInfo target = getTarget(context.getProject(), mainObject);
+    if (target == null) {
+      return false;
+    }
+    configuration.setTarget(target.key.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.getCommandState().setCommand(BlazeCommandName.RUN);
+    configuration.setGeneratedName();
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.RUN)) {
+      return false;
+    }
+    ScObject mainObject = getMainObject(context);
+    if (mainObject == null) {
+      return false;
+    }
+    TargetIdeInfo target = getTarget(context.getProject(), mainObject);
+    return target != null && Objects.equals(configuration.getTarget(), target.key.label);
+  }
+
+  @Nullable
+  private static ScObject getMainObject(ConfigurationContext context) {
+    Location location = context.getLocation();
+    if (location == null) {
+      return null;
+    }
+    location = JavaExecutionUtil.stepIntoSingleClass(context.getLocation());
+    if (location == null) {
+      return null;
+    }
+    PsiElement element = location.getPsiElement();
+    if (!(element.getContainingFile() instanceof ScalaFile)) {
+      return null;
+    }
+    if (!element.isPhysical()) {
+      return null;
+    }
+    return getMainObjectFromElement(element);
+  }
+
+  @Nullable
+  private static ScObject getMainObjectFromElement(PsiElement element) {
+    for (; element != null; element = element.getParent()) {
+      if (element instanceof ScObject) {
+        ScObject obj = (ScObject) element;
+        if (ScalaMainMethodUtil.hasMainMethod(obj)) {
+          return obj;
+        }
+      } else if (element instanceof ScalaFile) {
+        return getMainObjectFromFile((ScalaFile) element);
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static ScObject getMainObjectFromFile(ScalaFile file) {
+    for (PsiClass aClass : file.getClasses()) {
+      if (!(aClass instanceof ScObject)) {
+        continue;
+      }
+      ScObject obj = (ScObject) aClass;
+      if (ScalaMainMethodUtil.hasMainMethod(obj)) {
+        // Potentially multiple matches, we'll pick the first one.
+        // TODO: prefer class with same name as file?
+        // TODO: skip if not main_class of a rule.
+        return obj;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static TargetIdeInfo getTarget(Project project, ScObject mainObject) {
+    File mainObjectFile = RunUtil.getFileForClass(mainObject);
+    if (mainObjectFile == null) {
+      return null;
+    }
+    Collection<TargetIdeInfo> scalaBinaryTargets = findScalaBinaryTargets(project, mainObjectFile);
+
+    // Scala objects are basically singletons with a '$' appended to the class name.
+    // The trunced qualified name removes the '$',
+    // so it matches the main class specified in the scala_binary rule.
+    String qualifiedName = mainObject.getTruncedQualifiedName();
+
+    if (qualifiedName == null) {
+      // out of date psi element; just take the first match
+      return Iterables.getFirst(scalaBinaryTargets, null);
+    }
+
+    // Can't use getName because of the '$'.
+    String className = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
+
+    // first look for a matching main_class
+    TargetIdeInfo match =
+        scalaBinaryTargets
+            .stream()
+            .filter(
+                target ->
+                    target.javaIdeInfo != null
+                        && qualifiedName.equals(target.javaIdeInfo.javaBinaryMainClass))
+            .findFirst()
+            .orElse(null);
+    if (match != null) {
+      return match;
+    }
+
+    match =
+        scalaBinaryTargets
+            .stream()
+            .filter(target -> className.equals(target.key.label.targetName().toString()))
+            .findFirst()
+            .orElse(null);
+    if (match != null) {
+      return match;
+    }
+    return Iterables.getFirst(scalaBinaryTargets, null);
+  }
+
+  /** Returns all scala_binary targets reachable from the given source file. */
+  private static Collection<TargetIdeInfo> findScalaBinaryTargets(
+      Project project, File mainClassFile) {
+    FilteredTargetMap map =
+        SyncCache.getInstance(project)
+            .get(
+                SCALA_BINARY_MAP_KEY,
+                BlazeScalaMainClassRunConfigurationProducer::computeTargetMap);
+    return map != null ? map.targetsForSourceFile(mainClassFile) : ImmutableList.of();
+  }
+
+  private static FilteredTargetMap computeTargetMap(Project project, BlazeProjectData projectData) {
+    return new FilteredTargetMap(
+        project,
+        projectData.artifactLocationDecoder,
+        projectData.targetMap,
+        (target) -> target.kind == Kind.SCALA_BINARY && target.isPlainTarget());
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributor.java b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributor.java
new file mode 100644
index 0000000..12f36eb
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributor.java
@@ -0,0 +1,48 @@
+/*
+ * 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.scala.run.producers;
+
+import com.intellij.execution.lineMarker.RunLineMarkerContributor;
+import com.intellij.icons.AllIcons;
+import com.intellij.psi.PsiElement;
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+import org.jetbrains.plugins.scala.runner.ScalaRunLineMarkerContributor;
+
+/** Replaces the icon for {@link ScalaRunLineMarkerContributor} to match other blaze plugins. */
+public class BlazeScalaRunLineMarkerContributor extends RunLineMarkerContributor {
+  @Nullable
+  @Override
+  public Info getInfo(PsiElement element) {
+    Info info = new ScalaRunLineMarkerContributor().getInfo(element);
+    if (info == null) {
+      return null;
+    }
+    return new ReplacementInfo(info, AllIcons.RunConfigurations.TestState.Run);
+  }
+
+  private static class ReplacementInfo extends Info {
+    ReplacementInfo(Info info, Icon icon) {
+      super(icon, info.actions, info.tooltipProvider);
+    }
+
+    @Override
+    public boolean shouldReplace(Info other) {
+      return Arrays.equals(actions, other.actions);
+    }
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducer.java b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducer.java
new file mode 100644
index 0000000..d3b46b2
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducer.java
@@ -0,0 +1,154 @@
+/*
+ * 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.scala.run.producers;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.google.idea.blaze.java.run.producers.BlazeJavaTestClassConfigurationProducer;
+import com.google.idea.blaze.java.run.producers.TestSizeAnnotationMap;
+import com.intellij.codeInsight.TestFrameworks;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.testIntegration.TestFramework;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.ScClass;
+import org.jetbrains.plugins.scala.testingSupport.test.scalatest.ScalaTestTestFramework;
+
+/**
+ * Producer for run configurations related to Scala test classes (not handled by JUnit) in Blaze.
+ * Handles only {@link ScalaTestTestFramework}. Other supported frameworks (junit and specs2) are
+ * handled by {@link BlazeJavaTestClassConfigurationProducer}.
+ */
+public class BlazeScalaTestClassConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public BlazeScalaTestClassConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    ScClass testClass = getTestClass(context);
+    if (testClass == null) {
+      return false;
+    }
+    sourceElement.set(testClass);
+    TargetIdeInfo target =
+        RunUtil.targetForTestClass(testClass, TestSizeAnnotationMap.getTestSize(testClass));
+    if (target == null) {
+      return false;
+    }
+    configuration.setTarget(target.key.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    flags.add(getTestFilterFlag(testClass));
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
+
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
+    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+    nameBuilder.setTargetString(testClass.getName());
+    configuration.setName(nameBuilder.build());
+    configuration.setNameChangedByUser(true); // don't revert to generated name
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    PsiClass testClass = getTestClass(context);
+    if (testClass == null) {
+      return false;
+    }
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null
+        || !Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)
+        || !Objects.equals(handlerState.getTestFilterFlag(), getTestFilterFlag(testClass))) {
+      return false;
+    }
+    TargetIdeInfo target =
+        RunUtil.targetForTestClass(testClass, TestSizeAnnotationMap.getTestSize(testClass));
+    return target != null && Objects.equals(configuration.getTarget(), target.key.label);
+  }
+
+  @Nullable
+  private static ScClass getTestClass(ConfigurationContext context) {
+    Location<?> location = context.getLocation();
+    if (location == null) {
+      return null;
+    }
+    location = JavaExecutionUtil.stepIntoSingleClass(location);
+    if (location == null) {
+      return null;
+    }
+    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
+      // handled by a different producer
+      return null;
+    }
+    return getTestClass(location);
+  }
+
+  @Nullable
+  private static ScClass getTestClass(Location<?> location) {
+    PsiElement element = location.getPsiElement();
+    ScClass testClass;
+    if (element instanceof ScClass) {
+      testClass = (ScClass) element;
+    } else {
+      testClass = PsiTreeUtil.getParentOfType(element, ScClass.class);
+    }
+    if (testClass != null && isTestClass(testClass)) {
+      return testClass;
+    }
+    return null;
+  }
+
+  private static boolean isTestClass(ScClass testClass) {
+    TestFramework framework = TestFrameworks.detectFramework(testClass);
+    return framework instanceof ScalaTestTestFramework && framework.isTestClass(testClass);
+  }
+
+  private static String getTestFilterFlag(PsiClass testClass) {
+    // TODO: may need to append '#' if implementation changes.
+    // https://github.com/bazelbuild/rules_scala/pull/216
+    return BlazeFlags.TEST_FILTER + "=" + testClass.getQualifiedName();
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributor.java b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributor.java
new file mode 100644
index 0000000..9133e7b
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributor.java
@@ -0,0 +1,146 @@
+/*
+ * 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.scala.run.producers;
+
+import com.intellij.codeInsight.TestFrameworks;
+import com.intellij.execution.TestStateStorage;
+import com.intellij.execution.lineMarker.ExecutorAction;
+import com.intellij.execution.lineMarker.RunLineMarkerContributor;
+import com.intellij.execution.testframework.TestIconMapper;
+import com.intellij.execution.testframework.sm.runner.states.TestStateInfo;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.testIntegration.TestFramework;
+import com.intellij.testIntegration.TestRunLineMarkerProvider;
+import java.util.Arrays;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+import org.jetbrains.plugins.scala.lang.psi.api.expr.ScInfixExpr;
+import org.jetbrains.plugins.scala.lang.psi.api.expr.ScReferenceExpression;
+import org.jetbrains.plugins.scala.lang.psi.api.statements.ScFunctionDefinition;
+import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef.ScClass;
+import org.jetbrains.plugins.scala.testingSupport.test.ScalaTestRunLineMarkerProvider;
+
+/**
+ * Generates run/debug gutter icons for scala_test, and scala_junit_tests.
+ *
+ * <p>{@link ScalaTestRunLineMarkerProvider} exists in the scala plugin, but it does not currently
+ * try to handle scalatest and specs2 at all. For JUnit tests, it has a bug that causes it to not
+ * generate icons for a test without run state (i.e., newly written tests).
+ */
+public class BlazeScalaTestRunLineMarkerContributor extends ScalaTestRunLineMarkerProvider {
+  @Nullable
+  @Override
+  public Info getInfo(PsiElement e) {
+    if (isIdentifier(e)) {
+      PsiElement element = e.getParent();
+      if (element instanceof ScClass) {
+        return getInfo((ScClass) element, null, super.getInfo(e));
+      } else if (element instanceof ScFunctionDefinition) {
+        ScClass testClass = PsiTreeUtil.getParentOfType(element, ScClass.class);
+        if (testClass != null) {
+          return getInfo(testClass, element, super.getInfo(e));
+        }
+      } else if (element instanceof ScReferenceExpression) {
+        // TODO: handle infix expressions. E.g., "foo" should "bar" in { baz }
+        return null;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static Info getInfo(ScClass testClass, @Nullable PsiElement testCase, Info toReplace) {
+    TestFramework framework = TestFrameworks.detectFramework(testClass);
+    if (framework == null) {
+      return null;
+    }
+    boolean isClass = testCase == null;
+    String url;
+    if (isClass) {
+      if (!framework.isTestClass(testClass)) {
+        return null;
+      }
+      url = "java:suite://" + testClass.getQualifiedName();
+    } else if (testCase instanceof ScFunctionDefinition) {
+      ScFunctionDefinition method = (ScFunctionDefinition) testCase;
+      if (!framework.isTestMethod(method)) {
+        return null;
+      }
+      url = "java:test://" + testClass.getQualifiedName() + "." + method.getName();
+    } else if (testCase instanceof ScInfixExpr) {
+      // TODO: handle this case.
+      return null;
+    } else {
+      return null;
+    }
+
+    return getInfo(url, testClass.getProject(), isClass, toReplace);
+  }
+
+  private static Info getInfo(String url, Project project, boolean isClass, Info toReplace) {
+    Icon icon = getTestStateIcon(url, project, isClass);
+    return new ReplacementInfo(
+        icon,
+        ExecutorAction.getActions(1),
+        RunLineMarkerContributor.RUN_TEST_TOOLTIP_PROVIDER,
+        toReplace);
+  }
+
+  /** Copied from {@link TestRunLineMarkerProvider#getTestStateIcon(String, Project, boolean)} */
+  private static Icon getTestStateIcon(String url, Project project, boolean isClass) {
+    TestStateStorage.Record state = TestStateStorage.getInstance(project).getState(url);
+    if (state != null) {
+      TestStateInfo.Magnitude magnitude = TestIconMapper.getMagnitude(state.magnitude);
+      if (magnitude != null) {
+        switch (magnitude) {
+          case ERROR_INDEX:
+          case FAILED_INDEX:
+            return AllIcons.RunConfigurations.TestState.Red2;
+          case PASSED_INDEX:
+          case COMPLETE_INDEX:
+            return AllIcons.RunConfigurations.TestState.Green2;
+          default:
+        }
+      }
+    }
+    return isClass
+        ? AllIcons.RunConfigurations.TestState.Run_run
+        : AllIcons.RunConfigurations.TestState.Run;
+  }
+
+  private static class ReplacementInfo extends Info {
+    private final Info toReplace;
+
+    ReplacementInfo(
+        Icon icon,
+        AnAction[] actions,
+        Function<PsiElement, String> tooltipProvider,
+        Info toReplace) {
+      super(icon, actions, tooltipProvider);
+      this.toReplace = toReplace;
+    }
+
+    @Override
+    public boolean shouldReplace(Info other) {
+      return toReplace != null && Arrays.equals(toReplace.actions, other.actions);
+    }
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/run/producers/NonBlazeProducerSuppressor.java b/scala/src/com/google/idea/blaze/scala/run/producers/NonBlazeProducerSuppressor.java
new file mode 100644
index 0000000..1979631
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/run/producers/NonBlazeProducerSuppressor.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.scala.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.RunConfigurationProducerService;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.plugins.scala.runner.ScalaApplicationConfigurationProducer;
+import org.jetbrains.plugins.scala.testingSupport.test.scalatest.ScalaTestConfigurationProducer;
+import org.jetbrains.plugins.scala.testingSupport.test.specs2.Specs2ConfigurationProducer;
+
+/** Suppresses certain non-Blaze configuration producers in Blaze projects. */
+public class NonBlazeProducerSuppressor extends AbstractProjectComponent {
+
+  private static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              ScalaApplicationConfigurationProducer.class,
+              Specs2ConfigurationProducer.class,
+              ScalaTestConfigurationProducer.class);
+
+  public NonBlazeProducerSuppressor(Project project) {
+    super(project);
+  }
+
+  @Override
+  public void projectOpened() {
+    if (Blaze.isBlazeProject(myProject)) {
+      suppressProducers(myProject);
+    }
+  }
+
+  private static void suppressProducers(Project project) {
+    RunConfigurationProducerService producerService =
+        RunConfigurationProducerService.getInstance(project);
+    PRODUCERS_TO_SUPPRESS.forEach(producerService::addIgnoredProducer);
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java
index 86f276f..9cf373b 100644
--- a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java
+++ b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java
@@ -20,7 +20,7 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 import com.google.idea.blaze.scala.sync.model.BlazeScalaSyncData;
-import java.util.Collection;
+import java.util.List;
 
 /** Provides libraries required by Scala rules. */
 public class BlazeScalaLibrarySource extends LibrarySource.Adapter {
@@ -31,11 +31,11 @@
   }
 
   @Override
-  public Collection<? extends BlazeLibrary> getLibraries() {
+  public List<? extends BlazeLibrary> getLibraries() {
     BlazeScalaSyncData syncData = blazeProjectData.syncState.get(BlazeScalaSyncData.class);
     if (syncData == null) {
       return ImmutableList.of();
     }
-    return syncData.importResult.libraries.values();
+    return syncData.importResult.libraries.values().asList();
   }
 }
diff --git a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java
index 20fa54c..475d374 100644
--- a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java
+++ b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java
@@ -40,7 +40,8 @@
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ModifiableRootModel;
-import com.intellij.openapi.roots.OrderEnumerator;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.roots.ui.configuration.libraryEditor.ExistingLibraryEditor;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -70,20 +71,17 @@
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.SCALA)) {
       return;
     }
-    OrderEnumerator.orderEntries(workspaceModule)
-        .forEachLibrary(
-            library -> {
-              // Convert the type of the SDK library to prevent the scala plugin from
-              // showing the missing SDK notification.
-              // TODO: use a canonical class in the SDK (e.g., scala.App) instead of the name?
-              if (library.getName() != null && library.getName().startsWith("scala-library")) {
-                ExistingLibraryEditor editor = new ExistingLibraryEditor(library, null);
-                editor.setType(ScalaLibraryType.instance());
-                editor.commit();
-                return false; // stop
-              }
-              return true; // continue
-            });
+    for (Library library : ProjectLibraryTable.getInstance(project).getLibraries()) {
+      // Convert the type of the SDK library to prevent the scala plugin from
+      // showing the missing SDK notification.
+      // TODO: use a canonical class in the SDK (e.g., scala.App) instead of the name?
+      if (library.getName() != null && library.getName().startsWith("scala-library")) {
+        ExistingLibraryEditor editor = new ExistingLibraryEditor(library, null);
+        editor.setType(ScalaLibraryType.instance());
+        editor.commit();
+        return;
+      }
+    }
   }
 
   @Override
diff --git a/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java b/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java
index 5410f2c..b4fcee4 100644
--- a/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java
+++ b/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.scala.sync.importer;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
@@ -26,11 +27,13 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.projectview.ProjectViewTargetImportFilter;
 import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.google.idea.blaze.java.sync.importer.JavaSourceFilter;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.scala.sync.model.BlazeScalaImportResult;
 import com.intellij.openapi.project.Project;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /** Builds a BlazeWorkspace. */
@@ -66,7 +69,7 @@
             .map(target -> target.key)
             .collect(Collectors.toList());
 
-    ImmutableMap.Builder<LibraryKey, BlazeJarLibrary> libraries = ImmutableMap.builder();
+    Map<LibraryKey, BlazeJarLibrary> libraries = Maps.newHashMap();
 
     // Add every jar in the transitive closure of dependencies.
     // Direct dependencies of the working set will be double counted by BlazeJavaWorkspaceImporter,
@@ -78,7 +81,7 @@
         continue;
       }
       // Except source targets.
-      if (importFilter.isSourceTarget(target)) {
+      if (importFilter.isSourceTarget(target) && JavaSourceFilter.canImportAsSource(target)) {
         continue;
       }
       if (target.javaIdeInfo != null) {
@@ -87,10 +90,10 @@
             .jars
             .stream()
             .map(BlazeJarLibrary::new)
-            .forEach(library -> libraries.put(library.key, library));
+            .forEach(library -> libraries.putIfAbsent(library.key, library));
       }
     }
 
-    return new BlazeScalaImportResult(libraries.build());
+    return new BlazeScalaImportResult(ImmutableMap.copyOf(libraries));
   }
 }
diff --git a/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassConfigurationProducerTest.java b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassConfigurationProducerTest.java
new file mode 100644
index 0000000..66d7e78
--- /dev/null
+++ b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaMainClassConfigurationProducerTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.scala.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.psi.PsiFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeScalaMainClassRunConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeScalaMainClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testUniqueScalaBinaryChosen() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:UnrelatedName")
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary {",
+            "  object MainClass {",
+            "    def main(args: Array[String]) {}",
+            "  }",
+            "}",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+
+    RunConfiguration config = createConfigurationFromLocation(scalaFile);
+
+    assertThat(config).isInstanceOf(BlazeRunConfiguration.class);
+    BlazeRunConfiguration blazeConfig = (BlazeRunConfiguration) config;
+    assertThat(blazeConfig).isNotNull();
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//com/google/binary:UnrelatedName"));
+  }
+
+  @Test
+  public void testNoScalaBinaryChosenIfNotInRDeps() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:MainClass")
+                    .addSource(sourceRoot("com/google/binary/OtherClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary {",
+            "  object MainClass {",
+            "    def main(args: Array[String]) {}",
+            "  }",
+            "}",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+
+    assertThat(createConfigurationFromLocation(scalaFile))
+        .isNotInstanceOf(BlazeRunConfiguration.class);
+  }
+
+  @Test
+  public void testNoResultForObjectWithoutMainMethod() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:MainClass")
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .setJavaInfo(JavaIdeInfo.builder().setMainClass("com.google.binary.MainClass"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary { object MainClass {} }",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+
+    assertThat(createConfigurationFromLocation(scalaFile)).isNull();
+  }
+
+  @Test
+  public void testScalaBinaryWithMatchingNameChosen() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:UnrelatedName")
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:MainClass")
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary {",
+            "  object MainClass {",
+            "    def main(args: Array[String]) {}",
+            "  }",
+            "}",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+
+    RunConfiguration config = createConfigurationFromLocation(scalaFile);
+    assertThat(config).isInstanceOf(BlazeRunConfiguration.class);
+    BlazeRunConfiguration blazeConfig = (BlazeRunConfiguration) config;
+    assertThat(blazeConfig).isNotNull();
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//com/google/binary:MainClass"));
+  }
+
+  @Test
+  public void testScalaBinaryWithMatchingMainClassChosen() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:UnrelatedName")
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_binary")
+                    .setLabel("//com/google/binary:OtherName")
+                    .setJavaInfo(JavaIdeInfo.builder().setMainClass("com.google.binary.MainClass"))
+                    .addSource(sourceRoot("com/google/binary/MainClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary {",
+            "  object MainClass {",
+            "    def main(args: Array[String]) {}",
+            "  }",
+            "}",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+
+    RunConfiguration config = createConfigurationFromLocation(scalaFile);
+
+    assertThat(config).isInstanceOf(BlazeRunConfiguration.class);
+    BlazeRunConfiguration blazeConfig = (BlazeRunConfiguration) config;
+    assertThat(blazeConfig).isNotNull();
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//com/google/binary:OtherName"));
+  }
+}
diff --git a/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributorTest.java b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributorTest.java
new file mode 100644
index 0000000..f65f44b
--- /dev/null
+++ b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaRunLineMarkerContributorTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.scala.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.intellij.execution.lineMarker.RunLineMarkerContributor.Info;
+import com.intellij.icons.AllIcons;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import java.util.List;
+import org.jetbrains.plugins.scala.runner.ScalaRunLineMarkerContributor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeScalaRunLineMarkerContributor} */
+@RunWith(JUnit4.class)
+public class BlazeScalaRunLineMarkerContributorTest extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testGetMainInfo() {
+    BlazeScalaRunLineMarkerContributor markerContributor = new BlazeScalaRunLineMarkerContributor();
+    ScalaRunLineMarkerContributor replacedContributor = new ScalaRunLineMarkerContributor();
+
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("com/google/binary/MainClass.scala"),
+            "package com.google.binary {",
+            "  object MainClass {",
+            "    def main(args: Array[String]) {}",
+            "    def foo() {}",
+            "  }",
+            "}",
+            "package scala { final class Array[T] {} }",
+            "package java.lang { public final class String {} }");
+    List<LeafPsiElement> elements =
+        PsiUtils.findAllChildrenOfClassRecursive(scalaFile, LeafPsiElement.class);
+    LeafPsiElement objectIdentifier =
+        elements
+            .stream()
+            .filter(e -> Objects.equal(e.getText(), "MainClass"))
+            .findFirst()
+            .orElse(null);
+    LeafPsiElement methodIdentifier =
+        elements.stream().filter(e -> Objects.equal(e.getText(), "main")).findFirst().orElse(null);
+    assertThat(objectIdentifier).isNotNull();
+    assertThat(methodIdentifier).isNotNull();
+
+    // Have main object info.
+    Info objectInfo = markerContributor.getInfo(objectIdentifier);
+    assertThat(objectInfo).isNotNull();
+    assertThat(objectInfo.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run);
+    assertThat(objectInfo.actions).hasLength(2);
+    assertThat(objectInfo.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(objectInfo.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    // Main object info replaces the one from the scala plugin
+    Info replacedObjectInfo = replacedContributor.getInfo(objectIdentifier);
+    assertThat(replacedObjectInfo).isNotNull();
+    assertThat(objectInfo.shouldReplace(replacedObjectInfo)).isTrue();
+
+    // Hae main method info
+    Info methodInfo = markerContributor.getInfo(methodIdentifier);
+    assertThat(methodInfo).isNotNull();
+    assertThat(methodInfo.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run);
+    assertThat(methodInfo.actions).hasLength(2);
+    assertThat(methodInfo.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(methodInfo.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    // Main method info replaces the one from the scala plugin
+    Info replacedMethodInfo = replacedContributor.getInfo(methodIdentifier);
+    assertThat(replacedMethodInfo).isNotNull();
+    assertThat(methodInfo.shouldReplace(replacedMethodInfo)).isTrue();
+
+    // No other element should get an info
+    elements
+        .stream()
+        .filter(e -> !Objects.equal(e, objectIdentifier) && !Objects.equal(e, methodIdentifier))
+        .forEach(e -> assertThat(markerContributor.getInfo(e)).isNull());
+  }
+}
diff --git a/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducerTest.java b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducerTest.java
new file mode 100644
index 0000000..1bbe794
--- /dev/null
+++ b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestClassConfigurationProducerTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.scala.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.run.producers.BlazeJavaTestClassConfigurationProducer;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link BlazeScalaTestClassConfigurationProducer} and {@link
+ * BlazeJavaTestClassConfigurationProducer}.
+ */
+@RunWith(JUnit4.class)
+public class BlazeScalaTestClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testJunitTestProducedFromPsiClass() {
+    PsiFile file =
+        createAndIndexFile(
+            new WorkspacePath("scala/com/google/test/TestClass.scala"),
+            "package com.google.test {",
+            "  class TestClass {",
+            "    @org.junit.Test",
+            "    def testMethod() {}",
+            "  }",
+            "}",
+            "package org.junit { trait Test }");
+    assertThat(file).isInstanceOf(ScalaFile.class);
+    ScalaFile scalaFile = (ScalaFile) file;
+    PsiClass[] classes = scalaFile.getClasses();
+    assertThat(classes).isNotEmpty();
+    PsiClass testClass = classes[0];
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_junit_test")
+                    .setLabel("//scala/com/google/test:TestClass")
+                    .addSource(sourceRoot("scala/com/google/test/TestClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(testClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).isNotNull();
+    assertThat(configurations).hasSize(1);
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//scala/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testSpecs2TestProducedFromPsiClass() {
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/junit/runner/RunWith.scala"),
+        "package org.junit.runner",
+        "class RunWith");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/specs2/runner/JUnitRunner.scala"),
+        "package org.specs2.runner",
+        "class JUnitRunner");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/specs2/mutable/SpecificationWithJUnit.scala"),
+        "package org.specs2.mutable",
+        "@org.junit.runner.RunWith(classOf[org.specs2.runner.JUnitRunner])",
+        "abstract class SpecificationWithJUnit extends org.specs2.mutable.Specification");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/specs2/mutable/Specification.scala"),
+        "package org.specs2.mutable",
+        "abstract class Specification extends org.specs2.mutable.SpecificationLike");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/specs2/mutable/SpecificationLike.scala"),
+        "package org.specs2.mutable",
+        "trait SpecificationLike extends",
+        "org.specs2.specification.core.mutable.SpecificationStructure");
+    createAndIndexFile(
+        WorkspacePath.createIfValid(
+            "scala/org/specs2/specification/core/mutable/SpecificationStructure.scala"),
+        "package org.specs2.specification.core.mutable",
+        "trait SpecificationStructure extends",
+        "org.specs2.specification.core.SpecificationStructure");
+    createAndIndexFile(
+        WorkspacePath.createIfValid(
+            "scala/org/specs2/specification/core/SpecificationStructure.scala"),
+        "package org.specs2.specification.core",
+        "trait SpecificationStructure");
+    PsiFile file =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("scala/com/google/test/TestClass.scala"),
+            "package com.google.test",
+            "class TestClass extends org.specs2.mutable.SpecificationWithJUnit");
+    assertThat(file).isInstanceOf(ScalaFile.class);
+    ScalaFile scalaFile = (ScalaFile) file;
+    PsiClass[] classes = scalaFile.getClasses();
+    assertThat(classes).isNotEmpty();
+    PsiClass testClass = classes[0];
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_junit_test")
+                    .setLabel("//scala/com/google/test:TestClass")
+                    .addSource(sourceRoot("scala/com/google/test/TestClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(testClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).isNotNull();
+    assertThat(configurations).hasSize(1);
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//scala/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testScalaTestProducedFromPsiClass() {
+    PsiFile file =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("scala/com/google/test/TestClass.scala"),
+            "package com.google.test {",
+            "  class TestClass extends org.scalatest.FlatSpec {",
+            "    \"this test\" should \"pass\" in {}",
+            "  }",
+            "}",
+            "package org.scalatest {",
+            "  trait FlatSpec extends Suite",
+            "  trait Suite",
+            "}");
+    assertThat(file).isInstanceOf(ScalaFile.class);
+    ScalaFile scalaFile = (ScalaFile) file;
+    PsiClass[] classes = scalaFile.getClasses();
+    assertThat(classes).isNotEmpty();
+    PsiClass testClass = classes[0];
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("scala_test")
+                    .setLabel("//scala/com/google/test:TestClass")
+                    .addSource(sourceRoot("scala/com/google/test/TestClass.scala"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(testClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).isNotNull();
+    assertThat(configurations).hasSize(1);
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeScalaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//scala/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributorTest.java b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributorTest.java
new file mode 100644
index 0000000..b0dc3f1
--- /dev/null
+++ b/scala/tests/integrationtests/com/google/idea/blaze/scala/run/producers/BlazeScalaTestRunLineMarkerContributorTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.scala.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.intellij.execution.lineMarker.RunLineMarkerContributor.Info;
+import com.intellij.icons.AllIcons;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.LeafPsiElement;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeScalaTestRunLineMarkerContributor} */
+@RunWith(JUnit4.class)
+public class BlazeScalaTestRunLineMarkerContributorTest
+    extends BlazeRunConfigurationProducerTestCase {
+  private final BlazeScalaTestRunLineMarkerContributor markerContributor =
+      new BlazeScalaTestRunLineMarkerContributor();
+
+  @Test
+  public void testIgnoreNonTest() {
+    PsiFile scalaFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("src/main/scala/com/google/library/Library.scala"),
+            "package com.google.library",
+            "class Library {",
+            "  def method() {}",
+            "}");
+    List<LeafPsiElement> elements =
+        PsiUtils.findAllChildrenOfClassRecursive(scalaFile, LeafPsiElement.class);
+    elements.forEach(e -> assertThat(markerContributor.getInfo(e)).isNull());
+  }
+
+  @Test
+  public void testGetJunitTestInfo() {
+    PsiFile junitTestFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("src/test/scala/com/google/test/JunitTest.scala"),
+            "package com.google.test {",
+            "  class JunitTest {",
+            "    @org.junit.Test",
+            "    def testMethod() {}",
+            "  }",
+            "}",
+            "package org.junit { trait Test }");
+    List<LeafPsiElement> elements =
+        PsiUtils.findAllChildrenOfClassRecursive(junitTestFile, LeafPsiElement.class);
+    LeafPsiElement classIdentifier =
+        elements
+            .stream()
+            .filter(e -> Objects.equal(e.getText(), "JunitTest"))
+            .findFirst()
+            .orElse(null);
+    LeafPsiElement methodIdentifier =
+        elements
+            .stream()
+            .filter(e -> Objects.equal(e.getText(), "testMethod"))
+            .findFirst()
+            .orElse(null);
+    assertThat(classIdentifier).isNotNull();
+    assertThat(methodIdentifier).isNotNull();
+
+    Info classInfo = markerContributor.getInfo(classIdentifier);
+    assertThat(classInfo).isNotNull();
+    assertThat(classInfo.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run_run);
+    assertThat(classInfo.actions).hasLength(2);
+    assertThat(classInfo.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(classInfo.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    Info methodInfo = markerContributor.getInfo(methodIdentifier);
+    assertThat(methodInfo).isNotNull();
+    assertThat(methodInfo.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run);
+    assertThat(methodInfo.actions).hasLength(2);
+    assertThat(methodInfo.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(methodInfo.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    elements
+        .stream()
+        .filter(e -> !Objects.equal(e, classIdentifier) && !Objects.equal(e, methodIdentifier))
+        .forEach(e -> assertThat(markerContributor.getInfo(e)).isNull());
+  }
+
+  @Test
+  public void testGetSpecs2TestInfo() {
+    createAndIndexFile(
+        WorkspacePath.createIfValid("scala/org/junit/runner/RunWith.scala"),
+        "package org.junit.runner",
+        "class RunWith");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("src/test/scala/org/specs2/runner/JUnitRunner.scala"),
+        "package org.specs2.runner",
+        "class JUnitRunner");
+    createAndIndexFile(
+        WorkspacePath.createIfValid(
+            "src/test/scala/org/specs2/mutable/SpecificationWithJUnit.scala"),
+        "package org.specs2.mutable",
+        "@org.junit.runner.RunWith(classOf[org.specs2.runner.JUnitRunner])",
+        "abstract class SpecificationWithJUnit extends org.specs2.mutable.Specification");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("src/test/scala/org/specs2/mutable/Specification.scala"),
+        "package org.specs2.mutable",
+        "abstract class Specification extends org.specs2.mutable.SpecificationLike");
+    createAndIndexFile(
+        WorkspacePath.createIfValid("src/test/scala/org/specs2/mutable/SpecificationLike.scala"),
+        "package org.specs2.mutable",
+        "trait SpecificationLike extends",
+        "org.specs2.specification.core.mutable.SpecificationStructure");
+    createAndIndexFile(
+        WorkspacePath.createIfValid(
+            "src/test/scala/org/specs2/specification/core/mutable/SpecificationStructure.scala"),
+        "package org.specs2.specification.core.mutable",
+        "trait SpecificationStructure extends",
+        "org.specs2.specification.core.SpecificationStructure");
+    createAndIndexFile(
+        WorkspacePath.createIfValid(
+            "src/test/scala/org/specs2/specification/core/SpecificationStructure.scala"),
+        "package org.specs2.specification.core",
+        "trait SpecificationStructure");
+    PsiFile specs2TestFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("src/test/scala/com/google/test/Specs2Test.scala"),
+            "package com.google.test",
+            "class Specs2Test extends org.specs2.mutable.SpecificationWithJUnit");
+    List<LeafPsiElement> elements =
+        PsiUtils.findAllChildrenOfClassRecursive(specs2TestFile, LeafPsiElement.class);
+    LeafPsiElement classIdentifier =
+        elements
+            .stream()
+            .filter(e -> Objects.equal(e.getText(), "Specs2Test"))
+            .findFirst()
+            .orElse(null);
+    assertThat(classIdentifier).isNotNull();
+
+    Info info = markerContributor.getInfo(classIdentifier);
+    assertThat(info).isNotNull();
+    assertThat(info.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run_run);
+    assertThat(info.actions).hasLength(2);
+    assertThat(info.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(info.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    elements
+        .stream()
+        .filter(e -> !Objects.equal(e, classIdentifier))
+        .forEach(e -> assertThat(markerContributor.getInfo(e)).isNull());
+  }
+
+  @Test
+  public void testGetScalaTestInfo() {
+    PsiFile scalaTestFile =
+        createAndIndexFile(
+            WorkspacePath.createIfValid("src/test/scala/com/google/test/ScalaTest.scala"),
+            "package com.google.test {",
+            "  class ScalaTest extends org.scalatest.FlatSpec {",
+            "    \"this test\" should \"pass\" in {}",
+            "  }",
+            "}",
+            "package org.scalatest {",
+            "  trait FlatSpec extends Suite",
+            "  trait Suite",
+            "}");
+    List<LeafPsiElement> elements =
+        PsiUtils.findAllChildrenOfClassRecursive(scalaTestFile, LeafPsiElement.class);
+    LeafPsiElement classIdentifier =
+        elements
+            .stream()
+            .filter(e -> Objects.equal(e.getText(), "ScalaTest"))
+            .findFirst()
+            .orElse(null);
+    assertThat(classIdentifier).isNotNull();
+
+    Info info = markerContributor.getInfo(classIdentifier);
+    assertThat(info).isNotNull();
+    assertThat(info.icon).isEqualTo(AllIcons.RunConfigurations.TestState.Run_run);
+    assertThat(info.actions).hasLength(2);
+    assertThat(info.actions[0].getTemplatePresentation().getText()).startsWith("Run ");
+    assertThat(info.actions[1].getTemplatePresentation().getText()).startsWith("Debug ");
+
+    elements
+        .stream()
+        .filter(e -> !Objects.equal(e, classIdentifier))
+        .forEach(e -> assertThat(markerContributor.getInfo(e)).isNull());
+  }
+}
diff --git a/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java b/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java
index c364c85..b21cfd2 100644
--- a/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java
+++ b/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.io.MockInputStreamProvider;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -36,15 +37,8 @@
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
 import com.google.idea.blaze.scala.sync.source.ScalaJavaLikeLanguage;
 import com.intellij.openapi.extensions.ExtensionPoint;
-import com.intellij.util.containers.HashMap;
-import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.util.List;
-import java.util.Map;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -63,8 +57,7 @@
           artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
   @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
+  protected void initTest(Container applicationServices, Container projectServices) {
     super.initTest(applicationServices, projectServices);
 
     mockInputStreamProvider = new MockInputStreamProvider();
@@ -244,24 +237,4 @@
                 .build());
     issues.assertNoIssues();
   }
-
-  private static class MockInputStreamProvider implements InputStreamProvider {
-
-    private final Map<String, InputStream> files = new HashMap<>();
-
-    MockInputStreamProvider addFile(String filePath, String src) {
-      try {
-        files.put(filePath, new ByteArrayInputStream(src.getBytes("UTF-8")));
-      } catch (UnsupportedEncodingException ignored) {
-        // ignored
-      }
-      return this;
-    }
-
-    @Nullable
-    @Override
-    public InputStream getFile(@NotNull File path) {
-      return files.get(path.getPath());
-    }
-  }
 }
diff --git a/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java b/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java
index 363bcfb..6164d79 100644
--- a/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java
+++ b/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java
@@ -516,6 +516,59 @@
     assertThat(scalaImportResult.libraries).isEmpty();
   }
 
+  @Test
+  public void testDuplicateScalaLibraries() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar"))))
+                    .addDependency("//src/main/scala/imports:import1")
+                    .addDependency("//src/main/scala/imports:import2"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/imports:import1")
+                    .setKind("scala_import")
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/imports/import.jar"))
+                                    .setClassJar(gen("src/main/scala/imports/import.jar")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/imports:import2")
+                    .setKind("scala_import")
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/imports/import.jar"))
+                                    .setClassJar(gen("src/main/scala/imports/import.jar")))))
+            .build();
+
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(scalaImportResult.libraries).hasSize(1);
+    assertThat(hasLibrary(scalaImportResult.libraries, "import")).isTrue();
+  }
+
   private static boolean hasLibrary(
       Map<LibraryKey, BlazeJarLibrary> libraries, String libraryName) {
     return libraries
diff --git a/sdkcompat/BUILD b/sdkcompat/BUILD
index a152a21..1d629d7 100644
--- a/sdkcompat/BUILD
+++ b/sdkcompat/BUILD
@@ -8,12 +8,13 @@
     name = "sdkcompat",
     visibility = ["//visibility:public"],
     exports = select_for_plugin_api({
-        "android-studio-2.3.0.8": ["//sdkcompat/v162"],
+        "android-studio-3.0.0.9": ["//sdkcompat/v171"],
         "android-studio-2.3.1.0": ["//sdkcompat/v162"],
-        "intellij-2017.1.1": ["//sdkcompat/v171"],
-        "intellij-2016.3.1": ["//sdkcompat/v163"],
-        "clion-162.1967.7": ["//sdkcompat/v162"],
-        "clion-2016.3.2": ["//sdkcompat/v163"],
+        "intellij-2017.2.2": ["//sdkcompat/v172"],
+        "intellij-2017.1.5": ["//sdkcompat/v171"],
+        "intellij-ue-2017.2.2": ["//sdkcompat/v172"],
+        "intellij-ue-2017.1.5": ["//sdkcompat/v171"],
+        "clion-2017.2.1": ["//sdkcompat/v172"],
         "clion-2017.1.1": ["//sdkcompat/v171"],
     }),
 )
diff --git a/sdkcompat/v162/BUILD b/sdkcompat/v162/BUILD
index 8eb53e7..1a26f68 100644
--- a/sdkcompat/v162/BUILD
+++ b/sdkcompat/v162/BUILD
@@ -8,25 +8,31 @@
     name = "v162",
     srcs = glob([
         "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/profile/**",
+        "com/google/idea/sdkcompat/run/**",
         "com/google/idea/sdkcompat/smrunner/**",
         "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/ui/**",
         "com/google/idea/sdkcompat/vcs/**",
     ]) + select_for_ide(
         android_studio = glob([
             "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/java/**",
         ]),
         clion = glob([
             "com/google/idea/sdkcompat/cidr/**",
             "com/google/idea/sdkcompat/clion/**",
         ]),
         intellij = glob([
-            "com/google/idea/sdkcompat/python/**",
             "com/google/idea/sdkcompat/dart/**",
+            "com/google/idea/sdkcompat/java/**",
+            "com/google/idea/sdkcompat/python/**",
         ]),
     ),
     visibility = ["//sdkcompat:__pkg__"],
     deps = [
         "//intellij_platform_sdk:plugin_api",
+        "//intellij_platform_sdk:junit",
         "@jsr305_annotations//jar",
     ] + select_for_ide(
         intellij = ["//third_party/python"],
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
new file mode 100644
index 0000000..b330ffd
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+import java.util.Map;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerMacrosAdapter extends OCCompilerMacros {
+  // v171
+  public void addAllFeatures(Map<String, String> result, Map<String, String> features) {
+    result.putAll(features);
+  }
+  // v172
+  public abstract String getAllDefines(OCLanguageKind kind, VirtualFile vf);
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
new file mode 100644
index 0000000..f4fc622
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerSettingsAdapter extends OCCompilerSettings {}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
index 912eb77..02f0fa7 100644
--- a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -1,5 +1,6 @@
 package com.google.idea.sdkcompat.cidr;
 
+import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
@@ -8,13 +9,18 @@
 import java.util.Set;
 
 /** Adapter to bridge different SDK versions. */
-public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
+public abstract class OCResolveConfigurationAdapter extends UserDataHolderBase
+    implements OCResolveConfiguration {
   /* v171 */
-  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile);
+  public abstract List<VirtualFile> getPrecompiledHeaders(
+      OCLanguageKind kind, VirtualFile sourceFile);
 
   /* v171 */
-  public Collection<VirtualFile> getSources();
+  public abstract Collection<VirtualFile> getSources();
 
   /* v171 */
-  public Set<VirtualFile> getPrecompiledHeaders();
+  public abstract Set<VirtualFile> getPrecompiledHeaders();
+
+  /* v172 */
+  public abstract String getPreprocessorDefines(OCLanguageKind kind, VirtualFile virtualFile);
 }
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
index 4b17895..79efe83 100644
--- a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -14,12 +14,28 @@
     return trackers.computeIfAbsent(project, OCWorkspaceModificationTrackers::new);
   }
 
-  /** Must be called inside a write action, on the EDT. */
+  /**
+   * Causes symbol tables to be rebuilt and invalidates cidr caches attached to resolve
+   * configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
   public static void incrementModificationCounts(Project project) {
+    partialIncModificationCounts(project);
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+
+  /**
+   * Does not trigger symbol table rebuilding, and only clears part of the cidr caches attached to
+   * resolve configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
+  public static void partialIncModificationCounts(Project project) {
     OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
     modTrackers.getProjectFilesListTracker().incModificationCount();
     modTrackers.getSourceFilesListTracker().incModificationCount();
     modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
-    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
   }
 }
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java b/sdkcompat/v162/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
new file mode 100644
index 0000000..c20e8bf
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
@@ -0,0 +1,24 @@
+package com.google.idea.sdkcompat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+
+/** List of java-related configuration producers for a given plugin version. */
+public class JavaConfigurationProducerList {
+
+  /**
+   * Returns a list of run configuration producers to suppress for Blaze projects.
+   *
+   * <p>These classes must all be accessible from the Blaze plugin's classpath (e.g. they shouldn't
+   * belong to any plugins not listed as dependencies of the Blaze plugin).
+   */
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
+              com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
+              com.intellij.execution.junit.TestClassConfigurationProducer.class,
+              com.intellij.execution.junit.TestMethodConfigurationProducer.class,
+              com.intellij.execution.junit.PatternConfigurationProducer.class,
+              com.intellij.execution.application.ApplicationConfigurationProducer.class);
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java b/sdkcompat/v162/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
new file mode 100644
index 0000000..60bface
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.profile;
+
+import com.intellij.codeInspection.ex.InspectionProfileImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
+
+/** Utility to bridge different SDK versions. */
+public class InspectionProfileUtil {
+
+  public static void disableTool(String toolName, Project project) {
+    InspectionProfileImpl profile =
+        (InspectionProfileImpl)
+            InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
+    profile.disableTool(toolName, project);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
index b3785da..27468aa 100644
--- a/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -1,12 +1,11 @@
 package com.google.idea.sdkcompat.python;
 
 import com.google.common.collect.ImmutableList;
-import java.util.Collection;
 
 /** List of python configuration producers for a given plugin version. */
 public class PyConfigurationProducersList {
 
-  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
+  public static final ImmutableList<Class<?>> PRODUCERS_TO_SUPPRESS =
       ImmutableList.of(
           com.jetbrains.python.run.PythonRunConfigurationProducer.class,
           com.jetbrains.python.testing.attest.PythonAtTestConfigurationProducer.class,
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/PythonFacetUtil.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PythonFacetUtil.java
new file mode 100644
index 0000000..d5a5206
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PythonFacetUtil.java
@@ -0,0 +1,31 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.facet.FacetTypeId;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.jetbrains.python.facet.LibraryContributingFacet;
+import com.jetbrains.python.facet.PythonFacet;
+import com.jetbrains.python.facet.PythonFacetType;
+import javax.annotation.Nullable;
+
+/**
+ * This class is a hack to get around Python SDK incompatibilities. Provides a consistent API for
+ * both IntelliJ and CLion.
+ */
+public class PythonFacetUtil {
+  public static FacetTypeId<PythonFacet> getFacetId() {
+    return PythonFacet.ID;
+  }
+
+  public static PythonFacetType getTypeInstance() {
+    return PythonFacetType.getInstance();
+  }
+
+  @Nullable
+  public static Sdk getSdk(LibraryContributingFacet<?> facet) {
+    if (!(facet instanceof PythonFacet)) {
+      return null;
+    }
+    PythonFacet pythonFacet = (PythonFacet) facet;
+    return pythonFacet.getConfiguration().getSdk();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
new file mode 100644
index 0000000..09c1c12
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
@@ -0,0 +1,14 @@
+package com.google.idea.sdkcompat.run;
+
+import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
+import com.intellij.openapi.util.InvalidDataException;
+import org.jdom.Element;
+
+/** SDK compatibility bridge for {@link RunnerAndConfigurationSettingsImpl}. */
+public class RunnerAndConfigurationSettingsCompatUtils {
+
+  public static void readConfiguration(RunnerAndConfigurationSettingsImpl settings, Element element)
+      throws InvalidDataException {
+    settings.readExternal(element);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java
index 94ab905..8267111 100644
--- a/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -4,7 +4,7 @@
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
 
-/** Created by tomlu on 12/22/16. */
+/** SDK adapter to use transaction guards. */
 public class Transactions {
   public static void submitTransactionAndWait(Runnable runnable) {
     ApplicationManager.getApplication().invokeAndWait(runnable, ModalityState.any());
@@ -13,4 +13,10 @@
   public static void submitTransaction(Disposable disposable, Runnable runnable) {
     ApplicationManager.getApplication().invokeLater(runnable);
   }
+
+  /** Runs {@link Runnable} as a write action, inside a transaction. */
+  public static void submitWriteActionTransactionAndWait(Runnable runnable) {
+    submitTransactionAndWait(
+        (Runnable) () -> ApplicationManager.getApplication().runWriteAction(runnable));
+  }
 }
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
new file mode 100644
index 0000000..1b41d6d
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
@@ -0,0 +1,50 @@
+package com.google.idea.sdkcompat.ui;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.PsiElement;
+import com.intellij.xml.breadcrumbs.BreadcrumbsInfoProvider;
+import java.util.Arrays;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for {@link BreadcrumbsInfoProvider}, deprecated in 172. */
+public abstract class BreadcrumbsProviderSdkCompatAdapter extends BreadcrumbsInfoProvider {
+
+  public static BreadcrumbsProviderSdkCompatAdapter[] getBreadcrumbsProviders() {
+    return Arrays.stream(BreadcrumbsInfoProvider.EP_NAME.getExtensions())
+        .map(BreadcrumbsProviderSdkCompatAdapter::fromBreadcrumbsProvider)
+        .toArray(BreadcrumbsProviderSdkCompatAdapter[]::new);
+  }
+
+  private static BreadcrumbsProviderSdkCompatAdapter fromBreadcrumbsProvider(
+      BreadcrumbsInfoProvider delegate) {
+    return new BreadcrumbsProviderSdkCompatAdapter() {
+
+      @Override
+      public Language[] getLanguages() {
+        return delegate.getLanguages();
+      }
+
+      @Override
+      public boolean acceptElement(PsiElement psiElement) {
+        return delegate.acceptElement(psiElement);
+      }
+
+      @Override
+      public String getElementInfo(PsiElement psiElement) {
+        return delegate.getElementInfo(psiElement);
+      }
+
+      @Nullable
+      @Override
+      public String getElementTooltip(PsiElement element) {
+        return delegate.getElementTooltip(element);
+      }
+
+      @Nullable
+      @Override
+      public PsiElement getParent(PsiElement element) {
+        return delegate.getParent(element);
+      }
+    };
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
new file mode 100644
index 0000000..c962097
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
@@ -0,0 +1,17 @@
+package com.google.idea.sdkcompat.ui;
+
+import java.awt.Component;
+
+/** Works around b/64290580 which affects certain SDK versions. */
+public final class RequestFocusCompatUtils {
+  private RequestFocusCompatUtils() {}
+
+  /**
+   * Focuses the given component.
+   *
+   * @see Component#requestFocus()
+   */
+  public static void requestFocus(Component component) {
+    component.requestFocus();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
new file mode 100644
index 0000000..0aebb39
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
@@ -0,0 +1,32 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.openapi.vcs.changes.ChangeListManagerGate;
+
+/**
+ * Works around b/63888111 / <a href="https://youtrack.jetbrains.com/issue/IJSDK-284">IJSDK-284</a>,
+ * which breaks support for {@link ChangeListManagerGate#editName(String, String)} in some SDK
+ * versions. <br>
+ * <br>
+ * The methods in this class call the corresponding methods on the {@link ChangeListManagerGate}
+ * instead of the {@link ChangeListManager}. It is normal to use this during a VCS update.
+ */
+public final class ChangeListManagerGateCompatUtils {
+  private ChangeListManagerGateCompatUtils() {}
+
+  public static void editName(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String oldName,
+      String newName) {
+    addGate.editName(oldName, newName);
+  }
+
+  public static void editComment(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String name,
+      String newComment) {
+    addGate.editComment(name, newComment);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
new file mode 100644
index 0000000..88834c7
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.VcsConfiguration;
+import com.intellij.spellchecker.ui.SpellCheckingEditorCustomization;
+import com.intellij.ui.EditorCustomization;
+import com.intellij.ui.RightMarginEditorCustomization;
+import java.util.List;
+
+/** Handles VCS changelist editor customizations that differ between SDK versions. */
+public class VcsEditorConfigurationCompatUtils {
+
+  public static List<EditorCustomization> getVcsConfigurationCustomizations(
+      Project project, VcsConfiguration config) {
+    return ImmutableList.of(
+        SpellCheckingEditorCustomization.getInstance(config.CHECK_COMMIT_MESSAGE_SPELLING),
+        new RightMarginEditorCustomization(
+            config.USE_COMMIT_MESSAGE_MARGIN, config.COMMIT_MESSAGE_MARGIN_SIZE));
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
deleted file mode 100644
index cf3a258..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.google.idea.sdkcompat.cidr;
-
-import com.jetbrains.cidr.execution.testing.CidrTestUtil;
-
-/** Adapter to bridge different SDK versions. */
-public class CidrGoogleTestUtilAdapter extends CidrTestUtil {}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
deleted file mode 100644
index 68772fa..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.google.idea.sdkcompat.cidr;
-
-import com.google.common.base.Joiner;
-import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/** Adapter to bridge different SDK versions. */
-public class CidrSwitchBuilderAdapter extends CidrSwitchBuilder {
-  /**
-   * Old CidrSwitchBuilder is unable to deal with options with spaces embedded. This is a hack to
-   * preserve the old behaviour for 2016.3 Original hack explanation: - this list of switches is
-   * currently only used in one place -- GCCCompiler.tryRunGCC. - list is written to an argument
-   * file, whitespace-separated, then passed as a @file arg to clang. In this context, escaped
-   * whitespace within a single arg is not handled. Currently, the only way (short of using
-   * reflection) to ensure unescaped whitespace is to have CidrSwitchBuilder treat whitespace as a
-   * delimiter between args.
-   */
-  public CidrSwitchBuilderAdapter addAllRaw(List<String> switches) {
-    switches = switches.stream().map(flag -> flag.replace("\\ ", " ")).collect(Collectors.toList());
-    addAll(Joiner.on(" ").join(switches), CidrSwitchBuilder.Format.FILE_ARGS);
-    return this;
-  }
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
deleted file mode 100644
index 912eb77..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.google.idea.sdkcompat.cidr;
-
-import com.intellij.openapi.vfs.VirtualFile;
-import com.jetbrains.cidr.lang.OCLanguageKind;
-import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-/** Adapter to bridge different SDK versions. */
-public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
-  /* v171 */
-  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile);
-
-  /* v171 */
-  public Collection<VirtualFile> getSources();
-
-  /* v171 */
-  public Set<VirtualFile> getPrecompiledHeaders();
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
deleted file mode 100644
index f402269..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.google.idea.sdkcompat.clion;
-
-import com.google.common.collect.ImmutableList;
-import com.intellij.execution.actions.RunConfigurationProducer;
-import com.jetbrains.cidr.cpp.execution.testing.CMakeGoogleTestRunConfigurationProducer;
-
-/** List of C/C++ configuration producers for a given plugin version. */
-public class CMakeConfigurationProducersList {
-  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
-      PRODUCERS_TO_SUPPRESS = ImmutableList.of(CMakeGoogleTestRunConfigurationProducer.class);
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
deleted file mode 100644
index 8075c2e..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.google.idea.sdkcompat.codestyle;
-
-import com.intellij.openapi.util.TextRange;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.codeStyle.ChangedRangesInfo;
-import com.intellij.psi.codeStyle.CodeStyleManager;
-import com.intellij.util.IncorrectOperationException;
-import java.util.ArrayList;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-
-/** Adapter to bridge different SDK versions. */
-public abstract class DelegatingCodeStyleManagerSdkCompatAdapter extends CodeStyleManager {
-
-  protected CodeStyleManager delegate;
-
-  protected DelegatingCodeStyleManagerSdkCompatAdapter(CodeStyleManager delegate) {
-    this.delegate = delegate;
-  }
-
-  @Override
-  public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
-      throws IncorrectOperationException {
-    List<TextRange> ranges = new ArrayList<>();
-    ranges.addAll(info.insertedRanges);
-    ranges.addAll(info.allChangedRanges);
-    this.reformatTextWithContext(file, ranges);
-  }
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
deleted file mode 100644
index b3785da..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.google.idea.sdkcompat.python;
-
-import com.google.common.collect.ImmutableList;
-import java.util.Collection;
-
-/** List of python configuration producers for a given plugin version. */
-public class PyConfigurationProducersList {
-
-  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
-      ImmutableList.of(
-          com.jetbrains.python.run.PythonRunConfigurationProducer.class,
-          com.jetbrains.python.testing.attest.PythonAtTestConfigurationProducer.class,
-          com.jetbrains.python.testing.nosetest.PythonNoseTestConfigurationProducer.class,
-          com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer.class,
-          com.jetbrains.python.testing.pytest.PyTestConfigurationProducer.class,
-          com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer.class);
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
deleted file mode 100644
index 7330e11..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.google.idea.sdkcompat.python;
-
-import com.intellij.openapi.module.Module;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.projectRoots.Sdk;
-import com.intellij.psi.PsiElement;
-import com.intellij.psi.PsiFile;
-import com.intellij.psi.PsiManager;
-import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
-import javax.annotation.Nullable;
-
-/** Adapter to bridge different SDK versions. */
-public class PyQualifiedNameResolveContextAdapter extends QualifiedNameResolveContext {
-
-  private final QualifiedNameResolveContext delegate;
-
-  PyQualifiedNameResolveContextAdapter(QualifiedNameResolveContext delegate) {
-    this.delegate = delegate;
-  }
-
-  @Override
-  public void copyFrom(QualifiedNameResolveContext context) {
-    delegate.copyFrom(context);
-  }
-
-  @Override
-  public void setFromElement(PsiElement element) {
-    delegate.setFromElement(element);
-  }
-
-  @Override
-  public void setFromModule(Module module) {
-    delegate.setFromModule(module);
-  }
-
-  @Override
-  public void setFromSdk(Project project, Sdk sdk) {
-    delegate.setFromSdk(project, sdk);
-  }
-
-  @Override
-  public void setSdk(Sdk sdk) {
-    delegate.setSdk(sdk);
-  }
-
-  @Override
-  @Nullable
-  public Module getModule() {
-    return delegate.getModule();
-  }
-
-  @Override
-  public boolean isValid() {
-    return delegate.isValid();
-  }
-
-  @Override
-  @Nullable
-  public PsiFile getFootholdFile() {
-    return delegate.getFootholdFile();
-  }
-
-  @Override
-  public PsiManager getPsiManager() {
-    return delegate.getPsiManager();
-  }
-
-  @Override
-  public Project getProject() {
-    return delegate.getProject();
-  }
-
-  @Override
-  public Sdk getSdk() {
-    return delegate.getSdk();
-  }
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
deleted file mode 100644
index 36d4767..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.google.idea.sdkcompat.python;
-
-import com.jetbrains.python.psi.PyQualifiedExpression;
-import com.jetbrains.python.psi.resolve.PyReferenceResolveProvider;
-import com.jetbrains.python.psi.resolve.RatedResolveResult;
-import com.jetbrains.python.psi.types.TypeEvalContext;
-import java.util.List;
-
-/** Adapter to bridge different SDK versions. */
-public interface PyReferenceResolveProviderAdapter extends PyReferenceResolveProvider {
-
-  @Override
-  default List<RatedResolveResult> resolveName(PyQualifiedExpression element) {
-    TypeEvalContext context = TypeEvalContext.codeInsightFallback(element.getProject());
-    return resolveName(element, context);
-  }
-
-  List<RatedResolveResult> resolveName(PyQualifiedExpression element, TypeEvalContext context);
-}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java b/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
deleted file mode 100644
index b0a7697..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.google.idea.sdkcompat.vcs;
-
-import com.intellij.openapi.vcs.FilePath;
-import com.intellij.openapi.vcs.history.VcsRevisionNumber;
-import com.intellij.openapi.vcs.merge.MergeData;
-import org.jetbrains.annotations.Nullable;
-
-/** SDK adapter for creating {@link MergeData}. */
-public final class MergeDataBuilder {
-  private byte[] baseContent;
-  private byte[] theirsContent;
-  private byte[] yoursContent;
-
-  @Nullable private VcsRevisionNumber theirsRevisionNumber;
-
-  public void setBaseContent(byte[] baseContent) {
-    this.baseContent = baseContent;
-  }
-
-  public void setTheirsContent(byte[] theirsContent) {
-    this.theirsContent = theirsContent;
-  }
-
-  public void setYoursContent(byte[] yoursContent) {
-    this.yoursContent = yoursContent;
-  }
-
-  public void setBaseRevisionNumber(@Nullable VcsRevisionNumber baseRevisionNumber) {}
-
-  public void setTheirsRevisionNumber(@Nullable VcsRevisionNumber theirsRevisionNumber) {
-    this.theirsRevisionNumber = theirsRevisionNumber;
-  }
-
-  public void setYoursRevisionNumber(@Nullable VcsRevisionNumber yoursRevisionNumber) {}
-
-  public void setBaseFilePath(@Nullable FilePath baseFilePath) {}
-
-  public void setTheirsFilePath(@Nullable FilePath theirsFilePath) {}
-
-  public void setYoursFilePath(@Nullable FilePath yoursFilePath) {}
-
-  public MergeData build() {
-    MergeData mergeData = new MergeData();
-
-    mergeData.ORIGINAL = baseContent;
-
-    mergeData.LAST = theirsContent;
-    mergeData.LAST_REVISION_NUMBER = theirsRevisionNumber;
-
-    mergeData.CURRENT = yoursContent;
-
-    return mergeData;
-  }
-}
diff --git a/sdkcompat/v171/BUILD b/sdkcompat/v171/BUILD
index d8392a9..de67a83 100644
--- a/sdkcompat/v171/BUILD
+++ b/sdkcompat/v171/BUILD
@@ -8,26 +8,35 @@
     name = "v171",
     srcs = glob([
         "com/google/idea/sdkcompat/codestyle/**",
-        "com/google/idea/sdkcompat/python/**",
+        "com/google/idea/sdkcompat/profile/**",
+        "com/google/idea/sdkcompat/run/**",
         "com/google/idea/sdkcompat/smrunner/**",
         "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/ui/**",
         "com/google/idea/sdkcompat/vcs/**",
     ]) + select_for_ide(
         android_studio = glob([
             "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/java/**",
         ]),
         clion = glob([
             "com/google/idea/sdkcompat/cidr/**",
             "com/google/idea/sdkcompat/clion/**",
+            "com/google/idea/sdkcompat/python/**",
         ]),
         intellij = glob([
             "com/google/idea/sdkcompat/dart/**",
+            "com/google/idea/sdkcompat/java/**",
+            "com/google/idea/sdkcompat/python/**",
         ]),
     ),
     visibility = ["//sdkcompat:__pkg__"],
     deps = [
         "//intellij_platform_sdk:plugin_api",
-        "//third_party/python",
+        "//intellij_platform_sdk:junit",
         "@jsr305_annotations//jar",
-    ],
+    ] + select_for_ide(
+        clion = ["//third_party/python"],
+        intellij = ["//third_party/python"],
+    ),
 )
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
new file mode 100644
index 0000000..b330ffd
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+import java.util.Map;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerMacrosAdapter extends OCCompilerMacros {
+  // v171
+  public void addAllFeatures(Map<String, String> result, Map<String, String> features) {
+    result.putAll(features);
+  }
+  // v172
+  public abstract String getAllDefines(OCLanguageKind kind, VirtualFile vf);
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
new file mode 100644
index 0000000..f4fc622
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerSettingsAdapter extends OCCompilerSettings {}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
index a6bf252..dc6a164 100644
--- a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -1,14 +1,19 @@
 package com.google.idea.sdkcompat.cidr;
 
+import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
 
 /** Adapter to bridge different SDK versions. */
-public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
+public abstract class OCResolveConfigurationAdapter extends UserDataHolderBase
+    implements OCResolveConfiguration {
   /* v162/v163 */
-  public VirtualFile getPrecompiledHeader();
+  public abstract VirtualFile getPrecompiledHeader();
 
   /* v162/v163 */
-  public OCLanguageKind getPrecompiledLanguageKind();
+  public abstract OCLanguageKind getPrecompiledLanguageKind();
+
+  /* v172 */
+  public abstract String getPreprocessorDefines(OCLanguageKind kind, VirtualFile virtualFile);
 }
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
index 1f38a39..651f529 100644
--- a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -10,12 +10,28 @@
     return OCWorkspaceModificationTrackers.getInstance(project);
   }
 
-  /** Must be called inside a write action, on the EDT. */
+  /**
+   * Causes symbol tables to be rebuilt and invalidates cidr caches attached to resolve
+   * configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
   public static void incrementModificationCounts(Project project) {
+    partialIncModificationCounts(project);
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+
+  /**
+   * Does not trigger symbol table rebuilding, and only clears part of the cidr caches attached to
+   * resolve configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
+  public static void partialIncModificationCounts(Project project) {
     OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
     modTrackers.getProjectFilesListTracker().incModificationCount();
     modTrackers.getSourceFilesListTracker().incModificationCount();
     modTrackers.getSelectedResolveConfigurationTracker().incModificationCount();
-    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
   }
 }
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
index d6cbc72..fad4ed5 100644
--- a/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
@@ -26,7 +26,9 @@
   public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
       throws IncorrectOperationException {
     List<TextRange> ranges = new ArrayList<>();
-    ranges.addAll(info.insertedRanges);
+    if (info.insertedRanges != null) {
+      ranges.addAll(info.insertedRanges);
+    }
     ranges.addAll(info.allChangedRanges);
     this.reformatTextWithContext(file, ranges);
   }
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java b/sdkcompat/v171/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
new file mode 100644
index 0000000..c20e8bf
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
@@ -0,0 +1,24 @@
+package com.google.idea.sdkcompat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+
+/** List of java-related configuration producers for a given plugin version. */
+public class JavaConfigurationProducerList {
+
+  /**
+   * Returns a list of run configuration producers to suppress for Blaze projects.
+   *
+   * <p>These classes must all be accessible from the Blaze plugin's classpath (e.g. they shouldn't
+   * belong to any plugins not listed as dependencies of the Blaze plugin).
+   */
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
+              com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
+              com.intellij.execution.junit.TestClassConfigurationProducer.class,
+              com.intellij.execution.junit.TestMethodConfigurationProducer.class,
+              com.intellij.execution.junit.PatternConfigurationProducer.class,
+              com.intellij.execution.application.ApplicationConfigurationProducer.class);
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java b/sdkcompat/v171/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
new file mode 100644
index 0000000..483f0e7
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
@@ -0,0 +1,15 @@
+package com.google.idea.sdkcompat.profile;
+
+import com.intellij.codeInspection.ex.InspectionProfileImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
+
+/** Utility to bridge different SDK versions. */
+public class InspectionProfileUtil {
+
+  public static void disableTool(String toolName, Project project) {
+    InspectionProfileImpl profile =
+        InspectionProjectProfileManager.getInstance(project).getCurrentProfile();
+    profile.setToolEnabled(toolName, false, project);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
index e65ad6e..dd66800 100644
--- a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -1,12 +1,11 @@
 package com.google.idea.sdkcompat.python;
 
 import com.google.common.collect.ImmutableList;
-import java.util.Collection;
 
 /** List of python configuration producers for a given plugin version. */
 public class PyConfigurationProducersList {
 
-  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
+  public static final ImmutableList<Class<?>> PRODUCERS_TO_SUPPRESS =
       ImmutableList.of(
           com.jetbrains.python.run.PythonRunConfigurationProducer.class,
           com.jetbrains.python.testing.universalTests.PyUniversalTestsConfigurationProducer.class,
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PythonFacetUtil.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PythonFacetUtil.java
new file mode 100644
index 0000000..d5a5206
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PythonFacetUtil.java
@@ -0,0 +1,31 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.facet.FacetTypeId;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.jetbrains.python.facet.LibraryContributingFacet;
+import com.jetbrains.python.facet.PythonFacet;
+import com.jetbrains.python.facet.PythonFacetType;
+import javax.annotation.Nullable;
+
+/**
+ * This class is a hack to get around Python SDK incompatibilities. Provides a consistent API for
+ * both IntelliJ and CLion.
+ */
+public class PythonFacetUtil {
+  public static FacetTypeId<PythonFacet> getFacetId() {
+    return PythonFacet.ID;
+  }
+
+  public static PythonFacetType getTypeInstance() {
+    return PythonFacetType.getInstance();
+  }
+
+  @Nullable
+  public static Sdk getSdk(LibraryContributingFacet<?> facet) {
+    if (!(facet instanceof PythonFacet)) {
+      return null;
+    }
+    PythonFacet pythonFacet = (PythonFacet) facet;
+    return pythonFacet.getConfiguration().getSdk();
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
new file mode 100644
index 0000000..09c1c12
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
@@ -0,0 +1,14 @@
+package com.google.idea.sdkcompat.run;
+
+import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
+import com.intellij.openapi.util.InvalidDataException;
+import org.jdom.Element;
+
+/** SDK compatibility bridge for {@link RunnerAndConfigurationSettingsImpl}. */
+public class RunnerAndConfigurationSettingsCompatUtils {
+
+  public static void readConfiguration(RunnerAndConfigurationSettingsImpl settings, Element element)
+      throws InvalidDataException {
+    settings.readExternal(element);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java
index 8862aa8..3ebee37 100644
--- a/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -1,6 +1,7 @@
 package com.google.idea.sdkcompat.transactions;
 
 import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.TransactionGuard;
 
 /** SDK adapter to use transaction guards. */
@@ -12,4 +13,10 @@
   public static void submitTransaction(Disposable disposable, Runnable runnable) {
     TransactionGuard.submitTransaction(disposable, runnable);
   }
+
+  /** Runs {@link Runnable} as a write action, inside a transaction. */
+  public static void submitWriteActionTransactionAndWait(Runnable runnable) {
+    submitTransactionAndWait(
+        (Runnable) () -> ApplicationManager.getApplication().runWriteAction(runnable));
+  }
 }
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
new file mode 100644
index 0000000..1b41d6d
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
@@ -0,0 +1,50 @@
+package com.google.idea.sdkcompat.ui;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.PsiElement;
+import com.intellij.xml.breadcrumbs.BreadcrumbsInfoProvider;
+import java.util.Arrays;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for {@link BreadcrumbsInfoProvider}, deprecated in 172. */
+public abstract class BreadcrumbsProviderSdkCompatAdapter extends BreadcrumbsInfoProvider {
+
+  public static BreadcrumbsProviderSdkCompatAdapter[] getBreadcrumbsProviders() {
+    return Arrays.stream(BreadcrumbsInfoProvider.EP_NAME.getExtensions())
+        .map(BreadcrumbsProviderSdkCompatAdapter::fromBreadcrumbsProvider)
+        .toArray(BreadcrumbsProviderSdkCompatAdapter[]::new);
+  }
+
+  private static BreadcrumbsProviderSdkCompatAdapter fromBreadcrumbsProvider(
+      BreadcrumbsInfoProvider delegate) {
+    return new BreadcrumbsProviderSdkCompatAdapter() {
+
+      @Override
+      public Language[] getLanguages() {
+        return delegate.getLanguages();
+      }
+
+      @Override
+      public boolean acceptElement(PsiElement psiElement) {
+        return delegate.acceptElement(psiElement);
+      }
+
+      @Override
+      public String getElementInfo(PsiElement psiElement) {
+        return delegate.getElementInfo(psiElement);
+      }
+
+      @Nullable
+      @Override
+      public String getElementTooltip(PsiElement element) {
+        return delegate.getElementTooltip(element);
+      }
+
+      @Nullable
+      @Override
+      public PsiElement getParent(PsiElement element) {
+        return delegate.getParent(element);
+      }
+    };
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
new file mode 100644
index 0000000..c962097
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
@@ -0,0 +1,17 @@
+package com.google.idea.sdkcompat.ui;
+
+import java.awt.Component;
+
+/** Works around b/64290580 which affects certain SDK versions. */
+public final class RequestFocusCompatUtils {
+  private RequestFocusCompatUtils() {}
+
+  /**
+   * Focuses the given component.
+   *
+   * @see Component#requestFocus()
+   */
+  public static void requestFocus(Component component) {
+    component.requestFocus();
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
new file mode 100644
index 0000000..0aebb39
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
@@ -0,0 +1,32 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.openapi.vcs.changes.ChangeListManagerGate;
+
+/**
+ * Works around b/63888111 / <a href="https://youtrack.jetbrains.com/issue/IJSDK-284">IJSDK-284</a>,
+ * which breaks support for {@link ChangeListManagerGate#editName(String, String)} in some SDK
+ * versions. <br>
+ * <br>
+ * The methods in this class call the corresponding methods on the {@link ChangeListManagerGate}
+ * instead of the {@link ChangeListManager}. It is normal to use this during a VCS update.
+ */
+public final class ChangeListManagerGateCompatUtils {
+  private ChangeListManagerGateCompatUtils() {}
+
+  public static void editName(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String oldName,
+      String newName) {
+    addGate.editName(oldName, newName);
+  }
+
+  public static void editComment(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String name,
+      String newComment) {
+    addGate.editComment(name, newComment);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
new file mode 100644
index 0000000..88834c7
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.VcsConfiguration;
+import com.intellij.spellchecker.ui.SpellCheckingEditorCustomization;
+import com.intellij.ui.EditorCustomization;
+import com.intellij.ui.RightMarginEditorCustomization;
+import java.util.List;
+
+/** Handles VCS changelist editor customizations that differ between SDK versions. */
+public class VcsEditorConfigurationCompatUtils {
+
+  public static List<EditorCustomization> getVcsConfigurationCustomizations(
+      Project project, VcsConfiguration config) {
+    return ImmutableList.of(
+        SpellCheckingEditorCustomization.getInstance(config.CHECK_COMMIT_MESSAGE_SPELLING),
+        new RightMarginEditorCustomization(
+            config.USE_COMMIT_MESSAGE_MARGIN, config.COMMIT_MESSAGE_MARGIN_SIZE));
+  }
+}
diff --git a/sdkcompat/v163/BUILD b/sdkcompat/v172/BUILD
similarity index 63%
rename from sdkcompat/v163/BUILD
rename to sdkcompat/v172/BUILD
index 8e2c272..8d2ee7a 100644
--- a/sdkcompat/v163/BUILD
+++ b/sdkcompat/v172/BUILD
@@ -5,30 +5,40 @@
 load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
 
 java_library(
-    name = "v163",
+    name = "v172",
     srcs = glob([
         "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/profile/**",
         "com/google/idea/sdkcompat/smrunner/**",
         "com/google/idea/sdkcompat/transactions/**",
         "com/google/idea/sdkcompat/vcs/**",
+        "com/google/idea/sdkcompat/ui/**",
+        "com/google/idea/sdkcompat/run/**",
     ]) + select_for_ide(
         android_studio = glob([
             "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/java/**",
         ]),
         clion = glob([
-            "com/google/idea/sdkcompat/cidr/**",
             "com/google/idea/sdkcompat/clion/**",
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/python/*",
+            "clion/com/google/idea/sdkcompat/python/*",
         ]),
         intellij = glob([
-            "com/google/idea/sdkcompat/python/**",
             "com/google/idea/sdkcompat/dart/**",
+            "com/google/idea/sdkcompat/java/**",
+            "com/google/idea/sdkcompat/python/*",
+            "intellij/com/google/idea/sdkcompat/python/*",
         ]),
     ),
     visibility = ["//sdkcompat:__pkg__"],
     deps = [
         "//intellij_platform_sdk:plugin_api",
+        "//intellij_platform_sdk:junit",
         "@jsr305_annotations//jar",
     ] + select_for_ide(
+        clion = ["//third_party/python"],
         intellij = ["//third_party/python"],
     ),
 )
diff --git a/sdkcompat/v172/clion/com/google/idea/sdkcompat/python/PythonFacetUtil.java b/sdkcompat/v172/clion/com/google/idea/sdkcompat/python/PythonFacetUtil.java
new file mode 100644
index 0000000..e54822c
--- /dev/null
+++ b/sdkcompat/v172/clion/com/google/idea/sdkcompat/python/PythonFacetUtil.java
@@ -0,0 +1,31 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.facet.FacetTypeId;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.jetbrains.python.facet.LibraryContributingFacet;
+import com.jetbrains.python.minor.facet.PythonFacet;
+import com.jetbrains.python.minor.facet.PythonFacetType;
+import javax.annotation.Nullable;
+
+/**
+ * This class is a hack to get around Python SDK incompatibilities. Provides a consistent API for
+ * both IntelliJ and CLion.
+ */
+public class PythonFacetUtil {
+  public static FacetTypeId<PythonFacet> getFacetId() {
+    return PythonFacet.ID;
+  }
+
+  public static PythonFacetType getTypeInstance() {
+    return PythonFacetType.getInstance();
+  }
+
+  @Nullable
+  public static Sdk getSdk(LibraryContributingFacet<?> facet) {
+    if (!(facet instanceof PythonFacet)) {
+      return null;
+    }
+    PythonFacet pythonFacet = (PythonFacet) facet;
+    return pythonFacet.getConfiguration().getSdk();
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
similarity index 69%
rename from sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
index d60a1a6..c20ad1f 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
@@ -1,5 +1,6 @@
 package com.google.idea.sdkcompat.cidr;
 
+import com.intellij.openapi.util.text.StringUtil;
 import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
 import java.util.List;
 
@@ -7,14 +8,14 @@
 public class CidrCompilerSwitchesAdapter {
   /** Old interface does not know anything about CidrCompilerSwitches.Format */
   public static List<String> getFileArgs(CidrCompilerSwitches switches) {
-    return switches.getFileArgs();
+    return switches.getList(CidrCompilerSwitches.Format.RAW);
   }
 
   public static List<String> getCommandLineArgs(CidrCompilerSwitches switches) {
-    return switches.getCommandLineArgs();
+    return switches.getList(CidrCompilerSwitches.Format.BASH_SHELL);
   }
 
   public static String getCommandLineString(CidrCompilerSwitches switches) {
-    return switches.getCommandLineString();
+    return StringUtil.join(getCommandLineArgs(switches), " ");
   }
 }
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
similarity index 100%
rename from sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
new file mode 100644
index 0000000..efad3f4
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.execution.testing.google.CidrGoogleTestUtil;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrGoogleTestUtilAdapter extends CidrGoogleTestUtil {}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
new file mode 100644
index 0000000..40684be
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrSwitchBuilderAdapter extends CidrSwitchBuilder {}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
new file mode 100644
index 0000000..fdccd53
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerMacrosAdapter.java
@@ -0,0 +1,36 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.preprocessor.OCCompilerMacros;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContext;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerFeatures;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerMacrosAdapter extends OCCompilerMacros {
+
+  // v171
+  protected abstract void fillFileMacros(
+      @NotNull OCInclusionContext context, @NotNull PsiFile sourceFile);
+
+  // v171
+  protected void addAllFeatures(
+      Map<String, String> collection, Map<OCCompilerFeatures.Type<?>, ?> features) {}
+
+  // v171
+  public static void fillSubstitutions(OCInclusionContext context, String text) {}
+
+  // v171
+  public void enableClangFeatures(
+      @NotNull OCInclusionContext context, @NotNull Map<String, String> features) {}
+
+  // v171
+  public void enableClangExtensions(
+      @NotNull OCInclusionContext context, @NotNull Map<String, String> extensions) {}
+
+  // v172
+  public abstract String getAllDefines(OCLanguageKind kind, VirtualFile vf);
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
new file mode 100644
index 0000000..8ac7845
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCCompilerSettingsAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.toolchains.OCCompilerSettingsBackedByCompilerCache;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCCompilerSettingsAdapter extends OCCompilerSettingsBackedByCompilerCache {}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
new file mode 100644
index 0000000..3cb3714
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -0,0 +1,45 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.util.UserDataHolderBase;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.preprocessor.OCCompilerMacros;
+import com.jetbrains.cidr.lang.workspace.OCIncludeMap;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerFeatures.Type;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import com.jetbrains.cidr.toolchains.OCCompilerSettingsBackedByCompilerCache;
+import java.util.Collections;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCResolveConfigurationAdapter extends UserDataHolderBase
+    implements OCResolveConfiguration {
+  /* v162/v163 */
+  public abstract VirtualFile getPrecompiledHeader();
+
+  /* v162/v163 */
+  public abstract OCLanguageKind getPrecompiledLanguageKind();
+
+  /* v171 */
+  public abstract OCCompilerMacros getCompilerMacros();
+
+  @Override
+  public Map<Type<?>, ?> getCompilerFeatures(
+      OCLanguageKind kind, @Nullable VirtualFile virtualFile) {
+    OCCompilerSettings compilerSettings = getCompilerSettings();
+    if (!(compilerSettings instanceof OCCompilerSettingsBackedByCompilerCache)) {
+      return Collections.emptyMap();
+    }
+
+    OCCompilerSettingsBackedByCompilerCache backedCompilerSettings =
+        (OCCompilerSettingsBackedByCompilerCache) compilerSettings;
+    return backedCompilerSettings.getCompilerFeatures(kind, virtualFile);
+  }
+
+  @Override
+  public OCIncludeMap getIncludeMap() {
+    return OCIncludeMap.EMPTY;
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
similarity index 100%
rename from sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
similarity index 60%
rename from sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
index 1f38a39..651f529 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -10,12 +10,28 @@
     return OCWorkspaceModificationTrackers.getInstance(project);
   }
 
-  /** Must be called inside a write action, on the EDT. */
+  /**
+   * Causes symbol tables to be rebuilt and invalidates cidr caches attached to resolve
+   * configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
   public static void incrementModificationCounts(Project project) {
+    partialIncModificationCounts(project);
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+
+  /**
+   * Does not trigger symbol table rebuilding, and only clears part of the cidr caches attached to
+   * resolve configurations.
+   *
+   * <p>Must be called inside a write action, on the EDT.
+   */
+  public static void partialIncModificationCounts(Project project) {
     OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
     modTrackers.getProjectFilesListTracker().incModificationCount();
     modTrackers.getSourceFilesListTracker().incModificationCount();
     modTrackers.getSelectedResolveConfigurationTracker().incModificationCount();
-    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
   }
 }
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeActionList.java
similarity index 71%
rename from sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeActionList.java
index 55f4936..1230fd0 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -3,7 +3,6 @@
 import com.google.common.collect.ImmutableSet;
 import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
 import com.jetbrains.cidr.cpp.cmake.actions.ClearCMakeCacheAndReloadAction;
-import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
 import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
 import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
 
@@ -14,9 +13,11 @@
       ImmutableSet.of(
           ChangeCMakeProjectContentRootAction.ID,
           ClearCMakeCacheAndReloadAction.ID,
-          OpenCMakeSettingsAction.ID,
+          // 'CMake' -> 'CMake Settings' action: com.cidr.cpp.cmake.actions.OpenCMakeSettingsAction
+          "CMake.OpenCMakeSettings",
           ReloadCMakeProjectAction.ID,
           ToggleCMakeAutoReloadAction.ID,
-          // 'CMake' > 'Show Generated CMake Files' action
-          "CMake.ShowGeneratedDir");
+          // 'CMake' > 'Show Generated CMake Files' action:
+          //   com.cidr.cpp.cmake.actions.ShowCMakeGeneratedDirAction
+          "CMake.ShowCMakeGeneratedDir");
 }
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java b/sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
new file mode 100644
index 0000000..abc58af
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.jetbrains.cidr.cpp.execution.testing.google.CMakeGoogleTestRunConfigurationProducer;
+import com.jetbrains.cidr.cpp.execution.testing.tcatch.CMakeCatchTestRunConfigurationProducer;
+
+/** List of C/C++ configuration producers for a given plugin version. */
+public class CMakeConfigurationProducersList {
+
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              CMakeGoogleTestRunConfigurationProducer.class,
+              CMakeCatchTestRunConfigurationProducer.class);
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..0509343
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,65 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.formatting.FormattingMode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.ChangedRangesInfo;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster;
+import com.intellij.util.IncorrectOperationException;
+import java.util.ArrayList;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class DelegatingCodeStyleManagerSdkCompatAdapter extends CodeStyleManager
+    implements FormattingModeAwareIndentAdjuster {
+
+  protected CodeStyleManager delegate;
+
+  protected DelegatingCodeStyleManagerSdkCompatAdapter(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
+      throws IncorrectOperationException {
+    List<TextRange> ranges = new ArrayList<>();
+    if (info.insertedRanges != null) {
+      ranges.addAll(info.insertedRanges);
+    }
+    ranges.addAll(info.allChangedRanges);
+    this.reformatTextWithContext(file, ranges);
+  }
+
+  @Override
+  public int getSpacing(@NotNull PsiFile file, int offset) {
+    return delegate.getSpacing(file, offset);
+  }
+
+  @Override
+  public int getMinLineFeeds(@NotNull PsiFile file, int offset) {
+    return delegate.getMinLineFeeds(file, offset);
+  }
+
+  /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */
+  @Override
+  public FormattingMode getCurrentFormattingMode() {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode();
+    }
+    return FormattingMode.REFORMAT;
+  }
+
+  @Override
+  public int adjustLineIndent(
+      @NotNull final Document document, final int offset, FormattingMode mode)
+      throws IncorrectOperationException {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate)
+          .adjustLineIndent(document, offset, mode);
+    }
+    return offset;
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
similarity index 85%
rename from sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
index 58f48e4..c2d1a3e 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
@@ -16,7 +16,7 @@
 package com.google.idea.sdkcompat.dart;
 
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
 import com.intellij.openapi.roots.libraries.Library;
 import javax.annotation.Nullable;
 
@@ -27,6 +27,6 @@
 
   @Nullable
   public static Library findDartLibrary(Project project) {
-    return ApplicationLibraryTable.getApplicationTable().getLibraryByName(DART_SDK_LIBRARY_NAME);
+    return ProjectLibraryTable.getInstance(project).getLibraryByName(DART_SDK_LIBRARY_NAME);
   }
 }
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java b/sdkcompat/v172/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
new file mode 100644
index 0000000..977c744
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/java/JavaConfigurationProducerList.java
@@ -0,0 +1,25 @@
+package com.google.idea.sdkcompat.java;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+
+/** List of java-related configuration producers for a given plugin version. */
+public class JavaConfigurationProducerList {
+
+  /**
+   * Returns a list of run configuration producers to suppress for Blaze projects.
+   *
+   * <p>These classes must all be accessible from the Blaze plugin's classpath (e.g. they shouldn't
+   * belong to any plugins not listed as dependencies of the Blaze plugin).
+   */
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
+              com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
+              com.intellij.execution.junit.TestInClassConfigurationProducer.class,
+              com.intellij.execution.junit.TestClassConfigurationProducer.class,
+              com.intellij.execution.junit.TestMethodConfigurationProducer.class,
+              com.intellij.execution.junit.PatternConfigurationProducer.class,
+              com.intellij.execution.application.ApplicationConfigurationProducer.class);
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java b/sdkcompat/v172/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
new file mode 100644
index 0000000..483f0e7
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/profile/InspectionProfileUtil.java
@@ -0,0 +1,15 @@
+package com.google.idea.sdkcompat.profile;
+
+import com.intellij.codeInspection.ex.InspectionProfileImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
+
+/** Utility to bridge different SDK versions. */
+public class InspectionProfileUtil {
+
+  public static void disableTool(String toolName, Project project) {
+    InspectionProfileImpl profile =
+        InspectionProjectProfileManager.getInstance(project).getCurrentProfile();
+    profile.setToolEnabled(toolName, false, project);
+  }
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
new file mode 100644
index 0000000..1a85deb
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -0,0 +1,18 @@
+package com.google.idea.sdkcompat.python;
+
+import com.google.common.collect.ImmutableList;
+
+/** List of python configuration producers for a given plugin version. */
+public class PyConfigurationProducersList {
+
+  public static final ImmutableList<Class<?>> PRODUCERS_TO_SUPPRESS =
+      ImmutableList.of(
+          com.jetbrains.python.run.PythonRunConfigurationProducer.class,
+          com.jetbrains.python.testing.PyTestsConfigurationProducer.class,
+          com.jetbrains.python.testing.PythonTestLegacyConfigurationProducer.class,
+          com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer.class,
+          com.jetbrains.python.testing.nosetestLegacy.PythonNoseTestConfigurationProducer.class,
+          com.jetbrains.python.testing.pytestLegacy.PyTestConfigurationProducer.class,
+          com.jetbrains.python.testing.tox.PyToxConfigurationProducer.class,
+          com.jetbrains.python.testing.unittestLegacy.PythonUnitTestConfigurationProducer.class);
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
similarity index 80%
rename from sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
index 9ccba03..7edba6c 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
@@ -3,7 +3,7 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.util.QualifiedName;
 import com.jetbrains.python.psi.impl.PyImportResolver;
-import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
+import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext;
 import javax.annotation.Nullable;
 
 /** Adapter to bridge different SDK versions. */
@@ -16,7 +16,7 @@
   @Override
   @Nullable
   default PsiElement resolveImportReference(
-      QualifiedName name, QualifiedNameResolveContext context, boolean withRoots) {
+      QualifiedName name, PyQualifiedNameResolveContext context, boolean withRoots) {
     return resolveImportReference(
         name, new PyQualifiedNameResolveContextAdapter(context), withRoots);
   }
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
new file mode 100644
index 0000000..7311580
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
@@ -0,0 +1,152 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public class PyQualifiedNameResolveContextAdapter implements PyQualifiedNameResolveContext {
+
+  private final PyQualifiedNameResolveContext delegate;
+
+  PyQualifiedNameResolveContextAdapter(PyQualifiedNameResolveContext delegate) {
+    this.delegate = delegate;
+  }
+
+  @Nullable
+  @Override
+  public PsiElement getFoothold() {
+    return delegate.getFoothold();
+  }
+
+  @Override
+  public int getRelativeLevel() {
+    return delegate.getRelativeLevel();
+  }
+
+  @Nullable
+  @Override
+  public Sdk getSdk() {
+    return delegate.getSdk();
+  }
+
+  @Nullable
+  @Override
+  public Module getModule() {
+    return delegate.getModule();
+  }
+
+  @NotNull
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public boolean getWithoutRoots() {
+    return delegate.getWithoutRoots();
+  }
+
+  @Override
+  public boolean getWithoutForeign() {
+    return delegate.getWithoutForeign();
+  }
+
+  @Override
+  public boolean getWithoutStubs() {
+    return delegate.getWithoutStubs();
+  }
+
+  @NotNull
+  @Override
+  public PsiManager getPsiManager() {
+    return delegate.getPsiManager();
+  }
+
+  @Override
+  public boolean getWithMembers() {
+    return delegate.getWithMembers();
+  }
+
+  @Override
+  public boolean getWithPlainDirectories() {
+    return delegate.getWithPlainDirectories();
+  }
+
+  @Override
+  public boolean getVisitAllModules() {
+    return delegate.getVisitAllModules();
+  }
+
+  @Nullable
+  @Override
+  public Sdk getEffectiveSdk() {
+    return delegate.getEffectiveSdk();
+  }
+
+  @Override
+  public boolean isValid() {
+    return delegate.isValid();
+  }
+
+  @Nullable
+  @Override
+  public PsiFile getFootholdFile() {
+    return delegate.getFootholdFile();
+  }
+
+  @Nullable
+  @Override
+  public PsiDirectory getContainingDirectory() {
+    return delegate.getContainingDirectory();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutForeign() {
+    return delegate.copyWithoutForeign();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithMembers() {
+    return delegate.copyWithMembers();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithPlainDirectories() {
+    return delegate.copyWithPlainDirectories();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithRelative(int i) {
+    return delegate.copyWithRelative(i);
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutRoots() {
+    return delegate.copyWithoutRoots();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithRoots() {
+    return delegate.copyWithRoots();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutStubs() {
+    return delegate.copyWithoutStubs();
+  }
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
new file mode 100644
index 0000000..9f8b5c3
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.python;
+
+import com.jetbrains.python.psi.resolve.PyReferenceResolveProvider;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyReferenceResolveProviderAdapter extends PyReferenceResolveProvider {}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
similarity index 96%
rename from sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
index 5a6bef2..0329801 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
@@ -17,6 +17,6 @@
       boolean checkForPackage,
       boolean withoutStubs) {
     return ResolveImportUtil.resolveChild(
-        parent, referencedName, containingFile, fileOnly, checkForPackage);
+        parent, referencedName, containingFile, fileOnly, checkForPackage, withoutStubs);
   }
 }
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
new file mode 100644
index 0000000..df88400
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/run/RunnerAndConfigurationSettingsCompatUtils.java
@@ -0,0 +1,13 @@
+package com.google.idea.sdkcompat.run;
+
+import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
+import org.jdom.Element;
+
+/** SDK compatibility bridge for {@link RunnerAndConfigurationSettingsImpl}. */
+public class RunnerAndConfigurationSettingsCompatUtils {
+
+  public static void readConfiguration(
+      RunnerAndConfigurationSettingsImpl settings, Element element) {
+    settings.readExternal(element, false);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
similarity index 77%
rename from sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
index 1e9b474..8555ad8 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
@@ -8,6 +8,7 @@
 
   public static TestFailedEvent getTestFailedEvent(
       String name, @Nullable String message, @Nullable String content, long duration) {
-    return new TestFailedEvent(name, null, message, content, true, null, null, null, duration);
+    return new TestFailedEvent(
+        name, null, message, content, true, null, null, null, null, false, false, duration);
   }
 }
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v172/com/google/idea/sdkcompat/transactions/Transactions.java
similarity index 60%
rename from sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/transactions/Transactions.java
index 8862aa8..3ebee37 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -1,6 +1,7 @@
 package com.google.idea.sdkcompat.transactions;
 
 import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.TransactionGuard;
 
 /** SDK adapter to use transaction guards. */
@@ -12,4 +13,10 @@
   public static void submitTransaction(Disposable disposable, Runnable runnable) {
     TransactionGuard.submitTransaction(disposable, runnable);
   }
+
+  /** Runs {@link Runnable} as a write action, inside a transaction. */
+  public static void submitWriteActionTransactionAndWait(Runnable runnable) {
+    submitTransactionAndWait(
+        (Runnable) () -> ApplicationManager.getApplication().runWriteAction(runnable));
+  }
 }
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
new file mode 100644
index 0000000..6174578
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/ui/BreadcrumbsProviderSdkCompatAdapter.java
@@ -0,0 +1,63 @@
+package com.google.idea.sdkcompat.ui;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.PsiElement;
+import com.intellij.ui.breadcrumbs.BreadcrumbsProvider;
+import java.util.Arrays;
+import java.util.List;
+import javax.swing.Icon;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for {@link BreadcrumbsProvider}, added in 172. */
+public abstract class BreadcrumbsProviderSdkCompatAdapter implements BreadcrumbsProvider {
+
+  public static BreadcrumbsProviderSdkCompatAdapter[] getBreadcrumbsProviders() {
+    return Arrays.stream(BreadcrumbsProvider.EP_NAME.getExtensions())
+        .map(BreadcrumbsProviderSdkCompatAdapter::fromBreadcrumbsProvider)
+        .toArray(BreadcrumbsProviderSdkCompatAdapter[]::new);
+  }
+
+  private static BreadcrumbsProviderSdkCompatAdapter fromBreadcrumbsProvider(
+      BreadcrumbsProvider delegate) {
+    return new BreadcrumbsProviderSdkCompatAdapter() {
+
+      @Override
+      public Language[] getLanguages() {
+        return delegate.getLanguages();
+      }
+
+      @Override
+      public boolean acceptElement(PsiElement psiElement) {
+        return delegate.acceptElement(psiElement);
+      }
+
+      @Override
+      public String getElementInfo(PsiElement psiElement) {
+        return delegate.getElementInfo(psiElement);
+      }
+
+      @Nullable
+      @Override
+      public Icon getElementIcon(PsiElement element) {
+        return delegate.getElementIcon(element);
+      }
+
+      @Nullable
+      @Override
+      public String getElementTooltip(PsiElement element) {
+        return delegate.getElementTooltip(element);
+      }
+
+      @Nullable
+      @Override
+      public PsiElement getParent(PsiElement element) {
+        return delegate.getParent(element);
+      }
+
+      @Override
+      public List<PsiElement> getChildren(PsiElement element) {
+        return delegate.getChildren(element);
+      }
+    };
+  }
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
new file mode 100644
index 0000000..ff53498
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/ui/RequestFocusCompatUtils.java
@@ -0,0 +1,23 @@
+package com.google.idea.sdkcompat.ui;
+
+import com.intellij.ui.EditorTextField;
+import java.awt.Component;
+
+/** Works around b/64290580 which affects certain SDK versions. */
+public final class RequestFocusCompatUtils {
+  private RequestFocusCompatUtils() {}
+
+  /**
+   * Focuses the given component.
+   *
+   * @see Component#requestFocus()
+   */
+  public static void requestFocus(Component component) {
+    if (component instanceof EditorTextField && ((EditorTextField) component).getEditor() == null) {
+      // If the editor is null, requestFocus() will just indirectly call requestFocus(),
+      // until the stack overflows. Instead, just don't support focusing the editor.
+      return;
+    }
+    component.requestFocus();
+  }
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
new file mode 100644
index 0000000..a179908
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/ChangeListManagerGateCompatUtils.java
@@ -0,0 +1,42 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.openapi.vcs.changes.ChangeListManagerGate;
+
+/**
+ * Works around b/63888111 / <a href="https://youtrack.jetbrains.com/issue/IJSDK-284">IJSDK-284</a>,
+ * which breaks support for {@link ChangeListManagerGate#editName(String, String)} in some SDK
+ * versions. <br>
+ * <br>
+ * The methods in this class call the corresponding methods on the {@link ChangeListManager} instead
+ * of the {@link ChangeListManagerGate}. Normally in a VCS update, the latter should be used. Key
+ * known differences between the two are:
+ *
+ * <ul>
+ *   <li>During the update, the manager and gate have independent state, and thus modifications made
+ *       via one are not visible to the other.
+ *   <li>If the VCS update completes successfully, the gate's state becomes authoritative, and any
+ *       changes made via the manager during the update are applied on top of the gate's state.
+ *   <li>If the VCS update fails, the manager's state remains authoritative, and the gate's state
+ *       (and modifications) are discarded.
+ * </ul>
+ */
+public final class ChangeListManagerGateCompatUtils {
+  private ChangeListManagerGateCompatUtils() {}
+
+  public static void editName(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String oldName,
+      String newName) {
+    changeListManager.editName(oldName, newName);
+  }
+
+  public static void editComment(
+      ChangeListManagerGate addGate,
+      ChangeListManager changeListManager,
+      String name,
+      String newComment) {
+    changeListManager.editComment(name, newComment);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
similarity index 100%
rename from sdkcompat/v163/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
rename to sdkcompat/v172/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
new file mode 100644
index 0000000..49b4665
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
@@ -0,0 +1,78 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.history.VcsRevisionNumber;
+import com.intellij.openapi.vcs.merge.MergeData;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for creating {@link MergeData}. */
+// TODO(grl): Move to com.google.devtools.intellij.piper.resolve and make package-private
+// once versions less than v171 have been deleted. We may as well keep the builder around,
+// since it uses piper-relevant terminology and complies with Java style conventions.
+public final class MergeDataBuilder {
+  private byte[] baseContent;
+  private byte[] theirsContent;
+  private byte[] yoursContent;
+
+  @Nullable private VcsRevisionNumber baseRevisionNumber;
+  @Nullable private VcsRevisionNumber theirsRevisionNumber;
+  @Nullable private VcsRevisionNumber yoursRevisionNumber;
+
+  @Nullable private FilePath baseFilePath;
+  @Nullable private FilePath theirsFilePath;
+  @Nullable private FilePath yoursFilePath;
+
+  public void setBaseContent(byte[] baseContent) {
+    this.baseContent = baseContent;
+  }
+
+  public void setTheirsContent(byte[] theirsContent) {
+    this.theirsContent = theirsContent;
+  }
+
+  public void setYoursContent(byte[] yoursContent) {
+    this.yoursContent = yoursContent;
+  }
+
+  public void setBaseRevisionNumber(@Nullable VcsRevisionNumber baseRevisionNumber) {
+    this.baseRevisionNumber = baseRevisionNumber;
+  }
+
+  public void setTheirsRevisionNumber(@Nullable VcsRevisionNumber theirsRevisionNumber) {
+    this.theirsRevisionNumber = theirsRevisionNumber;
+  }
+
+  public void setYoursRevisionNumber(@Nullable VcsRevisionNumber yoursRevisionNumber) {
+    this.yoursRevisionNumber = yoursRevisionNumber;
+  }
+
+  public void setBaseFilePath(@Nullable FilePath baseFilePath) {
+    this.baseFilePath = baseFilePath;
+  }
+
+  public void setTheirsFilePath(@Nullable FilePath theirsFilePath) {
+    this.theirsFilePath = theirsFilePath;
+  }
+
+  public void setYoursFilePath(@Nullable FilePath yoursFilePath) {
+    this.yoursFilePath = yoursFilePath;
+  }
+
+  public MergeData build() {
+    MergeData mergeData = new MergeData();
+
+    mergeData.ORIGINAL = baseContent;
+    mergeData.ORIGINAL_REVISION_NUMBER = baseRevisionNumber;
+    mergeData.ORIGINAL_FILE_PATH = baseFilePath;
+
+    mergeData.LAST = theirsContent;
+    mergeData.LAST_REVISION_NUMBER = theirsRevisionNumber;
+    mergeData.LAST_FILE_PATH = theirsFilePath;
+
+    mergeData.CURRENT = yoursContent;
+    mergeData.CURRENT_REVISION_NUMBER = yoursRevisionNumber;
+    mergeData.CURRENT_FILE_PATH = yoursFilePath;
+
+    return mergeData;
+  }
+}
diff --git a/sdkcompat/v172/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
new file mode 100644
index 0000000..64ea32f
--- /dev/null
+++ b/sdkcompat/v172/com/google/idea/sdkcompat/vcs/VcsEditorConfigurationCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.VcsConfiguration;
+import com.intellij.ui.EditorCustomization;
+import com.intellij.ui.RightMarginEditorCustomization;
+import com.intellij.vcs.commit.CommitMessageInspectionProfile;
+import java.util.List;
+
+/** Handles VCS changelist editor customizations that differ between SDK versions. */
+public class VcsEditorConfigurationCompatUtils {
+
+  public static List<EditorCustomization> getVcsConfigurationCustomizations(
+      Project project, VcsConfiguration config) {
+    return ImmutableList.of(
+        new RightMarginEditorCustomization(
+            config.USE_COMMIT_MESSAGE_MARGIN,
+            CommitMessageInspectionProfile.getBodyRightMargin(project)));
+  }
+}
diff --git a/sdkcompat/v172/intellij/com/google/idea/sdkcompat/python/PythonFacetUtil.java b/sdkcompat/v172/intellij/com/google/idea/sdkcompat/python/PythonFacetUtil.java
new file mode 100644
index 0000000..d5a5206
--- /dev/null
+++ b/sdkcompat/v172/intellij/com/google/idea/sdkcompat/python/PythonFacetUtil.java
@@ -0,0 +1,31 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.facet.FacetTypeId;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.jetbrains.python.facet.LibraryContributingFacet;
+import com.jetbrains.python.facet.PythonFacet;
+import com.jetbrains.python.facet.PythonFacetType;
+import javax.annotation.Nullable;
+
+/**
+ * This class is a hack to get around Python SDK incompatibilities. Provides a consistent API for
+ * both IntelliJ and CLion.
+ */
+public class PythonFacetUtil {
+  public static FacetTypeId<PythonFacet> getFacetId() {
+    return PythonFacet.ID;
+  }
+
+  public static PythonFacetType getTypeInstance() {
+    return PythonFacetType.getInstance();
+  }
+
+  @Nullable
+  public static Sdk getSdk(LibraryContributingFacet<?> facet) {
+    if (!(facet instanceof PythonFacet)) {
+      return null;
+    }
+    PythonFacet pythonFacet = (PythonFacet) facet;
+    return pythonFacet.getConfiguration().getSdk();
+  }
+}
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java
index 6d6c083..3be405f 100644
--- a/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java
@@ -1,8 +1,8 @@
 package com.google.idea.testing.cidr;
 
 import com.google.common.collect.ImmutableList;
+import com.google.idea.sdkcompat.cidr.OCResolveConfigurationAdapter;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCFileTypeHelpers;
 import com.jetbrains.cidr.lang.OCLanguageKind;
@@ -13,10 +13,14 @@
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
 import com.jetbrains.cidr.lang.workspace.headerRoots.HeaderRoots;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
 import javax.annotation.Nullable;
 
 /** Stub {@link OCResolveConfiguration} for testing. */
-class StubOCResolveConfiguration extends UserDataHolderBase implements OCResolveConfiguration {
+class StubOCResolveConfiguration extends OCResolveConfigurationAdapter {
 
   private final Project project;
   private final HeaderRoots projectIncludeRoots;
@@ -40,10 +44,19 @@
     return "Stub resolve configuration";
   }
 
-  @Nullable
   @Override
-  public VirtualFile getPrecompiledHeader() {
-    return null;
+  public Set<VirtualFile> getPrecompiledHeaders() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile) {
+    return Collections.emptyList();
+  }
+
+  @Override
+  public Collection<VirtualFile> getSources() {
+    return Collections.emptyList();
   }
 
   @Nullable
@@ -67,11 +80,6 @@
   }
 
   @Override
-  public OCLanguageKind getPrecompiledLanguageKind() {
-    return getMaximumLanguageKind();
-  }
-
-  @Override
   public HeaderRoots getProjectHeadersRoots() {
     return projectIncludeRoots;
   }
@@ -101,4 +109,24 @@
   public int compareTo(OCResolveConfiguration o) {
     return OCWorkspaceUtil.compareConfigurations(this, o);
   }
+
+  /* v162/v163 */
+  @Nullable
+  @Override
+  public VirtualFile getPrecompiledHeader() {
+    return null;
+  }
+
+  /* v162/v163 */
+  @Override
+  public OCLanguageKind getPrecompiledLanguageKind() {
+    return getMaximumLanguageKind();
+  }
+
+  /* v172 */
+  @Nullable
+  @Override
+  public String getPreprocessorDefines(OCLanguageKind kind, VirtualFile virtualFile) {
+    return null;
+  }
 }
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java
index 05921b7..0a98a26 100644
--- a/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java
@@ -1,5 +1,7 @@
 package com.google.idea.testing.cidr;
 
+import com.google.idea.sdkcompat.cidr.OCWorkspaceModificationTrackersCompatUtils;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.lang.Language;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.fileTypes.PlainTextLanguage;
@@ -75,16 +77,14 @@
   }
 
   private static void rebuildSymbols(Project project, OCWorkspace workspace) {
-    ApplicationManager.getApplication()
-        .runReadAction(
-            () -> {
-              if (project.isDisposed()) {
-                return;
-              }
-              workspace
-                  .getModificationTrackers()
-                  .getBuildSettingsChangesTracker()
-                  .incModificationCount();
-            });
+    Transactions.submitTransaction(
+        project,
+        () ->
+            ApplicationManager.getApplication()
+                .runReadAction(
+                    () ->
+                        OCWorkspaceModificationTrackersCompatUtils.getTrackers(project)
+                            .getBuildSettingsChangesTracker()
+                            .incModificationCount()));
   }
 }
diff --git a/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java b/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
index f8d7f19..fe78fbd 100644
--- a/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
+++ b/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
@@ -111,8 +111,30 @@
       return null;
     }
     File jarFile = new File(platformJar).getAbsoluteFile();
-    File libDir = jarFile.getParentFile();
-    return libDir != null ? libDir.getParent() : null;
+    File jarDir = jarFile.getParentFile();
+    if (jarDir == null) {
+      return null;
+    }
+    if (jarDir.getName().equals("lib")) {
+      // Building against IDE distribution.
+      // root/ <- we want this
+      // |-lib/
+      // | `-openapi.jar (jarFile)
+      // `-plugins/
+      return jarDir.getParent();
+    } else if (jarDir.getName().equals("core-api")) {
+      // Building against source.
+      // tools/idea/ <- we want this
+      // |-platform/
+      // | `-core-api/
+      // |   `-libcore-api.jar (jarFile)
+      // `-plugins/
+      File platformDir = jarDir.getParentFile();
+      if (platformDir != null && platformDir.getName().equals("platform")) {
+        return platformDir.getParent();
+      }
+    }
+    return null;
   }
 
   private static void addArchiveFile(URL url, List<String> files) {
diff --git a/testing/src/com/google/idea/testing/DisablePluginsTestRule.java b/testing/src/com/google/idea/testing/DisablePluginsTestRule.java
new file mode 100644
index 0000000..5399482
--- /dev/null
+++ b/testing/src/com/google/idea/testing/DisablePluginsTestRule.java
@@ -0,0 +1,56 @@
+package com.google.idea.testing;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
+import java.lang.reflect.Field;
+import java.util.List;
+import org.junit.rules.ExternalResource;
+
+/**
+ * Test rule to disable a specified list of plugins during a test class.
+ *
+ * <p>Disabled plugins only take effect when initializing the {@link Application}, which can only
+ * happen once per integration test target.
+ *
+ * <p>As such, all users of this test rule must be in their own `java_test` target.
+ */
+public class DisablePluginsTestRule extends ExternalResource {
+
+  private final ImmutableList<String> disabledPluginIds;
+
+  public DisablePluginsTestRule(ImmutableList<String> disabledPluginIds) {
+    this.disabledPluginIds = Preconditions.checkNotNull(disabledPluginIds);
+  }
+
+  @Override
+  protected void before() throws Throwable {
+    if (ApplicationManager.getApplication() != null) {
+      // We may be able to relax this constraint if we check that the desired
+      // disabledPluginIds matches the existing value of ourDisabledPlugins.
+      throw new RuntimeException("Cannot disable plugins; they've already been loaded.");
+    }
+    forceSetDisabledPluginsField(disabledPluginIds);
+  }
+
+  @Override
+  protected void after() {
+    // no point resetting the list of disabled plugins to its prior value -- subsequent tests can't
+    // reinitialize the {@link Application} anyway.
+  }
+
+  /**
+   * Access the 'ourDisabledPlugins' field in {@link PluginManagerCore} via reflection, and set it.
+   * We can't simply populate a 'disabled_plugins.txt' file (the normal mechanism for disabling
+   * plugins), because that is ignored during tests.
+   */
+  private static void forceSetDisabledPluginsField(List<String> disabledPlugins)
+      throws NoSuchFieldException, IllegalAccessException {
+    Field ourDisabledPlugins = PluginManagerCore.class.getDeclaredField("ourDisabledPlugins");
+    ourDisabledPlugins.setAccessible(true);
+    ourDisabledPlugins.set(null, disabledPlugins);
+    ourDisabledPlugins.setAccessible(false);
+  }
+}
diff --git a/testing/src/com/google/idea/testing/VerifyRequiredPluginsEnabled.java b/testing/src/com/google/idea/testing/VerifyRequiredPluginsEnabled.java
new file mode 100644
index 0000000..e26c16a
--- /dev/null
+++ b/testing/src/com/google/idea/testing/VerifyRequiredPluginsEnabled.java
@@ -0,0 +1,50 @@
+/*
+ * 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.testing;
+
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.extensions.PluginId;
+
+/** Check that plugins flagged by the test runner as required are actually loaded. */
+public class VerifyRequiredPluginsEnabled {
+
+  /**
+   * Checks that the specified plugins are installed and enabled. Throws a {@link RuntimeException}
+   * if any are missing.
+   */
+  public static void runCheck(String[] requiredPlugins) {
+    for (String pluginId : requiredPlugins) {
+      if (pluginEnabled(pluginId)) {
+        continue;
+      }
+      String msg =
+          String.format(
+              "Required plugin '%s' is not %s",
+              pluginId, pluginInstalled(pluginId) ? "enabled" : "available");
+      throw new RuntimeException(msg);
+    }
+  }
+
+  private static boolean pluginEnabled(String pluginId) {
+    IdeaPluginDescriptor descriptor = PluginManager.getPlugin(PluginId.getId(pluginId));
+    return descriptor != null && descriptor.isEnabled();
+  }
+
+  private static boolean pluginInstalled(String pluginId) {
+    return PluginManager.getPlugin(PluginId.getId(pluginId)) != null;
+  }
+}
diff --git a/testing/test_defs.bzl b/testing/test_defs.bzl
index 8ee1992..53b107e 100644
--- a/testing/test_defs.bzl
+++ b/testing/test_defs.bzl
@@ -92,7 +92,6 @@
     test_package_root,
     deps,
     size="medium",
-    shard_count=None,
     jvm_flags = [],
     runtime_deps = [],
     platform_prefix="Idea",
@@ -110,7 +109,6 @@
     test_package_root: only tests under this package root will be run.
     deps: the required deps.
     size: the test size.
-    shard_count: the number of shards to use.
     jvm_flags: extra flags to be passed to the test vm.
     runtime_deps: the required runtime_deps.
     platform_prefix: Specifies the JetBrains product these tests are run against. Examples are
@@ -160,7 +158,6 @@
       size = size,
       srcs = srcs + [suite_class_name],
       data = data,
-      shard_count = shard_count,
       jvm_flags = jvm_flags,
       test_class = suite_class,
       runtime_deps = runtime_deps,
@@ -181,7 +178,7 @@
 
 def _get_test_srcs(targets):
   """Returns all files of the given targets that end with Test.java."""
-  files = set()
+  files = depset()
   for target in targets:
     files += target.files
   return [f for f in files if f.basename.endswith("Test.java")]
diff --git a/third_party/BUILD b/third_party/BUILD
index 06e9b19..1d79030 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -6,16 +6,6 @@
     jars = ["jdk8/jpda-jdi.jar"],
 )
 
-sh_binary(
-    name = "zip",
-    srcs = ["zip-wrap/zip.sh"],
-)
-
-sh_binary(
-    name = "unzip",
-    srcs = ["zip-wrap/unzip.sh"],
-)
-
 java_library(
     name = "python",
     exports = ["//third_party/python"],
diff --git a/third_party/auto_value/BUILD b/third_party/auto_value/BUILD
new file mode 100644
index 0000000..bbf4bb0
--- /dev/null
+++ b/third_party/auto_value/BUILD
@@ -0,0 +1,16 @@
+licenses(["notice"])
+
+java_plugin(
+    name = "autovalue-plugin",
+    generates_api = 1,
+    processor_class = "com.google.auto.value.processor.AutoValueProcessor",
+    deps = ["@auto_value//jar"],
+)
+
+# provides both the jar for compilation and the java_plugin.
+java_library(
+    name = "auto_value",
+    exported_plugins = [":autovalue-plugin"],
+    visibility = ["//visibility:public"],
+    exports = ["@auto_value//jar"],
+)
diff --git a/third_party/auto_value/LICENSE b/third_party/auto_value/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/auto_value/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/third_party/go/BUILD b/third_party/go/BUILD
new file mode 100644
index 0000000..9ed479c
--- /dev/null
+++ b/third_party/go/BUILD
@@ -0,0 +1,30 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+
+java_library(
+    name = "go_internal",
+    visibility = ["//visibility:private"],
+    exports = select_for_plugin_api({
+        "intellij-2017.2.2": ["@go_2017_2//:go"],
+        "intellij-2017.1.5": ["@go_2017_1//:go"],
+        "intellij-ue-2017.2.2": ["@go_2017_2//:go"],
+        "intellij-ue-2017.1.5": ["@go_2017_1//:go"],
+        "clion-2017.2.1": ["@go_2017_2//:go"],
+        "clion-2017.1.1": ["@go_2017_1//:go"],
+    }),
+)
+
+java_library(
+    name = "go_for_tests",
+    testonly = 1,
+    exports = [":go_internal"],
+)
+
+java_library(
+    name = "go",
+    neverlink = 1,
+    exports = [":go_internal"],
+)
diff --git a/third_party/python/BUILD b/third_party/python/BUILD
index 2f69d16..7863e0c 100644
--- a/third_party/python/BUILD
+++ b/third_party/python/BUILD
@@ -8,9 +8,12 @@
     name = "python_internal",
     visibility = ["//visibility:private"],
     exports = select_for_plugin_api({
-        "intellij-2016.3.1": ["@python_2016_3//:python"],
-        "intellij-2017.1.1": ["@python_2017_1//:python"],
+        "intellij-2017.1.5": ["@python_2017_1//:python"],
+        "intellij-2017.2.2": ["@python_2017_2//:python"],
+        "intellij-ue-2017.1.5": ["@python_2017_1//:python"],
+        "intellij-ue-2017.2.2": ["@python_2017_2//:python"],
         "clion-2017.1.1": ["@clion_2017_1_1//:python"],
+        "clion-2017.2.1": ["@clion_2017_2_1//:python"],
     }),
 )
 
diff --git a/third_party/scala/BUILD b/third_party/scala/BUILD
index 9786523..b23c3ab 100644
--- a/third_party/scala/BUILD
+++ b/third_party/scala/BUILD
@@ -8,7 +8,8 @@
     name = "scala_internal",
     visibility = ["//visibility:private"],
     exports = select_for_plugin_api({
-        "intellij-2017.1.1": ["@scala_2017_1//:scala"],
+        "intellij-2017.1.5": ["@scala_2017_1//:scala"],
+        "intellij-2017.2.2": ["@scala_2017_2//:scala"],
     }),
 )
 
diff --git a/third_party/zip-wrap/unzip.sh b/third_party/zip-wrap/unzip.sh
deleted file mode 100755
index f1be34d..0000000
--- a/third_party/zip-wrap/unzip.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2015 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-unzip "$@"
diff --git a/third_party/zip-wrap/zip.sh b/third_party/zip-wrap/zip.sh
deleted file mode 100755
index daed8f6..0000000
--- a/third_party/zip-wrap/zip.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright 2015 The Bazel Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-zip "$@"
diff --git a/version.bzl b/version.bzl
index 667e07a..d3dddd7 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,7 @@
 """Version of the blaze plugin."""
 
-VERSION = "2017.05.17.1"
+# This version will be overwritten in our rapid builds to the actual version number. We set the
+# default version to 9999 so that a dev plugin built from Piper HEAD will override any production
+# plugin (because IntelliJ will choose the highest version when it sees two conflicting plugins, so
+# 9999 > 2017.06.05.0.1).
+VERSION = "9999"