blob: b53ced286716b565699755ade3a30d0f2d934e0b [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.shell;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.util.Escaper;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.Verbosity;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;
public abstract class ShellStep implements Step {
/** Defined lazily by {@link #getShellCommand(com.facebook.buck.step.ExecutionContext)}. */
private ImmutableList<String> shellCommandArgs;
/** If specified, working directory will be different from project root. **/
@Nullable
protected final File workingDirectory;
/**
* This is set if {@link #shouldPrintStdout(Verbosity)} returns {@code true} when the command is
* executed.
*/
@Nullable
private String stdout;
/**
* This is set if {@link #shouldPrintStderr(Verbosity)} returns {@code true} when the command is
* executed.
*/
@Nullable
private String stderr;
private long startTime = 0L;
private long endTime = 0L;
protected ShellStep() {
this(/* workingDirectory */ null);
}
protected ShellStep(@Nullable File workingDirectory) {
this.workingDirectory = workingDirectory;
}
/**
* Get the working directory for this command.
* @return working directory specified on construction
* ({@code null} if project directory will be used).
*/
@Nullable
@VisibleForTesting
public File getWorkingDirectory() {
return workingDirectory;
}
@Override
public int execute(ExecutionContext context) {
// Kick off a Process in which this ShellCommand will be run.
ProcessBuilder processBuilder = new ProcessBuilder(getShellCommand(context));
// Add environment variables, if appropriate.
if (!getEnvironmentVariables(context).isEmpty()) {
Map<String, String> environment = processBuilder.environment();
environment.putAll(getEnvironmentVariables(context));
}
if (workingDirectory != null) {
processBuilder.directory(workingDirectory);
} else {
processBuilder.directory(context.getProjectDirectoryRoot());
}
Process process;
int exitCode;
try {
startTime = System.currentTimeMillis();
process = processBuilder.start();
exitCode = interactWithProcess(context, process);
} catch (IOException e) {
e.printStackTrace(context.getStdErr());
exitCode = 1;
}
endTime = System.currentTimeMillis();
onProcessFinished(exitCode);
return exitCode;
}
@VisibleForTesting
int interactWithProcess(ExecutionContext context, Process process) {
Verbosity verbosity = context.getVerbosity();
boolean shouldPrintStdOut = shouldPrintStdout(verbosity);
boolean shouldPrintStdErr = shouldPrintStderr(verbosity);
ProcessExecutor executor = context.getProcessExecutor();
ProcessExecutor.Result result = executor.execute(process,
shouldPrintStdOut,
shouldPrintStdErr,
context.getVerbosity() == Verbosity.SILENT);
this.stdout = result.getStdout();
this.stderr = result.getStderr();
return result.getExitCode();
}
public long getDuration() {
Preconditions.checkState(startTime > 0);
Preconditions.checkState(endTime > 0);
return endTime - startTime;
}
/**
* This method is idempotent.
* @return the shell command arguments
*/
public final ImmutableList<String> getShellCommand(ExecutionContext context) {
if (shellCommandArgs == null) {
shellCommandArgs = getShellCommandInternal(context);
}
return shellCommandArgs;
}
/**
* Implementations of this method should not have any observable side-effects.
*/
@VisibleForTesting
protected abstract ImmutableList<String> getShellCommandInternal(ExecutionContext context);
/**
* Callback function to be run after invoking the shell command.
* @param exitCode exit code from invoking the shell script.
*/
protected void onProcessFinished(int exitCode) {
// Do nothing by default.
}
@Override
public final String getDescription(ExecutionContext context) {
// Get environment variables for this command as VAR1=val1 VAR2=val2... etc., with values
// quoted as necessary.
Iterable<String> env = Iterables.transform(getEnvironmentVariables(context).entrySet(),
new Function<Entry<String, String>, String>() {
@Override
public String apply(Entry<String, String> e) {
return String.format("%s=%s", e.getKey(), Escaper.escapeAsBashString(e.getValue()));
}
});
// Quote the arguments to the shell command as needed (this applies to $0 as well
// e.g. if we run '/path/a b.sh' quoting is needed).
Iterable<String> cmd = Iterables.transform(getShellCommand(context), Escaper.BASH_ESCAPER);
String shellCommand = Joiner.on(" ").join(Iterables.concat(env, cmd));
if (getWorkingDirectory() == null) {
return shellCommand;
} else {
// If the ShellCommand has a specific working directory, set through ProcessBuilder, then
// this is what the user might type in a shell to get the same behavior. The (...) syntax
// introduces a subshell in which the command is only executed if cd was successful.
return String.format("(cd %s && %s)",
Escaper.escapeAsBashString(workingDirectory.getPath()),
shellCommand);
}
}
/**
* Returns the environment variables to include when running this {@link ShellStep}.
* <p>
* By default, this method returns an empty map.
* @param context that may be useful when determining environment variables to include.
*/
public ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) {
return ImmutableMap.of();
}
/**
* @param verbosity is provided in case that affects what should be printed.
* @return whether the stdout of the shell command, when executed, should be printed to the stderr
* of the specified {@link ExecutionContext}. If {@code false}, stdout will only be printed on
* error and only if verbosity is set to standard information.
*/
protected boolean shouldPrintStdout(Verbosity verbosity) {
return false;
}
/**
* @return the stdout of this ShellCommand or throws an exception if the stdout was not recorded
*/
public final String getStdout() {
Preconditions.checkNotNull(this.stdout, "stdout was not set: " +
"shouldPrintStdout() must return false and execute() must have been invoked");
return this.stdout;
}
/**
* @return whether the stderr of the shell command, when executed, should be printed to the stderr
* of the specified {@link ExecutionContext}. If {@code false}, stderr will only be printed on
* error and only if verbosity is set to standard information.
*/
protected boolean shouldPrintStderr(Verbosity verbosity) {
return verbosity.shouldPrintOutput();
}
/**
* @return the stderr of this ShellCommand or throws an exception if the stderr was not recorded
*/
public final String getStderr() {
Preconditions.checkNotNull(this.stderr, "stderr was not set: " +
"shouldPrintStdErr() must return false and execute() must have been invoked");
return this.stderr;
}
}