com.facebook.buck.shell.AbstractGenruleStep.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.shell.AbstractGenruleStep.java

Source

/*
 * 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.BuildRuleType;
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.Joiner;
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;

import javax.annotation.Nullable;

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 CommandString commandString;
    private final ImmutableSortedSet<BuildRule> depsToSubstituteInCommandString;
    private final BuildRuleType type;
    private final BuildTarget target;

    public AbstractGenruleStep(BuildRuleType type,

            BuildTarget target, CommandString commandString, Set<BuildRule> depsToSubstituteInCommandString,
            @Nullable File workingDirectory) {
        super(workingDirectory);
        this.type = type;
        this.target = target;
        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 target.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.or("").isEmpty()) {
                commandInUse = commandString.cmdExe.get();
            } else if (!commandString.cmd.or("").isEmpty()) {
                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.or("").isEmpty()) {
                commandInUse = commandString.bash.get();
            } else if (!commandString.cmd.or("").isEmpty()) {
                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 environment 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();
    }

    protected abstract 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 = String.format("//%s%s", target.getBasePath(), buildTarget);
            }
            BuildRule matchingRule = fullyQualifiedNameToBuildRule.get(buildTarget);
            if (matchingRule == null) {
                throw new HumanReadableException("No dep named %s for %s %s, cmd was %s", buildTarget,
                        type.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.getAbsolutifier().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 Joiner.on(' ').join(((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, type.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);
        }
    }
}