blob: 3a89854ac2150d8e119e889e047a28f7ec869b19 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.runtime.commands;
import com.google.common.annotations.VisibleForTesting;
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.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLine;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.ShToolchain;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.RunUnder;
import com.google.devtools.build.lib.analysis.test.TestConfiguration;
import com.google.devtools.build.lib.analysis.test.TestProvider;
import com.google.devtools.build.lib.analysis.test.TestRunnerAction;
import com.google.devtools.build.lib.analysis.test.TestTargetExecutionSettings;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.BuildTool;
import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
import com.google.devtools.build.lib.buildtool.PathPrettyPrinter;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.exec.SymlinkTreeHelper;
import com.google.devtools.build.lib.exec.TestPolicy;
import com.google.devtools.build.lib.exec.TestStrategy;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.pkgcache.LoadingFailedException;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.BlazeServerStartupOptions;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.server.CommandProtos.EnvironmentVariable;
import com.google.devtools.build.lib.server.CommandProtos.ExecRequest;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.util.CommandDescriptionForm;
import com.google.devtools.build.lib.util.CommandFailureUtils;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.FileType;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.OptionsUtils;
import com.google.devtools.build.lib.util.ShellEscaper;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingResult;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
/**
* Builds and run a target with the given command line arguments.
*/
@Command(name = "run",
builds = true,
options = { RunCommand.RunOptions.class },
inherits = { BuildCommand.class },
shortDescription = "Runs the specified target.",
help = "resource:run.txt",
allowResidue = true,
hasSensitiveResidue = true,
completion = "label-bin")
public class RunCommand implements BlazeCommand {
/** Options for the "run" command. */
public static class RunOptions extends OptionsBase {
@Option(
name = "script_path",
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.EXECUTION},
converter = OptionsUtils.PathFragmentConverter.class,
help =
"If set, write a shell script to the given file which invokes the target. "
+ "If this option is set, the target is not run from %{product}. "
+ "Use '%{product} run --script_path=foo //foo && ./foo' to invoke target '//foo' "
+ "This differs from '%{product} run //foo' in that the %{product} lock is released "
+ "and the executable is connected to the terminal's stdin."
)
public PathFragment scriptPath;
@Option(
name = "incompatible_windows_bashless_run_command",
documentationCategory = OptionDocumentationCategory.TESTING,
effectTags = {
OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS,
},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES,
},
defaultValue = "true",
help =
"On Windows: if true, the \"run\" command runs the binary directly instead of running "
+ "through Bash; when false, then the binary is ran through Bash. On other "
+ "platforms: no-op.")
public boolean bashlessRun;
}
// Thrown when a method needs Bash but ShToolchain.getPath yields none.
private static final class NoShellFoundException extends Exception {}
@VisibleForTesting
public static final String NO_TARGET_MESSAGE = "No targets found to run";
public static final String MULTIPLE_TESTS_MESSAGE =
"'run' only works with tests with one shard ('--test_sharding_strategy=disabled' is okay) "
+ "and without --runs_per_test";
// The test policy to determine the environment variables from when running tests
private final TestPolicy testPolicy;
// Value of --run_under as of the most recent command invocation.
private RunUnder currentRunUnder;
private static final FileType RUNFILES_MANIFEST = FileType.of(".runfiles_manifest");
public RunCommand(TestPolicy testPolicy) {
this.testPolicy = testPolicy;
}
@VisibleForTesting // productionVisibility = Visibility.PRIVATE
protected BuildResult processRequest(final CommandEnvironment env, BuildRequest request) {
List<String> targetPatternStrings = request.getTargets();
return new BuildTool(env)
.processRequest(
request,
(Collection<Target> targets, boolean keepGoing) ->
RunCommand.this.validateTargets(
env.getReporter(), targetPatternStrings, targets, keepGoing));
}
@Override
public void editOptions(OptionsParser optionsParser) { }
/**
* Compute the arguments the binary should be run with by concatenating the arguments in its
* {@code args=} attribute and the arguments on the Blaze command line.
*/
@Nullable
private List<String> computeArgs(CommandEnvironment env, ConfiguredTarget targetToRun,
List<String> commandLineArgs) {
List<String> args = Lists.newArrayList();
FilesToRunProvider provider = targetToRun.getProvider(FilesToRunProvider.class);
RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport();
if (runfilesSupport != null && runfilesSupport.getArgs() != null) {
CommandLine targetArgs = runfilesSupport.getArgs();
try {
Iterables.addAll(args, targetArgs.arguments());
} catch (CommandLineExpansionException e) {
env.getReporter().handle(Event.error("Could not expand target command line: " + e));
return null;
}
}
args.addAll(commandLineArgs);
return args;
}
private void constructCommandLine(
List<String> cmdLine,
List<String> prettyCmdLine,
CommandEnvironment env,
BuildConfiguration configuration,
ConfiguredTarget targetToRun,
ConfiguredTarget runUnderTarget,
List<String> args)
throws NoShellFoundException {
String productName = env.getRuntime().getProductName();
Artifact executable = targetToRun.getProvider(FilesToRunProvider.class).getExecutable();
BuildRequestOptions requestOptions = env.getOptions().getOptions(BuildRequestOptions.class);
PathFragment executablePath = executable.getPath().asFragment();
PathPrettyPrinter prettyPrinter =
OutputDirectoryLinksUtils.getPathPrettyPrinter(
requestOptions.getSymlinkPrefix(productName),
productName,
env.getWorkspace(),
requestOptions.printWorkspaceInOutputPathsIfNeeded
? env.getWorkingDirectory()
: env.getWorkspace());
PathFragment prettyExecutablePath = prettyPrinter.getPrettyPath(executable.getPath());
RunUnder runUnder = env.getOptions().getOptions(CoreOptions.class).runUnder;
// Insert the command prefix specified by the "--run_under=<command-prefix>" option
// at the start of the command line.
if (runUnder != null) {
String runUnderValue = runUnder.getValue();
if (runUnderTarget != null) {
// --run_under specifies a target. Get the corresponding executable.
// This must be an absolute path, because the run_under target is only
// in the runfiles of test targets.
runUnderValue = runUnderTarget
.getProvider(FilesToRunProvider.class).getExecutable().getPath().getPathString();
// If the run_under command contains any options, make sure to add them
// to the command line as well.
List<String> opts = runUnder.getOptions();
if (!opts.isEmpty()) {
runUnderValue += " " + ShellEscaper.escapeJoinAll(opts);
}
}
PathFragment shellExecutable = ShToolchain.getPath(configuration);
if (shellExecutable.isEmpty()) {
throw new NoShellFoundException();
}
cmdLine.add(shellExecutable.getPathString());
cmdLine.add("-c");
cmdLine.add(runUnderValue + " " + executablePath.getPathString() + " "
+ ShellEscaper.escapeJoinAll(args));
prettyCmdLine.add(shellExecutable.getPathString());
prettyCmdLine.add("-c");
prettyCmdLine.add(runUnderValue + " " + prettyExecutablePath.getPathString() + " "
+ ShellEscaper.escapeJoinAll(args));
} else {
cmdLine.add(executablePath.getPathString());
cmdLine.addAll(args);
prettyCmdLine.add(prettyExecutablePath.getPathString());
prettyCmdLine.addAll(args);
}
}
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
RunOptions runOptions = options.getOptions(RunOptions.class);
// This list should look like: ["//executable:target", "arg1", "arg2"]
List<String> targetAndArgs = options.getResidue();
// The user must at the least specify an executable target.
if (targetAndArgs.isEmpty()) {
env.getReporter().handle(Event.error("Must specify a target to run"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
String targetString = targetAndArgs.get(0);
List<String> commandLineArgs = targetAndArgs.subList(1, targetAndArgs.size());
RunUnder runUnder = options.getOptions(CoreOptions.class).runUnder;
OutErr outErr = env.getReporter().getOutErr();
List<String> targets = (runUnder != null) && (runUnder.getLabel() != null)
? ImmutableList.of(targetString, runUnder.getLabel().toString())
: ImmutableList.of(targetString);
BuildRequest request = BuildRequest.create(
this.getClass().getAnnotation(Command.class).name(), options,
env.getRuntime().getStartupOptionsProvider(), targets, outErr,
env.getCommandId(), env.getCommandStartTime());
currentRunUnder = runUnder;
BuildResult result;
try {
result = processRequest(env, request);
} finally {
currentRunUnder = null;
}
if (!result.getSuccess()) {
env.getReporter().handle(Event.error("Build failed. Not running target"));
return BlazeCommandResult.exitCode(result.getExitCondition());
}
// Make sure that we have exactly 1 built target (excluding --run_under),
// and that it is executable.
// These checks should only fail if keepGoing is true, because we already did
// validation before the build began. See {@link #validateTargets()}.
Collection<ConfiguredTarget> targetsBuilt = result.getSuccessfulTargets();
ConfiguredTarget targetToRun = null;
ConfiguredTarget runUnderTarget = null;
if (targetsBuilt != null) {
int maxTargets = runUnder != null && runUnder.getLabel() != null ? 2 : 1;
if (targetsBuilt.size() > maxTargets) {
env.getReporter()
.handle(
Event.error(
makeErrorMessageForNotHavingASingleTarget(
targetString,
Iterables.transform(targetsBuilt, ct -> ct.getLabel().toString()))));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
for (ConfiguredTarget target : targetsBuilt) {
ExitCode targetValidation = fullyValidateTarget(env, target);
if (!targetValidation.equals(ExitCode.SUCCESS)) {
return BlazeCommandResult.exitCode(targetValidation);
}
if (runUnder != null && target.getLabel().equals(runUnder.getLabel())) {
if (runUnderTarget != null) {
env.getReporter().handle(Event.error(
null, "Can't identify the run_under target from multiple options?"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
runUnderTarget = target;
} else if (targetToRun == null) {
targetToRun = target;
} else {
env.getReporter()
.handle(
Event.error(
makeErrorMessageForNotHavingASingleTarget(
targetString,
Iterables.transform(targetsBuilt, ct -> ct.getLabel().toString()))));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
}
}
// Handle target & run_under referring to the same target.
if (targetToRun == null && runUnderTarget != null) {
targetToRun = runUnderTarget;
}
if (targetToRun == null) {
env.getReporter().handle(Event.error(NO_TARGET_MESSAGE));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
BuildConfiguration configuration =
env.getSkyframeExecutor()
.getConfiguration(env.getReporter(), targetToRun.getConfigurationKey());
if (configuration == null) {
// The target may be an input file, which doesn't have a configuration. In that case, we
// choose any target configuration.
configuration = result.getBuildConfigurationCollection().getTargetConfigurations().get(0);
}
if (!configuration.buildRunfilesManifests()) {
env.getReporter()
.handle(
Event.error("--nobuild_runfile_manifests is incompatible with the \"run\" command"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
Path runfilesDir;
FilesToRunProvider provider = targetToRun.getProvider(FilesToRunProvider.class);
RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport();
if (runfilesSupport == null) {
runfilesDir = env.getWorkingDirectory();
} else {
try {
runfilesDir = ensureRunfilesBuilt(env, runfilesSupport,
env.getSkyframeExecutor().getConfiguration(env.getReporter(),
targetToRun.getConfigurationKey()));
} catch (CommandException e) {
env.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage()));
return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
}
Map<String, String> runEnvironment = new TreeMap<>();
List<String> cmdLine = new ArrayList<>();
List<String> prettyCmdLine = new ArrayList<>();
Path workingDir;
runEnvironment.put("BUILD_WORKSPACE_DIRECTORY", env.getWorkspace().getPathString());
runEnvironment.put("BUILD_WORKING_DIRECTORY", env.getWorkingDirectory().getPathString());
if (targetToRun.getProvider(TestProvider.class) != null) {
// This is a test. Provide it with a reasonable approximation of the actual test environment
ImmutableList<Artifact.DerivedArtifact> statusArtifacts =
TestProvider.getTestStatusArtifacts(targetToRun);
if (statusArtifacts.size() != 1) {
env.getReporter().handle(Event.error(MULTIPLE_TESTS_MESSAGE));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
TestRunnerAction testAction = (TestRunnerAction) env.getSkyframeExecutor()
.getActionGraph(env.getReporter()).getGeneratingAction(
Iterables.getOnlyElement(statusArtifacts));
TestTargetExecutionSettings settings = testAction.getExecutionSettings();
// ensureRunfilesBuilt does build the runfiles, but an extra consistency check won't hurt.
Preconditions.checkState(
settings.getRunfilesSymlinksCreated()
== options.getOptions(CoreOptions.class).buildRunfiles);
ExecutionOptions executionOptions = options.getOptions(ExecutionOptions.class);
Path tmpDirRoot = TestStrategy.getTmpRoot(
env.getWorkspace(), env.getExecRoot(), executionOptions);
PathFragment relativeTmpDir = tmpDirRoot.relativeTo(env.getExecRoot());
Duration timeout =
configuration
.getFragment(TestConfiguration.class)
.getTestTimeout()
.get(testAction.getTestProperties().getTimeout());
runEnvironment.putAll(testPolicy.computeTestEnvironment(
testAction,
env.getClientEnv(),
timeout,
settings.getRunfilesDir().relativeTo(env.getExecRoot()),
relativeTmpDir.getRelative(TestStrategy.getTmpDirName(testAction))));
workingDir = env.getExecRoot();
if (!prepareTestEnvironment(env, testAction)) {
return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
try {
cmdLine.addAll(TestStrategy.getArgs(testAction));
cmdLine.addAll(commandLineArgs);
prettyCmdLine.addAll(cmdLine);
} catch (ExecException e) {
env.getReporter().handle(Event.error(e.getMessage()));
return BlazeCommandResult.exitCode(ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
} else {
workingDir = runfilesDir;
List<String> args = computeArgs(env, targetToRun, commandLineArgs);
try {
constructCommandLine(
cmdLine, prettyCmdLine, env, configuration, targetToRun, runUnderTarget, args);
} catch (NoShellFoundException e) {
env.getReporter()
.handle(
Event.error(
"the \"run\" command needs a shell with \"--run_under\"; use the"
+ " --shell_executable=<path> flag to specify its path, e.g."
+ " --shell_executable=/bin/bash"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
}
if (runOptions.scriptPath != null) {
String unisolatedCommand = CommandFailureUtils.describeCommand(
CommandDescriptionForm.COMPLETE_UNISOLATED,
/* prettyPrintArgs= */ false,
cmdLine,
runEnvironment,
workingDir.getPathString());
PathFragment shExecutable = ShToolchain.getPath(configuration);
if (shExecutable.isEmpty()) {
env.getReporter()
.handle(
Event.error(
"the \"run\" command needs a shell with \"--script_path\"; use the"
+ " --shell_executable=<path> flag to specify its path, e.g."
+ " --shell_executable=/bin/bash"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
if (writeScript(env, shExecutable, runOptions.scriptPath, unisolatedCommand)) {
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
} else {
return BlazeCommandResult.exitCode(ExitCode.RUN_FAILURE);
}
}
// We need to do update runEnvironment so that the environment of --batch is not contaminated
// with that required for the server. Note that some differences between the environment of
// the process being run and the environment of the client are still possible if the environment
// variables added for the server were not in the original client environment.
//
// This is done after writing the script for --script_path so that that is not contaminated
// with the original client environment (CommandFailureUtils.describeCommand() puts
// runEnvironment into the written script)
boolean batchMode = env.getRuntime().getStartupOptionsProvider()
.getOptions(BlazeServerStartupOptions.class).batch;
if (batchMode) {
runEnvironment.putAll(env.getClientEnv());
}
env.getReporter().handle(Event.info(
null, "Running command line: " + ShellEscaper.escapeJoinAll(prettyCmdLine)));
ExecRequest.Builder execDescription = ExecRequest.newBuilder()
.setWorkingDirectory(
ByteString.copyFrom(workingDir.getPathString(), StandardCharsets.ISO_8859_1));
if (OS.getCurrent() == OS.WINDOWS && runOptions.bashlessRun) {
for (String arg : cmdLine) {
execDescription.addArgv(
ByteString.copyFrom(ShellUtils.windowsEscapeArg(arg), StandardCharsets.ISO_8859_1));
}
} else {
PathFragment shExecutable = ShToolchain.getPath(configuration);
if (shExecutable.isEmpty()) {
env.getReporter()
.handle(
Event.error(
"the \"run\" command needs a shell with; use the --shell_executable=<path> "
+ "flag to specify the shell's path, e.g. --shell_executable=/bin/bash"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
String shellEscaped = ShellEscaper.escapeJoinAll(cmdLine);
if (OS.getCurrent() == OS.WINDOWS) {
// On Windows, we run Bash as a subprocess of the client (via CreateProcessW).
// Bash uses its own (Bash-style) flag parsing logic, not the default logic for which
// ShellUtils.windowsEscapeArg escapes, so we escape the flags once again Bash-style.
shellEscaped = "\"" + shellEscaped.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
ImmutableList<String> shellCmdLine =
ImmutableList.<String>of(shExecutable.getPathString(), "-c", shellEscaped);
for (String arg : shellCmdLine) {
execDescription.addArgv(ByteString.copyFrom(arg, StandardCharsets.ISO_8859_1));
}
}
for (Map.Entry<String, String> variable : runEnvironment.entrySet()) {
execDescription.addEnvironmentVariable(EnvironmentVariable.newBuilder()
.setName(ByteString.copyFrom(variable.getKey(), StandardCharsets.ISO_8859_1))
.setValue(ByteString.copyFrom(variable.getValue(), StandardCharsets.ISO_8859_1))
.build());
}
return BlazeCommandResult.execute(execDescription.build());
}
private boolean prepareTestEnvironment(CommandEnvironment env, TestRunnerAction action) {
try {
action.prepare(env.getExecRoot());
return true;
} catch (IOException e) {
env.getReporter().handle(Event.error("Error while setting up test: " + e.getMessage()));
return false;
}
}
/**
* Ensures that runfiles are built for the specified target. If they already
* are, does nothing, otherwise builds them.
*/
private Path ensureRunfilesBuilt(CommandEnvironment env, RunfilesSupport runfilesSupport,
BuildConfiguration configuration) throws CommandException {
Artifact manifest = Preconditions.checkNotNull(runfilesSupport.getRunfilesManifest());
PathFragment runfilesDir = runfilesSupport.getRunfilesDirectoryExecPath();
Path workingDir = env.getExecRoot().getRelative(runfilesDir);
// On Windows, runfiles tree is disabled.
// Workspace name directory doesn't exist, so don't add it.
if (configuration.runfilesEnabled()) {
workingDir = workingDir.getRelative(runfilesSupport.getRunfiles().getSuffix());
}
// When runfiles are not generated, getManifest() returns the
// .runfiles_manifest file, otherwise it returns the MANIFEST file. This is
// a handy way to check whether runfiles were built or not.
if (!RUNFILES_MANIFEST.matches(manifest.getFilename())) {
// Runfiles already built, nothing to do.
return workingDir;
}
SymlinkTreeHelper helper = new SymlinkTreeHelper(
manifest.getPath(),
runfilesSupport.getRunfilesDirectory(),
false);
helper.createSymlinksUsingCommand(
env.getExecRoot(), env.getBlazeWorkspace().getBinTools(),
/* shellEnvironment= */ ImmutableMap.of(), /* outErr= */ null);
return workingDir;
}
private boolean writeScript(
CommandEnvironment env,
PathFragment shellExecutable,
PathFragment scriptPathFrag,
String cmd) {
Path scriptPath = env.getWorkingDirectory().getRelative(scriptPathFrag);
try {
if (OS.getCurrent() == OS.WINDOWS) {
FileSystemUtils.writeContent(
scriptPath,
StandardCharsets.ISO_8859_1,
"@echo off\n" + cmd + " %*");
scriptPath.setExecutable(true);
} else {
FileSystemUtils.writeContent(
scriptPath,
StandardCharsets.ISO_8859_1,
"#!" + shellExecutable.getPathString() + "\n" + cmd + " \"$@\"");
scriptPath.setExecutable(true);
}
} catch (IOException e) {
env.getReporter().handle(Event.error("Error writing run script:" + e.getMessage()));
return false;
}
return true;
}
// Make sure we are building exactly 1 binary target.
// If keepGoing, we'll build all the targets even if they are non-binary.
private void validateTargets(
Reporter reporter,
List<String> targetPatternStrings,
Collection<Target> targets,
boolean keepGoing)
throws LoadingFailedException {
Target targetToRun = null;
Target runUnderTarget = null;
boolean singleTargetWarningWasOutput = false;
int maxTargets = currentRunUnder != null && currentRunUnder.getLabel() != null ? 2 : 1;
if (targets.size() > maxTargets) {
warningOrException(
reporter,
makeErrorMessageForNotHavingASingleTarget(
targetPatternStrings.get(0),
Iterables.transform(targets, t -> t.getLabel().toString())),
keepGoing);
singleTargetWarningWasOutput = true;
}
for (Target target : targets) {
String targetError = validateTarget(target);
if (targetError != null) {
warningOrException(reporter, targetError, keepGoing);
}
if (currentRunUnder != null && target.getLabel().equals(currentRunUnder.getLabel())) {
// It's impossible to have two targets with the same label.
Preconditions.checkState(runUnderTarget == null);
runUnderTarget = target;
} else if (targetToRun == null) {
targetToRun = target;
} else {
if (!singleTargetWarningWasOutput) {
warningOrException(
reporter,
makeErrorMessageForNotHavingASingleTarget(
targetPatternStrings.get(0),
Iterables.transform(targets, t -> t.getLabel().toString())),
keepGoing);
}
return;
}
}
// Handle target & run_under referring to the same target.
if ((targetToRun == null) && (runUnderTarget != null)) {
targetToRun = runUnderTarget;
}
if (targetToRun == null) {
warningOrException(reporter, NO_TARGET_MESSAGE, keepGoing);
}
}
// If keepGoing, print a warning and return the given collection.
// Otherwise, throw InvalidTargetException.
private void warningOrException(Reporter reporter, String message,
boolean keepGoing) throws LoadingFailedException {
if (keepGoing) {
reporter.handle(Event.warn(message + ". Will continue anyway"));
} else {
throw new LoadingFailedException(message);
}
}
private static String notExecutableError(Target target) {
return "Cannot run target " + target.getLabel() + ": Not executable";
}
/** Returns null if the target is a runnable rule, or an appropriate error message otherwise. */
private static String validateTarget(Target target) {
return isExecutable(target)
? null
: notExecutableError(target);
}
/**
* Performs all available validation checks on an individual target.
*
* @param configuredTarget ConfiguredTarget to validate
* @return BlazeCommandResult.exitCode(ExitCode.SUCCESS) if all checks succeeded, otherwise a
* different error code.
* @throws IllegalStateException if unable to find a target from the package manager.
*/
private ExitCode fullyValidateTarget(CommandEnvironment env, ConfiguredTarget configuredTarget) {
Target target;
try {
target = env.getPackageManager().getTarget(env.getReporter(), configuredTarget.getLabel());
} catch (NoSuchTargetException | NoSuchPackageException | InterruptedException e) {
env.getReporter().handle(Event.error("Failed to find a target to validate. " + e));
throw new IllegalStateException("Failed to find a target to validate");
}
String targetError = validateTarget(target);
if (targetError != null) {
env.getReporter().handle(Event.error(targetError));
return ExitCode.COMMAND_LINE_ERROR;
}
Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
if (executable == null) {
env.getReporter().handle(Event.error(notExecutableError(target)));
return ExitCode.COMMAND_LINE_ERROR;
}
// Shouldn't happen: We just validated the target.
Preconditions.checkState(
executable != null, "Could not find executable for target %s", configuredTarget);
Path executablePath = executable.getPath();
try {
if (!executablePath.exists() || !executablePath.isExecutable()) {
env.getReporter().handle(Event.error(
null, "Non-existent or non-executable " + executablePath));
return ExitCode.BLAZE_INTERNAL_ERROR;
}
} catch (IOException e) {
env.getReporter().handle(Event.error(
"Error checking " + executablePath.getPathString() + ": " + e.getMessage()));
return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
}
return ExitCode.SUCCESS;
}
/**
* Return true iff it is possible that {@code target} is a rule that has an executable file. This
* *_test rules, *_binary rules, aliases, generated outputs, and inputs.
*
* <p>Determining definitively whether a rule produces an executable can only be done after
* analysis; this is only an early sanity check to quickly catch most mistakes.
*/
private static boolean isExecutable(Target target) {
return isPlainFile(target)
|| isExecutableNonTestRule(target)
|| TargetUtils.isTestRule(target)
|| TargetUtils.isAlias(target);
}
/**
* Return true iff {@code target} is a rule that generates an executable file and is user-executed
* code.
*/
private static boolean isExecutableNonTestRule(Target target) {
if (!(target instanceof Rule)) {
return false;
}
Rule rule = ((Rule) target);
if (rule.getRuleClassObject().hasAttr("$is_executable", Type.BOOLEAN)) {
return NonconfigurableAttributeMapper.of(rule).get("$is_executable", Type.BOOLEAN);
}
return false;
}
private static boolean isPlainFile(Target target) {
return (target instanceof OutputFile) || (target instanceof InputFile);
}
private String makeErrorMessageForNotHavingASingleTarget(
String targetPatternString, Iterable<String> expandedTargetNames) {
final int maxNumExpandedTargetsToIncludeInErrorMessage = 5;
boolean truncateTargetNameList = Iterables.size(expandedTargetNames) > 5;
Iterable<String> targetNamesToIncludeInErrorMessage =
truncateTargetNameList
? Iterables.limit(expandedTargetNames, maxNumExpandedTargetsToIncludeInErrorMessage)
: expandedTargetNames;
return String.format(
"Only a single target can be run. Your target pattern %s expanded to the targets %s%s",
targetPatternString,
Joiner.on(", ").join(ImmutableSortedSet.copyOf(targetNamesToIncludeInErrorMessage)),
truncateTargetNameList ? "[TRUNCATED]" : "");
}
}