| // 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.analysis.config; |
| |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.FileProvider; |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; |
| import com.google.devtools.build.lib.analysis.LicensesProviderImpl; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.RunfilesProvider; |
| import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper; |
| import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; |
| import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory; |
| import com.google.devtools.build.lib.syntax.Type; |
| import com.google.devtools.build.lib.util.Preconditions; |
| import com.google.devtools.common.options.OptionsBase; |
| import com.google.devtools.common.options.OptionsParser; |
| import com.google.devtools.common.options.OptionsParsingException; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Implementation for the config_setting rule. |
| * |
| * <p>This is a "pseudo-rule" in that its purpose isn't to generate output artifacts |
| * from input artifacts. Rather, it provides configuration context to rules that |
| * depend on it. |
| */ |
| public class ConfigSetting implements RuleConfiguredTargetFactory { |
| |
| @Override |
| public ConfiguredTarget create(RuleContext ruleContext) |
| throws InterruptedException, RuleErrorException { |
| // Get the required flag=value settings for this rule. |
| Map<String, String> settings = NonconfigurableAttributeMapper.of(ruleContext.getRule()) |
| .get(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE, Type.STRING_DICT); |
| if (settings.isEmpty()) { |
| ruleContext.attributeError(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE, |
| "no settings specified"); |
| return null; |
| } |
| |
| ConfigMatchingProvider configMatcher; |
| try { |
| configMatcher = new ConfigMatchingProvider(ruleContext.getLabel(), settings, |
| matchesConfig(settings, ruleContext.getConfiguration())); |
| } catch (OptionsParsingException e) { |
| ruleContext.attributeError(ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE, |
| "error while parsing configuration settings: " + e.getMessage()); |
| return null; |
| } |
| |
| return new RuleConfiguredTargetBuilder(ruleContext) |
| .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY) |
| .addProvider(FileProvider.class, FileProvider.EMPTY) |
| .addProvider(FilesToRunProvider.class, FilesToRunProvider.EMPTY) |
| .addProvider(LicensesProviderImpl.EMPTY) |
| .addProvider(ConfigMatchingProvider.class, configMatcher) |
| .build(); |
| } |
| |
| /** |
| * Given a list of [flagName, flagValue] pairs, returns true if flagName == flagValue for |
| * every item in the list under this configuration, false otherwise. |
| */ |
| private boolean matchesConfig(Map<String, String> expectedSettings, BuildConfiguration config) |
| throws OptionsParsingException { |
| // Rather than returning fast when we find a mismatch, continue looking at the other flags |
| // to check that they're indeed valid flag specifications. |
| boolean foundMismatch = false; |
| |
| // Since OptionsParser instantiation involves reflection, let's try to minimize that happening. |
| Map<Class<? extends OptionsBase>, OptionsParser> parserCache = new HashMap<>(); |
| |
| for (Map.Entry<String, String> setting : expectedSettings.entrySet()) { |
| String optionName = setting.getKey(); |
| String expectedRawValue = setting.getValue(); |
| |
| Class<? extends OptionsBase> optionClass = config.getOptionClass(optionName); |
| if (optionClass == null) { |
| throw new OptionsParsingException("unknown option: '" + optionName + "'"); |
| } |
| |
| OptionsParser parser = parserCache.get(optionClass); |
| if (parser == null) { |
| parser = OptionsParser.newOptionsParser(optionClass); |
| parserCache.put(optionClass, parser); |
| } |
| parser.parse("--" + optionName + "=" + expectedRawValue); |
| Object expectedParsedValue = parser.getOptions(optionClass).asMap().get(optionName); |
| |
| if (!optionMatches(config, optionName, expectedParsedValue)) { |
| foundMismatch = true; |
| } |
| } |
| return !foundMismatch; |
| } |
| |
| /** |
| * For single-value options, returns true iff the option's value matches the expected value. |
| * |
| * <p>For multi-value List options, returns true iff any of the option's values matches |
| * the expected value. This means, e.g. "--tool_tag=foo --tool_tag=bar" would match the |
| * expected condition { 'tool_tag': 'bar' }. |
| * |
| * <p>For multi-value Map options, returns true iff the last instance with the same key as the |
| * expected key has the same value. This means, e.g. "--define foo=1 --define bar=2" would |
| * match { 'define': 'foo=1' }, but "--define foo=1 --define bar=2 --define foo=3" would not |
| * match. Note that the definition of --define states that the last instance takes precedence. |
| */ |
| private static boolean optionMatches(BuildConfiguration config, String optionName, |
| Object expectedValue) { |
| Object actualValue = config.getOptionValue(optionName); |
| if (actualValue == null) { |
| return expectedValue == null; |
| |
| // Single-value case: |
| } else if (!config.allowsMultipleValues(optionName)) { |
| return actualValue.equals(expectedValue); |
| } |
| |
| // Multi-value case: |
| Preconditions.checkState(actualValue instanceof List); |
| Preconditions.checkState(expectedValue instanceof List); |
| List<?> actualList = (List<?>) actualValue; |
| List<?> expectedList = (List<?>) expectedValue; |
| |
| if (actualList.isEmpty() || expectedList.isEmpty()) { |
| return actualList.isEmpty() && expectedList.isEmpty(); |
| } |
| |
| // We're expecting a single value of a multi-value type: the options parser still embeds |
| // that single value within a List container. Retrieve it here. |
| Object expectedSingleValue = Iterables.getOnlyElement(expectedList); |
| |
| // Multi-value map: |
| if (actualList.get(0) instanceof Map.Entry) { |
| Map.Entry<?, ?> expectedEntry = (Map.Entry<?, ?>) expectedSingleValue; |
| for (Map.Entry<?, ?> actualEntry : Lists.reverse((List<Map.Entry<?, ?>>) actualList)) { |
| if (actualEntry.getKey().equals(expectedEntry.getKey())) { |
| // Found a key match! |
| return actualEntry.getValue().equals(expectedEntry.getValue()); |
| } |
| } |
| return false; // Never found any matching key. |
| } |
| |
| // Multi-value list: |
| return actualList.contains(expectedSingleValue); |
| } |
| } |