From 60068ee3307b048d9f151e1c3dcca6e2b39cec1e Mon Sep 17 00:00:00 2001 From: Erik Kerber Date: Sat, 17 Oct 2026 05:49:14 -0500 Subject: [PATCH 01/24] Improve Xcode selection diagnostics (#644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An attempt to (greatly) increase the legibility of the currently inscrutible error message developers receive when the configured Xcode is not available locally or remotely. ```text Bazel couldn’t match the selected Xcode to this project’s supported versions: 27.0 (build 27A266a) Supported versions: 26.5 (build 17F42) 27.2 (build 27B5019j) Detected locally: 26.5 (build 17F42) 27.0 (build 27A266a) 27.2 (build 27B5019j) Install a supported Xcode version and select it with xcode-select. ``` ### Validation - `bazel test //test:xcode_config_test` on Bazel 8.7.0, 9.0.0, 9.0.1, and 9.2.0: all 51 tests pass on each release, including unsupported, unknown, ambiguous, duplicate, and empty inventories, plus custom and empty hints. - Verified the failure against a consuming project's configuration and checked the fallback when discovery fails. A few examples: CleanShot 2026-09-17 at 13 32 56@2x CleanShot 2026-09-17 at 13 33 21@2x --- test/xcode_config_test.bzl | 82 ++++++++++++++++++++++++- xcode/xcode_config.bzl | 121 ++++++++++++++++++++++++++++++------- xcode/xcode_configure.bzl | 12 +++- 3 files changed, 188 insertions(+), 27 deletions(-) diff --git a/test/xcode_config_test.bzl b/test/xcode_config_test.bzl index 3e41ab42..1d911375 100644 --- a/test/xcode_config_test.bzl +++ b/test/xcode_config_test.bzl @@ -22,7 +22,7 @@ load( "//xcode:providers.bzl", "XcodeVersionPropertiesInfo", ) -load("//xcode:xcode_config.bzl", "xcode_config") +load("//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") load("//xcode:xcode_version.bzl", "xcode_version") load(":test_helpers.bzl", "FIXTURE_TAGS", "find_action", "make_all_tests") @@ -620,7 +620,9 @@ def _invalid_xcode_from_mutual_throws(namer): def _invalid_xcode_from_mutual_throws_test_impl(ctx): env = analysistest.begin(ctx) - asserts.expect_failure(env, "--xcode_version=6 specified, but '6' is not an available Xcode version. Locally available versions: [8.4]. Remotely available versions: [5.1.2, 8.4].") + asserts.expect_failure(env, "Xcode 6 was not found.") + asserts.expect_failure(env, "Detected locally: 8.4") + asserts.expect_failure(env, "Available remotely: 5.1.2, 8.4") return analysistest.end(env) _invalid_xcode_from_mutual_throws_test = analysistest.make( @@ -1174,7 +1176,8 @@ def _invalid_xcode_specified(namer): 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") + asserts.expect_failure(env, "Bazel couldn’t match the selected Xcode to this project’s supported versions:") + asserts.expect_failure(env, "\n 6\n\n") return analysistest.end(env) _invalid_xcode_specified_test = analysistest.make( @@ -1193,6 +1196,78 @@ _invalid_xcode_specified_test = analysistest.make( # ------------------------------------------------------------------------------ +def _detected_xcode_diagnostics(namer): + xcode_version( + name = namer("supported"), + version = "26.5.0.17F42", + aliases = ["17F42"], + tags = FIXTURE_TAGS, + ) + tests = [] + for case, versions, selected, hint in [ + ("unsupported", ["27.0.27A266a"], "27.0 (build 27A266a)", None), + ("normalized_duplicates", ["27.0.27A266a", "27.0.0.27A266a"], "27.0 (build 27A266a)", None), + ("ambiguous", ["27.0.27A266a", "27.1.27A266a"], "27A266a", None), + ("unknown", ["26.5.17F42"], "27A266a", None), + ("empty", [], "27A266a", None), + ("custom_hint", ["27.0.27A266a"], "27.0 (build 27A266a)", "Follow https://example.com/xcode\nThen run ./setup-xcode {team}."), + ("empty_hint", ["27.0.27A266a"], "27.0 (build 27A266a)", ""), + ]: + detected_xcodes( + name = namer(case + "_detected"), + versions = versions, + tags = FIXTURE_TAGS, + ) + xcode_config( + name = namer(case + "_config"), + default = namer(":supported"), + versions = [namer(":supported")], + detected_versions = namer(":" + case + "_detected"), + tags = FIXTURE_TAGS, + **({"xcode_selection_hint": hint} if hint != None else {}) + ) + name = "detected_xcode_diagnostics_" + case + _detected_xcode_diagnostics_test( + name = name, + target_under_test = namer(":" + case + "_config"), + expected_selected = selected, + expect_detected = bool(versions), + expected_hint = hint if hint != None else "Install a supported Xcode version and select it with", + custom_hint = hint != None, + ) + tests.append(name) + return tests + +def _detected_xcode_diagnostics_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Bazel couldn’t match the selected Xcode to this project’s supported versions:") + asserts.expect_failure(env, "\n " + ctx.attr.expected_selected + "\n\n") + asserts.expect_failure(env, "26.5 (build 17F42)") + errors = "\n".join([cause.message for cause in analysistest.target_under_test(env)[AnalysisFailureInfo].causes.to_list()]) + if ctx.attr.expected_hint: + asserts.expect_failure(env, ctx.attr.expected_hint) + if ctx.attr.custom_hint: + asserts.false(env, "Install a supported Xcode version and select it with" in errors) + if ctx.attr.expect_detected: + asserts.expect_failure(env, "Detected locally:") + else: + asserts.false(env, "Detected locally:" in errors) + return analysistest.end(env) + +_detected_xcode_diagnostics_test = analysistest.make( + _detected_xcode_diagnostics_test_impl, + attrs = { + "expected_selected": attr.string(), + "expect_detected": attr.bool(), + "expected_hint": attr.string(), + "custom_hint": attr.bool(), + }, + config_settings = {"//command_line_option:xcode_version": "27A266a"}, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + def _requires_default(namer): xcode_config( name = "requires_default__foo", @@ -2327,6 +2402,7 @@ def xcode_config_test(name): _valid_alias_dotted_version, _valid_alias_nonnumerical, _invalid_xcode_specified, + _detected_xcode_diagnostics, _requires_default, _duplicate_aliases_defined_version, _duplicate_aliases_within_available_xcodes, diff --git a/xcode/xcode_config.bzl b/xcode/xcode_config.bzl index 1268564c..0b128450 100644 --- a/xcode/xcode_config.bzl +++ b/xcode/xcode_config.bzl @@ -15,6 +15,7 @@ """Implementation of the `xcode_config` build rule.""" load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_features//private:util.bzl", "ge") load("//build_settings:build_settings.bzl", "read_possibly_native_flag") load( "//xcode:providers.bzl", @@ -28,7 +29,21 @@ load( visibility("public") -unavailable_xcode_message = "'bazel fetch --configure'" +_DetectedXcodesInfo = provider( + doc = "Locally detected Xcode versions used only in diagnostics.", + fields = { + "versions": "Full Xcode version strings reported by xcode-locator.", + }, +) + +def _detected_xcodes_impl(ctx): + return [_DetectedXcodesInfo(versions = ctx.attr.versions)] + +detected_xcodes = rule( + implementation = _detected_xcodes_impl, + attrs = {"versions": attr.string_list()}, + doc = "Diagnostic inventory generated by xcode_autoconf; does not select an Xcode.", +) def _xcode_config_impl(ctx): # TODO: remove when we test with Bazel 8+ @@ -74,6 +89,8 @@ def _xcode_config_impl(ctx): explicit_versions, explicit_default_version, read_possibly_native_flag(ctx, "xcode_version"), + ctx.attr.detected_versions[_DetectedXcodesInfo].versions if ctx.attr.detected_versions else [], + ctx.attr.xcode_selection_hint, ) availability = "UNKNOWN" @@ -139,6 +156,22 @@ def _xcode_config_impl(ctx): xcode_config = rule( attrs = { + "xcode_selection_hint": attr.string( + default = "Install a supported Xcode version and select it with \033[1;36mxcode-select\033[0m.", + doc = """\ +Guidance displayed when an explicitly selected Xcode does not match `versions`. +Replaces the default installation and selection instructions. Newlines are +preserved, and an empty string omits the hint. +""", + ), + "detected_versions": attr.label( + default = "@local_config_xcode//:detected_xcodes", + providers = [_DetectedXcodesInfo], + doc = """\ +Locally detected Xcode versions used only in diagnostics. These do not affect +which versions are supported or selected. +""", + ), "default": attr.label( doc = """\ The default official version of Xcode to use. @@ -253,6 +286,38 @@ def _aliases_to_xcode_version(versions): version_map[version_string] = version return version_map +def _fail_xcode_selection(message): + # stack_trace was added in Bazel 9.0.1. + # https://github.com/bazelbuild/bazel/pull/28817 + if ge("9.0.1"): + fail(message, stack_trace = False) + fail(message) + +def _display_xcode_version(version, known_versions = [], detected_versions = []): + # A build number can appear in version metadata without being a registered + # alias. Resolve it for display only; do not change version selection. + if "." not in version and not version.isdigit(): + matches = {} + for full_version in detected_versions + [known_version.xcode_version_properties.xcode_version for known_version in known_versions]: + if full_version.endswith("." + version): + matches[_format_xcode_version(full_version)] = True + if len(matches) == 1: + return matches.keys()[0] + return _format_xcode_version(version) + +def _format_xcode_version(version): + # Xcode version strings may append the build number to the release version, + # e.g. 27.0.27A266a or 27.0.0.27A266a. + components = version.split(".") + if len(components) < 3 or components[-1].isdigit(): + return version + release = components[:-1] + if not all([component.isdigit() for component in release]): + return version + if len(release) == 3 and release[-1] == "0": + release = release[:-1] + return "{} (build {})".format(".".join(release), components[-1]) + def _resolve_xcode_from_local_and_remote( local_versions, remote_versions, @@ -319,29 +384,27 @@ def _resolve_xcode_from_local_and_remote( elif remote_version_from_flag: # buildifier: disable=print - print(("--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}. ") + print(("\n\033[1;33mXcode {version} was not found locally.\033[0m\n\n" + + "Remote builds can use it; local actions require this Xcode.\n" + + "Detected locally: {local_versions}\n\n" + + "Install Xcode {version} to run actions locally.\n") .format( - version = xcode_version_flag, - command = unavailable_xcode_message, - local_versions = ", ".join([version for version in local_alias_to_version_map.keys()]), + version = _display_xcode_version(remote_version_from_flag.xcode_version_properties.xcode_version), + local_versions = ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in local_versions]) or "none", )) 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]), + _fail_xcode_selection( + ("\n\033[1;31mXcode {0} was not found.\033[0m\n\n" + + " Detected locally: {1}\n" + + " Available remotely: {2}\n\n" + + "Install Xcode {0} and retry.\n").format( + _display_xcode_version(xcode_version_flag, local_versions + remote_versions), + ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in local_versions]) or "none", + ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in remote_versions]) or "none", ), ) @@ -401,7 +464,9 @@ def _sdk_version_flag(apple_fragment, name): def _resolve_explicitly_defined_version( explicit_versions, explicit_default_version, - xcode_version_flag): + xcode_version_flag, + detected_versions, + xcode_selection_hint): if explicit_default_version and explicit_default_version.label not in [ version.label for version in explicit_versions @@ -425,10 +490,22 @@ def _resolve_explicitly_defined_version( 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), + detected_message = "" + if detected_versions: + detected_message = "\033[1mDetected locally:\033[0m\n {}\n\n".format( + "\n ".join(sorted({_format_xcode_version(version): True for version in detected_versions})), + ) + _fail_xcode_selection( + ("\n\033[1;31mBazel couldn’t match the selected Xcode to this project’s supported versions:\033[0m\n" + + " {0}\n\n" + + "\033[1mSupported versions:\033[0m\n {1}\n\n" + + "{2}" + + "{3}").format( + _display_xcode_version(xcode_version_flag, explicit_versions, detected_versions), + "\n ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in explicit_versions]), + detected_message, + xcode_selection_hint + "\n" if xcode_selection_hint else "", + ), ) return alias_to_versions.get(explicit_default_version.xcode_version_properties.xcode_version).xcode_version_properties diff --git a/xcode/xcode_configure.bzl b/xcode/xcode_configure.bzl index 40a40ef0..7d97baab 100644 --- a/xcode/xcode_configure.bzl +++ b/xcode/xcode_configure.bzl @@ -134,7 +134,8 @@ def _xcode_version_output(repository_ctx, name, version, aliases, developer_dir, return build_contents VERSION_CONFIG_STUB = """ -load("@apple_support//xcode:xcode_config.bzl", "xcode_config") +load("@apple_support//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") +detected_xcodes(name = 'detected_xcodes') xcode_config(name = 'host_xcodes') """ @@ -268,6 +269,9 @@ def _darwin_build_file(repository_ctx): if xcodeloc_err: return VERSION_CONFIG_STUB + "\n# Error: " + xcodeloc_err + "\n" + if not toolchains: + return VERSION_CONFIG_STUB + default_xcode_version = "" default_xcode_build_version = "" if xcodebuild_result.return_code == 0: @@ -280,10 +284,14 @@ def _darwin_build_file(repository_ctx): default_xcode_target = "" target_names = [] buildcontents = """ -load("@apple_support//xcode:xcode_config.bzl", "xcode_config") +load("@apple_support//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") load("@apple_support//xcode:available_xcodes.bzl", "available_xcodes") load("@apple_support//xcode:xcode_version.bzl", "xcode_version") """ + buildcontents += "detected_xcodes(name = 'detected_xcodes', versions = %s)\n" % repr([ + toolchain.version + for toolchain in toolchains + ]) for toolchain in toolchains: version = toolchain.version From d4595d8f66887617b084a57279544c9e6f2e9261 Mon Sep 17 00:00:00 2001 From: Thi Doan Date: Mon, 19 Oct 2026 14:36:09 +0100 Subject: [PATCH 02/24] Return native `XcodeProperties` from `xcode_config` on Bazel 8 (#669) --- test/xcode_config_test.bzl | 50 +++++++++++++++++++++++++++++++++++++- xcode/xcode_config.bzl | 24 ++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/test/xcode_config_test.bzl b/test/xcode_config_test.bzl index 1d911375..e4881c61 100644 --- a/test/xcode_config_test.bzl +++ b/test/xcode_config_test.bzl @@ -1724,6 +1724,51 @@ _default_sdk_versions_selected_xcode_test = analysistest.make( # ------------------------------------------------------------------------------ +def _native_xcode_properties(namer): + xcode_config( + name = "native_xcode_properties__foo", + default = namer(":version512"), + versions = [namer(":version512")], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + default_ios_sdk_version = "101", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + _native_xcode_properties_test( + name = "native_xcode_properties", + target_under_test = "native_xcode_properties__foo", + ) + return ["native_xcode_properties"] + +def _native_xcode_properties_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_properties = target_under_test[apple_common.XcodeProperties] + + asserts.equals(env, "5.1.2", xcode_properties.xcode_version) + asserts.equals(env, "101", xcode_properties.default_ios_sdk_version) + + return analysistest.end(env) + +_native_xcode_properties_test = analysistest.make( + _native_xcode_properties_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:native_xcode_properties__foo", + )), + str(Label("//xcode:starlark_version_config")): str(Label( + "//test:native_xcode_properties__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + def _default_without_version(namer): xcode_config( name = "default_without_version__foo", @@ -2424,7 +2469,10 @@ def xcode_config_test(name): _available_xcodes_mode_with_flag, ] + ([ _xcode_config_resolver_with_fragment_removed, - ] if _SUPPORTS_INCOMPATIBLE_REMOVE_CTX_APPLE_FRAGMENT else [])) if bazel_features.apple.xcode_config_migrated else [], + ] if _SUPPORTS_INCOMPATIBLE_REMOVE_CTX_APPLE_FRAGMENT else []) + ([ + # TODO: Remove once we no longer support Bazel 8. + _native_xcode_properties, + ] if getattr(apple_common, "XcodeProperties", None) else [])) if bazel_features.apple.xcode_config_migrated else [], ) # TODO: b/311385128 - The following tests from `XcodeConfigTest.java` diff --git a/xcode/xcode_config.bzl b/xcode/xcode_config.bzl index 0b128450..96312eb5 100644 --- a/xcode/xcode_config.bzl +++ b/xcode/xcode_config.bzl @@ -45,6 +45,14 @@ detected_xcodes = rule( doc = "Diagnostic inventory generated by xcode_autoconf; does not select an Xcode.", ) +_NATIVE_XCODE_PROPERTIES_SDK_FIELDS = [ + "default_ios_sdk_version", + "default_macos_sdk_version", + "default_tvos_sdk_version", + "default_watchos_sdk_version", + "default_visionos_sdk_version", +] + def _xcode_config_impl(ctx): # TODO: remove when we test with Bazel 8+ if not bazel_features.apple.xcode_config_migrated: @@ -147,6 +155,22 @@ def _xcode_config_impl(ctx): xcode_version_properties, ] + # Bazel 8's native `xcode_config` also returns this, and rules on that + # version may still read it instead of `XcodeVersionPropertiesInfo`. + # TODO: Remove once we no longer support Bazel 8. + native_xcode_properties = getattr(apple_common, "XcodeProperties", None) + if native_xcode_properties: + # `xcode_version` is required even when `None`; unset SDK versions + # take the native provider's defaults. + providers.append(native_xcode_properties( + xcode_version = xcode_version_properties.xcode_version, + **{ + field: getattr(xcode_version_properties, field) + for field in _NATIVE_XCODE_PROPERTIES_SDK_FIELDS + if getattr(xcode_version_properties, field) != None + } + )) + # TODO: b/367688113 - Once we've migrated all clients to generate and use # the new `xcode_sdk_variant` rules, fail if this field is not set. if xcode_version_properties.sdk_variant_info: From 6599e08659d61fa9d50cd2834846b347a72466ba Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 22 Oct 2026 10:26:17 -0400 Subject: [PATCH 03/24] Use direct indexing for required outputs in the Xcode path-substitution wrapper (#667) Since outputs is required by actions.run, accessing it via kwargs["outputs"] better reflects the API contract and allows static analysis to recognize that the value is always present. --- lib/apple_support.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/apple_support.bzl b/lib/apple_support.bzl index 1b9630c3..d50ce789 100644 --- a/lib/apple_support.bzl +++ b/lib/apple_support.bzl @@ -396,7 +396,7 @@ def _run( # Since a label/name isn't passed in, use the first output to derive a name # that will hopefully be unique. - output0 = kwargs.get("outputs")[0] + output0 = kwargs["outputs"][0] if xcode_path_resolve_level == _XCODE_PATH_RESOLVE_LEVEL.args: script = _XCODE_PROCESSOR__ARGS suffix = "args" From 24856d3a87033a58950bc9971382161964fb3cb9 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 22 Oct 2026 11:16:51 -0400 Subject: [PATCH 04/24] Accept Label values in Xcode threshold references (#678) The documented `after` argument accepted string references but failed when passed a `Label`, because companion-target construction called string methods. Construct the companion with `same_package_label` so the original repository and package identity survive calls from other repositories. --------- Co-authored-by: Adin Cebic --- doc/xcode_version_at_least.md | 12 ++--- test/xcode_version_at_least_test.bzl | 73 +++++++++++++++++++++++++++- xcode/xcode_version_at_least.bzl | 35 ++++++------- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/doc/xcode_version_at_least.md b/doc/xcode_version_at_least.md index 9c7bf834..5e106a23 100755 --- a/doc/xcode_version_at_least.md +++ b/doc/xcode_version_at_least.md @@ -70,11 +70,11 @@ With Xcode 27 or newer, both conditions match, but the more specialized `xcode_27_or_newer` branch wins. Without `after`, different values for these overlapping conditions would make the `select()` ambiguous. -Declare lower thresholds before higher thresholds and use local target names -or `:name` labels to inherit a same-package `after` chain transitively. For -cross-package or fully qualified references, list every lower threshold in -the chain explicitly. Each threshold must be strictly higher than those in -its `after` list. +Declare lower thresholds before higher thresholds to inherit a same-package +`after` chain transitively, whether references are strings or `Label` objects. +For cross-package references, list every lower threshold in the chain +explicitly. Each threshold must be strictly higher than those in its `after` +list. **PARAMETERS** @@ -84,7 +84,7 @@ its `after` list. | :------------- | :------------- | :------------- | | name | The name of the `config_setting` target to create. A companion feature flag target named `_flag` will also be created. | none | | minimum_xcode_version | The minimum Xcode version to match (inclusive), such as `"26.4"`, `"26.4.1"`, or `"27.0.0.27A5228h"`. | none | -| after | An optional list of lower-version `xcode_version_at_least` target names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited transitively. | `None` | +| after | An optional list of lower-version `xcode_version_at_least` target names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited transitively, whether the reference is a string or a `Label` object. | `None` | | visibility | The visibility of the generated targets. | `None` | | kwargs | Additional keyword arguments (such as `tags` or `constraint_values`) forwarded to `native.config_setting`. `tags` is also forwarded to the companion `_flag` target. | none | diff --git a/test/xcode_version_at_least_test.bzl b/test/xcode_version_at_least_test.bzl index 38f3627c..e3d5d572 100644 --- a/test/xcode_version_at_least_test.bzl +++ b/test/xcode_version_at_least_test.bzl @@ -313,11 +313,21 @@ def _test_qualified_labels_in_after(namer): minimum_xcode_version = "26.4", tags = FIXTURE_TAGS, ) + xcode_version_at_least( + name = namer("qual_27_0"), + after = [ + Label("@apple_support//test:" + namer("qual_26_2")), + "@apple_support//test:" + namer("qual_26_4"), + ], + minimum_xcode_version = "27.0", + tags = FIXTURE_TAGS, + ) _select_holder( name = namer("qual_holder"), selected_value = select({ namer(":qual_26_2"): "xcode_26_2_or_newer", namer(":qual_26_4"): "xcode_26_4_or_newer", + namer(":qual_27_0"): "xcode_27_0_or_newer", "//conditions:default": "older_than_26_2", }), tags = FIXTURE_TAGS, @@ -328,8 +338,68 @@ def _test_qualified_labels_in_after(namer): target_under_test = namer(":qual_holder"), expected_value = "xcode_26_4_or_newer", ) + _select_xcode_27_0_beta_test( + name = "label_objects_in_after_match_refined", + target_under_test = namer(":qual_holder"), + expected_value = "xcode_27_0_or_newer", + ) - return ["qualified_labels_in_after_matches_refined"] + return [ + "qualified_labels_in_after_matches_refined", + "label_objects_in_after_match_refined", + ] + +def _test_same_package_label_inheritance(namer): + xcode_version_at_least( + name = namer("base"), + minimum_xcode_version = "26.2", + tags = FIXTURE_TAGS, + ) + xcode_version_at_least( + name = namer("middle"), + after = [namer(":base")], + minimum_xcode_version = "26.4", + tags = FIXTURE_TAGS, + ) + + base_label = Label("//test:" + namer("base")) + middle_label = Label("//test:" + namer("middle")) + cases = [ + ("package_qualified", ["//test:" + namer("middle")], {}), + ("repo_qualified", ["@apple_support//test:" + namer("middle")], {}), + ("canonical", [str(middle_label)], {}), + ("label_object", [middle_label], {}), + ("mixed_references", [base_label, namer(":middle")], {}), + ("explicit_flag_values", [middle_label], {namer(":middle") + "_flag": "True"}), + ] + test_names = [] + for suffix, after, flag_values in cases: + xcode_version_at_least( + name = namer(suffix), + after = after, + flag_values = flag_values, + minimum_xcode_version = "27.0", + tags = FIXTURE_TAGS, + ) + _select_holder( + name = namer(suffix + "_holder"), + selected_value = select({ + namer(":base"): "base", + namer(":middle"): "middle", + namer(":" + suffix): "highest", + "//conditions:default": "default", + }), + tags = FIXTURE_TAGS, + ) + test_name = "same_package_inheritance_" + suffix + _select_xcode_27_0_beta_test( + name = test_name, + target_under_test = namer(":" + suffix + "_holder"), + expected_value = "highest", + ) + test_names.append(test_name) + + return test_names def _expect_failure_message_test_impl(ctx): env = analysistest.begin(ctx) @@ -431,6 +501,7 @@ def xcode_version_at_least_test(name): _test_padded_versions, _test_chained_select_refinement, _test_qualified_labels_in_after, + _test_same_package_label_inheritance, _test_after_conflicts, ] if bazel_features.apple.xcode_config_migrated else [], # TODO: Remove once we test with Bazel 8+ ) diff --git a/xcode/xcode_version_at_least.bzl b/xcode/xcode_version_at_least.bzl index 6b21a077..e0b1fc33 100644 --- a/xcode/xcode_version_at_least.bzl +++ b/xcode/xcode_version_at_least.bzl @@ -146,17 +146,15 @@ or `"27.0.0.27A5228h"`. def _flag_target_label(label): """Returns the `_flag` target label corresponding to an `xcode_version_at_least` target.""" - if not label.startswith(":") and not label.startswith("//") and not label.startswith("@"): - label = ":" + label - return label + "_flag" + label = native.package_relative_label(label) + return label.same_package_label(label.name + "_flag") def _local_target_name(label): """Returns the local target name if `label` refers to the current package, or `None`.""" - if label.startswith("//") or label.startswith("@"): - return None - if label.startswith(":"): - return label[1:] - return label + label = native.package_relative_label(label) + if label == native.package_relative_label(":" + label.name): + return label.name + return None def xcode_version_at_least( *, @@ -217,11 +215,11 @@ def xcode_version_at_least( `xcode_27_or_newer` branch wins. Without `after`, different values for these overlapping conditions would make the `select()` ambiguous. - Declare lower thresholds before higher thresholds and use local target names - or `:name` labels to inherit a same-package `after` chain transitively. For - cross-package or fully qualified references, list every lower threshold in - the chain explicitly. Each threshold must be strictly higher than those in - its `after` list. + Declare lower thresholds before higher thresholds to inherit a same-package + `after` chain transitively, whether references are strings or `Label` objects. + For cross-package references, list every lower threshold in the chain + explicitly. Each threshold must be strictly higher than those in its `after` + list. Args: name: The name of the `config_setting` target to create. A companion @@ -232,7 +230,7 @@ def xcode_version_at_least( names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited - transitively. + transitively, whether the reference is a string or a `Label` object. visibility: The visibility of the generated targets. **kwargs: Additional keyword arguments (such as `tags` or `constraint_values`) forwarded to `native.config_setting`. `tags` is @@ -240,7 +238,10 @@ def xcode_version_at_least( """ flag_name = name + "_flag" tags = kwargs.get("tags") - flag_values = dict(kwargs.pop("flag_values", None) or {}) + flag_values = { + native.package_relative_label(flag): value + for flag, value in (kwargs.pop("flag_values", None) or {}).items() + } after_flag_labels = [] if after: @@ -259,7 +260,7 @@ def xcode_version_at_least( continue for inherited_flag, inherited_value in existing_setting.get("flag_values").items(): - flag_values[inherited_flag] = inherited_value + flag_values[native.package_relative_label(inherited_flag)] = inherited_value _xcode_version_at_least_flag( name = flag_name, @@ -271,7 +272,7 @@ def xcode_version_at_least( visibility = visibility, ) - flag_values[":" + flag_name] = "True" + flag_values[native.package_relative_label(":" + flag_name)] = "True" native.config_setting( name = name, From e0c31aa91d55a7d076e5c6a6a7359143f2a19c27 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 17:02:56 -0400 Subject: [PATCH 05/24] Use the arm64 simulator for the default visionOS test split (#679) With no `--visionos_cpus`, the test transition selected `visionos_x86_64`. [#293](https://github.com/bazelbuild/apple_support/pull/293) removed that platform, so Bazel failed to analyze `//test/test_data:visionos_binary`. Use `sim_arm64` for the default split. The binary test checks that the resulting Mach-O binary targets the visionOS simulator. Codex for Tamir. --- test/binary_tests.bzl | 10 ++++++++++ test/transitions.bzl | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/test/binary_tests.bzl b/test/binary_tests.bzl index c6548933..2f433390 100644 --- a/test/binary_tests.bzl +++ b/test/binary_tests.bzl @@ -78,6 +78,16 @@ def binary_test_suite(name): target_under_test = "//test/test_data:visionos_binary", ) + apple_verification_test( + name = "{}_visionos_default_simulator_test".format(name), + tags = [name], + build_type = "simulator", + cpus = {"visionos_cpus": ""}, + expected_platform_type = "visionos", + verifier_script = "//test/shell:verify_binary.sh", + target_under_test = "//test/test_data:visionos_binary", + ) + apple_verification_test( name = "{}_visionos_arm64_simulator_test".format(name), tags = [name], diff --git a/test/transitions.bzl b/test/transitions.bzl index 90df5a57..7570db88 100644 --- a/test/transitions.bzl +++ b/test/transitions.bzl @@ -14,7 +14,7 @@ _PLATFORM_TYPE_TO_DEFAULT_ARCH = { "ios": "x86_64", "macos": "x86_64", "tvos": "x86_64", - "visionos": "x86_64", + "visionos": "sim_arm64", "watchos": "x86_64", } From 94b7636cf5b75b92b5b8c6fc66538f39d48f158f Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 17:16:25 -0400 Subject: [PATCH 06/24] Use the visionOS platform type directly (#673) Bazel 7.4, the minimum supported release, exports the visionOS platform type. Use that member directly in the dynamic toolchain helpers, as the C++ toolchain configuration already does. --- toolchain/dynamic_toolchain_info.bzl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index ad5f799e..c6cbec54 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -13,7 +13,7 @@ def _sdk_version_for_platform(xcode_config, platform_type): return xcode_config.sdk_version_for_platform(apple_common.platform.ios_device) elif platform_type == apple_common.platform_type.tvos: return xcode_config.sdk_version_for_platform(apple_common.platform.tvos_device) - elif platform_type == getattr(apple_common.platform_type, "visionos", None): + elif platform_type == apple_common.platform_type.visionos: return xcode_config.sdk_version_for_platform(apple_common.platform.visionos_device) elif platform_type == apple_common.platform_type.watchos: return xcode_config.sdk_version_for_platform(apple_common.platform.watchos_device) @@ -27,9 +27,9 @@ def _sdk_name(platform_type, is_simulator): return "iPhoneSimulator" elif platform_type == apple_common.platform_type.ios: return "iPhoneOS" - elif platform_type == getattr(apple_common.platform_type, "visionos", None) and is_simulator: + elif platform_type == apple_common.platform_type.visionos and is_simulator: return "XRSimulator" - elif platform_type == getattr(apple_common.platform_type, "visionos", None): + elif platform_type == apple_common.platform_type.visionos: return "XROS" elif platform_type == apple_common.platform_type.watchos and is_simulator: return "WatchSimulator" @@ -72,7 +72,7 @@ def _dynamic_toolchain_info_impl(ctx): platform_type = apple_common.platform_type.watchos elif ctx.target_platform_has_constraint(ctx.attr._visionos[platform_common.ConstraintValueInfo]): triple_os = "xros" - platform_type = getattr(apple_common.platform_type, "visionos", None) + platform_type = apple_common.platform_type.visionos elif ctx.target_platform_has_constraint(ctx.attr._linux[platform_common.ConstraintValueInfo]): return [platform_common.TemplateVariableInfo({"ARCH": triple_arch})] else: From cd8c22b4da0c3f267612a2628dbc79488a8a2e2d Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 17:26:14 -0400 Subject: [PATCH 07/24] Call the selected genrule action API directly (#675) --- rules/private/apple_genrule.bzl | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/rules/private/apple_genrule.bzl b/rules/private/apple_genrule.bzl index 9de53418..30df3391 100644 --- a/rules/private/apple_genrule.bzl +++ b/rules/private/apple_genrule.bzl @@ -61,21 +61,15 @@ def _apple_genrule_impl(ctx): label_dict = {dep.label: dep[DefaultInfo].files.to_list() for dep in ctx.attr.srcs} execution_requirements = {} - extra_args = {} - run = ctx.actions.run + xcode_config = None + apple_platform_info = None if ctx.toolchains[_APPLE_GENRULE_TOOLCHAIN_TYPE] != None: xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] execution_requirements.update(xcode_config.execution_info()) - extra_args = { - "actions": ctx.actions, - "xcode_config": xcode_config, - "apple_fragment": ctx.fragments.apple, - "apple_platform_info": apple_support.platform_info_from_rule_ctx( - ctx, - fail_on_missing_constraint = False, - ), - } - run = apple_support.run + apple_platform_info = apple_support.platform_info_from_rule_ctx( + ctx, + fail_on_missing_constraint = False, + ) if ctx.attr.no_sandbox: execution_requirements["no-sandbox"] = "1" @@ -97,7 +91,7 @@ def _apple_genrule_impl(ctx): message = ctx.attr.message or "Executing apple_genrule" - run( + action_args = dict( executable = argv[0], arguments = argv[1:], inputs = depset(resolved_inputs + [ctx.file._genrule_setup], transitive = [resolved_srcs]), @@ -107,8 +101,17 @@ def _apple_genrule_impl(ctx): execution_requirements = execution_requirements, progress_message = "%s %s" % (message, ctx.label), mnemonic = "Genrule", - **extra_args ) + if xcode_config != None: + apple_support.run( + actions = ctx.actions, + xcode_config = xcode_config, + apple_fragment = ctx.fragments.apple, + apple_platform_info = apple_platform_info, + **action_args + ) + else: + ctx.actions.run(**action_args) return [ DefaultInfo( From 436e9b77195468341a3b2bf2df58dd07a829fa3e Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 17:42:40 -0400 Subject: [PATCH 08/24] Handle developer-directory Xcode versions in threshold conditions (#672) `XcodeVersionConfig.xcode_version()` can return a developer directory rather than a dotted version. Comparing that string to `minimum_xcode_version` aborts analysis with `unsupported comparison: string <=> DottedVersion`. Treat the directory as a recent Xcode, consistent with #666. A configuration with no version still fails the condition; numeric versions retain their existing comparison. --- doc/xcode_version_at_least.md | 3 ++- test/xcode_version_at_least_test.bzl | 24 +++++++++++++++++++++++- xcode/xcode_version_at_least.bzl | 11 +++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/doc/xcode_version_at_least.md b/doc/xcode_version_at_least.md index 5e106a23..01b39741 100755 --- a/doc/xcode_version_at_least.md +++ b/doc/xcode_version_at_least.md @@ -22,7 +22,8 @@ Creates a `config_setting` target that matches when Xcode is at least `minimum_x The condition compares the resolved Xcode version, including when Xcode is selected by default without an explicit `--xcode_version` flag. It does not -match if the resolved configuration has no Xcode version. +match if the resolved configuration has no Xcode version. A developer +directory is assumed to represent a recent Xcode and matches any minimum. For example, enable a definition with Xcode 26.4 or newer: diff --git a/test/xcode_version_at_least_test.bzl b/test/xcode_version_at_least_test.bzl index e3d5d572..2612a333 100644 --- a/test/xcode_version_at_least_test.bzl +++ b/test/xcode_version_at_least_test.bzl @@ -15,6 +15,7 @@ """Tests for the `xcode_version_at_least` macro and rule.""" load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_features//private:util.bzl", "ge") load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load( "//xcode:xcode_config.bzl", @@ -82,6 +83,8 @@ _select_xcode_26_4_0_test = _make_xcode_select_test("26.4.0.17E192") _select_xcode_26_4_1_test = _make_xcode_select_test("26.4.1.17E202") _select_xcode_27_0_beta_test = _make_xcode_select_test("27.0.0.27A5228h") _select_xcode_padded_26_test = _make_xcode_select_test("26") +_DEVELOPER_DIR_CONFIG = "xcode_version_at_least_test__developer_dir_config" +_select_xcode_developer_dir_test = _make_xcode_select_test("", config_name = _DEVELOPER_DIR_CONFIG) # ------------------------------------------------------------------------------ @@ -168,12 +171,31 @@ def _test_single_threshold(namer): expected_value = "matched", ) - return [ + tests = [ "single_threshold_below", "single_threshold_equal", "single_threshold_above_patch", "single_threshold_above_major_beta", ] + if ge("9.0.0"): + xcode_version( + name = namer("developer_dir"), + version = "/Applications/Xcode.app/Contents/Developer", + tags = FIXTURE_TAGS, + ) + xcode_config( + name = _DEVELOPER_DIR_CONFIG, + default = namer(":developer_dir"), + versions = [namer(":developer_dir")], + tags = FIXTURE_TAGS, + ) + _select_xcode_developer_dir_test( + name = "single_threshold_developer_dir", + target_under_test = namer(":single_holder"), + expected_value = "matched", + ) + tests.append("single_threshold_developer_dir") + return tests def _test_precisions_and_build_numbers(namer): cases = [ diff --git a/xcode/xcode_version_at_least.bzl b/xcode/xcode_version_at_least.bzl index e0b1fc33..e45f7379 100644 --- a/xcode/xcode_version_at_least.bzl +++ b/xcode/xcode_version_at_least.bzl @@ -105,7 +105,13 @@ def _xcode_version_at_least_flag_impl(ctx): ) xcode_config = ctx.attr._xcode_config[XcodeVersionInfo] xcode_version = xcode_config.xcode_version() - matches = bool(xcode_version and xcode_version >= minimum_xcode_version) + + # The provider returns a string only for a developer directory, which has + # no comparable version. Assume a recent Xcode. + matches = bool(xcode_version) and ( + type(xcode_version) == "string" or + xcode_version >= minimum_xcode_version + ) return [ config_common.FeatureFlagInfo(value = str(matches)), @@ -167,7 +173,8 @@ def xcode_version_at_least( The condition compares the resolved Xcode version, including when Xcode is selected by default without an explicit `--xcode_version` flag. It does not - match if the resolved configuration has no Xcode version. + match if the resolved configuration has no Xcode version. A developer + directory is assumed to represent a recent Xcode and matches any minimum. For example, enable a definition with Xcode 26.4 or newer: From 348b7967e09cb4370d3e041143e597e540660b08 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 17:43:51 -0400 Subject: [PATCH 09/24] Use the Xcode configuration resolver for execution info (#674) `xcode_execution_info` reads the Xcode configuration through the Apple fragment. With `--incompatible_remove_ctx_apple_fragment`, its late-bound attribute becomes `None` and provider lookup fails. Use `apple_support.action_required_attrs()` to resolve the configuration through `//xcode:version_config`, which selects the native or Starlark-backed configuration. Remove the rule's unused Apple fragment declaration. --- toolchain/dynamic_toolchain_info.bzl | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index c6cbec54..086cdfca 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -151,13 +151,5 @@ def _xcode_execution_info_impl(ctx): xcode_execution_info = rule( implementation = _xcode_execution_info_impl, - attrs = { - "_xcode_config": attr.label(default = configuration_field( - fragment = "apple", - name = "xcode_config_label", - )), - }, - fragments = [ - "apple", - ], + attrs = apple_support.action_required_attrs(), ) From 4a3d6e69daf76b810ff4f409cca7e3d07c12bd96 Mon Sep 17 00:00:00 2001 From: UebelAndre Date: Mon, 26 Oct 2026 14:45:28 -0700 Subject: [PATCH 10/24] Fix http_dmg's 7-Zip downloads on Windows (#671) The Windows installer entries were swapped: x64 hosts were falling back to the 32-bit installer after a checksum failure on the mirror, and 32-bit hosts would have received an x64 binary. The unversioned upstream `7zr.exe` has also already been replaced by 7-zip.org, so that fallback was dead. --- MODULE.bazel | 1 + tools/http_dmg/BUILD.bazel | 1 + tools/http_dmg/http_dmg.bzl | 500 +++++++----------- tools/http_dmg/private/BUILD.bazel | 8 + tools/http_dmg/private/tests/BUILD.bazel | 6 + .../http_dmg/private/tests/firefox_info.patch | 11 + .../private/tests/firefox_info_patched.plist | 275 ++++++++++ .../private/tests/http_dmg_test_deps.bzl | 15 + tools/http_dmg/private/zip7.bzl | 247 +++++++++ 9 files changed, 765 insertions(+), 299 deletions(-) create mode 100644 tools/http_dmg/private/tests/firefox_info.patch create mode 100644 tools/http_dmg/private/tests/firefox_info_patched.plist create mode 100644 tools/http_dmg/private/zip7.bzl diff --git a/MODULE.bazel b/MODULE.bazel index cc777249..0064e4c3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -55,6 +55,7 @@ dmg_arc_test_deps = use_extension("//tools/http_dmg/private/tests:http_dmg_test_ use_repo( dmg_arc_test_deps, "http_dmg_test_firefox", + "http_dmg_test_firefox_patched", "http_dmg_test_firefox_strip_prefix", "http_dmg_test_krita", "http_dmg_test_krita_strip_prefix", diff --git a/tools/http_dmg/BUILD.bazel b/tools/http_dmg/BUILD.bazel index e6df15b5..fad998ae 100644 --- a/tools/http_dmg/BUILD.bazel +++ b/tools/http_dmg/BUILD.bazel @@ -5,6 +5,7 @@ bzl_library( srcs = ["http_dmg.bzl"], visibility = ["//visibility:public"], deps = [ + "//tools/http_dmg/private:zip7", "@bazel_tools//tools/build_defs/repo:utils.bzl", ], ) diff --git a/tools/http_dmg/http_dmg.bzl b/tools/http_dmg/http_dmg.bzl index db10f752..aa50481d 100644 --- a/tools/http_dmg/http_dmg.bzl +++ b/tools/http_dmg/http_dmg.bzl @@ -3,345 +3,169 @@ load( "@bazel_tools//tools/build_defs/repo:utils.bzl", "get_auth", + "patch", + "update_attrs", +) +load( + "//tools/http_dmg/private:zip7.bzl", + "zip7_download", + "zip7_extract", ) -def _zip7_data( - *, - urls, - integrity, - bin_path = None): - return struct( - urls = urls, - integrity = integrity, - bin_path = bin_path, - ) - -_7ZIP_SOURCES = { - "linux_arm64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-arm64.tar.xz", - "https://www.7-zip.org/a/7z2501-linux-arm64.tar.xz", - ], - integrity = "sha256-OcUUDwLORDZZkwPFmhSfZUyxu8R83BBalCEg10euBA0=", - bin_path = "7zz", - ), - "linux_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-x64.tar.xz", - "https://www.7-zip.org/a/7z2501-linux-x64.tar.xz", - ], - integrity = "sha256-TKO3xvL2eGa5JiKBi1gjPccDZ74vNrSY6wveqqRLU/Q=", - bin_path = "7zz", - ), - "macos_arm64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", - "https://www.7-zip.org/a/7z2501-mac.tar.xz", - ], - integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", - bin_path = "7zz", - ), - "macos_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", - "https://www.7-zip.org/a/7z2501-mac.tar.xz", - ], - integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", - bin_path = "7zz", - ), - "windows_i686": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", - "https://www.7-zip.org/a/7zr.exe", - ], - integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", - bin_path = "7zr.exe", - ), - "windows_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", - "https://www.7-zip.org/a/7zr.exe", - ], - integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", - bin_path = "7zr.exe", - ), -} - -_7ZIP_EXTRAS = { - "windows_i686": _zip7_data( - urls = [ - "https://mirror.bazel.build/7-zip.org/a/7z2501.exe", - "https://7-zip.org/a/7z2501-x64.exe", - ], - integrity = "sha256-eK+iocdzyvPPft9i+FfSqKXaVfsP/12kFgdMDSiytV8=", - bin_path = "7z.exe", - ), - "windows_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/7-zip.org/a/7z2501-x64.exe", - "https://7-zip.org/a/7z2501.exe", - ], - integrity = "sha256-uWgx7sWSg4TwVD1rV8H4ApUqDyZo5mKILAp4WitS+zs=", - bin_path = "7z.exe", - ), -} - -def _host_arch(repository_ctx): - arch = repository_ctx.os.arch - if arch == "amd64": - arch = "x86_64" - elif arch == "aarch64": - arch = "arm64" - - if "linux" in repository_ctx.os.name: - return "linux_{}".format(arch) - - if "mac" in repository_ctx.os.name: - return "macos_{}".format(arch) - - if "win" in repository_ctx.os.name: - return "windows_{}".format(arch) - - fail("Unknown host OS: {}".format(repository_ctx.os.name)) - -_DMG_IGNORES_PATTERNS = { - "name": ( - "Applications", - ), - "prefix": ( - ".DS_Store", - ".background", - ".VolumeIcon", - ), - "suffix": ( - "[]", - "[HFS+ Private Data]", - ".fseventsd", - ), -} - -def _7z_readdir(path, is_dmg): - """Read the contents of a directory, ignoring certain patterns for dmg archives. - - Args: - path (Path): The directory Path object - is_dmg (bool): Whether or not the directory is from a `.dmg` archive. +# Entries which are artifacts of the disk image rather than its payload. Each +# entry is excluded both at the root of the image and directly within the +# volume directory 7-Zip creates for the image. +_DMG_IGNORED_ENTRIES = [ + # Finder metadata. + ".DS_Store", + ".background", + ".VolumeIcon*", + ".fseventsd", + ".Trashes", + # The conventional `Applications -> /Applications` drag target. 7-Zip + # names symlinks with an empty name `[]`. + "Applications", + "[]", + # HFS+ bookkeeping. + "[HFS+ Private Data]", +] + +def _dmg_7z_args(): + """Arguments passed to 7-Zip when extracting a `.dmg` archive. Returns: - list: A list of Path. + list: A list of strings. """ - if not is_dmg: - return path.readdir() - - entries = [] - - for item in path.readdir(): - if item.basename.endswith(_DMG_IGNORES_PATTERNS["suffix"]): - continue - if item.basename.startswith(_DMG_IGNORES_PATTERNS["prefix"]): - continue - if item.basename in _DMG_IGNORES_PATTERNS["name"]: - continue - - # 7zip will write attributes using `:` delimiters. - if ":" in item.basename: - continue - - entries.append(item) - - return entries + args = [ + # Do not extract HFS+ extended attributes, which 7-Zip would otherwise + # write as `:` files beside each file. + "-sns-", + # Extract all symbolic links, including links to links and links which + # point outside of the extraction directory. These are common in `.dmg` + # files (e.g. `Applications -> /Applications`). The archive is + # checksum pinned so this is safe for the archives a user opts into. + "-snld20", + ] + + # 7-Zip wildcards do not match across `/`, so `*/` only matches an + # entry directly within the volume directory. + for entry in _DMG_IGNORED_ENTRIES: + args.append("-x!{}".format(entry)) + args.append("-x!*/{}".format(entry)) + + return args def _move(repository_ctx, src, dst): - if not hasattr(repository_ctx, "rename"): - program = "mv" - if "win" in repository_ctx.os.name: - program = "move" - result = repository_ctx.execute([program, src, dst]) - if result.return_code: - fail("mv failed with {}\nstdout:\n{}\nstderr:\n{}".format( - result.return_code, - result.stdout, - result.stderr, - )) + # TODO: Delete the fallback once Bazel 7.x is no longer supported. + # `repository_ctx.rename` was added in Bazel 8.0. + if hasattr(repository_ctx, "rename"): + repository_ctx.rename(src, dst) return - repository_ctx.rename(src, dst) + # Unlike `rename`, `mv`/`move` do not create parent directories. Writing + # and deleting a placeholder file creates them without any host tools. + parent = dst.dirname + if not parent.exists: + placeholder = parent.get_child(".http_dmg_mkdir") + repository_ctx.file(placeholder, content = "", executable = False) + repository_ctx.delete(placeholder) -def _extract_7z( + if "win" in repository_ctx.os.name: + # `move` is a `cmd.exe` builtin rather than an executable. + command = ["cmd", "/c", "move", src, dst] + else: + command = ["mv", src, dst] + + result = repository_ctx.execute(command) + if result.return_code: + fail("{} failed with {}\nstdout:\n{}\nstderr:\n{}".format( + " ".join([str(a) for a in command]), + result.return_code, + result.stdout, + result.stderr, + )) + +def _extract_dmg( *, repository_ctx, - z7_bin, + zip7_bin, archive, - output = None, - strip_prefix = None): - """Extract a 7zip archive to the repository directory. + strip_prefix = None, + add_prefix = None): + """Extract a `.dmg` archive into the repository directory. Args: repository_ctx (repository_ctx): The rule's repository context. - z7_bin (path): The path to a 7zip binary. - archive (path): path to the archive that will be unpacked, relative to the repository directory. - output (str, optional): path to the directory where the archive will be unpacked, relative to the repository directory. + zip7_bin (path): The path to a 7-Zip binary. + archive (path): The `.dmg` archive to extract. strip_prefix (str, optional): a directory prefix to strip from the extracted files. + add_prefix (str, optional): a directory, relative to the repository, to extract into. """ - if not output: - output = "." - - out_dir = repository_ctx.path(output) - temp_out_dir = repository_ctx.path("{}/_7z_out".format(out_dir)) - - command = [z7_bin, "x", archive, "-snld20", "-o{}".format(temp_out_dir), "-y"] - result = repository_ctx.execute(command) - if result.return_code != 0: - fail("7z command failed with exit code {}\n{}\n\nstdout:\n{}\nstderr:\n{}".format( - result.return_code, - " ".join([str(a) for a in command]), - result.stdout, - result.stderr, - )) + temp_out_dir = repository_ctx.path("_7z_out") - is_dmg = str(archive).endswith((".dmg", ".DMG")) + zip7_extract( + repository_ctx = repository_ctx, + zip7_bin = zip7_bin, + archive = archive, + output = temp_out_dir, + extra_args = _dmg_7z_args(), + ) + # 7-Zip places the contents of the image within a directory named after + # the volume. Treat that directory as the root of the archive. target_dir = temp_out_dir - if is_dmg: - entries = temp_out_dir.readdir() - if len(entries) == 1 and entries[0].is_dir: - target_dir = entries[0] + entries = temp_out_dir.readdir() + if len(entries) == 1 and entries[0].is_dir: + target_dir = entries[0] - # Check to see if any prefixes can be stripped if strip_prefix: stripped_dir = target_dir.get_child(strip_prefix) if not stripped_dir.exists: fail("Prefix \"{}\" was given, but not found in the archive. Here are possible prefixes for this archive: {}".format( strip_prefix, - [p.basename for p in _7z_readdir(target_dir, is_dmg)], + [p.basename for p in target_dir.readdir()], )) target_dir = stripped_dir - # Move the extracted contents to the root of the directory, leaving known bad files. - for item in _7z_readdir(target_dir, is_dmg): - _move(repository_ctx, item, repository_ctx.path("{}/{}".format(output, item.basename))) + dest_dir = repository_ctx.path(add_prefix or ".") + for item in target_dir.readdir(): + _move(repository_ctx, item, dest_dir.get_child(item.basename)) repository_ctx.delete(temp_out_dir) -def _urls_have_suffix(urls, suffix): - for url in urls: - if url.endswith(suffix): - return True - return False - -def _download_and_extract_7zip_binary(repository_ctx, host_arch): - """Fetch and extract a 7zip archive - - 7z is a cross platform way to extract dmg file. - - Args: - repository_ctx (repository_ctx): The rule's context object. - host_arch (str): An identifier for the host os and architecture. - - Returns: - tuple: - - path: The directory in which 7zip was downloaded to. - - path: The path to the 7zip binary. - """ - zip_data = _7ZIP_SOURCES[host_arch] - - auth = get_auth(repository_ctx, zip_data.urls) - - zip_dir = repository_ctx.path("7z") - zip_exe = repository_ctx.path("{}/{}".format(zip_dir, zip_data.bin_path)) - - if _urls_have_suffix(zip_data.urls, ".exe"): - repository_ctx.download( - url = zip_data.urls, - output = zip_exe, - integrity = zip_data.integrity, - executable = True, - auth = auth, - ) - else: - repository_ctx.download_and_extract( - url = zip_data.urls, - integrity = zip_data.integrity, - output = zip_dir, - auth = auth, - ) - - extras = _7ZIP_EXTRAS.get(host_arch) - if extras: - extras_output = repository_ctx.path("{}/{}".format( - zip_dir, - "extras", - )) - - if _urls_have_suffix(extras.urls, ".exe"): - urls_suffix = "exe" - elif _urls_have_suffix(extras.urls, ".msi"): - urls_suffix = "msi" - elif _urls_have_suffix(extras.urls, ".7z"): - urls_suffix = "7z" - else: - urls_suffix = None - - if urls_suffix: - extras_archive = repository_ctx.path("{}/{}".format( - zip_dir, - "extras.{}".format(urls_suffix), - )) - repository_ctx.download( - url = extras.urls, - output = extras_archive, - integrity = extras.integrity, - auth = auth, - ) - _extract_7z( - repository_ctx = repository_ctx, - z7_bin = zip_exe, - archive = extras_archive, - output = extras_output, - ) - else: - repository_ctx.download_and_extract( - url = extras.urls, - integrity = extras.integrity, - output = extras_output, - auth = auth, - ) - - # Allow extras to update the binary to use. - if extras.bin_path: - zip_exe = repository_ctx.path("{}/{}".format(extras_output, extras.bin_path)) - - if not zip_exe.exists: - fail("Failed to locate 7z binary from: {}".format(zip_data.urls)) - - return zip_dir, zip_exe - def _http_dmg_impl(repository_ctx): if repository_ctx.attr.build_file and repository_ctx.attr.build_file_content: fail("Only one of build_file and build_file_content can be provided.") - repository_ctx.report_progress("Downloading 7z.") - host_arch = _host_arch(repository_ctx) - z7_dir, z7_bin = _download_and_extract_7zip_binary(repository_ctx, host_arch) + source_urls = _get_source_urls(repository_ctx) + + zip7_dir = "7z" + zip7_bin = zip7_download(repository_ctx, zip7_dir) dmg = repository_ctx.path("dmg.dmg") repository_ctx.report_progress("Downloading dmg.") download_results = repository_ctx.download( - url = repository_ctx.attr.urls, + url = source_urls, output = dmg, integrity = repository_ctx.attr.integrity, sha256 = repository_ctx.attr.sha256, - auth = get_auth(repository_ctx, repository_ctx.attr.urls), + canonical_id = repository_ctx.attr.canonical_id, + auth = get_auth(repository_ctx, source_urls), ) repository_ctx.report_progress("Extracting dmg.") - _extract_7z( + _extract_dmg( repository_ctx = repository_ctx, - z7_bin = z7_bin, + zip7_bin = zip7_bin, archive = dmg, strip_prefix = repository_ctx.attr.strip_prefix, + add_prefix = repository_ctx.attr.add_prefix, ) + # Delete dmg remnants + repository_ctx.delete(dmg) + repository_ctx.delete(zip7_dir) + build_file_content = repository_ctx.attr.build_file_content if repository_ctx.attr.build_file: build_file_content = repository_ctx.read(repository_ctx.path(repository_ctx.attr.build_file)) @@ -350,24 +174,48 @@ def _http_dmg_impl(repository_ctx): # TODO: Delete once we drop 8.x repository_ctx.file("MODULE.bazel", content = """module(name = "{}")""".format(repository_ctx.name)) - # Delete dmg remnants - repository_ctx.delete(dmg) - repository_ctx.delete(z7_dir) + # `patch_strip` is only applied by the `patch` helper from Bazel 8.0, so it + # is folded into `patch_args` here. On newer versions this yields a repeated + # `-pN`, of which the last one wins. + patch( + repository_ctx, + patch_args = ["-p{}".format(repository_ctx.attr.patch_strip)] + repository_ctx.attr.patch_args, + ) # Return reproducibility attributes - repro_attrs = { - k: getattr(repository_ctx.attr, k) - for k in _ATTRS.keys() - } - - repro_attrs["name"] = repository_ctx.attr.name - + overrides = {} if not repository_ctx.attr.sha256: - repro_attrs["integrity"] = download_results.integrity + overrides["integrity"] = download_results.integrity + + repro_attrs = update_attrs(repository_ctx.attr, _ATTRS.keys(), overrides) + # TODO: Delete the fallback once we drop 7.x + if hasattr(repository_ctx, "repo_metadata"): + return repository_ctx.repo_metadata(attrs_for_reproducibility = repro_attrs) return repro_attrs +def _get_source_urls(repository_ctx): + """Returns the urls provided via the `url` and `urls` attributes.""" + if not repository_ctx.attr.url and not repository_ctx.attr.urls: + fail("At least one of url and urls must be provided") + + source_urls = [] + if repository_ctx.attr.urls: + source_urls = repository_ctx.attr.urls + if repository_ctx.attr.url: + source_urls = [repository_ctx.attr.url] + source_urls + return source_urls + _ATTRS = { + "add_prefix": attr.string( + default = "", + doc = """Destination directory relative to the repository directory. + +The archive will be unpacked into this directory, after applying `strip_prefix` +(if any) to the file paths within the archive. For example, file +`foo-1.2.3/src/foo.h` will be unpacked to `bar/src/foo.h` if `add_prefix = "bar"` +and `strip_prefix = "foo-1.2.3"`.""", + ), "auth_patterns": attr.string_dict( doc = """An optional dict mapping host names to custom authorization patterns. @@ -419,6 +267,13 @@ Authorization: Bearer RANDOM-TOKEN "Either build_file or build_file_content can be specified, but " + "not both.", ), + "canonical_id": attr.string( + doc = """A canonical ID of the file downloaded. + +If specified and non-empty, Bazel will not take the file from cache, unless it +was added to the cache by a request with the same canonical ID. +""", + ), "integrity": attr.string( doc = """Expected checksum in Subresource Integrity format of the file downloaded. @@ -430,6 +285,45 @@ easier but either this attribute or `sha256` should be set before shipping.""", "netrc": attr.string( doc = "Location of the .netrc file to use for authentication", ), + "patch_args": attr.string_list( + default = [], + doc = + "The arguments given to the patch tool. Defaults to -p0 (see the `patch_strip` " + + "attribute), however -p1 will usually be needed for patches generated by " + + "git. If multiple -p arguments are specified, the last one will take effect." + + "If arguments other than -p are specified, Bazel will fall back to use patch " + + "command line tool instead of the Bazel-native patch implementation. When falling " + + "back to patch command line tool and patch_tool attribute is not specified, " + + "`patch` will be used. This only affects patch files in the `patches` attribute.", + ), + "patch_cmds": attr.string_list( + default = [], + doc = "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", + ), + "patch_cmds_win": attr.string_list( + default = [], + doc = "Sequence of Powershell commands to be applied on Windows after patches are " + + "applied. If this attribute is not set, patch_cmds will be executed on Windows, " + + "which requires Bash binary to exist.", + ), + "patch_strip": attr.int( + default = 0, + doc = "When set to `N`, this is equivalent to inserting `-pN` to the beginning of `patch_args`.", + ), + "patch_tool": attr.string( + default = "", + doc = "The patch(1) utility to use. If this is specified, Bazel will use the specified " + + "patch tool instead of the Bazel-native patch implementation.", + ), + "patches": attr.label_list( + default = [], + doc = + "A list of files that are to be applied as patches after " + + "extracting the archive. By default, it uses the Bazel-native patch implementation " + + "which doesn't support binary patch, but Bazel will fall back to use " + + "patch command line tool if `patch_tool` attribute is specified or there are " + + "arguments other than `-p` in `patch_args` attribute.", + ), "sha256": attr.string( doc = """The expected SHA-256 of the file downloaded. @@ -457,6 +351,15 @@ discarded and inaccessible (e.g., a top-level license file). This includes files/directories that start with the prefix but are not in the directory (e.g., `foo-lib-1.2.3.release-notes`). If the specified prefix does not match a directory in the archive, Bazel will return an error.""", + ), + "url": attr.string( + doc = """A URL to a file that will be made available to Bazel. + +This must be a file, http or https URL. Redirections are followed. +Authentication is not supported. + +More flexibility can be achieved by the urls parameter that allows +to specify alternative URLs to fetch from.""", ), "urls": attr.string_list( doc = """A list of URLs to a file that will be made available to Bazel. @@ -466,7 +369,6 @@ Authentication is not supported. URLs are tried in order until one succeeds, so you should list local mirrors first. If all downloads fail, the rule will fail.""", - mandatory = True, ), } diff --git a/tools/http_dmg/private/BUILD.bazel b/tools/http_dmg/private/BUILD.bazel index e69de29b..93c64801 100644 --- a/tools/http_dmg/private/BUILD.bazel +++ b/tools/http_dmg/private/BUILD.bazel @@ -0,0 +1,8 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "zip7", + srcs = ["zip7.bzl"], + visibility = ["//visibility:public"], + deps = ["@bazel_tools//tools/build_defs/repo:utils.bzl"], +) diff --git a/tools/http_dmg/private/tests/BUILD.bazel b/tools/http_dmg/private/tests/BUILD.bazel index dea6f249..9030b400 100644 --- a/tools/http_dmg/private/tests/BUILD.bazel +++ b/tools/http_dmg/private/tests/BUILD.bazel @@ -6,6 +6,12 @@ diff_test( file2 = "firefox_info.plist", ) +diff_test( + name = "firefox_patched_diff_test", + file1 = "@http_dmg_test_firefox_patched//:info_plist", + file2 = "firefox_info_patched.plist", +) + diff_test( name = "firefox_strip_prefix_diff_test", file1 = "@http_dmg_test_firefox_strip_prefix//:info_plist", diff --git a/tools/http_dmg/private/tests/firefox_info.patch b/tools/http_dmg/private/tests/firefox_info.patch new file mode 100644 index 00000000..401de015 --- /dev/null +++ b/tools/http_dmg/private/tests/firefox_info.patch @@ -0,0 +1,11 @@ +--- a/firefox/Contents/Info.plist ++++ b/firefox/Contents/Info.plist +@@ -199,7 +199,7 @@ + CFBundlePackageType + APPL + CFBundleShortVersionString +- 141.0.3 ++ 141.0.3-patched + CFBundleSignature + MOZB + CFBundleURLTypes diff --git a/tools/http_dmg/private/tests/firefox_info_patched.plist b/tools/http_dmg/private/tests/firefox_info_patched.plist new file mode 100644 index 00000000..c30cecba --- /dev/null +++ b/tools/http_dmg/private/tests/firefox_info_patched.plist @@ -0,0 +1,275 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + html + htm + shtml + xht + xhtml + + CFBundleTypeIconFile + document.icns + CFBundleTypeName + HTML Document + CFBundleTypeOSTypes + + HTML + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + json + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + application/json + + CFBundleTypeName + JSON File + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + pdf + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + application/pdf + + CFBundleTypeName + PDF document + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + svg + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + image/svg+xml + + CFBundleTypeName + SVG document + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + NSDocumentClass + BrowserDocument + + + CFBundleTypeExtensions + + text + txt + js + log + css + xul + rdf + + CFBundleTypeIconFile + document.icns + CFBundleTypeName + Text Document + CFBundleTypeOSTypes + + TEXT + utxt + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + jpeg + jpg + png + gif + + CFBundleTypeIconFile + fileBookmark.icns + CFBundleTypeName + document.icns + CFBundleTypeOSTypes + + GIFf + JPEG + PNGf + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + oga + ogg + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + audio/ogg + + CFBundleTypeName + HTML5 Audio (Ogg) + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + ogv + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + video/ogg + + CFBundleTypeName + HTML5 Video (Ogg) + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + webm + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + video/webm + + CFBundleTypeName + HTML5 Video (WebM) + CFBundleTypeRole + Viewer + + + CFBundleExecutable + firefox + CFBundleGetInfoString + Firefox 141.0.3 + CFBundleIconFile + firefox.icns + CFBundleIdentifier + org.mozilla.firefox + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Firefox + CFBundlePackageType + APPL + CFBundleShortVersionString + 141.0.3-patched + CFBundleSignature + MOZB + CFBundleURLTypes + + + CFBundleURLIconFile + document.icns + CFBundleURLName + http URL + CFBundleURLSchemes + + http + + + + CFBundleURLIconFile + document.icns + CFBundleURLName + https URL + CFBundleURLSchemes + + https + + + + CFBundleURLName + file URL + CFBundleURLSchemes + + file + + + + CFBundleVersion + 14125.8.6 + NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + + NSAppleScriptEnabled + + LSApplicationCategoryType + public.app-category.productivity + LSEnvironment + + MallocNanoZone + 0 + SYSTEM_VERSION_COMPAT + 0 + + LSFileQuarantineEnabled + + LSMinimumSystemVersion + 10.15.0 + NSSupportsAutomaticGraphicsSwitching + + NSRequiresAquaSystemAppearance + + NSPrincipalClass + GeckoNSApplication + SMPrivilegedExecutables + + org.mozilla.updater + identifier "org.mozilla.updater" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96" + + NSCameraUsageDescription + Only sites you allow within Firefox will be able to use the camera. + NSMicrophoneUsageDescription + Only sites you allow within Firefox will be able to use the microphone. + NSCameraReactionEffectGesturesEnabledDefault + + + diff --git a/tools/http_dmg/private/tests/http_dmg_test_deps.bzl b/tools/http_dmg/private/tests/http_dmg_test_deps.bzl index 18122b3a..6a9471a6 100644 --- a/tools/http_dmg/private/tests/http_dmg_test_deps.bzl +++ b/tools/http_dmg/private/tests/http_dmg_test_deps.bzl @@ -38,6 +38,20 @@ def http_dmg_test_deps(): ), ) + http_dmg( + name = "http_dmg_test_firefox_patched", + # Explicitly test `url`. + url = "https://ftp.mozilla.org/pub/firefox/releases/141.0.3/mac/en-US/Firefox%20141.0.3.dmg", + integrity = "sha256-u5Is2mkFQ73aofvDs8ulCMYHdIMmQ0UrwmZZUzH0LbE=", + strip_prefix = "Firefox.app", + add_prefix = "firefox", + patches = [Label("//tools/http_dmg/private/tests:firefox_info.patch")], + patch_strip = 1, + build_file_content = _BUILD_FILE_CONTENT.format( + file = "firefox/Contents/Info.plist", + ), + ) + http_dmg( name = "http_dmg_test_krita", urls = [ @@ -65,6 +79,7 @@ def http_dmg_test_deps(): return [ "http_dmg_test_firefox", + "http_dmg_test_firefox_patched", "http_dmg_test_firefox_strip_prefix", "http_dmg_test_krita", "http_dmg_test_krita_strip_prefix", diff --git a/tools/http_dmg/private/zip7.bzl b/tools/http_dmg/private/zip7.bzl new file mode 100644 index 00000000..5bcde615 --- /dev/null +++ b/tools/http_dmg/private/zip7.bzl @@ -0,0 +1,247 @@ +"""Helpers for downloading a hermetic copy of 7-Zip within a repository rule. + +7-Zip is the only cross-platform tool that ships prebuilt binaries for Linux, +macOS, and Windows and can read `.dmg` images (UDIF containers wrapping HFS+ or +APFS filesystems, including LZFSE compressed chunks). +""" + +load( + "@bazel_tools//tools/build_defs/repo:utils.bzl", + "get_auth", +) + +def _zip7_data( + *, + urls, + integrity, + bin_path): + return struct( + urls = urls, + integrity = integrity, + bin_path = bin_path, + ) + +# Note: `7zzs` is the statically linked Linux build, `7zz` is dynamically +# linked against glibc. Both ship in the same tarball. +_LINUX_ARM64 = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-arm64.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-linux-arm64.tar.xz", + "https://www.7-zip.org/a/7z2501-linux-arm64.tar.xz", + ], + integrity = "sha256-OcUUDwLORDZZkwPFmhSfZUyxu8R83BBalCEg10euBA0=", + bin_path = "7zzs", +) + +_LINUX_X86_64 = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-x64.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-linux-x64.tar.xz", + "https://www.7-zip.org/a/7z2501-linux-x64.tar.xz", + ], + integrity = "sha256-TKO3xvL2eGa5JiKBi1gjPccDZ74vNrSY6wveqqRLU/Q=", + bin_path = "7zzs", +) + +# The macOS archive contains a universal binary. +_MACOS = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-mac.tar.xz", + "https://www.7-zip.org/a/7z2501-mac.tar.xz", + ], + integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", + bin_path = "7zz", +) + +# `7zr.exe` is a reduced 32-bit console build that only understands `.7z` +# archives (and 7-Zip's own SFX installers). It is used purely to bootstrap the +# full `7z.exe` + `7z.dll` from the installer below. 7-zip.org serves it +# unversioned and has already replaced it since this pin was taken, so only +# versioned URLs are listed here. +_WINDOWS_BOOTSTRAP = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7zr.exe", + ], + integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", + bin_path = "7zr.exe", +) + +ZIP7_SOURCES = { + "linux_arm64": _LINUX_ARM64, + "linux_x86_64": _LINUX_X86_64, + "macos_arm64": _MACOS, + "macos_x86_64": _MACOS, + "windows_arm64": _WINDOWS_BOOTSTRAP, + "windows_i686": _WINDOWS_BOOTSTRAP, + "windows_x86_64": _WINDOWS_BOOTSTRAP, +} + +# Secondary archives which are extracted using the binary from `ZIP7_SOURCES`. +# On Windows these are the official installers, which are 7-Zip SFX archives +# containing the full `7z.exe` and `7z.dll` for the host architecture. +ZIP7_EXTRAS = { + "windows_arm64": _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-arm64.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-arm64.exe", + "https://www.7-zip.org/a/7z2501-arm64.exe", + ], + integrity = "sha256-Y2XHxE4he5wQCeBl2vn5qjdFTmQxW0qqJj9/jwYHVdw=", + bin_path = "7z.exe", + ), + "windows_i686": _zip7_data( + urls = [ + "https://mirror.bazel.build/7-zip.org/a/7z2501.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501.exe", + "https://www.7-zip.org/a/7z2501.exe", + ], + integrity = "sha256-uWgx7sWSg4TwVD1rV8H4ApUqDyZo5mKILAp4WitS+zs=", + bin_path = "7z.exe", + ), + "windows_x86_64": _zip7_data( + urls = [ + "https://mirror.bazel.build/7-zip.org/a/7z2501-x64.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-x64.exe", + "https://www.7-zip.org/a/7z2501-x64.exe", + ], + integrity = "sha256-eK+iocdzyvPPft9i+FfSqKXaVfsP/12kFgdMDSiytV8=", + bin_path = "7z.exe", + ), +} + +def zip7_host_arch(ctx): + """Identify the host platform. + + Args: + ctx (repository_ctx): The rule's repository context. + + Returns: + str: A key of `ZIP7_SOURCES`. + """ + arch = ctx.os.arch + if arch == "amd64": + arch = "x86_64" + elif arch == "aarch64": + arch = "arm64" + elif arch == "x86": + arch = "i686" + + if "linux" in ctx.os.name: + host = "linux_{}".format(arch) + elif "mac" in ctx.os.name: + host = "macos_{}".format(arch) + elif "win" in ctx.os.name: + host = "windows_{}".format(arch) + else: + fail("Unknown host OS: {}".format(ctx.os.name)) + + if host not in ZIP7_SOURCES: + fail("7-Zip is not available for host `{}`. Supported hosts: {}".format( + host, + sorted(ZIP7_SOURCES.keys()), + )) + + return host + +def zip7_extract( + *, + repository_ctx, + zip7_bin, + archive, + output, + extra_args = []): + """Extract an archive using 7-Zip. + + Args: + repository_ctx (repository_ctx): The rule's repository context. + zip7_bin (path): The path to a 7-Zip binary. + archive (path): The archive to extract. + output (path): The directory to extract into. + extra_args (list): Additional arguments to pass to 7-Zip. + """ + command = [zip7_bin, "x", archive, "-o{}".format(output), "-y"] + extra_args + result = repository_ctx.execute(command) + if result.return_code != 0: + fail("7z command failed with exit code {}\n{}\n\nstdout:\n{}\nstderr:\n{}".format( + result.return_code, + " ".join([str(a) for a in command]), + result.stdout, + result.stderr, + )) + +_BAZEL_ARCHIVE_SUFFIXES = ( + ".tar.bz2", + ".tar.gz", + ".tar.xz", + ".tar.zst", + ".tar", + ".tbz", + ".tgz", + ".txz", + ".tzst", + ".zip", +) + +def zip7_download(repository_ctx, output): + """Download a pinned 7-Zip binary for the host platform. + + Args: + repository_ctx (repository_ctx): The rule's repository context. + output (str): The directory, relative to the repository, to download into. + + Returns: + path: The path to the 7-Zip binary. + """ + host = zip7_host_arch(repository_ctx) + source = ZIP7_SOURCES[host] + extras = ZIP7_EXTRAS.get(host) + + repository_ctx.report_progress("Downloading 7-Zip.") + output_dir = repository_ctx.path(output) + bin_path = output_dir.get_child(source.bin_path) + if source.urls[0].endswith(_BAZEL_ARCHIVE_SUFFIXES): + repository_ctx.download_and_extract( + url = source.urls, + output = output_dir, + integrity = source.integrity, + auth = get_auth(repository_ctx, source.urls), + ) + else: + repository_ctx.download( + url = source.urls, + output = bin_path, + integrity = source.integrity, + executable = True, + auth = get_auth(repository_ctx, source.urls), + ) + + if not bin_path.exists: + fail("Failed to locate the 7-Zip binary `{}` from: {}".format(source.bin_path, source.urls)) + + if not extras: + return bin_path + + repository_ctx.report_progress("Downloading 7-Zip extras.") + extras_archive = output_dir.get_child("extras_archive") + extras_dir = output_dir.get_child("extras") + repository_ctx.download( + url = extras.urls, + output = extras_archive, + integrity = extras.integrity, + auth = get_auth(repository_ctx, extras.urls), + ) + zip7_extract( + repository_ctx = repository_ctx, + zip7_bin = bin_path, + archive = extras_archive, + output = extras_dir, + ) + repository_ctx.delete(extras_archive) + + extras_bin_path = extras_dir.get_child(extras.bin_path) + if not extras_bin_path.exists: + fail("Failed to locate the 7-Zip binary `{}` from: {}".format(extras.bin_path, extras.urls)) + + return extras_bin_path From b11f5c451b70b160a7f93153f456b66547e0330f Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Mon, 26 Oct 2026 15:29:25 -0700 Subject: [PATCH 11/24] Fix PLATFORM_FRAMEWORKS without transition (#680) This was working under rules from rules_apple, but this is more correct and there was already a helper for it. Closes https://github.com/bazelbuild/apple_support/pull/676 --- test/test_data/toolchain_configs/ios_arm64.json | 2 +- test/test_data/toolchain_configs/ios_arm64e.json | 2 +- test/test_data/toolchain_configs/ios_sim_arm64.json | 2 +- test/test_data/toolchain_configs/ios_x86_64.json | 2 +- test/test_data/toolchain_configs/tvos_arm64.json | 2 +- test/test_data/toolchain_configs/tvos_sim_arm64.json | 2 +- test/test_data/toolchain_configs/tvos_x86_64.json | 2 +- test/test_data/toolchain_configs/visionos_arm64.json | 2 +- test/test_data/toolchain_configs/visionos_sim_arm64.json | 2 +- test/test_data/toolchain_configs/watchos_arm64.json | 2 +- test/test_data/toolchain_configs/watchos_arm64_32.json | 2 +- test/test_data/toolchain_configs/watchos_device_arm64.json | 2 +- test/test_data/toolchain_configs/watchos_device_arm64e.json | 2 +- test/test_data/toolchain_configs/watchos_x86_64.json | 2 +- toolchain/dynamic_toolchain_info.bzl | 6 ++++-- 15 files changed, 18 insertions(+), 16 deletions(-) diff --git a/test/test_data/toolchain_configs/ios_arm64.json b/test/test_data/toolchain_configs/ios_arm64.json index 40c61613..bd4de0d6 100644 --- a/test/test_data/toolchain_configs/ios_arm64.json +++ b/test/test_data/toolchain_configs/ios_arm64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_arm64e.json b/test/test_data/toolchain_configs/ios_arm64e.json index b529154e..2626cce3 100644 --- a/test/test_data/toolchain_configs/ios_arm64e.json +++ b/test/test_data/toolchain_configs/ios_arm64e.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_sim_arm64.json b/test/test_data/toolchain_configs/ios_sim_arm64.json index 34f52c3b..00d408ba 100644 --- a/test/test_data/toolchain_configs/ios_sim_arm64.json +++ b/test/test_data/toolchain_configs/ios_sim_arm64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_x86_64.json b/test/test_data/toolchain_configs/ios_x86_64.json index c40541fd..40b5384d 100644 --- a/test/test_data/toolchain_configs/ios_x86_64.json +++ b/test/test_data/toolchain_configs/ios_x86_64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_arm64.json b/test/test_data/toolchain_configs/tvos_arm64.json index b0de21be..7ef1fe2e 100644 --- a/test/test_data/toolchain_configs/tvos_arm64.json +++ b/test/test_data/toolchain_configs/tvos_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_sim_arm64.json b/test/test_data/toolchain_configs/tvos_sim_arm64.json index 5016256c..8542a23e 100644 --- a/test/test_data/toolchain_configs/tvos_sim_arm64.json +++ b/test/test_data/toolchain_configs/tvos_sim_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_x86_64.json b/test/test_data/toolchain_configs/tvos_x86_64.json index b528f3e5..a4be2220 100644 --- a/test/test_data/toolchain_configs/tvos_x86_64.json +++ b/test/test_data/toolchain_configs/tvos_x86_64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/visionos_arm64.json b/test/test_data/toolchain_configs/visionos_arm64.json index fe0bf69f..72a1ffc1 100644 --- a/test/test_data/toolchain_configs/visionos_arm64.json +++ b/test/test_data/toolchain_configs/visionos_arm64.json @@ -3685,7 +3685,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/XROS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/visionos_sim_arm64.json b/test/test_data/toolchain_configs/visionos_sim_arm64.json index 7b83d47e..bc3bef67 100644 --- a/test/test_data/toolchain_configs/visionos_sim_arm64.json +++ b/test/test_data/toolchain_configs/visionos_sim_arm64.json @@ -3685,7 +3685,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/XRSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_arm64.json b/test/test_data/toolchain_configs/watchos_arm64.json index 8e18c326..1413e22f 100644 --- a/test/test_data/toolchain_configs/watchos_arm64.json +++ b/test/test_data/toolchain_configs/watchos_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_arm64_32.json b/test/test_data/toolchain_configs/watchos_arm64_32.json index e2be2a0a..8c09150f 100644 --- a/test/test_data/toolchain_configs/watchos_arm64_32.json +++ b/test/test_data/toolchain_configs/watchos_arm64_32.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_device_arm64.json b/test/test_data/toolchain_configs/watchos_device_arm64.json index 36e5d3f0..7cc0057e 100644 --- a/test/test_data/toolchain_configs/watchos_device_arm64.json +++ b/test/test_data/toolchain_configs/watchos_device_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_device_arm64e.json b/test/test_data/toolchain_configs/watchos_device_arm64e.json index a71080dc..fa34d35f 100644 --- a/test/test_data/toolchain_configs/watchos_device_arm64e.json +++ b/test/test_data/toolchain_configs/watchos_device_arm64e.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_x86_64.json b/test/test_data/toolchain_configs/watchos_x86_64.json index 5f050f55..610e03d6 100644 --- a/test/test_data/toolchain_configs/watchos_x86_64.json +++ b/test/test_data/toolchain_configs/watchos_x86_64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index 086cdfca..e3bd3bd4 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -78,6 +78,8 @@ def _dynamic_toolchain_info_impl(ctx): else: fail("Unknown OS in target platform") + apple_platform_info = apple_support.platform_info_from_rule_ctx(ctx) + is_simulator = ctx.target_platform_has_constraint(ctx.attr._simulator[platform_common.ConstraintValueInfo]) if is_simulator: triple_suffix = "-simulator" @@ -110,14 +112,14 @@ def _dynamic_toolchain_info_impl(ctx): return [platform_common.TemplateVariableInfo( xcode_env | { "ARCH": triple_arch, - "PLATFORM_FRAMEWORKS": apple_support.path_placeholders.platform_frameworks(apple_fragment = ctx.fragments.apple), + "PLATFORM_FRAMEWORKS": apple_support.path_placeholders.platform_frameworks(apple_platform_info = apple_platform_info), "TARGET": target, }, )] dynamic_toolchain_info = rule( implementation = _dynamic_toolchain_info_impl, - attrs = { + attrs = apple_support.platform_constraint_attrs() | { "_linux": attr.label(default = "@platforms//os:linux"), "_macos": attr.label(default = "@platforms//os:macos"), "_ios": attr.label(default = "@platforms//os:ios"), From 5e91381e305604ed2632fff9720eb0179203bbb5 Mon Sep 17 00:00:00 2001 From: Paul Salaberria Date: Tue, 27 Oct 2026 00:36:38 +0100 Subject: [PATCH 12/24] Fix incremental archive creation with ar q (#670) Fix incremental archive creation when the archiver wrapper is invoked with `ar q`. The wrapper currently translates each invocation to `libtool -static -o`, which replaces the archive with only the current batch of object files. Include the existing archive as an input when appending, preserving objects from previous batches. This fixes a regression introduced by #601. In particular, cc-rs constructs archives in multiple `cq` / `cqD` invocations, and the lost objects cause undefined-symbol linker failures in native Rust dependencies such as ring. The wrapper rename also means rules_rust no longer bypasses it using its legacy `libtool` filename check. No rules_rust change is needed to address this append regression. --- crosstool/libtool.cc | 6 ++++++ test/shell/libtool_test.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/crosstool/libtool.cc b/crosstool/libtool.cc index e16d83b3..6da4ff6e 100644 --- a/crosstool/libtool.cc +++ b/crosstool/libtool.cc @@ -294,6 +294,12 @@ void processArCreateArgs( flags_consumer("-D"); // NOTE: Always added for hermiticity flags_consumer("-o"); flags_consumer(args[1]); + // libtool creates a new archive, whereas ar q appends to an existing one. + // Include the previous archive so batched callers such as cc-rs retain it. + if (args[0].find('q') != std::string::npos && + std::filesystem::exists(args[1])) { + flags_consumer(args[1]); + } } std::vector expandResponseFiles( diff --git a/test/shell/libtool_test.sh b/test/shell/libtool_test.sh index 1acb16ca..f5bb5a69 100755 --- a/test/shell/libtool_test.sh +++ b/test/shell/libtool_test.sh @@ -195,4 +195,30 @@ function test_ar_toc_only_requires_existing_archive() { expect_log "archive file '${archive}' does not exist" } +function test_ar_append_preserves_previous_batches() ( + unset __LIBTOOL_LOG_ONLY + export DEVELOPER_DIR="$(/usr/bin/xcode-select -p)" + export SDKROOT="$(/usr/bin/xcrun --show-sdk-path)" + local first="${TEST_TMPDIR}/first.o" + local second="${TEST_TMPDIR}/second.o" + + printf 'int first(void) { return 1; }\n' | + /usr/bin/xcrun clang -x c -c - -o "${first}" || fail "compile first" + printf 'int second(void) { return 2; }\n' | + /usr/bin/xcrun clang -x c -c - -o "${second}" || fail "compile second" + + local flags + for flags in cq cqD; do + local archive="${TEST_TMPDIR}/batched-${flags}.a" + local binary="${TEST_TMPDIR}/batched-${flags}" + "${LIBTOOL}" "${flags}" "${archive}" "${first}" || fail "create archive (${flags})" + "${LIBTOOL}" "${flags}" "${archive}" "${second}" || fail "append archive (${flags})" + "${LIBTOOL}" s "${archive}" || fail "index archive (${flags})" + printf 'int first(void); int second(void); int main(void) { return first() + second() != 3; }\n' | + /usr/bin/xcrun clang -x c - -x none "${archive}" -o "${binary}" || + fail "link both batches (${flags})" + "${binary}" || fail "run both batches (${flags})" + done +) + run_suite "libtool tests" From 373df5ad8e5de7ced7e05a16c57406856f01ee06 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 26 Oct 2026 19:41:50 -0400 Subject: [PATCH 13/24] Preserve strings in generated Apple toolchain configuration (#677) Both Apple toolchain repositories embed environment flags and include directories in generated BUILD files. Quotes could make the package fail to load, and backslashes could change the value. Xcode directories also passed through a Crosstool helper that doubled literal percent signs, producing a different absolute path. Quote both kinds of values as Starlark string literals and retain the original Xcode directory. The serializer leaves other control characters unchanged because Bazel 7 accepts them literally but rejects some escape sequences emitted by `repr`. --- .bazelci/presubmit.yml | 2 ++ crosstool/osx_cc_configure.bzl | 28 +++++++++------------------- lib/private/BUILD | 5 +++++ lib/private/toolchain_flags.bzl | 30 ++++++++++++++++++++++++++++++ test/shell/toolchain_flags_test.sh | 29 +++++++++++++++++++++++++++++ toolchain/toolchain_env.bzl | 19 +++++++------------ 6 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 lib/private/toolchain_flags.bzl create mode 100755 test/shell/toolchain_flags_test.sh diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 44dd7fc2..8755fb10 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -201,6 +201,7 @@ tasks: - test/shell/layering_check_test.sh - test/shell/header_parsing_test.sh - test/shell/coverage_test.sh + - test/shell/toolchain_flags_test.sh macos_latest_shell_scripts_toolchain_off: name: "Shell tests (rules toolchain off)" @@ -210,6 +211,7 @@ tasks: - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/layering_check_test.sh' - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/header_parsing_test.sh' - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/coverage_test.sh' + - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/toolchain_flags_test.sh' macos_last_green: name: "Last Green Bazel" diff --git a/crosstool/osx_cc_configure.bzl b/crosstool/osx_cc_configure.bzl index 1a23d670..b08c4435 100644 --- a/crosstool/osx_cc_configure.bzl +++ b/crosstool/osx_cc_configure.bzl @@ -14,10 +14,7 @@ # limitations under the License. """Configuring the C++ toolchain on macOS.""" -load( - "@bazel_tools//tools/cpp:lib_cc_configure.bzl", - "escape_string", -) +load("//lib/private:toolchain_flags.bzl", "get_starlark_list", "quote_starlark_string") load("//xcode:xcode_configure.bzl", "run_xcode_locator", "watch_selected_xcode") def _get_copts_env_var(repository_ctx, name, default = ""): @@ -96,13 +93,7 @@ def _split_escaped(string, delimiter): result.append("".join(accumulator)) return result -def _get_starlark_list(values): - """Convert a list of string into a string that can be passed to a rule attribute.""" - if not values: - return "" - return "\"" + "\",\n \"".join(values) + "\"" - -def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): +def _get_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): """Compute the list of default C++ include paths on Xcode-enabled darwin. Args: @@ -127,7 +118,7 @@ def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): # Include extra Xcode paths in case they're installed on other volumes for toolchain in xcode_toolchains: - include_dirs.append(escape_string(toolchain.developer_dir)) + include_dirs.append(toolchain.developer_dir) return include_dirs @@ -193,10 +184,9 @@ def configure_osx_toolchain(repository_ctx): if _succeeds(repository_ctx, "ld", "-reproducible", "-v"): features.append("reproducible_linker_flag") - escaped_include_paths = _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains) escaped_cxx_include_directories = [] - for path in escaped_include_paths: - escaped_cxx_include_directories.append((" \"%s\"," % path)) + for path in _get_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): + escaped_cxx_include_directories.append(" %s," % quote_starlark_string(path)) if xcodeloc_err: escaped_cxx_include_directories.append(" # Error: " + xcodeloc_err) @@ -209,13 +199,13 @@ def configure_osx_toolchain(repository_ctx): "BUILD", build_template, { - "%{c_flags}": _get_starlark_list(c_opts), - "%{conly_flags}": _get_starlark_list(conly_opts), + "%{c_flags}": get_starlark_list(c_opts), + "%{conly_flags}": get_starlark_list(conly_opts), "%{cxx_builtin_include_directories}": "\n".join(escaped_cxx_include_directories), - "%{cxx_flags}": _get_starlark_list(cxx_opts), + "%{cxx_flags}": get_starlark_list(cxx_opts), "%{features}": "\n".join([' "{}",'.format(x) for x in features]), "%{layering_check_modulemap}": "\"@apple_support//crosstool:exec_layering_check_modulemap\"," if enable_layering_check else "", - "%{link_flags}": _get_starlark_list(link_opts), + "%{link_flags}": get_starlark_list(link_opts), "%{placeholder_modulemap}": "\"@apple_support//crosstool:module.modulemap\"" if enable_layering_check else "None", "%{tool_paths_overrides}": ",\n ".join( ['"%s": "%s"' % (k, v) for k, v in tool_paths.items()], diff --git a/lib/private/BUILD b/lib/private/BUILD index 0d36d37c..863d49e4 100644 --- a/lib/private/BUILD +++ b/lib/private/BUILD @@ -8,3 +8,8 @@ bzl_library( name = "providers", srcs = ["providers.bzl"], ) + +bzl_library( + name = "toolchain_flags", + srcs = ["toolchain_flags.bzl"], +) diff --git a/lib/private/toolchain_flags.bzl b/lib/private/toolchain_flags.bzl new file mode 100644 index 00000000..a3982ba2 --- /dev/null +++ b/lib/private/toolchain_flags.bzl @@ -0,0 +1,30 @@ +# Copyright 2026 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. + +"""Format Apple toolchain flags and paths for generated BUILD files.""" + +visibility([ + "//crosstool", + "//toolchain", +]) + +def get_starlark_list(values): + """Formats string literals for a generated list attribute.""" + return ",\n ".join([quote_starlark_string(value) for value in values]) + +def quote_starlark_string(value): + """Returns a string literal that a BUILD file can read.""" + + # Bazel's repr can emit \x escapes, which BUILD files do not accept. + return '"%s"' % value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r") diff --git a/test/shell/toolchain_flags_test.sh b/test/shell/toolchain_flags_test.sh new file mode 100755 index 00000000..255ba657 --- /dev/null +++ b/test/shell/toolchain_flags_test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$script_path"/unittest.bash + +function test_quoted_copts_reach_the_compiler() { + ( + unset TEST_TMPDIR + bazel_cmd aquery --repo_env=$'BAZEL_COPTS=-DAPPLE_SUPPORT_QUOTE="value":-DAPPLE_SUPPORT_CTRL=alpha\fbeta:-DAPPLE_SUPPORT_APOSTROPHE=O\'Brien' --output=textproto \ + 'mnemonic("CppCompile", //test/test_data:cc_lib)' + ) &> "$TEST_log" || fail "Expected C++ action analysis to succeed" + grep -Fq 'arguments: "-DAPPLE_SUPPORT_QUOTE=\"value\""' "$TEST_log" || fail "Expected the quoted compiler flag" + grep -Fq 'arguments: "-DAPPLE_SUPPORT_CTRL=alpha\fbeta"' "$TEST_log" || fail "Expected the control character in the compiler flag" + grep -Fq "arguments: \"-DAPPLE_SUPPORT_APOSTROPHE=O\\'Brien\"" "$TEST_log" || fail "Expected the apostrophe in the compiler flag" +} + +function test_quoted_include_directory_reaches_the_toolchain() { + ( + unset TEST_TMPDIR + bazel_cmd cquery --repo_env='USER=quote"probe' --output=starlark \ + --starlark:expr='"/Users/quote\"probe/Applications/" in providers(target)["ToolchainInfo"].cc.built_in_include_directories' \ + 'kind("cc_toolchain rule", deps(//test/test_data:cc_lib))' + ) &> "$TEST_log" || fail "Expected C++ toolchain analysis to succeed" + grep -Fxq 'True' "$TEST_log" || fail "Expected the quoted include directory" +} + +run_suite "toolchain flags tests" diff --git a/toolchain/toolchain_env.bzl b/toolchain/toolchain_env.bzl index 690b9f56..2b1588ff 100644 --- a/toolchain/toolchain_env.bzl +++ b/toolchain/toolchain_env.bzl @@ -4,7 +4,7 @@ This does much less than in the past but is still required for a few environment variable reading use cases. """ -load("@bazel_tools//tools/cpp:lib_cc_configure.bzl", "escape_string") +load("//lib/private:toolchain_flags.bzl", "get_starlark_list", "quote_starlark_string") load("//xcode:xcode_configure.bzl", "run_xcode_locator") _DISABLE_ENV_VAR = "BAZEL_NO_APPLE_CPP_TOOLCHAIN" @@ -31,9 +31,9 @@ def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_locator): # Include extra Xcode paths in case they're installed on other volumes for toolchain in xcode_toolchains: - include_dirs.append(escape_string(toolchain.developer_dir)) + include_dirs.append(toolchain.developer_dir) - return [" \"%s\"," % x for x in include_dirs] + return [" %s," % quote_starlark_string(path) for path in include_dirs] def _get_copts_env_var(repository_ctx, name, default = ""): """Get an environment variable and split it on ":" to be used as copts. @@ -111,11 +111,6 @@ def _split_escaped(string, delimiter): result.append("".join(accumulator)) return result -def _get_starlark_list(values): - if not values: - return "" - return "\"" + "\",\n \"".join(values) + "\"" - def _toolchain_env_impl(repository_ctx): # All Label resolutions done at the top of the function to avoid issues # with starlark function restarts, see this: @@ -231,12 +226,12 @@ cc_feature_set( ) """.format( allowlist_absolute_include_directories = "\n".join(escaped_include_paths), - c_opts = _get_starlark_list(c_opts), - conly_opts = _get_starlark_list(conly_opts), - cxx_opts = _get_starlark_list(cxx_opts), + c_opts = get_starlark_list(c_opts), + conly_opts = get_starlark_list(conly_opts), + cxx_opts = get_starlark_list(cxx_opts), disable_toolchain = disable_toolchain, enable_layering_check = enable_layering_check, - link_opts = _get_starlark_list(link_opts), + link_opts = get_starlark_list(link_opts), ), ) From 6cc66ee4a49e65f72da66551ccc4227018286b59 Mon Sep 17 00:00:00 2001 From: Mathieu Olivari Date: Tue, 27 Oct 2026 12:23:34 +0100 Subject: [PATCH 14/24] Make the tool map public (#681) --- toolchain/tools/BUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolchain/tools/BUILD b/toolchain/tools/BUILD index fcc25af0..5689666d 100644 --- a/toolchain/tools/BUILD +++ b/toolchain/tools/BUILD @@ -21,7 +21,7 @@ cc_tool_map( "@rules_cc//cc/toolchains/actions:llvm_cov": ":llvm_cov_tool", "@rules_cc//cc/toolchains/actions:gcov": ":gcov_tool", }, - visibility = ["//:__subpackages__"], + visibility = ["//visibility:public"], ) cc_tool( From 0d7f09330d90164ad82fc11cbd54bced1504fb1e Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 29 Oct 2026 12:51:13 -0400 Subject: [PATCH 15/24] Keep string executables out of action tools (#684) --- lib/BUILD | 1 - lib/apple_support.bzl | 19 +++++++++---------- test/apple_support_test.bzl | 23 ++++++++++++++++++++++- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/BUILD b/lib/BUILD index d2fa327c..a78fbbe2 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -37,7 +37,6 @@ bzl_library( ":missing_platform_fallback_users", ":providers", "//lib/private:providers", - "@bazel_skylib//lib:types", ], ) diff --git a/lib/apple_support.bzl b/lib/apple_support.bzl index d50ce789..d3ad467f 100644 --- a/lib/apple_support.bzl +++ b/lib/apple_support.bzl @@ -23,7 +23,6 @@ load("@apple_support//lib:apple_support.bzl", "apple_support") ``` """ -load("@bazel_skylib//lib:types.bzl", "types") load( "//lib:missing_platform_fallback_users.bzl", "ALLOWED_USERS_OF_MISSING_PLATFORM_FALLBACK", @@ -443,18 +442,18 @@ def _run( # We also need to include the user executable in the "tools" argument of the action, since it # won't be referenced by "executable" anymore. + all_tools = [] + if type(original_executable) != "string": + all_tools.append(original_executable) + original_tools = processed_kwargs.pop("tools", None) - if types.is_list(original_tools): - all_tools = [original_executable] + original_tools - elif types.is_depset(original_tools): - all_tools = depset([original_executable], transitive = [original_tools]) + if type(original_tools) == "depset": + # Top-level depsets can contain providers; a nested depset must contain files. + all_tools.extend(original_tools.to_list()) + elif type(original_tools) in ("list", "tuple"): + all_tools.extend(original_tools) elif original_tools: fail("'tools' argument must be a sequence or depset.") - elif not types.is_string(original_executable): - # Only add the user_executable to the "tools" list if it's a File, not a string. - all_tools = [original_executable] - else: - all_tools = [] actions.run( executable = processor_script, diff --git a/test/apple_support_test.bzl b/test/apple_support_test.bzl index 5f1d76ec..f27f64c3 100644 --- a/test/apple_support_test.bzl +++ b/test/apple_support_test.bzl @@ -229,12 +229,33 @@ def _apple_support_test_impl(ctx): exec_group = "mac_exec_group", ) + # A string executable names a host tool; only the declared tools are inputs. + string_executable_outputs = [] + for kind, tools in [ + ("list", [test_tool]), + ("tuple", (test_tool,)), + ("depset", depset([test_tool])), + ]: + output = ctx.actions.declare_file("{}_string_executable_{}".format(ctx.label.name, kind)) + apple_support.run( + actions = ctx.actions, + xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig], + apple_platform_info = apple_platform_info, + outputs = [output], + executable = "/bin/bash", + tools = tools, + arguments = [test_tool.path, output.path], + xcode_path_resolve_level = apple_support.xcode_path_resolve_level.args, + exec_group = "mac_exec_group", + ) + string_executable_outputs.append(output) + test_files = [ run_output, run_output_xcode_path_in_args, run_output_xcode_path_in_file, run_shell_output, - ] + ] + string_executable_outputs test_script = ctx.actions.declare_file("{}_test_script".format(ctx.label.name)) ctx.actions.write(test_script, _TEST_SCRIPT_CONTENTS.format( From 37301c6cf72fd4a7e7acd99e23e11c8405413fd8 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 29 Oct 2026 13:12:55 -0400 Subject: [PATCH 16/24] Accept depset inputs in lipo.create (#683) --- lib/lipo.bzl | 2 ++ test/BUILD | 4 ++++ test/lipo_test.bzl | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 test/lipo_test.bzl diff --git a/lib/lipo.bzl b/lib/lipo.bzl index 56a4b29d..1c1fee84 100644 --- a/lib/lipo.bzl +++ b/lib/lipo.bzl @@ -47,6 +47,8 @@ def _create( xcode_config: The `apple_common.XcodeVersionConfig` provider used to configure the action environment. """ + if type(inputs) == "depset": + inputs = inputs.to_list() if not inputs: fail("lipo.create requires at least one input file.") diff --git a/test/BUILD b/test/BUILD index 1d3a5580..7439a6d6 100644 --- a/test/BUILD +++ b/test/BUILD @@ -13,6 +13,7 @@ load(":cc_toolchain_forwarder.bzl", "cc_toolchain_forwarder") load(":compiling_tests.bzl", "compiling_test_suite") load(":coverage_tests.bzl", "coverage_test_suite") load(":linking_tests.bzl", "linking_test_suite") +load(":lipo_test.bzl", "create_from_depset_test") load(":sanitizer_tests.bzl", "sanitizer_test_suite") load(":starlark_apple_binary.bzl", "starlark_apple_binary") load(":toolchains_test.bzl", "toolchain_config_test_suite") @@ -45,6 +46,8 @@ xcode_config_test(name = "xcode_config_test") xcode_support_test(name = "xcode_support_test") +create_from_depset_test(name = "lipo_create_from_depset_test") + xcode_version_at_least_test(name = "xcode_version_at_least_test") xcode_version_test(name = "xcode_version_test") @@ -82,6 +85,7 @@ bzl_library( srcs = glob(["*.bzl"]), deps = [ "//lib:apple_support", + "//lib:lipo", "//lib:xcode_support", "//rules:apple_genrule", "//xcode:available_xcodes", diff --git a/test/lipo_test.bzl b/test/lipo_test.bzl new file mode 100644 index 00000000..78cfef5c --- /dev/null +++ b/test/lipo_test.bzl @@ -0,0 +1,46 @@ +# Copyright 2026 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. +"""Tests for the lipo command construction helpers.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//lib:apple_support.bzl", "apple_support") +load("//lib:lipo.bzl", "lipo") + +def _create_from_depset_test_impl(ctx): + env = unittest.begin(ctx) + inputs = [ctx.actions.declare_file("first binary"), ctx.actions.declare_file("second binary")] + output = ctx.actions.declare_file("universal binary") + for file in inputs + [output]: + ctx.actions.write(file, "") + calls = [] + actions = struct(run_shell = lambda **kwargs: calls.append(kwargs)) + for files in [inputs, depset(inputs)]: + lipo.create( + actions = actions, + inputs = files, + output = output, + apple_platform_info = struct(platform = apple_common.platform.macos), + xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig], + ) + asserts.equals(env, 2, len(calls)) + asserts.equals(env, calls[0]["command"], calls[1]["command"]) + asserts.equals(env, inputs, calls[1]["inputs"]) + for file in inputs + [output]: + asserts.true(env, "'" + file.path + "'" in calls[1]["command"]) + return unittest.end(env) + +create_from_depset_test = unittest.make( + _create_from_depset_test_impl, + attrs = apple_support.action_required_attrs(), +) From 3b692bc67a376f5ea29cc69d4e8986b1a1df4a2c Mon Sep 17 00:00:00 2001 From: Mathieu Olivari Date: Mon, 2 Nov 2026 18:50:24 +0100 Subject: [PATCH 17/24] fix(toolchain): keep version suffixes out of the clang target triple (#682) In corner cases, Xcode versions may include non-numeric suffixes. These need to be passed along as part of the SDK platform name, but must not end up in the clang target triple, where they cannot be parsed. Use only the numeric part of the minimum OS version for the triple, and carry any suffix of the SDK version over to the SDK platform name. --- toolchain/dynamic_toolchain_info.bzl | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index e3bd3bd4..5288e5e2 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -44,6 +44,15 @@ def _sdk_name(platform_type, is_simulator): else: fail("Unhandled platform type: {}".format(platform_type)) +def _numeric_version(version): + """Returns the leading numeric components of a version, e.g. "1.2.foo" -> "1.2".""" + parts = [] + for part in str(version).split("."): + if not part.isdigit(): + break + parts.append(part) + return ".".join(parts) + def _dynamic_toolchain_info_impl(ctx): if ctx.target_platform_has_constraint(ctx.attr._arm64[platform_common.ConstraintValueInfo]): triple_arch = "arm64" @@ -87,9 +96,17 @@ def _dynamic_toolchain_info_impl(ctx): triple_suffix = "" xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] - target_os_version = xcode_config.minimum_os_for_platform_type(platform_type) sdk_version = _sdk_version_for_platform(xcode_config, platform_type) + # Clang cannot parse non-numeric version suffixes in a target triple and + # silently falls back to a very old deployment target, so only the numeric + # part of the version goes into the triple. + target_os_version = _numeric_version(xcode_config.minimum_os_for_platform_type(platform_type)) + + # Any suffix following the numeric part of the SDK version is carried over + # to the SDK platform name. + sdk_name = _sdk_name(platform_type, is_simulator) + str(sdk_version)[len(_numeric_version(sdk_version)):] + target = "{}-apple-{}{}{}".format( triple_arch, triple_os, @@ -104,7 +121,7 @@ def _dynamic_toolchain_info_impl(ctx): } if xcode_config.xcode_version(): xcode_env = { - "APPLE_SDK_PLATFORM": _sdk_name(platform_type, is_simulator), + "APPLE_SDK_PLATFORM": sdk_name, "APPLE_SDK_VERSION_OVERRIDE": str(sdk_version), # TODO: Remove once we drop bazel 7.x support "XCODE_VERSION_OVERRIDE": str(xcode_config.xcode_version()), } From df816cacdaecc9664fdc6b7b397bdea576bbdd48 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 29 Oct 2026 13:16:10 -0400 Subject: [PATCH 18/24] Describe Apple platform and action contracts Use source comments for ordinary functions and the DMG repository rule. Describe platform providers and forwarded action keywords in companion interfaces so callers and implementations use the same types. --- crosstool/osx_cc_configure.bzl | 15 +- crosstool/osx_cc_configure.bzli | 3 + crosstool/setup.bzl | 3 + crosstool/setup_internal.bzl | 4 + crosstool/universal_exec_tool.bzl | 1 + crosstool/universal_exec_tool.bzli | 10 ++ lib/apple_support.bzl | 60 ++++--- lib/apple_support.bzli | 169 ++++++++++++++++++ lib/lipo.bzl | 17 +- lib/lipo.bzli | 26 +++ lib/missing_platform_fallback_users.bzl | 2 +- lib/private/providers.bzl | 1 + lib/private/providers.bzli | 23 +++ lib/private/toolchain_flags.bzl | 2 + lib/transitions.bzli | 29 +++ lib/xcode_support.bzl | 4 +- lib/xcode_support.bzli | 9 + rules/apple_genrule.bzl | 77 +++++--- rules/apple_genrule.bzli | 24 +++ rules/private/apple_genrule.bzl | 5 + rules/toolchain_substitution.bzl | 1 + rules/universal_binary.bzl | 1 + test/BUILD | 9 +- test/apple_support_test.bzl | 15 +- test/binary_tests.bzl | 1 + test/lipo_test.bzl | 37 ++++ test/rules/apple_verification_test.bzl | 6 +- .../test_data/framework_includes_provider.bzl | 1 + test/universal_binary_test.bzl | 2 + test/xcode_support_test.bzl | 50 ++++++ toolchain/dynamic_toolchain_info.bzl | 10 +- toolchain/toolchain_env.bzl | 19 +- tools/http_dmg/http_dmg.bzl | 19 +- .../private/tests/http_dmg_test_deps.bzl | 1 + .../tests/http_dmg_test_extensions.bzl | 3 + tools/http_dmg/private/zip7.bzl | 2 + tools/http_dmg/private/zip7.bzli | 8 + xcode/available_xcodes.bzl | 1 + xcode/providers.bzli | 38 ++++ xcode/xcode_configure.bzl | 12 ++ xcode/xcode_configure.bzli | 9 + xcode/xcode_sdk_variant.bzl | 2 + xcode/xcode_version.bzl | 1 + 43 files changed, 634 insertions(+), 98 deletions(-) create mode 100644 crosstool/osx_cc_configure.bzli create mode 100644 crosstool/universal_exec_tool.bzli create mode 100644 lib/apple_support.bzli create mode 100644 lib/lipo.bzli create mode 100644 lib/private/providers.bzli create mode 100644 lib/transitions.bzli create mode 100644 lib/xcode_support.bzli create mode 100644 rules/apple_genrule.bzli create mode 100644 tools/http_dmg/private/zip7.bzli create mode 100644 xcode/providers.bzli create mode 100644 xcode/xcode_configure.bzli diff --git a/crosstool/osx_cc_configure.bzl b/crosstool/osx_cc_configure.bzl index b08c4435..99937723 100644 --- a/crosstool/osx_cc_configure.bzl +++ b/crosstool/osx_cc_configure.bzl @@ -18,6 +18,7 @@ load("//lib/private:toolchain_flags.bzl", "get_starlark_list", "quote_starlark_s load("//xcode:xcode_configure.bzl", "run_xcode_locator", "watch_selected_xcode") def _get_copts_env_var(repository_ctx, name, default = ""): + # type: (repository_ctx, str, str) -> list[str] """Get an environment variable and split it on ":" to be used as copts. Args: @@ -33,6 +34,7 @@ def _get_copts_env_var(repository_ctx, name, default = ""): return _split_escaped(default, ":") def _split_escaped(string, delimiter): + # type: (str, str) -> list[str] """Split string on the delimiter unless %-escaped. Examples: @@ -63,7 +65,7 @@ def _split_escaped(string, delimiter): i = 0 result = [] - accumulator = [] + accumulator = [] # type: list[str] length = len(string) delimiter_length = len(delimiter) @@ -83,7 +85,7 @@ def _split_escaped(string, delimiter): i += 1 + delimiter_length elif i + delimiter_length <= length and string[i:i + delimiter_length] == delimiter: result.append("".join(accumulator)) - accumulator = [] + accumulator = [] # type: list[str] i += delimiter_length else: accumulator.append(string[i]) @@ -123,6 +125,9 @@ def _get_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): return include_dirs def _succeeds(repository_ctx, *args): + # type: (repository_ctx, *str) -> bool + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose a repository context type. env = repository_ctx.os.environ result = repository_ctx.execute([ "env", @@ -136,9 +141,13 @@ def _succeeds(repository_ctx, *args): return result.return_code == 0 def _copy_file(repository_ctx, src, dest): + # type: (repository_ctx, str | Label | path, str | path) -> None + # TODO(https://github.com/bazelbuild/bazel/issues/32020): annotate repository_ctx + # when supported Bazel versions expose a repository context type. repository_ctx.file(dest, content = repository_ctx.read(src)) def configure_osx_toolchain(repository_ctx): + # type: (repository_ctx) -> tuple[bool, str | None] """Configure C++ toolchain on macOS. Args: @@ -156,7 +165,7 @@ def configure_osx_toolchain(repository_ctx): cc_toolchain_config = Label("@apple_support//crosstool:cc_toolchain_config.bzl") build_template = Label("@apple_support//crosstool:BUILD.tpl") - xcode_toolchains = [] + xcode_toolchains = [] # type: Collection[_DeveloperDirectory] xcodeloc_err = "" allow_non_applications_xcode = "BAZEL_ALLOW_NON_APPLICATIONS_XCODE" in repository_ctx.os.environ and repository_ctx.os.environ["BAZEL_ALLOW_NON_APPLICATIONS_XCODE"] == "1" if allow_non_applications_xcode: diff --git a/crosstool/osx_cc_configure.bzli b/crosstool/osx_cc_configure.bzli new file mode 100644 index 00000000..e2906b36 --- /dev/null +++ b/crosstool/osx_cc_configure.bzli @@ -0,0 +1,3 @@ +type _DeveloperDirectory = struct[{"developer_dir": str}, ...] + +def _get_xcode_cxx_inc_directories(repository_ctx: repository_ctx, xcode_toolchains: Collection[_DeveloperDirectory]) -> list[str]: ... diff --git a/crosstool/setup.bzl b/crosstool/setup.bzl index 1f8f628f..068762c3 100644 --- a/crosstool/setup.bzl +++ b/crosstool/setup.bzl @@ -4,6 +4,9 @@ load("@bazel_features//:features.bzl", "bazel_features") load(":setup_internal.bzl", "apple_cc_autoconf", "apple_cc_autoconf_toolchains") def _apple_cc_configure_extension_impl(module_ctx): + # type: (module_ctx) -> extension_metadata | None + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose context and metadata types. apple_cc_autoconf_toolchains(name = "local_config_apple_cc_toolchains") apple_cc_autoconf(name = "local_config_apple_cc") if bazel_features.external_deps.extension_metadata_has_reproducible: diff --git a/crosstool/setup_internal.bzl b/crosstool/setup_internal.bzl index 6846f3ba..c19c6e52 100644 --- a/crosstool/setup_internal.bzl +++ b/crosstool/setup_internal.bzl @@ -9,6 +9,9 @@ _OLD_DISABLE_ENV_VAR = "BAZEL_USE_CPP_ONLY_TOOLCHAIN" _NEW_TOOLCHAIN_VAR = "APPLE_SUPPORT_RULES_BASED_TOOLCHAIN" def _apple_cc_autoconf_toolchains_impl(repository_ctx): + # type: (...) -> repo_metadata | None + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose metadata types. """Generate BUILD file with 'toolchain' targets for the local host C++ toolchain. Args: @@ -55,6 +58,7 @@ apple_cc_autoconf_toolchains = repository_rule( ) def _apple_cc_autoconf_impl(repository_ctx): + # type: (...) -> repo_metadata | None env = repository_ctx.os.environ should_disable = _DISABLE_ENV_VAR in env and env[_DISABLE_ENV_VAR] == "1" old_should_disable = _OLD_DISABLE_ENV_VAR in env and env[_OLD_DISABLE_ENV_VAR] == "1" diff --git a/crosstool/universal_exec_tool.bzl b/crosstool/universal_exec_tool.bzl index 601e3367..7b592c7e 100644 --- a/crosstool/universal_exec_tool.bzl +++ b/crosstool/universal_exec_tool.bzl @@ -3,6 +3,7 @@ load("//rules:apple_genrule.bzl", "apple_genrule") def _force_exec_impl(ctx): + # type: (...) -> list[DefaultInfo] default_into = ctx.attr.target[DefaultInfo] return [ diff --git a/crosstool/universal_exec_tool.bzli b/crosstool/universal_exec_tool.bzli new file mode 100644 index 00000000..1bddade9 --- /dev/null +++ b/crosstool/universal_exec_tool.bzli @@ -0,0 +1,10 @@ +"""Source inputs for execution-configured universal tools.""" + +type _Labels = Collection[Label | str] | depset[Label | str] + +def universal_exec_tool( + *, + name: str, + out: Label | str, + srcs: _Labels | select[_Labels | None] | None, +) -> None: ... diff --git a/lib/apple_support.bzl b/lib/apple_support.bzl index d3ad467f..f941992d 100644 --- a/lib/apple_support.bzl +++ b/lib/apple_support.bzl @@ -186,6 +186,7 @@ def _platform_frameworks_path_placeholder( ) def _sdkroot_path_placeholder(): + # type: () -> str """Returns a placeholder value to be replaced with SDKROOT during action execution. In order to get this values replaced, you'll need to use the `apple_support.run()` API by @@ -199,6 +200,7 @@ def _sdkroot_path_placeholder(): return "__BAZEL_XCODE_SDKROOT__" def _xcode_path_placeholder(): + # type: () -> str """Returns a placeholder value to be replaced with DEVELOPER_DIR during action execution. In order to get this values replaced, you'll need to use the `apple_support.run()` API by @@ -211,14 +213,13 @@ def _xcode_path_placeholder(): """ return "__BAZEL_XCODE_DEVELOPER_DIR__" -def _kwargs_for_apple_platform( +def _set_apple_platform_kwargs( + kwargs, *, xcode_config, apple_platform_info = None, - apple_fragment = None, - **kwargs): - """Returns a modified dictionary with required arguments to run on Apple platforms.""" - processed_args = dict(kwargs) + apple_fragment = None): + """Updates action keyword arguments with the required Apple environment.""" if apple_platform_info: platform = apple_platform_info.platform @@ -228,12 +229,12 @@ def _kwargs_for_apple_platform( fail("Either apple_platform_info or apple_fragment must be provided.") merged_env = {} - original_env = processed_args.get("env") + original_env = kwargs.get("env") if original_env: merged_env.update(original_env) - if "use_default_shell_env" not in processed_args: - processed_args["use_default_shell_env"] = True + if "use_default_shell_env" not in kwargs: + kwargs["use_default_shell_env"] = True # Add the environment variables required for DEVELOPER_DIR and SDKROOT last to avoid clients # overriding these values. @@ -243,18 +244,18 @@ def _kwargs_for_apple_platform( ) merged_execution_requirements = {} - original_execution_requirements = processed_args.get("execution_requirements") + original_execution_requirements = kwargs.get("execution_requirements") if original_execution_requirements: merged_execution_requirements.update(original_execution_requirements) # Add the Xcode execution requirements last to avoid clients overriding these values. merged_execution_requirements.update(xcode_config.execution_info()) - processed_args["env"] = merged_env - processed_args["execution_requirements"] = merged_execution_requirements - return processed_args + kwargs["env"] = merged_env + kwargs["execution_requirements"] = merged_execution_requirements def _action_required_attrs(): + # type: () -> dict[{'_xcode_config': Attribute[Target]}] """Returns a dictionary with required attributes for registering actions on Apple platforms. This method adds private attributes which should not be used outside of the apple_support @@ -280,6 +281,7 @@ def _action_required_attrs(): } def _platform_constraint_attrs(): + # type: () -> dict[{'_ios_constraint': Attribute[Target], '_macos_constraint': Attribute[Target], '_tvos_constraint': Attribute[Target], '_visionos_constraint': Attribute[Target], '_watchos_constraint': Attribute[Target], '_arm64_constraint': Attribute[Target], '_arm64e_constraint': Attribute[Target], '_arm64_32_constraint': Attribute[Target], '_x86_64_constraint': Attribute[Target], '_pointer_authentication_constraint': Attribute[Target], '_apple_device_constraint': Attribute[Target], '_apple_simulator_constraint': Attribute[Target]}] """Returns a dictionary of all known Apple platform constraints that can be resolved. The returned `dict` can be added to the rule's attributes using a \ @@ -385,12 +387,13 @@ def _run( """ if xcode_path_resolve_level == _XCODE_PATH_RESOLVE_LEVEL.none: - actions.run(**_kwargs_for_apple_platform( + _set_apple_platform_kwargs( + kwargs, xcode_config = xcode_config, apple_platform_info = apple_platform_info, apple_fragment = apple_fragment, - **kwargs - )) + ) + actions.run(**kwargs) return # Since a label/name isn't passed in, use the first output to derive a name @@ -409,11 +412,11 @@ def _run( )) actions.write(processor_script, script, is_executable = True) - processed_kwargs = _kwargs_for_apple_platform( + _set_apple_platform_kwargs( + kwargs, xcode_config = xcode_config, apple_platform_info = apple_platform_info, apple_fragment = apple_fragment, - **kwargs ) all_arguments = [] @@ -423,7 +426,7 @@ def _run( # Note: Bounce through an actions.args() incase the executable was a `File`, this allows it # to then work within the arguments list. executable_args = actions.args() - original_executable = processed_kwargs.pop("executable") + original_executable = kwargs["executable"] # actions.run supports multiple executable types. (File; or string; or FilesToRunProvider) # If the passed in executable is a FilesToRunProvider, only add the main executable to the @@ -435,7 +438,7 @@ def _run( all_arguments.append(executable_args) # Append the original arguments to the full list of arguments, after the original executable. - original_args_list = processed_kwargs.pop("arguments", []) + original_args_list = kwargs.get("arguments", []) if not original_args_list: fail("Error: Does not make sense to request args processing without any `arguments`.") all_arguments.extend(original_args_list) @@ -446,7 +449,7 @@ def _run( if type(original_executable) != "string": all_tools.append(original_executable) - original_tools = processed_kwargs.pop("tools", None) + original_tools = kwargs.get("tools") if type(original_tools) == "depset": # Top-level depsets can contain providers; a nested depset must contain files. all_tools.extend(original_tools.to_list()) @@ -455,12 +458,10 @@ def _run( elif original_tools: fail("'tools' argument must be a sequence or depset.") - actions.run( - executable = processor_script, - arguments = all_arguments, - tools = all_tools, - **processed_kwargs - ) + kwargs["executable"] = processor_script + kwargs["arguments"] = all_arguments + kwargs["tools"] = all_tools + actions.run(**kwargs) def _run_shell( *, @@ -495,12 +496,13 @@ def _run_shell( **kwargs: See `ctx.actions.run_shell` for the rest of the available arguments. """ - actions.run_shell(**_kwargs_for_apple_platform( + _set_apple_platform_kwargs( + kwargs, xcode_config = xcode_config, apple_platform_info = apple_platform_info, apple_fragment = apple_fragment, - **kwargs - )) + ) + actions.run_shell(**kwargs) def _target_arch_from_rule_ctx( ctx, diff --git a/lib/apple_support.bzli b/lib/apple_support.bzli new file mode 100644 index 00000000..e3302d08 --- /dev/null +++ b/lib/apple_support.bzli @@ -0,0 +1,169 @@ +# Copyright 2026 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. +"""Apple platform constraints, attributes and action helpers.""" + +load("//lib/private:providers.bzl", "ApplePlatformInfo") + +# Installed by platform_constraint_attrs() for the context helpers below. +type _PlatformAttrs = struct[{ + "_ios_constraint": Target, + "_macos_constraint": Target, + "_tvos_constraint": Target, + "_visionos_constraint": Target, + "_watchos_constraint": Target, + "_arm64_constraint": Target, + "_arm64e_constraint": Target, + "_arm64_32_constraint": Target, + "_x86_64_constraint": Target, + "_pointer_authentication_constraint": Target, + "_apple_device_constraint": Target, + "_apple_simulator_constraint": Target, +}, ...] + +def _target_arch_from_rule_ctx( + ctx: ctx[object, _PlatformAttrs], + *, + fail_on_missing_constraint: bool = ..., +) -> str | None: ... + +def _target_environment_from_rule_ctx( + ctx: ctx[object, _PlatformAttrs], + *, + fail_on_missing_constraint: bool = ..., +) -> str | None: ... + +def _target_os_from_rule_ctx( + ctx: ctx[object, _PlatformAttrs], + *, + fail_on_missing_constraint: bool = ..., +) -> str | None: ... + +class _PlatformSelection(Protocol): + @property + def target_os(self) -> str: ... + @property + def target_environment(self) -> str | None: ... + +def _platform_frameworks_path_placeholder( + *, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., +) -> str: ... + +def _platform_from_info(*, apple_platform_info: _PlatformSelection) -> apple_platform: ... + +# Forwarded keywords of Bazel 9.3 actions.run and actions.run_shell APIs. +# Keep these closed so misspelled action keywords are rejected at the wrapper. +type _ActionTools = Sequence[File | FilesToRunProvider | depset[File]] | depset[File | FilesToRunProvider | depset[File]] +type _ResourceSet[K: str, N: float] = dict[K, N] | Callable[[str, int], dict[K, N]] + +type _RunArgs[K: str, N: float] = dict[{ + "outputs": Sequence[File], + "inputs": NotRequired[Sequence[File] | depset[File]], + "unused_inputs_list": NotRequired[File | None], + "executable": File | str | FilesToRunProvider, + "tools": NotRequired[_ActionTools], + "arguments": NotRequired[Sequence[str | Args]], + "mnemonic": NotRequired[str | None], + "progress_message": NotRequired[str | None], + "use_default_shell_env": NotRequired[bool], + "env": NotRequired[dict[str, str] | None], + "execution_requirements": NotRequired[dict[str, str] | None], + "input_manifests": NotRequired[Sequence[object] | None], + "exec_group": NotRequired[str | None], + "shadowed_action": NotRequired[Action], + "resource_set": NotRequired[_ResourceSet[K, N] | None], + "toolchain": NotRequired[Label | str | None], +}] + +# TODO(https://github.com/bazelbuild/bazel/issues/31770): annotate the forwarded action keywords +# in Starlark once Bazel supports dictionaries with named, typed keys. +type _RunShellArgs[K: str, N: float] = dict[{ + "outputs": Sequence[File], + "inputs": NotRequired[Sequence[File] | depset[File]], + "tools": NotRequired[_ActionTools], + "arguments": NotRequired[Sequence[str | Args]], + "mnemonic": NotRequired[str | None], + "command": str | Sequence[str], + "progress_message": NotRequired[str | None], + "use_default_shell_env": NotRequired[bool], + "env": NotRequired[dict[str, str] | None], + "execution_requirements": NotRequired[dict[str, str] | None], + "input_manifests": NotRequired[Sequence[object] | None], + "exec_group": NotRequired[str | None], + "shadowed_action": NotRequired[Action], + "resource_set": NotRequired[_ResourceSet[K, N] | None], + "toolchain": NotRequired[Label | str | None], +}] + +type _AppleActionSettings = dict[{ + "env": NotRequired[dict[str, str] | None], + "execution_requirements": NotRequired[dict[str, str] | None], + "use_default_shell_env": NotRequired[bool], +}, ...] + +def _set_apple_platform_kwargs( + kwargs: _AppleActionSettings, + *, + xcode_config: XcodeVersionConfig, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., +) -> None: ... + +class _WriteScript(Protocol): + def __call__(self, output: File, content: str, *, is_executable: bool) -> None: ... + +class _RunAction(Protocol): + def __call__[K: str = str, N: float = float](self, **kwargs: Unpack[_RunArgs[K, N]]) -> None: ... + +class _RunShellAction(Protocol): + def __call__[K: str = str, N: float = float](self, **kwargs: Unpack[_RunShellArgs[K, N]]) -> None: ... + +class _RunActions(Protocol): + @property + def args(self) -> Callable[[], Args]: ... + @property + def declare_file(self) -> Callable[[str], File]: ... + @property + def write(self) -> _WriteScript: ... + @property + def run(self) -> _RunAction: ... + +class RunShellActions(Protocol): + @property + def run_shell(self) -> _RunShellAction: ... + +def _run[K: str = str, N: float = float]( + *, + actions: _RunActions, + xcode_config: XcodeVersionConfig, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., + xcode_path_resolve_level: str | None = ..., + **kwargs: Unpack[_RunArgs[K, N]], +) -> None: ... + +def _run_shell[K: str = str, N: float = float]( + *, + actions: RunShellActions, + xcode_config: XcodeVersionConfig, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., + **kwargs: Unpack[_RunShellArgs[K, N]], +) -> None: ... + +def _platform_info_from_rule_ctx( + ctx: ctx[object, _PlatformAttrs], + fail_on_missing_constraint: bool = ..., +) -> ApplePlatformInfo: ... diff --git a/lib/lipo.bzl b/lib/lipo.bzl index 1c1fee84..ead2f3c6 100644 --- a/lib/lipo.bzl +++ b/lib/lipo.bzl @@ -107,15 +107,17 @@ def _extract_or_thin( xcode_config: The `apple_common.XcodeVersionConfig` provider used to configure the action environment. """ - if not input_file and not input_shell_expression: - fail("""\ -lipo.extract_or_thin needs a universal binary in `input_file` or `input_shell_expression`.""") - if input_file and input_shell_expression: - fail(""" + if input_file: + if input_shell_expression: + fail(""" lipo.extract_or_thin cannot use `input_file` along with `input_shell_expression` simultaneously. Please use only `input_file` or `input_shell_expression` to express the intended input. """) + input_shell_expression = shell.quote(input_file.path) + elif not input_shell_expression: + fail("""\ +lipo.extract_or_thin needs a universal binary in `input_file` or `input_shell_expression`.""") if not archs: fail("lipo.extract_or_thin requires at least one arch.") @@ -124,11 +126,8 @@ Please use only `input_file` or `input_shell_expression` to express the intended command = [ "mkdir -p {} &&".format(shell.quote(output.dirname)), "/usr/bin/lipo", + input_shell_expression, ] - if input_file: - command.append(shell.quote(input_file.path)) - else: - command.append(input_shell_expression) if len(archs) == 1: command.extend(["-thin", archs[0]]) else: diff --git a/lib/lipo.bzli b/lib/lipo.bzli new file mode 100644 index 00000000..60454e92 --- /dev/null +++ b/lib/lipo.bzli @@ -0,0 +1,26 @@ +"""Inputs for actions that assemble and extract universal binaries.""" + +load("//lib:apple_support.bzli", _Actions = "RunShellActions") +load("//lib/private:providers.bzl", "ApplePlatformInfo") + +def _create( + *, + actions: _Actions, + inputs: Sequence[File] | depset[File], + output: File, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., + xcode_config: XcodeVersionConfig, +) -> None: ... + +def _extract_or_thin( + *, + actions: _Actions, + apple_platform_info: ApplePlatformInfo | None = ..., + apple_fragment: apple | None = ..., + archs: Sequence[str], + input_shell_expression: str | None = ..., + input_file: File | None = ..., + output: File, + xcode_config: XcodeVersionConfig, +) -> None: ... diff --git a/lib/missing_platform_fallback_users.bzl b/lib/missing_platform_fallback_users.bzl index c0dc9ee0..a45e4e6a 100644 --- a/lib/missing_platform_fallback_users.bzl +++ b/lib/missing_platform_fallback_users.bzl @@ -18,4 +18,4 @@ visibility("public") # Allowlist of targets that are allowed to analyze Apple rules without active platform constraints. # For OSS, this list is empty by default. Populate it with your own targets if needed. -ALLOWED_USERS_OF_MISSING_PLATFORM_FALLBACK = [] +ALLOWED_USERS_OF_MISSING_PLATFORM_FALLBACK = [] # type: list[str] diff --git a/lib/private/providers.bzl b/lib/private/providers.bzl index fba070e5..4e988a50 100644 --- a/lib/private/providers.bzl +++ b/lib/private/providers.bzl @@ -20,6 +20,7 @@ visibility([ ]) def make_banned_init(*, preferred_public_factory = None, provider_name): + # type: (str | None, str) -> Callable[[], Never] """Generates a lambda with a fail(...) for providers to dictate preferred initializer patterns.""" if preferred_public_factory: return lambda: fail(""" diff --git a/lib/private/providers.bzli b/lib/private/providers.bzli new file mode 100644 index 00000000..66278b28 --- /dev/null +++ b/lib/private/providers.bzli @@ -0,0 +1,23 @@ +# Copyright 2026 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. + +"""The platform information assembled from Bazel constraints.""" + +def ApplePlatformInfo() -> record[{ + "target_arch": str | None, + "target_build_config": configuration, + "target_environment": str | None, + "target_os": str, + "platform": apple_platform, +}]: ... diff --git a/lib/private/toolchain_flags.bzl b/lib/private/toolchain_flags.bzl index a3982ba2..2a8aaf08 100644 --- a/lib/private/toolchain_flags.bzl +++ b/lib/private/toolchain_flags.bzl @@ -20,10 +20,12 @@ visibility([ ]) def get_starlark_list(values): + # type: (list[str]) -> str """Formats string literals for a generated list attribute.""" return ",\n ".join([quote_starlark_string(value) for value in values]) def quote_starlark_string(value): + # type: (str) -> str """Returns a string literal that a BUILD file can read.""" # Bazel's repr can emit \x escapes, which BUILD files do not accept. diff --git a/lib/transitions.bzli b/lib/transitions.bzli new file mode 100644 index 00000000..64c0f066 --- /dev/null +++ b/lib/transitions.bzli @@ -0,0 +1,29 @@ +# Copyright 2026 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. + +# TODO(https://github.com/bazelbuild/bazel/issues/31770): annotate the transition settings +# in Starlark once Bazel supports dictionaries with named, typed keys. +type _InputSettings = dict[{ + "//command_line_option:cpu": str, + "//command_line_option:platforms": list[Label], + "//platforms:macos_x86_64_platform": Label, + "//platforms:macos_arm64_platform": Label, +}] + +type _OutputSettings = dict[{ + "//command_line_option:cpu": str, + "//command_line_option:platforms": list[Label], +}] + +def _macos_universal_transition_impl(settings: _InputSettings, _attr: struct[{}, ...]) -> _OutputSettings | list[_OutputSettings]: ... diff --git a/lib/xcode_support.bzl b/lib/xcode_support.bzl index 6e6f5151..e4d5be4d 100644 --- a/lib/xcode_support.bzl +++ b/lib/xcode_support.bzl @@ -72,6 +72,7 @@ def _get_current_xcode(ctx): return xcode_config[apple_common.XcodeVersionConfig] def _is_xcode_at_least_version(xcode_config, version): + # type: (XcodeVersionConfig, str) -> bool """Returns True if Xcode version is at least a given version. This method takes as input an `XcodeVersionConfig` provider, which can be obtained from the @@ -89,7 +90,8 @@ def _is_xcode_at_least_version(xcode_config, version): """ current_version = xcode_config.xcode_version() - if str(current_version).startswith("/"): + # XcodeVersionConfig returns strings only for absolute developer directory paths. + if type(current_version) == "string": return True if not current_version: diff --git a/lib/xcode_support.bzli b/lib/xcode_support.bzli new file mode 100644 index 00000000..6b243a5a --- /dev/null +++ b/lib/xcode_support.bzli @@ -0,0 +1,9 @@ +"""Configured Xcode providers and version comparisons.""" + +load("//xcode:providers.bzl", "XcodeSdkVariantInfo") + +type _Context = ctx[object, struct[{"_xcode_config": Target | None}, ...]] + +def _get_current_sdk(ctx: _Context) -> XcodeSdkVariantInfo: ... + +def _get_current_xcode(ctx: _Context) -> XcodeVersionConfig: ... diff --git a/rules/apple_genrule.bzl b/rules/apple_genrule.bzl index 97aba90c..1ba930f2 100644 --- a/rules/apple_genrule.bzl +++ b/rules/apple_genrule.bzl @@ -20,45 +20,66 @@ load( ) # buildozer: disable=function-docstring-args -def apple_genrule(name, **kwargs): +def apple_genrule( + name, + *, + outs, + cmd, + executable = False, + srcs = None, + tools = None, + message = None, + no_sandbox = None, + toolchains = None, + expect_failure = None, + **kwargs): """Genrule which provides make variables and an Apple environment on macOS.""" # This split/indirection traces back to cl/128714692 and b/30413353. - if kwargs.get("executable", False): - outs = kwargs.pop("outs", []) + inner_kwargs = dict(kwargs) + if executable: + if type(outs) == "depset": + fail("apple_genrule executable outputs must be a list or tuple") + if expect_failure != None: + fail("apple_genrule executable rules do not support expect_failure") if len(outs) != 1: fail("apple_genrule, if executable, must have exactly one output") + output = outs[0] + if type(output) != "string": + fail("apple_genrule executable output must be a string") + intermediate_out = output + ".nonexecutable" + inner_name = name + "_nonexecutable" + inner_outs = [intermediate_out] - intermediate_out = outs[0] + ".nonexecutable" - - # Remove any visibility and make this sub rule private since it is an - # implementation detail. - sub_kwargs = dict(kwargs) - sub_kwargs.pop("visibility", None) - _apple_genrule_inner( - name = name + "_nonexecutable", - outs = [intermediate_out], - visibility = ["//visibility:private"], - **sub_kwargs - ) + # The intermediate rule is an implementation detail. + inner_kwargs["visibility"] = ["//visibility:private"] + else: + inner_name = name + inner_outs = outs - # Remove anything from kwargs that might have a meaning that isn't wanted - # on the genrule that does the copy. Generally, we are just trying to - # keep things like testonly, visibility, etc. - kwargs.pop("stamp", None) - kwargs.pop("cmd", None) - kwargs.pop("executable", None) - kwargs.pop("srcs", None) - kwargs.pop("message", None) - kwargs.pop("tools", None) - kwargs.pop("no_sandbox", None) + _apple_genrule_inner( + name = inner_name, + outs = inner_outs, + cmd = cmd, + executable = executable, + srcs = srcs, + tools = tools, + message = message, + no_sandbox = no_sandbox, + toolchains = toolchains, + expect_failure = expect_failure, + **inner_kwargs + ) + if executable: + # Only common attributes belong on the executable copy. + if type(toolchains) == "select": + fail("apple_genrule executable toolchains cannot be configurable") native.genrule( name = name, outs = outs, - srcs = [intermediate_out], + srcs = inner_outs, cmd = "cp $< $@", executable = True, + toolchains = toolchains, **kwargs ) - else: - _apple_genrule_inner(name = name, **kwargs) diff --git a/rules/apple_genrule.bzli b/rules/apple_genrule.bzli new file mode 100644 index 00000000..8f6cabea --- /dev/null +++ b/rules/apple_genrule.bzli @@ -0,0 +1,24 @@ +"""Arguments for Apple genrules and their executable copy rules.""" + +type _Labels = Collection[Label | str] | depset[Label | str] +type _Configurable[T] = T | select[T | None] | None + +class _Common(Protocol): + def __call__(self, **kwargs: Unpack[CommonRuleAttrs]) -> None: ... + +type _Forwarded = KeywordArgs[_Common, "expect_failure", "toolchains"] + +def apple_genrule( + name: str, + *, + outs: Sequence[Label | str] | depset[Label | str], + cmd: str | select[str | None], + executable: _Configurable[bool | Literal[0, 1]] = ..., + srcs: _Configurable[_Labels] = ..., + tools: _Configurable[_Labels] = ..., + message: _Configurable[str] = ..., + no_sandbox: _Configurable[bool | Literal[0, 1]] = ..., + toolchains: _Configurable[_Labels] = ..., + expect_failure: _Configurable[str] = ..., + **kwargs: Unpack[_Forwarded], +) -> None: ... diff --git a/rules/private/apple_genrule.bzl b/rules/private/apple_genrule.bzl index 30df3391..0aeb87d9 100644 --- a/rules/private/apple_genrule.bzl +++ b/rules/private/apple_genrule.bzl @@ -19,6 +19,7 @@ load("//lib:apple_support.bzl", "apple_support") _APPLE_GENRULE_TOOLCHAIN_TYPE = Label("//rules/private:apple_genrule_toolchain_type") def _apple_genrule_toolchain_impl(_ctx): + # type: (...) -> list[ToolchainInfo] return [platform_common.ToolchainInfo()] apple_genrule_toolchain = rule( @@ -30,6 +31,9 @@ def _compute_make_variables( label, resolved_srcs, files_to_build): + # type: (root, Label, depset[File], list[File]) -> dict[str, str] + # TODO(https://github.com/bazelbuild/bazel/issues/32021): annotate resolved_srcs + # when supported Bazel versions expose element-typed depsets. resolved_srcs_list = resolved_srcs.to_list() variables = { "OUTS": " ".join([x.path for x in files_to_build]), @@ -45,6 +49,7 @@ def _compute_make_variables( return variables def _apple_genrule_impl(ctx): + # type: (...) -> list[DefaultInfo] if not ctx.outputs.outs: fail("apple_genrule must have one or more outputs", attr = "outs") files_to_build = ctx.outputs.outs diff --git a/rules/toolchain_substitution.bzl b/rules/toolchain_substitution.bzl index 2481e15a..51cfb84b 100644 --- a/rules/toolchain_substitution.bzl +++ b/rules/toolchain_substitution.bzl @@ -4,6 +4,7 @@ target's attributes. Useful for passing custom tools to a compile or link """ def _toolchain_substitution(ctx): + # type: (...) -> list[DefaultInfo | TemplateVariableInfo] return [ DefaultInfo( files = depset([ctx.file.src]), diff --git a/rules/universal_binary.bzl b/rules/universal_binary.bzl index a63793a0..22d14386 100644 --- a/rules/universal_binary.bzl +++ b/rules/universal_binary.bzl @@ -19,6 +19,7 @@ load("//lib:lipo.bzl", "lipo") load("//lib:transitions.bzl", "macos_universal_transition") def _universal_binary_impl(ctx): + # type: (...) -> list[DefaultInfo] inputs = [ binary[DefaultInfo].files.to_list()[0] for binary in ctx.split_attr.binary.values() diff --git a/test/BUILD b/test/BUILD index 7439a6d6..b0e11876 100644 --- a/test/BUILD +++ b/test/BUILD @@ -13,13 +13,13 @@ load(":cc_toolchain_forwarder.bzl", "cc_toolchain_forwarder") load(":compiling_tests.bzl", "compiling_test_suite") load(":coverage_tests.bzl", "coverage_test_suite") load(":linking_tests.bzl", "linking_test_suite") -load(":lipo_test.bzl", "create_from_depset_test") +load(":lipo_test.bzl", "create_from_depset_test", "extract_or_thin_test") load(":sanitizer_tests.bzl", "sanitizer_test_suite") load(":starlark_apple_binary.bzl", "starlark_apple_binary") load(":toolchains_test.bzl", "toolchain_config_test_suite") load(":universal_binary_test.bzl", "universal_binary_test") load(":xcode_config_test.bzl", "xcode_config_test") -load(":xcode_support_test.bzl", "xcode_support_test") +load(":xcode_support_test.bzl", "missing_xcode_version_test", "xcode_support_test") load(":xcode_version_at_least_test.bzl", "xcode_version_at_least_test") load(":xcode_version_test.bzl", "xcode_version_test") @@ -48,6 +48,10 @@ xcode_support_test(name = "xcode_support_test") create_from_depset_test(name = "lipo_create_from_depset_test") +missing_xcode_version_test(name = "missing_xcode_version_test") + +extract_or_thin_test(name = "lipo_extract_or_thin_test") + xcode_version_at_least_test(name = "xcode_version_at_least_test") xcode_version_test(name = "xcode_version_test") @@ -93,6 +97,7 @@ bzl_library( "//xcode:xcode_version", "//xcode:xcode_version_at_least", "//xcode/private:providers", + "@bazel_features//:features", "@bazel_skylib//lib:unittest", ], ) diff --git a/test/apple_support_test.bzl b/test/apple_support_test.bzl index f27f64c3..c459fd16 100644 --- a/test/apple_support_test.bzl +++ b/test/apple_support_test.bzl @@ -117,6 +117,7 @@ exit 0 """ def _apple_support_test_impl(ctx): + # type: (...) -> list[DefaultInfo | ExecutionInfo | RunEnvironmentInfo] """Implementation of the apple_support_test rule.""" # Declare all the action outputs @@ -191,10 +192,11 @@ def _apple_support_test_impl(ctx): action_args.set_param_file_format("multiline") action_args.use_param_file("@%s", use_always = True) - params_processors = [] + params_processors = [] # type: list[File] - def write_params_processor(output, content, **kwargs): - ctx.actions.write(output, content, **kwargs) + def write_params_processor(output, content, *, is_executable): + # type: (File, str, bool) -> None + ctx.actions.write(output, content, is_executable = is_executable) params_processors.append(output) apple_support.run( @@ -270,9 +272,10 @@ def _apple_support_test_impl(ctx): xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] apple_platform = apple_platform_info.platform - test_env = {} - test_env.update(apple_common.apple_host_system_env(xcode_config)) - test_env.update(apple_common.target_apple_env(xcode_config, apple_platform)) + test_env = ( + apple_common.apple_host_system_env(xcode_config) | + apple_common.target_apple_env(xcode_config, apple_platform) + ) return [ testing.ExecutionInfo(xcode_config.execution_info()), diff --git a/test/binary_tests.bzl b/test/binary_tests.bzl index 2f433390..d328b77f 100644 --- a/test/binary_tests.bzl +++ b/test/binary_tests.bzl @@ -6,6 +6,7 @@ load( ) def binary_test_suite(name): + # type: (str) -> None """Test various aspects of binary generation Args: diff --git a/test/lipo_test.bzl b/test/lipo_test.bzl index 78cfef5c..88a08d83 100644 --- a/test/lipo_test.bzl +++ b/test/lipo_test.bzl @@ -44,3 +44,40 @@ create_from_depset_test = unittest.make( _create_from_depset_test_impl, attrs = apple_support.action_required_attrs(), ) + +def _extract_or_thin_test_impl(ctx): + env = unittest.begin(ctx) + input_file = ctx.actions.declare_file("input binary") + output = ctx.actions.declare_file("extracted binary") + for file in [input_file, output]: + ctx.actions.write(file, "") + calls = [] + actions = struct(run_shell = lambda **kwargs: calls.append(kwargs)) + for archs in [["arm64"], ["arm64", "x86_64"]]: + for use_file in [True, False]: + expression = "'" + input_file.path + "'" if use_file else '"$SDKROOT/input binary"' + lipo.extract_or_thin( + actions = actions, + archs = archs, + input_file = input_file if use_file else None, + input_shell_expression = None if use_file else expression, + output = output, + apple_platform_info = struct(platform = apple_common.platform.macos), + xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig], + ) + call = calls[-1] + flags = "-thin arm64" if len(archs) == 1 else "-extract arm64 -extract x86_64" + asserts.equals(env, "mkdir -p '{}' && /usr/bin/lipo {} {} -output '{}'".format( + output.dirname, + expression, + flags, + output.path, + ), call["command"]) + asserts.equals(env, [input_file] if use_file else [], call.get("inputs", [])) + asserts.equals(env, 4, len(calls)) + return unittest.end(env) + +extract_or_thin_test = unittest.make( + _extract_or_thin_test_impl, + attrs = apple_support.action_required_attrs(), +) diff --git a/test/rules/apple_verification_test.bzl b/test/rules/apple_verification_test.bzl index dbb5a7ca..5f52e6d6 100644 --- a/test/rules/apple_verification_test.bzl +++ b/test/rules/apple_verification_test.bzl @@ -14,9 +14,12 @@ """Test rule to perform generic bundle verification tests.""" +load("//lib:xcode_support.bzl", "xcode_support") + _supports_visionos = hasattr(apple_common.platform_type, "visionos") def _transition_impl(_, attr): + # type: (dict[str, object], struct[{'generate_dsym': bool, 'compilation_mode': str, 'objc_enable_binary_stripping': bool, 'build_type': str, 'cpus': dict[str, str]}, ...]) -> dict[str, bool | str] output_dictionary = { "//command_line_option:apple_generate_dsym": attr.generate_dsym, "//command_line_option:compilation_mode": attr.compilation_mode, @@ -66,6 +69,7 @@ _transition = transition( ) def _apple_verification_test_impl(ctx): + # type: (...) -> list[ExecutionInfo | RunEnvironmentInfo | DefaultInfo] binary = ctx.attr.target_under_test[0][DefaultInfo].files.to_list()[0] output_script = ctx.actions.declare_file("{}_test_script".format(ctx.label.name)) ctx.actions.expand_template( @@ -89,7 +93,7 @@ def _apple_verification_test_impl(ctx): cpu = cpu[4:] test_env["CPU"] = cpu - xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] + xcode_config = xcode_support.get_current_xcode(ctx) return [ testing.ExecutionInfo(xcode_config.execution_info()), diff --git a/test/test_data/framework_includes_provider.bzl b/test/test_data/framework_includes_provider.bzl index 893ef4cb..928f609a 100644 --- a/test/test_data/framework_includes_provider.bzl +++ b/test/test_data/framework_includes_provider.bzl @@ -4,6 +4,7 @@ load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") def _framework_includes_lib_impl(ctx): + # type: (...) -> list[CcInfo] return [ CcInfo( compilation_context = cc_common.create_compilation_context( diff --git a/test/universal_binary_test.bzl b/test/universal_binary_test.bzl index eb2db133..8451ff53 100644 --- a/test/universal_binary_test.bzl +++ b/test/universal_binary_test.bzl @@ -47,6 +47,7 @@ exit 0 """ def _universal_binary_test_transition(_settings, attr): + # type: (dict[str, object], struct[{"cpu": str}, ...]) -> dict[str, str] """Implementation of the universal_binary_test_transition transition.""" return { @@ -60,6 +61,7 @@ universal_binary_test_transition = transition( ) def _universal_binary_test_impl(ctx): + # type: (...) -> list[DefaultInfo] """Implementation of the universal_binary_test rule.""" target_under_test = ctx.split_attr.target_under_test.values()[0] diff --git a/test/xcode_support_test.bzl b/test/xcode_support_test.bzl index f03aa2cc..ad5ac1b1 100644 --- a/test/xcode_support_test.bzl +++ b/test/xcode_support_test.bzl @@ -13,6 +13,8 @@ # limitations under the License. """Definition of a test rule to test xcode_support.""" +load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load("//lib:apple_support.bzl", "apple_support") load("//lib:xcode_support.bzl", "xcode_support") @@ -36,9 +38,38 @@ if [[ "{future_version_is_false}" != "False" ]]; then fi """ +def _xcode_config(version): + return apple_common.XcodeVersionConfig( + ios_sdk_version = "1.0", + ios_minimum_os_version = "1.0", + watchos_sdk_version = "1.0", + watchos_minimum_os_version = "1.0", + tvos_sdk_version = "1.0", + tvos_minimum_os_version = "1.0", + macos_sdk_version = "1.0", + macos_minimum_os_version = "1.0", + visionos_sdk_version = "1.0", + visionos_minimum_os_version = "1.0", + xcode_version = version, + availability = "UNKNOWN", + xcode_version_flag = "", + include_xcode_execution_info = False, + ) + def _xcode_support_test_impl(ctx): """Implementation of the xcode_support_test rule.""" + cases = [ + ("15.1", "15.0", True), + ("15.1", "15.1", True), + ("15.1", "16.0", False), + ("/Applications/Xcode.app/Contents/Developer", "999", True), + ] if bazel_features.apple.xcode_config_migrated else [] + for version, minimum, expected in cases: + actual = xcode_support.is_xcode_at_least_version(_xcode_config(version), minimum) + if actual != expected: + fail("Expected Xcode %s >= %s to be %s, got %s" % (version, minimum, expected, actual)) + test_script = ctx.actions.declare_file("{}_test_script".format(ctx.label.name)) xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] ctx.actions.write(test_script, _TEST_SCRIPT_CONTENTS.format( @@ -53,3 +84,22 @@ xcode_support_test = rule( attrs = apple_support.action_required_attrs(), test = True, ) + +def _missing_xcode_version_impl(_ctx): + xcode_support.is_xcode_at_least_version(_xcode_config(""), "15.0") + return [] + +_missing_xcode_version = rule(implementation = _missing_xcode_version_impl) + +def _missing_xcode_version_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Could not determine Xcode version at all.") + return analysistest.end(env) + +_missing_xcode_version_test = analysistest.make(_missing_xcode_version_test_impl, expect_failure = True) + +def missing_xcode_version_test(name): + """Tests the missing version supported by the Starlark Xcode provider.""" + if bazel_features.apple.xcode_config_migrated: + _missing_xcode_version(name = name + "_subject", tags = ["manual"]) + _missing_xcode_version_test(name = name, target_under_test = ":" + name + "_subject") diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index 5288e5e2..3f702015 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -7,8 +7,10 @@ rules. load("@rules_cc//cc/toolchains:providers.bzl", "ExecutionRequirementsInfo") load("//lib:apple_support.bzl", "apple_support") +load("//lib:xcode_support.bzl", "xcode_support") def _sdk_version_for_platform(xcode_config, platform_type): + # type: (XcodeVersionConfig, apple_platform_type) -> DottedVersion if platform_type == apple_common.platform_type.ios: return xcode_config.sdk_version_for_platform(apple_common.platform.ios_device) elif platform_type == apple_common.platform_type.tvos: @@ -23,6 +25,7 @@ def _sdk_version_for_platform(xcode_config, platform_type): fail("Unhandled platform type: {}".format(platform_type)) def _sdk_name(platform_type, is_simulator): + # type: (apple_platform_type, bool) -> str if platform_type == apple_common.platform_type.ios and is_simulator: return "iPhoneSimulator" elif platform_type == apple_common.platform_type.ios: @@ -45,6 +48,7 @@ def _sdk_name(platform_type, is_simulator): fail("Unhandled platform type: {}".format(platform_type)) def _numeric_version(version): + # type: (DottedVersion) -> str """Returns the leading numeric components of a version, e.g. "1.2.foo" -> "1.2".""" parts = [] for part in str(version).split("."): @@ -54,6 +58,7 @@ def _numeric_version(version): return ".".join(parts) def _dynamic_toolchain_info_impl(ctx): + # type: (...) -> list[TemplateVariableInfo] if ctx.target_platform_has_constraint(ctx.attr._arm64[platform_common.ConstraintValueInfo]): triple_arch = "arm64" elif ctx.target_platform_has_constraint(ctx.attr._arm64e[platform_common.ConstraintValueInfo]): @@ -95,7 +100,7 @@ def _dynamic_toolchain_info_impl(ctx): else: triple_suffix = "" - xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] + xcode_config = xcode_support.get_current_xcode(ctx) sdk_version = _sdk_version_for_platform(xcode_config, platform_type) # Clang cannot parse non-numeric version suffixes in a target triple and @@ -160,7 +165,8 @@ dynamic_toolchain_info = rule( ) def _xcode_execution_info_impl(ctx): - xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] + # type: (...) -> list[ExecutionRequirementsInfo] + xcode_config = xcode_support.get_current_xcode(ctx) return [ ExecutionRequirementsInfo( label = ctx.label, diff --git a/toolchain/toolchain_env.bzl b/toolchain/toolchain_env.bzl index 2b1588ff..05c6d1e4 100644 --- a/toolchain/toolchain_env.bzl +++ b/toolchain/toolchain_env.bzl @@ -11,6 +11,9 @@ _DISABLE_ENV_VAR = "BAZEL_NO_APPLE_CPP_TOOLCHAIN" _OLD_DISABLE_ENV_VAR = "BAZEL_USE_CPP_ONLY_TOOLCHAIN" def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_locator): + # type: (repository_ctx, Label) -> list[str] + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose a repository context type. # Assume that everything is managed by Xcode / toolchain installations include_dirs = [ "/Applications/", @@ -24,18 +27,18 @@ def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_locator): "/Users/{}/Library/Developer/".format(user), # Custom Swift toolchains, user only installation of CLT ]) - xcode_toolchains = [] allow_non_applications_xcode = repository_ctx.os.environ.get("BAZEL_ALLOW_NON_APPLICATIONS_XCODE") == "1" if allow_non_applications_xcode: - (xcode_toolchains, _) = run_xcode_locator(repository_ctx, xcode_locator) + xcode_toolchains, _ = run_xcode_locator(repository_ctx, xcode_locator) - # Include extra Xcode paths in case they're installed on other volumes - for toolchain in xcode_toolchains: - include_dirs.append(toolchain.developer_dir) + # Include extra Xcode paths in case they're installed on other volumes + for toolchain in xcode_toolchains: + include_dirs.append(toolchain.developer_dir) return [" %s," % quote_starlark_string(path) for path in include_dirs] def _get_copts_env_var(repository_ctx, name, default = ""): + # type: (repository_ctx, str, str) -> list[str] """Get an environment variable and split it on ":" to be used as copts. Args: @@ -51,6 +54,7 @@ def _get_copts_env_var(repository_ctx, name, default = ""): return _split_escaped(default, ":") def _split_escaped(string, delimiter): + # type: (str, str) -> list[str] """Split string on the delimiter unless %-escaped. Examples: @@ -81,7 +85,7 @@ def _split_escaped(string, delimiter): i = 0 result = [] - accumulator = [] + accumulator = [] # type: list[str] length = len(string) delimiter_length = len(delimiter) @@ -101,7 +105,7 @@ def _split_escaped(string, delimiter): i += 1 + delimiter_length elif i + delimiter_length <= length and string[i:i + delimiter_length] == delimiter: result.append("".join(accumulator)) - accumulator = [] + accumulator = [] # type: list[str] i += delimiter_length else: accumulator.append(string[i]) @@ -112,6 +116,7 @@ def _split_escaped(string, delimiter): return result def _toolchain_env_impl(repository_ctx): + # type: (...) -> repo_metadata | None # All Label resolutions done at the top of the function to avoid issues # with starlark function restarts, see this: # https://github.com/bazelbuild/bazel/blob/ab71a1002c9c53a8061336e40f91204a2a32c38e/tools/cpp/lib_cc_configure.bzl#L17-L38 diff --git a/tools/http_dmg/http_dmg.bzl b/tools/http_dmg/http_dmg.bzl index aa50481d..e2c64b81 100644 --- a/tools/http_dmg/http_dmg.bzl +++ b/tools/http_dmg/http_dmg.bzl @@ -31,6 +31,7 @@ _DMG_IGNORED_ENTRIES = [ ] def _dmg_7z_args(): + # type: () -> list[str] """Arguments passed to 7-Zip when extracting a `.dmg` archive. Returns: @@ -55,7 +56,9 @@ def _dmg_7z_args(): return args +# TODO(https://github.com/bazelbuild/bazel/issues/32020): use a source annotation once Bazel exposes these context and metadata types. def _move(repository_ctx, src, dst): + # type: (repository_ctx, path, path) -> None # TODO: Delete the fallback once Bazel 7.x is no longer supported. # `repository_ctx.rename` was added in Bazel 8.0. if hasattr(repository_ctx, "rename"): @@ -92,6 +95,7 @@ def _extract_dmg( archive, strip_prefix = None, add_prefix = None): + # type: (repository_ctx, path, path, str | None, str | None) -> None """Extract a `.dmg` archive into the repository directory. Args: @@ -134,10 +138,11 @@ def _extract_dmg( repository_ctx.delete(temp_out_dir) def _http_dmg_impl(repository_ctx): + # type: (...) -> dict[str, object] | repo_metadata if repository_ctx.attr.build_file and repository_ctx.attr.build_file_content: fail("Only one of build_file and build_file_content can be provided.") - source_urls = _get_source_urls(repository_ctx) + source_urls = _get_source_urls(repository_ctx.attr.url, repository_ctx.attr.urls) zip7_dir = "7z" zip7_bin = zip7_download(repository_ctx, zip7_dir) @@ -194,17 +199,13 @@ def _http_dmg_impl(repository_ctx): return repository_ctx.repo_metadata(attrs_for_reproducibility = repro_attrs) return repro_attrs -def _get_source_urls(repository_ctx): +def _get_source_urls(url, urls): + # type: (str, list[str]) -> list[str] """Returns the urls provided via the `url` and `urls` attributes.""" - if not repository_ctx.attr.url and not repository_ctx.attr.urls: + if not url and not urls: fail("At least one of url and urls must be provided") - source_urls = [] - if repository_ctx.attr.urls: - source_urls = repository_ctx.attr.urls - if repository_ctx.attr.url: - source_urls = [repository_ctx.attr.url] + source_urls - return source_urls + return [url] + urls if url else urls _ATTRS = { "add_prefix": attr.string( diff --git a/tools/http_dmg/private/tests/http_dmg_test_deps.bzl b/tools/http_dmg/private/tests/http_dmg_test_deps.bzl index 6a9471a6..b584c04b 100644 --- a/tools/http_dmg/private/tests/http_dmg_test_deps.bzl +++ b/tools/http_dmg/private/tests/http_dmg_test_deps.bzl @@ -11,6 +11,7 @@ alias( """ def http_dmg_test_deps(): + # type: () -> list[str] """Download test dependencies for the `http_dmg` repository rule Returns: diff --git a/tools/http_dmg/private/tests/http_dmg_test_extensions.bzl b/tools/http_dmg/private/tests/http_dmg_test_extensions.bzl index 28a5447d..55d322e1 100644 --- a/tools/http_dmg/private/tests/http_dmg_test_extensions.bzl +++ b/tools/http_dmg/private/tests/http_dmg_test_extensions.bzl @@ -3,6 +3,9 @@ load(":http_dmg_test_deps.bzl", "http_dmg_test_deps") def _http_dmg_test_impl(module_ctx): + # type: (module_ctx) -> extension_metadata + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose context and metadata types. return module_ctx.extension_metadata( reproducible = True, root_module_direct_deps = [], diff --git a/tools/http_dmg/private/zip7.bzl b/tools/http_dmg/private/zip7.bzl index 5bcde615..26cdd70d 100644 --- a/tools/http_dmg/private/zip7.bzl +++ b/tools/http_dmg/private/zip7.bzl @@ -112,6 +112,7 @@ ZIP7_EXTRAS = { } def zip7_host_arch(ctx): + # type: (repository_ctx) -> str """Identify the host platform. Args: @@ -152,6 +153,7 @@ def zip7_extract( archive, output, extra_args = []): + # type: (repository_ctx, path, path, path, list[str]) -> None """Extract an archive using 7-Zip. Args: diff --git a/tools/http_dmg/private/zip7.bzli b/tools/http_dmg/private/zip7.bzli new file mode 100644 index 00000000..b95c0ed9 --- /dev/null +++ b/tools/http_dmg/private/zip7.bzli @@ -0,0 +1,8 @@ +"""Metadata for the pinned archive on each 7-Zip platform.""" + +type _ZipData = struct[{"urls": list[str], "integrity": str, "bin_path": str}] + +def _zip7_data(*, urls: list[str], integrity: str, bin_path: str) -> _ZipData: ... + +type _Auth = repository_ctx[struct[{"netrc": str, "auth_patterns": dict[str, str]}, ...]] +def zip7_download(repository_ctx: _Auth, output: str) -> path: ... diff --git a/xcode/available_xcodes.bzl b/xcode/available_xcodes.bzl index 2171f4c6..ff2cd41b 100644 --- a/xcode/available_xcodes.bzl +++ b/xcode/available_xcodes.bzl @@ -24,6 +24,7 @@ load( visibility("public") def _available_xcodes_impl(ctx): + # type: (...) -> list[AvailableXcodesInfo] # TODO: drop when requiring Bazel 8+ if not bazel_features.apple.xcode_config_migrated: fail("This rule is not available on the current Bazel version") diff --git a/xcode/providers.bzli b/xcode/providers.bzli new file mode 100644 index 00000000..226987b9 --- /dev/null +++ b/xcode/providers.bzli @@ -0,0 +1,38 @@ +# Copyright 2026 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. + +load("//xcode:providers.bzl", "XcodeSdkVariantInfo") + +# TODO(https://github.com/bazelbuild/bazel/issues/31770): annotate the Xcode provider initializer +# in Starlark once Bazel supports dictionaries with named, typed keys. +type _XcodeVersionPropertiesFields = dict[{ + "xcode_version": str | None, + "default_ios_sdk_version": str | None, + "default_macos_sdk_version": str | None, + "default_tvos_sdk_version": str | None, + "default_watchos_sdk_version": str | None, + "default_visionos_sdk_version": str | None, + "sdk_variant_info": XcodeSdkVariantInfo | None, +}] + +def _xcode_version_properties_info_init( + *, + xcode_version: str | None, + default_ios_sdk_version: str | None = ..., + default_macos_sdk_version: str | None = ..., + default_tvos_sdk_version: str | None = ..., + default_watchos_sdk_version: str | None = ..., + default_visionos_sdk_version: str | None = ..., + sdk_variant_info: XcodeSdkVariantInfo | None = ..., +) -> _XcodeVersionPropertiesFields: ... diff --git a/xcode/xcode_configure.bzl b/xcode/xcode_configure.bzl index 7d97baab..95e3f287 100644 --- a/xcode/xcode_configure.bzl +++ b/xcode/xcode_configure.bzl @@ -20,6 +20,9 @@ OSX_EXECUTE_TIMEOUT = 600 def _watch_xcode_version_files(repository_ctx, developer_dir): + # type: (repository_ctx | module_ctx, str | path) -> None + # TODO(https://github.com/bazelbuild/bazel/issues/32020): annotate repository_ctx + # when supported Bazel versions expose repository and module context types. # Module extensions cannot watch paths outside the workspace. if type(repository_ctx) != "repository_ctx": return @@ -31,6 +34,9 @@ def _watch_xcode_version_files(repository_ctx, developer_dir): repository_ctx.watch(developer_path.dirname.get_child("version.plist")) def watch_selected_xcode(repository_ctx): + # type: (repository_ctx | module_ctx) -> None + # TODO(https://github.com/bazelbuild/bazel/issues/32020): make this an + # annotation when supported Bazel versions expose repository and module context types. """Watches the selected Xcode's version.plist for repository rules. Args: @@ -60,6 +66,7 @@ def watch_selected_xcode(repository_ctx): _watch_xcode_version_files(repository_ctx, selected_path) def _search_string(fullstring, prefix, suffix): + # type: (str, str, str) -> str """Returns the substring between two given substrings of a larger string. Args: @@ -82,10 +89,12 @@ def _search_string(fullstring, prefix, suffix): return fullstring[result_start_index:suffix_index] def _search_sdk_output(output, sdkname): + # type: (str, str) -> str """Returns the SDK version given xcodebuild stdout and an sdkname.""" return _search_string(output, "(%s" % sdkname, ")") def _xcode_version_output(repository_ctx, name, version, aliases, developer_dir, timeout): + # type: (repository_ctx, str, str, list[str], str, int) -> str """Returns a string containing an xcode_version build target.""" build_contents = "" decorated_aliases = [] @@ -336,6 +345,7 @@ load("@apple_support//xcode:xcode_version.bzl", "xcode_version") return buildcontents def _impl(repository_ctx): + # type: (...) -> None """Implementation for the local_config_xcode repository rule. Generates a BUILD file containing a root xcode_config target named 'host_xcodes', @@ -369,6 +379,7 @@ xcode_autoconf = repository_rule( ) def xcode_configure(xcode_locator_label, remote_xcode_label = None): + # type: (str, str | None) -> None """Generates a repository containing host Xcode version information.""" xcode_autoconf( name = "local_config_xcode", @@ -377,6 +388,7 @@ def xcode_configure(xcode_locator_label, remote_xcode_label = None): ) def _xcode_configure_extension_impl(module_ctx): + # type: (module_ctx) -> extension_metadata xcode_configure("@bazel_tools//tools/osx:xcode_locator.m") return module_ctx.extension_metadata(reproducible = True) diff --git a/xcode/xcode_configure.bzli b/xcode/xcode_configure.bzli new file mode 100644 index 00000000..119d427c --- /dev/null +++ b/xcode/xcode_configure.bzli @@ -0,0 +1,9 @@ +"""Discovered Xcode installations.""" + +type _XcodeInstallation = struct[{"version": str, "aliases": list[str], "developer_dir": str}] + +def run_xcode_locator(repository_ctx: repository_ctx | module_ctx, xcode_locator_src_label: str | Label | path) -> tuple[list[_XcodeInstallation], str | None]: ... + +type _XcodeRepositoryAttrs = struct[{"xcode_locator": str, "remote_xcode": str}, ...] + +def _darwin_build_file(repository_ctx: repository_ctx[_XcodeRepositoryAttrs]) -> str: ... diff --git a/xcode/xcode_sdk_variant.bzl b/xcode/xcode_sdk_variant.bzl index 9d057485..281aa1f7 100644 --- a/xcode/xcode_sdk_variant.bzl +++ b/xcode/xcode_sdk_variant.bzl @@ -22,7 +22,9 @@ load( visibility("public") def _xcode_sdk_variant_impl(ctx): + # type: (...) -> list[XcodeSdkVariantInfo] def dotted_version_or_none(str): + # type: (str) -> DottedVersion | None if not str: return None return apple_common.dotted_version(str) diff --git a/xcode/xcode_version.bzl b/xcode/xcode_version.bzl index 4176e139..d5717570 100644 --- a/xcode/xcode_version.bzl +++ b/xcode/xcode_version.bzl @@ -28,6 +28,7 @@ load( visibility("public") def _xcode_version_impl(ctx): + # type: (...) -> list[XcodeVersionPropertiesInfo | XcodeVersionRuleInfo | DefaultInfo] # TODO: remove when we test with Bazel 8+ if not bazel_features.apple.xcode_config_migrated: fail("This rule is not available on the current Bazel version") From 48ced23a6618686b19606417e757891e0e20ee7a Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 29 Oct 2026 13:16:10 -0400 Subject: [PATCH 19/24] Check Apple type contracts with sty Check repository and toolchain implementations and report findings for the remaining tracked Starlark files. Validate the shell declarations against Skylib 1.9.0. Apply the pending rules_cc parameter comments only in the advisory workspace so native builds use the released C++ toolchain. --- .github/sty/rules_cc.MODULE.bazel | 6 +++ .github/sty/rules_cc.patch | 20 +++++++++ .github/workflows/sty.yml | 69 +++++++++++++++++++++++++++++++ MODULE.bazel | 8 ++++ lib/BUILD | 7 ++++ lib/private/BUILD | 2 + stubs/bazel_skylib/shell.bzli | 4 ++ stubs/bazel_skylib/stubs.toml | 9 ++++ sty.toml | 36 ++++++++++++++++ xcode/BUILD | 2 + 10 files changed, 163 insertions(+) create mode 100644 .github/sty/rules_cc.MODULE.bazel create mode 100644 .github/sty/rules_cc.patch create mode 100644 .github/workflows/sty.yml create mode 100644 stubs/bazel_skylib/shell.bzli create mode 100644 stubs/bazel_skylib/stubs.toml create mode 100644 sty.toml diff --git a/.github/sty/rules_cc.MODULE.bazel b/.github/sty/rules_cc.MODULE.bazel new file mode 100644 index 00000000..3ca54800 --- /dev/null +++ b/.github/sty/rules_cc.MODULE.bazel @@ -0,0 +1,6 @@ +# TODO(https://github.com/bazelbuild/rules_cc/pull/940): use the released C++ annotations. +single_version_override( + module_name = "rules_cc", + patch_strip = 1, + patches = ["//:.github/sty/rules_cc.patch"], +) diff --git a/.github/sty/rules_cc.patch b/.github/sty/rules_cc.patch new file mode 100644 index 00000000..e4ee791c --- /dev/null +++ b/.github/sty/rules_cc.patch @@ -0,0 +1,20 @@ +diff --git a/cc/private/cc_common.bzl b/cc/private/cc_common.bzl +index 4533f7f..2d005ec 100644 +--- a/cc/private/cc_common.bzl ++++ b/cc/private/cc_common.bzl +@@ -253,3 +253,5 @@ def _create_compilation_context( + quote_includes = None, +- framework_includes = None, ++ # TODO(https://github.com/bazelbuild/bazel/issues/32021): make this an ++ # annotation when supported Bazel versions expose element-typed depsets. ++ framework_includes = None, # type: depset[str] | None + defines = None, +diff --git a/cc/private/cc_info.bzl b/cc/private/cc_info.bzl +index 76eca95..7d6a599 100644 +--- a/cc/private/cc_info.bzl ++++ b/cc/private/cc_info.bzl +@@ -322,3 +322,3 @@ def _create_cc_info( + *, +- compilation_context = None, ++ compilation_context = None, # type: CcCompilationContextInfo | None + linking_context = None, diff --git a/.github/workflows/sty.yml b/.github/workflows/sty.yml new file mode 100644 index 00000000..f763566b --- /dev/null +++ b/.github/workflows/sty.yml @@ -0,0 +1,69 @@ +name: Starlark types + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + sty: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Prepare advisory inputs + env: + USE_BAZEL_VERSION: '9.3.0' + shell: bash + run: | + expected=rules_cc@0.2.26 + selected=$(bazelisk mod show_repo rules_cc --output=streamed_jsonproto | jq -r .moduleKey) + if [[ "$selected" != "$expected" ]]; then + echo "The advisory annotations cover $expected; Bazel selected $selected." >&2 + exit 1 + fi + cat .github/sty/rules_cc.MODULE.bazel >> MODULE.bazel + git -c core.quotePath=false ls-files -- '*.bzl' > "$RUNNER_TEMP/sty-root-files" + - uses: tamird/sty@ae287c79b273dcd955d29de379c7dc53922c7b48 + with: + audit-loads: true + advisory-files-from: ${{ runner.temp }}/sty-root-files + bazel-version: '9.3.0' + files: | + @bazel_skylib//lib:shell.bzl + configs/platforms.bzl + crosstool/osx_cc_configure.bzl + crosstool/setup.bzl + crosstool/setup_internal.bzl + crosstool/universal_exec_tool.bzl + lib/missing_platform_fallback_users.bzl + lib/private/toolchain_flags.bzl + lib/providers.bzl + rules/apple_genrule.bzl + rules/private/apple_genrule.bzl + rules/rules.doc.bzl + rules/toolchain_substitution.bzl + rules/universal_binary.bzl + test/apple_support_test.bzl + test/binary_tests.bzl + test/rules/apple_verification_test.bzl + test/test_data/framework_includes_provider.bzl + test/universal_binary_test.bzl + toolchain/dynamic_toolchain_info.bzl + toolchain/toolchain_env.bzl + tools/http_dmg/http_dmg.bzl + tools/http_dmg/private/tests/http_dmg_test_deps.bzl + tools/http_dmg/private/tests/http_dmg_test_extensions.bzl + xcode/available_xcodes.bzl + xcode/private/providers.bzl + xcode/xcode_sdk_variant.bzl + xcode/xcode_version.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 0064e4c3..4b9b2210 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -32,6 +32,14 @@ bazel_dep(name = "protobuf", version = "33.0", dev_dependency = True) http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "sty_bazel_tools", + dev_dependency = True, + integrity = "sha256-FayMMyMk37REW96xvJXMY1fYcWXBban9QgFAZUB+Kr0=", + strip_prefix = "sty-2ab74c830937d3ca85165ca0e9a02a42400ad9e5/crates/starpls_bazel/data/bazel_tools", + urls = ["https://github.com/tamird/sty/archive/2ab74c830937d3ca85165ca0e9a02a42400ad9e5.tar.gz"], +) + http_archive( name = "swift_linux_toolchain", build_file = "//test/linux_toolchain:swift_toolchain.BUILD", diff --git a/lib/BUILD b/lib/BUILD index a78fbbe2..98a735f8 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -9,6 +9,13 @@ package( exports_files(glob(["*.bzl"])) +exports_files([ + "apple_support.bzli", + "lipo.bzli", + "transitions.bzli", + "xcode_support.bzli", +]) + # This target is mainly for use by rules_apple and rules_swift. # When this target is linked into an XCTest binary, and the # `BUILD_WORKSPACE_DIRECTORY` environment variable is set, the relative paths of diff --git a/lib/private/BUILD b/lib/private/BUILD index 863d49e4..070b9a3c 100644 --- a/lib/private/BUILD +++ b/lib/private/BUILD @@ -4,6 +4,8 @@ licenses(["notice"]) package(default_visibility = ["//:__subpackages__"]) +exports_files(["providers.bzli"]) + bzl_library( name = "providers", srcs = ["providers.bzl"], diff --git a/stubs/bazel_skylib/shell.bzli b/stubs/bazel_skylib/shell.bzli new file mode 100644 index 00000000..192d4281 --- /dev/null +++ b/stubs/bazel_skylib/shell.bzli @@ -0,0 +1,4 @@ +# TODO(https://github.com/bazelbuild/bazel-skylib/pull/641): use the released shell annotation. +def _array_literal(iterable: Collection[object]) -> str: ... + +def _quote(s: str) -> str: ... diff --git a/stubs/bazel_skylib/stubs.toml b/stubs/bazel_skylib/stubs.toml new file mode 100644 index 00000000..c0e70397 --- /dev/null +++ b/stubs/bazel_skylib/stubs.toml @@ -0,0 +1,9 @@ +format-version = 1 + +[source] +repository = "@bazel_skylib" +module = "bazel_skylib" +versions = ["1.9.0"] + +[files] +"lib/shell.bzl" = "shell.bzli" diff --git a/sty.toml b/sty.toml new file mode 100644 index 00000000..f861b97c --- /dev/null +++ b/sty.toml @@ -0,0 +1,36 @@ +[analysis] +strict-generic-narrowing = true +require-valid-suppressions = true + +[type-interfaces] +"crosstool/osx_cc_configure.bzl" = "crosstool/osx_cc_configure.bzli" +"crosstool/universal_exec_tool.bzl" = "crosstool/universal_exec_tool.bzli" +"lib/apple_support.bzl" = "lib/apple_support.bzli" +"lib/private/providers.bzl" = "lib/private/providers.bzli" +"lib/lipo.bzl" = "lib/lipo.bzli" +"lib/transitions.bzl" = "lib/transitions.bzli" +"lib/xcode_support.bzl" = "lib/xcode_support.bzli" +"rules/apple_genrule.bzl" = "rules/apple_genrule.bzli" +"tools/http_dmg/private/zip7.bzl" = "tools/http_dmg/private/zip7.bzli" +"xcode/providers.bzl" = "xcode/providers.bzli" +"xcode/xcode_configure.bzl" = "xcode/xcode_configure.bzli" + +[rules] +missing-parameter-annotation = "error" +missing-return-annotation = "error" +missing-type-argument = "error" +unsound-assignment = "error" +unsound-return-statement = "error" +explicit-any = "error" +non-static-type = "error" +blanket-ignore-comment = "error" +possibly-unresolved-reference = "error" +possibly-missing-attribute = "error" +division-by-zero = "error" + +[[stub-packages]] +manifest = "@sty_bazel_tools//:stubs.toml" +allow-unversioned = true + +[[stub-packages]] +manifest = "stubs/bazel_skylib/stubs.toml" diff --git a/xcode/BUILD b/xcode/BUILD index 5b9da4d0..1e924ab2 100644 --- a/xcode/BUILD +++ b/xcode/BUILD @@ -9,7 +9,9 @@ package( ) exports_files([ + "providers.bzli", "xcode_configure.bzl", + "xcode_configure.bzli", "xcode_version_at_least.bzl", ]) From f373bbda2b5fca67eb90557b71d3491f40b43885 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 3 Nov 2026 09:56:55 -0500 Subject: [PATCH 20/24] Move crosstool contracts beside their implementations Describe the repository context and universal tool inputs in the source. Let sty discover the other adjacent interfaces and select tracked files through the shared action. --- .github/sty/rules_cc.MODULE.bazel | 3 ++- .github/workflows/sty.yml | 7 +++---- crosstool/osx_cc_configure.bzl | 7 +++++++ crosstool/osx_cc_configure.bzli | 3 --- crosstool/universal_exec_tool.bzl | 5 +++++ crosstool/universal_exec_tool.bzli | 10 ---------- stubs/bazel_skylib/shell.bzli | 3 ++- sty.toml | 13 ------------- 8 files changed, 19 insertions(+), 32 deletions(-) delete mode 100644 crosstool/osx_cc_configure.bzli delete mode 100644 crosstool/universal_exec_tool.bzli diff --git a/.github/sty/rules_cc.MODULE.bazel b/.github/sty/rules_cc.MODULE.bazel index 3ca54800..1f77fb85 100644 --- a/.github/sty/rules_cc.MODULE.bazel +++ b/.github/sty/rules_cc.MODULE.bazel @@ -1,4 +1,5 @@ -# TODO(https://github.com/bazelbuild/rules_cc/pull/940): use the released C++ annotations. +# TODO(https://github.com/bazelbuild/rules_cc/pull/940): Remove this override once +# the selected rules_cc release annotates framework_includes and compilation_context. single_version_override( module_name = "rules_cc", patch_strip = 1, diff --git a/.github/workflows/sty.yml b/.github/workflows/sty.yml index f763566b..c6a29a94 100644 --- a/.github/workflows/sty.yml +++ b/.github/workflows/sty.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - - name: Prepare advisory inputs + - name: Prepare C++ annotations env: USE_BAZEL_VERSION: '9.3.0' shell: bash @@ -32,11 +32,10 @@ jobs: exit 1 fi cat .github/sty/rules_cc.MODULE.bazel >> MODULE.bazel - git -c core.quotePath=false ls-files -- '*.bzl' > "$RUNNER_TEMP/sty-root-files" - - uses: tamird/sty@ae287c79b273dcd955d29de379c7dc53922c7b48 + - uses: tamird/sty@d400a00807b3c7f7b2ac82bf1e1e6a16db0290e3 with: audit-loads: true - advisory-files-from: ${{ runner.temp }}/sty-root-files + advisory-pathspecs: '*.bzl' bazel-version: '9.3.0' files: | @bazel_skylib//lib:shell.bzl diff --git a/crosstool/osx_cc_configure.bzl b/crosstool/osx_cc_configure.bzl index 99937723..148ddd7c 100644 --- a/crosstool/osx_cc_configure.bzl +++ b/crosstool/osx_cc_configure.bzl @@ -95,7 +95,14 @@ def _split_escaped(string, delimiter): result.append("".join(accumulator)) return result +# TODO(https://github.com/bazelbuild/bazel/pull/30373): Use a native alias when +# Bazel 8.8 is the minimum supported version. +# type _DeveloperDirectory = struct[{"developer_dir": str}, ...] + def _get_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): + # type: (repository_ctx, Collection[_DeveloperDirectory]) -> list[str] + # TODO(https://github.com/bazelbuild/bazel/issues/32020): Use a native + # signature when supported Bazel versions expose repository_ctx. """Compute the list of default C++ include paths on Xcode-enabled darwin. Args: diff --git a/crosstool/osx_cc_configure.bzli b/crosstool/osx_cc_configure.bzli deleted file mode 100644 index e2906b36..00000000 --- a/crosstool/osx_cc_configure.bzli +++ /dev/null @@ -1,3 +0,0 @@ -type _DeveloperDirectory = struct[{"developer_dir": str}, ...] - -def _get_xcode_cxx_inc_directories(repository_ctx: repository_ctx, xcode_toolchains: Collection[_DeveloperDirectory]) -> list[str]: ... diff --git a/crosstool/universal_exec_tool.bzl b/crosstool/universal_exec_tool.bzl index 7b592c7e..f44872fc 100644 --- a/crosstool/universal_exec_tool.bzl +++ b/crosstool/universal_exec_tool.bzl @@ -25,7 +25,12 @@ force_exec = rule( exec_compatible_with = ["@platforms//os:macos"], ) +# TODO(https://github.com/bazelbuild/bazel/issues/32021): Use native annotations +# once supported Bazel versions expose depset element types. +# type _Labels = Collection[Label | str] | depset[Label | str] + def universal_exec_tool(*, name, out, srcs): + # type: (str, Label | str, _Labels | select[_Labels | None] | None) -> None apple_genrule( name = name + ".target_config", srcs = srcs, diff --git a/crosstool/universal_exec_tool.bzli b/crosstool/universal_exec_tool.bzli deleted file mode 100644 index 1bddade9..00000000 --- a/crosstool/universal_exec_tool.bzli +++ /dev/null @@ -1,10 +0,0 @@ -"""Source inputs for execution-configured universal tools.""" - -type _Labels = Collection[Label | str] | depset[Label | str] - -def universal_exec_tool( - *, - name: str, - out: Label | str, - srcs: _Labels | select[_Labels | None] | None, -) -> None: ... diff --git a/stubs/bazel_skylib/shell.bzli b/stubs/bazel_skylib/shell.bzli index 192d4281..d3df3b8a 100644 --- a/stubs/bazel_skylib/shell.bzli +++ b/stubs/bazel_skylib/shell.bzli @@ -1,4 +1,5 @@ -# TODO(https://github.com/bazelbuild/bazel-skylib/pull/641): use the released shell annotation. +# TODO(https://github.com/bazelbuild/bazel-skylib/pull/641): Remove this external +# interface when the selected Skylib release supplies both shell helper contracts. def _array_literal(iterable: Collection[object]) -> str: ... def _quote(s: str) -> str: ... diff --git a/sty.toml b/sty.toml index f861b97c..381efcae 100644 --- a/sty.toml +++ b/sty.toml @@ -2,19 +2,6 @@ strict-generic-narrowing = true require-valid-suppressions = true -[type-interfaces] -"crosstool/osx_cc_configure.bzl" = "crosstool/osx_cc_configure.bzli" -"crosstool/universal_exec_tool.bzl" = "crosstool/universal_exec_tool.bzli" -"lib/apple_support.bzl" = "lib/apple_support.bzli" -"lib/private/providers.bzl" = "lib/private/providers.bzli" -"lib/lipo.bzl" = "lib/lipo.bzli" -"lib/transitions.bzl" = "lib/transitions.bzli" -"lib/xcode_support.bzl" = "lib/xcode_support.bzli" -"rules/apple_genrule.bzl" = "rules/apple_genrule.bzli" -"tools/http_dmg/private/zip7.bzl" = "tools/http_dmg/private/zip7.bzli" -"xcode/providers.bzl" = "xcode/providers.bzli" -"xcode/xcode_configure.bzl" = "xcode/xcode_configure.bzli" - [rules] missing-parameter-annotation = "error" missing-return-annotation = "error" From 913948d40ad49659dfa9794ca7dac1117766b14f Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 3 Nov 2026 10:29:18 -0500 Subject: [PATCH 21/24] Declare Xcode and 7-Zip helper types in source Place the Xcode context and locator types beside their consumers. Declare the 7-Zip download fields and function parameters in the implementation. Remove the three companion files and check these modules directly. --- .github/workflows/sty.yml | 3 +++ lib/BUILD | 1 - lib/lipo.bzli | 4 ++++ lib/private/providers.bzli | 3 +++ lib/xcode_support.bzl | 6 ++++++ lib/xcode_support.bzli | 9 --------- rules/apple_genrule.bzli | 4 ++++ tools/http_dmg/private/zip7.bzl | 7 +++++++ tools/http_dmg/private/zip7.bzli | 8 -------- xcode/BUILD | 1 - xcode/xcode_configure.bzl | 7 +++++++ xcode/xcode_configure.bzli | 9 --------- 12 files changed, 34 insertions(+), 28 deletions(-) delete mode 100644 lib/xcode_support.bzli delete mode 100644 tools/http_dmg/private/zip7.bzli delete mode 100644 xcode/xcode_configure.bzli diff --git a/.github/workflows/sty.yml b/.github/workflows/sty.yml index c6a29a94..bf387c02 100644 --- a/.github/workflows/sty.yml +++ b/.github/workflows/sty.yml @@ -47,6 +47,7 @@ jobs: lib/missing_platform_fallback_users.bzl lib/private/toolchain_flags.bzl lib/providers.bzl + lib/xcode_support.bzl rules/apple_genrule.bzl rules/private/apple_genrule.bzl rules/rules.doc.bzl @@ -62,7 +63,9 @@ jobs: tools/http_dmg/http_dmg.bzl tools/http_dmg/private/tests/http_dmg_test_deps.bzl tools/http_dmg/private/tests/http_dmg_test_extensions.bzl + tools/http_dmg/private/zip7.bzl xcode/available_xcodes.bzl xcode/private/providers.bzl + xcode/xcode_configure.bzl xcode/xcode_sdk_variant.bzl xcode/xcode_version.bzl diff --git a/lib/BUILD b/lib/BUILD index 98a735f8..5d2e0564 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -13,7 +13,6 @@ exports_files([ "apple_support.bzli", "lipo.bzli", "transitions.bzli", - "xcode_support.bzli", ]) # This target is mainly for use by rules_apple and rules_swift. diff --git a/lib/lipo.bzli b/lib/lipo.bzli index 60454e92..4a92bc9d 100644 --- a/lib/lipo.bzli +++ b/lib/lipo.bzli @@ -1,5 +1,9 @@ """Inputs for actions that assemble and extract universal binaries.""" +# TODO(https://github.com/bazelbuild/bazel/issues/32343): Move these signatures into lipo.bzl when the +# action interface can describe the run_shell keywords in source. A type comment +# cannot import RunShellActions from apple_support.bzli. + load("//lib:apple_support.bzli", _Actions = "RunShellActions") load("//lib/private:providers.bzl", "ApplePlatformInfo") diff --git a/lib/private/providers.bzli b/lib/private/providers.bzli index 66278b28..445d6f65 100644 --- a/lib/private/providers.bzli +++ b/lib/private/providers.bzli @@ -14,6 +14,9 @@ """The platform information assembled from Bazel constraints.""" +# TODO(https://github.com/bazelbuild/bazel/issues/32207): Move the result fields to ApplePlatformInfo +# when Bazel can type provider fields independently of its banned public initializer. + def ApplePlatformInfo() -> record[{ "target_arch": str | None, "target_build_config": configuration, diff --git a/lib/xcode_support.bzl b/lib/xcode_support.bzl index e4d5be4d..8911ef45 100644 --- a/lib/xcode_support.bzl +++ b/lib/xcode_support.bzl @@ -31,7 +31,12 @@ load( visibility("public") +# TODO(https://github.com/bazelbuild/bazel/issues/32369): Use Ctx when Bazel checks the rule’s +# _xcode_config attribute on the context passed to these helpers. +# type _Context = ctx[object, struct[{"_xcode_config": Target | None}, ...]] + def _get_current_sdk(ctx): + # type: (_Context) -> XcodeSdkVariantInfo """Returns the `XcodeSdkVariantInfo` provider for the current configuration. Callers of this function must define the `_xcode_config` attribute in their @@ -52,6 +57,7 @@ def _get_current_sdk(ctx): return xcode_config[XcodeSdkVariantInfo] def _get_current_xcode(ctx): + # type: (_Context) -> XcodeVersionConfig """Returns the `XcodeVersionConfig` provider for the current configuration. Callers of this function must define the `_xcode_config` attribute in their diff --git a/lib/xcode_support.bzli b/lib/xcode_support.bzli deleted file mode 100644 index 6b243a5a..00000000 --- a/lib/xcode_support.bzli +++ /dev/null @@ -1,9 +0,0 @@ -"""Configured Xcode providers and version comparisons.""" - -load("//xcode:providers.bzl", "XcodeSdkVariantInfo") - -type _Context = ctx[object, struct[{"_xcode_config": Target | None}, ...]] - -def _get_current_sdk(ctx: _Context) -> XcodeSdkVariantInfo: ... - -def _get_current_xcode(ctx: _Context) -> XcodeVersionConfig: ... diff --git a/rules/apple_genrule.bzli b/rules/apple_genrule.bzli index 8f6cabea..5252ba61 100644 --- a/rules/apple_genrule.bzli +++ b/rules/apple_genrule.bzli @@ -1,5 +1,9 @@ """Arguments for Apple genrules and their executable copy rules.""" +# TODO(https://github.com/bazelbuild/bazel/issues/32343): Move the wrapper signature into its source +# when Bazel can refer to the common rule keywords left after the wrapper consumes +# expect_failure and toolchains. A comment cannot declare the callable protocol. + type _Labels = Collection[Label | str] | depset[Label | str] type _Configurable[T] = T | select[T | None] | None diff --git a/tools/http_dmg/private/zip7.bzl b/tools/http_dmg/private/zip7.bzl index 26cdd70d..fe247398 100644 --- a/tools/http_dmg/private/zip7.bzl +++ b/tools/http_dmg/private/zip7.bzl @@ -10,11 +10,17 @@ load( "get_auth", ) +# TODO(https://github.com/bazelbuild/bazel/issues/32020): Use a source annotation for the +# repository context when supported Bazel versions expose its attributes. +# type _ZipData = struct[{"urls": list[str], "integrity": str, "bin_path": str}] +# type _Auth = repository_ctx[struct[{"netrc": str, "auth_patterns": dict[str, str]}, ...]] + def _zip7_data( *, urls, integrity, bin_path): + # type: (list[str], str, str) -> _ZipData return struct( urls = urls, integrity = integrity, @@ -187,6 +193,7 @@ _BAZEL_ARCHIVE_SUFFIXES = ( ) def zip7_download(repository_ctx, output): + # type: (_Auth, str) -> path """Download a pinned 7-Zip binary for the host platform. Args: diff --git a/tools/http_dmg/private/zip7.bzli b/tools/http_dmg/private/zip7.bzli deleted file mode 100644 index b95c0ed9..00000000 --- a/tools/http_dmg/private/zip7.bzli +++ /dev/null @@ -1,8 +0,0 @@ -"""Metadata for the pinned archive on each 7-Zip platform.""" - -type _ZipData = struct[{"urls": list[str], "integrity": str, "bin_path": str}] - -def _zip7_data(*, urls: list[str], integrity: str, bin_path: str) -> _ZipData: ... - -type _Auth = repository_ctx[struct[{"netrc": str, "auth_patterns": dict[str, str]}, ...]] -def zip7_download(repository_ctx: _Auth, output: str) -> path: ... diff --git a/xcode/BUILD b/xcode/BUILD index 1e924ab2..ea7c52e6 100644 --- a/xcode/BUILD +++ b/xcode/BUILD @@ -11,7 +11,6 @@ package( exports_files([ "providers.bzli", "xcode_configure.bzl", - "xcode_configure.bzli", "xcode_version_at_least.bzl", ]) diff --git a/xcode/xcode_configure.bzl b/xcode/xcode_configure.bzl index 95e3f287..c68efc53 100644 --- a/xcode/xcode_configure.bzl +++ b/xcode/xcode_configure.bzl @@ -148,7 +148,13 @@ detected_xcodes(name = 'detected_xcodes') xcode_config(name = 'host_xcodes') """ +# TODO(https://github.com/bazelbuild/bazel/issues/32020): Annotate the locator in source when +# supported Bazel versions expose both the module and repository contexts. +# type _XcodeInstallation = struct[{"version": str, "aliases": list[str], "developer_dir": str}] +# type _XcodeRepositoryAttrs = struct[{"xcode_locator": str, "remote_xcode": str}, ...] + def run_xcode_locator(repository_ctx, xcode_locator_src_label): + # type: (repository_ctx | module_ctx, str | Label | path) -> tuple[list[_XcodeInstallation], str | None] """Generates xcode-locator from source and runs it. Builds xcode-locator in the current repository directory. @@ -252,6 +258,7 @@ def run_xcode_locator(repository_ctx, xcode_locator_src_label): return (xcode_toolchains, None) def _darwin_build_file(repository_ctx): + # type: (repository_ctx[_XcodeRepositoryAttrs]) -> str """Evaluates local system state to create xcode_config and xcode_version targets.""" repository_ctx.report_progress("Fetching the default Xcode version") env = repository_ctx.os.environ diff --git a/xcode/xcode_configure.bzli b/xcode/xcode_configure.bzli deleted file mode 100644 index 119d427c..00000000 --- a/xcode/xcode_configure.bzli +++ /dev/null @@ -1,9 +0,0 @@ -"""Discovered Xcode installations.""" - -type _XcodeInstallation = struct[{"version": str, "aliases": list[str], "developer_dir": str}] - -def run_xcode_locator(repository_ctx: repository_ctx | module_ctx, xcode_locator_src_label: str | Label | path) -> tuple[list[_XcodeInstallation], str | None]: ... - -type _XcodeRepositoryAttrs = struct[{"xcode_locator": str, "remote_xcode": str}, ...] - -def _darwin_build_file(repository_ctx: repository_ctx[_XcodeRepositoryAttrs]) -> str: ... From d975571a74b7dc20ac8ee4800eeeef87753db5eb Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 3 Nov 2026 10:51:23 -0500 Subject: [PATCH 22/24] Explain the source annotation limits Identify the native types each exceptional comment needs. Document the language boundary beside its use. --- crosstool/universal_exec_tool.bzl | 3 +++ sty.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crosstool/universal_exec_tool.bzl b/crosstool/universal_exec_tool.bzl index f44872fc..ef7b173c 100644 --- a/crosstool/universal_exec_tool.bzl +++ b/crosstool/universal_exec_tool.bzl @@ -29,6 +29,9 @@ force_exec = rule( # once supported Bazel versions expose depset element types. # type _Labels = Collection[Label | str] | depset[Label | str] +# TODO(https://github.com/tamird/sty/blob/sty-action-v0.2.0/docs/type-interfaces.md#selectors): +# Use a native parameter annotation when Bazel can describe configurable srcs +# whose selected value can be a list, a depset, or None. def universal_exec_tool(*, name, out, srcs): # type: (str, Label | str, _Labels | select[_Labels | None] | None) -> None apple_genrule( diff --git a/sty.toml b/sty.toml index 381efcae..30a4c631 100644 --- a/sty.toml +++ b/sty.toml @@ -1,3 +1,6 @@ +# TODO(https://github.com/bazelbuild/bazel/pull/30373): Move ordinary signatures +# into function declarations once the minimum Bazel version supports annotation +# syntax. Bazel 7.4 must still be able to load these files. [analysis] strict-generic-narrowing = true require-valid-suppressions = true From 579ca885ff81ea32ada8eccfc5cefecea599aade Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 3 Nov 2026 10:51:23 -0500 Subject: [PATCH 23/24] Reference sty by release tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the repository’s release-tag convention for external actions. Use the same checkout and checker revisions as the previous workflow. --- .github/workflows/sty.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sty.yml b/.github/workflows/sty.yml index bf387c02..8e005d33 100644 --- a/.github/workflows/sty.yml +++ b/.github/workflows/sty.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Prepare C++ annotations @@ -32,7 +32,7 @@ jobs: exit 1 fi cat .github/sty/rules_cc.MODULE.bazel >> MODULE.bazel - - uses: tamird/sty@d400a00807b3c7f7b2ac82bf1e1e6a16db0290e3 + - uses: tamird/sty@sty-action-v0.2.0 with: audit-loads: true advisory-pathspecs: '*.bzl' From 6e47ac0b4422ea9d9b9a7e3ae7889988f23fc3d8 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 3 Nov 2026 12:57:48 -0500 Subject: [PATCH 24/24] Select the strict sty rules as a group Set one severity for the eleven strict diagnostics. Let the pinned checker define the group so an upgrade can extend it without edits to the project configuration. --- sty.toml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sty.toml b/sty.toml index 30a4c631..13cc002f 100644 --- a/sty.toml +++ b/sty.toml @@ -6,17 +6,7 @@ strict-generic-narrowing = true require-valid-suppressions = true [rules] -missing-parameter-annotation = "error" -missing-return-annotation = "error" -missing-type-argument = "error" -unsound-assignment = "error" -unsound-return-statement = "error" -explicit-any = "error" -non-static-type = "error" -blanket-ignore-comment = "error" -possibly-unresolved-reference = "error" -possibly-missing-attribute = "error" -division-by-zero = "error" +strict = "error" [[stub-packages]] manifest = "@sty_bazel_tools//:stubs.toml"