From 835814895fffef00fe88658f91e4550dc61546cf Mon Sep 17 00:00:00 2001 From: cparsons Date: Mon, 16 Apr 2018 11:49:24 -0700 Subject: Rollback https://github.com/bazelbuild/bazel/pull/5007 It breaks downstream rules_nodejs. See https://github.com/bazelbuild/bazel/issues/5028 for details. RELNOTES: None. PiperOrigin-RevId: 193074798 --- .../devtools/build/lib/util/CommandBuilder.java | 121 ++++++++++++++++++--- 1 file changed, 108 insertions(+), 13 deletions(-) (limited to 'src/main/java/com/google/devtools/build/lib/util') diff --git a/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java b/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java index 9bff531521..d60fd94dc4 100644 --- a/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/util/CommandBuilder.java @@ -14,44 +14,65 @@ 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 {@link Command} builder. + * Implements OS aware {@link Command} builder. At this point only Linux, Mac + * and Windows XP are supported. + * + *

Builder will also apply heuristic to identify trivial cases where + * unix-like command lines could be automatically converted into the + * Windows-compatible form. * - *

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. + *

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 final ArrayList argv = new ArrayList<>(); - private final HashMap env = new HashMap<>(); - private final File workingDir; + private static final ImmutableList SHELLS = ImmutableList.of("/bin/sh", "/bin/bash"); + + private static final Splitter ARGV_SPLITTER = Splitter.on(CharMatcher.anyOf(" \t")); - public CommandBuilder(Path workingDir) { - this(workingDir.getPathFile()); + private final OS system; + private final List argv = new ArrayList<>(); + private final Map env = new HashMap<>(); + private File workingDir = null; + private boolean useShell = false; + + public CommandBuilder() { + this(OS.getCurrent()); } - public CommandBuilder(File workingDir) { - this.workingDir = Preconditions.checkNotNull(workingDir); + @VisibleForTesting + CommandBuilder(OS system) { + this.system = system; } public CommandBuilder addArg(String arg) { - Preconditions.checkNotNull(arg); + Preconditions.checkNotNull(arg, "Argument must not be null"); argv.add(arg); return this; } public CommandBuilder addArgs(Iterable args) { - Preconditions.checkArgument(!Iterables.contains(args, null)); + Preconditions.checkArgument(!Iterables.contains(args, null), "Arguments must not be null"); Iterables.addAll(argv, args); return this; } @@ -77,8 +98,82 @@ public final class CommandBuilder { 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 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. + // When use CMD.EXE to invoke a bat/cmd file, the file path must have '\' instead of '/' + if (!command.toLowerCase().endsWith(".exe")) { + useShell = true; + modifiedArgv.set(0, argv0.replace('/', '\\')); + } + } 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(argv.toArray(new String[0]), env, workingDir); + + return new Command( + system == OS.WINDOWS ? transformArgvForWindows() : transformArgvForLinux(), + env, workingDir); } } -- cgit v1.2.3