diff options
author | mstaib <mstaib@google.com> | 2018-05-16 10:13:51 -0700 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2018-05-16 10:15:49 -0700 |
commit | 188b578c0e9995c864d284f22de4e48c220b0596 (patch) | |
tree | 104640b011b04e2d787098eacdd52df5bf22981d | |
parent | 238079669ec280a972adf68ee987c5d407e15b26 (diff) |
Enable manual trimming of config_feature_flags.
This enables users of config_feature_flags to specify the flags used by the transitive closure of a particular target in the transitive_configs attribute of all targets. It also adds a flag - --enforce_transitive_configs_for_config_feature_flag - which enforces this specification and uses it to trim the set of flags available to that target.
RELNOTES: None.
PiperOrigin-RevId: 196846092
17 files changed, 1283 insertions, 69 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java index 712be737a2..a1454b23d2 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/BaseRuleClasses.java @@ -181,6 +181,12 @@ public class BaseRuleClasses { } /** + * The attribute used to list the configuration properties used by a target and its transitive + * dependencies. Currently only supports config_feature_flag. + */ + public static final String TAGGED_TRIMMING_ATTR = "transitive_configs"; + + /** * Share common attributes across both base and Skylark base rules. */ public static RuleClass.Builder commonCoreAndSkylarkAttributes(RuleClass.Builder builder) { @@ -196,6 +202,10 @@ public class BaseRuleClasses { .nonconfigurable( "special attribute integrated more deeply into Bazel's core logic")) .add( + attr(TAGGED_TRIMMING_ATTR, NODEP_LABEL_LIST) + .orderIndependent() + .nonconfigurable("Used in determining configuration")) + .add( attr("deprecation", STRING) .value(deprecationDefault) .nonconfigurable("Used in core loading phase logic with no access to configs")) diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfiguration.java index 6a1e7c770e..708a8258d3 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfiguration.java @@ -14,8 +14,11 @@ package com.google.devtools.build.lib.rules.config; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.ImmutableSortedSet; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.devtools.build.lib.actions.ArtifactOwner; @@ -24,6 +27,7 @@ import com.google.devtools.build.lib.analysis.config.BuildOptions; import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment; import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory; import com.google.devtools.build.lib.analysis.config.FragmentOptions; +import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import java.util.Map; @@ -44,8 +48,25 @@ public final class ConfigFeatureFlagConfiguration extends BuildConfiguration.Fra public static final class Loader implements ConfigurationFragmentFactory { @Override public BuildConfiguration.Fragment create( - ConfigurationEnvironment env, BuildOptions buildOptions) { - return new ConfigFeatureFlagConfiguration(buildOptions.get(ConfigFeatureFlagOptions.class)); + ConfigurationEnvironment env, BuildOptions buildOptions) + throws InvalidConfigurationException { + ConfigFeatureFlagOptions options = buildOptions.get(ConfigFeatureFlagOptions.class); + if (!options.unknownFlags.isEmpty()) { + ImmutableList.Builder<String> errorMessage = new ImmutableList.Builder<>(); + for (Label missingLabel : options.unknownFlags) { + errorMessage.add( + String.format( + "Feature flag %1$s was accessed in a configuration it is not present in. All " + + "targets which depend on %1$s directly or indirectly must name it in their " + + "transitive_configs attribute.", + missingLabel)); + } + throw new InvalidConfigurationException( + "Some feature flags were incorrectly specified:\n" + + Joiner.on("\n").join(errorMessage.build())); + } + + return new ConfigFeatureFlagConfiguration(options); } @Override @@ -59,18 +80,46 @@ public final class ConfigFeatureFlagConfiguration extends BuildConfiguration.Fra } } + /** Exception thrown when a flag is accessed in a configuration it is not present in. */ + public static final class MissingFlagException extends RuntimeException { + public MissingFlagException(Label label) { + super( + String.format( + "Feature flag %1$s was accessed in a configuration it was trimmed from.", label)); + } + } + private final ImmutableSortedMap<Label, String> flagValues; + private final ImmutableSortedSet<Label> knownDefaultFlags; + private final boolean isTrimmed; @Nullable private final String flagHash; /** Creates a new configuration fragment from the given {@link ConfigFeatureFlagOptions}. */ public ConfigFeatureFlagConfiguration(ConfigFeatureFlagOptions options) { - this(options.getFlagValues()); + // TODO(mstaib): we'd love to only construct these when we're trimmed, but this is still what + // the top level looks like - make constructing an untrimmed configuration an error when no + // configurations are constructed untrimmed. + this( + options.getFlagValues(), + options.getKnownDefaultFlags().orElse(ImmutableSortedSet.of()), + options.enforceTransitiveConfigsForConfigFeatureFlag && options.isTrimmed()); } @AutoCodec.Instantiator - ConfigFeatureFlagConfiguration(ImmutableSortedMap<Label, String> flagValues) { + ConfigFeatureFlagConfiguration( + ImmutableSortedMap<Label, String> flagValues, + ImmutableSortedSet<Label> knownDefaultFlags, + boolean isTrimmed) { this.flagValues = flagValues; - this.flagHash = this.flagValues.isEmpty() ? null : hashFlags(this.flagValues); + this.knownDefaultFlags = knownDefaultFlags; + this.isTrimmed = isTrimmed; + // We don't hash flags set to their default values; all valid configurations of a target have + // the same set of known flags, so the set of flags set to something other than their default + // values is enough to disambiguate configurations. Similarly, isTrimmed need not be hashed; + // enforceTransitiveConfigsForConfigFeatureFlag should not change within a build, and when it's + // enabled, the only configuration which is untrimmed (the top-level configuration) shouldn't + // be used for any actual targets. + this.flagHash = flagValues.isEmpty() ? null : hashFlags(flagValues); } /** Converts the given flag values into a string hash for use as an output directory fragment. */ @@ -92,13 +141,24 @@ public final class ConfigFeatureFlagConfiguration extends BuildConfiguration.Fra * * <p>If the flag is not set in the current configuration, then the returned value will be absent. * + * <p>If the flag has been trimmed from the current configuration, a RuntimeException + * (MissingFlagException) will be thrown. Because the configuration should fail to construct if a + * required flag is missing, and because config_feature_flag (the only intended user of this + * method) automatically requires itself, this should not come to pass. + * * <p>This method should only be used by the rule whose label is passed here. Other rules should * depend on that rule and read a provider exported by it. To encourage callers of this method to * do the right thing, this class takes {@link ArtifactOwner} instead of {@link Label}; to get the * ArtifactOwner for a rule, call {@code ruleContext.getOwner()}. */ public Optional<String> getFeatureFlagValue(ArtifactOwner owner) { - return Optional.ofNullable(flagValues.get(owner.getLabel())); + if (flagValues.containsKey(owner.getLabel())) { + return Optional.of(flagValues.get(owner.getLabel())); + } else if (!isTrimmed || knownDefaultFlags.contains(owner.getLabel())) { + return Optional.empty(); + } else { + throw new MissingFlagException(owner.getLabel()); + } } /** diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagOptions.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagOptions.java index dda852b30d..dcbeb01c15 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagOptions.java +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagOptions.java @@ -62,8 +62,6 @@ public final class ConfigFeatureFlagOptions extends FragmentOptions { /** * Whether to perform user-guided trimming of feature flags based on the tagging in the * transitive_configs attribute. - * - * <p>Currently a no-op. */ @Option( name = "enforce_transitive_configs_for_config_feature_flag", @@ -277,4 +275,36 @@ public final class ConfigFeatureFlagOptions extends FragmentOptions { this.knownDefaultFlags = knownDefaultFlagsBuilder.build(); this.unknownFlags = unknownFlagsBuilder.build(); } + + /** Returns whether work needs to be done to trim this configuration to the given set of flags. */ + public boolean requiresTrimming(Set<Label> requiredFlags) { + if (!this.isTrimmed()) { + // this configuration is untrimmed, so there is definitely work to do + return true; + } + if (requiredFlags.size() + != this.flagValues.size() + this.knownDefaultFlags.size() + this.unknownFlags.size()) { + // the set of flags we know about isn't the same size as the set of flags we need to know + // about, so there must be work to do + return true; + } + for (Label flag : requiredFlags) { + if (!(flagValues.containsKey(flag) + || knownDefaultFlags.contains(flag) + || unknownFlags.contains(flag))) { + // we need a flag we don't know anything about, so there must be work to do + return true; + } + } + // the sets are the same size and contain the same elements, so trimming would just get us back + // to the state we're in right now + return false; + } + + @Override + public ConfigFeatureFlagOptions getHost() { + ConfigFeatureFlagOptions options = (ConfigFeatureFlagOptions) super.getHost(); + options.enforceTransitiveConfigsForConfigFeatureFlag = false; + return options; + } } diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTaggedTrimmingTransitionFactory.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTaggedTrimmingTransitionFactory.java new file mode 100644 index 0000000000..04806e5fcc --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTaggedTrimmingTransitionFactory.java @@ -0,0 +1,114 @@ +// Copyright 2018 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 static com.google.devtools.build.lib.packages.BuildType.LABEL_KEYED_STRING_DICT; +import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL_LIST; + +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Ordering; +import com.google.devtools.build.lib.analysis.config.BuildOptions; +import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper; +import com.google.devtools.build.lib.packages.Rule; +import com.google.devtools.build.lib.packages.RuleClass; +import com.google.devtools.build.lib.packages.RuleTransitionFactory; + +/** + * A transition factory for trimming feature flags manually via an attribute which specifies the + * feature flags used by transitive dependencies. + */ +public class ConfigFeatureFlagTaggedTrimmingTransitionFactory implements RuleTransitionFactory { + + private static final class ConfigFeatureFlagTaggedTrimmingTransition implements PatchTransition { + public static final ConfigFeatureFlagTaggedTrimmingTransition EMPTY = + new ConfigFeatureFlagTaggedTrimmingTransition(ImmutableSortedSet.of()); + + private final ImmutableSortedSet<Label> flags; + private final int cachedHashCode; + + ConfigFeatureFlagTaggedTrimmingTransition(ImmutableSortedSet<Label> flags) { + this.flags = flags; + this.cachedHashCode = this.flags.hashCode(); + } + + @Override + public BuildOptions apply(BuildOptions options) { + if (!(options.contains(ConfigFeatureFlagOptions.class) + && options.get(ConfigFeatureFlagOptions.class) + .enforceTransitiveConfigsForConfigFeatureFlag + && options.get(ConfigFeatureFlagOptions.class).requiresTrimming(flags))) { + return options; + } + BuildOptions result = options.clone(); + result.get(ConfigFeatureFlagOptions.class).trimFlagValues(flags); + return result; + } + + @Override + public boolean equals(Object other) { + return other instanceof ConfigFeatureFlagTaggedTrimmingTransition + && this.flags.equals(((ConfigFeatureFlagTaggedTrimmingTransition) other).flags); + } + + @Override + public int hashCode() { + return cachedHashCode; + } + + @Override + public String toString() { + return String.format("ConfigFeatureFlagTaggedTrimmingTransition{flags=%s}", flags); + } + } + + private final String attributeName; + + public ConfigFeatureFlagTaggedTrimmingTransitionFactory(String attributeName) { + this.attributeName = attributeName; + } + + @Override + public PatchTransition buildTransitionFor(Rule rule) { + NonconfigurableAttributeMapper attrs = NonconfigurableAttributeMapper.of(rule); + RuleClass ruleClass = rule.getRuleClassObject(); + if (ruleClass.getName().equals(ConfigRuleClasses.ConfigFeatureFlagRule.RULE_NAME)) { + return new ConfigFeatureFlagTaggedTrimmingTransition(ImmutableSortedSet.of(rule.getLabel())); + } + + ImmutableSortedSet.Builder<Label> requiredLabelsBuilder = + new ImmutableSortedSet.Builder<>(Ordering.natural()); + if (attrs.isAttributeValueExplicitlySpecified(attributeName) + && !attrs.get(attributeName, NODEP_LABEL_LIST).isEmpty()) { + requiredLabelsBuilder.addAll(attrs.get(attributeName, NODEP_LABEL_LIST)); + } + if (ruleClass.getTransitionFactory() instanceof ConfigFeatureFlagTransitionFactory) { + String settingAttribute = + ((ConfigFeatureFlagTransitionFactory) ruleClass.getTransitionFactory()) + .getAttributeName(); + // Because the process of setting a flag also creates a dependency on that flag, we need to + // include all the set flags, even if they aren't actually declared as used by this rule. + requiredLabelsBuilder.addAll(attrs.get(settingAttribute, LABEL_KEYED_STRING_DICT).keySet()); + } + + ImmutableSortedSet<Label> requiredLabels = requiredLabelsBuilder.build(); + if (requiredLabels.isEmpty()) { + return ConfigFeatureFlagTaggedTrimmingTransition.EMPTY; + } + + return new ConfigFeatureFlagTaggedTrimmingTransition(requiredLabels); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTransitionFactory.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTransitionFactory.java index 72b91b58a9..28fbf5aecb 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTransitionFactory.java +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTransitionFactory.java @@ -105,6 +105,13 @@ public class ConfigFeatureFlagTransitionFactory implements RuleTransitionFactory } } + /** + * Returns the attribute examined by this transition factory. + */ + public String getAttributeName() { + return this.attributeName; + } + @Override public boolean equals(Object other) { return other instanceof ConfigFeatureFlagTransitionFactory diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRuleClasses.java index 45a7163f78..bf0256ab92 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRuleClasses.java +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRuleClasses.java @@ -396,6 +396,7 @@ platform( /** Rule definition for Android's config_feature_flag rule. */ public static final class ConfigFeatureFlagRule implements RuleDefinition { + public static final String RULE_NAME = "config_feature_flag"; @Override public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) { @@ -412,13 +413,14 @@ platform( attr("default_value", STRING) .nonconfigurable(NONCONFIGURABLE_ATTRIBUTE_REASON)) .add(ConfigFeatureFlag.getWhitelistAttribute(env)) + .removeAttribute(BaseRuleClasses.TAGGED_TRIMMING_ATTR) .build(); } @Override public RuleDefinition.Metadata getMetadata() { return RuleDefinition.Metadata.builder() - .name("config_feature_flag") + .name(RULE_NAME) .ancestors(ConfigBaseRule.class) .factoryClass(ConfigFeatureFlag.class) .build(); diff --git a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRules.java b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRules.java index de5e22f779..4d49eafa58 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRules.java +++ b/src/main/java/com/google/devtools/build/lib/rules/config/ConfigRules.java @@ -14,6 +14,7 @@ package com.google.devtools.build.lib.rules.config; import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.analysis.BaseRuleClasses; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider.RuleSet; import com.google.devtools.build.lib.rules.core.CoreRules; @@ -30,9 +31,12 @@ public final class ConfigRules implements RuleSet { @Override public void init(ConfiguredRuleClassProvider.Builder builder) { + builder.setTrimmingTransitionFactory( + new ConfigFeatureFlagTaggedTrimmingTransitionFactory(BaseRuleClasses.TAGGED_TRIMMING_ATTR)); builder.addRuleDefinition(new ConfigRuleClasses.ConfigBaseRule()); builder.addRuleDefinition(new ConfigRuleClasses.ConfigSettingRule()); - builder.addConfig(ConfigFeatureFlagOptions.class, new ConfigFeatureFlagConfiguration.Loader()); + builder.addConfig(ConfigFeatureFlagOptions.class, + new ConfigFeatureFlagConfiguration.Loader()); builder.addRuleDefinition(new ConfigRuleClasses.ConfigFeatureFlagRule()); builder.addSkylarkAccessibleTopLevels("config_common", new ConfigSkylarkCommon()); diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AbstractAndroidLocalTestTestBase.java b/src/test/java/com/google/devtools/build/lib/rules/android/AbstractAndroidLocalTestTestBase.java index 301d2acc12..bf62f4f71c 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/android/AbstractAndroidLocalTestTestBase.java +++ b/src/test/java/com/google/devtools/build/lib/rules/android/AbstractAndroidLocalTestTestBase.java @@ -93,7 +93,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagsAttributeSetsSelectInDependency() throws Exception { - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/BUILD", "load('//java/bar:foo.bzl', 'extra_deps')", @@ -105,6 +107,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "config_feature_flag(", " name = 'flag2',", @@ -114,6 +117,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "config_setting(", " name = 'flag2@on',", " flag_values = {':flag2': 'on'},", + " transitive_configs = [':flag2'],", ")", "android_library(", " name = 'lib',", @@ -124,6 +128,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " ':flag2@on': ['Flag2On.java'],", " '//conditions:default': ['Flag2Off.java'],", " }),", + " transitive_configs = [':flag1', ':flag2'],", ")", "android_local_test(", " name = 'foo',", @@ -131,7 +136,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = [':lib'] + extra_deps,", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); ConfiguredTarget binary = getConfiguredTarget("//java/com/foo"); List<String> inputs = @@ -144,7 +150,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagsAttributeSetsSelectInTest() throws Exception { - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/BUILD", "load('//java/bar:foo.bzl', 'extra_deps')", @@ -156,6 +164,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "config_feature_flag(", " name = 'flag2',", @@ -165,6 +174,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "config_setting(", " name = 'flag2@on',", " flag_values = {':flag2': 'on'},", + " transitive_configs = [':flag2'],", ")", "android_local_test(", " name = 'foo',", @@ -178,7 +188,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " }),", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); ConfiguredTarget binary = getConfiguredTarget("//java/com/foo"); List<String> inputs = @@ -192,7 +203,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalid() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/BUILD", "load('//java/bar:foo.bzl', 'extra_deps')", @@ -204,13 +217,15 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "android_library(", " name = 'lib',", " srcs = select({", " ':flag1@on': ['Flag1On.java'],", " '//conditions:default': ['Flag1Off.java'],", - " })", + " }),", + " transitive_configs = [':flag1'],", ")", "android_local_test(", " name = 'foo',", @@ -218,7 +233,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = [':lib',] + extra_deps,", " feature_flags = {", " 'flag1': 'invalid',", - " }", + " },", + " transitive_configs = [':flag1'],", ")"); assertThat(getConfiguredTarget("//java/com/foo")).isNull(); assertContainsEvent( @@ -230,7 +246,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalidEvenIfNotUsed() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/BUILD", "load('//java/bar:foo.bzl', 'extra_deps')", @@ -259,7 +277,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagsAttributeSetsFeatureFlagProviderValues() throws Exception { - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/reader.bzl", "def _impl(ctx):", @@ -291,6 +311,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "flag_reader(", " name = 'FooFlags',", " flags = [':flag1', ':flag2'],", + " transitive_configs = [':flag1', ':flag2'],", ")", "android_local_test(", " name = 'foo',", @@ -298,7 +319,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = extra_deps,", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); Artifact flagList = actionsTestUtil().getFirstArtifactEndingWith( @@ -314,7 +336,9 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase public void testFeatureFlagsAttributeFailsAnalysisIfFlagIsAliased() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); writeFile( "java/com/foo/BUILD", "load('//java/bar:foo.bzl', 'extra_deps')", @@ -326,6 +350,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase "alias(", " name = 'alias',", " actual = 'flag1',", + " transitive_configs = [':flag1'],", ")", "android_local_test(", " name = 'foo',", @@ -333,7 +358,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = extra_deps,", " feature_flags = {", " 'alias': 'on',", - " }", + " },", + " transitive_configs = [':flag1'],", ")"); assertThat(getConfiguredTarget("//java/com/foo")).isNull(); assertContainsEvent(String.format( @@ -345,6 +371,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagPolicyMustBeVisibleToRuleToUseFeatureFlags() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(", @@ -367,7 +394,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = extra_deps,", " feature_flags = {", " '//flag:flag': 'right',", - " }", + " },", + " transitive_configs = ['//flag:flag'],", ")"); assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNull(); assertContainsEvent( @@ -378,6 +406,7 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase @Test public void testFeatureFlagPolicyDoesNotBlockRuleIfInPolicy() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(", @@ -400,7 +429,8 @@ public abstract class AbstractAndroidLocalTestTestBase extends BuildViewTestCase " deps = extra_deps,", " feature_flags = {", " '//flag:flag': 'right',", - " }", + " },", + " transitive_configs = ['//flag:flag'],", ")"); assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNotNull(); assertNoEvents(); diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java index 3a9bb90190..7d41301892 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java @@ -3006,7 +3006,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagsAttributeSetsSelectInDependency() throws Exception { - useConfiguration("--experimental_dynamic_configs=notrim"); + useConfiguration( + "--experimental_dynamic_configs=notrim", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3017,6 +3019,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "config_feature_flag(", " name = 'flag2',", @@ -3026,6 +3029,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag2@on',", " flag_values = {':flag2': 'on'},", + " transitive_configs = [':flag2'],", ")", "android_library(", " name = 'lib',", @@ -3036,6 +3040,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " ':flag2@on': ['Flag2On.java'],", " '//conditions:default': ['Flag2Off.java'],", " }),", + " transitive_configs = [':flag1', ':flag2'],", ")", "android_binary(", " name = 'foo',", @@ -3043,7 +3048,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " deps = [':lib'],", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); ConfiguredTarget binary = getConfiguredTarget("//java/com/foo"); List<String> inputs = @@ -3055,7 +3061,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagsAttributeSetsSelectInBinary() throws Exception { - useConfiguration("--experimental_dynamic_configs=notrim"); + useConfiguration( + "--experimental_dynamic_configs=notrim", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3066,6 +3074,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "config_feature_flag(", " name = 'flag2',", @@ -3075,6 +3084,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag2@on',", " flag_values = {':flag2': 'on'},", + " transitive_configs = [':flag2'],", ")", "android_binary(", " name = 'foo',", @@ -3088,7 +3098,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " }),", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); ConfiguredTarget binary = getConfiguredTarget("//java/com/foo"); List<String> inputs = @@ -3100,7 +3111,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagsAttributeSetsSelectInBinaryAlias() throws Exception { - useConfiguration("--experimental_dynamic_configs=notrim"); + useConfiguration( + "--experimental_dynamic_configs=notrim", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3111,6 +3124,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "android_binary(", " name = 'foo',", @@ -3121,7 +3135,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " }),", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1'],", ")", "alias(", " name = 'alias',", @@ -3138,7 +3153,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalid() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3149,13 +3166,15 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "android_library(", " name = 'lib',", " srcs = select({", " ':flag1@on': ['Flag1On.java'],", " '//conditions:default': ['Flag1Off.java'],", - " })", + " }),", + " transitive_configs = [':flag1'],", ")", "android_binary(", " name = 'foo',", @@ -3163,7 +3182,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " deps = [':lib'],", " feature_flags = {", " 'flag1': 'invalid',", - " }", + " },", + " transitive_configs = [':flag1'],", ")"); assertThat(getConfiguredTarget("//java/com/foo")).isNull(); assertContainsEvent( @@ -3175,7 +3195,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalidEvenIfNotUsed() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3186,13 +3208,15 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "android_binary(", " name = 'foo',", " manifest = 'AndroidManifest.xml',", " feature_flags = {", " 'flag1': 'invalid',", - " }", + " },", + " transitive_configs = [':flag1'],", ")"); assertThat(getConfiguredTarget("//java/com/foo")).isNull(); assertContainsEvent( @@ -3204,7 +3228,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { public void testFeatureFlagsAttributeFailsAnalysisIfFlagIsAliased() throws Exception { reporter.removeHandler(failFastHandler); - useConfiguration("--experimental_dynamic_configs=notrim"); + useConfiguration( + "--experimental_dynamic_configs=notrim", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/BUILD", "config_feature_flag(", @@ -3215,13 +3241,15 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "alias(", " name = 'alias',", " actual = 'flag1',", + " transitive_configs = [':flag1'],", ")", "android_binary(", " name = 'foo',", " manifest = 'AndroidManifest.xml',", " feature_flags = {", " 'alias': 'on',", - " }", + " },", + " transitive_configs = [':flag1'],", ")"); assertThat(getConfiguredTarget("//java/com/foo")).isNull(); assertContainsEvent( @@ -3232,7 +3260,9 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagsAttributeSetsFeatureFlagProviderValues() throws Exception { - useConfiguration("--experimental_dynamic_configs=notrim"); + useConfiguration( + "--experimental_dynamic_configs=notrim", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "java/com/foo/reader.bzl", "def _impl(ctx):", @@ -3263,6 +3293,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { "flag_reader(", " name = 'FooFlags',", " flags = [':flag1', ':flag2'],", + " transitive_configs = [':flag1', ':flag2'],", ")", "android_binary(", " name = 'foo',", @@ -3270,7 +3301,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " srcs = [':FooFlags.java'],", " feature_flags = {", " 'flag1': 'on',", - " }", + " },", + " transitive_configs = [':flag1', ':flag2'],", ")"); Artifact flagList = getFirstArtifactEndingWith( @@ -3316,6 +3348,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagPolicyMustContainRuleToUseFeatureFlags() throws Exception { reporter.removeHandler(failFastHandler); // expecting an error + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(", @@ -3337,7 +3370,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " srcs = [':FooFlags.java'],", " feature_flags = {", " '//flag:flag': 'right',", - " }", + " },", + " transitive_configs = ['//flag:flag'],", ")"); assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNull(); assertContainsEvent( @@ -3348,6 +3382,7 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { @Test public void testFeatureFlagPolicyDoesNotBlockRuleIfInPolicy() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.overwriteFile( "tools/whitelists/config_feature_flag/BUILD", "package_group(", @@ -3369,7 +3404,8 @@ public class AndroidBinaryTest extends AndroidBuildViewTestCase { " srcs = [':FooFlags.java'],", " feature_flags = {", " '//flag:flag': 'right',", - " }", + " },", + " transitive_configs = ['//flag:flag'],", ")"); assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNotNull(); assertNoEvents(); diff --git a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfigurationTest.java b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfigurationTest.java index 07dcae5dc2..7811335294 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfigurationTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagConfigurationTest.java @@ -16,12 +16,15 @@ package com.google.devtools.build.lib.rules.config; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.actions.util.LabelArtifactOwner; import com.google.devtools.build.lib.cmdline.Label; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -40,14 +43,40 @@ public final class ConfigFeatureFlagConfigurationTest { } @Test - public void getFeatureFlagValue_returnsEmptyOptionalWhenRequestingUnsetFlag() throws Exception { + public void + getFeatureFlagValue_returnsEmptyOptionalWhenRequestingUnknownFlagFromUntrimmedOptions() + throws Exception { Optional<String> flagValue = - getConfigurationWithFlags(ImmutableMap.of(Label.parseAbsoluteUnchecked("//a:a"), "valued")) + getUntrimmedConfigurationWithFlags( + ImmutableMap.of(Label.parseAbsoluteUnchecked("//a:a"), "valued")) .getFeatureFlagValue(new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//b:b"))); assertThat(flagValue).isEmpty(); } @Test + public void getFeatureFlagValue_returnsEmptyOptionalWhenRequestingKnownDefaultFlag() + throws Exception { + Optional<String> flagValue = + getConfigurationWithFlags( + ImmutableMap.of(Label.parseAbsoluteUnchecked("//a:a"), "valued"), + ImmutableSet.of( + Label.parseAbsoluteUnchecked("//a:a"), Label.parseAbsoluteUnchecked("//b:b"))) + .getFeatureFlagValue(new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//b:b"))); + assertThat(flagValue).isEmpty(); + } + + @Test + public void getFeatureFlagValue_throwsExceptionWhenRequestingUnknownFlag() throws Exception { + try { + getConfigurationWithFlags(ImmutableMap.of(Label.parseAbsoluteUnchecked("//a:a"), "valued")) + .getFeatureFlagValue(new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//b:b"))); + fail("no exception was thrown"); + } catch (ConfigFeatureFlagConfiguration.MissingFlagException expected) { + assertThat(expected).hasMessageThat().contains("//b:b"); + } + } + + @Test public void getOutputDirectoryName_returnsNullWhenFlagMapIsEmpty() throws Exception { assertThat(getConfigurationWithFlags(ImmutableMap.<Label, String>of()).getOutputDirectoryName()) .isNull(); @@ -133,7 +162,22 @@ public final class ConfigFeatureFlagConfigurationTest { /** Generates a configuration fragment with the given set of flag-value pairs. */ private static ConfigFeatureFlagConfiguration getConfigurationWithFlags( Map<Label, String> flags) { + return getConfigurationWithFlags(flags, flags.keySet()); + } + + private static ConfigFeatureFlagConfiguration getConfigurationWithFlags( + Map<Label, String> flags, Set<Label> availableFlags) { + ConfigFeatureFlagOptions options = new ConfigFeatureFlagOptions(); + options.enforceTransitiveConfigsForConfigFeatureFlag = true; + options.replaceFlagValues(flags); + options.trimFlagValues(availableFlags); + return new ConfigFeatureFlagConfiguration(options); + } + + private static ConfigFeatureFlagConfiguration getUntrimmedConfigurationWithFlags( + Map<Label, String> flags) { ConfigFeatureFlagOptions options = new ConfigFeatureFlagOptions(); + options.enforceTransitiveConfigsForConfigFeatureFlag = false; options.replaceFlagValues(flags); return new ConfigFeatureFlagConfiguration(options); } diff --git a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTest.java b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTest.java index 411294abbd..e9861dfc66 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/config/ConfigFeatureFlagTest.java @@ -38,7 +38,9 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { @Before public void useTrimmedConfigurations() throws Exception { - useConfiguration("--experimental_dynamic_configs=on"); + useConfiguration( + "--experimental_dynamic_configs=on", + "--enforce_transitive_configs_for_config_feature_flag"); } @Override @@ -72,6 +74,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':flag': 'configured',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -91,6 +94,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':flag': 'configured',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -157,10 +161,12 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':flag': 'configured',", " },", + " transitive_configs = [':flag'],", ")", "flag_reading_wrapper(", " name = 'wrapper',", " flag = ':flag',", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -215,6 +221,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { "flag_reading_wrapper(", " name = 'wrapper',", " flag = ':flag',", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -253,6 +260,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':other': 'configured',", " },", + " transitive_configs = [':flag', ':other'],", ")", "config_feature_flag(", " name = 'flag',", @@ -280,6 +288,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':other': 'configured',", " },", + " transitive_configs = [':flag', ':other'],", ")", "config_feature_flag(", " name = 'flag',", @@ -339,6 +348,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':flag': 'legal',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -362,6 +372,7 @@ public final class ConfigFeatureFlagTest extends SkylarkTestCase { " flag_values = {", " ':flag': 'invalid',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", 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 8faf64d8e4..46042fdce3 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 @@ -399,7 +399,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void matchesIfFlagValuesAndValuesBothMatch() throws Exception { - useConfiguration("--copt=-Dright"); + useConfiguration("--copt=-Dright", "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -410,6 +410,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -421,6 +422,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void matchesIfFlagValuesMatchAndValuesAreEmpty() throws Exception { + useConfiguration("--copt=-Dright", "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -429,6 +431,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':flag': 'right',", " },", " values = {},", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -455,7 +458,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfNeitherFlagValuesNorValuesMatches() throws Exception { - useConfiguration("--copt=-Dright"); + useConfiguration("--copt=-Dright", "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -466,6 +469,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dwrong',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -477,6 +481,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfFlagValuesDoNotMatchAndValuesAreEmpty() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -485,6 +490,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':flag': 'wrong',", " },", " values = {},", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -496,7 +502,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfFlagValuesDoNotMatchButValuesDo() throws Exception { - useConfiguration("--copt=-Dright"); + useConfiguration("--copt=-Dright", "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -507,6 +513,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -533,7 +540,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfValuesDoNotMatchButFlagValuesDo() throws Exception { - useConfiguration("--copt=-Dright"); + useConfiguration("--copt=-Dright", "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -544,6 +551,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dwrong',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -555,6 +563,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfEvenOneFlagValueDoesNotMatch() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -564,6 +573,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':flag2': 'bad',", " },", " values = {},", + " transitive_configs = [':flag', ':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -580,12 +590,14 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void matchesIfNonDefaultIsSpecifiedAndFlagValueIsThatValue() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'setter',", " exports_setting = ':match',", " flag_values = {':flag': 'actual'},", + " transitive_configs = [':flag'],", ")", "config_setting(", " name = 'match',", @@ -593,6 +605,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':flag': 'actual',", " },", " values = {},", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -604,12 +617,14 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotMatchIfDefaultIsSpecifiedAndFlagValueIsNotDefault() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "feature_flag_setter(", " name = 'setter',", " exports_setting = ':match',", " flag_values = {':flag': 'actual'},", + " transitive_configs = [':flag'],", ")", "config_setting(", " name = 'match',", @@ -617,6 +632,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':flag': 'default',", " },", " values = {},", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -628,7 +644,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithSameValuesAndSameFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -641,6 +660,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -652,6 +672,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -671,7 +692,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithDifferentValuesAndSameFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -683,6 +707,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -693,6 +718,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -712,7 +738,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithSameValuesAndDifferentFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -724,6 +753,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag'],", ")", "config_setting(", " name = 'other',", @@ -734,6 +764,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -753,7 +784,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithDifferentValuesAndDifferentFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -764,6 +798,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag'],", ")", "config_setting(", " name = 'other',", @@ -773,6 +808,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -792,7 +828,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithDifferentValuesAndSubsetFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -804,6 +843,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -813,6 +853,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -832,7 +873,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void doesNotRefineSettingWithSubsetValuesAndDifferentFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -844,6 +888,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag'],", ")", "config_setting(", " name = 'other',", @@ -853,6 +898,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -872,7 +918,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void refinesSettingWithSubsetValuesAndSameFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -885,6 +934,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -895,6 +945,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_feature_flag(", " name = 'flag',", @@ -914,7 +965,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void refinesSettingWithSameValuesAndSubsetFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -927,6 +981,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -937,6 +992,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -956,7 +1012,10 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void refinesSettingWithSubsetValuesAndSubsetFlagValues() throws Exception { - useConfiguration("--copt=-Dright", "--javacopt=-Dgood"); + useConfiguration( + "--copt=-Dright", + "--javacopt=-Dgood", + "--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -969,6 +1028,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " 'copt': '-Dright',", " 'javacopt': '-Dgood',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", @@ -978,6 +1038,7 @@ public class ConfigSettingTest extends BuildViewTestCase { " values = {", " 'copt': '-Dright',", " },", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -997,6 +1058,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void matchesAliasedFlagsInFlagValues() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -1004,10 +1066,12 @@ public class ConfigSettingTest extends BuildViewTestCase { " flag_values = {", " ':alias': 'right',", " },", + " transitive_configs = [':flag'],", ")", "alias(", " name = 'alias',", " actual = 'flag',", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -1019,6 +1083,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void aliasedFlagsAreCountedInRefining() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratch.file( "test/BUILD", "config_setting(", @@ -1027,16 +1092,19 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':alias': 'right',", " ':flag2': 'good',", " },", + " transitive_configs = [':flag', ':flag2'],", ")", "config_setting(", " name = 'other',", " flag_values = {", " ':flag': 'right',", " },", + " transitive_configs = [':flag'],", ")", "alias(", " name = 'alias',", " actual = 'flag',", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", @@ -1056,6 +1124,7 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void referencingSameFlagViaMultipleAliasesFails() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); checkError( "test", "multialias", @@ -1067,10 +1136,12 @@ public class ConfigSettingTest extends BuildViewTestCase { " ':alias': 'right',", " ':direct': 'right',", " },", + " transitive_configs = [':direct'],", ")", "alias(", " name = 'alias',", " actual = 'direct',", + " transitive_configs = [':direct'],", ")", "config_feature_flag(", " name = 'direct',", @@ -1098,15 +1169,19 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void requiresValidValueForFlagValues() throws Exception { - checkError("test", "invalid_flag", + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); + checkError( + "test", + "invalid_flag", "in flag_values attribute of config_setting rule //test:invalid_flag: " - + "error while parsing user-defined configuration values: " - + "'invalid' is not a valid value for '//test:flag'", + + "error while parsing user-defined configuration values: " + + "'invalid' is not a valid value for '//test:flag'", "config_setting(", " name = 'invalid_flag',", " flag_values = {", " ':flag': 'invalid',", - " })", + " },", + " transitive_configs = [':flag'])", "config_feature_flag(", " name = 'flag',", " allowed_values = ['right', 'valid'],", @@ -1116,18 +1191,23 @@ public class ConfigSettingTest extends BuildViewTestCase { @Test public void usesAliasLabelWhenReportingErrorInFlagValues() throws Exception { - checkError("test", "invalid_flag", + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); + checkError( + "test", + "invalid_flag", "in flag_values attribute of config_setting rule //test:invalid_flag: " - + "error while parsing user-defined configuration values: " - + "'invalid' is not a valid value for '//test:alias'", + + "error while parsing user-defined configuration values: " + + "'invalid' is not a valid value for '//test:alias'", "config_setting(", " name = 'invalid_flag',", " flag_values = {", " ':alias': 'invalid',", - " })", + " },", + " transitive_configs = [':flag'])", "alias(", " name = 'alias',", " actual = ':flag',", + " transitive_configs = [':flag'],", ")", "config_feature_flag(", " name = 'flag',", diff --git a/src/test/java/com/google/devtools/build/lib/rules/config/FeatureFlagManualTrimmingTest.java b/src/test/java/com/google/devtools/build/lib/rules/config/FeatureFlagManualTrimmingTest.java new file mode 100644 index 0000000000..e433fb98bf --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/rules/config/FeatureFlagManualTrimmingTest.java @@ -0,0 +1,770 @@ +// Copyright 2018 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 static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; +import com.google.devtools.build.lib.analysis.ConfiguredTarget; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.actions.FileWriteAction; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.skylark.util.SkylarkTestCase; +import com.google.devtools.build.lib.testutil.TestRuleClassProvider; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for manual trimming of feature flags with the transitive_configs attribute. */ +@RunWith(JUnit4.class) +public final class FeatureFlagManualTrimmingTest extends SkylarkTestCase { + + @Before + public void enableManualTrimming() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); + } + + @Override + protected ConfiguredRuleClassProvider getRuleClassProvider() { + ConfiguredRuleClassProvider.Builder builder = + new ConfiguredRuleClassProvider.Builder().addRuleDefinition(new FeatureFlagSetterRule()); + TestRuleClassProvider.addStandardRules(builder); + return builder.build(); + } + + @Before + public void setUpFlagReadingRule() throws Exception { + scratch.file( + "test/read_flags.bzl", + "def _read_flags_impl(ctx):", + " ctx.actions.write(", + " ctx.outputs.flagdict,", + " '\\n'.join(['%s:::%s' % (dep.label, dep[config_common.FeatureFlagInfo].value)", + " for dep in ctx.attr.flags]))", + " return [DefaultInfo(files = depset([ctx.outputs.flagdict]))]", + "read_flags = rule(", + " implementation = _read_flags_impl,", + " attrs = {'flags': attr.label_list()},", + " outputs = {'flagdict': '%{name}.flags'},", + ")"); + } + + @Before + public void setUpHostTransitionRule() throws Exception { + scratch.file( + "test/host_transition.bzl", + "def _host_transition_impl(ctx):", + " files = depset(transitive = [src[DefaultInfo].files for src in ctx.attr.srcs])", + " return [DefaultInfo(files = files)]", + "host_transition = rule(", + " implementation = _host_transition_impl,", + " attrs = {'srcs': attr.label_list(cfg='host')},", + ")"); + } + + private ImmutableSortedMap<Label, String> getFlagMapFromConfiguration(BuildConfiguration config) { + return config.getOptions().get(ConfigFeatureFlagOptions.class).getFlagValues(); + } + + private ImmutableSortedMap<Label, String> getFlagValuesFromOutputFile(Artifact flagDict) { + String fileContents = + ((FileWriteAction) getActionGraph().getGeneratingAction(flagDict)).getFileContents(); + return Splitter.on('\n') + .withKeyValueSeparator(":::") + .split(fileContents) + .entrySet() + .stream() + .collect( + toImmutableSortedMap( + Ordering.natural(), + (entry) -> Label.parseAbsoluteUnchecked(entry.getKey()), + Map.Entry::getValue)); + } + + @Test + public void duplicateTargetsCreatedWithTrimmingDisabled() throws Exception { + useConfiguration("--noenforce_transitive_configs_for_config_feature_flag"); + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'left',", + " deps = [':common'],", + " flag_values = {", + " ':different_flag': 'left',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':common_flag'],", + ")", + "feature_flag_setter(", + " name = 'right',", + " deps = [':common'],", + " flag_values = {", + " ':different_flag': 'right',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':common_flag'],", + ")", + "read_flags(", + " name = 'common',", + " flags = [':common_flag'],", + " transitive_configs = [':common_flag'],", + ")", + "config_feature_flag(", + " name = 'different_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'common_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact leftFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:left")).toList()); + Artifact rightFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:right")).toList()); + + assertThat(leftFlags).isNotEqualTo(rightFlags); + } + + @Test + public void featureFlagSetAndInTransitiveConfigs_GetsSetValue() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':trimmed_flag': 'left',", + " ':used_flag': 'configured',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact targetFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:target")).toList()); + + Label usedFlag = Label.parseAbsolute("//test:used_flag"); + assertThat(getFlagValuesFromOutputFile(targetFlags)).containsEntry(usedFlag, "configured"); + } + + @Test + public void featureFlagSetButNotInTransitiveConfigs_IsTrimmedOutAndCollapsesDuplicates() + throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'left',", + " deps = [':common'],", + " flag_values = {", + " ':different_flag': 'left',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':common_flag'],", + ")", + "feature_flag_setter(", + " name = 'right',", + " deps = [':common'],", + " flag_values = {" , + " ':different_flag': 'right',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':common_flag'],", + ")", + "read_flags(", + " name = 'common',", + " flags = [':common_flag'],", + " transitive_configs = [':common_flag'],", + ")", + "config_feature_flag(", + " name = 'different_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'common_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact leftFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:left")).toList()); + Artifact rightFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:right")).toList()); + + assertThat(leftFlags).isEqualTo(rightFlags); + assertThat(leftFlags.getArtifactOwner()).isEqualTo(rightFlags.getArtifactOwner()); + } + + @Test + public void featureFlagInTransitiveConfigsButNotSet_GetsDefaultValue() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':trimmed_flag': 'left',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact targetFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:target")).toList()); + + Label usedFlag = Label.parseAbsolute("//test:used_flag"); + assertThat(getFlagValuesFromOutputFile(targetFlags)).containsEntry(usedFlag, "default"); + } + + @Test + public void featureFlagInTransitiveConfigsButNotInTransitiveClosure_IsWastefulButDoesNotError() + throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'left',", + " deps = [':common'],", + " flag_values = {", + " ':different_flag': 'left',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':different_flag', ':common_flag'],", + ")", + "feature_flag_setter(", + " name = 'right',", + " deps = [':common'],", + " flag_values = {", + " ':different_flag': 'right',", + " ':common_flag': 'configured',", + " },", + " transitive_configs = [':different_flag', ':common_flag'],", + ")", + "read_flags(", + " name = 'common',", + " flags = [':common_flag'],", + " transitive_configs = [':different_flag', ':common_flag'],", + ")", + "config_feature_flag(", + " name = 'different_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'common_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact leftFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:left")).toList()); + Artifact rightFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:right")).toList()); + + assertThat(leftFlags).isNotEqualTo(rightFlags); + assertThat(leftFlags.getArtifactOwner()).isNotEqualTo(rightFlags.getArtifactOwner()); + } + + @Test + public void emptyTransitiveConfigs_EquivalentRegardlessOfFeatureFlags() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'left',", + " deps = [':reader'],", + " flag_values = {", + " ':used_flag': 'left',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "feature_flag_setter(", + " name = 'right',", + " deps = [':reader'],", + " flag_values = {", + " ':used_flag': 'right',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " transitive_configs = [],", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")"); + + Artifact leftFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:left")).toList()); + Artifact rightFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:right")).toList()); + Artifact directFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:reader")).toList()); + + assertThat(leftFlags).isEqualTo(rightFlags); + assertThat(leftFlags).isEqualTo(directFlags); + } + + @Test + public void absentTransitiveConfigs_EquivalentRegardlessOfFeatureFlags() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'left',", + " deps = [':reader'],", + " flag_values = {", + " ':used_flag': 'left',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "feature_flag_setter(", + " name = 'right',", + " deps = [':reader'],", + " flag_values = {", + " ':used_flag': 'right',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + // no transitive_configs = equivalent to [] + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")"); + + Artifact leftFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:left")).toList()); + Artifact rightFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:right")).toList()); + Artifact directFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:reader")).toList()); + + assertThat(leftFlags).isEqualTo(rightFlags); + assertThat(leftFlags).isEqualTo(directFlags); + } + + @Test + public void nonexistentLabelInTransitiveConfigs_DoesNotError() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':trimmed_flag': 'left',", + " },", + " transitive_configs = [':false_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " transitive_configs = [':false_flag'],", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")"); + + getConfiguredTarget("//test:target"); + assertNoEvents(); + } + + @Test + public void flagSetBySetterButNotInTransitiveConfigs_CanBeUsedByDeps() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':not_actually_trimmed_flag': 'left',", + " },", + " transitive_configs = [],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':not_actually_trimmed_flag'],", + " transitive_configs = [':not_actually_trimmed_flag'],", + ")", + "config_feature_flag(", + " name = 'not_actually_trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")"); + + getConfiguredTarget("//test:target"); + assertNoEvents(); + } + + @Test + public void featureFlagInUnusedSelectBranchButNotInTransitiveConfigs_DoesNotError() + throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':trimmed_flag': 'left',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " flags = select({':used_flag@other': [':trimmed_flag'], '//conditions:default': []}),", + " transitive_configs = [':used_flag'],", + ")", + "config_setting(", + " name = 'used_flag@other',", + " flag_values = {':used_flag': 'other'},", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + getConfiguredTarget("//test:target"); + assertNoEvents(); + } + + @Test + public void featureFlagTarget_IsTrimmedToOnlyItself() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " exports_flag = ':read_flag',", + " flag_values = {", + " ':trimmed_flag': 'left',", + " ':read_flag': 'configured',", + " },", + " transitive_configs = [':trimmed_flag', ':read_flag'],", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'read_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + ConfiguredTarget target = getConfiguredTarget("//test:target"); + RuleContext ruleContext = getRuleContext(target); + BuildConfiguration childConfiguration = + Iterables.getOnlyElement(ruleContext.getPrerequisiteConfiguredTargetAndTargets("exports_flag", Mode.TARGET)).getConfiguration(); + + Label childLabel = Label.parseAbsoluteUnchecked("//test:read_flag"); + assertThat(getFlagMapFromConfiguration(childConfiguration).keySet()) + .containsExactly(childLabel); + } + + @Test + public void featureFlagAccessedByPathWithMissingLabel_ProducesError() throws Exception { + reporter.removeHandler(failFastHandler); // expecting an error + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':broken'],", + " flag_values = {", + " ':used_flag': 'configured',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "filegroup(", + " name = 'broken',", + " srcs = [':reader'],", + " transitive_configs = [],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + assertThat(getConfiguredTarget("//test:target")).isNull(); + assertContainsEvent( + "Feature flag //test:used_flag was accessed in a configuration it is not present in. All " + + "targets which depend on //test:used_flag directly or indirectly must name it in " + + "their transitive_configs attribute."); + } + + @Test + public void featureFlagAccessedByPathWithMissingTransitiveConfigs_ProducesError() + throws Exception { + reporter.removeHandler(failFastHandler); // expecting an error + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':broken'],", + " flag_values = {", + " ':used_flag': 'configured',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "filegroup(", + " name = 'broken',", + " srcs = [':reader'],", + // no transitive_configs = equivalent to [] + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + assertThat(getConfiguredTarget("//test:target")).isNull(); + assertContainsEvent( + "Feature flag //test:used_flag was accessed in a configuration it is not present in. All " + + "targets which depend on //test:used_flag directly or indirectly must name it in " + + "their transitive_configs attribute."); + } + + @Test + public void featureFlagInHostConfiguration_HasDefaultValue() throws Exception { + scratch.file( + "test/BUILD", + "load(':host_transition.bzl', 'host_transition')", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':host'],", + " flag_values = {", + " ':used_flag': 'configured',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "host_transition(", + " name = 'host',", + " srcs = [':reader'],", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact targetFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:target")).toList()); + + Label usedFlag = Label.parseAbsolute("//test:used_flag"); + assertThat(getFlagValuesFromOutputFile(targetFlags)).containsEntry(usedFlag, "default"); + } + + @Test + public void featureFlagInHostConfiguration_HasNoTransitiveConfigEnforcement() throws Exception { + scratch.file( + "test/BUILD", + "load(':host_transition.bzl', 'host_transition')", + "load(':read_flags.bzl', 'read_flags')", + "feature_flag_setter(", + " name = 'target',", + " deps = [':host'],", + " flag_values = {", + " ':used_flag': 'configured',", + " },", + // no transitive_configs + ")", + "host_transition(", + " name = 'host',", + " srcs = [':reader'],", + // no transitive_configs + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + // no transitive_configs + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + getConfiguredTarget("//test:target"); + assertNoEvents(); + } + + @Test + public void featureFlagAccessedDirectly_ReturnsDefaultValue() throws Exception { + scratch.file( + "test/BUILD", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + assertThat( + ConfigFeatureFlagProvider.fromTarget(getConfiguredTarget("//test:used_flag")) + .getValue()) + .isEqualTo("default"); + } + + @Test + public void featureFlagAccessedViaTopLevelLibraryTarget_ReturnsDefaultValue() throws Exception { + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + Artifact targetFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:reader")).toList()); + + Label usedFlag = Label.parseAbsolute("//test:used_flag"); + assertThat(getFlagValuesFromOutputFile(targetFlags)).containsEntry(usedFlag, "default"); + } + + @Test + public void featureFlagSettingRules_OverrideFlagsFromReverseTransitiveClosure() + throws Exception { + // In other words: if you have a dependency which sets feature flags itself, you don't need to + // name any of the feature flags used by that target or its transitive closure, as it sets + // feature flags itself. + // This is because the feature flag setting transition (which calls replaceFlagValues) runs + // before the trimming transition and completely replaces the feature flag set. Thus, when + // the trimming transition (which calls trimFlagValues) runs, its requests are always satisfied. + + scratch.file( + "test/BUILD", + "load(':read_flags.bzl', 'read_flags')", + "filegroup(", + " name = 'toplevel',", + " srcs = [':target'],", + // no transitive_configs + ")", + "feature_flag_setter(", + " name = 'target',", + " deps = [':reader'],", + " flag_values = {", + " ':trimmed_flag': 'left',", + " ':used_flag': 'configured',", + " },", + " transitive_configs = [':used_flag'],", + ")", + "read_flags(", + " name = 'reader',", + " flags = [':used_flag'],", + " transitive_configs = [':used_flag'],", + ")", + "config_feature_flag(", + " name = 'trimmed_flag',", + " allowed_values = ['default', 'left', 'right'],", + " default_value = 'default',", + ")", + "config_feature_flag(", + " name = 'used_flag',", + " allowed_values = ['default', 'configured', 'other'],", + " default_value = 'default',", + ")"); + + Artifact targetFlags = + Iterables.getOnlyElement(getFilesToBuild(getConfiguredTarget("//test:toplevel")).toList()); + + Label usedFlag = Label.parseAbsolute("//test:used_flag"); + assertThat(getFlagValuesFromOutputFile(targetFlags)).containsEntry(usedFlag, "configured"); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java index 51c145a84f..cc8fb258eb 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleBinaryTest.java @@ -1494,6 +1494,7 @@ public class AppleBinaryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_offByDefault() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -1501,6 +1502,7 @@ public class AppleBinaryTest extends ObjcRuleTestCase { " name = 'bin',", " deps = ['//lib:objcLib'],", " platform_type = 'ios',", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkAction("//test:bin"); @@ -1522,6 +1524,7 @@ public class AppleBinaryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_oneFlagOn() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -1531,7 +1534,8 @@ public class AppleBinaryTest extends ObjcRuleTestCase { " platform_type = 'ios',", " feature_flags = {", " '//lib:flag2': 'on',", - " }", + " },", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkAction("//test:bin"); @@ -1553,6 +1557,7 @@ public class AppleBinaryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_allFlagsOn() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -1563,7 +1568,8 @@ public class AppleBinaryTest extends ObjcRuleTestCase { " feature_flags = {", " '//lib:flag1': 'on',", " '//lib:flag2': 'on',", - " }", + " },", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkAction("//test:bin"); diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleStaticLibraryTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleStaticLibraryTest.java index f8d056ad9e..2649ebca97 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/objc/AppleStaticLibraryTest.java +++ b/src/test/java/com/google/devtools/build/lib/rules/objc/AppleStaticLibraryTest.java @@ -567,6 +567,7 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_offByDefault() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -574,6 +575,7 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { " name = 'static_lib',", " deps = ['//lib:objcLib'],", " platform_type = 'ios',", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkLibAction("//test:static_lib"); @@ -595,6 +597,7 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_oneFlagOn() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -604,7 +607,8 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { " platform_type = 'ios',", " feature_flags = {", " '//lib:flag2': 'on',", - " }", + " },", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkLibAction("//test:static_lib"); @@ -626,6 +630,7 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { @Test public void testFeatureFlags_allFlagsOn() throws Exception { + useConfiguration("--enforce_transitive_configs_for_config_feature_flag"); scratchFeatureFlagTestLib(); scratch.file( "test/BUILD", @@ -636,7 +641,8 @@ public class AppleStaticLibraryTest extends ObjcRuleTestCase { " feature_flags = {", " '//lib:flag1': 'on',", " '//lib:flag2': 'on',", - " }", + " },", + " transitive_configs = ['//lib:flag1', '//lib:flag2'],", ")"); CommandAction linkAction = linkLibAction("//test:static_lib"); diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java index 598eda0fc0..4b72583b21 100644 --- a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java +++ b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java @@ -2267,6 +2267,7 @@ public abstract class ObjcRuleTestCase extends BuildViewTestCase { "config_setting(", " name = 'flag1@on',", " flag_values = {':flag1': 'on'},", + " transitive_configs = [':flag1'],", ")", "config_feature_flag(", " name = 'flag2',", @@ -2276,6 +2277,7 @@ public abstract class ObjcRuleTestCase extends BuildViewTestCase { "config_setting(", " name = 'flag2@on',", " flag_values = {':flag2': 'on'},", + " transitive_configs = [':flag2'],", ")", "objc_library(", " name = 'objcLib',", @@ -2293,6 +2295,7 @@ public abstract class ObjcRuleTestCase extends BuildViewTestCase { " ':flag2@on': ['-FLAG_2_ON'],", " '//conditions:default': ['-FLAG_2_OFF'],", " }),", + " transitive_configs = [':flag1', ':flag2'],", ")"); } } diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java index 8c4b7df066..e449ed1ceb 100644 --- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java +++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java @@ -613,6 +613,7 @@ public class SkylarkRuleContextTest extends SkylarkTestCase { assertThat(result).containsExactly( "name", "visibility", + "transitive_configs", "tags", "generator_name", "generator_function", |