/* * 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.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.Verbosity; import com.facebook.buck.util.environment.Platform; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.nio.file.Path; import java.util.Map; import java.util.Optional; public abstract class AbstractGenruleStep extends ShellStep { private final ProjectFilesystem projectFilesystem; private final CommandString commandString; private final BuildTarget target; private final LoadingCache<Platform, Path> scriptFilePath = CacheBuilder.newBuilder() .build( new CacheLoader<Platform, Path>() { @Override public Path load(Platform platform) throws IOException { ExecutionArgsAndCommand executionArgsAndCommand = getExecutionArgsAndCommand(platform); return projectFilesystem.resolve( projectFilesystem.createTempFile( "genrule-", "." + executionArgsAndCommand.shellType.extension)); } }); public AbstractGenruleStep( ProjectFilesystem projectFilesystem, BuildTarget target, CommandString commandString, Path workingDirectory) { super(workingDirectory); this.projectFilesystem = projectFilesystem; this.target = target; this.commandString = commandString; } @Override public String getShortName() { return "genrule"; } @Override public StepExecutionResult execute(ExecutionContext context) throws IOException, InterruptedException { Path scriptFilePath = getScriptFilePath(context); String scriptFileContents = getScriptFileContents(context); projectFilesystem.writeContentsToPath( scriptFileContents + System.lineSeparator(), scriptFilePath); return super.execute(context); } @Override protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) { ExecutionArgsAndCommand executionArgsAndCommand = getExecutionArgsAndCommand(context.getPlatform()); Path scriptFilePath = this.scriptFilePath.getUnchecked(context.getPlatform()); return ImmutableList.<String>builder() .addAll(executionArgsAndCommand.shellType.executionArgs) .add(scriptFilePath.toString()) .build(); } @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 = getExecutionArgsAndCommand(context.getPlatform()).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)) { usedEnvironmentVariablesBuilder.put(environmentVariable); } } return usedEnvironmentVariablesBuilder.build(); } @Override protected boolean shouldPrintStderr(Verbosity verbosity) { return true; } @VisibleForTesting public String getScriptFileContents(ExecutionContext context) { ExecutionArgsAndCommand executionArgsAndCommand = getExecutionArgsAndCommand(context.getPlatform()); if (context.getPlatform() == Platform.WINDOWS) { executionArgsAndCommand = getExpandedCommandAndExecutionArgs( executionArgsAndCommand, getEnvironmentVariables(context)); } return executionArgsAndCommand.command; } @VisibleForTesting public Path getScriptFilePath(ExecutionContext context) throws IOException { try { return scriptFilePath.get(context.getPlatform()); } catch (Exception e) { Throwables.propagateIfPossible(e, IOException.class); throw new RuntimeException(e); } } private ExecutionArgsAndCommand getExecutionArgsAndCommand(Platform platform) { return commandString.getCommandAndExecutionArgs(platform, target); } protected abstract void addEnvironmentVariables( ExecutionContext context, ImmutableMap.Builder<String, String> environmentVariablesBuilder); private static ExecutionArgsAndCommand getExpandedCommandAndExecutionArgs( ExecutionArgsAndCommand original, ImmutableMap<String, String> environmentVariablesToExpand) { String expandedCommand = original.command; for (Map.Entry<String, String> variable : environmentVariablesToExpand.entrySet()) { expandedCommand = expandedCommand .replace("$" + variable.getKey(), variable.getValue()) .replace("${" + variable.getKey() + "}", variable.getValue()); } return new ExecutionArgsAndCommand(original.shellType, expandedCommand); } private static class ExecutionArgsAndCommand { private final ShellType shellType; private final String command; private ExecutionArgsAndCommand(ShellType shellType, String command) { this.shellType = shellType; this.command = command; } } private enum ShellType { CMD_EXE("cmd", ImmutableList.of()), BASH("sh", ImmutableList.of("/bin/bash", "-e")), ; private final String extension; private final ImmutableList<String> executionArgs; ShellType(String extension, ImmutableList<String> executionArgs) { this.extension = extension; this.executionArgs = executionArgs; } } 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 = cmd; this.bash = bash; this.cmdExe = cmdExe; } public ExecutionArgsAndCommand getCommandAndExecutionArgs( Platform platform, BuildTarget target) { // 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 (platform == Platform.WINDOWS) { if (!cmdExe.orElse("").isEmpty()) { command = cmdExe.get(); } else if (!cmd.orElse("").isEmpty()) { command = cmd.get(); } else { throw new HumanReadableException( "You must specify either cmd_exe or cmd for genrule %s.", target.getFullyQualifiedName()); } return new ExecutionArgsAndCommand(ShellType.CMD_EXE, command); } else { if (!bash.orElse("").isEmpty()) { command = bash.get(); } else if (!cmd.orElse("").isEmpty()) { command = cmd.get(); } else { throw new HumanReadableException( "You must specify either bash or cmd for genrule %s.", target.getFullyQualifiedName()); } return new ExecutionArgsAndCommand(ShellType.BASH, command); } } } }