Merge pull request #29 from brendandouglas/master

Import of bazel plugin using copybara
diff --git a/.gitignore b/.gitignore
index 6d8ad95..25a88ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-bazel-*
\ No newline at end of file
+bazel-*
+!bazel-buildifier
diff --git a/BUILD b/BUILD
index 81608fe..8062413 100644
--- a/BUILD
+++ b/BUILD
@@ -21,7 +21,9 @@
     name = "aswb_tests",
     tests = [
         "//aswb:unit_tests",
+        "//base:integration_tests",
         "//base:unit_tests",
+        "//java:integration_tests",
         "//java:unit_tests",
     ],
 )
@@ -34,17 +36,3 @@
         "//cpp:unit_tests",
     ],
 )
-
-load(
-    ":version.bzl",
-    "VERSION",
-)
-
-# Version file
-genrule(
-    name = "version",
-    srcs = [],
-    outs = ["VERSION"],
-    cmd = "echo '%s' > $@" % VERSION,
-    visibility = ["//visibility:public"],
-)
diff --git a/README.md b/README.md
index 0fc36c1..946478a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# An IntelliJ plugin for [Bazel](http://bazel.io) projects
+# An IntelliJ plugin for [Bazel](http://bazel.build) projects
 
 This is an early-access version of our IntelliJ bazel plugin.
 
@@ -19,7 +19,7 @@
 To import an existing Bazel project, choose 'Import Bazel Project',
 and follow the instructions in the project import wizard.
 
-Detailed docs are available [here](http://ij.bazel.io).
+Detailed docs are available [here](http://ij.bazel.build).
 
 ## Building the plugin
 
diff --git a/aswb/BUILD b/aswb/BUILD
index 6e4a3a2..8c6fd4a 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -10,6 +10,7 @@
     "merged_plugin_xml",
     "stamped_plugin_xml",
 )
+load("//:version.bzl", "VERSION")
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
@@ -39,7 +40,7 @@
     plugin_name = "Android Studio with Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
-    version_file = "//:version",
+    version = VERSION,
 )
 
 java_library(
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index d8d9334..ab228a2 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -18,6 +18,7 @@
 
   <depends>com.intellij.modules.androidstudio</depends>
   <depends>org.jetbrains.android</depends>
+  <depends>com.android.tools.idea.updater</depends>
 
   <extensions defaultExtensionNs="com.intellij">
     <java.elementFinder implementation="com.google.idea.blaze.android.resources.AndroidResourceClassFinder"
@@ -62,11 +63,6 @@
     <sdkEventListener implementation="com.google.idea.blaze.android.sdk.AndroidSdkListener"/>
   </extensions>
 
-  <extensionPoints>
-    <extensionPoint qualifiedName="com.google.idea.blaze.android.InstrumentationRunnerProvider"
-                    interface="com.google.idea.blaze.android.run.test.InstrumentationRunnerProvider"/>
-  </extensionPoints>
-
   <!-- BEGIN NDK SUPPORT -->
   <extensions defaultExtensionNs="com.intellij">
     <applicationService serviceInterface="com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider"
diff --git a/aswb/src/META-INF/aswb_bazel.xml b/aswb/src/META-INF/aswb_bazel.xml
index 4d50f16..4f6f6e9 100644
--- a/aswb/src/META-INF/aswb_bazel.xml
+++ b/aswb/src/META-INF/aswb_bazel.xml
@@ -16,7 +16,7 @@
 <idea-plugin>
   <description>
     <![CDATA[
-      <a href="http://bazel.io">Bazel</a> support for Android Studio.
+      <a href="https://bazel.build">Bazel</a> support for Android Studio.
 
       Features:
         <ul>
@@ -25,7 +25,7 @@
         <li>Support for Bazel run configurations for certain rule classes.</li>
         </ul>
 
-      Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
+      Usage instructions at <a href="https://ij.bazel.build">ij.bazel.build</a>
       ]]>
   </description>
 </idea-plugin>
diff --git a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
index a8fe734..becaa66 100644
--- a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
+++ b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
@@ -61,7 +61,9 @@
       return null;
     }
     Manifest manifest = manifestFileMap.get(file);
-    if (manifest != null) {
+    // Note: The manifest may be invalid if the underlying VirtualFile is invalidated.
+    // Once invalid, it cannot become valid again, and must be reloaded.
+    if (manifest != null && manifest.isValid()) {
       return manifest;
     }
     final VirtualFile virtualFile;
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 e657124..6557f8b 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
@@ -46,7 +46,7 @@
   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 boolean useSplitApksIfPossible = true;
+  private boolean useSplitApksIfPossible = false;
   private boolean instantRun = false;
   private boolean useWorkProfileIfPresent = false;
   private Integer userId;
@@ -151,7 +151,8 @@
 
     setDeepLink(Strings.nullToEmpty(element.getAttributeValue(DEEP_LINK)));
     setActivityClass(Strings.nullToEmpty(element.getAttributeValue(ACTIVITY_CLASS)));
-    setMode(Strings.nullToEmpty(element.getAttributeValue(MODE)));
+    String modeValue = element.getAttributeValue(MODE);
+    setMode(Strings.isNullOrEmpty(modeValue) ? LAUNCH_DEFAULT_ACTIVITY : modeValue);
     setMobileInstall(Boolean.parseBoolean(element.getAttributeValue(MOBILE_INSTALL_ATTR)));
     setUseSplitApksIfPossible(
         Boolean.parseBoolean(element.getAttributeValue(USE_SPLIT_APKS_IF_POSSIBLE)));
@@ -173,7 +174,7 @@
           activityClass = Strings.nullToEmpty(value);
           break;
         case MODE:
-          mode = Strings.nullToEmpty(value);
+          mode = Strings.isNullOrEmpty(value) ? LAUNCH_DEFAULT_ACTIVITY : value;
           break;
         case ACTIVITY_EXTRA_FLAGS:
           if (userId == null) {
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java b/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java
index 9ad2aa0..8f7ce00 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java
@@ -17,7 +17,7 @@
 
 import com.android.sdklib.BuildToolInfo;
 import com.android.sdklib.BuildToolInfo.PathId;
-import com.google.idea.blaze.android.sdk.SdkUtil;
+import com.google.idea.blaze.android.sync.sdk.SdkUtil;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.execution.process.OSProcessHandler;
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 8f2f218..7ab695c 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
@@ -182,7 +182,6 @@
         applicationIdProvider,
         launchOptions.isDebug(),
         deployInfo,
-        facet,
         processHandlerLaunchStatus);
   }
 
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java
deleted file mode 100644
index 3c90aba..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java
+++ /dev/null
@@ -1,40 +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.test;
-
-import com.intellij.openapi.extensions.ExtensionPointName;
-import javax.annotation.Nullable;
-
-/** Provides a default instrumentation test runner class for android test configurations. */
-public interface InstrumentationRunnerProvider {
-
-  ExtensionPointName<InstrumentationRunnerProvider> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.android.InstrumentationRunnerProvider");
-
-  @Nullable
-  static String getDefaultInstrumentationRunnerClass() {
-    for (InstrumentationRunnerProvider provider : EP_NAME.getExtensions()) {
-      String path = provider.getInstrumentationRunnerClass();
-      if (path != null) {
-        return path;
-      }
-    }
-    return null;
-  }
-
-  @Nullable
-  String getInstrumentationRunnerClass();
-}
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 e17cb97..cf3664b 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
@@ -15,38 +15,36 @@
  */
 package com.google.idea.blaze.android.run.test;
 
-import com.android.builder.model.Variant;
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
-import com.android.tools.idea.gradle.AndroidGradleModel;
 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.tasks.LaunchTask;
 import com.android.tools.idea.run.testing.AndroidTestListener;
 import com.android.tools.idea.run.util.LaunchStatus;
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.Computable;
 import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.psi.PsiClass;
-import org.jetbrains.android.dom.manifest.Instrumentation;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.Nullable;
 
 final class StockAndroidTestLaunchTask implements LaunchTask {
   private static final Logger LOG = Logger.getInstance(StockAndroidTestLaunchTask.class);
 
   private final BlazeAndroidTestRunConfigurationState configState;
-  @Nullable private final String instrumentationTestRunner;
+  private final String instrumentationTestRunner;
   private final String testApplicationId;
   private final boolean waitForDebugger;
 
   private StockAndroidTestLaunchTask(
       BlazeAndroidTestRunConfigurationState configState,
-      @Nullable String runner,
+      String runner,
       String testPackage,
       boolean waitForDebugger) {
     this.configState = configState;
@@ -60,12 +58,7 @@
       ApplicationIdProvider applicationIdProvider,
       boolean waitForDebugger,
       BlazeAndroidDeployInfo deployInfo,
-      AndroidFacet facet,
       LaunchStatus launchStatus) {
-    String runner =
-        StringUtil.isEmpty(configState.getInstrumentationRunnerClass())
-            ? findInstrumentationRunner(deployInfo, facet)
-            : configState.getInstrumentationRunnerClass();
     String testPackage;
     try {
       testPackage = applicationIdProvider.getTestPackageName();
@@ -78,51 +71,80 @@
       return null;
     }
 
+    List<String> availableRunners = getRunnersFromManifest(deployInfo);
+    if (availableRunners.isEmpty()) {
+      launchStatus.terminateLaunch(
+          String.format(
+              "No instrumentation test runner is defined in the manifest.\n"
+                  + "At least one instrumentation tag must be defined for the\n"
+                  + "\"%1$s\" package in the AndroidManifest.xml, e.g.:\n"
+                  + "\n"
+                  + "<manifest\n"
+                  + "    package=\"%1$s\"\n"
+                  + "    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n"
+                  + "\n"
+                  + "    <instrumentation\n"
+                  + "        android:name=\"android.support.test.runner.AndroidJUnitRunner\"\n"
+                  + "        android:targetPackage=\"%1$s\">\n"
+                  + "    </instrumentation>\n"
+                  + "\n"
+                  + "</manifest>",
+              testPackage));
+      // Note: Gradle users will never see the above message, so don't mention Gradle here.
+      // Even if no runners are defined in build.gradle, Gradle will add a default to the manifest.
+      return null;
+    }
+    String runner = configState.getInstrumentationRunnerClass();
+    if (!StringUtil.isEmpty(runner)) {
+      if (!availableRunners.contains(runner)) {
+        launchStatus.terminateLaunch(
+            String.format(
+                "Instrumentation test runner \"%2$s\"\n"
+                    + "is not defined for the \"%1$s\" package in the manifest.\n"
+                    + "Clear the 'Specific instrumentation runner' field in your configuration\n"
+                    + "to default to \"%3$s\",\n"
+                    + "or add the runner to your AndroidManifest.xml:\n"
+                    + "\n"
+                    + "<manifest\n"
+                    + "    package=\"%1$s\"\n"
+                    + "    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n"
+                    + "\n"
+                    + "    <instrumentation\n"
+                    + "        android:name=\"%2$s\"\n"
+                    + "        android:targetPackage=\"%1$s\">\n"
+                    + "    </instrumentation>\n"
+                    + "\n"
+                    + "</manifest>",
+                testPackage, runner, availableRunners.get(0)));
+        return null;
+      }
+    } else {
+      // Default to the first available runner.
+      runner = availableRunners.get(0);
+    }
+
     return new StockAndroidTestLaunchTask(configState, runner, testPackage, waitForDebugger);
   }
 
-  @Nullable
-  private static String findInstrumentationRunner(
-      BlazeAndroidDeployInfo deployInfo, AndroidFacet facet) {
-    String runner = getRunnerFromManifest(deployInfo);
-
-    // TODO: Resolve direct AndroidGradleModel dep (b/22596984)
-    AndroidGradleModel androidModel = AndroidGradleModel.get(facet);
-    if (runner == null && androidModel != null) {
-      Variant selectedVariant = androidModel.getSelectedVariant();
-      String testRunner = selectedVariant.getMergedFlavor().getTestInstrumentationRunner();
-      if (testRunner != null) {
-        runner = testRunner;
-      }
-    }
-
-    // Fall back to the default runner.
-    if (runner == null) {
-      runner = InstrumentationRunnerProvider.getDefaultInstrumentationRunnerClass();
-    }
-
-    return runner;
-  }
-
-  @Nullable
-  private static String getRunnerFromManifest(final BlazeAndroidDeployInfo deployInfo) {
+  private static ImmutableList<String> getRunnersFromManifest(
+      final BlazeAndroidDeployInfo deployInfo) {
     if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
       return ApplicationManager.getApplication()
-          .runReadAction((Computable<String>) () -> getRunnerFromManifest(deployInfo));
+          .runReadAction(
+              (Computable<ImmutableList<String>>) () -> getRunnersFromManifest(deployInfo));
     }
 
     Manifest manifest = deployInfo.getMergedManifest();
     if (manifest != null) {
-      for (Instrumentation instrumentation : manifest.getInstrumentations()) {
-        if (instrumentation != null) {
-          PsiClass instrumentationClass = instrumentation.getInstrumentationClass().getValue();
-          if (instrumentationClass != null) {
-            return instrumentationClass.getQualifiedName();
-          }
-        }
-      }
+      return ImmutableList.copyOf(
+          manifest
+              .getInstrumentations()
+              .stream()
+              .map(instrumentation -> instrumentation.getInstrumentationClass().getStringValue())
+              .filter(Objects::nonNull)
+              .collect(Collectors.toList()));
     }
-    return null;
+    return ImmutableList.of();
   }
 
   @Override
diff --git a/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java b/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
index 9532859..53577f5 100644
--- a/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
+++ b/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
@@ -26,7 +26,7 @@
 @State(name = "AswbGlobalSettings", storages = @Storage("aswb.global.xml"))
 public class AswbGlobalSettings implements PersistentStateComponent<AswbGlobalSettings> {
 
-  private String localSdkLocation;
+  @Deprecated private String localSdkLocation;
 
   public static AswbGlobalSettings getInstance() {
     return ServiceManager.getService(AswbGlobalSettings.class);
@@ -43,10 +43,12 @@
     XmlSerializerUtil.copyBean(state, this);
   }
 
+  @Deprecated
   public void setLocalSdkLocation(String localSdkLocation) {
     this.localSdkLocation = localSdkLocation;
   }
 
+  @Deprecated
   public String getLocalSdkLocation() {
     return localSdkLocation;
   }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
index 0755074..a33fa08 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.android.sync;
 
+import com.android.tools.idea.sdk.IdeSdks;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -26,6 +27,9 @@
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
+import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
+import com.google.idea.blaze.android.sync.sdk.SdkExperiment;
+import com.google.idea.blaze.android.sync.sdklegacy.AndroidSdkPlatformSyncer;
 import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.SyncState;
@@ -37,6 +41,7 @@
 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.output.StatusOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
@@ -46,6 +51,7 @@
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleManager;
 import com.intellij.openapi.module.ModuleType;
@@ -57,6 +63,7 @@
 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
 import com.intellij.pom.java.LanguageLevel;
 import com.intellij.util.ui.UIUtil;
+import java.io.File;
 import java.util.Collection;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -98,6 +105,20 @@
   }
 
   @Override
+  public void installSdks(BlazeContext context) {
+    File path = IdeSdks.getAndroidSdkPath();
+    if (path != null) {
+      context.output(new StatusOutput("Installing SDK platforms..."));
+      ApplicationManager.getApplication()
+          .invokeAndWait(
+              () -> {
+                IdeSdks.createAndroidSdkPerAndroidTarget(path);
+              },
+              ModalityState.defaultModalityState());
+    }
+  }
+
+  @Override
   public void updateSyncState(
       Project project,
       BlazeContext context,
@@ -115,8 +136,13 @@
       return;
     }
 
-    AndroidSdkPlatform androidSdkPlatform =
-        AndroidSdkPlatformSyncer.getAndroidSdkPlatform(project, context);
+    final AndroidSdkPlatform androidSdkPlatform;
+    if (SdkExperiment.useStandardSdkManager()) {
+      androidSdkPlatform = AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet);
+    } else {
+      androidSdkPlatform = AndroidSdkPlatformSyncer.getAndroidSdkPlatform(project, context);
+    }
+
     BlazeAndroidWorkspaceImporter workspaceImporter =
         new BlazeAndroidWorkspaceImporter(project, context, workspaceRoot, projectViewSet, ruleMap);
     BlazeAndroidImportResult importResult =
@@ -131,7 +157,7 @@
   }
 
   @Override
-  public void updateSdk(
+  public void updateProjectSdk(
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
@@ -216,20 +242,27 @@
       return false;
     }
 
-    String androidSdkPlatform = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
-    if (Strings.isNullOrEmpty(androidSdkPlatform)) {
-      String error =
-          Joiner.on('\n')
-              .join(
-                  "No android_sdk_platform set.",
-                  "You should specify the android SDK platform in your '.blazeproject' file.",
-                  "To set this add an 'android_sdk_platform' line to your .blazeproject file,",
-                  "e.g. 'android_sdk_platform: \"android-N\"', where 'android-N' is a",
-                  "platform directory name in your local SDK directory.");
-      IssueOutput.error(error)
-          .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
-          .submit(context);
+    if (SdkExperiment.useStandardSdkManager()) {
+      if (AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet) == null) {
+        return false;
+      }
+    } else {
+      String androidSdkPlatform = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
+      if (Strings.isNullOrEmpty(androidSdkPlatform)) {
+        String error =
+            Joiner.on('\n')
+                .join(
+                    "No android_sdk_platform set.",
+                    "You should specify the android SDK platform in your '.blazeproject' file.",
+                    "To set this add an 'android_sdk_platform' line to your .blazeproject file,",
+                    "e.g. 'android_sdk_platform: \"android-N\"', where 'android-N' is a",
+                    "platform directory name in your local SDK directory.");
+        IssueOutput.error(error)
+            .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+            .submit(context);
+      }
     }
+
     return true;
   }
 
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
index 62a06a2..db6443a 100755
--- a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
@@ -59,5 +59,6 @@
     facetState.MANIFEST_FILE_RELATIVE_PATH = "";
     facetState.RES_FOLDER_RELATIVE_PATH = "";
     facetState.ASSETS_FOLDER_RELATIVE_PATH = "";
+    facetState.ENABLE_SOURCES_AUTOGENERATION = false;
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
index aff9a7e..7442635 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.android.resources.LightResourceClassService;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
-import com.google.idea.blaze.android.sync.AndroidSdkPlatformSyncer;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.android.sync.model.idea.BlazeAndroidModel;
 import com.google.idea.blaze.android.sync.model.idea.SourceProviderImpl;
+import com.google.idea.blaze.android.sync.sdk.SdkUtil;
 import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
@@ -220,8 +220,7 @@
     if (blazeProjectData == null) {
       return null;
     }
-    AndroidSdkPlatform androidSdkPlatform =
-        AndroidSdkPlatformSyncer.getAndroidSdkPlatform(blazeProjectData);
+    AndroidSdkPlatform androidSdkPlatform = SdkUtil.getAndroidSdkPlatform(blazeProjectData);
     if (androidSdkPlatform == null) {
       return null;
     }
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
new file mode 100644
index 0000000..2740352
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
@@ -0,0 +1,121 @@
+/*
+ * 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.sdk;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
+import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
+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.intellij.openapi.projectRoots.Sdk;
+import com.intellij.pom.Navigatable;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.sdk.AndroidPlatform;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+
+/** Calculates AndroidSdkPlatform. */
+public class AndroidSdkFromProjectView {
+  @Nullable
+  public static AndroidSdkPlatform getAndroidSdkPlatform(
+      BlazeContext context, ProjectViewSet projectViewSet) {
+    Collection<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+    if (sdks.isEmpty()) {
+      IssueOutput.error("No Android SDK configured. Please use the SDK manager to configure.")
+          .navigatable(
+              new Navigatable() {
+                @Override
+                public void navigate(boolean b) {
+                  SdkUtil.openSdkManager();
+                }
+
+                @Override
+                public boolean canNavigate() {
+                  return true;
+                }
+
+                @Override
+                public boolean canNavigateToSource() {
+                  return false;
+                }
+              })
+          .submit(context);
+      return null;
+    }
+    String androidSdk = null;
+    if (projectViewSet != null) {
+      androidSdk = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
+    }
+
+    if (androidSdk == null) {
+      IssueOutput.error(
+              ("No android_sdk_platform set. Please set to an android platform. "
+                  + "Available android_sdk_platforms are: "
+                  + getAvailableSdkPlatforms(sdks)))
+          .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+          .submit(context);
+      return null;
+    }
+
+    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
+    if (sdk == null) {
+      IssueOutput.error(
+              ("No such android_sdk_platform: '"
+                  + androidSdk
+                  + "'. "
+                  + "Available android_sdk_platforms are: "
+                  + getAvailableSdkPlatforms(sdks)
+                  + ". "
+                  + "Please change android_sdk_platform or run SDK manager "
+                  + "to download missing SDK platforms."))
+          .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+          .submit(context);
+      return null;
+    }
+
+    int androidSdkApiLevel = getAndroidSdkApiLevel(sdk);
+    return new AndroidSdkPlatform(androidSdk, androidSdkApiLevel);
+  }
+
+  private static String getAvailableSdkPlatforms(Collection<Sdk> sdks) {
+    List<String> names = Lists.newArrayList();
+    for (Sdk sdk : sdks) {
+      AndroidSdkAdditionalData additionalData = AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
+      if (additionalData == null) {
+        continue;
+      }
+      String targetHash = additionalData.getBuildTargetHashString();
+      names.add(targetHash);
+    }
+    return "{" + Joiner.on(", ").join(names) + "}";
+  }
+
+  private static int getAndroidSdkApiLevel(Sdk sdk) {
+    int androidSdkApiLevel = 1;
+    AndroidSdkAdditionalData additionalData = (AndroidSdkAdditionalData) sdk.getSdkAdditionalData();
+    if (additionalData != null) {
+      AndroidPlatform androidPlatform = additionalData.getAndroidPlatform();
+      if (androidPlatform != null) {
+        androidSdkApiLevel = androidPlatform.getApiLevel();
+      }
+    }
+    return androidSdkApiLevel;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.java
new file mode 100644
index 0000000..1ad1842
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.java
@@ -0,0 +1,30 @@
+/*
+ * 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.sdk;
+
+import com.android.tools.idea.startup.AndroidStudioInitializer;
+
+/**
+ * Wrapper class to keep track of "experiment" for later deletion.
+ *
+ * <p>The experiment is actually controlled by the JVM property, and we can't use a "normal"
+ * experiment for that since the JVM property also affects upstream Android Studio.
+ */
+public class SdkExperiment {
+  public static boolean useStandardSdkManager() {
+    return AndroidStudioInitializer.isAndroidSdkManagerEnabled();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
similarity index 60%
rename from aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.java
rename to aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
index a48166a..933679d 100644
--- a/aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
@@ -13,12 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.android.sdk;
+package com.google.idea.blaze.android.sync.sdk;
 
-import com.google.idea.blaze.android.sync.AndroidSdkPlatformSyncer;
+import com.android.tools.idea.updater.configure.SdkUpdaterConfigurableProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.openapi.options.ShowSettingsUtil;
+import com.intellij.openapi.options.ex.ConfigurableExtensionPointUtil;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.Sdk;
 import org.jetbrains.android.sdk.AndroidPlatform;
@@ -29,14 +33,19 @@
 /** SDK utilities. */
 public class SdkUtil {
   @Nullable
+  public static AndroidSdkPlatform getAndroidSdkPlatform(BlazeProjectData blazeProjectData) {
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    return syncData != null ? syncData.androidSdkPlatform : null;
+  }
+
+  @Nullable
   public static AndroidPlatform getAndroidPlatform(@NotNull Project project) {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
       return null;
     }
-    AndroidSdkPlatform androidSdkPlatform =
-        AndroidSdkPlatformSyncer.getAndroidSdkPlatform(blazeProjectData);
+    AndroidSdkPlatform androidSdkPlatform = getAndroidSdkPlatform(blazeProjectData);
     if (androidSdkPlatform == null) {
       return null;
     }
@@ -46,4 +55,12 @@
     }
     return AndroidPlatform.getInstance(sdk);
   }
+
+  /** Opens the SDK manager settings page */
+  public static void openSdkManager() {
+    Configurable configurable =
+        ConfigurableExtensionPointUtil.createApplicationConfigurableForProvider(
+            SdkUpdaterConfigurableProvider.class);
+    ShowSettingsUtil.getInstance().showSettingsDialog(null, configurable.getClass());
+  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java
similarity index 91%
rename from aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java
rename to aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java
index c219263..aa0fcd0 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.android.sync;
+package com.google.idea.blaze.android.sync.sdklegacy;
 
 import com.android.tools.idea.startup.AndroidStudioInitializer;
 import com.google.common.base.Joiner;
@@ -23,8 +23,6 @@
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
 import com.google.idea.blaze.android.settings.AswbGlobalSettings;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
-import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
-import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -41,9 +39,10 @@
 import org.jetbrains.android.sdk.AndroidSdkUtils;
 
 /** Calculates AndroidSdkPlatform. */
+@Deprecated
 public class AndroidSdkPlatformSyncer {
   @Nullable
-  static AndroidSdkPlatform getAndroidSdkPlatform(Project project, BlazeContext context) {
+  public static AndroidSdkPlatform getAndroidSdkPlatform(Project project, BlazeContext context) {
 
     final String localSdkLocation;
     if (AndroidStudioInitializer.isAndroidSdkManagerEnabled()) {
@@ -143,12 +142,6 @@
     return "<No platforms found>";
   }
 
-  @Nullable
-  public static AndroidSdkPlatform getAndroidSdkPlatform(BlazeProjectData blazeProjectData) {
-    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
-    return syncData != null ? syncData.androidSdkPlatform : null;
-  }
-
   private static int getAndroidSdkApiLevel(String androidSdk) {
     int androidSdkApiLevel = 1;
     Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java
similarity index 96%
rename from aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java
rename to aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java
index e9e838e..bee2b3b 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.android.sync;
+package com.google.idea.blaze.android.sync.sdklegacy;
 
 import com.android.SdkConstants;
 import com.android.sdklib.AndroidTargetHash;
@@ -28,7 +28,8 @@
 import javax.annotation.Nullable;
 
 /** Utility methods for handling the android sdk. */
-public final class BlazeAndroidSdk {
+@Deprecated
+final class BlazeAndroidSdk {
   private static final Logger LOG = Logger.getInstance(BlazeAndroidSdk.class);
 
   private BlazeAndroidSdk() {}
diff --git a/base/BUILD b/base/BUILD
index 0cd244d..2921507 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -6,6 +6,7 @@
     resources = glob(["resources/**/*"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//common/binaryhelper",
         "//common/experiments",
         "//intellij_platform_sdk:plugin_api",
         "//proto_deps",
diff --git a/base/scripts/create_bugreport.sh b/base/scripts/create_bugreport.sh
deleted file mode 100755
index d0c40d7..0000000
--- a/base/scripts/create_bugreport.sh
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/bin/bash
-
-# 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.
-
-#
-# Use this to create a bug report for IntelliJ plugin bugs.
-#
-# Usage: create_bugreport.sh
-#
-
-# For all jars in specified plugin directory, extracts plugin.xml
-# and copies to the specified output directory.
-# args:
-#   - IJ plugin directory
-#   - output directory
-attach_plugin_xmls() {
-  if [ ! -e $1 ]; then return; fi
-  pushd $1 >/dev/null
-  jars=(*.jar)
-  for jar in ${jars[@]}; do
-    plugin=${jar%.jar}
-    if [ -e $jar ]; then
-      mkdir -p $2
-      unzip -p $jar "META-INF/plugin.xml" > "$2/${plugin}.xml"
-      [ $? -eq 0 ] || { exit 1; }
-    fi
-  done
-  popd >/dev/null
-}
-
-files=""
-tmp_dir=$(mktemp -d)
-output_name=intellij-bug-report-$USER
-tar_dir=$tmp_dir/$output_name
-output_file=${output_name}.tar.gz
-
-mkdir -p "$tar_dir"
-[ $? -eq 0 ] || { exit 1; }
-
-# Attach process information
-process_info_dir=$tar_dir/process_info
-mkdir -p $process_info_dir
-
-pids=$(jps | awk '/[0-9]+ (Main)$/ { print $1 }')
-ts=$(date +%H%M%S)
-if [ -z "$pids" ]; then
-  echo "Warn: Could not find any IntelliJ processes."
-fi
-for pid in $pids
-do
-  stack_file=$process_info_dir/stack_${pid}_${ts}.txt
-  mem_file=$process_info_dir/mem_${pid}_${ts}.txt
-  capacity_file=$process_info_dir/capacity_${pid}_${ts}.txt
-  cmd_file=$process_info_dir/cmd_${pid}_${ts}.txt
-  uptime_file=$process_info_dir/uptime_${pid}_${ts}.txt
-  jps -v | grep $pid > $cmd_file
-  jstack $pid > $stack_file
-  jstat -gc $pid > $mem_file
-  jstat -gccapacity $pid > $capacity_file
-  ps -p $pid -o etime= > $uptime_file
-done
-
-# Copy core dumps
-mkdir -p $tar_dir/jvm-dumps
-cp -p $HOME/java_error_in_* $tar_dir/jvm-dumps 2>/dev/null
-
-# Copy vmoptions files
-mkdir -p $tar_dir
-cp -p $HOME/*.vmoptions $tar_dir 2>/dev/null
-
-# Copy details from IJ log directories
-dir_names=(
-  '.IntelliJIdea*'
-  '.IdeaIC*'
-  '.AndroidStudio*'
-  '.CLion*'
-)
-# other product codes, to add if we end up supporting them:
-# PhpStorm, WebStorm, RubyMine, PyCharm, WebIde, AppCode, DataGrip
-
-pushd $HOME >/dev/null
-for log_dirs in ${dir_names[@]}; do
-  for log_dir in $log_dirs ; do
-    if [ ! -e $log_dir ]; then
-      continue
-    fi
-    product=${log_dir:1}
-    mkdir ${tar_dir}/${product}
-    pushd ${tar_dir}/${product} >/dev/null
-
-    # Attach user's log directories
-    if [ -d ${HOME}/${log_dir}/system/log ]; then
-      mkdir -p "system/log"
-      cp -r "${HOME}/${log_dir}/system/log" "system"
-      [ $? -eq 0 ] || { exit 1; }
-    fi
-
-    # Attach product version
-    ij_home=$(<"${HOME}/${log_dir}/system/.home")
-    cp "${ij_home}/build.txt" "version"
-
-    # Attach plugin.xmls
-    attach_plugin_xmls "${HOME}/${log_dir}/system/plugins" "${tar_dir}/${product}/system/plugins"
-    attach_plugin_xmls "${HOME}/${log_dir}/config/plugins" "${tar_dir}/${product}/config/plugins"
-
-    # copy vmoptions
-    mkdir -p vmoptions/home
-    cp -p ${HOME}/${log_dir}/*.vmoptions vmoptions/home 2>/dev/null
-    mkdir -p vmoptions/installation
-    cp -p ${ij_home}/bin/*.vmoptions vmoptions/installation 2>/dev/null
-
-    popd >/dev/null
-  done
-done
-popd >/dev/null
-
-# Attach user's .blazerc
-if [ -f $HOME/.blazerc ]; then
-  cp $HOME/.blazerc $tar_dir/.blazerc
-fi
-
-pushd $tmp_dir >/dev/null
-tar -chzf $output_file $output_name
-[ $? -eq 0 ] || { exit 1; }
-popd >/dev/null
-
-tar_file=$tmp_dir/$output_file
-echo "Bug report produced in: $tar_file"
-echo "Path has been copied to clipboard."
-echo -n $tar_file | xclip -selection primary
-echo -n $tar_file | xclip -selection clipboard
-
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 7559da7..1715ec6 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -257,6 +257,7 @@
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazePsiDirectoryRootNodeNameModifier" interface="com.google.idea.blaze.base.treeview.BlazePsiDirectoryRootNodeNameModifier"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.FileCache" interface="com.google.idea.blaze.base.filecache.FileCache"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.TestRuleHeuristic" interface="com.google.idea.blaze.base.run.TestRuleHeuristic"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.ProjectDataDirectoryValidator" interface="com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
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 a3cee43..d207ff2 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -16,9 +16,13 @@
 package com.google.idea.blaze.base.bazel;
 
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.fileTypes.ExactFileNameMatcher;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
+import java.io.File;
 import javax.annotation.Nullable;
 
 /** Provides the bazel build system name string. */
@@ -44,6 +48,28 @@
   @Override
   public String getRuleDocumentationUrl(RuleDefinition rule) {
     // TODO: URL pointing to specific BUILD rule.
-    return "http://www.bazel.io/docs/be/overview.html";
+    return "http://www.bazel.build/docs/be/overview.html";
+  }
+
+  // TODO: Update the methods below when https://github.com/bazelbuild/bazel/issues/552 lands.
+  @Override
+  public boolean isBuildFile(String fileName) {
+    return fileName.equals("BUILD");
+  }
+
+  @Nullable
+  @Override
+  public File findBuildFileInDirectory(File directory) {
+    FileAttributeProvider provider = FileAttributeProvider.getInstance();
+    File child = new File(directory, "BUILD");
+    if (!provider.exists(child)) {
+      return null;
+    }
+    return child;
+  }
+
+  @Override
+  public FileNameMatcher buildFileMatcher() {
+    return new ExactFileNameMatcher("BUILD");
   }
 }
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 42bd43b..4b47716 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
@@ -21,6 +21,9 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.io.File;
 import javax.annotation.Nullable;
 
 /**
@@ -79,4 +82,27 @@
   /** The URL providing the built-in BUILD rule's documentation, if one can be found. */
   @Nullable
   String getRuleDocumentationUrl(RuleDefinition rule);
+
+  /** Check if the given filename is a valid BUILD file name. */
+  boolean isBuildFile(String fileName);
+
+  /**
+   * Check if the given directory has a child with a valid BUILD file name, and if so, returns the
+   * first such file.
+   */
+  @Nullable
+  File findBuildFileInDirectory(File directory);
+
+  /**
+   * Check if the given directory has a child with a valid BUILD file name, and if so, returns the
+   * first such file.
+   */
+  @Nullable
+  default VirtualFile findBuildFileInDirectory(VirtualFile directory) {
+    File file = new File(directory.getPath());
+    File buildFile = findBuildFileInDirectory(file);
+    return buildFile != null ? directory.getFileSystem().findFileByPath(buildFile.getPath()) : null;
+  }
+
+  FileNameMatcher buildFileMatcher();
 }
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java b/base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java
index 25079b2..b0b867a 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.buildmodifier;
 
-import com.google.idea.blaze.base.util.BlazeHelperBinaryUtil;
+import com.google.idea.common.binaryhelper.HelperBinaryUtil;
 import java.io.File;
 import javax.annotation.Nullable;
 
@@ -29,6 +29,6 @@
   @Nullable
   @Override
   public File getBuildifierBinary() {
-    return BlazeHelperBinaryUtil.getBlazeHelperBinary(BUILDIFIER_BINARY_PATH);
+    return HelperBinaryUtil.getHelperBinary(BUILDIFIER_BINARY_PATH);
   }
 }
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 02aa3f2..df5e61a 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.buildmodifier;
 
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.command.CommandProcessor;
 import com.intellij.openapi.editor.Document;
@@ -61,6 +62,6 @@
   }
 
   private static boolean isBuildFile(VirtualFile file) {
-    return file.getName().equals("BUILD");
+    return BuildSystemProvider.defaultBuildSystem().isBuildFile(file.getName());
   }
 }
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 d0d9887..484924b 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.command;
 
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -24,9 +23,7 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.project.Project;
 import com.intellij.util.PlatformUtils;
-import java.util.Collection;
 import java.util.List;
-import javax.annotation.Nullable;
 
 /** The collection of all the Bazel flag strings we use. */
 public final class BlazeFlags {
@@ -100,40 +97,5 @@
     return TOOL_TAG + platformPrefix;
   }
 
-  public static String testFilterFlagForClass(String className) {
-    return testFilterFlagForClassAndMethod(className, null);
-  }
-
-  public static String testFilterFlagForClassAndMethod(
-      String className, @Nullable String methodName) {
-    StringBuilder output = new StringBuilder(TEST_FILTER);
-    output.append('=');
-    output.append(className);
-
-    if (!Strings.isNullOrEmpty(methodName)) {
-      output.append('#');
-      output.append(methodName);
-      output.append('$');
-    }
-
-    return output.toString();
-  }
-
-  public static String testFilterFlagForClassAndMethods(
-      String className, Collection<String> methodNames, boolean isJUnit3Class) {
-    if (methodNames.size() == 0) {
-      return testFilterFlagForClass(className);
-    } else if (methodNames.size() == 1) {
-      return testFilterFlagForClassAndMethod(className, methodNames.iterator().next());
-    }
-    String methodNamePattern;
-    if (isJUnit3Class) {
-      methodNamePattern = String.join(",", methodNames);
-    } else {
-      methodNamePattern = String.format("(%s)", String.join("|", methodNames));
-    }
-    return testFilterFlagForClassAndMethod(className, methodNamePattern);
-  }
-
   private BlazeFlags() {}
 }
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 f507a52..d5c02c3 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
@@ -90,6 +90,7 @@
     Content console =
         layoutUi.createContent(
             BlazeConsoleToolWindowFactory.ID, myConsoleView.getComponent(), "", null, null);
+    console.setCloseable(false);
     layoutUi.addContent(console, 0, PlaceInGrid.right, false);
 
     // Adding actions
@@ -110,6 +111,7 @@
     //noinspection ConstantConditions
     Content content =
         ContentFactory.SERVICE.getInstance().createContent(layoutComponent, null, true);
+    content.setCloseable(false);
     toolWindow.getContentManager().addContent(content);
   }
 
diff --git a/base/src/com/google/idea/blaze/base/help/BlazeHelpHandlerImpl.java b/base/src/com/google/idea/blaze/base/help/BlazeHelpHandlerImpl.java
index e9a23b6..ec5471a 100644
--- a/base/src/com/google/idea/blaze/base/help/BlazeHelpHandlerImpl.java
+++ b/base/src/com/google/idea/blaze/base/help/BlazeHelpHandlerImpl.java
@@ -18,7 +18,7 @@
 import com.intellij.ide.BrowserUtil;
 
 class BlazeHelpHandlerImpl implements BlazeHelpHandler {
-  private static final String URL_BASE = "https://ij.bazel.io/";
+  private static final String URL_BASE = "https://ij.bazel.build/";
 
   @Override
   public void handleHelp(String urlFragment) {
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
index a8aaee8..d67dccf 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
@@ -19,9 +19,7 @@
 import com.google.idea.blaze.base.actions.BlazeAction;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.metrics.Action;
-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.scopes.BlazeConsoleScope;
 import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
@@ -56,19 +54,16 @@
     }
 
     Scope.root(
-        new ScopedOperation() {
-          @Override
-          public void execute(@NotNull BlazeContext context) {
-            context
-                .push(new ExperimentScope())
-                .push(new BlazeConsoleScope.Builder(project).build())
-                .push(new IssuesScope(project))
-                .push(new IdeaLogScope())
-                .push(new LoggedTimingScope(project, Action.CREATE_BLAZE_RULE));
-            NewBlazeRuleDialog newBlazeRuleDialog =
-                new NewBlazeRuleDialog(context, project, virtualFile);
-            newBlazeRuleDialog.show();
-          }
+        context -> {
+          context
+              .push(new ExperimentScope())
+              .push(new BlazeConsoleScope.Builder(project).build())
+              .push(new IssuesScope(project))
+              .push(new IdeaLogScope())
+              .push(new LoggedTimingScope(project, Action.CREATE_BLAZE_RULE));
+          NewBlazeRuleDialog newBlazeRuleDialog =
+              new NewBlazeRuleDialog(context, project, virtualFile);
+          newBlazeRuleDialog.show();
         });
   }
 
@@ -78,7 +73,10 @@
     DataContext dataContext = event.getDataContext();
     VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
     Project project = CommonDataKeys.PROJECT.getData(dataContext);
-    boolean enabled = (project != null && file != null && file.getName().equals("BUILD"));
+    boolean enabled =
+        (project != null
+            && file != null
+            && Blaze.getBuildSystemProvider(project).isBuildFile(file.getName()));
     presentation.setVisible(enabled || ActionPlaces.isMainMenuOrActionSearch(event.getPlace()));
     presentation.setEnabled(enabled);
     presentation.setText(getText(project));
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
index 32243bc..4c4a21c 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
@@ -15,10 +15,8 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.language;
 
-import com.google.common.collect.ImmutableList;
-import com.intellij.openapi.fileTypes.ExactFileNameMatcher;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
-import com.intellij.openapi.fileTypes.FileNameMatcher;
 import com.intellij.openapi.fileTypes.FileTypeConsumer;
 import com.intellij.openapi.fileTypes.FileTypeFactory;
 import org.jetbrains.annotations.NotNull;
@@ -26,11 +24,11 @@
 /** Factory for BuildFileType */
 public class BuildFileTypeFactory extends FileTypeFactory {
 
-  private static ImmutableList<FileNameMatcher> DEFAULT_ASSOCIATIONS =
-      ImmutableList.of(new ExactFileNameMatcher("BUILD"), new ExtensionFileNameMatcher("bzl"));
-
   @Override
   public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
-    consumer.consume(BuildFileType.INSTANCE, DEFAULT_ASSOCIATIONS.toArray(new FileNameMatcher[0]));
+    consumer.consume(
+        BuildFileType.INSTANCE,
+        BuildSystemProvider.defaultBuildSystem().buildFileMatcher(),
+        new ExtensionFileNameMatcher("bzl"));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
index 454ab79..6d728b8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
@@ -23,12 +23,14 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.RuleName;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.progress.ProgressManager;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
@@ -128,6 +130,10 @@
    */
   public BuildLookupElement[] resolvePackageLookupElements(FileLookupData lookupData) {
     String relativePath = lookupData.filePathFragment;
+    if (!relativePath.equals(FileUtil.toCanonicalUriPath(relativePath))) {
+      // ignore invalid labels containing './', '../', etc.
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
     File file = resolveWorkspaceRelativePath(relativePath);
 
     FileAttributeProvider provider = FileAttributeProvider.getInstance();
@@ -155,7 +161,7 @@
       List<VirtualFile> validChildren = Lists.newArrayListWithCapacity(children.length);
       for (VirtualFile child : children) {
         ProgressManager.checkCanceled();
-        if (child.getName().startsWith(pathFragment) && lookupData.acceptFile(child)) {
+        if (child.getName().startsWith(pathFragment) && lookupData.acceptFile(project, child)) {
           validChildren.add(child);
         }
       }
@@ -202,15 +208,15 @@
     if (packageDirectory == null || !provider.isDirectory(packageDirectory)) {
       return null;
     }
-    File buildFile = new File(packageDirectory, "BUILD");
-    if (!provider.exists(buildFile)) {
-      return null;
-    }
-    VirtualFile vf = getFileSystem().findFileByPath(buildFile.getPath());
+    VirtualFile vf = getFileSystem().findFileByPath(packageDirectory.getPath());
     if (vf == null) {
       return null;
     }
-    PsiFile psiFile = PsiManager.getInstance(project).findFile(vf);
+    VirtualFile buildFile = Blaze.getBuildSystemProvider(project).findBuildFileInDirectory(vf);
+    if (buildFile == null) {
+      return null;
+    }
+    PsiFile psiFile = PsiManager.getInstance(project).findFile(buildFile);
     return psiFile instanceof BuildFile ? (BuildFile) psiFile : null;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
index c6fe9ed..eabe428 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.icons.AllIcons;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.NullableLazyValue;
@@ -133,7 +134,7 @@
         || (containingPackage != null && containingFile != null));
   }
 
-  public boolean acceptFile(VirtualFile file) {
+  public boolean acceptFile(Project project, VirtualFile file) {
     if (fileFilter != null && !fileFilter.accept(file)) {
       return false;
     }
@@ -143,7 +144,8 @@
     if (file.equals(containingFile.getOriginalFile().getVirtualFile())) {
       return false;
     }
-    boolean blazePackage = file.findChild("BUILD") != null;
+    boolean blazePackage =
+        Blaze.getBuildSystemProvider(project).findBuildFileInDirectory(file) != null;
     return !blazePackage;
   }
 
@@ -188,6 +190,9 @@
 
   private String getItemText(String relativePath) {
     if (pathFormat == PathFormat.PackageLocal) {
+      if (containingPackage.length() > relativePath.length()) {
+        return "";
+      }
       return StringUtil.trimStart(relativePath.substring(containingPackage.length()), "/");
     }
     String parentPath = PathUtil.getParentPath(relativePath);
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
index 6c2e3a3..6f1b102 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
@@ -38,7 +38,8 @@
     if (file instanceof PsiFile) {
       file = ((PsiFile) file).getOriginalFile();
     }
-    if (file instanceof BuildFile && file.getName().equals("BUILD")) {
+    if (file instanceof BuildFile
+        && Blaze.getBuildSystemProvider(file.getProject()).isBuildFile(file.getName())) {
       return new BlazePackage((BuildFile) file);
     }
     return getContainingPackage(getPsiDirectory(file));
@@ -61,9 +62,14 @@
   @Nullable
   public static BlazePackage getContainingPackage(@Nullable PsiDirectory dir) {
     while (dir != null) {
-      PsiFile buildFile = dir.findFile("BUILD");
+      VirtualFile buildFile =
+          Blaze.getBuildSystemProvider(dir.getProject())
+              .findBuildFileInDirectory(dir.getVirtualFile());
       if (buildFile != null) {
-        return buildFile instanceof BuildFile ? new BlazePackage((BuildFile) buildFile) : null;
+        PsiFile psiFile = dir.getManager().findFile(buildFile);
+        if (psiFile != null) {
+          return psiFile instanceof BuildFile ? new BlazePackage((BuildFile) psiFile) : null;
+        }
       }
       dir = dir.getParentDirectory();
     }
@@ -151,8 +157,10 @@
     }
   }
 
-  private static boolean isBlazePackage(PsiDirectory directory) {
-    return directory.findFile("BUILD") != null;
+  private static boolean isBlazePackage(PsiDirectory dir) {
+    return Blaze.getBuildSystemProvider(dir.getProject())
+            .findBuildFileInDirectory(dir.getVirtualFile())
+        != null;
   }
 
   private static void processDirectory(Processor<PsiFile> processor, PsiDirectory directory) {
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
index 3bb2ccd..00ce9ee 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
@@ -70,7 +70,8 @@
     if (relativePath.startsWith("/")) {
       BlazeValidationError.collect(
           errors,
-          new BlazeValidationError("Workspace path may not start with '/': " + relativePath));
+          new BlazeValidationError(
+              "Workspace path must be relative; cannot start with '/': " + relativePath));
       return false;
     }
 
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 d9e5537..65d5b93 100644
--- a/base/src/com/google/idea/blaze/base/settings/Blaze.java
+++ b/base/src/com/google/idea/blaze/base/settings/Blaze.java
@@ -77,7 +77,9 @@
    * blaze build system if the project is null or not a blaze project.
    */
   public static BuildSystemProvider getBuildSystemProvider(@Nullable Project project) {
-    return BuildSystemProvider.getBuildSystemProvider(getBuildSystem(project));
+    BuildSystemProvider provider =
+        BuildSystemProvider.getBuildSystemProvider(getBuildSystem(project));
+    return provider != null ? provider : BuildSystemProvider.defaultBuildSystem();
   }
 
   /**
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 d541dc9..3b09467 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
@@ -119,8 +119,12 @@
                 suppressConsoleForRunAction.isSelected(), settings.getSuppressConsoleForRunAction())
             || !Objects.equal(resyncAutomatically.isSelected(), settings.getResyncAutomatically())
             || !Objects.equal(collapseProjectView.isSelected(), settings.getCollapseProjectView())
-            || !Objects.equal(blazeBinaryPathField.getText(), settings.getBlazeBinaryPath())
-            || !Objects.equal(bazelBinaryPathField.getText(), settings.getBazelBinaryPath());
+            || !Objects.equal(
+                Strings.nullToEmpty(blazeBinaryPathField.getText()),
+                Strings.nullToEmpty(settings.getBlazeBinaryPath()))
+            || !Objects.equal(
+                Strings.nullToEmpty(bazelBinaryPathField.getText()),
+                Strings.nullToEmpty(settings.getBazelBinaryPath()));
 
     for (BlazeUserSettingsContributor settingsContributor : settingsContributors) {
       isModified |= settingsContributor.isModified();
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
index 4c7d64e..f2639cf 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -90,6 +90,9 @@
   /** @return The set of supported languages under this workspace type. */
   Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType);
 
+  /** Installs any global SDKs */
+  void installSdks(BlazeContext context);
+
   /** Given the rule map, update the sync state for this plugin. Should not have side effects. */
   void updateSyncState(
       Project project,
@@ -105,8 +108,8 @@
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState);
 
-  /** Updates the sdk. */
-  void updateSdk(
+  /** Updates the sdk for the project. */
+  void updateProjectSdk(
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
@@ -152,9 +155,6 @@
   /** Returns any custom sections that this plugin supports. */
   Collection<SectionParser> getSections();
 
-  /** Returns whether this plugin requires resolving ide artifacts to function. */
-  boolean requiresResolveIdeArtifacts();
-
   /** Convenience adapter to help stubbing out methods. */
   class Adapter implements BlazeSyncPlugin {
 
@@ -176,6 +176,9 @@
     }
 
     @Override
+    public void installSdks(BlazeContext context) {}
+
+    @Override
     public void updateSyncState(
         Project project,
         BlazeContext context,
@@ -191,7 +194,7 @@
         @Nullable SyncState previousSyncState) {}
 
     @Override
-    public void updateSdk(
+    public void updateProjectSdk(
         Project project,
         BlazeContext context,
         ProjectViewSet projectViewSet,
@@ -237,9 +240,5 @@
       return ImmutableList.of();
     }
 
-    @Override
-    public boolean requiresResolveIdeArtifacts() {
-      return false;
-    }
   }
 }
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 6b5c4dc..d8d3642 100755
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.async.AsyncUtil;
@@ -66,6 +67,7 @@
 import com.google.idea.blaze.base.sync.projectstructure.ContentEntryEditor;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorImpl;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+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.workspace.ArtifactLocationDecoder;
@@ -92,6 +94,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -226,6 +229,10 @@
       return SyncResult.FAILURE;
     }
 
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.installSdks(context);
+    }
+
     if (!ProjectViewVerifier.verifyProjectView(
         context, workspaceRoot, projectViewSet, workspaceLanguageSettings)) {
       return SyncResult.FAILURE;
@@ -258,17 +265,12 @@
       SyncState previousSyncState =
           oldBlazeProjectData != null ? oldBlazeProjectData.syncState : null;
 
-      boolean syncPluginRequiresBuild = false;
-      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-        syncPluginRequiresBuild |= syncPlugin.requiresResolveIdeArtifacts();
-      }
-
       List<TargetExpression> targets = Lists.newArrayList();
       if (syncParams.addProjectViewTargets || oldBlazeProjectData == null) {
         targets.addAll(projectViewSet.listItems(TargetSection.KEY));
       }
       if (syncParams.addWorkingSet && workingSet != null) {
-        targets.addAll(getWorkingSetTargets(workingSet));
+        targets.addAll(getWorkingSetTargets(projectViewSet, workingSet));
       }
       targets.addAll(syncParams.targetExpressions);
 
@@ -298,14 +300,11 @@
       ListenableFuture<ImmutableMultimap<RuleKey, RuleKey>> reverseDependenciesFuture =
           BlazeExecutor.getInstance().submit(() -> ReverseDependencyMap.createRdepsMap(ruleMap));
 
-      boolean doResolve = syncPluginRequiresBuild || oldBlazeProjectData == null;
-      if (doResolve) {
-        ideResolveResult =
-            resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targets);
-        if (ideResolveResult == BuildResult.FATAL_ERROR) {
-          context.setHasError();
-          return SyncResult.FAILURE;
-        }
+      ideResolveResult =
+          resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targets);
+      if (ideResolveResult == BuildResult.FATAL_ERROR) {
+        context.setHasError();
+        return SyncResult.FAILURE;
       }
       if (context.isCancelled()) {
         return SyncResult.CANCELLED;
@@ -494,15 +493,22 @@
     }
   }
 
-  private Collection<? extends TargetExpression> getWorkingSetTargets(WorkingSet workingSet) {
-    List<TargetExpression> result = Lists.newArrayList();
+  private Collection<? extends TargetExpression> getWorkingSetTargets(
+      ProjectViewSet projectViewSet, WorkingSet workingSet) {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, importSettings.getBuildSystem())
+            .add(projectViewSet)
+            .build();
+    BuildTargetFinder buildTargetFinder =
+        new BuildTargetFinder(project, workspaceRoot, importRoots);
+
+    Set<TargetExpression> result = Sets.newHashSet();
     for (WorkspacePath workspacePath :
         Iterables.concat(workingSet.addedFiles, workingSet.modifiedFiles)) {
-      File buildFile = workspaceRoot.fileForPath(workspacePath);
-      if (buildFile.getName().equals("BUILD")) {
-        result.add(
-            TargetExpression.allFromPackageNonRecursive(
-                workspaceRoot.workspacePathFor(buildFile.getParentFile())));
+      File file = workspaceRoot.fileForPath(workspacePath);
+      TargetExpression targetExpression = buildTargetFinder.findTargetForFile(file);
+      if (targetExpression != null) {
+        result.add(targetExpression);
       }
     }
     return result;
@@ -587,7 +593,7 @@
                     ProjectRootManagerEx.getInstanceEx(this.project)
                         .mergeRootsChangesDuring(
                             () -> {
-                              updateSdk(context, projectViewSet, newBlazeProjectData);
+                              updateProjectSdk(context, projectViewSet, newBlazeProjectData);
                               updateProjectStructure(
                                   context,
                                   importSettings,
@@ -607,10 +613,10 @@
         });
   }
 
-  private void updateSdk(
+  private void updateProjectSdk(
       BlazeContext context, ProjectViewSet projectViewSet, BlazeProjectData newBlazeProjectData) {
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      syncPlugin.updateSdk(project, context, projectViewSet, newBlazeProjectData);
+      syncPlugin.updateProjectSdk(project, context, projectViewSet, newBlazeProjectData);
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/BuildTargetFinder.java b/base/src/com/google/idea/blaze/base/sync/BuildTargetFinder.java
new file mode 100644
index 0000000..8525a20
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/BuildTargetFinder.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Finds the best target to sync for a file. */
+public class BuildTargetFinder {
+  private final WorkspaceRoot workspaceRoot;
+  private final ImportRoots importRoots;
+  private final FileAttributeProvider fileAttributeProvider;
+  private final BuildSystemProvider buildSystemProvider;
+
+  public BuildTargetFinder(Project project, WorkspaceRoot workspaceRoot, ImportRoots importRoots) {
+    this.workspaceRoot = workspaceRoot;
+    this.importRoots = importRoots;
+    this.fileAttributeProvider = FileAttributeProvider.getInstance();
+    this.buildSystemProvider = Blaze.getBuildSystemProvider(project);
+  }
+
+  @Nullable
+  public TargetExpression findTargetForFile(File file) {
+    if (fileAttributeProvider.isFile(file)) {
+      file = file.getParentFile();
+      if (file == null) {
+        return null;
+      }
+    }
+
+    final File directory = file;
+    File root =
+        importRoots
+            .rootDirectories()
+            .stream()
+            .map(workspaceRoot::fileForPath)
+            .filter(potentialRoot -> FileUtil.isAncestor(potentialRoot, directory, false))
+            .findFirst()
+            .orElse(null);
+    if (root == null) {
+      return null;
+    }
+
+    File currentDirectory = directory;
+    do {
+      File buildFile = buildSystemProvider.findBuildFileInDirectory(currentDirectory);
+      if (buildFile != null) {
+        return TargetExpression.allFromPackageNonRecursive(
+            workspaceRoot.workspacePathFor(currentDirectory));
+      }
+      currentDirectory = currentDirectory.getParentFile();
+    } while (currentDirectory != null && FileUtil.isAncestor(root, currentDirectory, false));
+
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
index cba67de..08010af 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
@@ -19,10 +19,15 @@
 import com.google.idea.blaze.base.actions.BlazeAction;
 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.rulemaps.SourceToRuleMap;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
 import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BuildTargetFinder;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.CommonDataKeys;
 import com.intellij.openapi.actionSystem.Presentation;
@@ -80,32 +85,29 @@
       return null;
     }
 
-    String objectName = null;
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    SourceToRuleMap.getInstance(project);
 
-    if (virtualFile.isDirectory()) {
-      if (workspaceRoot.isInWorkspace(virtualFile)) {
-        targets.add(
-            TargetExpression.allFromPackageRecursive(workspaceRoot.workspacePathFor(virtualFile)));
-      }
-      objectName = "Package";
-    } else {
+    String objectName = virtualFile.isDirectory() ? "Package" : "File";
+    if (!virtualFile.isDirectory()) {
       targets.addAll(
           SourceToRuleMap.getInstance(project)
               .getTargetsToBuildForSourceFile(new File(virtualFile.getPath())));
-
-      // If empty, try to build parent package
-      if (targets.isEmpty()) {
-        VirtualFile parent = virtualFile.getParent();
-        if (parent.isDirectory()) {
-          if (workspaceRoot.isInWorkspace(parent)) {
-            targets.add(
-                TargetExpression.allFromPackageNonRecursive(
-                    workspaceRoot.workspacePathFor(parent)));
-          }
+    }
+    if (targets.isEmpty()) {
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      if (projectViewSet != null) {
+        BuildSystem buildSystem = Blaze.getBuildSystem(project);
+        ImportRoots importRoots =
+            ImportRoots.builder(workspaceRoot, buildSystem).add(projectViewSet).build();
+        BuildTargetFinder buildTargetFinder =
+            new BuildTargetFinder(project, workspaceRoot, importRoots);
+        TargetExpression targetExpression =
+            buildTargetFinder.findTargetForFile(new File(virtualFile.getPath()));
+        if (targetExpression != null) {
+          targets.add(targetExpression);
         }
       }
-      objectName = "File";
     }
 
     return objectName;
diff --git a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
index a6b062b..396f27d 100644
--- a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.sync.status;
 
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+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;
@@ -175,7 +176,7 @@
   /**
    * Listens for changes to files which impact the sync process (BUILD files and project view files)
    */
-  private static class FileFocusListener extends FileEditorManagerAdapter {
+  private class FileFocusListener extends FileEditorManagerAdapter {
     @Override
     public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
       processEvent(file);
@@ -197,12 +198,12 @@
     }
   }
 
-  private static boolean isSyncSensitiveFile(@Nullable VirtualFile file) {
+  private boolean isSyncSensitiveFile(@Nullable VirtualFile file) {
     return file != null
         && (isBuildFile(file) || ProjectViewStorageManager.isProjectViewFile(file.getPath()));
   }
 
-  private static boolean isBuildFile(VirtualFile file) {
-    return file.getName().equals("BUILD");
+  private boolean isBuildFile(VirtualFile file) {
+    return Blaze.getBuildSystemProvider(project).isBuildFile(file.getName());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java b/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
index 092fb48..bd54a39 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
@@ -125,4 +125,8 @@
     this.blazeBinExecutionRootPath = blazeBinExecutionRootPath;
     this.blazeGenfilesExecutionRootPath = blazeGenfilesExecutionRootPath;
   }
+
+  public File getGenfilesDirectory() {
+    return blazeGenfilesExecutionRootPath.getFileRootedAt(executionRoot);
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
index ad36704..11b4fbb 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
@@ -15,6 +15,10 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.plugin.dependency.PluginDependencyHelper;
 import com.google.idea.blaze.base.projectview.ProjectView;
@@ -30,19 +34,25 @@
 import com.intellij.openapi.util.text.StringUtil;
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
 import java.util.UUID;
 
 /** Contains the state to build a new project throughout the new project wizard process. */
 public final class BlazeNewProjectBuilder {
   private static final Logger LOG = Logger.getInstance(BlazeNewProjectBuilder.class);
 
+  // The import wizard should keep this many items around for fields that care about history
+  public static final int HISTORY_SIZE = 8;
+
   // Stored in user settings as the last imported workspace
   private static final String LAST_IMPORTED_BLAZE_WORKSPACE =
       "blaze-wizard.last-imported-workspace";
   private static final String LAST_IMPORTED_BAZEL_WORKSPACE =
       "blaze-wizard.last-imported-bazel-workspace";
+  private static final String HISTORY_SEPARATOR = "::";
 
-  public static String lastImportedWorkspaceKey(BuildSystem buildSystem) {
+  private static String lastImportedWorkspaceKey(BuildSystem buildSystem) {
     switch (buildSystem) {
       case Blaze:
         return LAST_IMPORTED_BLAZE_WORKSPACE;
@@ -72,6 +82,29 @@
     return userSettings;
   }
 
+  public String getLastImportedWorkspace(BuildSystem buildSystem) {
+    List<String> workspaceHistory = getWorkspaceHistory(buildSystem);
+    return workspaceHistory.isEmpty() ? "" : workspaceHistory.get(0);
+  }
+
+  public List<String> getWorkspaceHistory(BuildSystem buildSystem) {
+    String value = userSettings.get(lastImportedWorkspaceKey(buildSystem), "");
+    return Strings.isNullOrEmpty(value)
+        ? ImmutableList.of()
+        : Arrays.asList(value.split(HISTORY_SEPARATOR));
+  }
+
+  private void writeWorkspaceHistory(BuildSystem buildSystem, String newValue) {
+    List<String> history = Lists.newArrayList(getWorkspaceHistory(buildSystem));
+    history.remove(newValue);
+    history.add(0, newValue);
+    while (history.size() > HISTORY_SIZE) {
+      history.remove(history.size() - 1);
+    }
+    userSettings.put(
+        lastImportedWorkspaceKey(buildSystem), Joiner.on(HISTORY_SEPARATOR).join(history));
+  }
+
   public BlazeSelectWorkspaceOption getWorkspaceOption() {
     return workspaceOption;
   }
@@ -151,8 +184,8 @@
     workspaceOption.commit();
     projectViewOption.commit();
 
-    String workspaceKey = lastImportedWorkspaceKey(workspaceOption.getBuildSystemForWorkspace());
-    userSettings.put(workspaceKey, workspaceRoot.toString());
+    BuildSystem buildSystem = workspaceOption.getBuildSystemForWorkspace();
+    writeWorkspaceHistory(buildSystem, workspaceRoot.toString());
 
     if (!StringUtil.isEmpty(projectDataDirectory)) {
       File projectDataDir = new File(projectDataDirectory);
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 437f736..2d94db8 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
@@ -24,6 +24,7 @@
 import com.intellij.openapi.fileChooser.FileChooserFactory;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.TextFieldWithStoredHistory;
 import java.awt.Dimension;
 import java.io.File;
 import java.io.IOException;
@@ -34,20 +35,20 @@
 import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
-import javax.swing.JTextField;
 
 class CopyExternalProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "copy-external.last-project-view-path";
 
   final BlazeWizardUserSettings userSettings;
   final JComponent component;
-  final JTextField projectViewPathField;
+  final TextFieldWithStoredHistory projectViewPathField;
 
   CopyExternalProjectViewOption(BlazeNewProjectBuilder builder) {
     this.userSettings = builder.getUserSettings();
 
-    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
-    this.projectViewPathField = new JTextField(defaultWorkspacePath);
+    this.projectViewPathField = new TextFieldWithStoredHistory(LAST_WORKSPACE_PATH);
+    projectViewPathField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
+    projectViewPathField.setText(userSettings.get(LAST_WORKSPACE_PATH, ""));
 
     JButton button = new JButton("...");
     button.addActionListener(action -> chooseWorkspacePath());
@@ -108,6 +109,7 @@
   @Override
   public void commit() {
     userSettings.put(LAST_WORKSPACE_PATH, getProjectViewPath());
+    projectViewPathField.addCurrentTextToHistory();
   }
 
   private String getProjectViewPath() {
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 9826c2a..dce0fb7 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 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;
@@ -32,29 +33,31 @@
 import com.intellij.openapi.fileChooser.FileChooserDialog;
 import com.intellij.openapi.fileChooser.FileChooserFactory;
 import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.TextFieldWithStoredHistory;
 import java.awt.Dimension;
 import java.io.File;
 import javax.annotation.Nullable;
 import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
-import javax.swing.JTextField;
 
 class GenerateFromBuildFileSelectProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "generate-from-build-file.last-workspace-path";
   private final BlazeNewProjectBuilder builder;
   private final BlazeWizardUserSettings userSettings;
-  private final JTextField buildFilePathField;
+  private final TextFieldWithStoredHistory buildFilePathField;
   private final JComponent component;
 
   public GenerateFromBuildFileSelectProjectViewOption(BlazeNewProjectBuilder builder) {
     this.builder = builder;
     this.userSettings = builder.getUserSettings();
 
-    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
-    this.buildFilePathField = new JTextField(defaultWorkspacePath);
+    this.buildFilePathField = new TextFieldWithStoredHistory(LAST_WORKSPACE_PATH);
+    buildFilePathField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
+    buildFilePathField.setText(userSettings.get(LAST_WORKSPACE_PATH, ""));
 
     JButton button = new JButton("...");
     button.addActionListener(action -> chooseWorkspacePath());
@@ -117,6 +120,7 @@
   @Override
   public void commit() {
     userSettings.put(LAST_WORKSPACE_PATH, getBuildFilePath());
+    buildFilePathField.addCurrentTextToHistory();
   }
 
   private static String guessProjectViewFromLocation(
@@ -174,13 +178,15 @@
   }
 
   private void chooseWorkspacePath() {
+    BuildSystemProvider buildSystem =
+        BuildSystemProvider.getBuildSystemProvider(builder.getBuildSystem());
     FileChooserDescriptor descriptor =
         new FileChooserDescriptor(true, false, false, false, false, false)
             .withShowHiddenFiles(true) // Show root project view file
             .withHideIgnored(false)
             .withTitle("Select BUILD File")
             .withDescription("Select a BUILD file to synthesize a project view from.")
-            .withFileFilter(virtualFile -> virtualFile.getName().equals("BUILD"));
+            .withFileFilter(virtualFile -> buildSystem.isBuildFile(virtualFile.getName()));
     FileChooserDialog chooser =
         FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
 
@@ -188,10 +194,15 @@
 
     File startingLocation = temporaryWorkspaceRoot.directory();
     String buildFilePath = getBuildFilePath();
-    if (!buildFilePath.isEmpty()) {
-      File fileLocation = temporaryWorkspaceRoot.fileForPath(new WorkspacePath(buildFilePath));
-      if (fileLocation.exists()) {
-        startingLocation = fileLocation;
+    if (!buildFilePath.isEmpty() && WorkspacePath.validate(buildFilePath)) {
+      // If the user has typed part of the path then clicked the '...', try to start from the
+      // partial state
+      buildFilePath = StringUtil.trimEnd(buildFilePath, '/');
+      if (WorkspacePath.validate(buildFilePath)) {
+        File fileLocation = temporaryWorkspaceRoot.fileForPath(new WorkspacePath(buildFilePath));
+        if (fileLocation.exists()) {
+          startingLocation = fileLocation;
+        }
       }
     }
     VirtualFile toSelect =
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 1162d05..b7f2340 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -15,9 +15,11 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
+import com.google.common.collect.Lists;
 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.ProjectViewStorageManager;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
@@ -28,13 +30,14 @@
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.TextFieldWithStoredHistory;
 import java.awt.Dimension;
 import java.io.File;
+import java.util.List;
 import javax.annotation.Nullable;
 import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
-import javax.swing.JTextField;
 
 class ImportFromWorkspaceProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "import-from-workspace.last-workspace-path";
@@ -42,14 +45,15 @@
   final BlazeNewProjectBuilder builder;
   final BlazeWizardUserSettings userSettings;
   final JComponent component;
-  final JTextField projectViewPathField;
+  final TextFieldWithStoredHistory projectViewPathField;
 
   ImportFromWorkspaceProjectViewOption(BlazeNewProjectBuilder builder) {
     this.builder = builder;
     this.userSettings = builder.getUserSettings();
 
-    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
-    this.projectViewPathField = new JTextField(defaultWorkspacePath);
+    this.projectViewPathField = new TextFieldWithStoredHistory(LAST_WORKSPACE_PATH);
+    projectViewPathField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
+    projectViewPathField.setText(userSettings.get(LAST_WORKSPACE_PATH, ""));
 
     JButton button = new JButton("...");
     button.addActionListener(action -> chooseWorkspacePath());
@@ -83,6 +87,10 @@
     if (getProjectViewPath().isEmpty()) {
       return BlazeValidationResult.failure("Workspace path to project view file cannot be empty.");
     }
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!WorkspacePath.validate(getProjectViewPath(), errors)) {
+      return BlazeValidationResult.failure(errors.get(0));
+    }
     WorkspaceRoot temporaryWorkspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
     File file = temporaryWorkspaceRoot.fileForPath(getSharedProjectView());
     if (!file.exists()) {
@@ -107,6 +115,7 @@
   @Override
   public void commit() {
     userSettings.put(LAST_WORKSPACE_PATH, getProjectViewPath());
+    projectViewPathField.addCurrentTextToHistory();
   }
 
   private String getProjectViewPath() {
@@ -131,12 +140,14 @@
     File startingLocation = temporaryWorkspaceRoot.directory();
     String projectViewPath = getProjectViewPath();
     if (!projectViewPath.isEmpty()) {
-      // Avoid exception -- workspace paths cannot end with trailing slash
+      // If the user has typed part of the path then clicked the '...', try to start from the
+      // partial state
       projectViewPath = StringUtil.trimEnd(projectViewPath, '/');
-
-      File fileLocation = temporaryWorkspaceRoot.fileForPath(new WorkspacePath(projectViewPath));
-      if (fileLocation.exists()) {
-        startingLocation = fileLocation;
+      if (WorkspacePath.validate(projectViewPath)) {
+        File fileLocation = temporaryWorkspaceRoot.fileForPath(new WorkspacePath(projectViewPath));
+        if (fileLocation.exists()) {
+          startingLocation = fileLocation;
+        }
       }
     }
     VirtualFile toSelect =
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ProjectDataDirectoryValidator.java b/base/src/com/google/idea/blaze/base/wizard2/ProjectDataDirectoryValidator.java
new file mode 100644
index 0000000..0c3520d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/wizard2/ProjectDataDirectoryValidator.java
@@ -0,0 +1,28 @@
+/*
+ * 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.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import java.io.File;
+
+/** Validates the project data directory. */
+public interface ProjectDataDirectoryValidator {
+  ExtensionPointName<ProjectDataDirectoryValidator> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.ProjectDataDirectoryValidator");
+
+  BlazeValidationResult validateDataDirectory(File projectDataDirectory);
+}
diff --git a/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
index 9d18f4e..cff86ed 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
@@ -28,6 +28,7 @@
 import com.intellij.openapi.util.IconLoader;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.TextFieldWithHistory;
 import java.awt.Dimension;
 import java.io.File;
 import java.util.Arrays;
@@ -37,24 +38,21 @@
 import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
-import javax.swing.JTextField;
 
 /** Option to use an existing workspace */
 public abstract class UseExistingWorkspaceOption implements BlazeSelectWorkspaceOption {
 
-  private final BlazeWizardUserSettings userSettings;
   private final JComponent component;
-  private final JTextField directoryField;
+  private final TextFieldWithHistory directoryField;
   private final BuildSystem buildSystem;
 
   protected UseExistingWorkspaceOption(BlazeNewProjectBuilder builder, BuildSystem buildSystem) {
-    this.userSettings = builder.getUserSettings();
     this.buildSystem = buildSystem;
 
-    String defaultDirectory =
-        userSettings.get(BlazeNewProjectBuilder.lastImportedWorkspaceKey(buildSystem), "");
-
-    this.directoryField = new JTextField(defaultDirectory);
+    this.directoryField = new TextFieldWithHistory();
+    directoryField.setHistory(builder.getWorkspaceHistory(buildSystem));
+    directoryField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
+    directoryField.setText(builder.getLastImportedWorkspace(buildSystem));
 
     JButton button = new JButton("...");
     button.addActionListener(action -> chooseDirectory());
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 de7e373..81ace4e 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
@@ -23,22 +23,32 @@
 import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.projectview.ProjectViewVerifier;
 import com.google.idea.blaze.base.projectview.section.ScalarSection;
 import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.OutputSink.Propagation;
+import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.IssueOutput.Category;
 import com.google.idea.blaze.base.settings.ui.JPanelProvidingProject;
 import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
 import com.google.idea.blaze.base.wizard2.BlazeSelectProjectViewOption;
 import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
+import com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator;
 import com.intellij.ide.RecentProjectsManager;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationNamesInfo;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.progress.ProgressManager;
 import com.intellij.openapi.ui.TextComponentAccessor;
 import com.intellij.openapi.ui.TextFieldWithBrowseButton;
 import com.intellij.openapi.util.io.FileUtil;
@@ -55,6 +65,7 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /** The UI control to collect project settings when importing a Blaze project. */
@@ -72,6 +83,7 @@
   private JTextField projectNameField;
   private HashCode paramsHash;
   private WorkspaceRoot workspaceRoot;
+  private WorkspaceRoot temporaryWorkspaceRoot;
 
   public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     this.projectViewUi = new ProjectViewUi(parentDisposable);
@@ -162,6 +174,7 @@
       @Nullable WorkspacePath sharedProjectView,
       @Nullable String initialProjectViewText) {
     this.workspaceRoot = workspaceRoot;
+    this.temporaryWorkspaceRoot = temporaryWorkspaceRoot;
     projectNameField.setText(workspaceName);
     String defaultDataDir = getDefaultProjectDataDirectory(workspaceName);
     projectDataDirField.setText(defaultDataDir);
@@ -252,6 +265,13 @@
       return BlazeValidationResult.failure(
           new BlazeValidationError("Project data directory is not valid"));
     }
+    for (ProjectDataDirectoryValidator validator :
+        ProjectDataDirectoryValidator.EP_NAME.getExtensions()) {
+      BlazeValidationResult result = validator.validateDataDirectory(projectDataDir);
+      if (!result.success) {
+        return result;
+      }
+    }
     File workspaceRootDirectory = workspaceRoot.directory();
     if (FileUtil.isAncestor(projectDataDir, workspaceRootDirectory, false)) {
       return BlazeValidationResult.failure(
@@ -268,15 +288,71 @@
 
     List<IssueOutput> issues = Lists.newArrayList();
 
-    projectViewUi.parseProjectView(issues);
+    ProjectViewSet projectViewSet = projectViewUi.parseProjectView(issues);
     BlazeValidationError projectViewParseError = validationErrorFromIssueList(issues);
     if (projectViewParseError != null) {
       return BlazeValidationResult.failure(projectViewParseError);
     }
 
+    ProjectViewValidator projectViewValidator =
+        new ProjectViewValidator(temporaryWorkspaceRoot, projectViewSet);
+    ProgressManager.getInstance()
+        .runProcessWithProgressSynchronously(
+            projectViewValidator, "Validating Project", false, null);
+
+    if (!projectViewValidator.success) {
+      if (!projectViewValidator.errors.isEmpty()) {
+        return BlazeValidationResult.failure(
+            validationErrorFromIssueList(projectViewValidator.errors));
+      }
+      return BlazeValidationResult.failure(
+          "Project view validation failed, but we couldn't find an error message. "
+              + "Please report a bug.");
+    }
+
     return BlazeValidationResult.success();
   }
 
+  private static class ProjectViewValidator implements Runnable {
+    private final WorkspaceRoot workspaceRoot;
+    private final ProjectViewSet projectViewSet;
+
+    private boolean success;
+    List<IssueOutput> errors = Lists.newArrayList();
+
+    ProjectViewValidator(WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+      this.workspaceRoot = workspaceRoot;
+      this.projectViewSet = projectViewSet;
+    }
+
+    @Override
+    public void run() {
+      success = Scope.root(this::validateProjectView);
+    }
+
+    @NotNull
+    private Boolean validateProjectView(BlazeContext context) {
+      context.addOutputSink(
+          IssueOutput.class,
+          output -> {
+            if (output.getCategory() == Category.ERROR) {
+              errors.add(output);
+            }
+            return Propagation.Continue;
+          });
+      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+        syncPlugin.installSdks(context);
+      }
+      WorkspaceLanguageSettings workspaceLanguageSettings =
+          LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+      if (workspaceLanguageSettings == null) {
+        return false;
+      }
+      return ProjectViewVerifier.verifyProjectView(
+          context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    }
+  }
+
   @Nullable
   private static BlazeValidationError validationErrorFromIssueList(List<IssueOutput> issues) {
     List<IssueOutput> errors =
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
index 50a490a..32825ed 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
@@ -40,18 +40,4 @@
     assertThat(file).isInstanceOf(BuildFile.class);
   }
 
-  /**
-   * We may want to support these in the future (and in the meantime the user can manually have them
-   * recognized as BUILD files, for syntax highlighting, etc.).<br>
-   * Currently, turned off by default because references won't resolve correctly -- they'll point
-   * back to normal BUILD files.
-   */
-  @Test
-  public void testOtherBuildFilesNotRecognized() {
-    PsiFile file = createPsiFile("java/com/google/foo/BUILD.tools");
-    assertThat(file).isNotInstanceOf(BuildFile.class);
-
-    file = createPsiFile("java/com/google/foo/BUILD.bazel");
-    assertThat(file).isNotInstanceOf(BuildFile.class);
-  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java b/base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java
index 67e74e7..3a69631 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java
@@ -50,11 +50,13 @@
     List<BlazeValidationError> errors = Lists.newArrayList();
 
     WorkspacePath.validate("/foo", errors);
-    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not start with '/': /foo");
+    assertThat(errors.get(0).getError())
+        .isEqualTo("Workspace path must be relative; cannot start with '/': /foo");
     errors.clear();
 
     WorkspacePath.validate("/", errors);
-    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not start with '/': /");
+    assertThat(errors.get(0).getError())
+        .isEqualTo("Workspace path must be relative; cannot start with '/': /");
     errors.clear();
 
     WorkspacePath.validate("foo/", errors);
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/BuildTargetFinderTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/BuildTargetFinderTest.java
new file mode 100644
index 0000000..c1ff46d
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/BuildTargetFinderTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BazelBuildSystemProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+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.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import java.io.File;
+import java.util.Collection;
+import java.util.Set;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for build target finder. */
+@RunWith(JUnit4.class)
+public class BuildTargetFinderTest extends BlazeTestCase {
+
+  private static class MockFileAttributeProvider extends FileAttributeProvider {
+    final Set<File> files = Sets.newHashSet();
+
+    void addFiles(File... files) {
+      this.files.addAll(Lists.newArrayList(files));
+    }
+
+    @Override
+    public boolean isFile(File file) {
+      return files.contains(file);
+    }
+
+    @Override
+    public boolean exists(File file) {
+      return files.contains(file);
+    }
+  }
+
+  private final MockFileAttributeProvider fileAttributeProvider = new MockFileAttributeProvider();
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    applicationServices.register(FileAttributeProvider.class, fileAttributeProvider);
+    applicationServices.register(
+        BlazeImportSettingsManager.class, mock(BlazeImportSettingsManager.class));
+    ExtensionPoint<BuildSystemProvider> extensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    extensionPoint.registerExtension(new BazelBuildSystemProvider());
+  }
+
+  private BuildTargetFinder buildTargetFinder(Collection<WorkspacePath> roots) {
+    ImportRoots.Builder builder = ImportRoots.builder(workspaceRoot, BuildSystem.Bazel);
+    for (WorkspacePath root : roots) {
+      builder.add(DirectoryEntry.include(root));
+    }
+
+    return new BuildTargetFinder(project, workspaceRoot, builder.build());
+  }
+
+  @Test
+  public void firstBuildFileInDirectoryIsFound() {
+    BuildTargetFinder buildTargetFinder =
+        buildTargetFinder(ImmutableList.of(new WorkspacePath(".")));
+
+    fileAttributeProvider.addFiles(
+        new File("/root/j/c/g/some/BUILD"),
+        new File("/root/j/c/g/BUILD"),
+        new File("/root/j/c/g/some/dir/File.java"));
+
+    assertThat(buildTargetFinder.findTargetForFile(new File("/root/j/c/g/some/dir/File.java")))
+        .isEqualTo(TargetExpression.fromString("//j/c/g/some:all"));
+  }
+
+  @Test
+  public void testNothingFound() {
+    BuildTargetFinder buildTargetFinder =
+        buildTargetFinder(ImmutableList.of(new WorkspacePath(".")));
+
+    fileAttributeProvider.addFiles(
+        new File("/root/j/c/g/other/BUILD"), new File("/root/j/c/g/some/dir/File.java"));
+
+    assertThat(buildTargetFinder.findTargetForFile(new File("/root/j/c/g/some/dir/File.java")))
+        .isNull();
+  }
+
+  @Test
+  public void buildFilesBelowWorkspaceRootsNotFound() {
+    BuildTargetFinder buildTargetFinder =
+        buildTargetFinder(ImmutableList.of(new WorkspacePath("j/c/g/some")));
+
+    fileAttributeProvider.addFiles(
+        new File("/root/j/c/g/BUILD"), new File("/root/j/c/g/some/dir/File.java"));
+
+    assertThat(buildTargetFinder.findTargetForFile(new File("/root/j/c/g/some/dir/File.java")))
+        .isNull();
+  }
+
+  @Test
+  public void rightContentRootFound() {
+    BuildTargetFinder buildTargetFinder =
+        buildTargetFinder(
+            ImmutableList.of(
+                new WorkspacePath("j/c/g/foo"),
+                new WorkspacePath("j/c/g/bar"),
+                new WorkspacePath("j/c/g/baz")));
+
+    fileAttributeProvider.addFiles(
+        new File("/root/j/c/g/BUILD"),
+        new File("/root/j/c/g/foo/BUILD"),
+        new File("/root/j/c/g/bar/BUILD"),
+        new File("/root/j/c/g/baz/BUILD"),
+        new File("/root/j/c/g/bar/dir/File.java"));
+
+    assertThat(buildTargetFinder.findTargetForFile(new File("/root/j/c/g/bar/dir/File.java")))
+        .isEqualTo(TargetExpression.fromString("//j/c/g/bar:all"));
+  }
+
+  @Test
+  public void buildFileFindsSelf() {
+    BuildTargetFinder buildTargetFinder =
+        buildTargetFinder(ImmutableList.of(new WorkspacePath(".")));
+
+    fileAttributeProvider.addFiles(
+        new File("/root/j/c/g/some/BUILD"), new File("/root/j/c/g/BUILD"));
+
+    assertThat(buildTargetFinder.findTargetForFile(new File("/root/j/c/g/some/BUILD")))
+        .isEqualTo(TargetExpression.fromString("//j/c/g/some:all"));
+  }
+}
diff --git a/build_defs/BUILD b/build_defs/BUILD
index 0988550..3c8b0b3 100644
--- a/build_defs/BUILD
+++ b/build_defs/BUILD
@@ -1,5 +1,27 @@
+# Description:
+#
+# Scripts for building IntelliJ plugins
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])  # Apache 2.0
 
-exports_files(["LICENSE"])
+py_binary(
+    name = "merge_xml",
+    srcs = ["merge_xml.py"],
+)
+
+py_binary(
+    name = "stamp_plugin_xml",
+    srcs = ["stamp_plugin_xml.py"],
+)
+
+py_binary(
+    name = "product_build_txt",
+    srcs = ["product_build_txt.py"],
+)
+
+py_binary(
+    name = "api_version_txt",
+    srcs = ["api_version_txt.py"],
+)
diff --git a/build_defs/shared/api_version_txt.py b/build_defs/api_version_txt.py
similarity index 100%
rename from build_defs/shared/api_version_txt.py
rename to build_defs/api_version_txt.py
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
index d8c9613..f836204 100644
--- a/build_defs/build_defs.bzl
+++ b/build_defs/build_defs.bzl
@@ -1,29 +1,30 @@
-"""Custom build macros for plugin.xml handling.
+"""Custom build macros for IntelliJ plugin handling.
 """
 
-load("//build_defs/shared:build_defs.bzl",
-     "merged_plugin_xml_impl",
-     "stamped_plugin_xml_impl",
-     "product_build_txt_impl",
-     "api_version_txt_impl",
-     "intellij_plugin_impl",
-     "plugin_bundle_impl")
-
 def merged_plugin_xml(name, srcs, **kwargs):
   """Merges N plugin.xml files together."""
-  merged_plugin_xml_impl(
+  merge_tool = "//build_defs:merge_xml"
+  native.genrule(
       name = name,
       srcs = srcs,
-      merge_tool = "//build_defs/shared:merge_xml",
+      outs = [name + ".xml"],
+      cmd = "./$(location {merge_tool}) $(SRCS) > $@".format(
+          merge_tool=merge_tool,
+      ),
+      tools = [merge_tool],
       **kwargs)
 
+def _optstr(name, value):
+  return ("--" + name) if value else ""
+
 def stamped_plugin_xml(name,
                        plugin_xml,
-                       plugin_id=None,
-                       plugin_name=None,
+                       plugin_id = None,
+                       plugin_name = None,
                        stamp_since_build=False,
                        stamp_until_build=False,
                        include_product_code_in_stamp=False,
+                       version=None,
                        version_file=None,
                        changelog_file=None,
                        description_file=None,
@@ -39,60 +40,202 @@
     stamp_since_build: Add build number to idea-version since-build.
     stamp_until_build: Add build number to idea-version until-build.
     include_product_code_in_stamp: Whether the product code (eg. "IC")
-      is included in since-build and until-build.
+        is included in since-build and until-build.
+    version: A version number to stamp.
     version_file: A file with the version number to be included.
-    changelog_file: A file with changelog to be included.
+    changelog_file: A file with the changelog to be included.
     description_file: A file containing a plugin description to be included.
     vendor_file: A file containing the vendor info to be included.
     **kwargs: Any additional arguments to pass to the final target.
   """
+  stamp_tool = "//build_defs:stamp_plugin_xml"
+
+  api_version_txt_name = name + "_api_version"
   api_version_txt(
-      name = name + "_api_version",
+      name = api_version_txt_name,
   )
-  stamped_plugin_xml_impl(
+
+  args = [
+      "./$(location {stamp_tool})",
+      "--plugin_xml=$(location {plugin_xml})",
+      "--api_version_txt=$(location {api_version_txt_name})",
+      "{stamp_since_build}",
+      "{stamp_until_build}",
+      "{include_product_code_in_stamp}",
+  ]
+  srcs = [plugin_xml, api_version_txt_name]
+
+  if version and version_file:
+    fail("Cannot supply both version and version_file")
+
+  if plugin_id:
+    args.append("--plugin_id=%s" % plugin_id)
+
+  if plugin_name:
+    args.append("--plugin_name='%s'" % plugin_name)
+
+  if version:
+    args.append("--version='%s'" % version)
+
+  if version_file:
+    args.append("--version_file=$(location {version_file})")
+    srcs.append(version_file)
+
+  if changelog_file:
+    args.append("--changelog_file=$(location {changelog_file})")
+    srcs.append(changelog_file)
+
+  if description_file:
+    args.append("--description_file=$(location {description_file})")
+    srcs.append(description_file)
+
+  if vendor_file:
+    args.append("--vendor_file=$(location {vendor_file})")
+    srcs.append(vendor_file)
+
+  cmd = " ".join(args).format(
+      plugin_xml=plugin_xml,
+      api_version_txt_name=api_version_txt_name,
+      stamp_tool=stamp_tool,
+      stamp_since_build=_optstr("stamp_since_build",
+                                stamp_since_build),
+      stamp_until_build=_optstr("stamp_until_build",
+                                stamp_until_build),
+      include_product_code_in_stamp=_optstr(
+          "include_product_code_in_stamp",
+          include_product_code_in_stamp),
+      version_file=version_file,
+      changelog_file=changelog_file,
+      description_file=description_file,
+      vendor_file=vendor_file,
+  ) + "> $@"
+
+  native.genrule(
       name = name,
-      api_version_txt = name + "_api_version",
-      plugin_id = plugin_id,
-      plugin_name = plugin_name,
-      stamp_tool = "//build_defs/shared:stamp_plugin_xml",
-      plugin_xml = plugin_xml,
-      stamp_since_build = stamp_since_build,
-      stamp_until_build = stamp_until_build,
-      include_product_code_in_stamp = include_product_code_in_stamp,
-      version_file = version_file,
-      changelog_file = changelog_file,
-      description_file = description_file,
-      vendor_file = vendor_file,
+      srcs = srcs,
+      outs = [name + ".xml"],
+      cmd = cmd,
+      tools = [stamp_tool],
       **kwargs)
 
 def product_build_txt(name, **kwargs):
-  """Produces a product-build.txt file with the build number."""
-  product_build_txt_impl(
+  """Produces a product-build.txt file with the build number.
+
+  Args:
+    name: name of this target
+    **kwargs: Any additional arguments to pass to the final target.
+  """
+  application_info_jar = "//intellij_platform_sdk:application_info_jar"
+  application_info_name = "//intellij_platform_sdk:application_info_name"
+  product_build_txt_tool = "//build_defs:product_build_txt"
+
+  args = [
+      "./$(location {product_build_txt_tool})",
+      "--application_info_jar=$(location {application_info_jar})",
+      "--application_info_name=$(location {application_info_name})",
+  ]
+  cmd = " ".join(args).format(
+      application_info_jar=application_info_jar,
+      application_info_name=application_info_name,
+      product_build_txt_tool=product_build_txt_tool,
+  ) + "> $@"
+  native.genrule(
       name = name,
-      application_info_jar = "//intellij_platform_sdk:application_info_jar",
-      application_info_name = "//intellij_platform_sdk:application_info_name",
-      product_build_txt_tool = "//build_defs/shared:product_build_txt",
+      srcs = [application_info_jar, application_info_name],
+      outs = ["product-build.txt"],
+      cmd = cmd,
+      tools = [product_build_txt_tool],
       **kwargs)
 
 def api_version_txt(name, **kwargs):
-  """Produces an api_version.txt file with the api version, including the product code."""
-  api_version_txt_impl(
+  """Produces an api_version.txt file with the api version, including the product code.
+
+  Args:
+    name: name of this target
+    **kwargs: Any additional arguments to pass to the final target.
+  """
+  application_info_jar = "//intellij_platform_sdk:application_info_jar"
+  application_info_name = "//intellij_platform_sdk:application_info_name"
+  api_version_txt_tool = "//build_defs:api_version_txt"
+
+  args = [
+      "./$(location {api_version_txt_tool})",
+      "--application_info_jar=$(location {application_info_jar})",
+      "--application_info_name=$(location {application_info_name})",
+  ]
+  cmd = " ".join(args).format(
+      application_info_jar=application_info_jar,
+      application_info_name=application_info_name,
+      api_version_txt_tool=api_version_txt_tool,
+  ) + "> $@"
+  native.genrule(
       name = name,
-      application_info_jar = "//intellij_platform_sdk:application_info_jar",
-      application_info_name = "//intellij_platform_sdk:application_info_name",
-      api_version_txt_tool = "//build_defs/shared:api_version_txt",
+      srcs = [application_info_jar, application_info_name],
+      outs = [name + ".txt"],
+      cmd = cmd,
+      tools = [api_version_txt_tool],
       **kwargs)
 
-def intellij_plugin(name, plugin_xml, deps, meta_inf_files=[], **kwargs):
+def intellij_plugin(name, deps, plugin_xml, meta_inf_files=[], **kwargs):
   """Creates an intellij plugin from the given deps and plugin.xml."""
-  intellij_plugin_impl(
+  zip_tool = "//third_party:zip"
+  binary_name = name + "_binary"
+  deploy_jar = binary_name + "_deploy.jar"
+  native.java_binary(
+      name = binary_name,
+      runtime_deps = deps,
+      create_executable = 0,
+  )
+  cmd = [
+      "cp $(location {deploy_jar}) $@".format(deploy_jar=deploy_jar),
+      "chmod +w $@",
+      "mkdir -p META-INF",
+      "cp $(location {plugin_xml}) META-INF/plugin.xml".format(plugin_xml=plugin_xml),
+      "$(location {zip_tool}) -u $@ META-INF/plugin.xml >/dev/null".format(zip_tool=zip_tool),
+  ]
+  srcs = [
+      plugin_xml,
+      deploy_jar,
+  ]
+
+  for meta_inf_file in meta_inf_files:
+    cmd.append("cp $(location {meta_inf_file}) META-INF/$$(basename $(location {meta_inf_file}))".format(
+        meta_inf_file=meta_inf_file,
+    ))
+    cmd.append("$(location {zip_tool}) -u $@ META-INF/$$(basename $(location {meta_inf_file})) >/dev/null".format(
+        zip_tool=zip_tool,
+        meta_inf_file=meta_inf_file,
+    ))
+    srcs.append(meta_inf_file)
+
+  native.genrule(
+      name = name + "_genrule",
+      srcs = srcs,
+      tools = [zip_tool],
+      outs = [name + ".jar"],
+      cmd = " ; ".join(cmd),
+  )
+
+  # included (with tag) as a hack so that IJwB can recognize this is an intellij plugin
+  native.java_import(
       name = name,
-      plugin_xml = plugin_xml,
-      zip_tool = "//third_party:zip",
-      deps = deps,
-      meta_inf_files = meta_inf_files,
+      jars = [name + ".jar"],
+      tags = ["intellij-plugin"],
       **kwargs)
 
+def plugin_bundle(name, plugins):
+  """Communicates to IJwB a set of plugins which should be loaded together in a run configuration.
+
+  Args:
+    name: the name of this target
+    plugins: the 'intellij_plugin' targets to be bundled
+  """
+  native.java_library(
+      name = name,
+      exports = plugins,
+      tags = ["intellij-plugin-bundle"],
+  )
+
 def repackage_jar(name, src_rule, out,
                   rules = [
                       "com.google.common.** com.google.repackaged.common.@1",
@@ -142,12 +285,3 @@
       name = name,
       jars = [out],
       **kwargs)
-
-def plugin_bundle(name, plugins):
-  """Communicates to IJwB a set of plugins which should be loaded together in a run configuration.
-
-  Args:
-    name: the name of this target
-    plugins: the 'intellij_plugin' targets to be bundled
-  """
-  plugin_bundle_impl(name, plugins)
diff --git a/build_defs/shared/merge_xml.py b/build_defs/merge_xml.py
similarity index 100%
rename from build_defs/shared/merge_xml.py
rename to build_defs/merge_xml.py
diff --git a/build_defs/shared/product_build_txt.py b/build_defs/product_build_txt.py
similarity index 100%
rename from build_defs/shared/product_build_txt.py
rename to build_defs/product_build_txt.py
diff --git a/build_defs/shared/BUILD b/build_defs/shared/BUILD
deleted file mode 100644
index 3c8b0b3..0000000
--- a/build_defs/shared/BUILD
+++ /dev/null
@@ -1,27 +0,0 @@
-# Description:
-#
-# Scripts for building IntelliJ plugins
-
-package(default_visibility = ["//visibility:public"])
-
-licenses(["notice"])  # Apache 2.0
-
-py_binary(
-    name = "merge_xml",
-    srcs = ["merge_xml.py"],
-)
-
-py_binary(
-    name = "stamp_plugin_xml",
-    srcs = ["stamp_plugin_xml.py"],
-)
-
-py_binary(
-    name = "product_build_txt",
-    srcs = ["product_build_txt.py"],
-)
-
-py_binary(
-    name = "api_version_txt",
-    srcs = ["api_version_txt.py"],
-)
diff --git a/build_defs/shared/build_defs.bzl b/build_defs/shared/build_defs.bzl
deleted file mode 100644
index 5726ed0..0000000
--- a/build_defs/shared/build_defs.bzl
+++ /dev/null
@@ -1,235 +0,0 @@
-"""Custom build macros for IntelliJ plugin handling.
-"""
-
-def merged_plugin_xml_impl(name, srcs, merge_tool, **kwargs):
-  """Merges N plugin.xml files together."""
-  native.genrule(
-      name = name,
-      srcs = srcs,
-      outs = [name + ".xml"],
-      cmd = "./$(location {merge_tool}) $(SRCS) > $@".format(
-          merge_tool=merge_tool,
-      ),
-      tools = [merge_tool],
-      **kwargs)
-
-def _optstr(name, value):
-  return ("--" + name) if value else ""
-
-def stamped_plugin_xml_impl(name,
-                            plugin_xml,
-                            api_version_txt,
-                            stamp_tool,
-                            plugin_id = None,
-                            plugin_name = None,
-                            stamp_since_build=False,
-                            stamp_until_build=False,
-                            include_product_code_in_stamp=False,
-                            version_file=None,
-                            changelog_file=None,
-                            description_file=None,
-                            vendor_file=None,
-                            **kwargs):
-  """Stamps a plugin xml file with the IJ build number.
-
-  Args:
-    name: name of this target
-    plugin_xml: target plugin_xml to stamp
-    api_version_txt: the file containing the api version
-    stamp_tool: the tool to use to stamp the version
-    plugin_id: the plugin ID to stamp
-    plugin_name: the plugin name to stamp
-    stamp_since_build: Add build number to idea-version since-build.
-    stamp_until_build: Add build number to idea-version until-build.
-    include_product_code_in_stamp: Whether the product code (eg. "IC")
-        is included in since-build and until-build.
-    version_file: A file with the version number to be included.
-    changelog_file: A file with the changelog to be included.
-    description_file: A file containing a plugin description to be included.
-    vendor_file: A file containing the vendor info to be included.
-    **kwargs: Any additional arguments to pass to the final target.
-  """
-  args = [
-      "./$(location {stamp_tool})",
-      "--plugin_xml=$(location {plugin_xml})",
-      "--api_version_txt=$(location {api_version_txt})",
-      "{stamp_since_build}",
-      "{stamp_until_build}",
-      "{include_product_code_in_stamp}",
-  ]
-  srcs = [plugin_xml, api_version_txt]
-
-  if plugin_id:
-    args.append("--plugin_id=%s" % plugin_id)
-
-  if plugin_name:
-    args.append("--plugin_name='%s'" % plugin_name)
-
-  if version_file:
-    args.append("--version_file=$(location {version_file})")
-    srcs.append(version_file)
-
-  if changelog_file:
-    args.append("--changelog_file=$(location {changelog_file})")
-    srcs.append(changelog_file)
-
-  if description_file:
-    args.append("--description_file=$(location {description_file})")
-    srcs.append(description_file)
-
-  if vendor_file:
-    args.append("--vendor_file=$(location {vendor_file})")
-    srcs.append(vendor_file)
-
-  cmd = " ".join(args).format(
-      plugin_xml=plugin_xml,
-      api_version_txt=api_version_txt,
-      stamp_tool=stamp_tool,
-      stamp_since_build=_optstr("stamp_since_build",
-                                stamp_since_build),
-      stamp_until_build=_optstr("stamp_until_build",
-                                stamp_until_build),
-      include_product_code_in_stamp=_optstr(
-          "include_product_code_in_stamp",
-          include_product_code_in_stamp),
-      version_file=version_file,
-      changelog_file=changelog_file,
-      description_file=description_file,
-      vendor_file=vendor_file,
-  ) + "> $@"
-
-  native.genrule(
-      name = name,
-      srcs = srcs,
-      outs = [name + ".xml"],
-      cmd = cmd,
-      tools = [stamp_tool],
-      **kwargs)
-
-def product_build_txt_impl(name,
-                           product_build_txt_tool,
-                           application_info_jar,
-                           application_info_name,
-                           **kwargs):
-  """Produces a product-build.txt file with the build number.
-
-  Args:
-    name: name of this target
-    product_build_txt_tool: the product build tool
-    application_info_jar: the jar containing the application info
-    application_info_name: a file with the name of the application info
-    **kwargs: Any additional arguments to pass to the final target.
-  """
-  args = [
-      "./$(location {product_build_txt_tool})",
-      "--application_info_jar=$(location {application_info_jar})",
-      "--application_info_name=$(location {application_info_name})",
-  ]
-  cmd = " ".join(args).format(
-      application_info_jar=application_info_jar,
-      application_info_name=application_info_name,
-      product_build_txt_tool=product_build_txt_tool,
-  ) + "> $@"
-  native.genrule(
-      name = name,
-      srcs = [application_info_jar, application_info_name],
-      outs = ["product-build.txt"],
-      cmd = cmd,
-      tools = [product_build_txt_tool],
-      **kwargs)
-
-def api_version_txt_impl(name,
-                         api_version_txt_tool,
-                         application_info_jar,
-                         application_info_name,
-                         **kwargs):
-  """Produces an api_version.txt file with the api version, including the product code.
-
-  Args:
-    name: name of this target
-    api_version_txt_tool: the api version tool
-    application_info_jar: the jar containing the application info
-    application_info_name: a file with the name of the application info
-    **kwargs: Any additional arguments to pass to the final target.
-  """
-  args = [
-      "./$(location {api_version_txt_tool})",
-      "--application_info_jar=$(location {application_info_jar})",
-      "--application_info_name=$(location {application_info_name})",
-  ]
-  cmd = " ".join(args).format(
-      application_info_jar=application_info_jar,
-      application_info_name=application_info_name,
-      api_version_txt_tool=api_version_txt_tool,
-  ) + "> $@"
-  native.genrule(
-      name = name,
-      srcs = [application_info_jar, application_info_name],
-      outs = [name + ".txt"],
-      cmd = cmd,
-      tools = [api_version_txt_tool],
-      **kwargs)
-
-def intellij_plugin_impl(name,
-                         deps,
-                         plugin_xml,
-                         zip_tool,
-                         meta_inf_files=[],
-                         **kwargs):
-  """Creates an intellij plugin from the given deps and plugin.xml."""
-  binary_name = name + "_binary"
-  deploy_jar = binary_name + "_deploy.jar"
-  native.java_binary(
-      name = binary_name,
-      runtime_deps = deps,
-      create_executable = 0,
-  )
-  cmd = [
-      "cp $(location {deploy_jar}) $@".format(deploy_jar=deploy_jar),
-      "chmod +w $@",
-      "mkdir -p META-INF",
-      "cp $(location {plugin_xml}) META-INF/plugin.xml".format(plugin_xml=plugin_xml),
-      "$(location {zip_tool}) -u $@ META-INF/plugin.xml >/dev/null".format(zip_tool=zip_tool),
-  ]
-  srcs = [
-      plugin_xml,
-      deploy_jar,
-  ]
-
-  for meta_inf_file in meta_inf_files:
-    cmd.append("cp $(location {meta_inf_file}) META-INF/$$(basename $(location {meta_inf_file}))".format(
-        meta_inf_file=meta_inf_file,
-    ))
-    cmd.append("$(location {zip_tool}) -u $@ META-INF/$$(basename $(location {meta_inf_file})) >/dev/null".format(
-        zip_tool=zip_tool,
-        meta_inf_file=meta_inf_file,
-    ))
-    srcs.append(meta_inf_file)
-
-  native.genrule(
-      name = name + "_genrule",
-      srcs = srcs,
-      tools = [zip_tool],
-      outs = [name + ".jar"],
-      cmd = " ; ".join(cmd),
-  )
-
-  # included (with tag) as a hack so that IJwB can recognize this is an intellij plugin
-  native.java_import(
-      name = name,
-      jars = [name + ".jar"],
-      tags = ["intellij-plugin"],
-      **kwargs)
-
-def plugin_bundle_impl(name, plugins):
-  """Communicates to IJwB a set of plugins which should be loaded together in a run configuration.
-
-  Args:
-    name: the name of this target
-    plugins: the 'intellij_plugin' targets to be bundled
-  """
-  native.java_library(
-      name = name,
-      exports = plugins,
-      tags = ["intellij-plugin-bundle"],
-  )
diff --git a/build_defs/shared/stamp_plugin_xml.py b/build_defs/stamp_plugin_xml.py
similarity index 89%
rename from build_defs/shared/stamp_plugin_xml.py
rename to build_defs/stamp_plugin_xml.py
index aeaf5d2..72baa66 100755
--- a/build_defs/shared/stamp_plugin_xml.py
+++ b/build_defs/stamp_plugin_xml.py
@@ -35,6 +35,9 @@
     help="plugin name to stamp into the plugin.xml",
 )
 parser.add_argument(
+    "--version",
+    help="Version to stamp into the plugin.xml",)
+parser.add_argument(
     "--version_file",
     help="Version file to stamp into the plugin.xml",
 )
@@ -100,23 +103,24 @@
 
   idea_plugin = dom.documentElement
 
-  version_element = None
-  version_elements = idea_plugin.getElementsByTagName("version")
-  if len(version_elements) > 1:
-    raise ValueError("Ambigious version element")
+  if args.version and args.version_file:
+    raise ValueError("Cannot supply both version and version_file")
 
-  if len(version_elements) == 1:
-    version_element = version_elements[0].firstChild
-
-  if args.version_file:
-    if version_element:
-      raise ValueError("version element already in plugin.xml")
+  if args.version or args.version_file:
+    version_elements = idea_plugin.getElementsByTagName("version")
+    for element in version_elements:
+      idea_plugin.removeChild(element)
     version_element = dom.createElement("version")
     new_elements.append(version_element)
-    with open(args.version_file) as f:
-      value = f.read().strip()
-      version_text = dom.createTextNode(value)
-      version_element.appendChild(version_text)
+
+    version_value = None
+    if args.version:
+      version_value = args.version
+    else:
+      with open(args.version_file) as f:
+        version_value = f.read().strip()
+    version_text = dom.createTextNode(version_value)
+    version_element.appendChild(version_text)
 
   if args.stamp_since_build or args.stamp_until_build:
     if idea_plugin.getElementsByTagName("idea-version"):
diff --git a/clwb/BUILD b/clwb/BUILD
index 792d459..f1048b9 100644
--- a/clwb/BUILD
+++ b/clwb/BUILD
@@ -10,6 +10,7 @@
     "merged_plugin_xml",
     "stamped_plugin_xml",
 )
+load("//:version.bzl", "VERSION")
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
@@ -36,7 +37,7 @@
     plugin_name = "CLion with Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
-    version_file = "//:version",
+    version = VERSION,
 )
 
 java_library(
diff --git a/clwb/src/META-INF/clwb.xml b/clwb/src/META-INF/clwb.xml
index 4e871ca..b6d8dbf 100644
--- a/clwb/src/META-INF/clwb.xml
+++ b/clwb/src/META-INF/clwb.xml
@@ -17,8 +17,6 @@
   <vendor>Google</vendor>
 
   <depends>com.intellij.modules.clion</depends>
-  <depends>com.intellij.modules.cidr.lang</depends>
-  <depends>com.intellij.modules.cidr.debugger</depends>
 
   <extensions defaultExtensionNs="com.intellij">
     <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
@@ -70,6 +68,9 @@
     <component>
       <implementation-class>com.google.idea.blaze.plugin.ClwbProjectSpecificInitializer</implementation-class>
     </component>
+    <component>
+      <implementation-class>com.google.idea.blaze.clwb.run.producers.NonBlazeProducerSuppressor</implementation-class>
+    </component>
   </project-components>
 
 </idea-plugin>
diff --git a/clwb/src/META-INF/clwb_bazel.xml b/clwb/src/META-INF/clwb_bazel.xml
index 9bd8aff..4f66c87 100644
--- a/clwb/src/META-INF/clwb_bazel.xml
+++ b/clwb/src/META-INF/clwb_bazel.xml
@@ -16,7 +16,7 @@
 <idea-plugin>
   <description>
     <![CDATA[
-      <a href="http://bazel.io">Bazel</a> support for CLion.
+      <a href="https://bazel.build">Bazel</a> support for CLion.
 
       Features:
         <ul>
@@ -25,7 +25,7 @@
         <li>Support for Bazel run configurations for certain rule classes.</li>
         </ul>
 
-      Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
+      Usage instructions at <a href="https://ij.bazel.build">ij.bazel.build</a>
       ]]>
   </description>
-</idea-plugin>
\ No newline at end of file
+</idea-plugin>
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java
new file mode 100644
index 0000000..8f163a7
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.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 com.jetbrains.cidr.cpp.execution.testing.CMakeGoogleTestRunConfigurationProducer;
+import java.util.Collection;
+
+/** 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(CMakeGoogleTestRunConfigurationProducer.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/common/BUILD b/common/BUILD
new file mode 100644
index 0000000..fe756e1
--- /dev/null
+++ b/common/BUILD
@@ -0,0 +1 @@
+licenses(["notice"])  # Apache 2.0
diff --git a/common/binaryhelper/BUILD b/common/binaryhelper/BUILD
new file mode 100644
index 0000000..85ec9ff
--- /dev/null
+++ b/common/binaryhelper/BUILD
@@ -0,0 +1,11 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "binaryhelper",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java b/common/binaryhelper/src/com/google/idea/common/binaryhelper/HelperBinaryUtil.java
similarity index 63%
rename from base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
rename to common/binaryhelper/src/com/google/idea/common/binaryhelper/HelperBinaryUtil.java
index 49fb501..fb1be91 100644
--- a/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
+++ b/common/binaryhelper/src/com/google/idea/common/binaryhelper/HelperBinaryUtil.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.base.util;
+package com.google.idea.common.binaryhelper;
 
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.util.io.URLUtil;
@@ -25,34 +25,35 @@
 import java.nio.file.StandardCopyOption;
 import java.util.HashMap;
 import java.util.Map;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
-/** Extracts binaries from the resource section of the jar for execution */
-public final class BlazeHelperBinaryUtil {
+/** Binaries provided to IntelliJ at runtime */
+public final class HelperBinaryUtil {
 
-  private static final Logger LOG = Logger.getInstance(BlazeHelperBinaryUtil.class);
+  private static final Logger LOG = Logger.getInstance(HelperBinaryUtil.class);
 
-  private static final File tempDirectory = com.google.common.io.Files.createTempDir();
+  private static File tempDirectory;
   private static final Map<String, File> cachedFiles = new HashMap<>();
 
   @Nullable
-  public static synchronized File getBlazeHelperBinary(@NotNull String binaryName) {
+  public static synchronized File getHelperBinary(String binaryFilePath) {
+    // Assume the binaries have unique names. This saves having
+    // to recursively clean up directories
+    String binaryName = new File(binaryFilePath).getName();
+
     File file = cachedFiles.get(binaryName);
     if (file != null) {
       return file;
     }
-    file = new File(tempDirectory, binaryName);
-    File directory = file.getParentFile();
-
-    if (!directory.mkdirs()) {
-      LOG.error("Could not create temporary dir: " + directory);
-      return null;
+    if (tempDirectory == null) {
+      tempDirectory = com.google.common.io.Files.createTempDir();
+      tempDirectory.deleteOnExit();
     }
+    file = new File(tempDirectory, binaryName);
 
-    URL url = BlazeHelperBinaryUtil.class.getResource(binaryName);
+    URL url = HelperBinaryUtil.class.getResource(binaryFilePath);
     if (url == null) {
-      LOG.error(String.format("Blaze binary '%s' was not found", binaryName));
+      LOG.error(String.format("Helper binary '%s' was not found", binaryFilePath));
       return null;
     }
     try (InputStream inputStream = URLUtil.openResourceStream(url)) {
@@ -62,7 +63,7 @@
       cachedFiles.put(binaryName, file);
       return file;
     } catch (IOException e) {
-      LOG.error(String.format("Error loading blaze binary '%s'", binaryName));
+      LOG.error(String.format("Error loading helper binary '%s'", binaryFilePath));
       return null;
     }
   }
diff --git a/common/experiments/src/com/google/idea/common/experiments/ExperimentServiceImpl.java b/common/experiments/src/com/google/idea/common/experiments/ExperimentServiceImpl.java
index 6b908c1..62246fa 100644
--- a/common/experiments/src/com/google/idea/common/experiments/ExperimentServiceImpl.java
+++ b/common/experiments/src/com/google/idea/common/experiments/ExperimentServiceImpl.java
@@ -19,6 +19,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.components.ApplicationComponent;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.util.SystemProperties;
@@ -60,7 +61,9 @@
 
   @Override
   public void initComponent() {
-    services.forEach(ExperimentLoader::initialize);
+    if (!ApplicationManager.getApplication().isUnitTestMode()) {
+      services.forEach(ExperimentLoader::initialize);
+    }
   }
 
   @Override
diff --git a/cpp/src/META-INF/blaze-cpp.xml b/cpp/src/META-INF/blaze-cpp.xml
index 8b78eca..3c71cad 100644
--- a/cpp/src/META-INF/blaze-cpp.xml
+++ b/cpp/src/META-INF/blaze-cpp.xml
@@ -14,6 +14,9 @@
   ~ limitations under the License.
   -->
 <idea-plugin>
+  <depends>com.intellij.modules.cidr.lang</depends>
+  <depends>com.intellij.modules.cidr.debugger</depends>
+
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <SyncPlugin implementation="com.google.idea.blaze.cpp.BlazeCSyncPlugin"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.cpp.CPrefetchFileSource"/>
@@ -22,6 +25,7 @@
   <extensions defaultExtensionNs="cidr.lang">
     <languageKindHelper implementation="com.google.idea.blaze.cpp.BlazeLanguageKindCalculatorHelper"/>
     <autoImportHelper implementation="com.google.idea.blaze.cpp.BlazeCppAutoImportHelper"/>
+    <customHeaderProvider implementation="com.google.idea.blaze.cpp.BlazeCustomHeaderProvider"/>
   </extensions>
   <extensions defaultExtensionNs="com.intellij">
     <projectService serviceImplementation="com.google.idea.blaze.cpp.BlazeCWorkspace"/>
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
index 84fc646..e2c5a60 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -24,11 +24,13 @@
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.symbols.OCSymbol;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
 import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import java.io.File;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
@@ -70,12 +72,15 @@
     LOG.info(String.format("Blaze OCWorkspace update took: %d ms", (end - start)));
 
     ApplicationManager.getApplication()
-        .runReadAction(
+        .runWriteAction(
             () -> {
               if (project.isDisposed()) {
                 return;
               }
 
+              File genfilesDir = blazeProjectData.blazeRoots.getGenfilesDirectory();
+              LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(genfilesDir));
+
               // TODO(salguarnieri) Avoid bumping all of these trackers; figure out what has changed
               modTrackers.getProjectFilesListTracker().incModificationCount();
               modTrackers.getSourceFilesListTracker().incModificationCount();
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index 6204439..c9b7c37 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -145,7 +145,6 @@
       WorkspacePathResolver workspacePathResolver,
       BlazeProjectData blazeProjectData) {
     RuleKey ruleKey = rule.key;
-    LOG.info("Resolving " + ruleKey);
 
     CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(ruleKey);
     if (toolchainIdeInfo != null) {
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCustomHeaderProvider.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCustomHeaderProvider.java
new file mode 100644
index 0000000..9130e7e
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCustomHeaderProvider.java
@@ -0,0 +1,83 @@
+/*
+ * 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.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.jetbrains.cidr.lang.CustomHeaderProvider;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import java.io.File;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides a quick path to resolving non-generated includes, without file-system operations.
+ *
+ * <p>In typical projects, only a tiny fraction of includes are generated files (~0.1%), so handling
+ * non-generated files efficiently is very low hanging fruit.
+ *
+ * <p>Ideally our aspect would record which generated files are used, and we could avoid FS
+ * operations entirely.
+ */
+public class BlazeCustomHeaderProvider extends CustomHeaderProvider {
+
+  @Override
+  public boolean accepts(@Nullable OCResolveRootAndConfiguration configuration) {
+    return configuration != null
+        && configuration.getConfiguration() instanceof BlazeResolveConfiguration;
+  }
+
+  @Nullable
+  @Override
+  public VirtualFile getCustomHeaderFile(
+      String includeString,
+      HeaderSearchStage stage,
+      @Nullable OCResolveConfiguration configuration) {
+    if (stage != HeaderSearchStage.BEFORE_START
+        || includeString.startsWith("/")
+        || configuration == null) {
+      return null;
+    }
+    File file =
+        ((BlazeResolveConfiguration) configuration)
+            .getWorkspacePathResolver()
+            .resolveToFile(includeString);
+    return getFileSystem().findFileByIoFile(file);
+  }
+
+  @Nullable
+  @Override
+  public String provideSerializationPath(VirtualFile file) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public VirtualFile getCustomSerializedHeaderFile(
+      String serializationPath, Project project, VirtualFile currentFile) {
+    return null;
+  }
+
+  private static LocalFileSystem getFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
index ef7b9c0..a2a2b77 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
@@ -219,6 +219,10 @@
     return project;
   }
 
+  public WorkspacePathResolver getWorkspacePathResolver() {
+    return workspacePathResolver;
+  }
+
   @Override
   public String getDisplayName(boolean shorten) {
     return ruleKey.toString();
diff --git a/ijwb/BUILD b/ijwb/BUILD
index fdc10ee..c9524bd 100644
--- a/ijwb/BUILD
+++ b/ijwb/BUILD
@@ -10,6 +10,7 @@
     "merged_plugin_xml",
     "stamped_plugin_xml",
 )
+load("//:version.bzl", "VERSION")
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
@@ -37,7 +38,7 @@
     plugin_name = "IntelliJ with Bazel",
     plugin_xml = ":merged_plugin_xml",
     stamp_since_build = True,
-    version_file = "//:version",
+    version = VERSION,
 )
 
 java_library(
diff --git a/ijwb/src/META-INF/ijwb.xml b/ijwb/src/META-INF/ijwb.xml
index f81f4bf..b5fcb39 100644
--- a/ijwb/src/META-INF/ijwb.xml
+++ b/ijwb/src/META-INF/ijwb.xml
@@ -15,7 +15,6 @@
   -->
 <idea-plugin>
   <vendor>Google</vendor>
-  <depends>com.intellij.modules.java</depends>
 
   <extensions defaultExtensionNs="com.intellij">
     <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
diff --git a/ijwb/src/META-INF/ijwb_bazel.xml b/ijwb/src/META-INF/ijwb_bazel.xml
index 0c72d38..00430e9 100644
--- a/ijwb/src/META-INF/ijwb_bazel.xml
+++ b/ijwb/src/META-INF/ijwb_bazel.xml
@@ -17,7 +17,7 @@
 
   <description>
     <![CDATA[
-      <a href="http://bazel.io">Bazel</a> support for IntelliJ.
+      <a href="https://bazel.build">Bazel</a> support for IntelliJ.
 
       Features:
         <ul>
@@ -26,7 +26,7 @@
         <li>Support for Bazel run configurations for certain rule classes.</li>
         </ul>
 
-      Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
+      Usage instructions at <a href="https://ij.bazel.build">ij.bazel.build</a>
       ]]>
   </description>
 
@@ -36,4 +36,4 @@
     </component>
   </application-components>
 
-</idea-plugin>
\ No newline at end of file
+</idea-plugin>
diff --git a/intellij_platform_sdk/BUILD.android_studio b/intellij_platform_sdk/BUILD.android_studio
index 7a4839a..00edce8 100644
--- a/intellij_platform_sdk/BUILD.android_studio
+++ b/intellij_platform_sdk/BUILD.android_studio
@@ -29,6 +29,7 @@
         "android-studio/plugins/Groovy/lib/*.jar",
         "android-studio/plugins/java-i18n/lib/*.jar",
         "android-studio/plugins/junit/lib/*.jar",
+        "android-studio/plugins/ndk-workspace/lib/*.jar",
         "android-studio/plugins/properties/lib/*.jar",
     ]),
     tags = ["intellij-provided-by-sdk"],
diff --git a/java/BUILD b/java/BUILD
index 2655b20..80799c3 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -7,6 +7,7 @@
     deps = [
         "//base",
         "//common/experiments",
+        "//intellij_platform_sdk:junit",
         "//intellij_platform_sdk:plugin_api",
         "//proto_deps",
         "@jsr305_annotations//jar",
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index 157152f..937cb85 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -15,6 +15,7 @@
   -->
 <idea-plugin>
   <depends>JUnit</depends>
+  <depends>com.intellij.modules.java</depends>
 
   <actions>
     <action class="com.google.idea.blaze.java.libraries.ExcludeLibraryAction"
@@ -99,6 +100,12 @@
     <projectService serviceImplementation="com.google.idea.blaze.java.syncstatus.SyncStatusHelper"/>
   </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.java.JavaSyncAugmenter"
                     interface="com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter"/>
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
new file mode 100644
index 0000000..1c6fef8
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
@@ -0,0 +1,76 @@
+/*
+ * 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.producers;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+/** Utilities for building test filter flags for JUnit tests. */
+public final class BlazeJUnitTestFilterFlags {
+
+  /** A version of JUnit to generate test filter flags for. */
+  public enum JUnitVersion {
+    JUNIT_3,
+    JUNIT_4
+  }
+
+  public static String testFilterFlagForClass(String className, JUnitVersion jUnitVersion) {
+    return testFilterFlagForClassAndMethod(className, null, jUnitVersion);
+  }
+
+  public static String testFilterFlagForClassAndMethod(
+      String className, @Nullable String methodName, JUnitVersion jUnitVersion) {
+    StringBuilder output = new StringBuilder(BlazeFlags.TEST_FILTER);
+    output.append('=');
+    output.append(className);
+
+    if (!Strings.isNullOrEmpty(methodName)) {
+      output.append('#');
+      output.append(methodName);
+      // JUnit 4 test filters are regexes, and must be terminated to avoid matching
+      // unintended classes/methods. JUnit 3 test filters do not need or support this syntax.
+      if (jUnitVersion == JUnitVersion.JUNIT_4) {
+        output.append('$');
+      }
+    } else if (jUnitVersion == JUnitVersion.JUNIT_4) {
+      output.append('#');
+    }
+
+    return output.toString();
+  }
+
+  public static String testFilterFlagForClassAndMethods(
+      String className, Collection<String> methodNames, JUnitVersion jUnitVersion) {
+    if (methodNames.size() == 0) {
+      return testFilterFlagForClass(className, jUnitVersion);
+    } else if (methodNames.size() == 1) {
+      return testFilterFlagForClassAndMethod(
+          className, methodNames.iterator().next(), jUnitVersion);
+    }
+    String methodNamePattern;
+    if (jUnitVersion == JUnitVersion.JUNIT_4) {
+      methodNamePattern = String.format("(%s)", String.join("|", methodNames));
+    } else {
+      methodNamePattern = String.join(",", methodNames);
+    }
+    return testFilterFlagForClassAndMethod(className, methodNamePattern, jUnitVersion);
+  }
+
+  private BlazeJUnitTestFilterFlags() {}
+}
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 cd1764c..6a92166 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
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 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.BlazeJUnitTestFilterFlags.JUnitVersion;
 import com.intellij.execution.JavaExecutionUtil;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
@@ -34,6 +35,7 @@
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
 import java.util.List;
 import java.util.Objects;
 import org.jetbrains.annotations.NotNull;
@@ -67,6 +69,9 @@
     if (testClass == null) {
       return false;
     }
+    if (testClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
+      return false;
+    }
     sourceElement.set(testClass);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(testClass);
@@ -85,9 +90,11 @@
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
 
-    String qualifiedName = testClass.getQualifiedName();
+    final String qualifiedName = testClass.getQualifiedName();
     if (qualifiedName != null) {
-      flags.add(BlazeFlags.testFilterFlagForClass(qualifiedName));
+      final JUnitVersion jUnitVersion =
+          JUnitUtil.isJUnit4TestClass(testClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
+      flags.add(BlazeJUnitTestFilterFlags.testFilterFlagForClass(qualifiedName, jUnitVersion));
     }
 
     flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
@@ -142,6 +149,10 @@
     }
     List<String> flags = handlerState.getBlazeFlags();
 
-    return flags.contains(BlazeFlags.testFilterFlagForClass(testClass.getQualifiedName()));
+    final JUnitVersion jUnitVersion =
+        JUnitUtil.isJUnit4TestClass(testClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
+    return flags.contains(
+        BlazeJUnitTestFilterFlags.testFilterFlagForClass(
+            testClass.getQualifiedName(), jUnitVersion));
   }
 }
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 d10be31..419f979 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
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 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.BlazeJUnitTestFilterFlags.JUnitVersion;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.junit.JUnitUtil;
 import com.intellij.openapi.util.Ref;
@@ -162,9 +163,11 @@
     if (qualifiedName == null) {
       return null;
     }
-    final boolean isJUnit3Class = JUnitUtil.isJUnit3TestClass(containingClass);
+    final JUnitVersion jUnitVersion =
+        JUnitUtil.isJUnit4TestClass(containingClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
     final String testFilterFlag =
-        BlazeFlags.testFilterFlagForClassAndMethods(qualifiedName, methodNames, isJUnit3Class);
+        BlazeJUnitTestFilterFlags.testFilterFlagForClassAndMethods(
+            qualifiedName, methodNames, jUnitVersion);
 
     return new SelectedMethodInfo(firstMethod, containingClass, methodNames, testFilterFlag);
   }
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
new file mode 100644
index 0000000..7c4f4ca
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
@@ -0,0 +1,54 @@
+/*
+ * 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.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 java.util.Collection;
+
+/** 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(
+              // JUnit test producers
+              com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
+              com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
+              com.intellij.execution.junit.TestClassConfigurationProducer.class,
+              com.intellij.execution.junit.TestMethodConfigurationProducer.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/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
index 257dcab..088be73 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -33,6 +33,7 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.PerformanceWarning;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -114,7 +115,9 @@
       @Nullable SyncState previousSyncState) {
     JavaWorkingSet javaWorkingSet = null;
     if (workingSet != null) {
-      javaWorkingSet = new JavaWorkingSet(workspaceRoot, workingSet);
+      javaWorkingSet =
+          new JavaWorkingSet(
+              workspaceRoot, workingSet, Blaze.getBuildSystemProvider(project)::isBuildFile);
     }
 
     JavaSourceFilter sourceFilter =
@@ -165,7 +168,7 @@
   }
 
   @Override
-  public void updateSdk(
+  public void updateProjectSdk(
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
@@ -283,11 +286,6 @@
         JavaLanguageLevelSection.PARSER);
   }
 
-  @Override
-  public boolean requiresResolveIdeArtifacts() {
-    return true;
-  }
-
   /**
    * Looks at your jars for anything that seems to be a deploy jar and warns about it. This often
    * turns out to be a duplicate copy of all your application's code, so you don't want it in your
diff --git a/java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java b/java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java
index 0f40445..02c6881 100644
--- a/java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java
+++ b/java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Computes the working set of files of directories from source control.
@@ -41,13 +42,16 @@
   private final Set<String> modifiedBuildFileRelativePaths;
   private final Set<String> modifiedJavaFileRelativePaths;
 
-  public JavaWorkingSet(WorkspaceRoot workspaceRoot, WorkingSet workingSet) {
+  public JavaWorkingSet(
+      WorkspaceRoot workspaceRoot,
+      WorkingSet workingSet,
+      Predicate<String> buildFileNamePredicate) {
     Set<String> modifiedBuildFileRelativePaths = Sets.newHashSet();
     Set<String> modifiedJavaFileRelativePaths = Sets.newHashSet();
 
     for (WorkspacePath workspacePath :
         Iterables.concat(workingSet.addedFiles, workingSet.modifiedFiles)) {
-      if (workspaceRoot.fileForPath(workspacePath).getName().equals("BUILD")) {
+      if (buildFileNamePredicate.test(workspaceRoot.fileForPath(workspacePath).getName())) {
         modifiedBuildFileRelativePaths.add(workspacePath.relativePath());
       } else if (workspacePath.relativePath().endsWith(".java")) {
         modifiedJavaFileRelativePaths.add(workspacePath.relativePath());
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 74fc43f..352553b 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -21,39 +21,60 @@
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
 import java.util.Set;
 
 class SyncStatusHelper {
+
   static SyncStatusHelper getInstance(Project project) {
     return ServiceManager.getService(project, SyncStatusHelper.class);
   }
 
-  private Set<File> syncedJavaFiles = ImmutableSet.of();
+  private final Project project;
+  private Set<File> syncedJavaFiles = null;
+
+  SyncStatusHelper(Project project) {
+    this.project = project;
+  }
 
   boolean isUnsynced(VirtualFile virtualFile) {
     if (!virtualFile.isInLocalFileSystem()) {
       return false;
     }
+    if (ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(virtualFile) == null) {
+      return false;
+    }
+    if (syncedJavaFiles == null) {
+      syncedJavaFiles = refresh();
+    }
+    if (syncedJavaFiles == null) {
+      return false;
+    }
     File file = new File(virtualFile.getPath());
     return !syncedJavaFiles.contains(file);
   }
 
-  void refresh(BlazeProjectData blazeProjectData) {
+  Set<File> refresh() {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
     BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
-      return;
+      return null;
     }
     ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
-    syncedJavaFiles =
-        ImmutableSet.<File>builder()
-            .addAll(artifactLocationDecoder.decodeAll(syncData.importResult.javaSourceFiles))
-            .build();
+    return ImmutableSet.<File>builder()
+        .addAll(artifactLocationDecoder.decodeAll(syncData.importResult.javaSourceFiles))
+        .build();
   }
 
   static class UpdateSyncStatusMap extends SyncListener.Adapter {
@@ -65,7 +86,7 @@
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
         SyncResult syncResult) {
-      getInstance(project).refresh(blazeProjectData);
+      getInstance(project).syncedJavaFiles = null;
     }
   }
 }
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 c5461f1..f6e59f8 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
@@ -81,6 +81,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -1021,7 +1022,8 @@
             new WorkingSet(
                 ImmutableList.of(new WorkspacePath("java/apps/example/Test.java")),
                 ImmutableList.of(),
-                ImmutableList.of()));
+                ImmutableList.of()),
+            Predicate.isEqual("BUILD"));
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, ruleMapBuilder, projectView);
     assertThat(
@@ -1047,7 +1049,8 @@
     workingSet =
         new JavaWorkingSet(
             workspaceRoot,
-            new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()));
+            new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+            Predicate.isEqual("BUILD"));
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, ruleMapBuilder, projectView);
     assertThat(
@@ -1214,7 +1217,8 @@
     workingSet =
         new JavaWorkingSet(
             workspaceRoot,
-            new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()));
+            new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+            Predicate.isEqual("BUILD"));
 
     // First test - make sure that jdeps is working
     jdepsMap.put(
@@ -1233,7 +1237,8 @@
             new WorkingSet(
                 ImmutableList.of(new WorkspacePath("java/example/BUILD")),
                 ImmutableList.of(),
-                ImmutableList.of()));
+                ImmutableList.of()),
+            Predicate.isEqual("BUILD"));
 
     result = importWorkspace(workspaceRoot, ruleMapBuilder, projectView);
     errorCollector.assertNoIssues();
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 b4d13c7..93a27a3 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
@@ -63,7 +63,7 @@
   }
 
   @Override
-  public void updateSdk(
+  public void updateProjectSdk(
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
diff --git a/testing/test_defs.bzl b/testing/test_defs.bzl
index 38baa7c..9fdbd90 100644
--- a/testing/test_defs.bzl
+++ b/testing/test_defs.bzl
@@ -6,6 +6,62 @@
     "api_version_txt",
 )
 
+def _generate_test_suite_impl(ctx):
+  """Generates a JUnit4 test suite pulling in all the referenced classes.
+
+  Args:
+    ctx: the rule context
+  """
+  suite_class_name = ctx.label.name
+  lines = []
+  lines.append("package %s;" % ctx.attr.test_package_root)
+  lines.append("")
+  test_srcs = _get_test_srcs(ctx.attr.srcs)
+  test_classes = [_get_test_class(test_src, ctx.attr.test_package_root) for test_src in test_srcs]
+  class_rules = ctx.attr.class_rules
+  if (class_rules):
+    lines.append("import org.junit.ClassRule;")
+  lines.append("import org.junit.runner.RunWith;")
+  lines.append("import org.junit.runners.Suite;")
+  lines.append("")
+  for test_class in test_classes:
+    lines.append("import %s;" % test_class)
+  lines.append("")
+  lines.append("@RunWith(Suite.class)")
+  lines.append("@Suite.SuiteClasses({")
+  for test_class in test_classes:
+    lines.append("    %s.class," % test_class.split(".")[-1])
+  lines.append("})")
+  lines.append("public class %s {" % suite_class_name)
+  lines.append("")
+
+  i = 1
+  for class_rule in class_rules:
+    lines.append("@ClassRule")
+    lines.append("public static %s setupRule_%d = new %s();" % (class_rule, i, class_rule))
+    i += 1
+
+  lines.append("}")
+
+  contents = "\n".join(lines)
+  ctx.file_action(
+      output = ctx.outputs.out,
+      content = contents,
+  )
+
+_generate_test_suite = rule(
+    implementation = _generate_test_suite_impl,
+    attrs = {
+        # srcs for the test classes included in the suite (only keep those ending in Test.java)
+        "srcs": attr.label_list(allow_files=True, mandatory=True),
+        # the package string of the output test suite.
+        "test_package_root": attr.string(mandatory=True),
+        # optional list of classes to instantiate as a @ClassRule in the test suite.
+        "class_rules": attr.string_list()
+    },
+    outputs={"out": "%{name}.java"},
+)
+
 def intellij_unit_test_suite(name, srcs, test_package_root, **kwargs):
   """Creates a java_test rule comprising all valid test classes in the specified srcs.
 
@@ -17,14 +73,12 @@
     test_package_root: only tests under this package root will be run.
     **kwargs: Any other args to be passed to the java_test.
   """
-  test_srcs = [test for test in srcs if test.endswith("Test.java")]
-  test_classes = [_get_test_class(test_src, test_package_root) for test_src in test_srcs]
   suite_class_name = name + "TestSuite"
   suite_class = test_package_root + "." + suite_class_name
   _generate_test_suite(
       name = suite_class_name,
+      srcs = srcs,
       test_package_root = test_package_root,
-      test_classes = test_classes,
   )
   native.java_test(
       name = name,
@@ -62,15 +116,13 @@
         these plugins aren't loaded at runtime.
     **kwargs: Any other args to be passed to the java_test.
   """
-  test_srcs = [test for test in srcs if test.endswith("Test.java")]
-  test_classes = [_get_test_class(test_src, test_package_root) for test_src in test_srcs]
   suite_class_name = name + "TestSuite"
   suite_class = test_package_root + "." + suite_class_name
   _generate_test_suite(
       name = suite_class_name,
+      srcs = srcs,
       test_package_root = test_package_root,
-      test_classes = test_classes,
-      class_rules = ["com.google.idea.testing.BlazeTestSystemPropertiesRule"]
+      class_rules = ["com.google.idea.testing.BlazeTestSystemPropertiesRule"],
   )
 
   api_version_txt_name = name + "_api_version"
@@ -85,7 +137,7 @@
   runtime_deps = list(runtime_deps)
   runtime_deps.extend([
       "//intellij_platform_sdk:bundled_plugins",
-      "//third_party:jdk8_tools",
+      "//third_party:jpda-jdi",
   ])
 
   jvm_flags = list(jvm_flags)
@@ -111,56 +163,20 @@
       **kwargs
   )
 
-def _generate_test_suite(name, test_package_root, test_classes, class_rules = []):
-  """Generates a JUnit4 test suite pulling in all the referenced classes.
-
-  Args:
-    name: the name of the genrule and output test suite class.
-    test_package_root: the package string of the output test suite.
-    test_classes: the test classes included in the suite.
-    class_rules: optional list of classes to instantiate as a @ClassRule in the test suite.
-  """
-  lines = []
-  lines.append("package %s;" % test_package_root)
-  lines.append("")
-  if (class_rules):
-    lines.append("import org.junit.ClassRule;")
-  lines.append("import org.junit.runner.RunWith;")
-  lines.append("import org.junit.runners.Suite;")
-  lines.append("")
-  for test_class in test_classes:
-    lines.append("import %s;" % test_class)
-  lines.append("")
-  lines.append("@RunWith(Suite.class)")
-  lines.append("@Suite.SuiteClasses({")
-  for test_class in test_classes:
-    lines.append("    %s.class," % test_class.split(".")[-1])
-  lines.append("})")
-  lines.append("public class %s {" % name)
-  lines.append("")
-
-  i = 1
-  for class_rule in class_rules:
-    lines.append("@ClassRule")
-    lines.append("public static %s setupRule_%d = new %s();" % (class_rule, i, class_rule))
-    i += 1
-
-  lines.append("}")
-
-  contents = "\\n".join(lines)
-  native.genrule(
-      name = name,
-      cmd = "printf '%s' > $@" % contents,
-      outs = [name + ".java"],
-  )
-
 def _get_test_class(test_src, test_package_root):
   """Returns the package string of the test class, beginning with the given root."""
-  full_path = PACKAGE_NAME + "/" + test_src
-  temp = full_path[:-5]
+  test_path = test_src.short_path
+  temp = test_path[:-len(".java")]
   temp = temp.replace("/", ".")
   i = temp.rfind(test_package_root)
   if i < 0:
-    fail("Test source '%s' not under package root '%s'" % (full_path, test_package_root))
+    fail("Test source '%s' not under package root '%s'" % (test_path, test_package_root))
   test_class = temp[i:]
   return test_class
+
+def _get_test_srcs(targets):
+  """Returns all files of the given targets that end with Test.java."""
+  files = set()
+  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 d7eae12..04f13a6 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -1,8 +1,9 @@
 package(default_visibility = ["//visibility:public"])
 
+# The JDI parts of tools.jar.
 java_import(
-    name = "jdk8_tools",
-    jars = ["jdk8/tools.jar"],
+    name = "jpda-jdi",
+    jars = ["jdk8/jpda-jdi.jar"],
 )
 
 sh_binary(
diff --git a/third_party/jdk8/jpda-jdi.jar b/third_party/jdk8/jpda-jdi.jar
new file mode 100755
index 0000000..70c5058
--- /dev/null
+++ b/third_party/jdk8/jpda-jdi.jar
Binary files differ
diff --git a/third_party/jdk8/tools.jar b/third_party/jdk8/tools.jar
deleted file mode 100644
index f9f71d6..0000000
--- a/third_party/jdk8/tools.jar
+++ /dev/null
Binary files differ
diff --git a/version.bzl b/version.bzl
index 5cdb6a6..3775bd9 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "1.9.4"
+VERSION = "1.11.0"