diff --git a/.github/sty/rules_cc.MODULE.bazel b/.github/sty/rules_cc.MODULE.bazel new file mode 100644 index 00000000..1f77fb85 --- /dev/null +++ b/.github/sty/rules_cc.MODULE.bazel @@ -0,0 +1,7 @@ +# 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, + 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..8e005d33 --- /dev/null +++ b/.github/workflows/sty.yml @@ -0,0 +1,71 @@ +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@v4 + with: + persist-credentials: false + - name: Prepare C++ annotations + 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 + - uses: tamird/sty@sty-action-v0.2.0 + with: + audit-loads: true + advisory-pathspecs: '*.bzl' + 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 + lib/xcode_support.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 + 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/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/crosstool/osx_cc_configure.bzl b/crosstool/osx_cc_configure.bzl index b08c4435..148ddd7c 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]) @@ -93,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: @@ -123,6 +132,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 +148,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 +172,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/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..ef7b173c 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 [ @@ -24,7 +25,15 @@ 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] + +# 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( name = name + ".target_config", srcs = srcs, diff --git a/lib/BUILD b/lib/BUILD index a78fbbe2..5d2e0564 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -9,6 +9,12 @@ package( exports_files(glob(["*.bzl"])) +exports_files([ + "apple_support.bzli", + "lipo.bzli", + "transitions.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/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..4a92bc9d --- /dev/null +++ b/lib/lipo.bzli @@ -0,0 +1,30 @@ +"""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") + +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/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/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..445d6f65 --- /dev/null +++ b/lib/private/providers.bzli @@ -0,0 +1,26 @@ +# 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.""" + +# 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, + "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..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 @@ -72,6 +78,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 +96,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/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..5252ba61 --- /dev/null +++ b/rules/apple_genrule.bzli @@ -0,0 +1,28 @@ +"""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 + +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/stubs/bazel_skylib/shell.bzli b/stubs/bazel_skylib/shell.bzli new file mode 100644 index 00000000..d3df3b8a --- /dev/null +++ b/stubs/bazel_skylib/shell.bzli @@ -0,0 +1,5 @@ +# 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/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..13cc002f --- /dev/null +++ b/sty.toml @@ -0,0 +1,16 @@ +# 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 + +[rules] +strict = "error" + +[[stub-packages]] +manifest = "@sty_bazel_tools//:stubs.toml" +allow-unversioned = true + +[[stub-packages]] +manifest = "stubs/bazel_skylib/stubs.toml" 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..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, @@ -112,6 +118,7 @@ ZIP7_EXTRAS = { } def zip7_host_arch(ctx): + # type: (repository_ctx) -> str """Identify the host platform. Args: @@ -152,6 +159,7 @@ def zip7_extract( archive, output, extra_args = []): + # type: (repository_ctx, path, path, path, list[str]) -> None """Extract an archive using 7-Zip. Args: @@ -185,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/xcode/BUILD b/xcode/BUILD index 5b9da4d0..ea7c52e6 100644 --- a/xcode/BUILD +++ b/xcode/BUILD @@ -9,6 +9,7 @@ package( ) exports_files([ + "providers.bzli", "xcode_configure.bzl", "xcode_version_at_least.bzl", ]) 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..c68efc53 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 = [] @@ -139,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. @@ -243,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 @@ -336,6 +352,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 +386,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 +395,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_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")