| // 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 --config=%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. 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; | |
| } | |
| } |