Allow config_settings to match on constraint_values to a target_platform. This is on the way to making select() work with constraint_values re: #305.
PiperOrigin-RevId: 169454982
diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/BUILD b/src/main/java/com/google/devtools/build/lib/rules/config/BUILD
index 4145009..4f04785 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/config/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/rules/config/BUILD
@@ -21,6 +21,8 @@
"//src/main/java/com/google/devtools/build/lib:preconditions",
"//src/main/java/com/google/devtools/build/lib:skylarkinterface",
"//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/analysis/platform",
+ "//src/main/java/com/google/devtools/build/lib/analysis/platform:utils",
"//src/main/java/com/google/devtools/build/lib/analysis/whitelisting",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/collect",
diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigSetting.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigSetting.java
index 88d1283..f4cc35e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigSetting.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigSetting.java
@@ -39,6 +39,10 @@
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.config.TransitiveOptionDetails;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.platform.ConstraintSettingInfo;
+import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
+import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
+import com.google.devtools.build.lib.analysis.platform.PlatformProviderUtils;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
@@ -51,6 +55,7 @@
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.util.Collection;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -91,25 +96,37 @@
ruleContext.getPrerequisites(
ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE, Mode.TARGET);
- if (nativeFlagSettings.size() == 0 && userDefinedFlagSettings.isEmpty()) {
- ruleContext.ruleError(
- String.format(
- "Either %s or %s must be specified and non-empty",
- ConfigSettingRule.SETTINGS_ATTRIBUTE,
- ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE));
+ // Get the constraint values that match this rule
+ Iterable<ConstraintValueInfo> constraintValues =
+ PlatformProviderUtils.constraintValues(
+ ruleContext.getPrerequisites(
+ ConfigSettingRule.CONSTRAINT_VALUES_ATTRIBUTE, Mode.DONT_CHECK));
+
+ // Get the target platform
+ Iterable<PlatformInfo> targetPlatforms =
+ PlatformProviderUtils.platforms(
+ ruleContext.getPrerequisites(
+ ConfigSettingRule.TARGET_PLATFORMS_ATTRIBUTE, Mode.DONT_CHECK));
+
+ // Check that this config_setting contains at least one of {values, define_values,
+ // constraint_values}
+ if (!checkValidConditions(
+ nativeFlagSettings, userDefinedFlagSettings, constraintValues, ruleContext)) {
return null;
}
- ConfigFeatureFlagMatch featureFlags =
- ConfigFeatureFlagMatch.fromAttributeValueAndPrerequisites(
- userDefinedFlagSettings, flagValues, ruleContext);
-
boolean nativeFlagsMatch =
matchesConfig(
nativeFlagSettings.entries(),
BuildConfigurationOptionDetails.get(ruleContext.getConfiguration()),
ruleContext);
+ ConfigFeatureFlagMatch featureFlags =
+ ConfigFeatureFlagMatch.fromAttributeValueAndPrerequisites(
+ userDefinedFlagSettings, flagValues, ruleContext);
+
+ boolean constraintValuesMatch = matchesConstraints(constraintValues, targetPlatforms);
+
if (ruleContext.hasErrors()) {
return null;
}
@@ -119,7 +136,7 @@
ruleContext.getLabel(),
nativeFlagSettings,
featureFlags.getSpecifiedFlagValues(),
- nativeFlagsMatch && featureFlags.matches());
+ nativeFlagsMatch && featureFlags.matches() && constraintValuesMatch);
return new RuleConfiguredTargetBuilder(ruleContext)
.addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
@@ -130,11 +147,79 @@
.build();
}
+ private boolean checkValidConditions(
+ ImmutableMultimap<String, String> nativeFlagSettings,
+ Map<Label, String> userDefinedFlagSettings,
+ Iterable<ConstraintValueInfo> constraintValues,
+ RuleErrorConsumer errors) {
+ // Check to make sure this config_setting contains and sets least one of {values, define_values,
+ // flag_value or constraint_values}.
+ if (!valuesAreSet(nativeFlagSettings, userDefinedFlagSettings, constraintValues, errors)) {
+ return false;
+ }
+
+ // The set of constraint_values in a config_setting should never contain multiple
+ // constraint_values that map to the same constraint_setting. This checks if there are
+ // duplicates and records an error if so.
+ if (containsDuplicateSettings(constraintValues, errors)) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* User error when value settings can't be properly parsed.
*/
private static final String PARSE_ERROR_MESSAGE = "error while parsing configuration settings: ";
+ private boolean valuesAreSet(
+ ImmutableMultimap<String, String> nativeFlagSettings,
+ Map<Label, String> userDefinedFlagSettings,
+ Iterable<ConstraintValueInfo> constraintValues,
+ RuleErrorConsumer errors) {
+ if (nativeFlagSettings.isEmpty()
+ && userDefinedFlagSettings.isEmpty()
+ && Iterables.isEmpty(constraintValues)) {
+ errors.ruleError(
+ String.format(
+ "Either %s, %s or %s must be specified and non-empty",
+ ConfigSettingRule.SETTINGS_ATTRIBUTE,
+ ConfigSettingRule.FLAG_SETTINGS_ATTRIBUTE,
+ ConfigSettingRule.CONSTRAINT_VALUES_ATTRIBUTE));
+ return false;
+ }
+ return true;
+ }
+
+
+ /**
+ * The set of constraint_values in a config_setting should never contain multiple
+ * constraint_values that map to the same constraint_setting. This method checks if there are
+ * duplicates and records an error if so.
+ */
+ private boolean containsDuplicateSettings(
+ Iterable<ConstraintValueInfo> constraintValues, RuleErrorConsumer errors) {
+ HashMap<ConstraintSettingInfo, ConstraintValueInfo> constraints = new HashMap<>();
+ for (ConstraintValueInfo constraint : constraintValues) {
+ ConstraintSettingInfo setting = constraint.constraint();
+ if (constraints.containsKey(setting)) {
+ errors.attributeError(
+ ConfigSettingRule.TARGET_PLATFORMS_ATTRIBUTE,
+ String.format(
+ PARSE_ERROR_MESSAGE
+ + "the target platform contains multiple values '%s' "
+ + "and '%s' that map to the same setting '%s'",
+ constraint.label(),
+ constraints.get(setting).label(),
+ setting.label()));
+ return true;
+ }
+ constraints.put(setting, constraint);
+ }
+ return false;
+ }
+
/**
* Given a list of [flagName, flagValue] pairs for native Blaze flags, returns true if
* flagName == flagValue for every item in the list under this configuration, false otherwise.
@@ -244,6 +329,33 @@
return actualList.contains(expectedSingleValue);
}
+ private boolean matchesConstraints(
+ Iterable<ConstraintValueInfo> expected, Iterable<PlatformInfo> targetPlatforms) {
+ // config_setting didn't specify any constraint values
+ if (Iterables.isEmpty(expected)) {
+ return true;
+ }
+
+ // TODO(jcater): re-evaluate this for multiple target platforms.
+ PlatformInfo targetPlatform = Iterables.getOnlyElement(targetPlatforms);
+ // config_setting DID specify constraint_value(s) but no target platforms are set
+ // in the configuration.
+ if (Iterables.isEmpty(targetPlatform.constraints())) {
+ return false;
+ }
+
+ // For every constraint in the attr check if it is (1)set aka non-null and
+ // (2)set correctly in the platform.
+ for (ConstraintValueInfo constraint : expected) {
+ ConstraintSettingInfo setting = constraint.constraint();
+ ConstraintValueInfo targetValue = targetPlatform.getConstraint(setting);
+ if (targetValue == null || !constraint.equals(targetValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static final class ConfigFeatureFlagMatch {
private final boolean matches;
private final ImmutableMap<Label, String> specifiedFlagValues;
@@ -338,3 +450,4 @@
}
}
}
+
diff --git a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigSettingTest.java b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigSettingTest.java
index 9def906..f45a85e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigSettingTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigSettingTest.java
@@ -239,9 +239,11 @@
*/
@Test
public void emptySettings() throws Exception {
- checkError("foo", "empty",
+ checkError(
+ "foo",
+ "empty",
"in config_setting rule //foo:empty: "
- + "Either values or flag_values must be specified and non-empty",
+ + "Either values, flag_values or constraint_values must be specified and non-empty",
"config_setting(",
" name = 'empty',",
" values = {})");
@@ -1122,4 +1124,130 @@
" default_value = 'valid',",
")");
}
+
+ @Test
+ public void constraintValue() throws Exception {
+ scratch.file(
+ "test/BUILD",
+ "constraint_setting(name = 'notable_building')",
+ "constraint_value(name = 'empire_state', constraint_setting = 'notable_building')",
+ "constraint_value(name = 'space_needle', constraint_setting = 'notable_building')",
+ "platform(",
+ " name = 'new_york_platform',",
+ " constraint_values = [':empire_state'],",
+ ")",
+ "platform(",
+ " name = 'seattle_platform',",
+ " constraint_values = [':space_needle'],",
+ ")",
+ "config_setting(",
+ " name = 'match',",
+ " constraint_values = [':empire_state'],",
+ ");");
+
+ useConfiguration("--experimental_platforms=//test:new_york_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isTrue();
+ useConfiguration("--experimental_platforms=//test:seattle_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ useConfiguration("");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ }
+
+ @Test
+ public void multipleConstraintValues() throws Exception {
+ scratch.file(
+ "test/BUILD",
+ "constraint_setting(name = 'notable_building')",
+ "constraint_value(name = 'empire_state', constraint_setting = 'notable_building')",
+ "constraint_setting(name = 'museum')",
+ "constraint_value(name = 'cloisters', constraint_setting = 'museum')",
+ "constraint_setting(name = 'theme_park')",
+ "constraint_value(name = 'coney_island', constraint_setting = 'theme_park')",
+ "platform(",
+ " name = 'manhattan_platform',",
+ " constraint_values = [",
+ " ':empire_state',",
+ " ':cloisters',",
+ " ],",
+ ")",
+ "platform(",
+ " name = 'museum_platform',",
+ " constraint_values = [':cloisters'],",
+ ")",
+ "platform(",
+ " name = 'new_york_platform',",
+ " constraint_values = [",
+ " ':empire_state',",
+ " ':cloisters',",
+ " ':coney_island',",
+ " ],",
+ ")",
+ "config_setting(",
+ " name = 'match',",
+ " constraint_values = [':empire_state', ':cloisters'],",
+ ");");
+ useConfiguration("--experimental_platforms=//test:manhattan_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isTrue();
+ useConfiguration("--experimental_platforms=//test:museum_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ useConfiguration("--experimental_platforms=//test:new_york_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isTrue();
+ }
+
+ @Test
+ public void definesAndConstraints() throws Exception {
+ scratch.file(
+ "test/BUILD",
+ "constraint_setting(name = 'notable_building')",
+ "constraint_value(name = 'empire_state', constraint_setting = 'notable_building')",
+ "constraint_value(name = 'space_needle', constraint_setting = 'notable_building')",
+ "platform(",
+ " name = 'new_york_platform',",
+ " constraint_values = [':empire_state'],",
+ ")",
+ "platform(",
+ " name = 'seattle_platform',",
+ " constraint_values = [':space_needle'],",
+ ")",
+ "config_setting(",
+ " name = 'match',",
+ " constraint_values = [':empire_state'],",
+ " values = {",
+ " 'define': 'a=c',",
+ " },",
+ " define_values = {",
+ " 'b': 'd',",
+ " },",
+ ");");
+
+ useConfiguration(
+ "--experimental_platforms=//test:new_york_platform", "--define", "a=c", "--define", "b=d");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isTrue();
+ useConfiguration("--experimental_platforms=//test:new_york_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ useConfiguration("--define", "a=c");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ useConfiguration("--define", "a=c", "--experimental_platforms=//test:new_york_platform");
+ assertThat(getConfigMatchingProvider("//test:match").matches()).isFalse();
+ }
+
+ /**
+ * Tests that a config_setting doesn't allow a constraint_values list with more than one
+ * constraint value per constraint setting.
+ */
+ @Test
+ public void multipleValuesPerSetting() throws Exception {
+ checkError("foo", "bad",
+ "in :target_platforms attribute of config_setting rule //foo:bad: "
+ + "error while parsing configuration settings: "
+ + "the target platform contains multiple values '//foo:space_needle' "
+ + "and '//foo:empire_state' that map to the same setting '//foo:notable_building'",
+ "constraint_setting(name = 'notable_building')",
+ "constraint_value(name = 'empire_state', constraint_setting = 'notable_building')",
+ "constraint_value(name = 'space_needle', constraint_setting = 'notable_building')",
+ "config_setting(",
+ " name = 'bad',",
+ " constraint_values = [':empire_state', ':space_needle'],",
+ ");");
+ }
}