diff --git a/test/BUILD b/test/BUILD index 623a81f..b8ca1d1 100644 --- a/test/BUILD +++ b/test/BUILD @@ -62,6 +62,7 @@ bzl_library( "//lib:xcode_support", "//xcode:available_xcodes", "//xcode:xcode_config", + "//xcode:xcode_config_alias", "//xcode:xcode_version", "@bazel_skylib//lib:unittest", ], diff --git a/test/test_helpers.bzl b/test/test_helpers.bzl index 7917ffe..996db59 100644 --- a/test/test_helpers.bzl +++ b/test/test_helpers.bzl @@ -14,6 +14,8 @@ """Common Starlark helpers used by apple_support tests.""" +load("@bazel_skylib//lib:unittest.bzl", "analysistest") + visibility(["//test/..."]) # Common tags used for all test fixtures to ensure that they don't build unless @@ -22,6 +24,67 @@ FIXTURE_TAGS = [ "manual", ] +def assert_warning(env, msg_id, data = None): + """Asserts that a warning was printed. + + The logic in this helper is specifically meant to handle warnings printed by + `xcode_config.bzl`, because we want to preserve the behavior of the original + rules and their tests. + + Args: + env: The analysis test environment. + msg_id: The fixed identifier of the warning message. + data: The semicolon-delimited, sorted by key, `key=value` string + representation of the format arguments for the message, if any. + """ + expected = "Warning:{}".format(msg_id) + if data: + expected += ":{}".format(data) + + found_warnings = [] + + actions = analysistest.target_actions(env) + for action in actions: + mnemonic = action.mnemonic + if mnemonic == expected: + return + if mnemonic.startswith("Warning:"): + found_warnings.append(mnemonic) + + found_warnings_msg = "" + if found_warnings: + found_warnings_msg = "; the following warnings were emitted:\n{}".format( + "\n".join(found_warnings), + ) + + analysistest.fail( + env, + "Expected warning '{}' was not emitted{}".format( + expected, + found_warnings_msg, + ), + ) + +def find_action(env, mnemonic): + """Finds the first action with the given mnemonic in the target under test. + + This generates an analysis test failure if no action was found. + + Args: + env: The analysis test environment. + mnemonic: The mnemonic to find. + + Returns: + The first action matching the mnemonic, or `None` if none was found. + """ + actions = analysistest.target_actions(env) + for action in actions: + if action.mnemonic == mnemonic: + return action + + analysistest.fail(env, "No '{}' action found".format(mnemonic)) + return None + def make_unique_namer(*, prefix, index): """Returns a function used to generate unique names in a package. diff --git a/test/xcode_config_test.bzl b/test/xcode_config_test.bzl index 1aa28ec..4c4a8f4 100644 --- a/test/xcode_config_test.bzl +++ b/test/xcode_config_test.bzl @@ -16,14 +16,88 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("//xcode:available_xcodes.bzl", "available_xcodes") -load("//xcode:xcode_config.bzl", "xcode_config") +load( + "//xcode:xcode_config.bzl", + "UNAVAILABLE_XCODE_MESSAGE", + "xcode_config", +) +load("//xcode:xcode_config_alias.bzl", "xcode_config_alias") load("//xcode:xcode_version.bzl", "xcode_version") -load(":test_helpers.bzl", "FIXTURE_TAGS", "make_all_tests") +load( + "//xcode/private:providers.bzl", + "XcodeVersionPropertiesInfo", +) # buildifier: disable=bzl-visibility +load( + ":test_helpers.bzl", + "FIXTURE_TAGS", + "assert_warning", + "find_action", + "make_all_tests", +) visibility("private") # ------------------------------------------------------------------------------ +def _version_retriever_impl(ctx): + xcode_properties = ctx.attr.dep[XcodeVersionPropertiesInfo] + version = xcode_properties.xcode_version + return [config_common.FeatureFlagInfo(value = version)] + +version_retriever = rule( + implementation = _version_retriever_impl, + attrs = {"dep": attr.label()}, +) + +def _provider_grabber_impl(ctx): + return [ctx.attr._xcode_dep[apple_common.XcodeVersionConfig]] + +provider_grabber = rule( + implementation = _provider_grabber_impl, + attrs = { + "_xcode_dep": attr.label( + default = configuration_field( + fragment = "apple", + name = "xcode_config_label", + ), + ), + }, + fragments = ["apple"], +) + +def _provider_grabber_aspect_impl(_target, ctx): + return [ctx.attr._xcode_dep[apple_common.XcodeVersionConfig]] + +provider_grabber_aspect = aspect( + implementation = _provider_grabber_aspect_impl, + attrs = { + "_xcode_dep": attr.label( + default = configuration_field( + fragment = "apple", + name = "xcode_config_label", + ), + ), + }, + fragments = ["apple"], +) + +def _provider_grabber_with_aspect_impl(ctx): + return [ctx.attr.deps[0][apple_common.XcodeVersionConfig]] + +provider_grabber_with_aspect = rule( + implementation = _provider_grabber_with_aspect_impl, + attrs = { + "deps": attr.label_list( + mandatory = True, + allow_files = True, + aspects = [provider_grabber_aspect], + ), + }, + fragments = ["apple"], +) + +# ------------------------------------------------------------------------------ + def _mutual_and_explicit_xcodes_fails(namer): xcode_config( name = namer("foo"), @@ -204,6 +278,8 @@ def _accepts_flag_for_mutually_available(namer): _accepts_flag_for_mutually_available_test( name = "accepts_flag_for_mutually_available", target_under_test = "accepts_flag_for_mutually_available__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["accepts_flag_for_mutually_available"] @@ -248,6 +324,8 @@ def _prefers_flag_over_mutually_available(namer): _prefers_flag_over_mutually_available_test( name = "prefers_flag_over_mutually_available", target_under_test = "prefers_flag_over_mutually_available__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["prefers_flag_over_mutually_available"] @@ -263,6 +341,12 @@ def _prefers_flag_over_mutually_available_test_impl(ctx): asserts.true(env, "no-local" in xcode_version_info.execution_info()) asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + assert_warning( + env, + "version_not_available_locally", + "command={};local_versions=8.4;version=5.1.2".format(UNAVAILABLE_XCODE_MESSAGE), + ) + return analysistest.end(env) _prefers_flag_over_mutually_available_test = analysistest.make( @@ -292,6 +376,8 @@ def _warn_with_explicit_local_only_version(namer): _warn_with_explicit_local_only_version_test( name = "warn_with_explicit_local_only_version", target_under_test = "warn_with_explicit_local_only_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["warn_with_explicit_local_only_version"] @@ -301,16 +387,18 @@ def _warn_with_explicit_local_only_version_test_impl(ctx): target_under_test = analysistest.target_under_test(env) xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] - # TODO: b/311385128 - Once we move the rules to apple_support, hack up - # something that would let us actually test the warning messages. We can't - # test `print`. - asserts.equals(env, "8.4", str(xcode_version_info.xcode_version())) asserts.equals(env, "local", xcode_version_info.availability()) asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) asserts.true(env, "no-remote" in xcode_version_info.execution_info()) asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + assert_warning( + env, + "explicit_version_not_available_remotely", + "version=8.4", + ) + return analysistest.end(env) _warn_with_explicit_local_only_version_test = analysistest.make( @@ -340,6 +428,8 @@ def _prefer_local_default_if_no_mutual_no_flag_different_main_version(namer): _prefer_local_default_if_no_mutual_no_flag_different_main_version_test( name = "prefer_local_default_if_no_mutual_no_flag_different_main_version", target_under_test = "prefer_local_default_if_no_mutual_no_flag_different_main_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["prefer_local_default_if_no_mutual_no_flag_different_main_version"] @@ -349,16 +439,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_main_version_test_impl( target_under_test = analysistest.target_under_test(env) xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] - # TODO: b/311385128 - Once we move the rules to apple_support, hack up - # something that would let us actually test the warning messages. We can't - # test `print`. - asserts.equals(env, "8.4", str(xcode_version_info.xcode_version())) asserts.equals(env, "local", xcode_version_info.availability()) asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) asserts.true(env, "no-remote" in xcode_version_info.execution_info()) asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + assert_warning( + env, + "local_default_not_available_remotely", + "local_version=8.4;remote_versions=5.1.2", + ) + return analysistest.end(env) _prefer_local_default_if_no_mutual_no_flag_different_main_version_test = analysistest.make( @@ -387,6 +479,8 @@ def _prefer_local_default_if_no_mutual_no_flag_different_build_alias(namer): _prefer_local_default_if_no_mutual_no_flag_different_build_alias_test( name = "prefer_local_default_if_no_mutual_no_flag_different_build_alias", target_under_test = "prefer_local_default_if_no_mutual_no_flag_different_build_alias__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["prefer_local_default_if_no_mutual_no_flag_different_build_alias"] @@ -396,16 +490,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_build_alias_test_impl(c target_under_test = analysistest.target_under_test(env) xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] - # TODO: b/311385128 - Once we move the rules to apple_support, hack up - # something that would let us actually test the warning messages. We can't - # test `print`. - asserts.equals(env, "10.0.0.10C504", str(xcode_version_info.xcode_version())) asserts.equals(env, "local", xcode_version_info.availability()) asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) asserts.true(env, "no-remote" in xcode_version_info.execution_info()) asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + assert_warning( + env, + "local_default_not_available_remotely", + "local_version=10.0.0.10C504;remote_versions=10.0", + ) + return analysistest.end(env) _prefer_local_default_if_no_mutual_no_flag_different_build_alias_test = analysistest.make( @@ -434,6 +530,8 @@ def _prefer_local_default_if_no_mutual_no_flag_different_full_version(namer): _prefer_local_default_if_no_mutual_no_flag_different_full_version_test( name = "prefer_local_default_if_no_mutual_no_flag_different_full_version", target_under_test = "prefer_local_default_if_no_mutual_no_flag_different_full_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["prefer_local_default_if_no_mutual_no_flag_different_full_version"] @@ -443,16 +541,18 @@ def _prefer_local_default_if_no_mutual_no_flag_different_full_version_test_impl( target_under_test = analysistest.target_under_test(env) xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] - # TODO: b/311385128 - Once we move the rules to apple_support, hack up - # something that would let us actually test the warning messages. We can't - # test `print`. - asserts.equals(env, "10.0.0.10C504", str(xcode_version_info.xcode_version())) asserts.equals(env, "local", xcode_version_info.availability()) asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) asserts.true(env, "no-remote" in xcode_version_info.execution_info()) asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + assert_warning( + env, + "local_default_not_available_remotely", + "local_version=10.0.0.10C504;remote_versions=10.0.0.101ff", + ) + return analysistest.end(env) _prefer_local_default_if_no_mutual_no_flag_different_full_version_test = analysistest.make( @@ -485,6 +585,8 @@ def _choose_newest_mutual_xcode(namer): _choose_newest_mutual_xcode_test( name = "choose_newest_mutual_xcode", target_under_test = "choose_newest_mutual_xcode__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], ) return ["choose_newest_mutual_xcode"] @@ -494,10 +596,6 @@ def _choose_newest_mutual_xcode_test_impl(ctx): target_under_test = analysistest.target_under_test(env) xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] - # TODO: b/311385128 - Once we move the rules to apple_support, hack up - # something that would let us actually test the warning messages. We can't - # test `print`. - asserts.equals(env, "10", str(xcode_version_info.xcode_version())) asserts.equals(env, "both", xcode_version_info.availability()) asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) @@ -555,61 +653,1689 @@ _invalid_xcode_from_mutual_throws_test = analysistest.make( # ------------------------------------------------------------------------------ -def _make_xcode_fixtures( - *, - namer, - xcode_config_name, - remote_versions = [], - local_versions = []): - """Helper function to splat out fixtures used by multiple tests.""" - all_versions = {} +def _apple_common_xcode_version_config_constructor_fails_on_bad_input(namer): + _apple_common_xcode_version_config_fails_on_bad_input_rule( + name = namer("test"), + tags = FIXTURE_TAGS, + ) - remote_default_label = None - for version_info in remote_versions: - version_name = version_info.name - all_versions[version_name] = version_info - if getattr(version_info, "is_default", False): - if remote_default_label: - fail("Only one remote version may be the default") - remote_default_label = version_name + _apple_common_xcode_version_config_constructor_fails_on_bad_input_test( + name = "apple_common_xcode_version_config_constructor_fails_on_bad_input", + target_under_test = namer("test"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["apple_common_xcode_version_config_constructor_fails_on_bad_input"] + +def _apple_common_xcode_version_config_fails_on_bad_input_rule_impl(_ctx): + return [ + apple_common.XcodeVersionConfig( + ios_sdk_version = "not a valid dotted version", + ios_minimum_os_version = "1.2", + watchos_sdk_version = "1.3", + watchos_minimum_os_version = "1.4", + tvos_sdk_version = "1.5", + tvos_minimum_os_version = "1.6", + macos_sdk_version = "1.7", + macos_minimum_os_version = "1.8", + visionos_sdk_version = "1.9", + visionos_minimum_os_version = "1.10", + xcode_version = "1.11", + availability = "UNKNOWN", + xcode_version_flag = "0.0", + include_xcode_execution_info = False, + ), + ] + +_apple_common_xcode_version_config_fails_on_bad_input_rule = rule( + _apple_common_xcode_version_config_fails_on_bad_input_rule_impl, +) - local_default_label = None - for version_info in local_versions: - version_name = version_info.name - all_versions[version_name] = version_info - if getattr(version_info, "is_default", False): - if local_default_label: - fail("Only one local version may be the default") - local_default_label = version_name +def _apple_common_xcode_version_config_constructor_fails_on_bad_input_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Dotted version components must all start with the form") + asserts.expect_failure(env, "got 'not a valid dotted version'") + return analysistest.end(env) - for version_name, version in all_versions.items(): - xcode_version( - name = namer(version.name), - version = version.version, - aliases = getattr(version, "aliases", []), - tags = FIXTURE_TAGS, - ) +_apple_common_xcode_version_config_constructor_fails_on_bad_input_test = analysistest.make( + _apple_common_xcode_version_config_constructor_fails_on_bad_input_test_impl, + expect_failure = True, +) - if local_versions or remote_versions: - if local_versions: - available_xcodes( - name = namer("local"), - default = namer(local_default_label), - versions = [namer(version.name) for version in local_versions], - tags = FIXTURE_TAGS, - ) - if remote_versions: - available_xcodes( - name = namer("remote"), - default = namer(remote_default_label), - versions = [namer(version.name) for version in remote_versions], - tags = FIXTURE_TAGS, - ) - xcode_config( - name = xcode_config_name, - local_versions = namer("local"), - remote_versions = namer("remote"), - ) +# ------------------------------------------------------------------------------ + +def _apple_common_xcode_version_config_constructor(namer): + _apple_common_xcode_version_config_rule( + name = namer("test"), + tags = FIXTURE_TAGS, + ) + + _apple_common_xcode_version_config_constructor_test( + name = "apple_common_xcode_version_config_constructor", + target_under_test = namer("test"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["apple_common_xcode_version_config_constructor"] + +def _apple_common_xcode_version_config_rule_impl(_ctx): + return [ + apple_common.XcodeVersionConfig( + ios_sdk_version = "1.1", + ios_minimum_os_version = "1.2", + watchos_sdk_version = "1.3", + watchos_minimum_os_version = "1.4", + tvos_sdk_version = "1.5", + tvos_minimum_os_version = "1.6", + macos_sdk_version = "1.7", + macos_minimum_os_version = "1.8", + visionos_sdk_version = "1.9", + visionos_minimum_os_version = "1.10", + xcode_version = "1.11", + availability = "UNKNOWN", + xcode_version_flag = "0.0", + include_xcode_execution_info = False, + ), + ] + +_apple_common_xcode_version_config_rule = rule( + _apple_common_xcode_version_config_rule_impl, +) + +def _apple_common_xcode_version_config_constructor_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "1.1", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_device))) + asserts.equals(env, "1.1", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_simulator))) + asserts.equals(env, "1.2", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.ios))) + asserts.equals(env, "1.2", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.catalyst))) + asserts.equals(env, "1.3", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.watchos_device))) + asserts.equals(env, "1.3", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.watchos_simulator))) + asserts.equals(env, "1.4", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.watchos))) + asserts.equals(env, "1.5", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.tvos_device))) + asserts.equals(env, "1.5", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.tvos_simulator))) + asserts.equals(env, "1.6", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.tvos))) + asserts.equals(env, "1.7", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.macos))) + asserts.equals(env, "1.7", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.catalyst))) + asserts.equals(env, "1.8", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.macos))) + asserts.equals(env, "1.9", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.visionos_device))) + asserts.equals(env, "1.9", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.visionos_simulator))) + asserts.equals(env, "1.10", str(xcode_version_info.minimum_os_for_platform_type(apple_common.platform_type.visionos))) + asserts.equals(env, "1.11", str(xcode_version_info.xcode_version())) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.equals(env, { + "requires-darwin": "", + "supports-xcode-requirements-set": "", + }, xcode_version_info.execution_info()) + + return analysistest.end(env) + +_apple_common_xcode_version_config_constructor_test = analysistest.make( + _apple_common_xcode_version_config_constructor_test_impl, +) + +# ------------------------------------------------------------------------------ + +def _config_alias_config_setting(namer): + version_retriever( + name = namer("flag_propagator"), + dep = namer(":alias"), + tags = FIXTURE_TAGS, + ) + + xcode_config( + name = "config_alias_config_setting__config", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + namer(":version12"), + ], + tags = FIXTURE_TAGS, + ) + + xcode_config_alias( + name = namer("alias"), + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "six", + "6", + ], + version = "6.4", + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version12"), + version = "12", + tags = FIXTURE_TAGS, + ) + + native.config_setting( + name = namer("xcode_5_1_2"), + flag_values = {namer(":flag_propagator"): "5.1.2"}, + ) + + native.config_setting( + name = namer("xcode_6_4"), + flag_values = {namer(":flag_propagator"): "6.4"}, + ) + + native.genrule( + name = namer("gen"), + srcs = [], + outs = [namer("out")], + cmd = select({ + namer(":xcode_5_1_2"): "5.1.2", + namer(":xcode_6_4"): "6.4", + "//conditions:default": "none", + }), + tags = FIXTURE_TAGS, + ) + + _config_alias_config_setting_no_flag_test( + name = "config_alias_config_setting_no_flag", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _config_alias_config_setting_6_4_test( + name = "config_alias_config_setting_6_4", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _config_alias_config_setting_6_test( + name = "config_alias_config_setting_6", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _config_alias_config_setting_12_test( + name = "config_alias_config_setting_12", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return [ + "config_alias_config_setting_no_flag", + "config_alias_config_setting_6_4", + "config_alias_config_setting_6", + "config_alias_config_setting_12", + ] + +def _config_alias_config_setting_no_flag_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.true(env, find_action(env, "Genrule").argv[-1].endswith("5.1.2")) + return analysistest.end(env) + +_config_alias_config_setting_no_flag_test = analysistest.make( + _config_alias_config_setting_no_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:config_alias_config_setting__config", + )), + }, +) + +def _config_alias_config_setting_6_4_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.true(env, find_action(env, "Genrule").argv[-1].endswith("6.4")) + return analysistest.end(env) + +_config_alias_config_setting_6_4_test = analysistest.make( + _config_alias_config_setting_6_4_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:config_alias_config_setting__config", + )), + "//command_line_option:xcode_version": "6.4", + }, +) + +_config_alias_config_setting_6_test = analysistest.make( + _config_alias_config_setting_6_4_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:config_alias_config_setting__config", + )), + "//command_line_option:xcode_version": "6", + }, +) + +def _config_alias_config_setting_12_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.true(env, find_action(env, "Genrule").argv[-1].endswith("none")) + return analysistest.end(env) + +_config_alias_config_setting_12_test = analysistest.make( + _config_alias_config_setting_12_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:config_alias_config_setting__config", + )), + "//command_line_option:xcode_version": "12", + }, +) + +# ------------------------------------------------------------------------------ + +def _default_version_config_setting(namer): + version_retriever( + name = namer("flag_propagator"), + dep = namer(":alias"), + tags = FIXTURE_TAGS, + ) + + xcode_config_alias( + name = namer("alias"), + tags = FIXTURE_TAGS, + ) + + xcode_config( + name = "default_version_config_setting__foo", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + ], + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "foo", + "6", + ], + version = "6.4", + tags = FIXTURE_TAGS, + ) + + native.config_setting( + name = namer("xcode_5_1_2"), + flag_values = {namer(":flag_propagator"): "5.1.2"}, + ) + + native.config_setting( + name = namer("xcode_6_4"), + flag_values = {namer(":flag_propagator"): "6.4"}, + ) + + native.genrule( + name = namer("gen"), + srcs = [], + outs = [namer("out")], + cmd = select({ + namer(":xcode_5_1_2"): "5.1.2", + namer(":xcode_6_4"): "6.4", + "//conditions:default": "none", + }), + tags = FIXTURE_TAGS, + ) + + _default_version_config_setting_no_flag_test( + name = "default_version_config_setting_no_flag", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _default_version_config_setting_6_4_test( + name = "default_version_config_setting_6_4", + target_under_test = namer("gen"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return [ + "default_version_config_setting_no_flag", + "default_version_config_setting_6_4", + ] + +def _default_version_config_setting_no_flag_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.true(env, find_action(env, "Genrule").argv[-1].endswith("5.1.2")) + return analysistest.end(env) + +_default_version_config_setting_no_flag_test = analysistest.make( + _default_version_config_setting_no_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_version_config_setting__foo", + )), + }, +) + +def _default_version_config_setting_6_4_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.true(env, find_action(env, "Genrule").argv[-1].endswith("6.4")) + return analysistest.end(env) + +_default_version_config_setting_6_4_test = analysistest.make( + _default_version_config_setting_6_4_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_version_config_setting__foo", + )), + "//command_line_option:xcode_version": "6.4", + }, +) + +# ------------------------------------------------------------------------------ + +def _valid_version(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "valid_version__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True), + ], + ) + + _valid_version_test( + name = "valid_version", + target_under_test = "valid_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["valid_version"] + +def _valid_version_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_valid_version_test = analysistest.make( + _valid_version_test_impl, + config_settings = { + "//command_line_option:xcode_version": "5.1.2", + "//command_line_option:xcode_version_config": str(Label( + "//test:valid_version__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _valid_alias_dotted_version(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "valid_alias_dotted_version__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5"]), + ], + ) + + _valid_alias_dotted_version_test( + name = "valid_alias_dotted_version", + target_under_test = "valid_alias_dotted_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["valid_alias_dotted_version"] + +def _valid_alias_dotted_version_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_valid_alias_dotted_version_test = analysistest.make( + _valid_alias_dotted_version_test_impl, + config_settings = { + "//command_line_option:xcode_version": "5", + "//command_line_option:xcode_version_config": str(Label( + "//test:valid_alias_dotted_version__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _valid_alias_nonnumerical(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "valid_alias_nonnumerical__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["valid_version"]), + ], + ) + + _valid_alias_nonnumerical_test( + name = "valid_alias_nonnumerical", + target_under_test = "valid_alias_nonnumerical__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["valid_alias_nonnumerical"] + +def _valid_alias_nonnumerical_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_valid_alias_nonnumerical_test = analysistest.make( + _valid_alias_nonnumerical_test_impl, + config_settings = { + "//command_line_option:xcode_version": "valid_version", + "//command_line_option:xcode_version_config": str(Label( + "//test:valid_alias_nonnumerical__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _invalid_xcode_specified(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "invalid_xcode_specified__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True), + struct(name = "version84", version = "8.4"), + ], + ) + + _invalid_xcode_specified_test( + name = "invalid_xcode_specified", + target_under_test = "invalid_xcode_specified__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["invalid_xcode_specified"] + +def _invalid_xcode_specified_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "--xcode_version=6 specified, but '6' is not an available Xcode version. If you believe you have '6' installed") + return analysistest.end(env) + +_invalid_xcode_specified_test = analysistest.make( + _invalid_xcode_specified_test_impl, + config_settings = { + "//command_line_option:xcode_version": "6", + "//command_line_option:xcode_version_config": str(Label( + "//test:invalid_xcode_specified__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _requires_default(namer): + xcode_config( + name = "requires_default__foo", + versions = [namer(":version512")], + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + version = "5.1.2", + ) + + _requires_default_test( + name = "requires_default", + target_under_test = "requires_default__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["requires_default"] + +def _requires_default_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "default version must be specified") + return analysistest.end(env) + +_requires_default_test = analysistest.make( + _requires_default_test_impl, + config_settings = { + "//command_line_option:xcode_version": "6", + "//command_line_option:xcode_version_config": str(Label( + "//test:requires_default__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _duplicate_aliases_defined_version(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "duplicate_aliases_defined_version__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5"]), + struct(name = "version5", version = "5.0", aliases = ["5"]), + ], + ) + + _duplicate_aliases_defined_version_test( + name = "duplicate_aliases_defined_version", + target_under_test = "duplicate_aliases_defined_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["duplicate_aliases_defined_version"] + +def _duplicate_aliases_defined_version_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "'5' is registered to multiple labels") + asserts.expect_failure(env, "__version512") + asserts.expect_failure(env, "__version5") + return analysistest.end(env) + +_duplicate_aliases_defined_version_test = analysistest.make( + _duplicate_aliases_defined_version_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:duplicate_aliases_defined_version__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _duplicate_aliases_within_available_xcodes(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "duplicate_aliases_within_available_xcodes__foo", + remote_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5"]), + struct(name = "version5", version = "5.0", aliases = ["5"]), + ], + local_versions = [ + struct(name = "version5", version = "5.0", is_default = True, aliases = ["5"]), + ], + ) + + _duplicate_aliases_within_available_xcodes_test( + name = "duplicate_aliases_within_available_xcodes", + target_under_test = "duplicate_aliases_within_available_xcodes__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["duplicate_aliases_within_available_xcodes"] + +def _duplicate_aliases_within_available_xcodes_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "'5' is registered to multiple labels") + asserts.expect_failure(env, "__version512") + asserts.expect_failure(env, "__version5") + return analysistest.end(env) + +_duplicate_aliases_within_available_xcodes_test = analysistest.make( + _duplicate_aliases_within_available_xcodes_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:duplicate_aliases_within_available_xcodes__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _version_aliased_to_itself(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "version_aliased_to_itself__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5.1.2"]), + ], + ) + + _version_aliased_to_itself_test( + name = "version_aliased_to_itself", + target_under_test = "version_aliased_to_itself__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["version_aliased_to_itself"] + +def _version_aliased_to_itself_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_version_aliased_to_itself_test = analysistest.make( + _version_aliased_to_itself_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:version_aliased_to_itself__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _duplicate_version_numbers(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "duplicate_version_numbers__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True), + struct(name = "version5", version = "5.1.2", aliases = ["5"]), + ], + ) + + _duplicate_version_numbers_test( + name = "duplicate_version_numbers", + target_under_test = "duplicate_version_numbers__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["duplicate_version_numbers"] + +def _duplicate_version_numbers_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "'5.1.2' is registered to multiple labels") + asserts.expect_failure(env, "__version512") + asserts.expect_failure(env, "__version5") + return analysistest.end(env) + +_duplicate_version_numbers_test = analysistest.make( + _duplicate_version_numbers_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:duplicate_version_numbers__foo", + )), + "//command_line_option:xcode_version": "5", + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _version_conflicts_with_alias(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "version_conflicts_with_alias__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True), + struct(name = "version5", version = "5.0", aliases = ["5.1.2"]), + ], + ) + + _version_conflicts_with_alias_test( + name = "version_conflicts_with_alias", + target_under_test = "version_conflicts_with_alias__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["version_conflicts_with_alias"] + +def _version_conflicts_with_alias_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "'5.1.2' is registered to multiple labels") + asserts.expect_failure(env, "__version512") + asserts.expect_failure(env, "__version5") + return analysistest.end(env) + +_version_conflicts_with_alias_test = analysistest.make( + _version_conflicts_with_alias_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:version_conflicts_with_alias__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _default_ios_sdk_version(namer): + xcode_config( + name = "default_ios_sdk_version__foo", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + ], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + default_ios_sdk_version = "7.1", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "foo", + "6", + ], + default_ios_sdk_version = "43.0", + version = "6.4", + tags = FIXTURE_TAGS, + ) + + _default_ios_sdk_version_test( + name = "default_ios_sdk_version", + target_under_test = "default_ios_sdk_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["default_ios_sdk_version"] + +def _default_ios_sdk_version_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "7.1", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_simulator))) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_default_ios_sdk_version_test = analysistest.make( + _default_ios_sdk_version_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_ios_sdk_version__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _default_sdk_versions(namer): + xcode_config( + name = "default_sdk_versions__foo", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + ], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + default_ios_sdk_version = "101", + default_macos_sdk_version = "104", + default_tvos_sdk_version = "103", + default_visionos_sdk_version = "105", + default_watchos_sdk_version = "102", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "foo", + "6", + ], + default_ios_sdk_version = "43.0", + version = "6.4", + tags = FIXTURE_TAGS, + ) + + _default_sdk_versions_test( + name = "default_sdk_versions", + target_under_test = "default_sdk_versions__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["default_sdk_versions"] + +def _default_sdk_versions_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "101", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_simulator))) + asserts.equals(env, "102", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.watchos_simulator))) + asserts.equals(env, "103", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.tvos_simulator))) + asserts.equals(env, "104", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.macos))) + asserts.equals(env, "105", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.visionos_simulator))) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_default_sdk_versions_test = analysistest.make( + _default_sdk_versions_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_sdk_versions__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _default_sdk_versions_selected_xcode(namer): + xcode_config( + name = "default_sdk_versions_selected_xcode__foo", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + ], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + default_ios_sdk_version = "7.1", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "foo", + "6", + ], + default_ios_sdk_version = "43", + default_macos_sdk_version = "46", + default_tvos_sdk_version = "45", + default_visionos_sdk_version = "47", + default_watchos_sdk_version = "44", + version = "6.4", + tags = FIXTURE_TAGS, + ) + + _default_sdk_versions_selected_xcode_test( + name = "default_sdk_versions_selected_xcode", + target_under_test = "default_sdk_versions_selected_xcode__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["default_sdk_versions_selected_xcode"] + +def _default_sdk_versions_selected_xcode_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "6.4", str(xcode_version_info.xcode_version())) + asserts.equals(env, "43", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_simulator))) + asserts.equals(env, "44", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.watchos_simulator))) + asserts.equals(env, "45", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.tvos_simulator))) + asserts.equals(env, "46", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.macos))) + asserts.equals(env, "47", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.visionos_simulator))) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_default_sdk_versions_selected_xcode_test = analysistest.make( + _default_sdk_versions_selected_xcode_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_sdk_versions_selected_xcode__foo", + )), + "//command_line_option:xcode_version": "6", + }, +) + +# ------------------------------------------------------------------------------ + +def _override_default_sdk_versions(namer): + xcode_config( + name = "override_default_sdk_versions__foo", + default = namer(":version512"), + versions = [ + namer(":version512"), + namer(":version64"), + ], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + ], + default_ios_sdk_version = "7.1", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version64"), + aliases = [ + "6.0", + "foo", + "6", + ], + default_ios_sdk_version = "101", + default_macos_sdk_version = "104", + default_tvos_sdk_version = "103", + default_visionos_sdk_version = "105", + default_watchos_sdk_version = "102", + version = "6.4", + tags = FIXTURE_TAGS, + ) + + _override_default_sdk_versions_test( + name = "override_default_sdk_versions", + target_under_test = "override_default_sdk_versions__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["override_default_sdk_versions"] + +def _override_default_sdk_versions_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "6.4", str(xcode_version_info.xcode_version())) + asserts.equals(env, "15.3", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.ios_simulator))) + asserts.equals(env, "15.4", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.watchos_simulator))) + asserts.equals(env, "15.5", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.tvos_simulator))) + asserts.equals(env, "15.6", str(xcode_version_info.sdk_version_for_platform(apple_common.platform.macos))) + asserts.equals(env, "unknown", xcode_version_info.availability()) + asserts.true(env, "requires-darwin" in xcode_version_info.execution_info()) + asserts.true(env, "supports-xcode-requirements-set" in xcode_version_info.execution_info()) + + return analysistest.end(env) + +_override_default_sdk_versions_test = analysistest.make( + _override_default_sdk_versions_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:override_default_sdk_versions__foo", + )), + "//command_line_option:xcode_version": "6", + "//command_line_option:ios_sdk_version": "15.3", + "//command_line_option:watchos_sdk_version": "15.4", + "//command_line_option:tvos_sdk_version": "15.5", + "//command_line_option:macos_sdk_version": "15.6", + }, +) + +# ------------------------------------------------------------------------------ + +def _default_without_version(namer): + xcode_config( + name = "default_without_version__foo", + default = namer(":version512"), + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + "5.1.2", + ], + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + _default_without_version_test( + name = "default_without_version", + target_under_test = "default_without_version__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["default_without_version"] + +def _default_without_version_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "default label") + asserts.expect_failure(env, "must be contained in versions attribute") + return analysistest.end(env) + +_default_without_version_test = analysistest.make( + _default_without_version_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:default_without_version__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _version_does_not_contain_default(namer): + xcode_config( + name = "version_does_not_contain_default__foo", + default = namer(":version512"), + versions = [namer(":version6")], + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version512"), + aliases = [ + "5", + "5.1", + "5.1.2", + ], + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version6"), + version = "6.0", + tags = FIXTURE_TAGS, + ) + + _version_does_not_contain_default_test( + name = "version_does_not_contain_default", + target_under_test = "version_does_not_contain_default__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["version_does_not_contain_default"] + +def _version_does_not_contain_default_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "must be contained in versions attribute") + return analysistest.end(env) + +_version_does_not_contain_default_test = analysistest.make( + _version_does_not_contain_default_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:version_does_not_contain_default__foo", + )), + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _configuration_field_for_rule(namer): + # Verifies that the `--xcode_version_config` configuration value can be + # accessed via the `configuration_field()`. + xcode_config( + name = "configuration_field_for_rule__config1", + default = namer(":version1"), + versions = [namer(":version1")], + tags = FIXTURE_TAGS, + ) + xcode_config( + name = "configuration_field_for_rule__config2", + default = namer(":version2"), + versions = [namer(":version2")], + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version1"), + version = "1.0", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version2"), + version = "2.0", + tags = FIXTURE_TAGS, + ) + + provider_grabber( + name = namer("provider_grabber"), + tags = FIXTURE_TAGS, + ) + + _configuration_field_for_rule_1_test( + name = "configuration_field_for_rule_1", + target_under_test = namer("provider_grabber"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _configuration_field_for_rule_2_test( + name = "configuration_field_for_rule_2", + target_under_test = namer("provider_grabber"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return [ + "configuration_field_for_rule_1", + "configuration_field_for_rule_2", + ] + +def _configuration_field_for_rule_1_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "1.0", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_configuration_field_for_rule_1_test = analysistest.make( + _configuration_field_for_rule_1_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:configuration_field_for_rule__config1", + )), + }, +) + +def _configuration_field_for_rule_2_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "2.0", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_configuration_field_for_rule_2_test = analysistest.make( + _configuration_field_for_rule_2_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:configuration_field_for_rule__config2", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _configuration_field_for_aspect(namer): + # Verifies that the `--xcode_version_config` configuration value can be + # accessed via the `configuration_field()`. + xcode_config( + name = "configuration_field_for_aspect__config1", + default = namer(":version1"), + versions = [namer(":version1")], + tags = FIXTURE_TAGS, + ) + xcode_config( + name = "configuration_field_for_aspect__config2", + default = namer(":version2"), + versions = [namer(":version2")], + tags = FIXTURE_TAGS, + ) + + xcode_version( + name = namer("version1"), + version = "1.0", + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version2"), + version = "2.0", + tags = FIXTURE_TAGS, + ) + + native.filegroup( + name = namer("dummy"), + tags = FIXTURE_TAGS, + ) + provider_grabber_with_aspect( + name = namer("provider_grabber"), + deps = [namer("dummy")], + tags = FIXTURE_TAGS, + ) + + _configuration_field_for_aspect_1_test( + name = "configuration_field_for_aspect_1", + target_under_test = namer("provider_grabber"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + _configuration_field_for_aspect_2_test( + name = "configuration_field_for_aspect_2", + target_under_test = namer("provider_grabber"), + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return [ + "configuration_field_for_aspect_1", + "configuration_field_for_aspect_2", + ] + +def _configuration_field_for_aspect_1_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "1.0", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_configuration_field_for_aspect_1_test = analysistest.make( + _configuration_field_for_aspect_1_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:configuration_field_for_aspect__config1", + )), + }, +) + +def _configuration_field_for_aspect_2_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "2.0", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_configuration_field_for_aspect_2_test = analysistest.make( + _configuration_field_for_aspect_2_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:configuration_field_for_aspect__config2", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _explicit_xcodes_mode_no_flag(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "explicit_xcodes_mode_no_flag__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5", "5.1"]), + struct(name = "version64", version = "6.4", aliases = ["6.0", "foo", "6"]), + ], + ) + + _explicit_xcodes_mode_no_flag_test( + name = "explicit_xcodes_mode_no_flag", + target_under_test = "explicit_xcodes_mode_no_flag__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["explicit_xcodes_mode_no_flag"] + +def _explicit_xcodes_mode_no_flag_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_explicit_xcodes_mode_no_flag_test = analysistest.make( + _explicit_xcodes_mode_no_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:explicit_xcodes_mode_no_flag__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _explicit_xcodes_mode_with_flag(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "explicit_xcodes_mode_with_flag__foo", + explicit_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5", "5.1"]), + struct(name = "version64", version = "6.4", aliases = ["6.0", "foo", "6"]), + ], + ) + + _explicit_xcodes_mode_with_flag_test( + name = "explicit_xcodes_mode_with_flag", + target_under_test = "explicit_xcodes_mode_with_flag__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["explicit_xcodes_mode_with_flag"] + +def _explicit_xcodes_mode_with_flag_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "6.4", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_explicit_xcodes_mode_with_flag_test = analysistest.make( + _explicit_xcodes_mode_with_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:explicit_xcodes_mode_with_flag__foo", + )), + "//command_line_option:xcode_version": "6.4", + }, +) + +# ------------------------------------------------------------------------------ + +def _available_xcodes_mode_no_flag(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "available_xcodes_mode_no_flag__foo", + remote_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5", "5.1"]), + struct(name = "version84", version = "8.4"), + ], + local_versions = [ + struct(name = "version84", version = "8.4", is_default = True), + ], + ) + + _available_xcodes_mode_no_flag_test( + name = "available_xcodes_mode_no_flag", + target_under_test = "available_xcodes_mode_no_flag__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["available_xcodes_mode_no_flag"] + +def _available_xcodes_mode_no_flag_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "8.4", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_available_xcodes_mode_no_flag_test = analysistest.make( + _available_xcodes_mode_no_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:available_xcodes_mode_no_flag__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + +def _available_xcodes_mode_different_alias(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "available_xcodes_mode_different_alias__foo", + remote_versions = [ + struct(name = "version5", version = "5.1", is_default = True, aliases = ["5"]), + ], + local_versions = [ + struct(name = "version5.1.2", version = "5.1.2", is_default = True, aliases = ["5"]), + ], + ) + + _available_xcodes_mode_different_alias_test( + name = "available_xcodes_mode_different_alias", + target_under_test = "available_xcodes_mode_different_alias__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["available_xcodes_mode_different_alias"] + +def _available_xcodes_mode_different_alias_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Xcode version 5 was selected") + asserts.expect_failure(env, "This corresponds to local Xcode version 5.1.2") + return analysistest.end(env) + +_available_xcodes_mode_different_alias_test = analysistest.make( + _available_xcodes_mode_different_alias_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:available_xcodes_mode_different_alias__foo", + )), + "//command_line_option:xcode_version": "5", + }, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + +def _available_xcodes_mode_different_alias_fully_specified(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "available_xcodes_mode_different_alias_fully_specified__foo", + remote_versions = [ + struct(name = "version5", version = "5.1", is_default = True, aliases = ["5"]), + ], + local_versions = [ + struct(name = "version5.1.2", version = "5.1.2", is_default = True, aliases = ["5"]), + ], + ) + + _available_xcodes_mode_different_alias_fully_specified_test( + name = "available_xcodes_mode_different_alias_fully_specified", + target_under_test = "available_xcodes_mode_different_alias_fully_specified__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["available_xcodes_mode_different_alias_fully_specified"] + +def _available_xcodes_mode_different_alias_fully_specified_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + asserts.equals(env, "local", xcode_version_info.availability()) + + return analysistest.end(env) + +_available_xcodes_mode_different_alias_fully_specified_test = analysistest.make( + _available_xcodes_mode_different_alias_fully_specified_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:available_xcodes_mode_different_alias_fully_specified__foo", + )), + "//command_line_option:xcode_version": "5.1.2", + }, +) + +# ------------------------------------------------------------------------------ + +def _available_xcodes_mode_with_flag(namer): + _make_xcode_fixtures( + namer = namer, + xcode_config_name = "available_xcodes_mode_with_flag__foo", + remote_versions = [ + struct(name = "version512", version = "5.1.2", is_default = True, aliases = ["5", "5.1"]), + struct(name = "version84", version = "8.4"), + ], + local_versions = [ + struct(name = "version84", version = "8.4", is_default = True), + ], + ) + + _available_xcodes_mode_with_flag_test( + name = "available_xcodes_mode_with_flag", + target_under_test = "available_xcodes_mode_with_flag__foo", + # TODO: Remove once we test with Bazel 8+ + tags = ["manual"], + ) + return ["available_xcodes_mode_with_flag"] + +def _available_xcodes_mode_with_flag_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_version_info = target_under_test[apple_common.XcodeVersionConfig] + + asserts.equals(env, "5.1.2", str(xcode_version_info.xcode_version())) + + return analysistest.end(env) + +_available_xcodes_mode_with_flag_test = analysistest.make( + _available_xcodes_mode_with_flag_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:available_xcodes_mode_with_flag__foo", + )), + "//command_line_option:xcode_version": "5.1.2", + }, +) + +# ------------------------------------------------------------------------------ + +def _make_xcode_fixtures( + *, + namer, + xcode_config_name, + remote_versions = [], + local_versions = [], + explicit_versions = []): + """Helper function to splat out fixtures used by multiple tests.""" + all_versions = {} + + remote_default_label = None + for version_info in remote_versions: + version_name = version_info.name + all_versions[version_name] = version_info + if getattr(version_info, "is_default", False): + if remote_default_label: + fail("Only one remote version may be the default") + remote_default_label = version_name + + local_default_label = None + for version_info in local_versions: + version_name = version_info.name + all_versions[version_name] = version_info + if getattr(version_info, "is_default", False): + if local_default_label: + fail("Only one local version may be the default") + local_default_label = version_name + + explicit_default_label = None + for version_info in explicit_versions: + version_name = version_info.name + all_versions[version_name] = version_info + if getattr(version_info, "is_default", False): + if explicit_default_label: + fail("Only one explicit version may be the default") + explicit_default_label = version_name + + for version_name, version in all_versions.items(): + xcode_version( + name = namer(version.name), + version = version.version, + aliases = getattr(version, "aliases", []), + tags = FIXTURE_TAGS, + ) + + if local_versions or remote_versions: + if local_versions: + available_xcodes( + name = namer("local"), + default = namer(local_default_label), + versions = [namer(version.name) for version in local_versions], + tags = FIXTURE_TAGS, + ) + if remote_versions: + available_xcodes( + name = namer("remote"), + default = namer(remote_default_label), + versions = [namer(version.name) for version in remote_versions], + tags = FIXTURE_TAGS, + ) + xcode_config( + name = xcode_config_name, + local_versions = namer("local"), + remote_versions = namer("remote"), + tags = FIXTURE_TAGS, + ) + + if explicit_versions: + xcode_config( + name = xcode_config_name, + default = namer(explicit_default_label), + versions = [namer(version.name) for version in explicit_versions], + tags = FIXTURE_TAGS, + ) + +# ------------------------------------------------------------------------------ def xcode_config_test(name): make_all_tests( @@ -626,6 +2352,34 @@ def xcode_config_test(name): _prefer_local_default_if_no_mutual_no_flag_different_full_version, _choose_newest_mutual_xcode, _invalid_xcode_from_mutual_throws, + _apple_common_xcode_version_config_constructor, + _apple_common_xcode_version_config_constructor_fails_on_bad_input, + _config_alias_config_setting, + _default_version_config_setting, + _valid_version, + _valid_alias_dotted_version, + _valid_alias_nonnumerical, + _invalid_xcode_specified, + _requires_default, + _duplicate_aliases_defined_version, + _duplicate_aliases_within_available_xcodes, + _version_aliased_to_itself, + _duplicate_version_numbers, + _version_conflicts_with_alias, + _default_ios_sdk_version, + _default_sdk_versions, + _default_sdk_versions_selected_xcode, + _override_default_sdk_versions, + _default_without_version, + _version_does_not_contain_default, + _configuration_field_for_rule, + _configuration_field_for_aspect, + _explicit_xcodes_mode_no_flag, + _explicit_xcodes_mode_with_flag, + _available_xcodes_mode_no_flag, + _available_xcodes_mode_different_alias, + _available_xcodes_mode_different_alias_fully_specified, + _available_xcodes_mode_with_flag, ], # TODO: Remove once we test with Bazel 8+ tags = ["manual"], @@ -639,3 +2393,6 @@ def xcode_config_test(name): # * testPreferMutualXcodeFalseOverridesMutual # * testLocalDefaultCanBeMutuallyAvailable # * testPreferLocalDefaultOverDifferentBuild + # * testXcodeWithExtensionMatchingRemote + # * testXcodeVersionWithExtensionMatchingRemoteAndLocal + # * testXcodeVersionWithNoExtension diff --git a/test/xcode_version_test.bzl b/test/xcode_version_test.bzl index 1518a05..b97289d 100644 --- a/test/xcode_version_test.bzl +++ b/test/xcode_version_test.bzl @@ -16,6 +16,10 @@ load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("//xcode:xcode_version.bzl", "xcode_version") +load( + "//xcode/private:providers.bzl", + "XcodeVersionPropertiesInfo", +) # buildifier: disable=bzl-visibility load(":test_helpers.bzl", "FIXTURE_TAGS", "make_all_tests") visibility("private") @@ -44,7 +48,7 @@ def _read_version_from_provider_test_impl(ctx): env = analysistest.begin(ctx) target_under_test = analysistest.target_under_test(env) - xcode_properties = target_under_test[apple_common.XcodeProperties] + xcode_properties = target_under_test[XcodeVersionPropertiesInfo] asserts.equals(env, "8", xcode_properties.xcode_version) asserts.equals(env, "9.0", xcode_properties.default_ios_sdk_version) diff --git a/xcode/BUILD b/xcode/BUILD index 6d856d1..788fea1 100644 --- a/xcode/BUILD +++ b/xcode/BUILD @@ -9,6 +9,7 @@ package( bzl_library( name = "available_xcodes", srcs = ["available_xcodes.bzl"], + deps = ["//xcode/private:providers"], ) bzl_library( @@ -19,22 +20,27 @@ bzl_library( bzl_library( name = "xcode_config", srcs = ["xcode_config.bzl"], + deps = ["//xcode/private:providers"], ) bzl_library( name = "xcode_config_alias", srcs = ["xcode_config_alias.bzl"], + deps = ["//xcode/private:providers"], ) bzl_library( name = "xcode_version", srcs = ["xcode_version.bzl"], + deps = ["//xcode/private:providers"], ) # Consumed by bazel tests. filegroup( name = "for_bazel_tests", testonly = True, - srcs = glob(["**"]), + srcs = glob(["**"]) + [ + "//xcode/private:for_bazel_tests", + ], visibility = ["//:__pkg__"], ) diff --git a/xcode/available_xcodes.bzl b/xcode/available_xcodes.bzl index ec0adfd..18e8314 100644 --- a/xcode/available_xcodes.bzl +++ b/xcode/available_xcodes.bzl @@ -14,11 +14,45 @@ """Implementation of the `available_xcodes` build rule.""" +load( + "@build_bazel_apple_support//xcode/private:providers.bzl", + "AvailableXcodesInfo", + "XcodeVersionRuleInfo", +) + visibility("public") -def available_xcodes(name, **kwargs): - # TODO: b/311385128 - Migrate the native implementation here. - native.available_xcodes( - name = name, - **kwargs - ) +def _available_xcodes_impl(ctx): + available_versions = [ + target[XcodeVersionRuleInfo] + for target in ctx.attr.versions + ] + default_version = ctx.attr.default[XcodeVersionRuleInfo] + + return [ + AvailableXcodesInfo( + available_versions = available_versions, + default_version = default_version, + ), + ] + +available_xcodes = rule( + attrs = { + "default": attr.label( + doc = "The default Xcode version for this platform.", + mandatory = True, + providers = [[XcodeVersionRuleInfo]], + ), + "versions": attr.label_list( + doc = "The Xcode versions that are available on this platform.", + providers = [[XcodeVersionRuleInfo]], + ), + }, + doc = """\ +Two targets of this rule can be depended on by an `xcode_config` rule instance +to indicate the remotely and locally available Xcode versions. This allows +selection of an official Xcode version from the collectively available Xcodes. + """, + implementation = _available_xcodes_impl, + provides = [AvailableXcodesInfo], +) diff --git a/xcode/private/BUILD b/xcode/private/BUILD new file mode 100644 index 0000000..11160cc --- /dev/null +++ b/xcode/private/BUILD @@ -0,0 +1,20 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +licenses(["notice"]) + +package( + default_visibility = ["//xcode:__subpackages__"], +) + +bzl_library( + name = "providers", + srcs = ["providers.bzl"], +) + +# Consumed by bazel tests. +filegroup( + name = "for_bazel_tests", + testonly = True, + srcs = glob(["**"]), + visibility = ["//xcode:__pkg__"], +) diff --git a/xcode/private/providers.bzl b/xcode/private/providers.bzl new file mode 100644 index 0000000..6e2339b --- /dev/null +++ b/xcode/private/providers.bzl @@ -0,0 +1,110 @@ +# Copyright 2024 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. + +"""Providers used internally by the Xcode rules not meant for client use.""" + +visibility([ + "//test/...", + "//xcode/...", +]) + +AvailableXcodesInfo = provider( + doc = """\ +The available Xcode versions computed from the `available_xcodes` rule. +""", + fields = { + "available_versions": """\ +The available Xcode versions from `available_xcodes`. +""", + "default_version": """\ +The default Xcode version from `available_xcodes`. +""", + }, +) + +def _xcode_version_properties_info_init( + *, + xcode_version, + default_ios_sdk_version = "8.4", + default_macos_sdk_version = "10.11", + default_tvos_sdk_version = "9.0", + default_watchos_sdk_version = "2.0", + default_visionos_sdk_version = "1.0"): + # Ensure that all fields get default values if they weren't specified. + return { + "xcode_version": xcode_version, + "default_ios_sdk_version": default_ios_sdk_version, + "default_macos_sdk_version": default_macos_sdk_version, + "default_tvos_sdk_version": default_tvos_sdk_version, + "default_watchos_sdk_version": default_watchos_sdk_version, + "default_visionos_sdk_version": default_visionos_sdk_version, + } + +XcodeVersionPropertiesInfo, _new_xcode_version_properties_info = provider( + doc = """\ +Information about a specific Xcode version, such as its default SDK versions. +""", + fields = { + "xcode_version": """\ +A string representing the Xcode version number, or `None` if it is unknown. +""", + "default_ios_sdk_version": """\ +A string representing the default iOS SDK version number for this version of +Xcode, or `None` if it is unknown. +""", + "default_macos_sdk_version": """\ +A string representing the default macOS SDK version number for this version of +Xcode, or `None` if it is unknown. +""", + "default_tvos_sdk_version": """\ +A string representing the default tvOS SDK version number for this version of +Xcode, or `None` if it is unknown. +""", + "default_watchos_sdk_version": """\ +A string representing the default watchOS SDK version number for this version of +Xcode, or `None` if it is unknown. +""", + "default_visionos_sdk_version": """\ +A string representing the default visionOS SDK version number for this version +of Xcode, or `None` if it is unknown. +""", + }, + init = _xcode_version_properties_info_init, +) + +XcodeVersionRuleInfo = provider( + doc = """\ +The information in a single target of the `xcode_version` rule. A single target +of this rule contains an official version label decided by Apple, a number of +supported aliases one might use to reference this version, and various +properties of the Xcode version (such as default SDK versions). + +For example, one may want to reference official Xcode version 7.0.1 using the +"7" or "7.0" aliases. This official version of Xcode may have a default +supported iOS SDK of 9.0. +""", + fields = { + "aliases": """\ +A list of strings denoting aliases that can be used to reference this Xcode +version. +""", + "label": """\ +The build `Label` of the `xcode_version` target that propagated this provider. +""", + "xcode_version_properties": """\ +An `XcodeVersionPropertiesInfo` provider that contains the details about this +Xcode version, such as its default SDK versions. +""", + }, +) diff --git a/xcode/xcode_config.bzl b/xcode/xcode_config.bzl index 5d4a855..bbdb764 100644 --- a/xcode/xcode_config.bzl +++ b/xcode/xcode_config.bzl @@ -14,11 +14,426 @@ """Implementation of the `xcode_config` build rule.""" +load( + "@build_bazel_apple_support//xcode/private:providers.bzl", + "AvailableXcodesInfo", + "XcodeVersionPropertiesInfo", + "XcodeVersionRuleInfo", +) + visibility("public") -def xcode_config(name, **kwargs): - # TODO: b/311385128 - Migrate the native implementation here. - native.xcode_config( - name = name, - **kwargs +UNAVAILABLE_XCODE_MESSAGE = "'bazel fetch --configure' (Bzlmod) or 'bazel sync --configure' (WORKSPACE)" + +def _xcode_config_impl(ctx): + apple_fragment = ctx.fragments.apple + cpp_fragment = ctx.fragments.cpp + + explicit_default_version = ctx.attr.default[XcodeVersionRuleInfo] if ctx.attr.default else None + explicit_versions = [ + target[XcodeVersionRuleInfo] + for target in ctx.attr.versions + ] if ctx.attr.versions else [] + remote_versions = [ + target + for target in ctx.attr.remote_versions[AvailableXcodesInfo].available_versions + ] if ctx.attr.remote_versions else [] + local_versions = [ + target + for target in ctx.attr.local_versions[AvailableXcodesInfo].available_versions + ] if ctx.attr.local_versions else [] + + local_default_version = ctx.attr.local_versions[AvailableXcodesInfo].default_version if ctx.attr.local_versions else None + xcode_version_properties = None + availability = "unknown" + + if _use_available_xcodes( + explicit_default_version, + explicit_versions, + local_versions, + remote_versions, + ): + xcode_version_properties, availability = _resolve_xcode_from_local_and_remote( + ctx.actions, + local_versions, + remote_versions, + apple_fragment.xcode_version_flag, + apple_fragment.prefer_mutual_xcode, + local_default_version, + ) + else: + xcode_version_properties = _resolve_explicitly_defined_version( + explicit_versions, + explicit_default_version, + apple_fragment.xcode_version_flag, + ) + availability = "UNKNOWN" + + ios_sdk_version = apple_fragment.ios_sdk_version_flag or _dotted_version_or_default(xcode_version_properties.default_ios_sdk_version, "8.4") + macos_sdk_version = apple_fragment.macos_sdk_version_flag or _dotted_version_or_default(xcode_version_properties.default_macos_sdk_version, "10.11") + tvos_sdk_version = apple_fragment.tvos_sdk_version_flag or _dotted_version_or_default(xcode_version_properties.default_tvos_sdk_version, "9.0") + watchos_sdk_version = apple_fragment.watchos_sdk_version_flag or _dotted_version_or_default(xcode_version_properties.default_watchos_sdk_version, "2.0") + visionos_sdk_version = _dotted_version_or_default(xcode_version_properties.default_visionos_sdk_version, "1.0") + + ios_minimum_os = apple_fragment.ios_minimum_os_flag or ios_sdk_version + macos_minimum_os = apple_fragment.macos_minimum_os_flag or macos_sdk_version + tvos_minimum_os = apple_fragment.tvos_minimum_os_flag or tvos_sdk_version + watchos_minimum_os = apple_fragment.watchos_minimum_os_flag or watchos_sdk_version + if cpp_fragment.minimum_os_version(): + visionos_minimum_os = apple_common.dotted_version(cpp_fragment.minimum_os_version()) + else: + visionos_minimum_os = visionos_sdk_version + + # TODO: b/335817541 - At this time, there is still one place in the Bazel + # Starlark built-in code that relies on this specific provider -- the code + # in `objc/compilation_support.bzl` that registers the + # `ObjcBinarySymbolStrip` action. Until that code is removed or updated, we + # must make sure to always return this provider. However, we should also + # return a newer, modernized provider, and have non-builtin Starlark clients + # migrate to that provider ASAP. + xcode_versions = apple_common.XcodeVersionConfig( + ios_sdk_version = str(ios_sdk_version), + ios_minimum_os_version = str(ios_minimum_os), + visionos_sdk_version = str(visionos_sdk_version), + visionos_minimum_os_version = str(visionos_minimum_os), + watchos_sdk_version = str(watchos_sdk_version), + watchos_minimum_os_version = str(watchos_minimum_os), + tvos_sdk_version = str(tvos_sdk_version), + tvos_minimum_os_version = str(tvos_minimum_os), + macos_sdk_version = str(macos_sdk_version), + macos_minimum_os_version = str(macos_minimum_os), + xcode_version = xcode_version_properties.xcode_version, + availability = availability, + xcode_version_flag = apple_fragment.xcode_version_flag, + include_xcode_execution_info = apple_fragment.include_xcode_exec_requirements, + ) + return [ + DefaultInfo(runfiles = ctx.runfiles()), + xcode_versions, + xcode_version_properties, + ] + +xcode_config = rule( + attrs = { + "default": attr.label( + doc = """\ +The default official version of Xcode to use. + +The version specified by the provided `xcode_version` target is to be used if +no `xcode_version` build flag is specified. This is required if any `versions` +are set. This may not be set if `remote_versions` or `local_versions` is set. +""", + providers = [[XcodeVersionRuleInfo]], + ), + "versions": attr.label_list( + doc = """\ +Accepted `xcode_version` targets that may be used. + +If the value of the `xcode_version` build flag matches one of the aliases or +version number of any of the given `xcode_version` targets, the matching target +will be used. This may not be set if `remote_versions` or `local_versions` is +set. +""", + providers = [[XcodeVersionRuleInfo]], + ), + "remote_versions": attr.label( + doc = """\ +The `xcode_version` targets that are available remotely. + +These are used along with `local_versions` to select a mutually available +version. This may not be set if `versions` is set. +""", + providers = [[AvailableXcodesInfo]], + ), + "local_versions": attr.label( + doc = """\ +The `xcode_version` targets that are available locally. + +These are used along with `remote_versions` to select a mutually available +version. This may not be set if `versions` is set. +""", + providers = [[AvailableXcodesInfo]], + ), + }, + doc = """\ +A single target of this rule can be referenced by the `--xcode_version_config` +build flag to translate the `--xcode_version` flag into an accepted official +Xcode version. This allows selection of an official Xcode version from a number +of registered aliases. +""", + fragments = ["apple", "cpp"], + implementation = _xcode_config_impl, +) + +def _use_available_xcodes( + explicit_default_version, + explicit_versions, + local_versions, + remote_versions): + if remote_versions: + if explicit_versions: + fail("'versions' may not be set if '[local,remote]_versions' is set.") + if explicit_default_version: + fail("'default' may not be set if '[local,remote]_versions' is set.") + if not local_versions: + fail("if 'remote_versions' are set, you must also set 'local_versions'") + return True + return False + +def _duplicate_alias_error(alias, versions): + labels_containing_alias = [] + for version in versions: + if alias in version.aliases or (version.xcode_version_properties.xcode_version == alias): + labels_containing_alias.append(str(version.label)) + return "'{}' is registered to multiple labels ({}) in a single xcode_config rule".format( + alias, + ", ".join(labels_containing_alias), + ) + +def _aliases_to_xcode_version(versions): + version_map = {} + if not versions: + return version_map + for version in versions: + for alias in version.aliases: + if alias in version_map: + fail(_duplicate_alias_error(alias, versions)) + else: + version_map[alias] = version + version_string = version.xcode_version_properties.xcode_version + if version_string not in version.aliases: # only add the version if it's not also an alias + if version_string in version_map: + fail(_duplicate_alias_error(version_string, versions)) + else: + version_map[version_string] = version + return version_map + +def _resolve_xcode_from_local_and_remote( + actions, + local_versions, + remote_versions, + xcode_version_flag, + prefer_mutual_xcode, + local_default_version): + local_alias_to_version_map = _aliases_to_xcode_version(local_versions) + remote_alias_to_version_map = _aliases_to_xcode_version(remote_versions) + + # A version is mutually available (available both locally and remotely) if the local version + # attribute matches either the version attribute or one of the aliases of the remote version. + # mutually_vailable_versions is a subset of remote_versions. + # We assume the "version" attribute in local xcode_version contains a full version string, + # e.g. including the build, while the versions in "alias" attribute may be less granular. + # We don't make this assumption for remote xcode_versions. + mutually_available_versions = {} + for version in local_versions: + version_string = version.xcode_version_properties.xcode_version + if version_string in remote_alias_to_version_map: + mutually_available_versions[version_string] = remote_alias_to_version_map[version_string] + + # We'd log an event here if we could!! + if xcode_version_flag: + remote_version_from_flag = remote_alias_to_version_map.get(xcode_version_flag) + local_version_from_flag = local_alias_to_version_map.get(xcode_version_flag) + availability = "BOTH" + + if remote_version_from_flag and local_version_from_flag: + local_version_from_remote_versions = remote_alias_to_version_map.get(local_version_from_flag.xcode_version_properties.xcode_version) + if local_version_from_remote_versions: + return remote_version_from_flag.xcode_version_properties, availability + else: + fail( + ("Xcode version {0} was selected, either because --xcode_version was passed, or" + + " because xcode-select points to this version locally. This corresponds to" + + " local Xcode version {1}. That build of version {0} is not available" + + " remotely, but there is a different build of version {2}, which has" + + " version {2} and aliases {3}. You probably meant to use this version." + + " Please download it *and delete version {1}, then run `bazel shutdown`" + + " to continue using dynamic execution. If you really did intend to use" + + " local version {1}, please specify it fully with --xcode_version={1}.").format( + xcode_version_flag, + local_version_from_flag.xcode_version_properties.xcode_version, + remote_version_from_flag.xcode_version_properties.xcode_version, + remote_version_from_flag.aliases, + ), + ) + + elif local_version_from_flag: + if mutually_available_versions: + _warn( + actions, + "explicit_version_not_available_remotely_consider_mutual", + version = xcode_version_flag, + mutual_versions = [version for version in mutually_available_versions], + ) + else: + _warn( + actions, + "explicit_version_not_available_remotely", + version = xcode_version_flag, + ) + return local_version_from_flag.xcode_version_properties, "LOCAL" + + elif remote_version_from_flag: + _warn( + actions, + "version_not_available_locally", + version = xcode_version_flag, + command = UNAVAILABLE_XCODE_MESSAGE, + local_versions = ", ".join([version for version in local_alias_to_version_map.keys()]), + ) + availability = "REMOTE" + + return remote_version_from_flag.xcode_version_properties, availability + + else: # fail if we can't find any version to match + fail( + ("--xcode_version={0} specified, but '{0}' is not an available Xcode version." + + " Locally available versions: [{2}]. Remotely available versions: [{3}]. If" + + " you believe you have '{0}' installed, try running {1}, and then" + + " re-run your command.").format( + xcode_version_flag, + UNAVAILABLE_XCODE_MESSAGE, + ", ".join([version.xcode_version_properties.xcode_version for version in local_versions]), + ", ".join([version.xcode_version_properties.xcode_version for version in remote_versions]), + ), + ) + + # --xcode_version is not set + availability = "UNKNOWN" + local_version = None + + # If there aren't any mutually available versions, select the local default. + if not mutually_available_versions: + _warn( + actions, + "local_default_not_available_remotely", + local_version = local_default_version.xcode_version_properties.xcode_version, + remote_versions = ", ".join([version.xcode_version_properties.xcode_version for version in remote_versions]), + ) + local_version = local_default_version + availability = "LOCAL" + elif (local_default_version.xcode_version_properties.xcode_version in remote_alias_to_version_map): + # If the local default version is also available remotely, use it. + availability = "BOTH" + local_version = remote_alias_to_version_map.get(local_default_version.xcode_version_properties.xcode_version) + else: + # If an alias of the local default version is available remotely, use it. + for version_number in local_default_version.aliases: + if version_number in remote_alias_to_version_map: + availability = "BOTH" + local_version = remote_alias_to_version_map.get(version_number) + break + + if local_version: + return local_version.xcode_version_properties, availability + + # The local default is not available remotely. + if prefer_mutual_xcode: + # If we prefer a mutually available version, the newest one. + newest_version = "0.0" + default_version = None + for _, version in mutually_available_versions.items(): + if _compare_version_strings(version.xcode_version_properties.xcode_version, newest_version) > 0: + default_version = version + newest_version = default_version.xcode_version_properties.xcode_version + + return default_version.xcode_version_properties, "BOTH" + else: + # Use the local default + return local_default_version.xcode_version_properties, "LOCAL" + +def _compare_version_strings(first, second): + return apple_common.dotted_version(first).compare_to( + apple_common.dotted_version(second), + ) + +def _resolve_explicitly_defined_version( + explicit_versions, + explicit_default_version, + xcode_version_flag): + if explicit_default_version and explicit_default_version.label not in [ + version.label + for version in explicit_versions + ]: + fail( + "default label '{}' must be contained in versions attribute".format( + explicit_default_version.label, + ), + ) + if not explicit_versions: + if explicit_default_version: + fail("default label must be contained in versions attribute") + return XcodeVersionPropertiesInfo(xcode_version = None) + + if not explicit_default_version: + fail("if any versions are specified, a default version must be specified") + + alias_to_versions = _aliases_to_xcode_version(explicit_versions) + if xcode_version_flag: + flag_version = alias_to_versions.get(str(xcode_version_flag)) + if flag_version: + return flag_version.xcode_version_properties + else: + fail( + ("--xcode_version={0} specified, but '{0}' is not an available Xcode version. " + + "If you believe you have '{0}' installed, try running \"bazel shutdown\", and then " + + "re-run your command.").format(xcode_version_flag), + ) + return alias_to_versions.get(explicit_default_version.xcode_version_properties.xcode_version).xcode_version_properties + +def _dotted_version_or_default(field, default): + return apple_common.dotted_version(field) or default + +_WARNINGS = { + "version_not_available_locally": """\ +--xcode_version={version} specified, but it is not available locally. \ +Your build will fail if any actions require a local Xcode. \ +If you believe you have '{version}' installed, try running {command}, \ +and then re-run your command. Locally available versions: {local_versions}. +""", + "local_default_not_available_remotely": """\ +Using a local Xcode version, '{local_version}', since there are no \ +remotely available Xcodes on this machine. Consider downloading one of the \ +remotely available Xcode versions ({remote_versions}) in order to get the best \ +build performance. +""", + "explicit_version_not_available_remotely": """\ +--xcode_version={version} specified, but it is not available remotely. Actions \ +requiring Xcode will be run locally, which could make your build slower. +""", + "explicit_version_not_available_remotely_consider_mutual": """\ +--xcode_version={version} specified, but it is not available remotely. Actions \ +requiring Xcode will be run locally, which could make your build slower. \ +Consider using one of [{mutual_versions}]. +""", +} + +def _warn(actions, msg_id, **kwargs): + """Print a warning and also record it as a testable dummy action. + + Starlark doesn't support testing the output of the `print()` function, so + this function also registers a dummy action with a specifically formatted + mnemonic that can be read in the test using the `assert_warning` helper. + + Args: + actions: The object used to register actions. + msg_id: A string identifying the warning as a key in the `_WARNINGS` + dictionary. + **kwargs: Formatting arguments for the message string. + """ + + # buildifier: disable=print + print(_WARNINGS[msg_id].format(**kwargs)) + + mnemonic = "Warning:{}".format(msg_id) + if kwargs: + # Sort the format arguments by key so that they're deterministically + # ordered for tests. + sorted_values = [] + for key in sorted(kwargs.keys()): + sorted_values.append("{}={}".format(key, str(kwargs[key]))) + mnemonic += ":{}".format(";".join(sorted_values)) + + actions.do_nothing( + mnemonic = mnemonic, ) diff --git a/xcode/xcode_config_alias.bzl b/xcode/xcode_config_alias.bzl index b0ed996..541676e 100644 --- a/xcode/xcode_config_alias.bzl +++ b/xcode/xcode_config_alias.bzl @@ -12,13 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Implementation of the `xcode_config_alias` build rule.""" +"""Implementation of the `xcode_config_alias` build rule. + +This rule is an alias to the `xcode_config` rule currently in use, which in turn +depends on the current configuration; in particular, the value of the +`--xcode_version_config`. + +This is intentionally undocumented for users; the workspace is expected to +contain exactly one instance of this rule under `@bazel_tools//tools/osx` and +people who want to get data this rule provides should depend on that one. +""" + +load( + "@build_bazel_apple_support//xcode/private:providers.bzl", + "XcodeVersionPropertiesInfo", +) visibility("public") -def xcode_config_alias(name, **kwargs): - # TODO: b/311385128 - Migrate the native implementation here. - native.xcode_config_alias( - name = name, - **kwargs - ) +def _xcode_config_alias_impl(ctx): + xcode_config = ctx.attr._xcode_config + return [ + xcode_config[XcodeVersionPropertiesInfo], + # TODO: b/335817541 - At this time, there is still one place in the + # Bazel Starlark built-in code that relies on this specific provider -- + # the code in `objc/compilation_support.bzl` that registers the + # `ObjcBinarySymbolStrip` action. Until that code is removed or updated, + # we must make sure to always return this provider. However, we should + # also return a newer, modernized provider, and have non-builtin + # Starlark clients migrate to that provider ASAP. + xcode_config[apple_common.XcodeVersionConfig], + ] + +xcode_config_alias = rule( + attrs = { + "_xcode_config": attr.label( + default = configuration_field( + fragment = "apple", + name = "xcode_config_label", + ), + ), + }, + fragments = ["apple"], + implementation = _xcode_config_alias_impl, +) diff --git a/xcode/xcode_version.bzl b/xcode/xcode_version.bzl index a91b567..bc52f99 100644 --- a/xcode/xcode_version.bzl +++ b/xcode/xcode_version.bzl @@ -14,11 +14,100 @@ """Implementation of the `xcode_version` build rule.""" +load( + "@build_bazel_apple_support//xcode/private:providers.bzl", + "XcodeVersionPropertiesInfo", + "XcodeVersionRuleInfo", +) + visibility("public") -def xcode_version(name, **kwargs): - # TODO: b/311385128 - Migrate the native implementation here. - native.xcode_version( - name = name, - **kwargs +def _xcode_version_impl(ctx): + xcode_version_properties = XcodeVersionPropertiesInfo( + xcode_version = ctx.attr.version, + default_ios_sdk_version = ctx.attr.default_ios_sdk_version, + default_visionos_sdk_version = ctx.attr.default_visionos_sdk_version, + default_watchos_sdk_version = ctx.attr.default_watchos_sdk_version, + default_tvos_sdk_version = ctx.attr.default_tvos_sdk_version, + default_macos_sdk_version = ctx.attr.default_macos_sdk_version, ) + return [ + xcode_version_properties, + XcodeVersionRuleInfo( + label = ctx.label, + xcode_version_properties = xcode_version_properties, + aliases = ctx.attr.aliases, + ), + DefaultInfo(runfiles = ctx.runfiles()), + ] + +xcode_version = rule( + attrs = { + "aliases": attr.string_list( + doc = """\ +Accepted aliases for this version of Xcode. If the value of the +`--xcode_version` build flag matches any of the given alias strings, this Xcode +version will be used. +""", + allow_empty = True, + mandatory = False, + ), + "default_ios_sdk_version": attr.string( + default = "8.4", + doc = """\ +The iOS SDK version that is used by default when this version of Xcode is being +used. The `--ios_sdk_version` build flag will override the value specified here. + +NOTE: The `--ios_sdk_version` flag is deprecated and not recommended for use. +""", + mandatory = False, + ), + "default_macos_sdk_version": attr.string( + default = "10.11", + doc = """\ +The macOS SDK version that is used by default when this version of Xcode is +being used. The `--macos_sdk_version` build flag will override the value +specified here. + +NOTE: The `--macos_sdk_version` flag is deprecated and not recommended for use. +""", + mandatory = False, + ), + "default_tvos_sdk_version": attr.string( + default = "9.0", + doc = """\ +The tvOS SDK version that is used by default when this version of Xcode is being +used. The `--tvos_sdk_version` build flag will override the value specified +here. + +NOTE: The `--tvos_sdk_version` flag is deprecated and not recommended for use. +""", + mandatory = False, + ), + "default_visionos_sdk_version": attr.string( + default = "1.0", + doc = """\ +The visionOS SDK version that is used by default when this version of Xcode is +being used. +""", + mandatory = False, + ), + "default_watchos_sdk_version": attr.string( + default = "2.0", + doc = """\ +The watchOS SDK version that is used by default when this version of Xcode is +being used. The `--watchos_sdk_version` build flag will override the value +specified here. + +NOTE: The `--watchos_sdk_version` flag is deprecated and not recommended for +use. +""", + mandatory = False, + ), + "version": attr.string( + doc = "The official version number for this version of Xcode.", + mandatory = True, + ), + }, + implementation = _xcode_version_impl, +)