| # Copyright 2024 The Bazel Authors. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """{obj}`pkg_aliases` is a macro to generate aliases for selecting the right wheel for the right target platform. | |
| If you see an error where the distribution selection error indicates the config setting names this | |
| page may help to describe the naming convention and relationship between various flags and options | |
| in `rules_python` and the error message contents. | |
| Definitions: | |
| :minor_version: Python interpreter minor version that the distributions are compatible with. | |
| :suffix: Can be either empty or `_<os>_<suffix>`, which is usually used to distinguish multiple versions used for different target platforms. | |
| :os: OS identifier that exists in `@platforms//os:<os>`. | |
| :cpu: CPU architecture identifier that exists in `@platforms//cpu:<cpu>`. | |
| All of the config settings used by this macro are generated by | |
| {obj}`config_settings`, for more detailed documentation on what each config | |
| setting maps to and their precedence, refer to documentation on that page. | |
| `//_config:is_cp3<minor_version><suffix>` is used to select any target platforms. | |
| """ | |
| load("@bazel_skylib//lib:selects.bzl", "selects") | |
| load("//python/private:common_labels.bzl", "labels") | |
| load("//python/private:text_util.bzl", "render") | |
| load( | |
| ":labels.bzl", | |
| "DATA_LABEL", | |
| "DIST_INFO_LABEL", | |
| "EXTRACTED_WHEEL_FILES", | |
| "PY_LIBRARY_IMPL_LABEL", | |
| "PY_LIBRARY_PUBLIC_LABEL", | |
| "PY_SRCS_LABEL", | |
| "WHEEL_FILE", | |
| "WHEEL_FILE_IMPL_LABEL", | |
| "WHEEL_FILE_PUBLIC_LABEL", | |
| ) | |
| load(":whl_library_deps_targets.bzl", "whl_library_deps_targets") | |
| _NO_MATCH_ERROR_TEMPLATE = """\ | |
| No matching wheel for current configuration's Python version and platform. | |
| The current build configuration's Python version doesn't match any of the Python | |
| wheels available for this distribution. This distribution supports the following Python | |
| configuration settings: | |
| {config_settings} | |
| As configured by the `pip.parse.target_platforms` attribute. Note that | |
| `requirements_by_platform` only affects the Bazel host platform unless | |
| `target_platforms` is also set. | |
| To determine the current configuration's Python version, run: | |
| `bazel config <config id>` (shown further below) | |
| For the current configuration value see the debug message above that is | |
| printing the current flag values. If you can't see the message, then re-run the | |
| build to make it a failure instead by running the build with: | |
| --{current_flags}=fail | |
| However, the command above will hide the `bazel config <config id>` message. | |
| """ | |
| _LABEL_NONE = labels.NONE | |
| _LABEL_CURRENT_CONFIG = Label("//python/config_settings:current_config") | |
| _LABEL_CURRENT_CONFIG_NO_MATCH = Label("//python/config_settings:is_not_matching_current_config") | |
| _INCOMPATIBLE = "_no_matching_repository" | |
| def pkg_aliases( | |
| *, | |
| name, | |
| actual, | |
| group_name = None, | |
| extra_aliases = None, | |
| requires_dist = [], | |
| extras = [], | |
| include = [], | |
| group_deps = [], | |
| **kwargs): | |
| """Create aliases for an actual package. | |
| Exposed only to be used from the hub repositories created by `rules_python`. | |
| Args: | |
| name: {type}`str` The name of the package. | |
| actual: {type}`dict[Label | tuple, str] | str` The name of the repo the | |
| aliases point to, or a dict of select conditions to repo names for | |
| the aliases to point to mapping to repositories. The keys are passed | |
| to bazel skylib's `selects.with_or`, so they can be tuples as well. | |
| group_name: {type}`str` The group name that the pkg belongs to. | |
| group_deps: {type}`list[str]` The packages that are in the given group. Comes | |
| as an arg through the hub repository. | |
| extra_aliases: {type}`list[str]` The extra aliases to be created. | |
| requires_dist: {type}`list[str]` The list of dependencies. Comes from METADATA or a lock | |
| file. | |
| extras: {type}`list[str]` The extras for which we should add extra | |
| dependencies when parsing the requires_dist. Comes from METADATA or a lock file. | |
| include: {type}`list[str]` The subset of packages to include. | |
| Comes from `//:config.bzl#packages` | |
| **kwargs: extra kwargs to pass to {bzl:obj}`get_config_settings`. | |
| """ | |
| _native = kwargs.pop("native", native) | |
| rules = kwargs.pop("rules", struct( | |
| whl_library_deps_targets = whl_library_deps_targets, | |
| )) | |
| select = kwargs.pop("select", selects.with_or) | |
| _native.alias( | |
| name = name, | |
| actual = ":" + PY_LIBRARY_PUBLIC_LABEL, | |
| ) | |
| if requires_dist: | |
| # if it has a group_name set, then it will create the impl label target in the | |
| # current macro, we still need to do the aliases to the actual groups package as | |
| # per below | |
| rules.whl_library_deps_targets( | |
| repo = None, | |
| aliases = {}, # not none | |
| metadata_name = name, | |
| requires_dist = requires_dist, | |
| extras = extras, | |
| include = include, | |
| group_deps = group_deps, | |
| group_name = group_name, | |
| dep_template = "//{name}:{target}", # this is const in this setting | |
| visibility = ["//visibility:public"], | |
| native = _native, | |
| rules = rules, | |
| ) | |
| target_names = {} | |
| else: | |
| if group_name: | |
| py_library_target = PY_LIBRARY_IMPL_LABEL | |
| whl_target = WHEEL_FILE_IMPL_LABEL | |
| else: | |
| py_library_target = PY_LIBRARY_PUBLIC_LABEL | |
| whl_target = WHEEL_FILE_PUBLIC_LABEL | |
| target_names = { | |
| PY_LIBRARY_PUBLIC_LABEL: py_library_target, | |
| WHEEL_FILE_PUBLIC_LABEL: whl_target, | |
| } | |
| target_names = target_names | { | |
| DATA_LABEL: DATA_LABEL, | |
| DIST_INFO_LABEL: DIST_INFO_LABEL, | |
| EXTRACTED_WHEEL_FILES: EXTRACTED_WHEEL_FILES, | |
| PY_SRCS_LABEL: PY_SRCS_LABEL, | |
| WHEEL_FILE: WHEEL_FILE, | |
| } | { | |
| x: x | |
| for x in extra_aliases or [] | |
| } | |
| actual = multiplatform_whl_aliases(aliases = actual, **kwargs) | |
| if type(actual) == type({}) and "//conditions:default" not in actual: | |
| _native.alias( | |
| name = _INCOMPATIBLE, | |
| actual = select( | |
| {_LABEL_CURRENT_CONFIG_NO_MATCH: _LABEL_NONE}, | |
| no_match_error = _NO_MATCH_ERROR_TEMPLATE.format( | |
| config_settings = render.indent( | |
| "\n".join(sorted([ | |
| value | |
| for key in actual | |
| for value in (key if type(key) == "tuple" else [key]) | |
| ])), | |
| ).lstrip(), | |
| current_flags = str(_LABEL_CURRENT_CONFIG), | |
| ), | |
| ), | |
| visibility = ["//visibility:private"], | |
| tags = ["manual"], | |
| ) | |
| actual["//conditions:default"] = _INCOMPATIBLE | |
| for name, target_name in target_names.items(): | |
| if type(actual) == type(""): | |
| _actual = "@{repo}//:{target_name}".format( | |
| repo = actual, | |
| target_name = name, | |
| ) | |
| elif type(actual) == type({}): | |
| _actual = select( | |
| { | |
| v: "@{repo}//:{target_name}".format( | |
| repo = repo, | |
| target_name = name, | |
| ) if repo != _INCOMPATIBLE else repo | |
| for v, repo in actual.items() | |
| }, | |
| ) | |
| else: | |
| fail("The `actual` arg must be a dictionary or a string") | |
| kwargs = {} | |
| if target_name.startswith("_"): | |
| kwargs["visibility"] = ["//_groups:__subpackages__"] | |
| elif target_name in (WHEEL_FILE, PY_SRCS_LABEL): | |
| kwargs["visibility"] = ["//visibility:private"] | |
| _native.alias( | |
| name = target_name, | |
| actual = _actual, | |
| **kwargs | |
| ) | |
| if group_name: | |
| _native.alias( | |
| name = PY_LIBRARY_PUBLIC_LABEL, | |
| actual = "//_groups:{}_pkg".format(group_name), | |
| ) | |
| _native.alias( | |
| name = WHEEL_FILE_PUBLIC_LABEL, | |
| actual = "//_groups:{}_whl".format(group_name), | |
| ) | |
| def multiplatform_whl_aliases( | |
| *, | |
| aliases = []): | |
| """convert a list of aliases from filename to config_setting ones. | |
| Exposed only for unit tests. | |
| Args: | |
| aliases: {type}`str | dict[struct | str, str]`: The aliases | |
| to process. Any aliases that have the filename set will be | |
| converted to a dict of config settings to repo names. The | |
| struct is created by {bzl:obj}`whl_config_setting`. | |
| Returns: | |
| A dict with of config setting labels to repo names or the repo name itself. | |
| """ | |
| if type(aliases) == type(""): | |
| # We don't have any aliases, this is a repo name | |
| return aliases | |
| ret = {} | |
| for alias, repo in aliases.items(): | |
| if type(alias) != "struct": | |
| ret[alias] = repo | |
| continue | |
| # This is if we are using `whl_config_setting` struct | |
| config_settings = get_config_settings( | |
| target_platforms = alias.target_platforms, | |
| python_version = alias.version, | |
| ) | |
| for setting in config_settings: | |
| ret["//_config" + setting] = repo | |
| return ret | |
| def get_config_settings( | |
| *, | |
| target_platforms, | |
| python_version): | |
| """Get the filename config settings. | |
| Exposed only for unit tests. | |
| Args: | |
| target_platforms: list[str], target platforms in "{abi}_{os}_{cpu}" format. | |
| python_version: the python version to generate the config_settings for. | |
| Returns: | |
| A tuple: | |
| * A list of config settings that are generated by ./pip_config_settings.bzl | |
| * The list of default version settings. | |
| """ | |
| prefixes = [ | |
| "cp{}".format(python_version).replace(".", ""), | |
| ] | |
| if target_platforms: | |
| target_platforms = target_platforms or [] | |
| suffixes = [_non_versioned_platform(p) for p in target_platforms] | |
| return [ | |
| ":is_{}_{}".format(prefix, suffix) | |
| for prefix in prefixes | |
| for suffix in suffixes | |
| ] | |
| else: | |
| return [":is_{}".format(p) for p in prefixes] | |
| def _non_versioned_platform(p, *, strict = False): | |
| """A small utility function that converts 'cp311_linux_x86_64' to 'linux_x86_64'. | |
| This is so that we can tighten the code structure later by using strict = True. | |
| """ | |
| has_abi = p.startswith("cp") | |
| if has_abi: | |
| return p.partition("_")[-1] | |
| elif not strict: | |
| return p | |
| else: | |
| fail("Expected to always have a platform in the form '{{abi}}_{{os}}_{{arch}}', got: {}".format(p)) |