// 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.collect.ImmutableList; 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.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.actions.CommandLine; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.analysis.config.RunUnder; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.buildtool.BuildRequest.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.TargetValidator; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.exec.SymlinkTreeHelper; import com.google.devtools.build.lib.packages.InputFile; 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.pkgcache.LoadingFailedException; import com.google.devtools.build.lib.runtime.BlazeCommand; import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.shell.AbnormalTerminationException; import com.google.devtools.build.lib.shell.BadExitStatusException; import com.google.devtools.build.lib.shell.CommandException; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.CommandBuilder; 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.OptionsUtils; import com.google.devtools.build.lib.util.OsUtils; import com.google.devtools.build.lib.util.Preconditions; 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.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsProvider; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * 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, binaryStdOut = true, completion = "label-bin", binaryStdErr = true) public class RunCommand implements BlazeCommand { public static class RunOptions extends OptionsBase { @Option( name = "script_path", category = "run", defaultValue = "null", 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; } @VisibleForTesting public static final String SINGLE_TARGET_MESSAGE = "Blaze can only run a single target. " + "Do not use wildcards that match more than one target"; @VisibleForTesting public static final String NO_TARGET_MESSAGE = "No targets found to run"; private static final String PROCESS_WRAPPER = "process-wrapper" + OsUtils.executableExtension(); // Value of --run_under as of the most recent command invocation. private RunUnder currentRunUnder; private static final FileType RUNFILES_MANIFEST = FileType.of(".runfiles_manifest"); @VisibleForTesting // productionVisibility = Visibility.PRIVATE protected BuildResult processRequest(final CommandEnvironment env, BuildRequest request) { return new BuildTool(env).processRequest(request, new TargetValidator() { @Override public void validateTargets(Collection<Target> targets, boolean keepGoing) throws LoadingFailedException { RunCommand.this.validateTargets(env.getReporter(), targets, keepGoing); } }); } @Override public void editOptions(CommandEnvironment env, OptionsParser optionsParser) { } @Override public ExitCode exec(CommandEnvironment env, OptionsProvider 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 ExitCode.COMMAND_LINE_ERROR; } String targetString = targetAndArgs.get(0); List<String> runTargetArgs = targetAndArgs.subList(1, targetAndArgs.size()); RunUnder runUnder = options.getOptions(BuildConfiguration.Options.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 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(SINGLE_TARGET_MESSAGE)); return ExitCode.COMMAND_LINE_ERROR; } for (ConfiguredTarget target : targetsBuilt) { ExitCode targetValidation = fullyValidateTarget(env, target); if (!targetValidation.equals(ExitCode.SUCCESS)) { return 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 ExitCode.COMMAND_LINE_ERROR; } runUnderTarget = target; } else if (targetToRun == null) { targetToRun = target; } else { env.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE)); return 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 ExitCode.COMMAND_LINE_ERROR; } Path executablePath = Preconditions.checkNotNull( targetToRun.getProvider(FilesToRunProvider.class).getExecutable().getPath()); BuildConfiguration configuration = targetToRun.getConfiguration(); 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); } Path workingDir; try { workingDir = ensureRunfilesBuilt(env, targetToRun); } catch (CommandException e) { env.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage())); return ExitCode.LOCAL_ENVIRONMENTAL_ERROR; } 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(); Iterables.addAll(args, targetArgs.arguments()); } args.addAll(runTargetArgs); String productName = env.getRuntime().getProductName(); // // We now have a unique executable ready to be run. // // We build up two different versions of the command to run: one with an absolute path, which // we'll actually run, and a prettier one with the long absolute path to the executable // replaced with a shorter relative path that uses the symlinks in the workspace. PathFragment prettyExecutablePath = OutputDirectoryLinksUtils.getPrettyPath(executablePath, env.getWorkspaceName(), env.getWorkspace(), options.getOptions(BuildRequestOptions.class).getSymlinkPrefix(productName), productName); List<String> cmdLine = new ArrayList<>(); if (runOptions.scriptPath == null) { PathFragment processWrapperPath = env.getBlazeWorkspace().getBinTools().getExecPath(PROCESS_WRAPPER); Preconditions.checkNotNull( processWrapperPath, PROCESS_WRAPPER + " not found in embedded tools"); cmdLine.add(env.getExecRoot().getRelative(processWrapperPath).getPathString()); cmdLine.add("-1"); cmdLine.add("15"); cmdLine.add("-"); cmdLine.add("-"); } List<String> prettyCmdLine = new ArrayList<>(); // 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); } } cmdLine.add(configuration.getShellExecutable().getPathString()); cmdLine.add("-c"); cmdLine.add(runUnderValue + " " + executablePath.getPathString() + " " + ShellEscaper.escapeJoinAll(args)); prettyCmdLine.add(configuration.getShellExecutable().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); } // Add a newline between the blaze output and the binary's output. outErr.printErrLn(""); if (runOptions.scriptPath != null) { String unisolatedCommand = CommandFailureUtils.describeCommand( CommandDescriptionForm.COMPLETE_UNISOLATED, cmdLine, null, workingDir.getPathString()); if (writeScript(env, runOptions.scriptPath, unisolatedCommand)) { return ExitCode.SUCCESS; } else { return ExitCode.RUN_FAILURE; } } env.getReporter().handle(Event.info( null, "Running command line: " + ShellEscaper.escapeJoinAll(prettyCmdLine))); com.google.devtools.build.lib.shell.Command command = new CommandBuilder() .addArgs(cmdLine).setEnv(env.getClientEnv()).setWorkingDir(workingDir).build(); try { // Restore a raw EventHandler if it is registered. This allows for blaze run to produce the // actual output of the command being run even if --color=no is specified. env.getReporter().switchToAnsiAllowingHandler(); // The command API is a little strange in that the following statement // will return normally only if the program exits with exit code 0. // If it ends with any other code, we have to catch BadExitStatusException. command.execute(com.google.devtools.build.lib.shell.Command.NO_INPUT, com.google.devtools.build.lib.shell.Command.NO_OBSERVER, outErr.getOutputStream(), outErr.getErrorStream(), true /* interruptible */).getTerminationStatus().getExitCode(); return ExitCode.SUCCESS; } catch (BadExitStatusException e) { String message = "Non-zero return code '" + e.getResult().getTerminationStatus().getExitCode() + "' from command: " + e.getMessage(); env.getReporter().handle(Event.error(message)); return ExitCode.RUN_FAILURE; } catch (AbnormalTerminationException e) { // The process was likely terminated by a signal in this case. return ExitCode.INTERRUPTED; } catch (CommandException e) { env.getReporter().handle(Event.error("Error running program: " + e.getMessage())); return ExitCode.RUN_FAILURE; } } /** * Ensures that runfiles are built for the specified target. If they already * are, does nothing, otherwise builds them. * * @param target the target to build runfiles for. * @return the path of the runfiles directory. * @throws CommandException */ private Path ensureRunfilesBuilt(CommandEnvironment env, ConfiguredTarget target) throws CommandException { FilesToRunProvider provider = target.getProvider(FilesToRunProvider.class); RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport(); if (runfilesSupport == null) { return env.getWorkingDirectory(); } Artifact manifest = 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 (target.getConfiguration().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(), target.getConfiguration(), env.getBlazeWorkspace().getBinTools()); return workingDir; } private boolean writeScript(CommandEnvironment env, PathFragment scriptPathFrag, String cmd) { final String SH_SHEBANG = "#!/bin/sh"; Path scriptPath = env.getWorkingDirectory().getRelative(scriptPathFrag); try { FileSystemUtils.writeContent(scriptPath, StandardCharsets.ISO_8859_1, SH_SHEBANG + "\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, 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, SINGLE_TARGET_MESSAGE, 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, SINGLE_TARGET_MESSAGE, 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 target ConfiguredTarget to validate * @return ExitCode.SUCCESS if all checks succeeded, otherwise a different error code. */ private ExitCode fullyValidateTarget(CommandEnvironment env, ConfiguredTarget target) { String targetError = validateTarget(target.getTarget()); if (targetError != null) { env.getReporter().handle(Event.error(targetError)); return ExitCode.COMMAND_LINE_ERROR; } Artifact executable = target.getProvider(FilesToRunProvider.class).getExecutable(); if (executable == null) { env.getReporter().handle(Event.error(notExecutableError(target.getTarget()))); return ExitCode.COMMAND_LINE_ERROR; } // Shouldn't happen: We just validated the target. Preconditions.checkState(executable != null, "Could not find executable for target %s", target); 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 {@code target} is a rule that has an executable file. This includes * *_test rules, *_binary rules, generated outputs, and inputs. */ private static boolean isExecutable(Target target) { return isPlainFile(target) || isExecutableNonTestRule(target) || TargetUtils.isTestRule(target) || isAliasRule(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 static boolean isAliasRule(Target target) { if (!(target instanceof Rule)) { return false; } Rule rule = (Rule) target; return rule.getRuleClass().equals("alias") || rule.getRuleClass().equals("bind"); } }