| /* |
| * 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); |
| } |
| } |
| } |