blob: 0a1d72c4c6eab060787d852583513289b3e9e276 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.util;
16
Laurent Le Brun54733f92015-09-03 13:59:44 +000017import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
18
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010019import com.google.common.annotations.VisibleForTesting;
20import com.google.common.base.CharMatcher;
21import com.google.common.base.Joiner;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010022import com.google.common.base.Splitter;
23import com.google.common.collect.ImmutableList;
24import com.google.common.collect.Iterables;
25import com.google.common.collect.Lists;
26import com.google.devtools.build.lib.shell.Command;
27import com.google.devtools.build.lib.vfs.Path;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010028import java.io.File;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.HashMap;
32import java.util.List;
33import java.util.Map;
34
35/**
36 * Implements OS aware {@link Command} builder. At this point only Linux, Mac
37 * and Windows XP are supported.
38 *
39 * <p>Builder will also apply heuristic to identify trivial cases where
40 * unix-like command lines could be automatically converted into the
41 * Windows-compatible form.
42 *
43 * <p>TODO(bazel-team): (2010) Some of the code here is very similar to the
44 * {@link com.google.devtools.build.lib.shell.Shell} class. This should be looked at.
45 */
46public final class CommandBuilder {
47
Googler3786a192017-03-14 11:07:42 +000048 private static final ImmutableList<String> SHELLS = ImmutableList.of("/bin/sh", "/bin/bash");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010049
50 private static final Splitter ARGV_SPLITTER = Splitter.on(CharMatcher.anyOf(" \t"));
51
52 private final OS system;
53 private final List<String> argv = new ArrayList<>();
54 private final Map<String, String> env = new HashMap<>();
55 private File workingDir = null;
56 private boolean useShell = false;
57
58 public CommandBuilder() {
59 this(OS.getCurrent());
60 }
61
62 @VisibleForTesting
63 CommandBuilder(OS system) {
64 this.system = system;
65 }
66
67 public CommandBuilder addArg(String arg) {
68 Preconditions.checkNotNull(arg, "Argument must not be null");
69 argv.add(arg);
70 return this;
71 }
72
73 public CommandBuilder addArgs(Iterable<String> args) {
74 Preconditions.checkArgument(!Iterables.contains(args, null), "Arguments must not be null");
75 Iterables.addAll(argv, args);
76 return this;
77 }
78
79 public CommandBuilder addArgs(String... args) {
80 return addArgs(Arrays.asList(args));
81 }
82
83 public CommandBuilder addEnv(Map<String, String> env) {
84 Preconditions.checkNotNull(env);
85 this.env.putAll(env);
86 return this;
87 }
88
89 public CommandBuilder emptyEnv() {
90 env.clear();
91 return this;
92 }
93
94 public CommandBuilder setEnv(Map<String, String> env) {
95 emptyEnv();
96 addEnv(env);
97 return this;
98 }
99
100 public CommandBuilder setWorkingDir(Path path) {
101 Preconditions.checkNotNull(path);
102 workingDir = path.getPathFile();
103 return this;
104 }
105
106 public CommandBuilder useTempDir() {
Laurent Le Brun54733f92015-09-03 13:59:44 +0000107 workingDir = new File(JAVA_IO_TMPDIR.value());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100108 return this;
109 }
110
111 public CommandBuilder useShell(boolean useShell) {
112 this.useShell = useShell;
113 return this;
114 }
115
116 private boolean argvStartsWithSh() {
117 return argv.size() >= 2 && SHELLS.contains(argv.get(0)) && "-c".equals(argv.get(1));
118 }
119
120 private String[] transformArgvForLinux() {
121 // If command line already starts with "/bin/sh -c", ignore useShell attribute.
122 if (useShell && !argvStartsWithSh()) {
123 // c.g.io.base.shell.Shell.shellify() actually concatenates argv into the space-separated
124 // string here. Not sure why, but we will do the same.
125 return new String[] { "/bin/sh", "-c", Joiner.on(' ').join(argv) };
126 }
127 return argv.toArray(new String[argv.size()]);
128 }
129
130 private String[] transformArgvForWindows() {
131 List<String> modifiedArgv;
132 // Heuristic: replace "/bin/sh -c" with something more appropriate for Windows.
133 if (argvStartsWithSh()) {
134 useShell = true;
135 modifiedArgv = Lists.newArrayList(argv.subList(2, argv.size()));
136 } else {
137 modifiedArgv = Lists.newArrayList(argv);
138 }
139
140 if (!modifiedArgv.isEmpty()) {
141 // args can contain whitespace, so figure out the first word
142 String argv0 = modifiedArgv.get(0);
143 String command = ARGV_SPLITTER.split(argv0).iterator().next();
144
145 // Automatically enable CMD.EXE use if we are executing something else besides "*.exe" file.
146 if (!command.toLowerCase().endsWith(".exe")) {
147 useShell = true;
148 }
149 } else {
150 // This is degenerate "/bin/sh -c" case. We ensure that Windows behavior is identical
151 // to the Linux - call shell that will do nothing.
152 useShell = true;
153 }
154 if (useShell) {
155 // /S - strip first and last quotes and execute everything else as is.
156 // /E:ON - enable extended command set.
157 // /V:ON - enable delayed variable expansion
158 // /D - ignore AutoRun registry entries.
159 // /C - execute command. This must be the last option before the command itself.
160 return new String[] { "CMD.EXE", "/S", "/E:ON", "/V:ON", "/D", "/C",
161 "\"" + Joiner.on(' ').join(modifiedArgv) + "\"" };
162 } else {
163 return modifiedArgv.toArray(new String[argv.size()]);
164 }
165 }
166
167 public Command build() {
168 Preconditions.checkState(system != OS.UNKNOWN, "Unidentified operating system");
169 Preconditions.checkNotNull(workingDir, "Working directory must be set");
Ulf Adams07dba942015-03-05 14:47:37 +0000170 Preconditions.checkState(!argv.isEmpty(), "At least one argument is expected");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100171
172 return new Command(
173 system == OS.WINDOWS ? transformArgvForWindows() : transformArgvForLinux(),
174 env, workingDir);
175 }
176}