blob: 31e86a78ad93089683fea16b8a8d0a054116f1bb [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.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.util.OS;
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.ParsedOptionDescription;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/** Encapsulates logic for performing --config option expansion. */
final class ConfigExpander {
private ConfigExpander() {}
private static String getPlatformName() {
switch (OS.getCurrent()) {
case LINUX:
return "linux";
case DARWIN:
return "macos";
case WINDOWS:
return "windows";
case FREEBSD:
return "freebsd";
case OPENBSD:
return "openbsd";
default:
return OS.getCurrent().getCanonicalName();
}
}
/**
* If --enable_platform_specific_config is true and the corresponding config definition exists, we
* should enable the platform specific config.
*/
private static boolean shouldEnablePlatformSpecificConfig(
OptionValueDescription enablePlatformSpecificConfigDescription,
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
List<String> commandsToParse) {
if (enablePlatformSpecificConfigDescription == null
|| !(boolean) enablePlatformSpecificConfigDescription.getValue()) {
return false;
}
for (String commandName : commandsToParse) {
String defaultConfigDef = commandName + ":" + getPlatformName();
if (commandToRcArgs.containsKey(defaultConfigDef)) {
return true;
}
}
return false;
}
/**
* Expands --config options present in the requested commands using the options configuration
* provided in commandToRcArgs.
*
* @param eventHandler collects any warnings encountered.
* @param rcFileNotesConsumer collects any informational messages encountered.
* @param optionsParser will parse the expanded --config representations.
* @throws OptionsParsingException if a fatal problem with the configuration is encountered.
*/
static void expandConfigOptions(
EventHandler eventHandler,
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
List<String> commandsToParse,
Consumer<String> rcFileNotesConsumer,
OptionsParser optionsParser)
throws OptionsParsingException {
OptionValueDescription configValueDescription =
optionsParser.getOptionValueDescription("config");
if (configValueDescription != null && configValueDescription.getCanonicalInstances() != null) {
// Find the base set of configs. This does not include the config options that might be
// recursively included.
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,
commandsToParse,
configValueToExpand,
rcFileNotesConsumer);
optionsParser.parseArgsAsExpansionOfOption(
configInstance, String.format("expanded from --%s", configValueToExpand), expansion);
}
}
OptionValueDescription enablePlatformSpecificConfigDescription =
optionsParser.getOptionValueDescription("enable_platform_specific_config");
if (shouldEnablePlatformSpecificConfig(
enablePlatformSpecificConfigDescription, commandToRcArgs, commandsToParse)) {
List<String> expansion =
getExpansion(
eventHandler,
commandToRcArgs,
commandsToParse,
getPlatformName(),
rcFileNotesConsumer);
optionsParser.parseArgsAsExpansionOfOption(
Iterables.getOnlyElement(enablePlatformSpecificConfigDescription.getCanonicalInstances()),
String.format("enabled by --enable_platform_specific_config"),
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 static List<String> getExpansion(
EventHandler eventHandler,
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
List<String> commandsToParse,
String configToExpand,
Consumer<String> rcFileNotesConsumer)
throws OptionsParsingException {
LinkedHashSet<String> configAncestorSet = new LinkedHashSet<>();
configAncestorSet.add(configToExpand);
List<String> longestChain = new ArrayList<>();
List<String> finalExpansion =
getExpansion(
commandToRcArgs,
commandsToParse,
configAncestorSet,
configToExpand,
longestChain,
rcFileNotesConsumer);
// 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 static List<String> getExpansion(
ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
List<String> commandsToParse,
LinkedHashSet<String> configAncestorSet,
String configToExpand,
List<String> longestChain,
Consumer<String> rcFileNotesConsumer)
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 : commandsToParse) {
String configDef = commandToParse + ":" + configToExpand;
for (RcChunkOfArgs rcArgs : commandToRcArgs.get(configDef)) {
foundDefinition = true;
rcFileNotesConsumer.accept(
String.format(
"Found applicable config definition %s in file %s: %s",
configDef, rcArgs.getRcFile(), String.join(" ", rcArgs.getArgs())));
// 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.getArgs()) {
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.getRcFile(), 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(
commandToRcArgs,
commandsToParse,
extendedConfigAncestorSet,
newConfigValue,
longestChain,
rcFileNotesConsumer));
}
}
}
}
if (!foundDefinition) {
throw new OptionsParsingException(
"Config value " + configToExpand + " is not defined in any .rc file");
}
return expansion;
}
}