|  | // 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.base.Joiner; | 
|  | 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.OpaqueOptionsData; | 
|  | 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; | 
|  | import javax.annotation.Nullable; | 
|  |  | 
|  | /** 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, | 
|  | String currentCommand, | 
|  | List<String> commandsToParse, | 
|  | Consumer<String> rcFileNotesConsumer, | 
|  | OptionsParser optionsParser, | 
|  | @Nullable OpaqueOptionsData fallbackData) | 
|  | 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); | 
|  | var ignoredArgs = | 
|  | optionsParser.parseArgsAsExpansionOfOption( | 
|  | configInstance, | 
|  | String.format("expanded from --config=%s", configValueToExpand), | 
|  | expansion, | 
|  | fallbackData); | 
|  | if (!ignoredArgs.isEmpty()) { | 
|  | rcFileNotesConsumer.accept( | 
|  | String.format( | 
|  | "Ignored as unsupported by '%s': %s", | 
|  | currentCommand, Joiner.on(' ').join(ignoredArgs))); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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, | 
|  | fallbackData); | 
|  | } | 
|  |  | 
|  | // 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; | 
|  | } | 
|  | } |