| // Copyright 2014 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.build.lib.util; | |
| import static java.util.Map.Entry.comparingByKey; | |
| import com.google.common.annotations.VisibleForTesting; | |
| 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, boolean isolated); | |
| void describeCommandEnvVar(StringBuilder message, Map.Entry<String, String> entry); | |
| /** | |
| * Formats the command element and adds it to the message. | |
| * | |
| * @param message the message to modify | |
| * @param commandElement the command element to be added to the message | |
| * @param isBinary is true if the `commandElement` is the binary to be executed | |
| */ | |
| void describeCommandElement(StringBuilder message, String commandElement, boolean isBinary); | |
| 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, boolean isolated) { | |
| message.append(isolated | |
| ? "env - \\\n " | |
| : "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, boolean isBinary) { | |
| 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("/d ").append(cwd).append("\n"); | |
| } | |
| @Override | |
| public void describeCommandEnvPrefix(StringBuilder message, boolean isolated) { } | |
| @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, boolean isBinary) { | |
| // Replace the forward slashes with back slashes if the `commandElement` is the binary path | |
| message.append(isBinary ? commandElement.replace('/', '\\') : 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 static final int APPROXIMATE_MAXIMUM_MESSAGE_LENGTH = 200; | |
| private CommandFailureUtils() {} // Prevent instantiation. | |
| /** | |
| * 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, | |
| boolean prettyPrintArgs, | |
| Collection<String> commandLineElements, | |
| @Nullable Map<String, String> environment, | |
| @Nullable String cwd, | |
| @Nullable String configurationChecksum, | |
| @Nullable String executionPlatformAsLabelString) { | |
| Preconditions.checkNotNull(form); | |
| 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, form != CommandDescriptionForm.COMPLETE_UNISOLATED); | |
| // A map can never have two keys with the same value, so we only need to compare the keys. | |
| Comparator<Map.Entry<String, String>> mapEntryComparator = comparingByKey(); | |
| for (Map.Entry<String, String> entry : | |
| Ordering.from(mapEntryComparator).sortedCopy(environment.entrySet())) { | |
| message.append(" "); | |
| describeCommandImpl.describeCommandEnvVar(message, entry); | |
| } | |
| } | |
| } | |
| boolean isFirstArgument = true; | |
| for (String commandElement : commandLineElements) { | |
| if (form == CommandDescriptionForm.ABBREVIATED | |
| && message.length() + commandElement.length() > APPROXIMATE_MAXIMUM_MESSAGE_LENGTH) { | |
| message | |
| .append(" ... (remaining ") | |
| .append(numberRemaining) | |
| .append(numberRemaining == 1 ? " argument" : " arguments") | |
| .append(" skipped)"); | |
| break; | |
| } else { | |
| if (numberRemaining < size) { | |
| message.append(prettyPrintArgs ? " \\\n " : " "); | |
| } | |
| describeCommandImpl.describeCommandElement(message, commandElement, isFirstArgument); | |
| numberRemaining--; | |
| } | |
| isFirstArgument = false; | |
| } | |
| if (form == CommandDescriptionForm.COMPLETE) { | |
| describeCommandImpl.describeCommandEndIsolate(message); | |
| } | |
| if (form == CommandDescriptionForm.COMPLETE) { | |
| if (configurationChecksum != null) { | |
| message.append("\n"); | |
| message.append("# Configuration: ").append(configurationChecksum); | |
| } | |
| if (executionPlatformAsLabelString != null) { | |
| message.append("\n"); | |
| message.append("# Execution platform: ").append(executionPlatformAsLabelString); | |
| } | |
| } | |
| return message.toString(); | |
| } | |
| /** | |
| * 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". | |
| */ | |
| @VisibleForTesting | |
| static String describeCommandFailure( | |
| boolean verbose, | |
| Collection<String> commandLineElements, | |
| Map<String, String> env, | |
| @Nullable String cwd, | |
| @Nullable String configurationChecksum, | |
| @Nullable String targetLabel, | |
| @Nullable String executionPlatformAsLabelString) { | |
| String commandName = commandLineElements.iterator().next(); | |
| // Extract the part of the command name after the last "/", if any. | |
| String shortCommandName = new File(commandName).getName(); | |
| CommandDescriptionForm form = verbose | |
| ? CommandDescriptionForm.COMPLETE | |
| : CommandDescriptionForm.ABBREVIATED; | |
| StringBuilder output = new StringBuilder(); | |
| output.append("error executing command "); | |
| if (targetLabel != null) { | |
| output.append("(from target ").append(targetLabel).append(") "); | |
| } | |
| if (verbose) { | |
| output.append("\n "); | |
| } | |
| output.append( | |
| describeCommand( | |
| form, | |
| /* prettyPrintArgs= */ false, | |
| commandLineElements, | |
| env, | |
| cwd, | |
| configurationChecksum, | |
| executionPlatformAsLabelString)); | |
| return shortCommandName + " failed: " + output; | |
| } | |
| public static String describeCommandFailure( | |
| boolean verboseFailures, @Nullable String cwd, DescribableExecutionUnit command) { | |
| return describeCommandFailure( | |
| verboseFailures, | |
| command.getArguments(), | |
| command.getEnvironment(), | |
| cwd, | |
| command.getConfigurationChecksum(), | |
| command.getTargetLabel(), | |
| command.getExecutionPlatformLabelString()); | |
| } | |
| } |