// 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.rules.config;
import com.google.common.collect.ImmutableMap;
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.RuleConfiguredTarget.Mode;
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.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationOptionDetails;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.config.TransitiveOptionDetails;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.RuleErrorConsumer;
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);
Map<Label, String> flagSettings =
NonconfigurableAttributeMapper.of(ruleContext.getRule())
.get(
ConfigRuleClasses.ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE,
BuildType.LABEL_KEYED_STRING_DICT);
List<? extends TransitiveInfoCollection> flagValues =
ruleContext.getPrerequisites(
ConfigRuleClasses.ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE, Mode.TARGET);
ImmutableMap<Label, ConfigFeatureFlagProvider> configProviders =
buildConfigFeatureFlagMap(flagValues);
if (settings.isEmpty() && flagSettings.isEmpty()) {
ruleContext.ruleError(
String.format(
"Either %s or %s must be specified and non-empty",
ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE,
ConfigRuleClasses.ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE));
return null;
}
boolean flagSettingsMatch = matchesUserConfig(flagSettings, configProviders, ruleContext);
boolean settingsMatch =
matchesConfig(
settings,
BuildConfigurationOptionDetails.get(ruleContext.getConfiguration()),
ruleContext);
if (ruleContext.hasErrors()) {
return null;
}
ConfigMatchingProvider configMatcher =
new ConfigMatchingProvider(
ruleContext.getLabel(), settings, flagSettings, settingsMatch && flagSettingsMatch);
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();
}
/** Maps the labels of the given prerequisites to their {@link ConfigFeatureFlagProvider}s. */
private static ImmutableMap<Label, ConfigFeatureFlagProvider> buildConfigFeatureFlagMap(
Iterable<? extends TransitiveInfoCollection> prerequisites) {
ImmutableMap.Builder<Label, ConfigFeatureFlagProvider> output = new ImmutableMap.Builder<>();
for (TransitiveInfoCollection target : prerequisites) {
ConfigFeatureFlagProvider provider = ConfigFeatureFlagProvider.fromTarget(target);
// We know the provider exists because only labels with ConfigFeatureFlagProvider can be added
// to this attribute.
assert provider != null;
output.put(target.getLabel(), provider);
}
return output.build();
}
/** Returns whether the actual user-defined flags are set to the specified values. */
private static boolean matchesUserConfig(
Map<Label, String> specifiedFlags,
Map<Label, ConfigFeatureFlagProvider> actualFlags,
RuleErrorConsumer errors) {
boolean foundMismatch = false;
for (Map.Entry<Label, String> specifiedFlag : specifiedFlags.entrySet()) {
Label label = specifiedFlag.getKey();
String specifiedValue = specifiedFlag.getValue();
ConfigFeatureFlagProvider provider = actualFlags.get(label);
// Both specifiedFlags and actualFlags are built from the same set of keys; therefore, the
// provider will always be present.
assert provider != null;
if (!provider.isValidValue(specifiedValue)) {
errors.attributeError(
ConfigRuleClasses.ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE,
String.format(
"error while parsing user-defined configuration values: "
+ "'%s' is not a valid value for '%s'",
specifiedValue, label));
foundMismatch = true;
continue;
}
if (!provider.getValue().equals(specifiedValue)) {
foundMismatch = true;
}
}
return !foundMismatch;
}
/**
* 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,
TransitiveOptionDetails options,
RuleErrorConsumer errors) {
// 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 = options.getOptionClass(optionName);
if (optionClass == null) {
errors.attributeError(
ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE,
String.format(
"error while parsing configuration settings: unknown option: '%s'", optionName));
foundMismatch = true;
continue;
}
OptionsParser parser = parserCache.get(optionClass);
if (parser == null) {
parser = OptionsParser.newOptionsParser(optionClass);
parserCache.put(optionClass, parser);
}
try {
parser.parse("--" + optionName + "=" + expectedRawValue);
} catch (OptionsParsingException ex) {
errors.attributeError(
ConfigRuleClasses.ConfigSettingRule.SETTINGS_ATTRIBUTE,
"error while parsing configuration settings: " + ex.getMessage());
foundMismatch = true;
continue;
}
Object expectedParsedValue = parser.getOptions(optionClass).asMap().get(optionName);
if (!optionMatches(options, 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(
TransitiveOptionDetails options, String optionName, Object expectedValue) {
Object actualValue = options.getOptionValue(optionName);
if (actualValue == null) {
return expectedValue == null;
// Single-value case:
} else if (!options.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);
}
}