aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java
blob: 13cb298143d6c76b996e7c521bb58d47e5b76b18 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// 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 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.Preconditions;
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);
  }
}