| // 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 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; | |
| 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; | |
| /** | |
| * 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}; | |
| 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", "--aspects=" + 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 String packagePath; | |
| 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.packagePath = String.join("", runBazel("info", "package_path")) + ":" | |
| + aspectLocation.getWorkspaceDirectory().toString(); | |
| 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").add("--package_path", packagePath) | |
| .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", "--package_path", packagePath) | |
| .addAll(buildOptions).add(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").add("--package_path", packagePath) | |
| .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; | |
| } | |
| } | |
| } |