blob: 31b3ebc4f5d2fae54607621bb15e17d16a73c06e [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;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.runtime.commands.ProjectFileSupport;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.InvocationPolicyEnforcer;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionValueDescription;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import com.google.devtools.common.options.ParsedOptionDescription;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
/**
* Handles parsing the blaze command arguments.
*
* <p>This class manages rc options, configs, default options, and invocation policy.
*/
public final class BlazeOptionHandler {
// Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
private static final ImmutableSet<String> INTERNAL_COMMAND_OPTIONS =
ImmutableSet.of(
"rc_source",
"default_override",
"isatty",
"terminal_columns",
"ignore_client_env",
"client_env",
"client_cwd");
private final BlazeRuntime runtime;
private final OptionsParser optionsParser;
private final BlazeWorkspace workspace;
private final BlazeCommand command;
private final Command commandAnnotation;
private final InvocationPolicy invocationPolicy;
private final List<String> rcfileNotes = new ArrayList<>();
BlazeOptionHandler(
BlazeRuntime runtime,
BlazeWorkspace workspace,
BlazeCommand command,
Command commandAnnotation,
OptionsParser optionsParser,
InvocationPolicy invocationPolicy) {
this.runtime = runtime;
this.workspace = workspace;
this.command = command;
this.commandAnnotation = commandAnnotation;
this.optionsParser = optionsParser;
this.invocationPolicy = invocationPolicy;
}
/**
* Return options as {@link OptionsParsingResult} so the options can't be easily modified after
* we've applied the invocation policy.
*/
OptionsParsingResult getOptionsResult() {
return optionsParser;
}
public List<String> getRcfileNotes() {
return rcfileNotes;
}
/**
* Only some commands work if cwd != workspaceSuffix in Blaze. In that case, also check if Blaze
* was called from the output directory and fail if it was.
*/
private ExitCode checkCwdInWorkspace(EventHandler eventHandler) {
if (!commandAnnotation.mustRunInWorkspace()) {
return ExitCode.SUCCESS;
}
if (!workspace.getDirectories().inWorkspace()) {
eventHandler.handle(
Event.error(
"The '"
+ commandAnnotation.name()
+ "' command is only supported from within a workspace"
+ " (below a directory having a WORKSPACE file).\n"
+ "See documentation at"
+ " https://docs.bazel.build/versions/master/build-ref.html#workspace"));
return ExitCode.COMMAND_LINE_ERROR;
}
Path workspacePath = workspace.getWorkspace();
// TODO(kchodorow): Remove this once spaces are supported.
if (workspacePath.getPathString().contains(" ")) {
eventHandler.handle(
Event.error(
runtime.getProductName()
+ " does not currently work properly from paths "
+ "containing spaces ("
+ workspace
+ ")."));
return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
}
if (workspacePath.getParentDirectory() != null) {
Path doNotBuild =
workspacePath.getParentDirectory().getRelative(BlazeWorkspace.DO_NOT_BUILD_FILE_NAME);
if (doNotBuild.exists()) {
if (!commandAnnotation.canRunInOutputDirectory()) {
eventHandler.handle(Event.error(getNotInRealWorkspaceError(doNotBuild)));
return ExitCode.COMMAND_LINE_ERROR;
} else {
eventHandler.handle(
Event.warn(
runtime.getProductName() + " is run from output directory. This is unsound."));
}
}
}
return ExitCode.SUCCESS;
}
/**
* Parses the unconditional options from .rc files for the current command.
*
* <p>This is not as trivial as simply taking the list of options for the specified command
* because commands can inherit arguments from each other, and we have to respect that (e.g. if an
* option is specified for 'build', it needs to take effect for the 'test' command, too). More
* specific commands should have priority over the broader commands (say a "build" option that
* conflicts with a "common" option should override the common one regardless of order.)
*
* <p>For each command, the options are parsed in rc order. This uses the master rc file first,
* and follows import statements. This is the order in which they were passed by the client.
*/
void parseRcOptions(
EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
throws OptionsParsingException {
for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
// Get all args defined for this command (or "common"), grouped by rc chunk.
for (RcChunkOfArgs rcArgs : commandToRcArgs.get(commandToParse)) {
if (!rcArgs.args.isEmpty()) {
String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
String source =
rcArgs.rcFile.equals("client")
? "Options provided by the client"
: String.format(
"Reading rc options for '%s' from %s",
commandAnnotation.name(), rcArgs.rcFile);
rcfileNotes.add(
String.format(
"%s:\n %s'%s' options: %s",
source, inherited, commandToParse, Joiner.on(' ').join(rcArgs.args)));
}
optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.rcFile, rcArgs.args);
}
}
}
private void parseArgsAndConfigs(List<String> args, ExtendedEventHandler eventHandler)
throws OptionsParsingException {
Path workspaceDirectory = workspace.getWorkspace();
// TODO(ulfjack): The working directory is passed by the client as part of CommonCommandOptions,
// and we can't know it until after we've parsed the options, so use the workspace for now.
Path workingDirectory = workspace.getWorkspace();
Function<OptionDefinition, String> commandOptionSourceFunction =
option -> {
if (INTERNAL_COMMAND_OPTIONS.contains(option.getOptionName())) {
return "options generated by " + runtime.getProductName() + " launcher";
} else {
return "command line options";
}
};
// Explicit command-line options:
List<String> cmdLineAfterCommand = args.subList(1, args.size());
optionsParser.parseWithSourceFunction(
PriorityCategory.COMMAND_LINE, commandOptionSourceFunction, cmdLineAfterCommand);
// Command-specific options from .blazerc passed in via --default_override and --rc_source.
ClientOptions rcFileOptions = optionsParser.getOptions(ClientOptions.class);
ListMultimap<String, RcChunkOfArgs> commandToRcArgs =
structureRcOptionsAndConfigs(
eventHandler,
rcFileOptions.rcSource,
rcFileOptions.optionsOverrides,
runtime.getCommandMap().keySet());
parseRcOptions(eventHandler, commandToRcArgs);
if (commandAnnotation.builds()) {
// splits project files from targets in the traditional sense
ProjectFileSupport.handleProjectFiles(
eventHandler,
runtime.getProjectFileProvider(),
workspaceDirectory,
workingDirectory,
optionsParser,
commandAnnotation.name());
}
expandConfigOptions(eventHandler, commandToRcArgs);
}
/**
* TODO(bazel-team): When we move BuildConfiguration.Options options to be defined in starlark,
* make sure they're not passed in here during {@link #getOptionsResult}.
*/
ExitCode parseStarlarkOptions(CommandEnvironment env, ExtendedEventHandler eventHandler) {
// For now, restrict starlark options to commands that already build to ensure that loading
// will work. We may want to open this up to other commands in the future. The "info"
// and "clean" commands have builds=true set in their annotation but don't actually do any
// building (b/120041419).
if (!commandAnnotation.builds()
|| commandAnnotation.name().equals("info")
|| commandAnnotation.name().equals("clean")) {
return ExitCode.SUCCESS;
}
try {
StarlarkOptionsParser.newStarlarkOptionsParser(env, optionsParser, runtime)
.parse(commandAnnotation, eventHandler);
} catch (OptionsParsingException e) {
eventHandler.handle(Event.error(e.getMessage()));
return ExitCode.COMMAND_LINE_ERROR;
}
return ExitCode.SUCCESS;
}
/**
* Parses the options, taking care not to generate any output to outErr, return, or throw an
* exception.
*
* @return ExitCode.SUCCESS if everything went well, or some other value if not
*/
ExitCode parseOptions(List<String> args, ExtendedEventHandler eventHandler) {
// The initialization code here was carefully written to parse the options early before we call
// into the BlazeModule APIs, which means we must not generate any output to outErr, return, or
// throw an exception. All the events happening here are instead stored in a temporary event
// handler, and later replayed.
ExitCode earlyExitCode = checkCwdInWorkspace(eventHandler);
if (!earlyExitCode.equals(ExitCode.SUCCESS)) {
return earlyExitCode;
}
try {
parseArgsAndConfigs(args, eventHandler);
// Allow the command to edit the options.
command.editOptions(optionsParser);
// Migration of --watchfs to a command option.
// TODO(ulfjack): Get rid of the startup option and drop this code.
if (runtime.getStartupOptionsProvider().getOptions(BlazeServerStartupOptions.class).watchFS) {
try {
optionsParser.parse("--watchfs");
} catch (OptionsParsingException e) {
// This should never happen.
throw new IllegalStateException(e);
}
}
// Merge the invocation policy that is user-supplied, from the command line, and any
// invocation policy that was added by a module. The module one goes 'first,' so the user
// one has priority.
InvocationPolicy combinedPolicy =
InvocationPolicy.newBuilder()
.mergeFrom(runtime.getModuleInvocationPolicy())
.mergeFrom(invocationPolicy)
.build();
InvocationPolicyEnforcer optionsPolicyEnforcer =
new InvocationPolicyEnforcer(combinedPolicy, Level.INFO);
// Enforce the invocation policy. It is intentional that this is the last step in preparing
// the options. The invocation policy is used in security-critical contexts, and may be used
// as a last resort to override flags. That means that the policy can override flags set in
// BlazeCommand.editOptions, so the code needs to be safe regardless of the actual flag
// values. At the time of this writing, editOptions was only used as a convenience feature or
// to improve the user experience, but not required for safety or correctness.
optionsPolicyEnforcer.enforce(optionsParser, commandAnnotation.name());
// Print warnings for odd options usage
for (String warning : optionsParser.getWarnings()) {
eventHandler.handle(Event.warn(warning));
}
CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
for (String warning : commonOptions.deprecationWarnings) {
eventHandler.handle(Event.warn(warning));
}
} catch (OptionsParsingException e) {
eventHandler.handle(Event.error(e.getMessage()));
return ExitCode.COMMAND_LINE_ERROR;
}
return ExitCode.SUCCESS;
}
/**
* Expand the values of --config according to the definitions provided in the rc files and the
* applicable command.
*/
void expandConfigOptions(
EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
throws OptionsParsingException {
OptionValueDescription configValueDescription =
optionsParser.getOptionValueDescription("config");
if (configValueDescription == null || configValueDescription.getCanonicalInstances() == null) {
// No --config values were set, we can avoid this whole thing.
return;
}
// Find the base set of configs. This does not include the config options that might be
// recursively incuded.
ImmutableList<ParsedOptionDescription> configInstances =
ImmutableList.copyOf(configValueDescription.getCanonicalInstances());
// Expand the configs that are mentioned in the input. Flatten these expansions before parsing
// them, to preserve order.
for (ParsedOptionDescription configInstance : configInstances) {
String configValueToExpand = (String) configInstance.getConvertedValue();
List<String> expansion = getExpansion(eventHandler, commandToRcArgs, configValueToExpand);
optionsParser.parseArgsAsExpansionOfOption(
configInstance, String.format("expanded from --%s", configValueToExpand), expansion);
}
// At this point, we've expanded everything, identify duplicates, if any, to warn about
// re-application.
List<String> configs = optionsParser.getOptions(CommonCommandOptions.class).configs;
Set<String> configSet = new HashSet<>();
LinkedHashSet<String> duplicateConfigs = new LinkedHashSet<>();
for (String configValue : configs) {
if (!configSet.add(configValue)) {
duplicateConfigs.add(configValue);
}
}
if (!duplicateConfigs.isEmpty()) {
eventHandler.handle(
Event.warn(
String.format(
"The following configs were expanded more than once: %s. For repeatable flags, "
+ "repeats are counted twice and may lead to unexpected behavior.",
duplicateConfigs)));
}
}
private List<String> getExpansion(
EventHandler eventHandler,
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
String configToExpand)
throws OptionsParsingException {
LinkedHashSet<String> configAncestorSet = new LinkedHashSet<>();
configAncestorSet.add(configToExpand);
List<String> longestChain = new ArrayList<>();
List<String> finalExpansion =
getExpansion(
eventHandler, commandToRcArgs, configAncestorSet, configToExpand, longestChain);
// In order to prevent warning about a long chain of 13 configs at the 10, 11, 12, and 13
// point, we identify the longest chain for this 'high-level' --config found and only warn
// about it once. This may mean we missed a fork where each branch was independently long
// enough to warn, but the single warning should convey the message reasonably.
if (longestChain.size() >= 10) {
eventHandler.handle(
Event.warn(
String.format(
"There is a recursive chain of configs %s configs long: %s. This seems "
+ "excessive, and might be hiding errors.",
longestChain.size(), longestChain)));
}
return finalExpansion;
}
/**
* @param configAncestorSet is the chain of configs that have led to this one getting expanded.
* This should only contain the configs that expanded, recursively, to this one, and should
* not contain "siblings," as it is used to detect cycles. {@code build:foo --config=bar},
* {@code build:bar --config=foo}, is a cycle, detected because this list will be [foo, bar]
* when we find another 'foo' to expand. However, {@code build:foo --config=bar}, {@code
* build:foo --config=bar} is not a cycle just because bar is expanded twice, and the 1st bar
* should not be in the parents list of the second bar.
* @param longestChain will be populated with the longest inheritance chain of configs.
*/
private List<String> getExpansion(
EventHandler eventHandler,
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
LinkedHashSet<String> configAncestorSet,
String configToExpand,
List<String> longestChain)
throws OptionsParsingException {
List<String> expansion = new ArrayList<>();
boolean foundDefinition = false;
// The expansion order of rc files is first by command priority, and then in the order the
// rc files were read, respecting import statement placement.
for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
String configDef = commandToParse + ":" + configToExpand;
for (RcChunkOfArgs rcArgs : commandToRcArgs.get(configDef)) {
foundDefinition = true;
rcfileNotes.add(
String.format(
"Found applicable config definition %s in file %s: %s",
configDef, rcArgs.rcFile, String.join(" ", rcArgs.args)));
// For each arg in the rcARgs chunk, we first check if it is a config, and if so, expand
// it in place. We avoid cycles by tracking the parents of this config.
for (String arg : rcArgs.args) {
expansion.add(arg);
if (arg.length() >= 8 && arg.substring(0, 8).equals("--config")) {
// We have a config. For sanity, because we don't want to worry about formatting,
// we will only accept --config=value, and will not accept value on a following line.
int charOfConfigValue = arg.indexOf('=');
if (charOfConfigValue < 0) {
throw new OptionsParsingException(
String.format(
"In file %s, the definition of config %s expands to another config "
+ "that either has no value or is not in the form --config=value. For "
+ "recursive config definitions, please do not provide the value in a "
+ "separate token, such as in the form '--config value'.",
rcArgs.rcFile, configToExpand));
}
String newConfigValue = arg.substring(charOfConfigValue + 1);
LinkedHashSet<String> extendedConfigAncestorSet =
new LinkedHashSet<>(configAncestorSet);
if (!extendedConfigAncestorSet.add(newConfigValue)) {
throw new OptionsParsingException(
String.format(
"Config expansion has a cycle: config value %s expands to itself, "
+ "see inheritance chain %s",
newConfigValue, extendedConfigAncestorSet));
}
if (extendedConfigAncestorSet.size() > longestChain.size()) {
longestChain.clear();
longestChain.addAll(extendedConfigAncestorSet);
}
expansion.addAll(
getExpansion(
eventHandler,
commandToRcArgs,
extendedConfigAncestorSet,
newConfigValue,
longestChain));
}
}
}
}
if (!foundDefinition) {
throw new OptionsParsingException(
"Config value " + configToExpand + " is not defined in any .rc file");
}
return expansion;
}
private static List<String> getCommandNamesToParse(Command commandAnnotation) {
List<String> result = new ArrayList<>();
result.add("common");
getCommandNamesToParseHelper(commandAnnotation, result);
return result;
}
private static void getCommandNamesToParseHelper(
Command commandAnnotation, List<String> accumulator) {
for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
getCommandNamesToParseHelper(base.getAnnotation(Command.class), accumulator);
}
accumulator.add(commandAnnotation.name());
}
private String getNotInRealWorkspaceError(Path doNotBuildFile) {
String message =
String.format(
"%1$s should not be called from a %1$s output directory. ", runtime.getProductName());
try {
String realWorkspace = new String(FileSystemUtils.readContentAsLatin1(doNotBuildFile));
message += String.format("The pertinent workspace directory is: '%s'", realWorkspace);
} catch (IOException e) {
// We are exiting anyway.
}
return message;
}
/**
* We receive the rc file arguments from the client in an order that maintains the location of
* "import" statements, expanding the imported rc file in place so that its args override previous
* args in the file and are overridden by later arguments. We cannot group the args by rc file for
* parsing, as we would lose this ordering, so we store them in these "chunks."
*
* <p>Each chunk comes from a single rc file, but the args stored here may not contain the entire
* file if its contents were interrupted by an import statement.
*/
static class RcChunkOfArgs {
public RcChunkOfArgs(String rcFile, List<String> args) {
this.rcFile = rcFile;
this.args = args;
}
// The name of the rc file, usually a path.
String rcFile;
// The list of arguments specified in this rc "chunk". This is all for a single command (or
// command:config definition), as different commands will be grouped together, so this list of
// arguments can all be parsed as a continuous group.
List<String> args;
@Override
public boolean equals(Object o) {
if (o instanceof RcChunkOfArgs) {
RcChunkOfArgs other = (RcChunkOfArgs) o;
return rcFile.equals(other.rcFile) && args.equals(other.args);
}
return false;
}
@Override
public int hashCode() {
return rcFile.hashCode() + args.hashCode();
}
}
/**
* The rc options are passed via {@link ClientOptions#optionsOverrides} and {@link
* ClientOptions#rcSource}, which is basically a line-by-line transfer of the rc files read by the
* client. This is not a particularly useful format for expanding the options, so this method
* structures the list so that it is easier to find the arguments that apply to a command, or to
* find the definitions of a config value.
*/
@VisibleForTesting
static ListMultimap<String, RcChunkOfArgs> structureRcOptionsAndConfigs(
EventHandler eventHandler,
List<String> rcFiles,
List<ClientOptions.OptionOverride> rawOverrides,
Set<String> validCommands) {
ListMultimap<String, RcChunkOfArgs> commandToRcArgs = ArrayListMultimap.create();
String lastRcFile = null;
ListMultimap<String, String> commandToArgMapForLastRc = null;
for (ClientOptions.OptionOverride override : rawOverrides) {
if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
eventHandler.handle(
Event.warn("inconsistency in generated command line args. Ignoring bogus argument\n"));
continue;
}
String rcFile = rcFiles.get(override.blazeRc);
String command = override.command;
int index = command.indexOf(':');
if (index > 0) {
command = command.substring(0, index);
}
if (!validCommands.contains(command) && !command.equals("common")) {
eventHandler.handle(
Event.warn(
"while reading option defaults file '"
+ rcFile
+ "':\n"
+ " invalid command name '"
+ override.command
+ "'."));
continue;
}
// We've moved on to another rc file "chunk," store the accumulated args from the last one.
if (!rcFile.equals(lastRcFile)) {
if (lastRcFile != null) {
// Go through the various commands identified in this rc file (or chunk of file) and
// store them grouped first by command, then by rc chunk.
for (String commandKey : commandToArgMapForLastRc.keySet()) {
commandToRcArgs.put(
commandKey,
new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
}
}
lastRcFile = rcFile;
commandToArgMapForLastRc = ArrayListMultimap.create();
}
commandToArgMapForLastRc.put(override.command, override.option);
}
if (lastRcFile != null) {
// Once again, for this last rc file chunk, store them grouped by command.
for (String commandKey : commandToArgMapForLastRc.keySet()) {
commandToRcArgs.put(
commandKey, new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
}
}
return commandToRcArgs;
}
}