// 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 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); } }