diff --git a/lib/selects.bzl b/lib/selects.bzl index 1c589d3..eb06098 100644 --- a/lib/selects.bzl +++ b/lib/selects.bzl @@ -24,7 +24,7 @@ def _with_or(input_dict, no_match_error = ""): `//foo:config1` OR `//foo:config2` OR `...`. no_match_error: Optional custom error to report if no condition matches. - Example: + Example: ```build deps = selects.with_or({ @@ -76,7 +76,171 @@ def _with_or_dict(input_dict): output_dict[key] = value return output_dict +def _config_setting_group(name, match_any = [], match_all = []): + """Matches if all or any of its member `config_setting`s match. + + Args: + name: The group's name. This is how `select()`s reference it. + match_any: A list of `config_settings`. This group matches if *any* member + in the list matches. If this is set, `match_all` must not be set. + match_all: A list of `config_settings`. This group matches if *every* + member in the list matches. If this is set, `match_any` must be not + set. + + Exactly one of match_any or match_all must be non-empty. + + Example: + + ```build + config_setting(name = "one", define_values = {"foo": "true"}) + config_setting(name = "two", define_values = {"bar": "false"}) + config_setting(name = "three", define_values = {"baz": "more_false"}) + + config_setting_group( + name = "one_two_three", + match_all = [":one", ":two", ":three"] + ) + + cc_binary( + name = "myapp", + srcs = ["myapp.cc"], + deps = select({ + ":one_two_three": [":special_deps"], + "//conditions:default": [":default_deps"] + }) + ``` + """ + empty1 = not bool(len(match_any)) + empty2 = not bool(len(match_all)) + if (empty1 and empty2) or (not empty1 and not empty2): + fail('Either "match_any" or "match_all" must be set, but not both.') + _check_duplicates(match_any) + _check_duplicates(match_all) + + if ((len(match_any) == 1 and match_any[0] == "//conditions:default") or + (len(match_all) == 1 and match_all[0] == "//conditions:default")): + # If the only entry is "//conditions:default", the condition is + # automatically true. + _config_setting_always_true(name) + elif not empty1: + _config_setting_or_group(name, match_any) + else: + _config_setting_and_group(name, match_all) + +def _check_duplicates(settings): + """Fails if any entry in settings appears more than once.""" + seen = {} + for setting in settings: + if setting in seen: + fail(setting + " appears more than once. Duplicates not allowed.") + seen[setting] = True + +def _remove_default_condition(settings): + """Returns settings with "//conditions:default" entries filtered out.""" + new_settings = [] + for setting in settings: + if settings != "//conditions:default": + new_settings.append(setting) + return new_settings + +def _config_setting_or_group(name, settings): + """ORs multiple config_settings together (inclusively). + + The core idea is to create a sequential chain of alias targets where each is + select-resolved as follows: If alias n matches config_setting n, the chain + is true so it resolves to config_setting n. Else it resolves to alias n+1 + (which checks config_setting n+1, and so on). If none of the config_settings + match, the final alias resolves to one of them arbitrarily, which by + definition doesn't match. + """ + + # "//conditions:default" is present, the whole chain is automatically true. + if len(_remove_default_condition(settings)) < len(settings): + _config_setting_always_true(name) + return + + elif len(settings) == 1: # One entry? Just alias directly to it. + native.alias( + name = name, + actual = settings[0], + ) + return + + # We need n-1 aliases for n settings. The first alias has no extension. The + # second alias is named name + "_2", and so on. For the first n-2 aliases, + # if they don't match they reference the next alias over. If the n-1st alias + # doesn't match, it references the final setting (which is then evaluated + # directly to determine the final value of the AND chain). + actual = [name + "_" + str(i) for i in range(2, len(settings))] + actual.append(settings[-1]) + + for i in range(1, len(settings)): + native.alias( + name = name if i == 1 else name + "_" + str(i), + actual = select({ + settings[i - 1]: settings[i - 1], + "//conditions:default": actual[i - 1], + }), + ) + +def _config_setting_and_group(name, settings): + """ANDs multiple config_settings together. + + The core idea is to create a sequential chain of alias targets where each is + select-resolved as follows: If alias n matches config_setting n, it resolves to + alias n+1 (which evaluates config_setting n+1, and so on). Else it resolves to + config_setting n, which doesn't match by definition. The only way to get a + matching final result is if all config_settings match. + """ + + # "//conditions:default" is automatically true so doesn't need checking. + settings = _remove_default_condition(settings) + + # One config_setting input? Just alias directly to it. + if len(settings) == 1: + native.alias( + name = name, + actual = settings[0], + ) + return + + # We need n-1 aliases for n settings. The first alias has no extension. The + # second alias is named name + "_2", and so on. For the first n-2 aliases, + # if they match they reference the next alias over. If the n-1st alias matches, + # it references the final setting (which is then evaluated directly to determine + # the final value of the AND chain). + actual = [name + "_" + str(i) for i in range(2, len(settings))] + actual.append(settings[-1]) + + for i in range(1, len(settings)): + native.alias( + name = name if i == 1 else name + "_" + str(i), + actual = select({ + settings[i - 1]: actual[i - 1], + "//conditions:default": settings[i - 1], + }), + ) + +def _config_setting_always_true(name): + """Returns a config_setting with the given name that's always true. + + This is achieved by constructing a two-entry OR chain where each + config_setting takes opposite values of a boolean flag. + """ + name_on = name + "_stamp_binary_on_check" + name_off = name + "_stamp_binary_off_check" + native.config_setting( + name = name_on, + values = {"stamp": True}, + ) + native.config_setting( + name = name_off, + values = {"stamp": False}, + ) + return _config_setting_or_group(name, [":" + name_on, ":" + name_off]) + selects = struct( with_or = _with_or, with_or_dict = _with_or_dict, + config_setting_group = _config_setting_group, ) diff --git a/tests/selects_tests.bzl b/tests/selects_tests.bzl index b9a354d..1d77cee 100644 --- a/tests/selects_tests.bzl +++ b/tests/selects_tests.bzl @@ -15,8 +15,11 @@ """Unit tests for selects.bzl.""" load("//lib:selects.bzl", "selects") -load("//lib:unittest.bzl", "asserts", "unittest") +load("//lib:unittest.bzl", "analysistest", "asserts", "unittest") +################################################### +# with_or_test +################################################### def _with_or_test(ctx): """Unit tests for with_or.""" env = unittest.begin(ctx) @@ -57,9 +60,526 @@ def _with_or_test(ctx): with_or_test = unittest.make(_with_or_test) +################################################### +# BUILD declarations for config_setting_group tests +################################################### + +# TODO: redefine these config_settings with Starlark build flags when +# they're available non-experimentally. +def _create_config_settings(): + native.config_setting( + name = "condition1", + values = {"cpu": "ppc"}, + ) + native.config_setting( + name = "condition2", + values = {"compilation_mode": "opt"}, + ) + native.config_setting( + name = "condition3", + values = {"features": "myfeature"}, + ) + +def _create_config_setting_groups(): + selects.config_setting_group( + name = "1_and_2_and_3", + match_all = [":condition1", ":condition2", ":condition3"], + ) + selects.config_setting_group( + name = "1_and_nothing_else", + match_all = [":condition1"], + ) + selects.config_setting_group( + name = "1_or_2_or_3", + match_any = [":condition1", ":condition2", ":condition3"], + ) + selects.config_setting_group( + name = "1_or_nothing_else", + match_any = [":condition1"], + ) + +################################################### +# Support code for config_setting_group tests +################################################### + +def _set_conditions(condition_list): + """Returns an argument for config_settings that sets specific options. + + Args: + condition_list: a list of three booleans + + Returns: + a dictionary parameter for config_settings such that ":conditionN" is True + iff condition_list[N + 1] is True + """ + if len(condition_list) != 3: + fail("condition_list must be a list of 3 booleans") + ans = {} + if condition_list[0]: + ans["//command_line_option:cpu"] = "ppc" + if condition_list[1]: + ans["//command_line_option:compilation_mode"] = "opt" + if condition_list[2]: + ans["//command_line_option:features"] = ["myfeature"] + return ans + +_BooleanInfo = provider() + +def _boolean_attr_impl(ctx): + return [_BooleanInfo(value = ctx.attr.myboolean)] + +boolean_attr_rule = rule( + implementation = _boolean_attr_impl, + attrs = {"myboolean": attr.bool()}, +) + +def _expect_matches(ctx): + """Generic test implementation expecting myboolean == True.""" + env = analysistest.begin(ctx) + attrval = analysistest.target_under_test(env)[_BooleanInfo].value + asserts.equals(env, True, attrval) + return analysistest.end(env) + +def _expect_doesnt_match(ctx): + """Generic test implementation expecting myboolean == False.""" + env = analysistest.begin(ctx) + attrval = analysistest.target_under_test(env)[_BooleanInfo].value + asserts.equals(env, False, attrval) + return analysistest.end(env) + +def _config_setting_group_test(name, config_settings): + return analysistest.make() + +################################################### +# and_config_setting_group_matches_test +################################################### +and_config_setting_group_matches_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([True, True, True]), +) + +def _and_config_setting_group_matches_test(): + """Test verifying match on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_matches_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_matches_test( + name = "and_config_setting_group_matches_test", + target_under_test = ":and_config_setting_group_matches_rule", + ) + +################################################### +# and_config_setting_group_first_match_fails_test +################################################### +and_config_setting_group_first_match_fails_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([False, True, True]), +) + +def _and_config_setting_group_first_match_fails_test(): + """Test verifying first condition mismatch on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_first_match_fails_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_first_match_fails_test( + name = "and_config_setting_group_first_match_fails_test", + target_under_test = ":and_config_setting_group_first_match_fails_rule", + ) + +################################################### +# and_config_setting_group_middle_match_fails_test +################################################### +and_config_setting_group_middle_match_fails_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([True, False, True]), +) + +def _and_config_setting_group_middle_match_fails_test(): + """Test verifying middle condition mismatch on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_middle_match_fails_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_middle_match_fails_test( + name = "and_config_setting_group_middle_match_fails_test", + target_under_test = ":and_config_setting_group_middle_match_fails_rule", + ) + +################################################### +# and_config_setting_group_last_match_fails_test +################################################### +and_config_setting_group_last_match_fails_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([True, True, False]), +) + +def _and_config_setting_group_last_match_fails_test(): + """Test verifying last condition mismatch on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_last_match_fails_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_last_match_fails_test( + name = "and_config_setting_group_last_match_fails_test", + target_under_test = ":and_config_setting_group_last_match_fails_rule", + ) + +################################################### +# and_config_setting_group_multiple_matches_fail_test +################################################### +and_config_setting_group_multiple_matches_fail_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([True, False, False]), +) + +def _and_config_setting_group_multiple_matches_fail_test(): + """Test verifying multple conditions mismatch on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_multiple_matches_fail_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_multiple_matches_fail_test( + name = "and_config_setting_group_multiple_matches_fail_test", + target_under_test = ":and_config_setting_group_multiple_matches_fail_rule", + ) + +################################################### +# and_config_setting_group_all_matches_fail_test +################################################### +and_config_setting_group_all_matches_fail_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([False, False, False]), +) + +def _and_config_setting_group_all_matches_fail_test(): + """Test verifying all conditions mismatch on an ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_all_matches_fail_rule", + myboolean = select( + { + ":1_and_2_and_3": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_all_matches_fail_test( + name = "and_config_setting_group_all_matches_fail_test", + target_under_test = ":and_config_setting_group_all_matches_fail_rule", + ) + +################################################### +# and_config_setting_group_single_setting_matches_test +################################################### +and_config_setting_group_single_setting_matches_test = analysistest.make( + _expect_matches, + config_settings = {"//command_line_option:cpu": "ppc"}, +) + +def _and_config_setting_group_single_setting_matches_test(): + """Test verifying match on single-entry ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_single_setting_matches_rule", + myboolean = select( + { + ":1_and_nothing_else": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_single_setting_matches_test( + name = "and_config_setting_group_single_setting_matches_test", + target_under_test = ":and_config_setting_group_single_setting_matches_rule", + ) + +################################################### +# and_config_setting_group_single_setting_fails_test +################################################### +and_config_setting_group_single_setting_fails_test = analysistest.make( + _expect_doesnt_match, + config_settings = {"//command_line_option:cpu": "x86"}, +) + +def _and_config_setting_group_single_setting_fails_test(): + """Test verifying no match on single-entry ANDing config_setting_group.""" + boolean_attr_rule( + name = "and_config_setting_group_single_setting_fails_rule", + myboolean = select( + { + ":1_and_nothing_else": True, + "//conditions:default": False, + }, + ), + ) + and_config_setting_group_single_setting_fails_test( + name = "and_config_setting_group_single_setting_fails_test", + target_under_test = ":and_config_setting_group_single_setting_fails_rule", + ) + +################################################### +# or_config_setting_group_no_match_test +################################################### +or_config_setting_group_no_matches_test = analysistest.make( + _expect_doesnt_match, + config_settings = _set_conditions([False, False, False]), +) + +def _or_config_setting_group_no_matches_test(): + """Test verifying no matches on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_no_matches_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_no_matches_test( + name = "or_config_setting_group_no_matches_test", + target_under_test = ":or_config_setting_group_no_matches_rule", + ) + +################################################### +# or_config_setting_group_first_cond_matches_test +################################################### +or_config_setting_group_first_cond_matches_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([True, False, False]), +) + +def _or_config_setting_group_first_cond_matches_test(): + """Test verifying first condition matching on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_first_cond_matches_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_first_cond_matches_test( + name = "or_config_setting_group_first_cond_matches_test", + target_under_test = ":or_config_setting_group_first_cond_matches_rule", + ) + +################################################### +# or_config_setting_group_middle_cond_matches_test +################################################### +or_config_setting_group_middle_cond_matches_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([False, True, False]), +) + +def _or_config_setting_group_middle_cond_matches_test(): + """Test verifying middle condition matching on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_middle_cond_matches_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_middle_cond_matches_test( + name = "or_config_setting_group_middle_cond_matches_test", + target_under_test = ":or_config_setting_group_middle_cond_matches_rule", + ) + +################################################### +# or_config_setting_group_last_cond_matches_test +################################################### +or_config_setting_group_last_cond_matches_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([False, False, True]), +) + +def _or_config_setting_group_last_cond_matches_test(): + """Test verifying last condition matching on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_last_cond_matches_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_last_cond_matches_test( + name = "or_config_setting_group_last_cond_matches_test", + target_under_test = ":or_config_setting_group_last_cond_matches_rule", + ) + +################################################### +# or_config_setting_group_multiple_conds_match_test +################################################### +or_config_setting_group_multiple_conds_match_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([False, True, True]), +) + +def _or_config_setting_group_multiple_conds_match_test(): + """Test verifying multple conditions matching on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_multiple_conds_match_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_multiple_conds_match_test( + name = "or_config_setting_group_multiple_conds_match_test", + target_under_test = ":or_config_setting_group_multiple_conds_match_rule", + ) + +################################################### +# or_config_setting_group_all_conds_match_test +################################################### +or_config_setting_group_all_conds_match_test = analysistest.make( + _expect_matches, + config_settings = _set_conditions([False, True, True]), +) + +def _or_config_setting_group_all_conds_match_test(): + """Test verifying all conditions matching on an ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_all_conds_match_rule", + myboolean = select( + { + ":1_or_2_or_3": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_all_conds_match_test( + name = "or_config_setting_group_all_conds_match_test", + target_under_test = ":or_config_setting_group_all_conds_match_rule", + ) + +################################################### +# or_config_setting_group_single_setting_matches_test +################################################### +or_config_setting_group_single_setting_matches_test = analysistest.make( + _expect_matches, + config_settings = {"//command_line_option:cpu": "ppc"}, +) + +def _or_config_setting_group_single_setting_matches_test(): + """Test verifying match on single-entry ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_single_setting_matches_rule", + myboolean = select( + { + ":1_or_nothing_else": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_single_setting_matches_test( + name = "or_config_setting_group_single_setting_matches_test", + target_under_test = ":or_config_setting_group_single_setting_matches_rule", + ) + +################################################### +# or_config_setting_group_single_setting_fails_test +################################################### +or_config_setting_group_single_setting_fails_test = analysistest.make( + _expect_doesnt_match, + config_settings = {"//command_line_option:cpu": "x86"}, +) + +def _or_config_setting_group_single_setting_fails_test(): + """Test verifying no match on single-entry ORing config_setting_group.""" + boolean_attr_rule( + name = "or_config_setting_group_single_setting_fails_rule", + myboolean = select( + { + ":1_or_nothing_else": True, + "//conditions:default": False, + }, + ), + ) + or_config_setting_group_single_setting_fails_test( + name = "or_config_setting_group_single_setting_fails_test", + target_under_test = ":or_config_setting_group_single_setting_fails_rule", + ) + +################################################### +# empty_config_setting_group_not_allowed_test +################################################### + +# config_setting_group with no parameters triggers a failure. +# TODO: how do we test this? This requires catching macro +# evaluation failure. + +################################################### +# and_and_or_not_allowed_together_test +################################################### + +# config_setting_group: setting both match_any and match_or +# triggers a failure. +# TODO: how do we test this? This requires catching macro +# evaluation failure. + +################################################### + def selects_test_suite(): """Creates the test targets and test suite for selects.bzl tests.""" unittest.suite( "selects_tests", with_or_test, ) + + _create_config_settings() + _create_config_setting_groups() + + _and_config_setting_group_matches_test() + _and_config_setting_group_first_match_fails_test() + _and_config_setting_group_middle_match_fails_test() + _and_config_setting_group_last_match_fails_test() + _and_config_setting_group_multiple_matches_fail_test() + _and_config_setting_group_all_matches_fail_test() + _and_config_setting_group_single_setting_matches_test() + _and_config_setting_group_single_setting_fails_test() + + _or_config_setting_group_no_matches_test() + _or_config_setting_group_first_cond_matches_test() + _or_config_setting_group_middle_cond_matches_test() + _or_config_setting_group_last_cond_matches_test() + _or_config_setting_group_multiple_conds_match_test() + _or_config_setting_group_all_conds_match_test() + _or_config_setting_group_single_setting_matches_test() + _or_config_setting_group_single_setting_fails_test() + + # _empty_config_setting_group_not_allowed_test() + # _and_and_or_not_allowed_together_test()