|  | // 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 com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR; | 
|  |  | 
|  | import com.google.common.annotations.VisibleForTesting; | 
|  | import com.google.common.base.CharMatcher; | 
|  | import com.google.common.base.Joiner; | 
|  | import com.google.common.base.Splitter; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.Iterables; | 
|  | import com.google.common.collect.Lists; | 
|  | import com.google.devtools.build.lib.shell.Command; | 
|  | import com.google.devtools.build.lib.vfs.Path; | 
|  |  | 
|  | import java.io.File; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.HashMap; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  |  | 
|  | /** | 
|  | * Implements OS aware {@link Command} builder. At this point only Linux, Mac | 
|  | * and Windows XP are supported. | 
|  | * | 
|  | * <p>Builder will also apply heuristic to identify trivial cases where | 
|  | * unix-like command lines could be automatically converted into the | 
|  | * Windows-compatible form. | 
|  | * | 
|  | * <p>TODO(bazel-team): (2010) Some of the code here is very similar to the | 
|  | * {@link com.google.devtools.build.lib.shell.Shell} class. This should be looked at. | 
|  | */ | 
|  | public final class CommandBuilder { | 
|  |  | 
|  | private static final List<String> SHELLS = ImmutableList.of("/bin/sh", "/bin/bash"); | 
|  |  | 
|  | private static final Splitter ARGV_SPLITTER = Splitter.on(CharMatcher.anyOf(" \t")); | 
|  |  | 
|  | private final OS system; | 
|  | private final List<String> argv = new ArrayList<>(); | 
|  | private final Map<String, String> env = new HashMap<>(); | 
|  | private File workingDir = null; | 
|  | private boolean useShell = false; | 
|  |  | 
|  | public CommandBuilder() { | 
|  | this(OS.getCurrent()); | 
|  | } | 
|  |  | 
|  | @VisibleForTesting | 
|  | CommandBuilder(OS system) { | 
|  | this.system = system; | 
|  | } | 
|  |  | 
|  | public CommandBuilder addArg(String arg) { | 
|  | Preconditions.checkNotNull(arg, "Argument must not be null"); | 
|  | argv.add(arg); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder addArgs(Iterable<String> args) { | 
|  | Preconditions.checkArgument(!Iterables.contains(args, null), "Arguments must not be null"); | 
|  | Iterables.addAll(argv, args); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder addArgs(String... args) { | 
|  | return addArgs(Arrays.asList(args)); | 
|  | } | 
|  |  | 
|  | public CommandBuilder addEnv(Map<String, String> env) { | 
|  | Preconditions.checkNotNull(env); | 
|  | this.env.putAll(env); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder emptyEnv() { | 
|  | env.clear(); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder setEnv(Map<String, String> env) { | 
|  | emptyEnv(); | 
|  | addEnv(env); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder setWorkingDir(Path path) { | 
|  | Preconditions.checkNotNull(path); | 
|  | workingDir = path.getPathFile(); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder useTempDir() { | 
|  | workingDir = new File(JAVA_IO_TMPDIR.value()); | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public CommandBuilder useShell(boolean useShell) { | 
|  | this.useShell = useShell; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | private boolean argvStartsWithSh() { | 
|  | return argv.size() >= 2 && SHELLS.contains(argv.get(0)) && "-c".equals(argv.get(1)); | 
|  | } | 
|  |  | 
|  | private String[] transformArgvForLinux() { | 
|  | // If command line already starts with "/bin/sh -c", ignore useShell attribute. | 
|  | if (useShell && !argvStartsWithSh()) { | 
|  | // c.g.io.base.shell.Shell.shellify() actually concatenates argv into the space-separated | 
|  | // string here. Not sure why, but we will do the same. | 
|  | return new String[] { "/bin/sh", "-c", Joiner.on(' ').join(argv) }; | 
|  | } | 
|  | return argv.toArray(new String[argv.size()]); | 
|  | } | 
|  |  | 
|  | private String[] transformArgvForWindows() { | 
|  | List<String> modifiedArgv; | 
|  | // Heuristic: replace "/bin/sh -c" with something more appropriate for Windows. | 
|  | if (argvStartsWithSh()) { | 
|  | useShell = true; | 
|  | modifiedArgv = Lists.newArrayList(argv.subList(2, argv.size())); | 
|  | } else { | 
|  | modifiedArgv = Lists.newArrayList(argv); | 
|  | } | 
|  |  | 
|  | if (!modifiedArgv.isEmpty()) { | 
|  | // args can contain whitespace, so figure out the first word | 
|  | String argv0 = modifiedArgv.get(0); | 
|  | String command = ARGV_SPLITTER.split(argv0).iterator().next(); | 
|  |  | 
|  | // Automatically enable CMD.EXE use if we are executing something else besides "*.exe" file. | 
|  | if (!command.toLowerCase().endsWith(".exe")) { | 
|  | useShell = true; | 
|  | } | 
|  | } else { | 
|  | // This is degenerate "/bin/sh -c" case. We ensure that Windows behavior is identical | 
|  | // to the Linux - call shell that will do nothing. | 
|  | useShell = true; | 
|  | } | 
|  | if (useShell) { | 
|  | // /S - strip first and last quotes and execute everything else as is. | 
|  | // /E:ON - enable extended command set. | 
|  | // /V:ON - enable delayed variable expansion | 
|  | // /D - ignore AutoRun registry entries. | 
|  | // /C - execute command. This must be the last option before the command itself. | 
|  | return new String[] { "CMD.EXE", "/S", "/E:ON", "/V:ON", "/D", "/C", | 
|  | "\"" + Joiner.on(' ').join(modifiedArgv) + "\"" }; | 
|  | } else { | 
|  | return modifiedArgv.toArray(new String[argv.size()]); | 
|  | } | 
|  | } | 
|  |  | 
|  | public Command build() { | 
|  | Preconditions.checkState(system != OS.UNKNOWN, "Unidentified operating system"); | 
|  | Preconditions.checkNotNull(workingDir, "Working directory must be set"); | 
|  | Preconditions.checkState(!argv.isEmpty(), "At least one argument is expected"); | 
|  |  | 
|  | return new Command( | 
|  | system == OS.WINDOWS ? transformArgvForWindows() : transformArgvForLinux(), | 
|  | env, workingDir); | 
|  | } | 
|  | } |