| // 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.devtools.bazel.e4b.command; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Function; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.devtools.bazel.e4b.command.CommandConsole.CommandConsoleFactory; |
| |
| /** |
| * 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.5.0) |
| private static int[] MINIMUM_BAZEL_VERSION = {0, 5, 0}; |
| |
| private static enum ConsoleType { |
| NO_CONSOLE, SYSTEM, WORKSPACE |
| } |
| |
| private final BazelAspectLocation aspectLocation; |
| private final CommandConsoleFactory consoleFactory; |
| |
| private final List<String> buildOptions; |
| private final List<String> aspectOptions; |
| |
| private final Map<File, BazelInstance> instances = new HashMap<>(); |
| private String bazel = null; |
| |
| /** |
| * Create a {@link BazelCommand} object, providing the implementation for locating aspect and |
| * getting console streams. |
| */ |
| public BazelCommand(BazelAspectLocation aspectLocation, CommandConsoleFactory consoleFactory) { |
| this.aspectLocation = aspectLocation; |
| this.consoleFactory = consoleFactory; |
| this.buildOptions = ImmutableList.of("--watchfs", |
| "--override_repository=local_eclipse_aspect=" + aspectLocation.getWorkspaceDirectory(), |
| "--aspects=@local_eclipse_aspect" + aspectLocation.getAspectLabel()); |
| this.aspectOptions = ImmutableList.<String>builder().addAll(buildOptions).add("-k", |
| "--output_groups=ide-info-text,ide-resolve,-_,-defaults", "--experimental_show_artifacts") |
| .build(); |
| } |
| |
| private String getBazelPath() throws BazelNotFoundException { |
| if (bazel == null) { |
| throw new BazelNotFoundException.BazelNotSetException(); |
| } |
| return bazel; |
| } |
| |
| /** |
| * 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 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(consoleFactory).setConsoleName(null) |
| .setDirectory(aspectLocation.getWorkspaceDirectory()).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, BazelNotFoundException { |
| File workspaceRoot = getWorkspaceRoot(directory); |
| if (workspaceRoot == null) { |
| return null; |
| } |
| if (!instances.containsKey(workspaceRoot)) { |
| instances.put(workspaceRoot, new BazelInstance(workspaceRoot)); |
| } |
| return instances.get(workspaceRoot); |
| } |
| |
| /** |
| * An instance of the Bazel interface for a specific workspace. Provides means to query Bazel on |
| * this workspace. |
| */ |
| public class BazelInstance { |
| private final File workspaceRoot; |
| private final File execRoot; |
| |
| private final Map<String, ImmutableMap<String, IdeBuildInfo>> buildInfoCache = new HashMap<>(); |
| |
| private BazelInstance(File workspaceRoot) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| this.workspaceRoot = workspaceRoot; |
| this.execRoot = new File(String.join("", runBazel("info", "execution_root"))); |
| } |
| |
| /** |
| * 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, BazelNotFoundException { |
| StringBuilder builder = new StringBuilder(); |
| for (File f : directories) { |
| builder.append(f.toURI().relativize(workspaceRoot.toURI()).getPath()).append("/... "); |
| } |
| return runBazel("query", builder.toString()); |
| } |
| |
| private synchronized List<String> runBazel(String... args) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return runBazel(ImmutableList.<String>builder().add(args).build()); |
| } |
| |
| private synchronized List<String> runBazel(List<String> args) |
| 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, BazelNotFoundException { |
| return BazelCommand.this.runBazelAndGetErrorLines(ConsoleType.WORKSPACE, workspaceRoot, |
| ImmutableList.<String>builder().add("build").addAll(aspectOptions).addAll(targets) |
| .build(), |
| // Strip out the artifact list, keeping the e4b-build.json files. |
| t -> t.startsWith(">>>") ? (t.endsWith(".e4b-build.json") ? t.substring(3) : "") : null); |
| } |
| |
| /** |
| * Runs the analysis of the given list of targets using the IDE build information aspect and |
| * returns a map of {@link IdeBuildInfo}-s (key is the label of the target) containing the |
| * parsed form of the JSON file created by the aspect. |
| * |
| * <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 Map<String, IdeBuildInfo> getIdeInfo(Collection<String> targets) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| String key = NEW_LINE_JOINER.join(targets); |
| if (!buildInfoCache.containsKey(key)) { |
| buildInfoCache.put(key, IdeBuildInfo.getInfo(buildIdeInfo(targets))); |
| } |
| return buildInfoCache.get(key); |
| } |
| |
| /** |
| * Clear the IDE build information cache. This cache is filled upon request and never emptied |
| * unless we call that function. |
| * |
| * <p> |
| * This function totally clear the cache and that might leads to useless rebuilds when several |
| * eclipse project points to the same workspace but that is a rare case. |
| */ |
| public synchronized void markAsDirty() { |
| buildInfoCache.clear(); |
| } |
| |
| /** |
| * Build a list of targets in the current workspace. |
| * |
| * @throws BazelNotFoundException |
| */ |
| public synchronized int build(List<String> targets, String... extraArgs) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return BazelCommand.this.runBazel(workspaceRoot, ImmutableList.<String>builder().add("build") |
| .addAll(buildOptions).add(extraArgs).addAll(targets).build()); |
| } |
| |
| /** |
| * Build a list of targets in the current workspace. |
| * |
| * @throws BazelNotFoundException |
| */ |
| public synchronized int build(List<String> targets, List<String> extraArgs) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return BazelCommand.this.runBazel(workspaceRoot, ImmutableList.<String>builder().add("build") |
| .addAll(buildOptions).addAll(extraArgs).addAll(targets).build()); |
| } |
| |
| /** |
| * 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, BazelNotFoundException { |
| return BazelCommand.this.runBazel(workspaceRoot, ImmutableList.<String>builder().add("test") |
| .addAll(buildOptions).add(extraArgs).addAll(targets).build()); |
| } |
| |
| /** |
| * Returns the workspace root corresponding to this object. |
| */ |
| public File getWorkspaceRoot() { |
| return workspaceRoot; |
| } |
| |
| /** |
| * Returns the execution root of the current workspace. |
| */ |
| public File getExecRoot() { |
| return execRoot; |
| } |
| |
| /** |
| * 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 List<String> complete(String string) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| if (string.equals("/") || string.isEmpty()) { |
| return ImmutableList.of("//"); |
| } else if (string.contains(":")) { |
| // complete targets using `bazel query` |
| int idx = string.indexOf(':'); |
| final String packageName = string.substring(0, idx); |
| final String targetPrefix = string.substring(idx + 1); |
| ImmutableList.Builder<String> builder = ImmutableList.builder(); |
| builder.addAll( |
| BazelCommand.this.runBazelAndGetOuputLines(ConsoleType.NO_CONSOLE, workspaceRoot, |
| ImmutableList.<String>builder().add("query", packageName + ":*").build(), line -> { |
| int i = line.indexOf(':'); |
| String s = line.substring(i + 1); |
| return !s.isEmpty() && s.startsWith(targetPrefix) ? (packageName + ":" + s) |
| : null; |
| })); |
| if ("all".startsWith(targetPrefix)) { |
| builder.add(packageName + ":all"); |
| } |
| if ("*".startsWith(targetPrefix)) { |
| builder.add(packageName + ":*"); |
| } |
| return builder.build(); |
| } else { |
| // complete packages |
| int lastSlash = string.lastIndexOf('/'); |
| final String prefix = lastSlash > 0 ? string.substring(0, lastSlash + 1) : ""; |
| final String suffix = lastSlash > 0 ? string.substring(lastSlash + 1) : string; |
| final String directory = (prefix.isEmpty() || prefix.equals("//")) ? "" |
| : prefix.substring(string.startsWith("//") ? 2 : 0, prefix.length() - 1); |
| File file = directory.isEmpty() ? workspaceRoot : new File(workspaceRoot, directory); |
| ImmutableList.Builder<String> builder = ImmutableList.builder(); |
| File[] files = file.listFiles((f) -> { |
| // Only give directories whose name starts with suffix... |
| return f.getName().startsWith(suffix) && f.isDirectory() |
| // ...that does not start with '.'... |
| && !f.getName().startsWith(".") |
| // ...and is not a Bazel convenience link |
| && (!file.equals(workspaceRoot) || !f.getName().startsWith("bazel-")); |
| }); |
| if (files != null) { |
| for (File d : files) { |
| builder.add(prefix + d.getName() + "/"); |
| if (new File(d, "BUILD").exists()) { |
| builder.add(prefix + d.getName() + ":"); |
| } |
| } |
| } |
| if ("...".startsWith(suffix)) { |
| builder.add(prefix + "..."); |
| } |
| return builder.build(); |
| } |
| } |
| } |
| |
| private File getWorkspaceRoot(File directory) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| List<String> result = runBazelAndGetOuputLines(ConsoleType.SYSTEM, directory, |
| ImmutableList.of("info", "workspace")); |
| if (result.size() > 0) { |
| return new File(result.get(0)); |
| } |
| return null; |
| } |
| |
| private List<String> runBazelAndGetOuputLines(ConsoleType type, File directory, List<String> args) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return runBazelAndGetOuputLines(type, directory, args, (t) -> t); |
| } |
| |
| private synchronized List<String> runBazelAndGetOuputLines(ConsoleType type, File directory, |
| List<String> args, Function<String, String> selector) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| Command command = Command.builder(consoleFactory) |
| .setConsoleName(getConsoleName(type, directory)).setDirectory(directory) |
| .addArguments(getBazelPath()).addArguments(args).setStdoutLineSelector(selector).build(); |
| if (command.run() == 0) { |
| return command.getSelectedOutputLines(); |
| } |
| return ImmutableList.of(); |
| } |
| |
| private synchronized List<String> runBazelAndGetErrorLines(ConsoleType type, File directory, |
| List<String> args, Function<String, String> selector) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| Command command = Command.builder(consoleFactory) |
| .setConsoleName(getConsoleName(type, directory)).setDirectory(directory) |
| .addArguments(getBazelPath()).addArguments(args).setStderrLineSelector(selector).build(); |
| if (command.run() == 0) { |
| return command.getSelectedErrorLines(); |
| } |
| return ImmutableList.of(); |
| } |
| |
| private synchronized int runBazel(ConsoleType type, File directory, List<String> args, |
| OutputStream stdout, OutputStream stderr) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return Command.builder(consoleFactory).setConsoleName(getConsoleName(type, directory)) |
| .setDirectory(directory).addArguments(getBazelPath()).addArguments(args) |
| .setStandardOutput(stdout).setStandardError(stderr).build().run(); |
| } |
| |
| private int runBazel(File directory, List<String> args) |
| throws IOException, InterruptedException, BazelNotFoundException { |
| return runBazel(ConsoleType.WORKSPACE, directory, args, null, null); |
| } |
| |
| private String getConsoleName(ConsoleType type, File directory) { |
| switch (type) { |
| case SYSTEM: |
| return "Bazel [system]"; |
| case WORKSPACE: |
| return "Bazel [" + directory.toString() + "]"; |
| case NO_CONSOLE: |
| default: |
| return null; |
| } |
| } |
| |
| } |