From df816cacdaecc9664fdc6b7b397bdea576bbdd48 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Thu, 29 Oct 2026 13:16:10 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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"