| // Copyright 2014 Google Inc. 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.build.lib.util; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.Ordering; |
| |
| import java.io.File; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.Map; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Utility methods for describing command failures. |
| * See also the CommandUtils class. |
| * Unlike that one, this class does not depend on Command; |
| * instead, it just manipulates command lines represented as |
| * Collection<String>. |
| */ |
| public class CommandFailureUtils { |
| |
| // Interface that provides building blocks when describing command. |
| private interface DescribeCommandImpl { |
| void describeCommandBeginIsolate(StringBuilder message); |
| void describeCommandEndIsolate(StringBuilder message); |
| void describeCommandCwd(String cwd, StringBuilder message); |
| void describeCommandEnvPrefix(StringBuilder message); |
| void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry); |
| void describeCommandElement(StringBuilder message, String commandElement); |
| void describeCommandExec(StringBuilder message); |
| } |
| |
| private static final class LinuxDescribeCommandImpl implements DescribeCommandImpl { |
| |
| @Override |
| public void describeCommandBeginIsolate(StringBuilder message) { |
| message.append("("); |
| } |
| |
| @Override |
| public void describeCommandEndIsolate(StringBuilder message) { |
| message.append(")"); |
| } |
| |
| @Override |
| public void describeCommandCwd(String cwd, StringBuilder message) { |
| message.append("cd ").append(ShellEscaper.escapeString(cwd)).append(" && \\\n "); |
| } |
| |
| @Override |
| public void describeCommandEnvPrefix(StringBuilder message) { |
| message.append("env - \\\n "); |
| } |
| |
| @Override |
| public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) { |
| message.append(ShellEscaper.escapeString(entry.getKey())).append('=') |
| .append(ShellEscaper.escapeString(entry.getValue())).append(" \\\n "); |
| } |
| |
| @Override |
| public void describeCommandElement(StringBuilder message, String commandElement) { |
| message.append(ShellEscaper.escapeString(commandElement)); |
| } |
| |
| @Override |
| public void describeCommandExec(StringBuilder message) { |
| message.append("exec "); |
| } |
| } |
| |
| // TODO(bazel-team): (2010) Add proper escaping. We can't use ShellUtils.shellEscape() as it is |
| // incompatible with CMD.EXE syntax, but something else might be needed. |
| private static final class WindowsDescribeCommandImpl implements DescribeCommandImpl { |
| |
| @Override |
| public void describeCommandBeginIsolate(StringBuilder message) { |
| // TODO(bazel-team): Implement this. |
| } |
| |
| @Override |
| public void describeCommandEndIsolate(StringBuilder message) { |
| // TODO(bazel-team): Implement this. |
| } |
| |
| @Override |
| public void describeCommandCwd(String cwd, StringBuilder message) { |
| message.append("cd ").append(cwd).append("\n"); |
| } |
| |
| @Override |
| public void describeCommandEnvPrefix(StringBuilder message) { } |
| |
| @Override |
| public void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry) { |
| message.append("SET ").append(entry.getKey()).append('=') |
| .append(entry.getValue()).append("\n "); |
| } |
| |
| @Override |
| public void describeCommandElement(StringBuilder message, String commandElement) { |
| message.append(commandElement); |
| } |
| |
| @Override |
| public void describeCommandExec(StringBuilder message) { |
| // TODO(bazel-team): Implement this if possible for greater efficiency. |
| } |
| } |
| |
| private static final DescribeCommandImpl describeCommandImpl = |
| OS.getCurrent() == OS.WINDOWS ? new WindowsDescribeCommandImpl() |
| : new LinuxDescribeCommandImpl(); |
| |
| private CommandFailureUtils() {} // Prevent instantiation. |
| |
| private static Comparator<Map.Entry<String, String>> mapEntryComparator = |
| new Comparator<Map.Entry<String, String>>() { |
| @Override |
| public int compare(Map.Entry<String, String> x, Map.Entry<String, String> y) { |
| // A map can never have two keys with the same value, so we only need to compare the keys. |
| return x.getKey().compareTo(y.getKey()); |
| } |
| }; |
| |
| /** |
| * Construct a string that describes the command. |
| * Currently this returns a message of the form "foo bar baz", |
| * with shell meta-characters appropriately quoted and/or escaped, |
| * prefixed (if verbose is true) with an "env" command to set the environment. |
| * |
| * @param form Form of the command to generate; see the documentation of the |
| * {@link CommandDescriptionForm} values. |
| */ |
| public static String describeCommand(CommandDescriptionForm form, |
| Collection<String> commandLineElements, |
| @Nullable Map<String, String> environment, @Nullable String cwd) { |
| Preconditions.checkNotNull(form); |
| final int APPROXIMATE_MAXIMUM_MESSAGE_LENGTH = 200; |
| StringBuilder message = new StringBuilder(); |
| int size = commandLineElements.size(); |
| int numberRemaining = size; |
| if (form == CommandDescriptionForm.COMPLETE) { |
| describeCommandImpl.describeCommandBeginIsolate(message); |
| } |
| if (form != CommandDescriptionForm.ABBREVIATED) { |
| if (cwd != null) { |
| describeCommandImpl.describeCommandCwd(cwd, message); |
| } |
| /* |
| * On Linux, insert an "exec" keyword to save a fork in "blaze run" |
| * generated scripts. If we use "env" as a wrapper, the "exec" needs to |
| * be applied to the entire "env" invocation. |
| * |
| * On Windows, this is a no-op. |
| */ |
| describeCommandImpl.describeCommandExec(message); |
| /* |
| * Java does not provide any way to invoke a subprocess with the environment variables |
| * in a specified order. The order of environment variables in the 'environ' array |
| * (which is set by the 'envp' parameter to the execve() system call) |
| * is determined by the order of iteration on a HashMap constructed inside Java's |
| * ProcessBuilder class (in the ProcessEnvironment class), which is nondeterministic. |
| * |
| * Nevertheless, we *print* the environment variables here in sorted order, rather |
| * than in the potentially nondeterministic order that will be actually used. |
| * This is slightly dubious... in theory a process's behaviour could depend on the order |
| * of the environment variables passed to it. (For example, the order of environment |
| * variables in the environ array affects the output of '/usr/bin/env'.) |
| * However, in practice very few processes depend on the order of the environment |
| * variables, and using a deterministic sorted order here makes Blaze's output more |
| * deterministic and easier to read. So this seems the lesser of two evils... I think. |
| * Anyway, it's not like we have much choice... even if we wanted to, there's no way to |
| * print out the nondeterministic order that will actually be used, since there's |
| * no way to guarantee that the iteration over entrySet() here will return the same |
| * sequence as the iteration over entrySet() inside the ProcessBuilder class |
| * (in ProcessEnvironment.StringEnvironment.toEnvironmentBlock()). |
| */ |
| if (environment != null) { |
| describeCommandImpl.describeCommandEnvPrefix(message); |
| for (Map.Entry<String, String> entry : |
| Ordering.from(mapEntryComparator).sortedCopy(environment.entrySet())) { |
| message.append(" "); |
| describeCommandImpl.describeCommandEnvVar(message, entry); |
| } |
| } |
| } |
| for (String commandElement : commandLineElements) { |
| if (form == CommandDescriptionForm.ABBREVIATED && |
| message.length() + commandElement.length() > APPROXIMATE_MAXIMUM_MESSAGE_LENGTH) { |
| message.append( |
| " ... (remaining " + numberRemaining + " argument(s) skipped)"); |
| break; |
| } else { |
| if (numberRemaining < size) { |
| message.append(' '); |
| } |
| describeCommandImpl.describeCommandElement(message, commandElement); |
| numberRemaining--; |
| } |
| } |
| if (form == CommandDescriptionForm.COMPLETE) { |
| describeCommandImpl.describeCommandEndIsolate(message); |
| } |
| return message.toString(); |
| } |
| |
| /** |
| * Construct an error message that describes a failed command invocation. |
| * Currently this returns a message of the form "error executing command foo |
| * bar baz". |
| */ |
| public static String describeCommandError(boolean verbose, |
| Collection<String> commandLineElements, |
| Map<String, String> env, String cwd) { |
| CommandDescriptionForm form = verbose |
| ? CommandDescriptionForm.COMPLETE |
| : CommandDescriptionForm.ABBREVIATED; |
| return "error executing command " + (verbose ? "\n " : "") |
| + describeCommand(form, commandLineElements, env, cwd); |
| } |
| |
| /** |
| * Construct an error message that describes a failed command invocation. |
| * Currently this returns a message of the form "foo failed: error executing |
| * command /dir/foo bar baz". |
| */ |
| public static String describeCommandFailure(boolean verbose, |
| Collection<String> commandLineElements, |
| Map<String, String> env, String cwd) { |
| String commandName = commandLineElements.iterator().next(); |
| // Extract the part of the command name after the last "/", if any. |
| String shortCommandName = new File(commandName).getName(); |
| return shortCommandName + " failed: " + |
| describeCommandError(verbose, commandLineElements, env, cwd); |
| } |
| |
| } |