diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 44dd7fc2..8755fb10 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -201,6 +201,7 @@ tasks: - test/shell/layering_check_test.sh - test/shell/header_parsing_test.sh - test/shell/coverage_test.sh + - test/shell/toolchain_flags_test.sh macos_latest_shell_scripts_toolchain_off: name: "Shell tests (rules toolchain off)" @@ -210,6 +211,7 @@ tasks: - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/layering_check_test.sh' - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/header_parsing_test.sh' - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/coverage_test.sh' + - 'BAZEL_EXTRA_ARGS="--repo_env=APPLE_SUPPORT_RULES_BASED_TOOLCHAIN=0" test/shell/toolchain_flags_test.sh' macos_last_green: name: "Last Green Bazel" diff --git a/.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 cc777249..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", @@ -55,6 +63,7 @@ dmg_arc_test_deps = use_extension("//tools/http_dmg/private/tests:http_dmg_test_ use_repo( dmg_arc_test_deps, "http_dmg_test_firefox", + "http_dmg_test_firefox_patched", "http_dmg_test_firefox_strip_prefix", "http_dmg_test_krita", "http_dmg_test_krita_strip_prefix", diff --git a/crosstool/libtool.cc b/crosstool/libtool.cc index e16d83b3..6da4ff6e 100644 --- a/crosstool/libtool.cc +++ b/crosstool/libtool.cc @@ -294,6 +294,12 @@ void processArCreateArgs( flags_consumer("-D"); // NOTE: Always added for hermiticity flags_consumer("-o"); flags_consumer(args[1]); + // libtool creates a new archive, whereas ar q appends to an existing one. + // Include the previous archive so batched callers such as cc-rs retain it. + if (args[0].find('q') != std::string::npos && + std::filesystem::exists(args[1])) { + flags_consumer(args[1]); + } } std::vector expandResponseFiles( diff --git a/crosstool/osx_cc_configure.bzl b/crosstool/osx_cc_configure.bzl index 1a23d670..148ddd7c 100644 --- a/crosstool/osx_cc_configure.bzl +++ b/crosstool/osx_cc_configure.bzl @@ -14,13 +14,11 @@ # limitations under the License. """Configuring the C++ toolchain on macOS.""" -load( - "@bazel_tools//tools/cpp:lib_cc_configure.bzl", - "escape_string", -) +load("//lib/private:toolchain_flags.bzl", "get_starlark_list", "quote_starlark_string") load("//xcode:xcode_configure.bzl", "run_xcode_locator", "watch_selected_xcode") def _get_copts_env_var(repository_ctx, name, default = ""): + # type: (repository_ctx, str, str) -> list[str] """Get an environment variable and split it on ":" to be used as copts. Args: @@ -36,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: @@ -66,7 +65,7 @@ def _split_escaped(string, delimiter): i = 0 result = [] - accumulator = [] + accumulator = [] # type: list[str] length = len(string) delimiter_length = len(delimiter) @@ -86,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]) @@ -96,13 +95,14 @@ def _split_escaped(string, delimiter): result.append("".join(accumulator)) return result -def _get_starlark_list(values): - """Convert a list of string into a string that can be passed to a rule attribute.""" - if not values: - return "" - return "\"" + "\",\n \"".join(values) + "\"" +# 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_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): +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: @@ -127,11 +127,14 @@ def _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): # Include extra Xcode paths in case they're installed on other volumes for toolchain in xcode_toolchains: - include_dirs.append(escape_string(toolchain.developer_dir)) + include_dirs.append(toolchain.developer_dir) return include_dirs 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", @@ -145,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: @@ -165,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: @@ -193,10 +200,9 @@ def configure_osx_toolchain(repository_ctx): if _succeeds(repository_ctx, "ld", "-reproducible", "-v"): features.append("reproducible_linker_flag") - escaped_include_paths = _get_escaped_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains) escaped_cxx_include_directories = [] - for path in escaped_include_paths: - escaped_cxx_include_directories.append((" \"%s\"," % path)) + for path in _get_xcode_cxx_inc_directories(repository_ctx, xcode_toolchains): + escaped_cxx_include_directories.append(" %s," % quote_starlark_string(path)) if xcodeloc_err: escaped_cxx_include_directories.append(" # Error: " + xcodeloc_err) @@ -209,13 +215,13 @@ def configure_osx_toolchain(repository_ctx): "BUILD", build_template, { - "%{c_flags}": _get_starlark_list(c_opts), - "%{conly_flags}": _get_starlark_list(conly_opts), + "%{c_flags}": get_starlark_list(c_opts), + "%{conly_flags}": get_starlark_list(conly_opts), "%{cxx_builtin_include_directories}": "\n".join(escaped_cxx_include_directories), - "%{cxx_flags}": _get_starlark_list(cxx_opts), + "%{cxx_flags}": get_starlark_list(cxx_opts), "%{features}": "\n".join([' "{}",'.format(x) for x in features]), "%{layering_check_modulemap}": "\"@apple_support//crosstool:exec_layering_check_modulemap\"," if enable_layering_check else "", - "%{link_flags}": _get_starlark_list(link_opts), + "%{link_flags}": get_starlark_list(link_opts), "%{placeholder_modulemap}": "\"@apple_support//crosstool:module.modulemap\"" if enable_layering_check else "None", "%{tool_paths_overrides}": ",\n ".join( ['"%s": "%s"' % (k, v) for k, v in tool_paths.items()], diff --git a/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/doc/xcode_version_at_least.md b/doc/xcode_version_at_least.md index 9c7bf834..01b39741 100755 --- a/doc/xcode_version_at_least.md +++ b/doc/xcode_version_at_least.md @@ -22,7 +22,8 @@ Creates a `config_setting` target that matches when Xcode is at least `minimum_x The condition compares the resolved Xcode version, including when Xcode is selected by default without an explicit `--xcode_version` flag. It does not -match if the resolved configuration has no Xcode version. +match if the resolved configuration has no Xcode version. A developer +directory is assumed to represent a recent Xcode and matches any minimum. For example, enable a definition with Xcode 26.4 or newer: @@ -70,11 +71,11 @@ With Xcode 27 or newer, both conditions match, but the more specialized `xcode_27_or_newer` branch wins. Without `after`, different values for these overlapping conditions would make the `select()` ambiguous. -Declare lower thresholds before higher thresholds and use local target names -or `:name` labels to inherit a same-package `after` chain transitively. For -cross-package or fully qualified references, list every lower threshold in -the chain explicitly. Each threshold must be strictly higher than those in -its `after` list. +Declare lower thresholds before higher thresholds to inherit a same-package +`after` chain transitively, whether references are strings or `Label` objects. +For cross-package references, list every lower threshold in the chain +explicitly. Each threshold must be strictly higher than those in its `after` +list. **PARAMETERS** @@ -84,7 +85,7 @@ its `after` list. | :------------- | :------------- | :------------- | | name | The name of the `config_setting` target to create. A companion feature flag target named `_flag` will also be created. | none | | minimum_xcode_version | The minimum Xcode version to match (inclusive), such as `"26.4"`, `"26.4.1"`, or `"27.0.0.27A5228h"`. | none | -| after | An optional list of lower-version `xcode_version_at_least` target names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited transitively. | `None` | +| after | An optional list of lower-version `xcode_version_at_least` target names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited transitively, whether the reference is a string or a `Label` object. | `None` | | visibility | The visibility of the generated targets. | `None` | | kwargs | Additional keyword arguments (such as `tags` or `constraint_values`) forwarded to `native.config_setting`. `tags` is also forwarded to the companion `_flag` target. | none | diff --git a/lib/BUILD b/lib/BUILD index d2fa327c..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 @@ -37,7 +43,6 @@ bzl_library( ":missing_platform_fallback_users", ":providers", "//lib/private:providers", - "@bazel_skylib//lib:types", ], ) diff --git a/lib/apple_support.bzl b/lib/apple_support.bzl index 1b9630c3..f941992d 100644 --- a/lib/apple_support.bzl +++ b/lib/apple_support.bzl @@ -23,7 +23,6 @@ load("@apple_support//lib:apple_support.bzl", "apple_support") ``` """ -load("@bazel_skylib//lib:types.bzl", "types") load( "//lib:missing_platform_fallback_users.bzl", "ALLOWED_USERS_OF_MISSING_PLATFORM_FALLBACK", @@ -187,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 @@ -200,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 @@ -212,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 @@ -229,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. @@ -244,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 @@ -281,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 \ @@ -386,17 +387,18 @@ 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 # that will hopefully be unique. - output0 = kwargs.get("outputs")[0] + output0 = kwargs["outputs"][0] if xcode_path_resolve_level == _XCODE_PATH_RESOLVE_LEVEL.args: script = _XCODE_PROCESSOR__ARGS suffix = "args" @@ -410,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 = [] @@ -424,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 @@ -436,32 +438,30 @@ 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) # We also need to include the user executable in the "tools" argument of the action, since it # won't be referenced by "executable" anymore. - original_tools = processed_kwargs.pop("tools", None) - if types.is_list(original_tools): - all_tools = [original_executable] + original_tools - elif types.is_depset(original_tools): - all_tools = depset([original_executable], transitive = [original_tools]) + all_tools = [] + if type(original_executable) != "string": + all_tools.append(original_executable) + + 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()) + elif type(original_tools) in ("list", "tuple"): + all_tools.extend(original_tools) elif original_tools: fail("'tools' argument must be a sequence or depset.") - elif not types.is_string(original_executable): - # Only add the user_executable to the "tools" list if it's a File, not a string. - all_tools = [original_executable] - else: - all_tools = [] - actions.run( - executable = processor_script, - 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( *, @@ -496,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 56a4b29d..ead2f3c6 100644 --- a/lib/lipo.bzl +++ b/lib/lipo.bzl @@ -47,6 +47,8 @@ def _create( xcode_config: The `apple_common.XcodeVersionConfig` provider used to configure the action environment. """ + if type(inputs) == "depset": + inputs = inputs.to_list() if not inputs: fail("lipo.create requires at least one input file.") @@ -105,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.") @@ -122,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 0d36d37c..070b9a3c 100644 --- a/lib/private/BUILD +++ b/lib/private/BUILD @@ -4,7 +4,14 @@ licenses(["notice"]) package(default_visibility = ["//:__subpackages__"]) +exports_files(["providers.bzli"]) + bzl_library( name = "providers", srcs = ["providers.bzl"], ) + +bzl_library( + name = "toolchain_flags", + srcs = ["toolchain_flags.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 new file mode 100644 index 00000000..2a8aaf08 --- /dev/null +++ b/lib/private/toolchain_flags.bzl @@ -0,0 +1,32 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Format Apple toolchain flags and paths for generated BUILD files.""" + +visibility([ + "//crosstool", + "//toolchain", +]) + +def get_starlark_list(values): + # 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. + return '"%s"' % value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r") 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 9de53418..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 @@ -61,21 +66,15 @@ def _apple_genrule_impl(ctx): label_dict = {dep.label: dep[DefaultInfo].files.to_list() for dep in ctx.attr.srcs} execution_requirements = {} - extra_args = {} - run = ctx.actions.run + xcode_config = None + apple_platform_info = None if ctx.toolchains[_APPLE_GENRULE_TOOLCHAIN_TYPE] != None: xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] execution_requirements.update(xcode_config.execution_info()) - extra_args = { - "actions": ctx.actions, - "xcode_config": xcode_config, - "apple_fragment": ctx.fragments.apple, - "apple_platform_info": apple_support.platform_info_from_rule_ctx( - ctx, - fail_on_missing_constraint = False, - ), - } - run = apple_support.run + apple_platform_info = apple_support.platform_info_from_rule_ctx( + ctx, + fail_on_missing_constraint = False, + ) if ctx.attr.no_sandbox: execution_requirements["no-sandbox"] = "1" @@ -97,7 +96,7 @@ def _apple_genrule_impl(ctx): message = ctx.attr.message or "Executing apple_genrule" - run( + action_args = dict( executable = argv[0], arguments = argv[1:], inputs = depset(resolved_inputs + [ctx.file._genrule_setup], transitive = [resolved_srcs]), @@ -107,8 +106,17 @@ def _apple_genrule_impl(ctx): execution_requirements = execution_requirements, progress_message = "%s %s" % (message, ctx.label), mnemonic = "Genrule", - **extra_args ) + if xcode_config != None: + apple_support.run( + actions = ctx.actions, + xcode_config = xcode_config, + apple_fragment = ctx.fragments.apple, + apple_platform_info = apple_platform_info, + **action_args + ) + else: + ctx.actions.run(**action_args) return [ DefaultInfo( 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 1d3a5580..b0e11876 100644 --- a/test/BUILD +++ b/test/BUILD @@ -13,12 +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", "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") @@ -45,6 +46,12 @@ xcode_config_test(name = "xcode_config_test") 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") @@ -82,6 +89,7 @@ bzl_library( srcs = glob(["*.bzl"]), deps = [ "//lib:apple_support", + "//lib:lipo", "//lib:xcode_support", "//rules:apple_genrule", "//xcode:available_xcodes", @@ -89,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 5f1d76ec..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( @@ -229,12 +231,33 @@ def _apple_support_test_impl(ctx): exec_group = "mac_exec_group", ) + # A string executable names a host tool; only the declared tools are inputs. + string_executable_outputs = [] + for kind, tools in [ + ("list", [test_tool]), + ("tuple", (test_tool,)), + ("depset", depset([test_tool])), + ]: + output = ctx.actions.declare_file("{}_string_executable_{}".format(ctx.label.name, kind)) + apple_support.run( + actions = ctx.actions, + xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig], + apple_platform_info = apple_platform_info, + outputs = [output], + executable = "/bin/bash", + tools = tools, + arguments = [test_tool.path, output.path], + xcode_path_resolve_level = apple_support.xcode_path_resolve_level.args, + exec_group = "mac_exec_group", + ) + string_executable_outputs.append(output) + test_files = [ run_output, run_output_xcode_path_in_args, run_output_xcode_path_in_file, run_shell_output, - ] + ] + string_executable_outputs test_script = ctx.actions.declare_file("{}_test_script".format(ctx.label.name)) ctx.actions.write(test_script, _TEST_SCRIPT_CONTENTS.format( @@ -249,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 c6548933..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: @@ -78,6 +79,16 @@ def binary_test_suite(name): target_under_test = "//test/test_data:visionos_binary", ) + apple_verification_test( + name = "{}_visionos_default_simulator_test".format(name), + tags = [name], + build_type = "simulator", + cpus = {"visionos_cpus": ""}, + expected_platform_type = "visionos", + verifier_script = "//test/shell:verify_binary.sh", + target_under_test = "//test/test_data:visionos_binary", + ) + apple_verification_test( name = "{}_visionos_arm64_simulator_test".format(name), tags = [name], diff --git a/test/lipo_test.bzl b/test/lipo_test.bzl new file mode 100644 index 00000000..88a08d83 --- /dev/null +++ b/test/lipo_test.bzl @@ -0,0 +1,83 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the lipo command construction helpers.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//lib:apple_support.bzl", "apple_support") +load("//lib:lipo.bzl", "lipo") + +def _create_from_depset_test_impl(ctx): + env = unittest.begin(ctx) + inputs = [ctx.actions.declare_file("first binary"), ctx.actions.declare_file("second binary")] + output = ctx.actions.declare_file("universal binary") + for file in inputs + [output]: + ctx.actions.write(file, "") + calls = [] + actions = struct(run_shell = lambda **kwargs: calls.append(kwargs)) + for files in [inputs, depset(inputs)]: + lipo.create( + actions = actions, + inputs = files, + output = output, + apple_platform_info = struct(platform = apple_common.platform.macos), + xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig], + ) + asserts.equals(env, 2, len(calls)) + asserts.equals(env, calls[0]["command"], calls[1]["command"]) + asserts.equals(env, inputs, calls[1]["inputs"]) + for file in inputs + [output]: + asserts.true(env, "'" + file.path + "'" in calls[1]["command"]) + return unittest.end(env) + +create_from_depset_test = unittest.make( + _create_from_depset_test_impl, + attrs = apple_support.action_required_attrs(), +) + +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/shell/libtool_test.sh b/test/shell/libtool_test.sh index 1acb16ca..f5bb5a69 100755 --- a/test/shell/libtool_test.sh +++ b/test/shell/libtool_test.sh @@ -195,4 +195,30 @@ function test_ar_toc_only_requires_existing_archive() { expect_log "archive file '${archive}' does not exist" } +function test_ar_append_preserves_previous_batches() ( + unset __LIBTOOL_LOG_ONLY + export DEVELOPER_DIR="$(/usr/bin/xcode-select -p)" + export SDKROOT="$(/usr/bin/xcrun --show-sdk-path)" + local first="${TEST_TMPDIR}/first.o" + local second="${TEST_TMPDIR}/second.o" + + printf 'int first(void) { return 1; }\n' | + /usr/bin/xcrun clang -x c -c - -o "${first}" || fail "compile first" + printf 'int second(void) { return 2; }\n' | + /usr/bin/xcrun clang -x c -c - -o "${second}" || fail "compile second" + + local flags + for flags in cq cqD; do + local archive="${TEST_TMPDIR}/batched-${flags}.a" + local binary="${TEST_TMPDIR}/batched-${flags}" + "${LIBTOOL}" "${flags}" "${archive}" "${first}" || fail "create archive (${flags})" + "${LIBTOOL}" "${flags}" "${archive}" "${second}" || fail "append archive (${flags})" + "${LIBTOOL}" s "${archive}" || fail "index archive (${flags})" + printf 'int first(void); int second(void); int main(void) { return first() + second() != 3; }\n' | + /usr/bin/xcrun clang -x c - -x none "${archive}" -o "${binary}" || + fail "link both batches (${flags})" + "${binary}" || fail "run both batches (${flags})" + done +) + run_suite "libtool tests" diff --git a/test/shell/toolchain_flags_test.sh b/test/shell/toolchain_flags_test.sh new file mode 100755 index 00000000..255ba657 --- /dev/null +++ b/test/shell/toolchain_flags_test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$script_path"/unittest.bash + +function test_quoted_copts_reach_the_compiler() { + ( + unset TEST_TMPDIR + bazel_cmd aquery --repo_env=$'BAZEL_COPTS=-DAPPLE_SUPPORT_QUOTE="value":-DAPPLE_SUPPORT_CTRL=alpha\fbeta:-DAPPLE_SUPPORT_APOSTROPHE=O\'Brien' --output=textproto \ + 'mnemonic("CppCompile", //test/test_data:cc_lib)' + ) &> "$TEST_log" || fail "Expected C++ action analysis to succeed" + grep -Fq 'arguments: "-DAPPLE_SUPPORT_QUOTE=\"value\""' "$TEST_log" || fail "Expected the quoted compiler flag" + grep -Fq 'arguments: "-DAPPLE_SUPPORT_CTRL=alpha\fbeta"' "$TEST_log" || fail "Expected the control character in the compiler flag" + grep -Fq "arguments: \"-DAPPLE_SUPPORT_APOSTROPHE=O\\'Brien\"" "$TEST_log" || fail "Expected the apostrophe in the compiler flag" +} + +function test_quoted_include_directory_reaches_the_toolchain() { + ( + unset TEST_TMPDIR + bazel_cmd cquery --repo_env='USER=quote"probe' --output=starlark \ + --starlark:expr='"/Users/quote\"probe/Applications/" in providers(target)["ToolchainInfo"].cc.built_in_include_directories' \ + 'kind("cc_toolchain rule", deps(//test/test_data:cc_lib))' + ) &> "$TEST_log" || fail "Expected C++ toolchain analysis to succeed" + grep -Fxq 'True' "$TEST_log" || fail "Expected the quoted include directory" +} + +run_suite "toolchain flags tests" diff --git a/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/test_data/toolchain_configs/ios_arm64.json b/test/test_data/toolchain_configs/ios_arm64.json index 40c61613..bd4de0d6 100644 --- a/test/test_data/toolchain_configs/ios_arm64.json +++ b/test/test_data/toolchain_configs/ios_arm64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_arm64e.json b/test/test_data/toolchain_configs/ios_arm64e.json index b529154e..2626cce3 100644 --- a/test/test_data/toolchain_configs/ios_arm64e.json +++ b/test/test_data/toolchain_configs/ios_arm64e.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_sim_arm64.json b/test/test_data/toolchain_configs/ios_sim_arm64.json index 34f52c3b..00d408ba 100644 --- a/test/test_data/toolchain_configs/ios_sim_arm64.json +++ b/test/test_data/toolchain_configs/ios_sim_arm64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/ios_x86_64.json b/test/test_data/toolchain_configs/ios_x86_64.json index c40541fd..40b5384d 100644 --- a/test/test_data/toolchain_configs/ios_x86_64.json +++ b/test/test_data/toolchain_configs/ios_x86_64.json @@ -3712,7 +3712,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_arm64.json b/test/test_data/toolchain_configs/tvos_arm64.json index b0de21be..7ef1fe2e 100644 --- a/test/test_data/toolchain_configs/tvos_arm64.json +++ b/test/test_data/toolchain_configs/tvos_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_sim_arm64.json b/test/test_data/toolchain_configs/tvos_sim_arm64.json index 5016256c..8542a23e 100644 --- a/test/test_data/toolchain_configs/tvos_sim_arm64.json +++ b/test/test_data/toolchain_configs/tvos_sim_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/tvos_x86_64.json b/test/test_data/toolchain_configs/tvos_x86_64.json index b528f3e5..a4be2220 100644 --- a/test/test_data/toolchain_configs/tvos_x86_64.json +++ b/test/test_data/toolchain_configs/tvos_x86_64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/AppleTVSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/visionos_arm64.json b/test/test_data/toolchain_configs/visionos_arm64.json index fe0bf69f..72a1ffc1 100644 --- a/test/test_data/toolchain_configs/visionos_arm64.json +++ b/test/test_data/toolchain_configs/visionos_arm64.json @@ -3685,7 +3685,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/XROS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/visionos_sim_arm64.json b/test/test_data/toolchain_configs/visionos_sim_arm64.json index 7b83d47e..bc3bef67 100644 --- a/test/test_data/toolchain_configs/visionos_sim_arm64.json +++ b/test/test_data/toolchain_configs/visionos_sim_arm64.json @@ -3685,7 +3685,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/XRSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_arm64.json b/test/test_data/toolchain_configs/watchos_arm64.json index 8e18c326..1413e22f 100644 --- a/test/test_data/toolchain_configs/watchos_arm64.json +++ b/test/test_data/toolchain_configs/watchos_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_arm64_32.json b/test/test_data/toolchain_configs/watchos_arm64_32.json index e2be2a0a..8c09150f 100644 --- a/test/test_data/toolchain_configs/watchos_arm64_32.json +++ b/test/test_data/toolchain_configs/watchos_arm64_32.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_device_arm64.json b/test/test_data/toolchain_configs/watchos_device_arm64.json index 36e5d3f0..7cc0057e 100644 --- a/test/test_data/toolchain_configs/watchos_device_arm64.json +++ b/test/test_data/toolchain_configs/watchos_device_arm64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_device_arm64e.json b/test/test_data/toolchain_configs/watchos_device_arm64e.json index a71080dc..fa34d35f 100644 --- a/test/test_data/toolchain_configs/watchos_device_arm64e.json +++ b/test/test_data/toolchain_configs/watchos_device_arm64e.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchOS.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/test_data/toolchain_configs/watchos_x86_64.json b/test/test_data/toolchain_configs/watchos_x86_64.json index 5f050f55..610e03d6 100644 --- a/test/test_data/toolchain_configs/watchos_x86_64.json +++ b/test/test_data/toolchain_configs/watchos_x86_64.json @@ -3686,7 +3686,7 @@ "-isysroot", "__BAZEL_XCODE_SDKROOT__", "-F__BAZEL_XCODE_SDKROOT__/System/Library/Frameworks", - "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/MacOSX.platform/Developer/Library/Frameworks" + "-F__BAZEL_XCODE_DEVELOPER_DIR__/Platforms/WatchSimulator.platform/Developer/Library/Frameworks" ], "iterate_over": null, "type_name": "flag_group" diff --git a/test/transitions.bzl b/test/transitions.bzl index 90df5a57..7570db88 100644 --- a/test/transitions.bzl +++ b/test/transitions.bzl @@ -14,7 +14,7 @@ _PLATFORM_TYPE_TO_DEFAULT_ARCH = { "ios": "x86_64", "macos": "x86_64", "tvos": "x86_64", - "visionos": "x86_64", + "visionos": "sim_arm64", "watchos": "x86_64", } 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_config_test.bzl b/test/xcode_config_test.bzl index 3e41ab42..e4881c61 100644 --- a/test/xcode_config_test.bzl +++ b/test/xcode_config_test.bzl @@ -22,7 +22,7 @@ load( "//xcode:providers.bzl", "XcodeVersionPropertiesInfo", ) -load("//xcode:xcode_config.bzl", "xcode_config") +load("//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") load("//xcode:xcode_version.bzl", "xcode_version") load(":test_helpers.bzl", "FIXTURE_TAGS", "find_action", "make_all_tests") @@ -620,7 +620,9 @@ def _invalid_xcode_from_mutual_throws(namer): def _invalid_xcode_from_mutual_throws_test_impl(ctx): env = analysistest.begin(ctx) - asserts.expect_failure(env, "--xcode_version=6 specified, but '6' is not an available Xcode version. Locally available versions: [8.4]. Remotely available versions: [5.1.2, 8.4].") + asserts.expect_failure(env, "Xcode 6 was not found.") + asserts.expect_failure(env, "Detected locally: 8.4") + asserts.expect_failure(env, "Available remotely: 5.1.2, 8.4") return analysistest.end(env) _invalid_xcode_from_mutual_throws_test = analysistest.make( @@ -1174,7 +1176,8 @@ def _invalid_xcode_specified(namer): def _invalid_xcode_specified_test_impl(ctx): env = analysistest.begin(ctx) - asserts.expect_failure(env, "--xcode_version=6 specified, but '6' is not an available Xcode version. If you believe you have '6' installed") + asserts.expect_failure(env, "Bazel couldn’t match the selected Xcode to this project’s supported versions:") + asserts.expect_failure(env, "\n 6\n\n") return analysistest.end(env) _invalid_xcode_specified_test = analysistest.make( @@ -1193,6 +1196,78 @@ _invalid_xcode_specified_test = analysistest.make( # ------------------------------------------------------------------------------ +def _detected_xcode_diagnostics(namer): + xcode_version( + name = namer("supported"), + version = "26.5.0.17F42", + aliases = ["17F42"], + tags = FIXTURE_TAGS, + ) + tests = [] + for case, versions, selected, hint in [ + ("unsupported", ["27.0.27A266a"], "27.0 (build 27A266a)", None), + ("normalized_duplicates", ["27.0.27A266a", "27.0.0.27A266a"], "27.0 (build 27A266a)", None), + ("ambiguous", ["27.0.27A266a", "27.1.27A266a"], "27A266a", None), + ("unknown", ["26.5.17F42"], "27A266a", None), + ("empty", [], "27A266a", None), + ("custom_hint", ["27.0.27A266a"], "27.0 (build 27A266a)", "Follow https://example.com/xcode\nThen run ./setup-xcode {team}."), + ("empty_hint", ["27.0.27A266a"], "27.0 (build 27A266a)", ""), + ]: + detected_xcodes( + name = namer(case + "_detected"), + versions = versions, + tags = FIXTURE_TAGS, + ) + xcode_config( + name = namer(case + "_config"), + default = namer(":supported"), + versions = [namer(":supported")], + detected_versions = namer(":" + case + "_detected"), + tags = FIXTURE_TAGS, + **({"xcode_selection_hint": hint} if hint != None else {}) + ) + name = "detected_xcode_diagnostics_" + case + _detected_xcode_diagnostics_test( + name = name, + target_under_test = namer(":" + case + "_config"), + expected_selected = selected, + expect_detected = bool(versions), + expected_hint = hint if hint != None else "Install a supported Xcode version and select it with", + custom_hint = hint != None, + ) + tests.append(name) + return tests + +def _detected_xcode_diagnostics_test_impl(ctx): + env = analysistest.begin(ctx) + asserts.expect_failure(env, "Bazel couldn’t match the selected Xcode to this project’s supported versions:") + asserts.expect_failure(env, "\n " + ctx.attr.expected_selected + "\n\n") + asserts.expect_failure(env, "26.5 (build 17F42)") + errors = "\n".join([cause.message for cause in analysistest.target_under_test(env)[AnalysisFailureInfo].causes.to_list()]) + if ctx.attr.expected_hint: + asserts.expect_failure(env, ctx.attr.expected_hint) + if ctx.attr.custom_hint: + asserts.false(env, "Install a supported Xcode version and select it with" in errors) + if ctx.attr.expect_detected: + asserts.expect_failure(env, "Detected locally:") + else: + asserts.false(env, "Detected locally:" in errors) + return analysistest.end(env) + +_detected_xcode_diagnostics_test = analysistest.make( + _detected_xcode_diagnostics_test_impl, + attrs = { + "expected_selected": attr.string(), + "expect_detected": attr.bool(), + "expected_hint": attr.string(), + "custom_hint": attr.bool(), + }, + config_settings = {"//command_line_option:xcode_version": "27A266a"}, + expect_failure = True, +) + +# ------------------------------------------------------------------------------ + def _requires_default(namer): xcode_config( name = "requires_default__foo", @@ -1649,6 +1724,51 @@ _default_sdk_versions_selected_xcode_test = analysistest.make( # ------------------------------------------------------------------------------ +def _native_xcode_properties(namer): + xcode_config( + name = "native_xcode_properties__foo", + default = namer(":version512"), + versions = [namer(":version512")], + tags = FIXTURE_TAGS, + ) + xcode_version( + name = namer("version512"), + default_ios_sdk_version = "101", + version = "5.1.2", + tags = FIXTURE_TAGS, + ) + + _native_xcode_properties_test( + name = "native_xcode_properties", + target_under_test = "native_xcode_properties__foo", + ) + return ["native_xcode_properties"] + +def _native_xcode_properties_test_impl(ctx): + env = analysistest.begin(ctx) + + target_under_test = analysistest.target_under_test(env) + xcode_properties = target_under_test[apple_common.XcodeProperties] + + asserts.equals(env, "5.1.2", xcode_properties.xcode_version) + asserts.equals(env, "101", xcode_properties.default_ios_sdk_version) + + return analysistest.end(env) + +_native_xcode_properties_test = analysistest.make( + _native_xcode_properties_test_impl, + config_settings = { + "//command_line_option:xcode_version_config": str(Label( + "//test:native_xcode_properties__foo", + )), + str(Label("//xcode:starlark_version_config")): str(Label( + "//test:native_xcode_properties__foo", + )), + }, +) + +# ------------------------------------------------------------------------------ + def _default_without_version(namer): xcode_config( name = "default_without_version__foo", @@ -2327,6 +2447,7 @@ def xcode_config_test(name): _valid_alias_dotted_version, _valid_alias_nonnumerical, _invalid_xcode_specified, + _detected_xcode_diagnostics, _requires_default, _duplicate_aliases_defined_version, _duplicate_aliases_within_available_xcodes, @@ -2348,7 +2469,10 @@ def xcode_config_test(name): _available_xcodes_mode_with_flag, ] + ([ _xcode_config_resolver_with_fragment_removed, - ] if _SUPPORTS_INCOMPATIBLE_REMOVE_CTX_APPLE_FRAGMENT else [])) if bazel_features.apple.xcode_config_migrated else [], + ] if _SUPPORTS_INCOMPATIBLE_REMOVE_CTX_APPLE_FRAGMENT else []) + ([ + # TODO: Remove once we no longer support Bazel 8. + _native_xcode_properties, + ] if getattr(apple_common, "XcodeProperties", None) else [])) if bazel_features.apple.xcode_config_migrated else [], ) # TODO: b/311385128 - The following tests from `XcodeConfigTest.java` diff --git a/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/test/xcode_version_at_least_test.bzl b/test/xcode_version_at_least_test.bzl index 38f3627c..2612a333 100644 --- a/test/xcode_version_at_least_test.bzl +++ b/test/xcode_version_at_least_test.bzl @@ -15,6 +15,7 @@ """Tests for the `xcode_version_at_least` macro and rule.""" load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_features//private:util.bzl", "ge") load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") load( "//xcode:xcode_config.bzl", @@ -82,6 +83,8 @@ _select_xcode_26_4_0_test = _make_xcode_select_test("26.4.0.17E192") _select_xcode_26_4_1_test = _make_xcode_select_test("26.4.1.17E202") _select_xcode_27_0_beta_test = _make_xcode_select_test("27.0.0.27A5228h") _select_xcode_padded_26_test = _make_xcode_select_test("26") +_DEVELOPER_DIR_CONFIG = "xcode_version_at_least_test__developer_dir_config" +_select_xcode_developer_dir_test = _make_xcode_select_test("", config_name = _DEVELOPER_DIR_CONFIG) # ------------------------------------------------------------------------------ @@ -168,12 +171,31 @@ def _test_single_threshold(namer): expected_value = "matched", ) - return [ + tests = [ "single_threshold_below", "single_threshold_equal", "single_threshold_above_patch", "single_threshold_above_major_beta", ] + if ge("9.0.0"): + xcode_version( + name = namer("developer_dir"), + version = "/Applications/Xcode.app/Contents/Developer", + tags = FIXTURE_TAGS, + ) + xcode_config( + name = _DEVELOPER_DIR_CONFIG, + default = namer(":developer_dir"), + versions = [namer(":developer_dir")], + tags = FIXTURE_TAGS, + ) + _select_xcode_developer_dir_test( + name = "single_threshold_developer_dir", + target_under_test = namer(":single_holder"), + expected_value = "matched", + ) + tests.append("single_threshold_developer_dir") + return tests def _test_precisions_and_build_numbers(namer): cases = [ @@ -313,11 +335,21 @@ def _test_qualified_labels_in_after(namer): minimum_xcode_version = "26.4", tags = FIXTURE_TAGS, ) + xcode_version_at_least( + name = namer("qual_27_0"), + after = [ + Label("@apple_support//test:" + namer("qual_26_2")), + "@apple_support//test:" + namer("qual_26_4"), + ], + minimum_xcode_version = "27.0", + tags = FIXTURE_TAGS, + ) _select_holder( name = namer("qual_holder"), selected_value = select({ namer(":qual_26_2"): "xcode_26_2_or_newer", namer(":qual_26_4"): "xcode_26_4_or_newer", + namer(":qual_27_0"): "xcode_27_0_or_newer", "//conditions:default": "older_than_26_2", }), tags = FIXTURE_TAGS, @@ -328,8 +360,68 @@ def _test_qualified_labels_in_after(namer): target_under_test = namer(":qual_holder"), expected_value = "xcode_26_4_or_newer", ) + _select_xcode_27_0_beta_test( + name = "label_objects_in_after_match_refined", + target_under_test = namer(":qual_holder"), + expected_value = "xcode_27_0_or_newer", + ) - return ["qualified_labels_in_after_matches_refined"] + return [ + "qualified_labels_in_after_matches_refined", + "label_objects_in_after_match_refined", + ] + +def _test_same_package_label_inheritance(namer): + xcode_version_at_least( + name = namer("base"), + minimum_xcode_version = "26.2", + tags = FIXTURE_TAGS, + ) + xcode_version_at_least( + name = namer("middle"), + after = [namer(":base")], + minimum_xcode_version = "26.4", + tags = FIXTURE_TAGS, + ) + + base_label = Label("//test:" + namer("base")) + middle_label = Label("//test:" + namer("middle")) + cases = [ + ("package_qualified", ["//test:" + namer("middle")], {}), + ("repo_qualified", ["@apple_support//test:" + namer("middle")], {}), + ("canonical", [str(middle_label)], {}), + ("label_object", [middle_label], {}), + ("mixed_references", [base_label, namer(":middle")], {}), + ("explicit_flag_values", [middle_label], {namer(":middle") + "_flag": "True"}), + ] + test_names = [] + for suffix, after, flag_values in cases: + xcode_version_at_least( + name = namer(suffix), + after = after, + flag_values = flag_values, + minimum_xcode_version = "27.0", + tags = FIXTURE_TAGS, + ) + _select_holder( + name = namer(suffix + "_holder"), + selected_value = select({ + namer(":base"): "base", + namer(":middle"): "middle", + namer(":" + suffix): "highest", + "//conditions:default": "default", + }), + tags = FIXTURE_TAGS, + ) + test_name = "same_package_inheritance_" + suffix + _select_xcode_27_0_beta_test( + name = test_name, + target_under_test = namer(":" + suffix + "_holder"), + expected_value = "highest", + ) + test_names.append(test_name) + + return test_names def _expect_failure_message_test_impl(ctx): env = analysistest.begin(ctx) @@ -431,6 +523,7 @@ def xcode_version_at_least_test(name): _test_padded_versions, _test_chained_select_refinement, _test_qualified_labels_in_after, + _test_same_package_label_inheritance, _test_after_conflicts, ] if bazel_features.apple.xcode_config_migrated else [], # TODO: Remove once we test with Bazel 8+ ) diff --git a/toolchain/dynamic_toolchain_info.bzl b/toolchain/dynamic_toolchain_info.bzl index ad5f799e..3f702015 100644 --- a/toolchain/dynamic_toolchain_info.bzl +++ b/toolchain/dynamic_toolchain_info.bzl @@ -7,13 +7,15 @@ 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: return xcode_config.sdk_version_for_platform(apple_common.platform.tvos_device) - elif platform_type == getattr(apple_common.platform_type, "visionos", None): + elif platform_type == apple_common.platform_type.visionos: return xcode_config.sdk_version_for_platform(apple_common.platform.visionos_device) elif platform_type == apple_common.platform_type.watchos: return xcode_config.sdk_version_for_platform(apple_common.platform.watchos_device) @@ -23,13 +25,14 @@ 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: return "iPhoneOS" - elif platform_type == getattr(apple_common.platform_type, "visionos", None) and is_simulator: + elif platform_type == apple_common.platform_type.visionos and is_simulator: return "XRSimulator" - elif platform_type == getattr(apple_common.platform_type, "visionos", None): + elif platform_type == apple_common.platform_type.visionos: return "XROS" elif platform_type == apple_common.platform_type.watchos and is_simulator: return "WatchSimulator" @@ -44,7 +47,18 @@ def _sdk_name(platform_type, is_simulator): else: 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("."): + if not part.isdigit(): + break + parts.append(part) + 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]): @@ -72,22 +86,32 @@ def _dynamic_toolchain_info_impl(ctx): platform_type = apple_common.platform_type.watchos elif ctx.target_platform_has_constraint(ctx.attr._visionos[platform_common.ConstraintValueInfo]): triple_os = "xros" - platform_type = getattr(apple_common.platform_type, "visionos", None) + platform_type = apple_common.platform_type.visionos elif ctx.target_platform_has_constraint(ctx.attr._linux[platform_common.ConstraintValueInfo]): return [platform_common.TemplateVariableInfo({"ARCH": triple_arch})] else: fail("Unknown OS in target platform") + apple_platform_info = apple_support.platform_info_from_rule_ctx(ctx) + is_simulator = ctx.target_platform_has_constraint(ctx.attr._simulator[platform_common.ConstraintValueInfo]) if is_simulator: triple_suffix = "-simulator" else: triple_suffix = "" - xcode_config = ctx.attr._xcode_config[apple_common.XcodeVersionConfig] - target_os_version = xcode_config.minimum_os_for_platform_type(platform_type) + 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 + # silently falls back to a very old deployment target, so only the numeric + # part of the version goes into the triple. + target_os_version = _numeric_version(xcode_config.minimum_os_for_platform_type(platform_type)) + + # Any suffix following the numeric part of the SDK version is carried over + # to the SDK platform name. + sdk_name = _sdk_name(platform_type, is_simulator) + str(sdk_version)[len(_numeric_version(sdk_version)):] + target = "{}-apple-{}{}{}".format( triple_arch, triple_os, @@ -102,7 +126,7 @@ def _dynamic_toolchain_info_impl(ctx): } if xcode_config.xcode_version(): xcode_env = { - "APPLE_SDK_PLATFORM": _sdk_name(platform_type, is_simulator), + "APPLE_SDK_PLATFORM": sdk_name, "APPLE_SDK_VERSION_OVERRIDE": str(sdk_version), # TODO: Remove once we drop bazel 7.x support "XCODE_VERSION_OVERRIDE": str(xcode_config.xcode_version()), } @@ -110,14 +134,14 @@ def _dynamic_toolchain_info_impl(ctx): return [platform_common.TemplateVariableInfo( xcode_env | { "ARCH": triple_arch, - "PLATFORM_FRAMEWORKS": apple_support.path_placeholders.platform_frameworks(apple_fragment = ctx.fragments.apple), + "PLATFORM_FRAMEWORKS": apple_support.path_placeholders.platform_frameworks(apple_platform_info = apple_platform_info), "TARGET": target, }, )] dynamic_toolchain_info = rule( implementation = _dynamic_toolchain_info_impl, - attrs = { + attrs = apple_support.platform_constraint_attrs() | { "_linux": attr.label(default = "@platforms//os:linux"), "_macos": attr.label(default = "@platforms//os:macos"), "_ios": attr.label(default = "@platforms//os:ios"), @@ -141,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, @@ -151,13 +176,5 @@ def _xcode_execution_info_impl(ctx): xcode_execution_info = rule( implementation = _xcode_execution_info_impl, - attrs = { - "_xcode_config": attr.label(default = configuration_field( - fragment = "apple", - name = "xcode_config_label", - )), - }, - fragments = [ - "apple", - ], + attrs = apple_support.action_required_attrs(), ) diff --git a/toolchain/toolchain_env.bzl b/toolchain/toolchain_env.bzl index 690b9f56..05c6d1e4 100644 --- a/toolchain/toolchain_env.bzl +++ b/toolchain/toolchain_env.bzl @@ -4,13 +4,16 @@ This does much less than in the past but is still required for a few environment variable reading use cases. """ -load("@bazel_tools//tools/cpp:lib_cc_configure.bzl", "escape_string") +load("//lib/private:toolchain_flags.bzl", "get_starlark_list", "quote_starlark_string") load("//xcode:xcode_configure.bzl", "run_xcode_locator") _DISABLE_ENV_VAR = "BAZEL_NO_APPLE_CPP_TOOLCHAIN" _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(escape_string(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\"," % x for x in include_dirs] + return [" %s," % quote_starlark_string(path) for path in include_dirs] def _get_copts_env_var(repository_ctx, name, default = ""): + # 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]) @@ -111,12 +115,8 @@ def _split_escaped(string, delimiter): result.append("".join(accumulator)) return result -def _get_starlark_list(values): - if not values: - return "" - return "\"" + "\",\n \"".join(values) + "\"" - def _toolchain_env_impl(repository_ctx): + # 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 @@ -231,12 +231,12 @@ cc_feature_set( ) """.format( allowlist_absolute_include_directories = "\n".join(escaped_include_paths), - c_opts = _get_starlark_list(c_opts), - conly_opts = _get_starlark_list(conly_opts), - cxx_opts = _get_starlark_list(cxx_opts), + c_opts = get_starlark_list(c_opts), + conly_opts = get_starlark_list(conly_opts), + cxx_opts = get_starlark_list(cxx_opts), disable_toolchain = disable_toolchain, enable_layering_check = enable_layering_check, - link_opts = _get_starlark_list(link_opts), + link_opts = get_starlark_list(link_opts), ), ) diff --git a/toolchain/tools/BUILD b/toolchain/tools/BUILD index fcc25af0..5689666d 100644 --- a/toolchain/tools/BUILD +++ b/toolchain/tools/BUILD @@ -21,7 +21,7 @@ cc_tool_map( "@rules_cc//cc/toolchains/actions:llvm_cov": ":llvm_cov_tool", "@rules_cc//cc/toolchains/actions:gcov": ":gcov_tool", }, - visibility = ["//:__subpackages__"], + visibility = ["//visibility:public"], ) cc_tool( diff --git a/tools/http_dmg/BUILD.bazel b/tools/http_dmg/BUILD.bazel index e6df15b5..fad998ae 100644 --- a/tools/http_dmg/BUILD.bazel +++ b/tools/http_dmg/BUILD.bazel @@ -5,6 +5,7 @@ bzl_library( srcs = ["http_dmg.bzl"], visibility = ["//visibility:public"], deps = [ + "//tools/http_dmg/private:zip7", "@bazel_tools//tools/build_defs/repo:utils.bzl", ], ) diff --git a/tools/http_dmg/http_dmg.bzl b/tools/http_dmg/http_dmg.bzl index db10f752..e2c64b81 100644 --- a/tools/http_dmg/http_dmg.bzl +++ b/tools/http_dmg/http_dmg.bzl @@ -3,345 +3,174 @@ load( "@bazel_tools//tools/build_defs/repo:utils.bzl", "get_auth", + "patch", + "update_attrs", +) +load( + "//tools/http_dmg/private:zip7.bzl", + "zip7_download", + "zip7_extract", ) -def _zip7_data( - *, - urls, - integrity, - bin_path = None): - return struct( - urls = urls, - integrity = integrity, - bin_path = bin_path, - ) - -_7ZIP_SOURCES = { - "linux_arm64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-arm64.tar.xz", - "https://www.7-zip.org/a/7z2501-linux-arm64.tar.xz", - ], - integrity = "sha256-OcUUDwLORDZZkwPFmhSfZUyxu8R83BBalCEg10euBA0=", - bin_path = "7zz", - ), - "linux_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-x64.tar.xz", - "https://www.7-zip.org/a/7z2501-linux-x64.tar.xz", - ], - integrity = "sha256-TKO3xvL2eGa5JiKBi1gjPccDZ74vNrSY6wveqqRLU/Q=", - bin_path = "7zz", - ), - "macos_arm64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", - "https://www.7-zip.org/a/7z2501-mac.tar.xz", - ], - integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", - bin_path = "7zz", - ), - "macos_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", - "https://www.7-zip.org/a/7z2501-mac.tar.xz", - ], - integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", - bin_path = "7zz", - ), - "windows_i686": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", - "https://www.7-zip.org/a/7zr.exe", - ], - integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", - bin_path = "7zr.exe", - ), - "windows_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", - "https://www.7-zip.org/a/7zr.exe", - ], - integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", - bin_path = "7zr.exe", - ), -} - -_7ZIP_EXTRAS = { - "windows_i686": _zip7_data( - urls = [ - "https://mirror.bazel.build/7-zip.org/a/7z2501.exe", - "https://7-zip.org/a/7z2501-x64.exe", - ], - integrity = "sha256-eK+iocdzyvPPft9i+FfSqKXaVfsP/12kFgdMDSiytV8=", - bin_path = "7z.exe", - ), - "windows_x86_64": _zip7_data( - urls = [ - "https://mirror.bazel.build/7-zip.org/a/7z2501-x64.exe", - "https://7-zip.org/a/7z2501.exe", - ], - integrity = "sha256-uWgx7sWSg4TwVD1rV8H4ApUqDyZo5mKILAp4WitS+zs=", - bin_path = "7z.exe", - ), -} - -def _host_arch(repository_ctx): - arch = repository_ctx.os.arch - if arch == "amd64": - arch = "x86_64" - elif arch == "aarch64": - arch = "arm64" - - if "linux" in repository_ctx.os.name: - return "linux_{}".format(arch) - - if "mac" in repository_ctx.os.name: - return "macos_{}".format(arch) - - if "win" in repository_ctx.os.name: - return "windows_{}".format(arch) - - fail("Unknown host OS: {}".format(repository_ctx.os.name)) - -_DMG_IGNORES_PATTERNS = { - "name": ( - "Applications", - ), - "prefix": ( - ".DS_Store", - ".background", - ".VolumeIcon", - ), - "suffix": ( - "[]", - "[HFS+ Private Data]", - ".fseventsd", - ), -} - -def _7z_readdir(path, is_dmg): - """Read the contents of a directory, ignoring certain patterns for dmg archives. - - Args: - path (Path): The directory Path object - is_dmg (bool): Whether or not the directory is from a `.dmg` archive. +# Entries which are artifacts of the disk image rather than its payload. Each +# entry is excluded both at the root of the image and directly within the +# volume directory 7-Zip creates for the image. +_DMG_IGNORED_ENTRIES = [ + # Finder metadata. + ".DS_Store", + ".background", + ".VolumeIcon*", + ".fseventsd", + ".Trashes", + # The conventional `Applications -> /Applications` drag target. 7-Zip + # names symlinks with an empty name `[]`. + "Applications", + "[]", + # HFS+ bookkeeping. + "[HFS+ Private Data]", +] + +def _dmg_7z_args(): + # type: () -> list[str] + """Arguments passed to 7-Zip when extracting a `.dmg` archive. Returns: - list: A list of Path. + list: A list of strings. """ - if not is_dmg: - return path.readdir() - - entries = [] - - for item in path.readdir(): - if item.basename.endswith(_DMG_IGNORES_PATTERNS["suffix"]): - continue - if item.basename.startswith(_DMG_IGNORES_PATTERNS["prefix"]): - continue - if item.basename in _DMG_IGNORES_PATTERNS["name"]: - continue - - # 7zip will write attributes using `:` delimiters. - if ":" in item.basename: - continue - - entries.append(item) - - return entries - + args = [ + # Do not extract HFS+ extended attributes, which 7-Zip would otherwise + # write as `:` files beside each file. + "-sns-", + # Extract all symbolic links, including links to links and links which + # point outside of the extraction directory. These are common in `.dmg` + # files (e.g. `Applications -> /Applications`). The archive is + # checksum pinned so this is safe for the archives a user opts into. + "-snld20", + ] + + # 7-Zip wildcards do not match across `/`, so `*/` only matches an + # entry directly within the volume directory. + for entry in _DMG_IGNORED_ENTRIES: + args.append("-x!{}".format(entry)) + args.append("-x!*/{}".format(entry)) + + return args + +# 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): - if not hasattr(repository_ctx, "rename"): - program = "mv" - if "win" in repository_ctx.os.name: - program = "move" - result = repository_ctx.execute([program, src, dst]) - if result.return_code: - fail("mv failed with {}\nstdout:\n{}\nstderr:\n{}".format( - result.return_code, - result.stdout, - result.stderr, - )) + # 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"): + repository_ctx.rename(src, dst) return - repository_ctx.rename(src, dst) + # Unlike `rename`, `mv`/`move` do not create parent directories. Writing + # and deleting a placeholder file creates them without any host tools. + parent = dst.dirname + if not parent.exists: + placeholder = parent.get_child(".http_dmg_mkdir") + repository_ctx.file(placeholder, content = "", executable = False) + repository_ctx.delete(placeholder) -def _extract_7z( + if "win" in repository_ctx.os.name: + # `move` is a `cmd.exe` builtin rather than an executable. + command = ["cmd", "/c", "move", src, dst] + else: + command = ["mv", src, dst] + + result = repository_ctx.execute(command) + if result.return_code: + fail("{} failed with {}\nstdout:\n{}\nstderr:\n{}".format( + " ".join([str(a) for a in command]), + result.return_code, + result.stdout, + result.stderr, + )) + +def _extract_dmg( *, repository_ctx, - z7_bin, + zip7_bin, archive, - output = None, - strip_prefix = None): - """Extract a 7zip archive to the repository directory. + strip_prefix = None, + add_prefix = None): + # type: (repository_ctx, path, path, str | None, str | None) -> None + """Extract a `.dmg` archive into the repository directory. Args: repository_ctx (repository_ctx): The rule's repository context. - z7_bin (path): The path to a 7zip binary. - archive (path): path to the archive that will be unpacked, relative to the repository directory. - output (str, optional): path to the directory where the archive will be unpacked, relative to the repository directory. + zip7_bin (path): The path to a 7-Zip binary. + archive (path): The `.dmg` archive to extract. strip_prefix (str, optional): a directory prefix to strip from the extracted files. + add_prefix (str, optional): a directory, relative to the repository, to extract into. """ - if not output: - output = "." - - out_dir = repository_ctx.path(output) - temp_out_dir = repository_ctx.path("{}/_7z_out".format(out_dir)) + temp_out_dir = repository_ctx.path("_7z_out") - command = [z7_bin, "x", archive, "-snld20", "-o{}".format(temp_out_dir), "-y"] - result = repository_ctx.execute(command) - if result.return_code != 0: - fail("7z command failed with exit code {}\n{}\n\nstdout:\n{}\nstderr:\n{}".format( - result.return_code, - " ".join([str(a) for a in command]), - result.stdout, - result.stderr, - )) - - is_dmg = str(archive).endswith((".dmg", ".DMG")) + zip7_extract( + repository_ctx = repository_ctx, + zip7_bin = zip7_bin, + archive = archive, + output = temp_out_dir, + extra_args = _dmg_7z_args(), + ) + # 7-Zip places the contents of the image within a directory named after + # the volume. Treat that directory as the root of the archive. target_dir = temp_out_dir - if is_dmg: - entries = temp_out_dir.readdir() - if len(entries) == 1 and entries[0].is_dir: - target_dir = entries[0] + entries = temp_out_dir.readdir() + if len(entries) == 1 and entries[0].is_dir: + target_dir = entries[0] - # Check to see if any prefixes can be stripped if strip_prefix: stripped_dir = target_dir.get_child(strip_prefix) if not stripped_dir.exists: fail("Prefix \"{}\" was given, but not found in the archive. Here are possible prefixes for this archive: {}".format( strip_prefix, - [p.basename for p in _7z_readdir(target_dir, is_dmg)], + [p.basename for p in target_dir.readdir()], )) target_dir = stripped_dir - # Move the extracted contents to the root of the directory, leaving known bad files. - for item in _7z_readdir(target_dir, is_dmg): - _move(repository_ctx, item, repository_ctx.path("{}/{}".format(output, item.basename))) + dest_dir = repository_ctx.path(add_prefix or ".") + for item in target_dir.readdir(): + _move(repository_ctx, item, dest_dir.get_child(item.basename)) repository_ctx.delete(temp_out_dir) -def _urls_have_suffix(urls, suffix): - for url in urls: - if url.endswith(suffix): - return True - return False - -def _download_and_extract_7zip_binary(repository_ctx, host_arch): - """Fetch and extract a 7zip archive - - 7z is a cross platform way to extract dmg file. - - Args: - repository_ctx (repository_ctx): The rule's context object. - host_arch (str): An identifier for the host os and architecture. - - Returns: - tuple: - - path: The directory in which 7zip was downloaded to. - - path: The path to the 7zip binary. - """ - zip_data = _7ZIP_SOURCES[host_arch] - - auth = get_auth(repository_ctx, zip_data.urls) - - zip_dir = repository_ctx.path("7z") - zip_exe = repository_ctx.path("{}/{}".format(zip_dir, zip_data.bin_path)) - - if _urls_have_suffix(zip_data.urls, ".exe"): - repository_ctx.download( - url = zip_data.urls, - output = zip_exe, - integrity = zip_data.integrity, - executable = True, - auth = auth, - ) - else: - repository_ctx.download_and_extract( - url = zip_data.urls, - integrity = zip_data.integrity, - output = zip_dir, - auth = auth, - ) - - extras = _7ZIP_EXTRAS.get(host_arch) - if extras: - extras_output = repository_ctx.path("{}/{}".format( - zip_dir, - "extras", - )) - - if _urls_have_suffix(extras.urls, ".exe"): - urls_suffix = "exe" - elif _urls_have_suffix(extras.urls, ".msi"): - urls_suffix = "msi" - elif _urls_have_suffix(extras.urls, ".7z"): - urls_suffix = "7z" - else: - urls_suffix = None - - if urls_suffix: - extras_archive = repository_ctx.path("{}/{}".format( - zip_dir, - "extras.{}".format(urls_suffix), - )) - repository_ctx.download( - url = extras.urls, - output = extras_archive, - integrity = extras.integrity, - auth = auth, - ) - _extract_7z( - repository_ctx = repository_ctx, - z7_bin = zip_exe, - archive = extras_archive, - output = extras_output, - ) - else: - repository_ctx.download_and_extract( - url = extras.urls, - integrity = extras.integrity, - output = extras_output, - auth = auth, - ) - - # Allow extras to update the binary to use. - if extras.bin_path: - zip_exe = repository_ctx.path("{}/{}".format(extras_output, extras.bin_path)) - - if not zip_exe.exists: - fail("Failed to locate 7z binary from: {}".format(zip_data.urls)) - - return zip_dir, zip_exe - def _http_dmg_impl(repository_ctx): + # 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.") - repository_ctx.report_progress("Downloading 7z.") - host_arch = _host_arch(repository_ctx) - z7_dir, z7_bin = _download_and_extract_7zip_binary(repository_ctx, host_arch) + source_urls = _get_source_urls(repository_ctx.attr.url, repository_ctx.attr.urls) + + zip7_dir = "7z" + zip7_bin = zip7_download(repository_ctx, zip7_dir) dmg = repository_ctx.path("dmg.dmg") repository_ctx.report_progress("Downloading dmg.") download_results = repository_ctx.download( - url = repository_ctx.attr.urls, + url = source_urls, output = dmg, integrity = repository_ctx.attr.integrity, sha256 = repository_ctx.attr.sha256, - auth = get_auth(repository_ctx, repository_ctx.attr.urls), + canonical_id = repository_ctx.attr.canonical_id, + auth = get_auth(repository_ctx, source_urls), ) repository_ctx.report_progress("Extracting dmg.") - _extract_7z( + _extract_dmg( repository_ctx = repository_ctx, - z7_bin = z7_bin, + zip7_bin = zip7_bin, archive = dmg, strip_prefix = repository_ctx.attr.strip_prefix, + add_prefix = repository_ctx.attr.add_prefix, ) + # Delete dmg remnants + repository_ctx.delete(dmg) + repository_ctx.delete(zip7_dir) + build_file_content = repository_ctx.attr.build_file_content if repository_ctx.attr.build_file: build_file_content = repository_ctx.read(repository_ctx.path(repository_ctx.attr.build_file)) @@ -350,24 +179,44 @@ def _http_dmg_impl(repository_ctx): # TODO: Delete once we drop 8.x repository_ctx.file("MODULE.bazel", content = """module(name = "{}")""".format(repository_ctx.name)) - # Delete dmg remnants - repository_ctx.delete(dmg) - repository_ctx.delete(z7_dir) + # `patch_strip` is only applied by the `patch` helper from Bazel 8.0, so it + # is folded into `patch_args` here. On newer versions this yields a repeated + # `-pN`, of which the last one wins. + patch( + repository_ctx, + patch_args = ["-p{}".format(repository_ctx.attr.patch_strip)] + repository_ctx.attr.patch_args, + ) # Return reproducibility attributes - repro_attrs = { - k: getattr(repository_ctx.attr, k) - for k in _ATTRS.keys() - } - - repro_attrs["name"] = repository_ctx.attr.name - + overrides = {} if not repository_ctx.attr.sha256: - repro_attrs["integrity"] = download_results.integrity + overrides["integrity"] = download_results.integrity + + repro_attrs = update_attrs(repository_ctx.attr, _ATTRS.keys(), overrides) + # TODO: Delete the fallback once we drop 7.x + if hasattr(repository_ctx, "repo_metadata"): + return repository_ctx.repo_metadata(attrs_for_reproducibility = repro_attrs) return repro_attrs +def _get_source_urls(url, urls): + # type: (str, list[str]) -> list[str] + """Returns the urls provided via the `url` and `urls` attributes.""" + if not url and not urls: + fail("At least one of url and urls must be provided") + + return [url] + urls if url else urls + _ATTRS = { + "add_prefix": attr.string( + default = "", + doc = """Destination directory relative to the repository directory. + +The archive will be unpacked into this directory, after applying `strip_prefix` +(if any) to the file paths within the archive. For example, file +`foo-1.2.3/src/foo.h` will be unpacked to `bar/src/foo.h` if `add_prefix = "bar"` +and `strip_prefix = "foo-1.2.3"`.""", + ), "auth_patterns": attr.string_dict( doc = """An optional dict mapping host names to custom authorization patterns. @@ -419,6 +268,13 @@ Authorization: Bearer RANDOM-TOKEN "Either build_file or build_file_content can be specified, but " + "not both.", ), + "canonical_id": attr.string( + doc = """A canonical ID of the file downloaded. + +If specified and non-empty, Bazel will not take the file from cache, unless it +was added to the cache by a request with the same canonical ID. +""", + ), "integrity": attr.string( doc = """Expected checksum in Subresource Integrity format of the file downloaded. @@ -430,6 +286,45 @@ easier but either this attribute or `sha256` should be set before shipping.""", "netrc": attr.string( doc = "Location of the .netrc file to use for authentication", ), + "patch_args": attr.string_list( + default = [], + doc = + "The arguments given to the patch tool. Defaults to -p0 (see the `patch_strip` " + + "attribute), however -p1 will usually be needed for patches generated by " + + "git. If multiple -p arguments are specified, the last one will take effect." + + "If arguments other than -p are specified, Bazel will fall back to use patch " + + "command line tool instead of the Bazel-native patch implementation. When falling " + + "back to patch command line tool and patch_tool attribute is not specified, " + + "`patch` will be used. This only affects patch files in the `patches` attribute.", + ), + "patch_cmds": attr.string_list( + default = [], + doc = "Sequence of Bash commands to be applied on Linux/Macos after patches are applied.", + ), + "patch_cmds_win": attr.string_list( + default = [], + doc = "Sequence of Powershell commands to be applied on Windows after patches are " + + "applied. If this attribute is not set, patch_cmds will be executed on Windows, " + + "which requires Bash binary to exist.", + ), + "patch_strip": attr.int( + default = 0, + doc = "When set to `N`, this is equivalent to inserting `-pN` to the beginning of `patch_args`.", + ), + "patch_tool": attr.string( + default = "", + doc = "The patch(1) utility to use. If this is specified, Bazel will use the specified " + + "patch tool instead of the Bazel-native patch implementation.", + ), + "patches": attr.label_list( + default = [], + doc = + "A list of files that are to be applied as patches after " + + "extracting the archive. By default, it uses the Bazel-native patch implementation " + + "which doesn't support binary patch, but Bazel will fall back to use " + + "patch command line tool if `patch_tool` attribute is specified or there are " + + "arguments other than `-p` in `patch_args` attribute.", + ), "sha256": attr.string( doc = """The expected SHA-256 of the file downloaded. @@ -457,6 +352,15 @@ discarded and inaccessible (e.g., a top-level license file). This includes files/directories that start with the prefix but are not in the directory (e.g., `foo-lib-1.2.3.release-notes`). If the specified prefix does not match a directory in the archive, Bazel will return an error.""", + ), + "url": attr.string( + doc = """A URL to a file that will be made available to Bazel. + +This must be a file, http or https URL. Redirections are followed. +Authentication is not supported. + +More flexibility can be achieved by the urls parameter that allows +to specify alternative URLs to fetch from.""", ), "urls": attr.string_list( doc = """A list of URLs to a file that will be made available to Bazel. @@ -466,7 +370,6 @@ Authentication is not supported. URLs are tried in order until one succeeds, so you should list local mirrors first. If all downloads fail, the rule will fail.""", - mandatory = True, ), } diff --git a/tools/http_dmg/private/BUILD.bazel b/tools/http_dmg/private/BUILD.bazel index e69de29b..93c64801 100644 --- a/tools/http_dmg/private/BUILD.bazel +++ b/tools/http_dmg/private/BUILD.bazel @@ -0,0 +1,8 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "zip7", + srcs = ["zip7.bzl"], + visibility = ["//visibility:public"], + deps = ["@bazel_tools//tools/build_defs/repo:utils.bzl"], +) diff --git a/tools/http_dmg/private/tests/BUILD.bazel b/tools/http_dmg/private/tests/BUILD.bazel index dea6f249..9030b400 100644 --- a/tools/http_dmg/private/tests/BUILD.bazel +++ b/tools/http_dmg/private/tests/BUILD.bazel @@ -6,6 +6,12 @@ diff_test( file2 = "firefox_info.plist", ) +diff_test( + name = "firefox_patched_diff_test", + file1 = "@http_dmg_test_firefox_patched//:info_plist", + file2 = "firefox_info_patched.plist", +) + diff_test( name = "firefox_strip_prefix_diff_test", file1 = "@http_dmg_test_firefox_strip_prefix//:info_plist", diff --git a/tools/http_dmg/private/tests/firefox_info.patch b/tools/http_dmg/private/tests/firefox_info.patch new file mode 100644 index 00000000..401de015 --- /dev/null +++ b/tools/http_dmg/private/tests/firefox_info.patch @@ -0,0 +1,11 @@ +--- a/firefox/Contents/Info.plist ++++ b/firefox/Contents/Info.plist +@@ -199,7 +199,7 @@ + CFBundlePackageType + APPL + CFBundleShortVersionString +- 141.0.3 ++ 141.0.3-patched + CFBundleSignature + MOZB + CFBundleURLTypes diff --git a/tools/http_dmg/private/tests/firefox_info_patched.plist b/tools/http_dmg/private/tests/firefox_info_patched.plist new file mode 100644 index 00000000..c30cecba --- /dev/null +++ b/tools/http_dmg/private/tests/firefox_info_patched.plist @@ -0,0 +1,275 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + html + htm + shtml + xht + xhtml + + CFBundleTypeIconFile + document.icns + CFBundleTypeName + HTML Document + CFBundleTypeOSTypes + + HTML + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + json + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + application/json + + CFBundleTypeName + JSON File + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + pdf + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + application/pdf + + CFBundleTypeName + PDF document + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + + + CFBundleTypeExtensions + + svg + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + image/svg+xml + + CFBundleTypeName + SVG document + CFBundleTypeOSTypes + + TEXT + + CFBundleTypeRole + Viewer + NSDocumentClass + BrowserDocument + + + CFBundleTypeExtensions + + text + txt + js + log + css + xul + rdf + + CFBundleTypeIconFile + document.icns + CFBundleTypeName + Text Document + CFBundleTypeOSTypes + + TEXT + utxt + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + jpeg + jpg + png + gif + + CFBundleTypeIconFile + fileBookmark.icns + CFBundleTypeName + document.icns + CFBundleTypeOSTypes + + GIFf + JPEG + PNGf + + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + oga + ogg + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + audio/ogg + + CFBundleTypeName + HTML5 Audio (Ogg) + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + ogv + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + video/ogg + + CFBundleTypeName + HTML5 Video (Ogg) + CFBundleTypeRole + Viewer + + + CFBundleTypeExtensions + + webm + + CFBundleTypeIconFile + document.icns + CFBundleTypeMIMETypes + + video/webm + + CFBundleTypeName + HTML5 Video (WebM) + CFBundleTypeRole + Viewer + + + CFBundleExecutable + firefox + CFBundleGetInfoString + Firefox 141.0.3 + CFBundleIconFile + firefox.icns + CFBundleIdentifier + org.mozilla.firefox + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Firefox + CFBundlePackageType + APPL + CFBundleShortVersionString + 141.0.3-patched + CFBundleSignature + MOZB + CFBundleURLTypes + + + CFBundleURLIconFile + document.icns + CFBundleURLName + http URL + CFBundleURLSchemes + + http + + + + CFBundleURLIconFile + document.icns + CFBundleURLName + https URL + CFBundleURLSchemes + + https + + + + CFBundleURLName + file URL + CFBundleURLSchemes + + file + + + + CFBundleVersion + 14125.8.6 + NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + + NSAppleScriptEnabled + + LSApplicationCategoryType + public.app-category.productivity + LSEnvironment + + MallocNanoZone + 0 + SYSTEM_VERSION_COMPAT + 0 + + LSFileQuarantineEnabled + + LSMinimumSystemVersion + 10.15.0 + NSSupportsAutomaticGraphicsSwitching + + NSRequiresAquaSystemAppearance + + NSPrincipalClass + GeckoNSApplication + SMPrivilegedExecutables + + org.mozilla.updater + identifier "org.mozilla.updater" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96" + + NSCameraUsageDescription + Only sites you allow within Firefox will be able to use the camera. + NSMicrophoneUsageDescription + Only sites you allow within Firefox will be able to use the microphone. + NSCameraReactionEffectGesturesEnabledDefault + + + diff --git a/tools/http_dmg/private/tests/http_dmg_test_deps.bzl b/tools/http_dmg/private/tests/http_dmg_test_deps.bzl index 18122b3a..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: @@ -38,6 +39,20 @@ def http_dmg_test_deps(): ), ) + http_dmg( + name = "http_dmg_test_firefox_patched", + # Explicitly test `url`. + url = "https://ftp.mozilla.org/pub/firefox/releases/141.0.3/mac/en-US/Firefox%20141.0.3.dmg", + integrity = "sha256-u5Is2mkFQ73aofvDs8ulCMYHdIMmQ0UrwmZZUzH0LbE=", + strip_prefix = "Firefox.app", + add_prefix = "firefox", + patches = [Label("//tools/http_dmg/private/tests:firefox_info.patch")], + patch_strip = 1, + build_file_content = _BUILD_FILE_CONTENT.format( + file = "firefox/Contents/Info.plist", + ), + ) + http_dmg( name = "http_dmg_test_krita", urls = [ @@ -65,6 +80,7 @@ def http_dmg_test_deps(): return [ "http_dmg_test_firefox", + "http_dmg_test_firefox_patched", "http_dmg_test_firefox_strip_prefix", "http_dmg_test_krita", "http_dmg_test_krita_strip_prefix", diff --git a/tools/http_dmg/private/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 new file mode 100644 index 00000000..fe247398 --- /dev/null +++ b/tools/http_dmg/private/zip7.bzl @@ -0,0 +1,256 @@ +"""Helpers for downloading a hermetic copy of 7-Zip within a repository rule. + +7-Zip is the only cross-platform tool that ships prebuilt binaries for Linux, +macOS, and Windows and can read `.dmg` images (UDIF containers wrapping HFS+ or +APFS filesystems, including LZFSE compressed chunks). +""" + +load( + "@bazel_tools//tools/build_defs/repo:utils.bzl", + "get_auth", +) + +# 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, + bin_path = bin_path, + ) + +# Note: `7zzs` is the statically linked Linux build, `7zz` is dynamically +# linked against glibc. Both ship in the same tarball. +_LINUX_ARM64 = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-arm64.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-linux-arm64.tar.xz", + "https://www.7-zip.org/a/7z2501-linux-arm64.tar.xz", + ], + integrity = "sha256-OcUUDwLORDZZkwPFmhSfZUyxu8R83BBalCEg10euBA0=", + bin_path = "7zzs", +) + +_LINUX_X86_64 = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-linux-x64.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-linux-x64.tar.xz", + "https://www.7-zip.org/a/7z2501-linux-x64.tar.xz", + ], + integrity = "sha256-TKO3xvL2eGa5JiKBi1gjPccDZ74vNrSY6wveqqRLU/Q=", + bin_path = "7zzs", +) + +# The macOS archive contains a universal binary. +_MACOS = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-mac.tar.xz", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-mac.tar.xz", + "https://www.7-zip.org/a/7z2501-mac.tar.xz", + ], + integrity = "sha256-Jqp1vCYrsQvwgFYXuVVpwwNcLFkKmffbVcfpYHsmheA=", + bin_path = "7zz", +) + +# `7zr.exe` is a reduced 32-bit console build that only understands `.7z` +# archives (and 7-Zip's own SFX installers). It is used purely to bootstrap the +# full `7z.exe` + `7z.dll` from the installer below. 7-zip.org serves it +# unversioned and has already replaced it since this pin was taken, so only +# versioned URLs are listed here. +_WINDOWS_BOOTSTRAP = _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7zr.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7zr.exe", + ], + integrity = "sha256-J8vj1YBK0J6Qu8qpFtoNXDsL6UYtDg+2y1S+XtkDCHU=", + bin_path = "7zr.exe", +) + +ZIP7_SOURCES = { + "linux_arm64": _LINUX_ARM64, + "linux_x86_64": _LINUX_X86_64, + "macos_arm64": _MACOS, + "macos_x86_64": _MACOS, + "windows_arm64": _WINDOWS_BOOTSTRAP, + "windows_i686": _WINDOWS_BOOTSTRAP, + "windows_x86_64": _WINDOWS_BOOTSTRAP, +} + +# Secondary archives which are extracted using the binary from `ZIP7_SOURCES`. +# On Windows these are the official installers, which are 7-Zip SFX archives +# containing the full `7z.exe` and `7z.dll` for the host architecture. +ZIP7_EXTRAS = { + "windows_arm64": _zip7_data( + urls = [ + "https://mirror.bazel.build/www.7-zip.org/a/7z2501-arm64.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-arm64.exe", + "https://www.7-zip.org/a/7z2501-arm64.exe", + ], + integrity = "sha256-Y2XHxE4he5wQCeBl2vn5qjdFTmQxW0qqJj9/jwYHVdw=", + bin_path = "7z.exe", + ), + "windows_i686": _zip7_data( + urls = [ + "https://mirror.bazel.build/7-zip.org/a/7z2501.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501.exe", + "https://www.7-zip.org/a/7z2501.exe", + ], + integrity = "sha256-uWgx7sWSg4TwVD1rV8H4ApUqDyZo5mKILAp4WitS+zs=", + bin_path = "7z.exe", + ), + "windows_x86_64": _zip7_data( + urls = [ + "https://mirror.bazel.build/7-zip.org/a/7z2501-x64.exe", + "https://github.com/ip7z/7zip/releases/download/25.01/7z2501-x64.exe", + "https://www.7-zip.org/a/7z2501-x64.exe", + ], + integrity = "sha256-eK+iocdzyvPPft9i+FfSqKXaVfsP/12kFgdMDSiytV8=", + bin_path = "7z.exe", + ), +} + +def zip7_host_arch(ctx): + # type: (repository_ctx) -> str + """Identify the host platform. + + Args: + ctx (repository_ctx): The rule's repository context. + + Returns: + str: A key of `ZIP7_SOURCES`. + """ + arch = ctx.os.arch + if arch == "amd64": + arch = "x86_64" + elif arch == "aarch64": + arch = "arm64" + elif arch == "x86": + arch = "i686" + + if "linux" in ctx.os.name: + host = "linux_{}".format(arch) + elif "mac" in ctx.os.name: + host = "macos_{}".format(arch) + elif "win" in ctx.os.name: + host = "windows_{}".format(arch) + else: + fail("Unknown host OS: {}".format(ctx.os.name)) + + if host not in ZIP7_SOURCES: + fail("7-Zip is not available for host `{}`. Supported hosts: {}".format( + host, + sorted(ZIP7_SOURCES.keys()), + )) + + return host + +def zip7_extract( + *, + repository_ctx, + zip7_bin, + archive, + output, + extra_args = []): + # type: (repository_ctx, path, path, path, list[str]) -> None + """Extract an archive using 7-Zip. + + Args: + repository_ctx (repository_ctx): The rule's repository context. + zip7_bin (path): The path to a 7-Zip binary. + archive (path): The archive to extract. + output (path): The directory to extract into. + extra_args (list): Additional arguments to pass to 7-Zip. + """ + command = [zip7_bin, "x", archive, "-o{}".format(output), "-y"] + extra_args + result = repository_ctx.execute(command) + if result.return_code != 0: + fail("7z command failed with exit code {}\n{}\n\nstdout:\n{}\nstderr:\n{}".format( + result.return_code, + " ".join([str(a) for a in command]), + result.stdout, + result.stderr, + )) + +_BAZEL_ARCHIVE_SUFFIXES = ( + ".tar.bz2", + ".tar.gz", + ".tar.xz", + ".tar.zst", + ".tar", + ".tbz", + ".tgz", + ".txz", + ".tzst", + ".zip", +) + +def zip7_download(repository_ctx, output): + # type: (_Auth, str) -> path + """Download a pinned 7-Zip binary for the host platform. + + Args: + repository_ctx (repository_ctx): The rule's repository context. + output (str): The directory, relative to the repository, to download into. + + Returns: + path: The path to the 7-Zip binary. + """ + host = zip7_host_arch(repository_ctx) + source = ZIP7_SOURCES[host] + extras = ZIP7_EXTRAS.get(host) + + repository_ctx.report_progress("Downloading 7-Zip.") + output_dir = repository_ctx.path(output) + bin_path = output_dir.get_child(source.bin_path) + if source.urls[0].endswith(_BAZEL_ARCHIVE_SUFFIXES): + repository_ctx.download_and_extract( + url = source.urls, + output = output_dir, + integrity = source.integrity, + auth = get_auth(repository_ctx, source.urls), + ) + else: + repository_ctx.download( + url = source.urls, + output = bin_path, + integrity = source.integrity, + executable = True, + auth = get_auth(repository_ctx, source.urls), + ) + + if not bin_path.exists: + fail("Failed to locate the 7-Zip binary `{}` from: {}".format(source.bin_path, source.urls)) + + if not extras: + return bin_path + + repository_ctx.report_progress("Downloading 7-Zip extras.") + extras_archive = output_dir.get_child("extras_archive") + extras_dir = output_dir.get_child("extras") + repository_ctx.download( + url = extras.urls, + output = extras_archive, + integrity = extras.integrity, + auth = get_auth(repository_ctx, extras.urls), + ) + zip7_extract( + repository_ctx = repository_ctx, + zip7_bin = bin_path, + archive = extras_archive, + output = extras_dir, + ) + repository_ctx.delete(extras_archive) + + extras_bin_path = extras_dir.get_child(extras.bin_path) + if not extras_bin_path.exists: + fail("Failed to locate the 7-Zip binary `{}` from: {}".format(extras.bin_path, extras.urls)) + + return extras_bin_path 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_config.bzl b/xcode/xcode_config.bzl index 1268564c..96312eb5 100644 --- a/xcode/xcode_config.bzl +++ b/xcode/xcode_config.bzl @@ -15,6 +15,7 @@ """Implementation of the `xcode_config` build rule.""" load("@bazel_features//:features.bzl", "bazel_features") +load("@bazel_features//private:util.bzl", "ge") load("//build_settings:build_settings.bzl", "read_possibly_native_flag") load( "//xcode:providers.bzl", @@ -28,7 +29,29 @@ load( visibility("public") -unavailable_xcode_message = "'bazel fetch --configure'" +_DetectedXcodesInfo = provider( + doc = "Locally detected Xcode versions used only in diagnostics.", + fields = { + "versions": "Full Xcode version strings reported by xcode-locator.", + }, +) + +def _detected_xcodes_impl(ctx): + return [_DetectedXcodesInfo(versions = ctx.attr.versions)] + +detected_xcodes = rule( + implementation = _detected_xcodes_impl, + attrs = {"versions": attr.string_list()}, + doc = "Diagnostic inventory generated by xcode_autoconf; does not select an Xcode.", +) + +_NATIVE_XCODE_PROPERTIES_SDK_FIELDS = [ + "default_ios_sdk_version", + "default_macos_sdk_version", + "default_tvos_sdk_version", + "default_watchos_sdk_version", + "default_visionos_sdk_version", +] def _xcode_config_impl(ctx): # TODO: remove when we test with Bazel 8+ @@ -74,6 +97,8 @@ def _xcode_config_impl(ctx): explicit_versions, explicit_default_version, read_possibly_native_flag(ctx, "xcode_version"), + ctx.attr.detected_versions[_DetectedXcodesInfo].versions if ctx.attr.detected_versions else [], + ctx.attr.xcode_selection_hint, ) availability = "UNKNOWN" @@ -130,6 +155,22 @@ def _xcode_config_impl(ctx): xcode_version_properties, ] + # Bazel 8's native `xcode_config` also returns this, and rules on that + # version may still read it instead of `XcodeVersionPropertiesInfo`. + # TODO: Remove once we no longer support Bazel 8. + native_xcode_properties = getattr(apple_common, "XcodeProperties", None) + if native_xcode_properties: + # `xcode_version` is required even when `None`; unset SDK versions + # take the native provider's defaults. + providers.append(native_xcode_properties( + xcode_version = xcode_version_properties.xcode_version, + **{ + field: getattr(xcode_version_properties, field) + for field in _NATIVE_XCODE_PROPERTIES_SDK_FIELDS + if getattr(xcode_version_properties, field) != None + } + )) + # TODO: b/367688113 - Once we've migrated all clients to generate and use # the new `xcode_sdk_variant` rules, fail if this field is not set. if xcode_version_properties.sdk_variant_info: @@ -139,6 +180,22 @@ def _xcode_config_impl(ctx): xcode_config = rule( attrs = { + "xcode_selection_hint": attr.string( + default = "Install a supported Xcode version and select it with \033[1;36mxcode-select\033[0m.", + doc = """\ +Guidance displayed when an explicitly selected Xcode does not match `versions`. +Replaces the default installation and selection instructions. Newlines are +preserved, and an empty string omits the hint. +""", + ), + "detected_versions": attr.label( + default = "@local_config_xcode//:detected_xcodes", + providers = [_DetectedXcodesInfo], + doc = """\ +Locally detected Xcode versions used only in diagnostics. These do not affect +which versions are supported or selected. +""", + ), "default": attr.label( doc = """\ The default official version of Xcode to use. @@ -253,6 +310,38 @@ def _aliases_to_xcode_version(versions): version_map[version_string] = version return version_map +def _fail_xcode_selection(message): + # stack_trace was added in Bazel 9.0.1. + # https://github.com/bazelbuild/bazel/pull/28817 + if ge("9.0.1"): + fail(message, stack_trace = False) + fail(message) + +def _display_xcode_version(version, known_versions = [], detected_versions = []): + # A build number can appear in version metadata without being a registered + # alias. Resolve it for display only; do not change version selection. + if "." not in version and not version.isdigit(): + matches = {} + for full_version in detected_versions + [known_version.xcode_version_properties.xcode_version for known_version in known_versions]: + if full_version.endswith("." + version): + matches[_format_xcode_version(full_version)] = True + if len(matches) == 1: + return matches.keys()[0] + return _format_xcode_version(version) + +def _format_xcode_version(version): + # Xcode version strings may append the build number to the release version, + # e.g. 27.0.27A266a or 27.0.0.27A266a. + components = version.split(".") + if len(components) < 3 or components[-1].isdigit(): + return version + release = components[:-1] + if not all([component.isdigit() for component in release]): + return version + if len(release) == 3 and release[-1] == "0": + release = release[:-1] + return "{} (build {})".format(".".join(release), components[-1]) + def _resolve_xcode_from_local_and_remote( local_versions, remote_versions, @@ -319,29 +408,27 @@ def _resolve_xcode_from_local_and_remote( elif remote_version_from_flag: # buildifier: disable=print - print(("--xcode_version={version} specified, but it is not available locally. " + - "Your build will fail if any actions require a local Xcode. " + - "If you believe you have '{version}' installed, try running {command}," + - "and then re-run your command. Locally available versions: {local_versions}. ") + print(("\n\033[1;33mXcode {version} was not found locally.\033[0m\n\n" + + "Remote builds can use it; local actions require this Xcode.\n" + + "Detected locally: {local_versions}\n\n" + + "Install Xcode {version} to run actions locally.\n") .format( - version = xcode_version_flag, - command = unavailable_xcode_message, - local_versions = ", ".join([version for version in local_alias_to_version_map.keys()]), + version = _display_xcode_version(remote_version_from_flag.xcode_version_properties.xcode_version), + local_versions = ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in local_versions]) or "none", )) availability = "REMOTE" return remote_version_from_flag.xcode_version_properties, availability else: # fail if we can't find any version to match - fail( - ("--xcode_version={0} specified, but '{0}' is not an available Xcode version." + - " Locally available versions: [{2}]. Remotely available versions: [{3}]. If" + - " you believe you have '{0}' installed, try running {1}, and then" + - " re-run your command.").format( - xcode_version_flag, - unavailable_xcode_message, - ", ".join([version.xcode_version_properties.xcode_version for version in local_versions]), - ", ".join([version.xcode_version_properties.xcode_version for version in remote_versions]), + _fail_xcode_selection( + ("\n\033[1;31mXcode {0} was not found.\033[0m\n\n" + + " Detected locally: {1}\n" + + " Available remotely: {2}\n\n" + + "Install Xcode {0} and retry.\n").format( + _display_xcode_version(xcode_version_flag, local_versions + remote_versions), + ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in local_versions]) or "none", + ", ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in remote_versions]) or "none", ), ) @@ -401,7 +488,9 @@ def _sdk_version_flag(apple_fragment, name): def _resolve_explicitly_defined_version( explicit_versions, explicit_default_version, - xcode_version_flag): + xcode_version_flag, + detected_versions, + xcode_selection_hint): if explicit_default_version and explicit_default_version.label not in [ version.label for version in explicit_versions @@ -425,10 +514,22 @@ def _resolve_explicitly_defined_version( if flag_version: return flag_version.xcode_version_properties else: - fail( - ("--xcode_version={0} specified, but '{0}' is not an available Xcode version. " + - "If you believe you have '{0}' installed, try running \"bazel shutdown\", and then " + - "re-run your command.").format(xcode_version_flag), + detected_message = "" + if detected_versions: + detected_message = "\033[1mDetected locally:\033[0m\n {}\n\n".format( + "\n ".join(sorted({_format_xcode_version(version): True for version in detected_versions})), + ) + _fail_xcode_selection( + ("\n\033[1;31mBazel couldn’t match the selected Xcode to this project’s supported versions:\033[0m\n" + + " {0}\n\n" + + "\033[1mSupported versions:\033[0m\n {1}\n\n" + + "{2}" + + "{3}").format( + _display_xcode_version(xcode_version_flag, explicit_versions, detected_versions), + "\n ".join([_display_xcode_version(version.xcode_version_properties.xcode_version) for version in explicit_versions]), + detected_message, + xcode_selection_hint + "\n" if xcode_selection_hint else "", + ), ) return alias_to_versions.get(explicit_default_version.xcode_version_properties.xcode_version).xcode_version_properties diff --git a/xcode/xcode_configure.bzl b/xcode/xcode_configure.bzl index 40a40ef0..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 = [] @@ -134,11 +143,18 @@ def _xcode_version_output(repository_ctx, name, version, aliases, developer_dir, return build_contents VERSION_CONFIG_STUB = """ -load("@apple_support//xcode:xcode_config.bzl", "xcode_config") +load("@apple_support//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") +detected_xcodes(name = 'detected_xcodes') xcode_config(name = 'host_xcodes') """ +# 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. @@ -242,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 @@ -268,6 +285,9 @@ def _darwin_build_file(repository_ctx): if xcodeloc_err: return VERSION_CONFIG_STUB + "\n# Error: " + xcodeloc_err + "\n" + if not toolchains: + return VERSION_CONFIG_STUB + default_xcode_version = "" default_xcode_build_version = "" if xcodebuild_result.return_code == 0: @@ -280,10 +300,14 @@ def _darwin_build_file(repository_ctx): default_xcode_target = "" target_names = [] buildcontents = """ -load("@apple_support//xcode:xcode_config.bzl", "xcode_config") +load("@apple_support//xcode:xcode_config.bzl", "detected_xcodes", "xcode_config") load("@apple_support//xcode:available_xcodes.bzl", "available_xcodes") load("@apple_support//xcode:xcode_version.bzl", "xcode_version") """ + buildcontents += "detected_xcodes(name = 'detected_xcodes', versions = %s)\n" % repr([ + toolchain.version + for toolchain in toolchains + ]) for toolchain in toolchains: version = toolchain.version @@ -328,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', @@ -361,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", @@ -369,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") diff --git a/xcode/xcode_version_at_least.bzl b/xcode/xcode_version_at_least.bzl index 6b21a077..e45f7379 100644 --- a/xcode/xcode_version_at_least.bzl +++ b/xcode/xcode_version_at_least.bzl @@ -105,7 +105,13 @@ def _xcode_version_at_least_flag_impl(ctx): ) xcode_config = ctx.attr._xcode_config[XcodeVersionInfo] xcode_version = xcode_config.xcode_version() - matches = bool(xcode_version and xcode_version >= minimum_xcode_version) + + # The provider returns a string only for a developer directory, which has + # no comparable version. Assume a recent Xcode. + matches = bool(xcode_version) and ( + type(xcode_version) == "string" or + xcode_version >= minimum_xcode_version + ) return [ config_common.FeatureFlagInfo(value = str(matches)), @@ -146,17 +152,15 @@ or `"27.0.0.27A5228h"`. def _flag_target_label(label): """Returns the `_flag` target label corresponding to an `xcode_version_at_least` target.""" - if not label.startswith(":") and not label.startswith("//") and not label.startswith("@"): - label = ":" + label - return label + "_flag" + label = native.package_relative_label(label) + return label.same_package_label(label.name + "_flag") def _local_target_name(label): """Returns the local target name if `label` refers to the current package, or `None`.""" - if label.startswith("//") or label.startswith("@"): - return None - if label.startswith(":"): - return label[1:] - return label + label = native.package_relative_label(label) + if label == native.package_relative_label(":" + label.name): + return label.name + return None def xcode_version_at_least( *, @@ -169,7 +173,8 @@ def xcode_version_at_least( The condition compares the resolved Xcode version, including when Xcode is selected by default without an explicit `--xcode_version` flag. It does not - match if the resolved configuration has no Xcode version. + match if the resolved configuration has no Xcode version. A developer + directory is assumed to represent a recent Xcode and matches any minimum. For example, enable a definition with Xcode 26.4 or newer: @@ -217,11 +222,11 @@ def xcode_version_at_least( `xcode_27_or_newer` branch wins. Without `after`, different values for these overlapping conditions would make the `select()` ambiguous. - Declare lower thresholds before higher thresholds and use local target names - or `:name` labels to inherit a same-package `after` chain transitively. For - cross-package or fully qualified references, list every lower threshold in - the chain explicitly. Each threshold must be strictly higher than those in - its `after` list. + Declare lower thresholds before higher thresholds to inherit a same-package + `after` chain transitively, whether references are strings or `Label` objects. + For cross-package references, list every lower threshold in the chain + explicitly. Each threshold must be strictly higher than those in its `after` + list. Args: name: The name of the `config_setting` target to create. A companion @@ -232,7 +237,7 @@ def xcode_version_at_least( names or labels that this condition should refine when used together in the same `select()` expression. If a referenced target is in the same package, any lower thresholds it already refines are inherited - transitively. + transitively, whether the reference is a string or a `Label` object. visibility: The visibility of the generated targets. **kwargs: Additional keyword arguments (such as `tags` or `constraint_values`) forwarded to `native.config_setting`. `tags` is @@ -240,7 +245,10 @@ def xcode_version_at_least( """ flag_name = name + "_flag" tags = kwargs.get("tags") - flag_values = dict(kwargs.pop("flag_values", None) or {}) + flag_values = { + native.package_relative_label(flag): value + for flag, value in (kwargs.pop("flag_values", None) or {}).items() + } after_flag_labels = [] if after: @@ -259,7 +267,7 @@ def xcode_version_at_least( continue for inherited_flag, inherited_value in existing_setting.get("flag_values").items(): - flag_values[inherited_flag] = inherited_value + flag_values[native.package_relative_label(inherited_flag)] = inherited_value _xcode_version_at_least_flag( name = flag_name, @@ -271,7 +279,7 @@ def xcode_version_at_least( visibility = visibility, ) - flag_values[":" + flag_name] = "True" + flag_values[native.package_relative_label(":" + flag_name)] = "True" native.config_setting( name = name,