diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java new file mode 100644 index 0000000000..b128d372ab --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java @@ -0,0 +1,519 @@ +// Copyright 2014 Google Inc. 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.Preconditions; +import com.google.common.collect.ImmutableList; +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.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.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.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.BlazeRuntime; +import com.google.devtools.build.lib.runtime.Command; +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.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.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, + binaryStdOut = true, + 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 Blaze. " + + "Use 'blaze run --script_path=foo //foo && foo' to invoke target '//foo' " + + "This differs from 'blaze run //foo' in that the Blaze 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"; + + // 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 BlazeRuntime runtime, BuildRequest request) { + return runtime.getBuildTool().processRequest(request, new TargetValidator() { + @Override + public void validateTargets(Collection<Target> targets, boolean keepGoing) + throws LoadingFailedException { + RunCommand.this.validateTargets(runtime.getReporter(), targets, keepGoing); + } + }); + } + + @Override + public void editOptions(BlazeRuntime runtime, OptionsParser optionsParser) { } + + @Override + public ExitCode exec(BlazeRuntime runtime, 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()) { + runtime.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 = runtime.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, + runtime.getStartupOptionsProvider(), targets, outErr, + runtime.getCommandId(), runtime.getCommandStartTime()); + if (request.getBuildOptions().compileOnly) { + String message = "The '" + getClass().getAnnotation(Command.class).name() + + "' command is incompatible with the --compile_only option"; + runtime.getReporter().handle(Event.error(message)); + return ExitCode.COMMAND_LINE_ERROR; + } + + currentRunUnder = runUnder; + BuildResult result; + try { + result = processRequest(runtime, request); + } finally { + currentRunUnder = null; + } + + if (!result.getSuccess()) { + runtime.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) { + runtime.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE)); + return ExitCode.COMMAND_LINE_ERROR; + } + for (ConfiguredTarget target : targetsBuilt) { + ExitCode targetValidation = fullyValidateTarget(runtime, target); + if (targetValidation != ExitCode.SUCCESS) { + return targetValidation; + } + if (runUnder != null && target.getLabel().equals(runUnder.getLabel())) { + if (runUnderTarget != null) { + runtime.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 { + runtime.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) { + runtime.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 = runtime.getBuildTool().getView().getConfigurationCollection() + .getTargetConfigurations().get(0); + } + Path workingDir; + try { + workingDir = ensureRunfilesBuilt(runtime, targetToRun); + } catch (CommandException e) { + runtime.getReporter().handle(Event.error("Error creating runfiles: " + e.getMessage())); + return ExitCode.LOCAL_ENVIRONMENTAL_ERROR; + } + + List<String> args = runTargetArgs; + + FilesToRunProvider provider = targetToRun.getProvider(FilesToRunProvider.class); + RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport(); + if (runfilesSupport != null && runfilesSupport.getArgs() != null) { + List<String> targetArgs = runfilesSupport.getArgs(); + if (!targetArgs.isEmpty()) { + args = Lists.newArrayListWithCapacity(targetArgs.size() + runTargetArgs.size()); + args.addAll(targetArgs); + args.addAll(runTargetArgs); + } + } + + // + // 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, + runtime.getWorkspaceName(), runtime.getWorkspace(), + options.getOptions(BuildRequestOptions.class).symlinkPrefix); + List<String> cmdLine = new ArrayList<>(); + if (runOptions.scriptPath == null) { + cmdLine.add(runtime.getDirectories().getExecRoot() + .getRelative(runtime.getBinTools().getExecPath(PROCESS_WRAPPER)).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.getShExecutable().getPathString()); + cmdLine.add("-c"); + cmdLine.add(runUnderValue + " " + executablePath.getPathString() + " " + + ShellEscaper.escapeJoinAll(args)); + prettyCmdLine.add(configuration.getShExecutable().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(runtime, runOptions.scriptPath, unisolatedCommand)) { + return ExitCode.SUCCESS; + } else { + return ExitCode.RUN_FAILURE; + } + } + + runtime.getReporter().handle(Event.info( + null, "Running command line: " + ShellEscaper.escapeJoinAll(prettyCmdLine))); + + com.google.devtools.build.lib.shell.Command command = new CommandBuilder() + .addArgs(cmdLine).setEnv(runtime.getClientEnv()).setWorkingDir(workingDir).build(); + + try { + // 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(); + runtime.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) { + runtime.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(BlazeRuntime runtime, ConfiguredTarget target) + throws CommandException { + FilesToRunProvider provider = target.getProvider(FilesToRunProvider.class); + RunfilesSupport runfilesSupport = provider == null ? null : provider.getRunfilesSupport(); + if (runfilesSupport == null) { + return runtime.getWorkingDirectory(); + } + + Artifact manifest = runfilesSupport.getRunfilesManifest(); + PathFragment runfilesDir = runfilesSupport.getRunfilesDirectoryExecPath(); + Path workingDir = runtime.getExecRoot() + .getRelative(runfilesDir) + .getRelative(runtime.getRunfilesPrefix()); + + // 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.getExecPath(), + runfilesDir, + false); + helper.createSymlinksUsingCommand(runtime.getExecRoot(), target.getConfiguration(), + runtime.getBinTools()); + return workingDir; + } + + private boolean writeScript(BlazeRuntime runtime, PathFragment scriptPathFrag, String cmd) { + final String SH_SHEBANG = "#!/bin/sh"; + Path scriptPath = runtime.getWorkingDirectory().getRelative(scriptPathFrag); + try { + FileSystemUtils.writeContent(scriptPath, StandardCharsets.ISO_8859_1, + SH_SHEBANG + "\n" + cmd + " \"$@\""); + scriptPath.setExecutable(true); + } catch (IOException e) { + runtime.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(BlazeRuntime runtime, ConfiguredTarget target) { + String targetError = validateTarget(target.getTarget()); + + if (targetError != null) { + runtime.getReporter().handle(Event.error(targetError)); + return ExitCode.COMMAND_LINE_ERROR; + } + + Artifact executable = target.getProvider(FilesToRunProvider.class).getExecutable(); + if (executable == null) { + runtime.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()) { + runtime.getReporter().handle(Event.error( + null, "Non-existent or non-executable " + executablePath)); + return ExitCode.BLAZE_INTERNAL_ERROR; + } + } catch (IOException e) { + runtime.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, and generated outputs. + */ + private static boolean isExecutable(Target target) { + return isOutputFile(target) || isExecutableNonTestRule(target) + || TargetUtils.isTestRule(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 isOutputFile(Target target) { + return (target instanceof OutputFile); + } +} |