blob: 8e13e21f69b0824364bd7b8189d0054e478e2f3e [file] [log] [blame]
/*
* Copyright 2013-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.model.BuildTarget;
import com.facebook.buck.rules.BinaryBuildRule;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.Buildable;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProjectFilesystem;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.util.environment.Platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Maps;
import java.io.File;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class AbstractGenruleStep extends ShellStep {
/**
* Matches either a relative or fully-qualified build target wrapped in <tt>${}</tt>, unless the
* <code>$</code> is preceded by a backslash.
*
* Given the input: $(exe //foo:bar), capturing groups are
* 1: $(exe //foo:bar)
* 2: exe
* 3: //foo:bar
* 4: //foo
* 5: :bar
* If we match against $(location :bar), the capturing groups are:
* 1: $(location :bar)
* 2: location
* 3: :bar
* 4: null
* 5: :bar
*/
@VisibleForTesting
static final Pattern BUILD_TARGET_PATTERN = Pattern.compile(
// We want a negative lookbehind to ensure we don't have a '\$', which is why this starts off
// in such an interesting way.
"(?<!\\\\)(\\$\\((exe|location)\\s+((\\/\\/[^:]*)?(:[^\\)]+))\\))"
);
private final BuildRule buildRule;
private final CommandString commandString;
private final ImmutableSortedSet<BuildRule> depsToSubstituteInCommandString;
public AbstractGenruleStep(BuildRule buildRule,
CommandString commandString,
Set<BuildRule> depsToSubstituteInCommandString) {
this.buildRule = Preconditions.checkNotNull(buildRule);
this.commandString = Preconditions.checkNotNull(commandString);
this.depsToSubstituteInCommandString = ImmutableSortedSet.copyOf(
depsToSubstituteInCommandString);
}
public static class CommandString {
private Optional<String> cmd;
private Optional<String> bash;
private Optional<String> cmdExe;
public CommandString(Optional<String> cmd, Optional<String> bash, Optional<String> cmdExe) {
this.cmd = Preconditions.checkNotNull(cmd);
this.bash = Preconditions.checkNotNull(bash);
this.cmdExe = Preconditions.checkNotNull(cmdExe);
}
}
private String getFullyQualifiedName() {
return buildRule.getFullyQualifiedName();
}
@Override
public String getShortName() {
return "genrule";
}
@Override
protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) {
ExecutionArgsAndCommand commandAndExecutionArgs = getCommandAndExecutionArgs(context);
return ImmutableList.<String>builder()
.addAll(commandAndExecutionArgs.executionArgs)
.add(commandAndExecutionArgs.command)
.build();
}
private ExecutionArgsAndCommand getCommandAndExecutionArgs(ExecutionContext context) {
// The priority sequence is
// "cmd.exe /c winCommand" (Windows Only)
// "/bin/bash -e -c shCommand" (Non-windows Only)
// "(/bin/bash -c) or (cmd.exe /c) cmd" (All platforms)
String command;
if (context.getPlatform() == Platform.WINDOWS) {
String commandInUse;
if (commandString.cmdExe.isPresent()) {
commandInUse = commandString.cmdExe.get();
} else if (commandString.cmd.isPresent()) {
commandInUse = commandString.cmd.get();
} else {
throw new HumanReadableException("You must specify either cmd_exe or cmd for genrule %s.",
getFullyQualifiedName());
}
command = replaceMatches(context.getProjectFilesystem(), commandInUse);
return new ExecutionArgsAndCommand(ImmutableList.of("cmd.exe", "/c"), command);
} else {
String commandInUse;
if (commandString.bash.isPresent()) {
commandInUse = commandString.bash.get();
} else if (commandString.cmd.isPresent()) {
commandInUse = commandString.cmd.get();
} else {
throw new HumanReadableException("You must specify either bash or cmd for genrule %s.",
getFullyQualifiedName());
}
command = replaceMatches(context.getProjectFilesystem(), commandInUse);
return new ExecutionArgsAndCommand(ImmutableList.of("/bin/bash", "-e", "-c"), command);
}
}
@Override
public ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) {
ImmutableMap.Builder<String, String> allEnvironmentVariablesBuilder = ImmutableMap.builder();
addEnvironmentVariables(context, allEnvironmentVariablesBuilder);
ImmutableMap<String, String> allEnvironmentVariables = allEnvironmentVariablesBuilder.build();
// Long lists of environment variables can extend the length of the command such that it exceeds
// exec()'s ARG_MAX limit. Defend against this by filtering out variables that do not appear in
// the command string.
String command = getCommandAndExecutionArgs(context).command;
ImmutableMap.Builder<String, String> usedEnvironmentVariablesBuilder = ImmutableMap.builder();
for (Map.Entry<String, String> environmentVariable : allEnvironmentVariables.entrySet()) {
// We check for the presence of the variable without adornment for $ or %% so it works on both
// Windows and non-Windows environments. Eventually, we will require $ in the command string
// and modify the command directly rather than using envrionment variables.
String environmentVariableName = environmentVariable.getKey();
if (command.contains(environmentVariableName)) {
// I hate this $DEPS variable so much...
if ("DEPS".equals(environmentVariableName) &&
allEnvironmentVariables.containsKey("GEN_DIR")) {
usedEnvironmentVariablesBuilder.put("GEN_DIR", allEnvironmentVariables.get("GEN_DIR"));
}
usedEnvironmentVariablesBuilder.put(environmentVariable);
}
}
return usedEnvironmentVariablesBuilder.build();
}
abstract protected void addEnvironmentVariables(ExecutionContext context,
ImmutableMap.Builder<String, String> environmentVariablesBuilder);
@Override
protected boolean shouldPrintStderr(Verbosity verbosity) {
return true;
}
/**
* @return the cmd with binary and location build targets interpolated as either commands or the
* location of the outputs of those targets.
*/
@VisibleForTesting
String replaceMatches(ProjectFilesystem filesystem, String command) {
Matcher matcher = BUILD_TARGET_PATTERN.matcher(command);
StringBuffer buffer = new StringBuffer();
Map<String, BuildRule> fullyQualifiedNameToBuildRule = null;
while (matcher.find()) {
if (fullyQualifiedNameToBuildRule == null) {
fullyQualifiedNameToBuildRule = Maps.newHashMap();
for (BuildRule dep : depsToSubstituteInCommandString) {
fullyQualifiedNameToBuildRule.put(dep.getFullyQualifiedName(), dep);
}
}
String buildTarget = matcher.group(3);
String base = matcher.group(4);
if (base == null) {
// This is a relative build target, so make it fully qualified.
BuildTarget myBuildTarget = buildRule.getBuildTarget();
buildTarget = String.format("//%s%s", myBuildTarget.getBasePath(), buildTarget);
}
BuildRule matchingRule = fullyQualifiedNameToBuildRule.get(buildTarget);
if (matchingRule == null) {
throw new HumanReadableException("No dep named %s for %s %s, cmd was %s",
buildTarget, buildRule.getType().getName(), getFullyQualifiedName(), command);
}
String replacement;
Buildable matchingBuildable = matchingRule.getBuildable();
switch (matcher.group(2)) {
case "exe":
replacement = getExecutableReplacementFrom(filesystem, command, matchingBuildable);
break;
case "location":
replacement = getLocationReplacementFrom(filesystem, matchingBuildable).toString();
break;
default:
throw new HumanReadableException("Unable to determine replacement for '%s' in target %s",
matcher.group(2), getFullyQualifiedName());
}
// `replacement` may contain Windows style directory separator backslash (\), which will be
// considered as escape character. Escape them.
matcher.appendReplacement(buffer, replacement.replace("\\", "\\\\"));
}
matcher.appendTail(buffer);
return buffer.toString();
}
private Path getLocationReplacementFrom(ProjectFilesystem filesystem, Buildable matchingRule) {
return filesystem.getPathRelativizer().apply(matchingRule.getPathToOutputFile());
}
/**
* A build rule can be executable in one of two ways: either by being a file with the executable
* bit set, or by the rule being a {@link com.facebook.buck.rules.BinaryBuildRule}.
*
* @param filesystem The project file system to resolve files with.
* @param cmd The command being executed.
* @param matchingRule The BuildRule which may or may not be an executable.
* @return A string which can be inserted to cause matchingRule to be executed.
*/
private String getExecutableReplacementFrom(
ProjectFilesystem filesystem,
String cmd,
Buildable matchingRule) {
if (matchingRule instanceof BinaryBuildRule) {
return ((BinaryBuildRule) matchingRule).getExecutableCommand(filesystem);
}
File output = filesystem.getFileForRelativePath(matchingRule.getPathToOutputFile());
if (output != null && output.exists() && output.canExecute()) {
return output.getAbsolutePath();
}
throw new HumanReadableException(
"%s must correspond to a binary rule or file in %s for %s %s",
matchingRule,
cmd,
buildRule.getType().getName(),
getFullyQualifiedName());
}
private static class ExecutionArgsAndCommand {
private final ImmutableList<String> executionArgs;
private final String command;
private ExecutionArgsAndCommand(ImmutableList<String> executionArgs, String command) {
this.executionArgs = Preconditions.checkNotNull(executionArgs);
this.command = Preconditions.checkNotNull(command);
}
}
}