Add version detection for Bazel and more detailed error message for choosing bazel (#27) Fixes #3.
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/Activator.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/Activator.java index 69aa39a..a7ff2ad 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/Activator.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/Activator.java
@@ -31,6 +31,7 @@ import com.google.common.collect.ImmutableList; import com.google.devtools.bazel.e4b.command.BazelCommand; import com.google.devtools.bazel.e4b.command.BazelCommand.BazelInstance; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; /** * The activator class controls the plug-in life cycle @@ -122,9 +123,11 @@ /** * Return the {@link BazelInstance} corresponding to the given <code>project</code>. It looks for * the instance that runs for the workspace root configured for that project. + * + * @throws BazelNotFoundException */ public static BazelCommand.BazelInstance getBazelCommandInstance(IProject project) - throws BackingStoreException, IOException, InterruptedException { + throws BackingStoreException, IOException, InterruptedException, BazelNotFoundException { IScopeContext projectScope = new ProjectScope(project.getProject()); Preferences projectNode = projectScope.getNode(Activator.PLUGIN_ID); File workspaceRoot =
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/builder/BazelBuilder.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/builder/BazelBuilder.java index 2faa4c4..4c16fd4 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/builder/BazelBuilder.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/builder/BazelBuilder.java
@@ -26,6 +26,7 @@ import com.google.devtools.bazel.e4b.Activator; import com.google.devtools.bazel.e4b.command.BazelCommand.BazelInstance; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; public class BazelBuilder extends IncrementalProjectBuilder { @@ -46,6 +47,8 @@ instance.build(Activator.getTargets(project)); } catch (BackingStoreException | IOException | InterruptedException e) { Activator.error("Failed to build " + project.getName(), e); + } catch (BazelNotFoundException e) { + Activator.error("Bazel not found: " + e.getMessage()); } return null; }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainer.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainer.java index ec18b89..696122a 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainer.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainer.java
@@ -38,6 +38,7 @@ import com.google.common.collect.ImmutableMap; import com.google.devtools.bazel.e4b.Activator; import com.google.devtools.bazel.e4b.command.BazelCommand.BazelInstance; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; import com.google.devtools.bazel.e4b.command.IdeBuildInfo; import com.google.devtools.bazel.e4b.command.IdeBuildInfo.Jars; @@ -49,7 +50,8 @@ private final BazelInstance instance; public BazelClasspathContainer(IPath path, IJavaProject project) - throws IOException, InterruptedException, BackingStoreException, JavaModelException { + throws IOException, InterruptedException, BackingStoreException, JavaModelException, + BazelNotFoundException { this.path = path; this.project = project; this.instance = Activator.getBazelCommandInstance(project.getProject()); @@ -114,6 +116,9 @@ } catch (JavaModelException | BackingStoreException | IOException | InterruptedException e) { Activator.error("Unable to compute classpath containers entries.", e); return new IClasspathEntry[] {}; + } catch (BazelNotFoundException e) { + Activator.error("Bazel not found: " + e.getMessage()); + return new IClasspathEntry[] {}; } }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainerInitilalizer.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainerInitilalizer.java index 204b0dd..8c6ab96 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainerInitilalizer.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/classpath/BazelClasspathContainerInitilalizer.java
@@ -25,6 +25,7 @@ import org.osgi.service.prefs.BackingStoreException; import com.google.devtools.bazel.e4b.Activator; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; public class BazelClasspathContainerInitilalizer extends ClasspathContainerInitializer { @@ -40,6 +41,8 @@ } } catch (IOException | InterruptedException | BackingStoreException e) { Activator.error("Error while creating Bazel classpath container.", e); + } catch (BazelNotFoundException e) { + Activator.error("Bazel not found: " + e.getMessage()); } }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelCommand.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelCommand.java index ea6b3bd..89da270 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelCommand.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelCommand.java
@@ -14,6 +14,10 @@ package com.google.devtools.bazel.e4b.command; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.bazel.e4b.Activator; import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -23,21 +27,22 @@ import java.util.List; import java.util.Map; import java.util.function.Function; - +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.devtools.bazel.e4b.Activator; - /** * Main utility to call bazel commands, wrapping its input and output to the message console. */ public class BazelCommand { private static Joiner NEW_LINE_JOINER = Joiner.on("\n"); + private static Pattern VERSION_PATTERN = + Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)([^0-9].*)?$"); + + // Minimum bazel version needed to work with this plugin (currently 0.4.0) + private static int[] MINIMUM_BAZEL_VERSION = {0, 4, 0}; // Returns the path of the resources file from this plugin. private static File getAspectWorkspace() { @@ -63,20 +68,77 @@ .build(); private final Map<File, BazelInstance> instances = new HashMap<>(); - private String bazel = Activator.DEFAULT_BAZEL_PATH; + private String bazel = null; + + private String getBazelPath() throws BazelNotFoundException { + if (bazel == null) { + throw new BazelNotFoundException.BazelNotSetException(); + } + return bazel; + } /** - * Set the path to the Bazel binary (/usr/local/bin/bazel by default). + * Set the path to the Bazel binary. */ public synchronized void setBazelPath(String bazel) { this.bazel = bazel; } /** + * Check the version of Bazel: throws an exception if the version is incorrect or the path does + * not point to a Bazel binary. + */ + public static void checkVersion(String bazel) throws BazelNotFoundException { + File path = new File(bazel); + if (!path.exists() || !path.canExecute()) { + throw new BazelNotFoundException.BazelNotExecutableException(); + } + try { + Command command = Command.builder().setConsoleName(null).setDirectory(ASPECT_WORKSPACE) + .addArguments(bazel, "version") + .setStdoutLineSelector((s) -> s.startsWith("Build label:") ? s.substring(13) : null) + .build(); + if (command.run() != 0) { + throw new BazelNotFoundException.BazelNotExecutableException(); + } + List<String> result = command.getSelectedOutputLines(); + if (result.size() != 1) { + throw new BazelNotFoundException.BazelTooOldException("unknown"); + } + String version = result.get(0); + Matcher versionMatcher = VERSION_PATTERN.matcher(version); + if (versionMatcher == null || !versionMatcher.matches()) { + throw new BazelNotFoundException.BazelTooOldException(version); + } + int[] versionNumbers = {Integer.parseInt(versionMatcher.group(1)), + Integer.parseInt(versionMatcher.group(2)), Integer.parseInt(versionMatcher.group(3))}; + if (compareVersion(versionNumbers, MINIMUM_BAZEL_VERSION) < 0) { + throw new BazelNotFoundException.BazelTooOldException(version); + } + } catch (IOException | InterruptedException e) { + throw new BazelNotFoundException.BazelNotExecutableException(); + } + } + + private static int compareVersion(int[] version1, int[] version2) { + for (int i = 0; i < Math.min(version1.length, version2.length); i++) { + if (version1[i] < version2[i]) { + return -1; + } else if (version1[i] > version2[i]) { + return 1; + } + } + return Integer.compare(version1.length, version2.length); + } + + /** * Returns a {@link BazelInstance} for the given directory. It looks for the enclosing workspace * and returns the instance that correspond to it. If not in a workspace, returns null. + * + * @throws BazelNotFoundException */ - public BazelInstance getInstance(File directory) throws IOException, InterruptedException { + public BazelInstance getInstance(File directory) + throws IOException, InterruptedException, BazelNotFoundException { File workspaceRoot = getWorkspaceRoot(directory); if (workspaceRoot == null) { return null; @@ -98,7 +160,8 @@ private final Map<String, ImmutableMap<String, IdeBuildInfo>> buildInfoCache = new HashMap<>(); - private BazelInstance(File workspaceRoot) throws IOException, InterruptedException { + private BazelInstance(File workspaceRoot) + throws IOException, InterruptedException, BazelNotFoundException { this.workspaceRoot = workspaceRoot; this.packagePath = String.join("", runBazel("info", "package_path")) + ":" + ASPECT_WORKSPACE.toString(); @@ -107,9 +170,11 @@ /** * Returns the list of targets present in the BUILD files for the given sub-directories. + * + * @throws BazelNotFoundException */ public synchronized List<String> listTargets(File... directories) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { StringBuilder builder = new StringBuilder(); for (File f : directories) { builder.append(f.toURI().relativize(workspaceRoot.toURI()).getPath()).append("/... "); @@ -118,21 +183,23 @@ } private synchronized List<String> runBazel(String... args) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { return runBazel(ImmutableList.<String>builder().add(args).build()); } private synchronized List<String> runBazel(List<String> args) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { return BazelCommand.this.runBazelAndGetOuputLines(ConsoleType.WORKSPACE, workspaceRoot, args); } /** * Returns the IDE build information from running the aspect over the given list of targets. The * result is a list of of path to the output artifact created by the build. + * + * @throws BazelNotFoundException */ private synchronized List<String> buildIdeInfo(Collection<String> targets) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { return BazelCommand.this.runBazelAndGetErrorLines(ConsoleType.WORKSPACE, workspaceRoot, ImmutableList.<String>builder().add("build").add("--package_path", packagePath) .addAll(ASPECT_OPTIONS).addAll(targets).build(), @@ -148,9 +215,11 @@ * <p> * This method cache it results and won't recompute a previously computed version unless * {@link #markAsDirty()} has been called in between. + * + * @throws BazelNotFoundException */ public synchronized ImmutableMap<String, IdeBuildInfo> getIdeInfo(Collection<String> targets) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { String key = NEW_LINE_JOINER.join(targets); if (!buildInfoCache.containsKey(key)) { buildInfoCache.put(key, IdeBuildInfo.getInfo(buildIdeInfo(targets))); @@ -172,9 +241,11 @@ /** * Build a list of targets in the current workspace. + * + * @throws BazelNotFoundException */ public synchronized int build(List<String> targets, String... extraArgs) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { return BazelCommand.this.runBazel(workspaceRoot, ImmutableList.<String>builder().add("build", "--package_path", packagePath) .addAll(BUILD_OPTIONS).add(extraArgs).addAll(targets).build()); @@ -182,9 +253,11 @@ /** * Run test on a list of targets in the current workspace. + * + * @throws BazelNotFoundException */ public synchronized int tests(List<String> targets, String... extraArgs) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { return BazelCommand.this.runBazel(workspaceRoot, ImmutableList.<String>builder().add("test").add("--package_path", packagePath) .addAll(BUILD_OPTIONS).add(extraArgs).addAll(targets).build()); @@ -207,8 +280,11 @@ /** * Gives a list of target completions for the given beginning string. The result is the list of * possible completion for a target pattern starting with string. + * + * @throws BazelNotFoundException */ - public ImmutableList<String> complete(String string) throws IOException, InterruptedException { + public ImmutableList<String> complete(String string) + throws IOException, InterruptedException, BazelNotFoundException { if (string.equals("/") || string.isEmpty()) { return ImmutableList.of("//"); } else if (string.contains(":")) { @@ -267,7 +343,8 @@ } } - private File getWorkspaceRoot(File directory) throws IOException, InterruptedException { + private File getWorkspaceRoot(File directory) + throws IOException, InterruptedException, BazelNotFoundException { List<String> result = runBazelAndGetOuputLines(ConsoleType.SYSTEM, directory, ImmutableList.of("info", "workspace")); if (result.size() > 0) { @@ -277,15 +354,15 @@ } private ImmutableList<String> runBazelAndGetOuputLines(ConsoleType type, File directory, - List<String> args) throws IOException, InterruptedException { + List<String> args) throws IOException, InterruptedException, BazelNotFoundException { return runBazelAndGetOuputLines(type, directory, args, (t) -> t); } private synchronized ImmutableList<String> runBazelAndGetOuputLines(ConsoleType type, File directory, List<String> args, Function<String, String> selector) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { Command command = Command.builder().setConsoleName(getConsoleName(type, directory)) - .setDirectory(directory).addArguments(bazel.toString()).addArguments(args) + .setDirectory(directory).addArguments(getBazelPath()).addArguments(args) .setStdoutLineSelector(selector).build(); if (command.run() == 0) { return command.getSelectedOutputLines(); @@ -295,9 +372,9 @@ private synchronized ImmutableList<String> runBazelAndGetErrorLines(ConsoleType type, File directory, List<String> args, Function<String, String> selector) - throws IOException, InterruptedException { + throws IOException, InterruptedException, BazelNotFoundException { Command command = Command.builder().setConsoleName(getConsoleName(type, directory)) - .setDirectory(directory).addArguments(bazel.toString()).addArguments(args) + .setDirectory(directory).addArguments(getBazelPath()).addArguments(args) .setStderrLineSelector(selector).build(); if (command.run() == 0) { return command.getSelectedErrorLines(); @@ -306,13 +383,15 @@ } private synchronized int runBazel(ConsoleType type, File directory, List<String> args, - OutputStream stdout, OutputStream stderr) throws IOException, InterruptedException { + OutputStream stdout, OutputStream stderr) + throws IOException, InterruptedException, BazelNotFoundException { return Command.builder().setConsoleName(getConsoleName(type, directory)).setDirectory(directory) - .addArguments(bazel.toString()).addArguments(args).setStandardOutput(stdout) + .addArguments(getBazelPath()).addArguments(args).setStandardOutput(stdout) .setStandardError(stderr).build().run(); } - private int runBazel(File directory, List<String> args) throws IOException, InterruptedException { + private int runBazel(File directory, List<String> args) + throws IOException, InterruptedException, BazelNotFoundException { return runBazel(ConsoleType.WORKSPACE, directory, args, null, null); }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelNotFoundException.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelNotFoundException.java new file mode 100644 index 0000000..e67bc20 --- /dev/null +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/command/BazelNotFoundException.java
@@ -0,0 +1,36 @@ +package com.google.devtools.bazel.e4b.command; + +public class BazelNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + private BazelNotFoundException(String msg) { + super(msg); + } + + public static final class BazelNotSetException extends BazelNotFoundException { + private static final long serialVersionUID = 1L; + + public BazelNotSetException() { + super("Path to Bazel binary is not set, please set it " + + "(Preferences... > Bazel Plugins Preferences)"); + } + } + + public static final class BazelNotExecutableException extends BazelNotFoundException { + private static final long serialVersionUID = 1L; + + public BazelNotExecutableException() { + super("Path to Bazel is wrong (does not point to a binary), please set it " + + "(Preferences... > Bazel Plugins Preferences)"); + } + } + + public static final class BazelTooOldException extends BazelNotFoundException { + private static final long serialVersionUID = 1L; + + public BazelTooOldException(String version) { + super("Bazel version (" + version + ") is unsupported (too old or development version), " + + "please update your Bazel binary."); + } + } +}
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/preferences/BazelPreferencePage.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/preferences/BazelPreferencePage.java index 1619e80..02ffad9 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/preferences/BazelPreferencePage.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/preferences/BazelPreferencePage.java
@@ -14,13 +14,15 @@ package com.google.devtools.bazel.e4b.preferences; +import com.google.devtools.bazel.e4b.Activator; +import com.google.devtools.bazel.e4b.command.BazelCommand; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.preference.FileFieldEditor; +import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; -import com.google.devtools.bazel.e4b.Activator; - /** * Page to configure the e4b plugin. The only configuration parameter is the path to the Bazel * binary so this page provide a file field to specify it. @@ -29,13 +31,30 @@ implements IWorkbenchPreferencePage { + private static class BazelBinaryFieldEditor extends FileFieldEditor { + BazelBinaryFieldEditor(Composite parent) { + super("BAZEL_PATH", "Path to the &Bazel binary:", true, parent); + setValidateStrategy(VALIDATE_ON_KEY_STROKE); + } + + @Override + protected boolean doCheckState() { + try { + BazelCommand.checkVersion(getTextControl().getText()); + return true; + } catch (BazelNotFoundException e) { + setErrorMessage(e.getMessage()); + return false; + } + } + } + public BazelPreferencePage() { super(GRID); } public void createFieldEditors() { - addField(new FileFieldEditor("BAZEL_PATH", "Path to the &Bazel binary:", true, - getFieldEditorParent())); + addField(new BazelBinaryFieldEditor(getFieldEditorParent())); } @Override @@ -43,5 +62,4 @@ setPreferenceStore(Activator.getDefault().getPreferenceStore()); setDescription("Bazel plugin settings"); } - }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/BazelTargetCompletionContentProposalProvider.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/BazelTargetCompletionContentProposalProvider.java index ef69e60..a67eaf9 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/BazelTargetCompletionContentProposalProvider.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/BazelTargetCompletionContentProposalProvider.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.devtools.bazel.e4b.Activator; import com.google.devtools.bazel.e4b.command.BazelCommand.BazelInstance; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; /** * A {@link IContentProposalProvider} to provide completion for Bazel. Use the @@ -53,6 +54,8 @@ Activator.error("Failed to run Bazel to get completion information", e); } catch (InterruptedException e) { Activator.error("Bazel was interrupted", e); + } catch (BazelNotFoundException e) { + Activator.error("Bazel not found: " + e.getMessage()); } return null; }
diff --git a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/WorkspaceWizardPage.java b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/WorkspaceWizardPage.java index 350e596..bb4da5b 100644 --- a/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/WorkspaceWizardPage.java +++ b/com.google.devtools.bazel.e4b/src/com/google/devtools/bazel/e4b/wizard/WorkspaceWizardPage.java
@@ -42,6 +42,7 @@ import com.google.common.collect.ImmutableList; import com.google.devtools.bazel.e4b.Activator; +import com.google.devtools.bazel.e4b.command.BazelNotFoundException; /** * This is a quick wizard page that ask the user for the various targets and source path he wants to @@ -167,6 +168,8 @@ getWorkspaceRoot() + " does not seems to be a Bazel workspace"); } catch (InterruptedException e1) { Activator.error("Bazel was interrupted", e1); + } catch (BazelNotFoundException e1) { + MessageDialog.openError(getShell(), "Error", "Cannot found Bazel: " + e1.getMessage()); } }