blob: ac063fac482f959ef50a38772fd6c804a58796f1 [file] [log] [blame]
# 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",
"WHEEL_FILE_IMPL_LABEL",
"WHEEL_FILE_PUBLIC_LABEL",
)
_NO_MATCH_ERROR_TEMPLATE = """\
No matching wheel for current configuration's Python version.
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}
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,
**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.
extra_aliases: {type}`list[str]` The extra aliases to be created.
**kwargs: extra kwargs to pass to {bzl:obj}`get_config_settings`.
"""
alias = kwargs.pop("native", native).alias
select = kwargs.pop("select", selects.with_or)
alias(
name = name,
actual = ":" + PY_LIBRARY_PUBLIC_LABEL,
)
target_names = {
PY_LIBRARY_PUBLIC_LABEL: PY_LIBRARY_IMPL_LABEL if group_name else PY_LIBRARY_PUBLIC_LABEL,
WHEEL_FILE_PUBLIC_LABEL: WHEEL_FILE_IMPL_LABEL if group_name else WHEEL_FILE_PUBLIC_LABEL,
DATA_LABEL: DATA_LABEL,
DIST_INFO_LABEL: DIST_INFO_LABEL,
EXTRACTED_WHEEL_FILES: EXTRACTED_WHEEL_FILES,
} | {
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:
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__"]
alias(
name = target_name,
actual = _actual,
**kwargs
)
if group_name:
alias(
name = PY_LIBRARY_PUBLIC_LABEL,
actual = "//_groups:{}_pkg".format(group_name),
)
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 {func}`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
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))