blob: a138ece66e6ec75e7cca0dbe09300aa8034ce1f5 [file] [log] [blame]
// Copyright 2024 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.runtime.commands;
import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.lib.buildtool.PathPrettyPrinter;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.ShellEscaper;
import com.google.devtools.build.lib.vfs.Path;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import javax.annotation.Nullable;
/**
* Encapsulates information for launching the command specified by a run invocation.
*
* <p>Notably, this class handles per-platform command-line formatting (windows vs unix).
*/
class RunCommandLine {
private final ImmutableList<String> argsWithResidue;
private final ImmutableList<String> prettyPrintArgsWithResidue;
private final ImmutableList<String> argsWithoutResidue;
@Nullable private final String runUnderPrefix;
@Nullable private final String prettyRunUnderPrefix;
private final ImmutableSortedMap<String, String> runEnvironment;
private final Path workingDir;
private final boolean isTestTarget;
private RunCommandLine(
ImmutableList<String> argsWithResidue,
ImmutableList<String> prettyPrintArgsWithResidue,
ImmutableList<String> argsWithoutResidue,
@Nullable String runUnderPrefix,
@Nullable String prettyRunUnderPrefix,
ImmutableSortedMap<String, String> runEnvironment,
Path workingDir,
boolean isTestTarget) {
this.argsWithResidue = argsWithResidue;
this.prettyPrintArgsWithResidue = prettyPrintArgsWithResidue;
this.argsWithoutResidue = argsWithoutResidue;
this.runUnderPrefix = runUnderPrefix;
this.prettyRunUnderPrefix = prettyRunUnderPrefix;
this.runEnvironment = runEnvironment;
this.workingDir = workingDir;
this.isTestTarget = isTestTarget;
}
Path getWorkingDir() {
return workingDir;
}
ImmutableSortedMap<String, String> getEnvironment() {
return runEnvironment;
}
boolean isTestTarget() {
return isTestTarget;
}
/**
* Returns a console-friendly (including relative paths) representation of the command line which
* would be returned by {@link #getArgs}.
*/
String getPrettyArgs() {
StringBuilder result = new StringBuilder();
if (prettyRunUnderPrefix != null) {
result.append(prettyRunUnderPrefix).append(" ");
}
for (int i = 0; i < prettyPrintArgsWithResidue.size(); i++) {
if (i > 0) {
result.append(" ");
}
result.append(ShellEscaper.escapeString(prettyPrintArgsWithResidue.get(i)));
}
return result.toString();
}
boolean requiresShExecutable() {
return OS.getCurrent() != OS.WINDOWS || runUnderPrefix != null;
}
/** Returns the command arguments including residue. */
ImmutableList<String> getArgs(String shExecutable) {
return formatter().formatArgv(shExecutable, runUnderPrefix, argsWithResidue);
}
/**
* Returns the command arguments without residue (extra arguments from the run invocation's
* command line). This is intended to be used in places where we don't want to include the residue
* in case it contains sensitive information.
*/
ImmutableList<String> getArgsWithoutResidue(@Nullable String shExecutable) {
return formatter().formatArgv(shExecutable, runUnderPrefix, argsWithoutResidue);
}
/**
* Returns the script form of the command, to be used as the contents of output file in
* --script_path mode.
*/
String getScriptForm(String shExecutable, ImmutableSortedSet<String> environmentVarsToUnset) {
return formatter()
.getScriptForm(
shExecutable,
workingDir.getPathString(),
environmentVarsToUnset,
runEnvironment,
runUnderPrefix,
argsWithResidue);
}
private static Formatter formatter() {
return OS.getCurrent() == OS.WINDOWS ? new WindowsFormatter() : new LinuxFormatter();
}
private interface Formatter {
ImmutableList<String> formatArgv(
@Nullable String shExecutable, @Nullable String runUnderPrefix, ImmutableList<String> args);
String getScriptForm(
String shExecutable,
String workingDir,
ImmutableSortedSet<String> environmentVarsToUnset,
ImmutableSortedMap<String, String> environment,
@Nullable String runUnderPrefix,
ImmutableList<String> args);
}
@VisibleForTesting
static class LinuxFormatter implements Formatter {
@Override
public ImmutableList<String> formatArgv(
@Nullable String shExecutable,
@Nullable String runUnderPrefix,
ImmutableList<String> args) {
Preconditions.checkArgument(shExecutable != null, "shExecutable must be non-null");
StringBuilder command = new StringBuilder();
if (runUnderPrefix != null) {
command.append(runUnderPrefix).append(" ");
}
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
command.append(" ");
}
command.append(ShellEscaper.escapeString(args.get(i)));
}
return ImmutableList.of(shExecutable, "-c", command.toString());
}
@Override
public String getScriptForm(
String shExecutable,
String workingDir,
ImmutableSortedSet<String> environmentVarsToUnset,
ImmutableSortedMap<String, String> environment,
@Nullable String runUnderPrefix,
ImmutableList<String> args) {
String unsetEnv =
environmentVarsToUnset.stream().map(v -> "-u " + v).collect(joining(" \\\n "));
String setEnv =
environment.entrySet().stream()
.map(
kv ->
ShellEscaper.escapeString(kv.getKey())
+ "="
+ ShellEscaper.escapeString(kv.getValue()))
.collect(joining(" \\\n "));
String commandLine = getCommandLine(shExecutable, runUnderPrefix, args);
StringBuilder result = new StringBuilder();
result.append("#!").append(shExecutable).append("\n");
result.append("cd ").append(ShellEscaper.escapeString(workingDir)).append(" && \\\n");
result.append(" exec env \\\n");
result.append(" ").append(unsetEnv).append(" \\\n");
result.append(" ").append(setEnv).append(" \\\n");
result.append(" ").append(commandLine).append(" \"$@\"");
return result.toString();
}
private static String getCommandLine(
String shExecutable, @Nullable String runUnderPrefix, ImmutableList<String> args) {
StringBuilder command = new StringBuilder();
if (runUnderPrefix != null) {
command.append(runUnderPrefix).append(" ");
}
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
command.append(" ");
}
command.append(ShellEscaper.escapeString(args.get(i)));
}
if (runUnderPrefix == null) {
return command.toString();
} else {
return shExecutable + " -c " + ShellEscaper.escapeString(command.toString());
}
}
}
@VisibleForTesting
static class WindowsFormatter implements Formatter {
@Override
public ImmutableList<String> formatArgv(
@Nullable String shExecutable,
@Nullable String runUnderPrefix,
ImmutableList<String> args) {
if (runUnderPrefix != null) {
Preconditions.checkArgument(
shExecutable != null, "shExecutable must be non-null when --run_under is used");
StringBuilder command = new StringBuilder();
command.append(runUnderPrefix).append(" ");
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
command.append(" ");
}
command.append(ShellEscaper.escapeString(args.get(i)));
}
return ImmutableList.of(
shExecutable, "-c", ShellUtils.windowsEscapeArg(command.toString()));
}
ImmutableList.Builder<String> result = ImmutableList.builder();
for (int i = 0; i < args.size(); i++) {
if (i == 0) {
// All but the first element in `cmdLine` have to be escaped. The first element is the
// binary, which must not be escaped.
result.add(args.get(i));
} else {
result.add(ShellUtils.windowsEscapeArg(args.get(i)));
}
}
return result.build();
}
@Override
public String getScriptForm(
String shExecutable,
String workingDir,
ImmutableSortedSet<String> environmentVarsToUnset,
ImmutableSortedMap<String, String> environment,
@Nullable String runUnderPrefix,
ImmutableList<String> args) {
String unsetEnv =
environmentVarsToUnset.stream().map(v -> "SET " + v + "=").collect(joining("\n "));
String setEnv =
environment.entrySet().stream()
.map(kv -> "SET " + kv.getKey() + "=" + kv.getValue())
.collect(joining("\n "));
String commandLine = getCommandLine(shExecutable, runUnderPrefix, args);
StringBuilder result = new StringBuilder();
result.append("@echo off\n");
result.append("cd /d ").append(workingDir).append("\n");
result.append(" ").append(unsetEnv).append("\n");
result.append(" ").append(setEnv).append("\n");
result.append(" ").append(commandLine).append(" %*");
return result.toString();
}
private static String getCommandLine(
String shExecutable, @Nullable String runUnderPrefix, ImmutableList<String> args) {
StringBuilder command = new StringBuilder();
if (runUnderPrefix != null) {
command.append(runUnderPrefix).append(" ");
}
for (int i = 0; i < args.size(); i++) {
if (i == 0) {
command.append(args.get(i).replace('/', '\\'));
} else {
command.append(" ").append(ShellUtils.windowsEscapeArg(args.get(i)));
}
}
if (runUnderPrefix == null) {
return command.toString();
} else {
return shExecutable + " -c " + ShellEscaper.escapeString(command.toString());
}
}
}
static class Builder {
private final ImmutableSortedMap<String, String> runEnvironment;
private final Path workingDir;
private final boolean isTestTarget;
@Nullable private String runUnderPrefix;
@Nullable private String prettyRunUnderPrefix;
private final ImmutableList.Builder<String> args = ImmutableList.builder();
private final ImmutableList.Builder<String> prettyPrintArgs = ImmutableList.builder();
private final ImmutableList.Builder<String> residueArgs = ImmutableList.builder();
Builder(
ImmutableSortedMap<String, String> runEnvironment, Path workingDir, boolean isTestTarget) {
this.runEnvironment = runEnvironment;
this.workingDir = workingDir;
this.isTestTarget = isTestTarget;
}
@CanIgnoreReturnValue
Builder setRunUnderPrefix(String runUnderPrefix) {
this.runUnderPrefix = runUnderPrefix;
this.prettyRunUnderPrefix = runUnderPrefix;
return this;
}
@CanIgnoreReturnValue
Builder setRunUnderTarget(
Path runUnderBinary, List<String> args, PathPrettyPrinter pathPrettyPrinter) {
StringBuilder runUnder = new StringBuilder();
StringBuilder prettyRunUnder = new StringBuilder();
runUnder.append(ShellEscaper.escapeString(runUnderBinary.getPathString()));
prettyRunUnder.append(
ShellEscaper.escapeString(
pathPrettyPrinter.getPrettyPath(runUnderBinary.asFragment()).getPathString()));
for (String arg : args) {
String escapedArg = ShellEscaper.escapeString(arg);
runUnder.append(" ").append(escapedArg);
prettyRunUnder.append(" ").append(escapedArg);
}
this.runUnderPrefix = runUnder.toString();
this.prettyRunUnderPrefix = prettyRunUnder.toString();
return this;
}
@CanIgnoreReturnValue
Builder addArg(String arg) {
return addArgInternal(arg, arg);
}
@CanIgnoreReturnValue
Builder addArg(Path path, PathPrettyPrinter pathPrettyPrinter) {
return addArgInternal(
path.getPathString(), pathPrettyPrinter.getPrettyPath(path.asFragment()).getPathString());
}
@CanIgnoreReturnValue
Builder addArgs(Iterable<String> args) {
for (String arg : args) {
addArg(arg);
}
return this;
}
@CanIgnoreReturnValue
Builder addArgsFromResidue(ImmutableList<String> args) {
residueArgs.addAll(args);
return this;
}
@CanIgnoreReturnValue
private Builder addArgInternal(String arg, String prettyPrintArg) {
args.add(arg);
prettyPrintArgs.add(prettyPrintArg);
return this;
}
RunCommandLine build() {
ImmutableList<String> argsWithoutResidue = args.build();
ImmutableList<String> argsWithResidue =
ImmutableList.<String>builder()
.addAll(argsWithoutResidue)
.addAll(residueArgs.build())
.build();
ImmutableList<String> prettyPrintArgsWithResidue =
ImmutableList.<String>builder()
.addAll(prettyPrintArgs.build())
.addAll(residueArgs.build())
.build();
return new RunCommandLine(
argsWithResidue,
prettyPrintArgsWithResidue,
argsWithoutResidue,
runUnderPrefix,
prettyRunUnderPrefix,
runEnvironment,
workingDir,
isTestTarget);
}
}
}