blob: 1045ffef354a1cf8a2d07a623eca42180bfcc2fe [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.
"""
The {obj}`config_settings` macro is used to create the config setting targets
that can be used in the {obj}`pkg_aliases` macro for selecting the compatible
repositories.
Bazel's selects work by selecting the most-specialized configuration setting
that matches the target platform, which is further described in [bazel documentation][docs].
We can leverage this fact to ensure that the most specialized matches are used
by default with the users being able to configure string_flag values to select
the less specialized ones.
[docs]: https://bazel.build/docs/configurable-attributes
The config settings in the order from the least specialized to the most
specialized is as follows:
* `:is_cp3<minor_version><suffix>`
* `:is_cp3<minor_version>_sdist<suffix>`
* `:is_cp3<minor_version>_py_none_any<suffix>`
* `:is_cp3<minor_version>_py3_none_any<suffix>`
* `:is_cp3<minor_version>_py3_abi3_any<suffix>`
* `:is_cp3<minor_version>_none_any<suffix>`
* `:is_cp3<minor_version>_any_any<suffix>`
* `:is_cp3<minor_version>_cp3<minor_version>_any<suffix>` and `:is_cp3<minor_version>_cp3<minor_version>t_any<suffix>`
* `:is_cp3<minor_version>_py_none_<platform_suffix>`
* `:is_cp3<minor_version>_py3_none_<platform_suffix>`
* `:is_cp3<minor_version>_py3_abi3_<platform_suffix>`
* `:is_cp3<minor_version>_none_<platform_suffix>`
* `:is_cp3<minor_version>_abi3_<platform_suffix>`
* `:is_cp3<minor_version>_cp3<minor_version>_<platform_suffix>` and `:is_cp3<minor_version>_cp3<minor_version>t_<platform_suffix>`
The specialization of free-threaded vs non-free-threaded wheels is the same as
they are just variants of each other. The same goes for the specialization of
`musllinux` vs `manylinux`.
The goal of this macro is to provide config settings that provide unambigous
matches if any pair of them is used together for any target configuration
setting. We achieve this by using dummy internal `flag_values` keys to force the
items further down the list to appear to be more specialized than the ones above.
What is more, the names of the config settings are as similar to the platform wheel
specification as possible. How the wheel names map to the config setting names defined
in here is described in {obj}`pkg_aliases` documentation.
:::{note}
Right now the specialization of adjacent config settings where one is with
`constraint_values` and one is without is ambiguous. I.e. `py_none_any` and
`sdist_linux_x86_64` have the same specialization from bazel point of view
because one has one `flag_value` entry and `constraint_values` and the
other has 2 flag_value entries. And unfortunately there is no way to disambiguate
it, because we are essentially in two dimensions here (`flag_values` and
`constraint_values`). Hence, when using the `config_settings` from here,
either have all of them with empty `suffix` or all of them with a non-empty
suffix.
:::
"""
load("//python/private:flags.bzl", "LibcFlag")
load(":flags.bzl", "INTERNAL_FLAGS", "UniversalWhlFlag")
FLAGS = struct(
**{
f: str(Label("//python/config_settings:" + f))
for f in [
"is_pip_whl_auto",
"is_pip_whl_no",
"is_pip_whl_only",
"is_py_freethreaded",
"is_py_non_freethreaded",
"pip_whl_glibc_version",
"pip_whl_muslc_version",
"pip_whl_osx_arch",
"pip_whl_osx_version",
"py_linux_libc",
"python_version",
]
}
)
_DEFAULT = "//conditions:default"
_INCOMPATIBLE = "@platforms//:incompatible"
# Here we create extra string flags that are just to work with the select
# selecting the most specialized match. We don't allow the user to change
# them.
_flags = struct(
**{
f: str(Label("//python/config_settings:_internal_pip_" + f))
for f in INTERNAL_FLAGS
}
)
def config_settings(
*,
python_versions = [],
glibc_versions = [],
muslc_versions = [],
osx_versions = [],
target_platforms = [],
name = None,
**kwargs):
"""Generate all of the pip config settings.
Args:
name (str): Currently unused.
python_versions (list[str]): The list of python versions to configure
config settings for.
glibc_versions (list[str]): The list of glibc version of the wheels to
configure config settings for.
muslc_versions (list[str]): The list of musl version of the wheels to
configure config settings for.
osx_versions (list[str]): The list of OSX OS versions to configure
config settings for.
target_platforms (list[str]): The list of "{os}_{cpu}" for deriving
constraint values for each condition.
**kwargs: Other args passed to the underlying implementations, such as
{obj}`native`.
"""
glibc_versions = [""] + glibc_versions
muslc_versions = [""] + muslc_versions
osx_versions = [""] + osx_versions
target_platforms = [("", ""), ("osx", "universal2")] + [
t.split("_", 1)
for t in target_platforms
]
for python_version in python_versions:
for os, cpu in target_platforms:
constraint_values = []
suffix = ""
if os:
constraint_values.append("@platforms//os:" + os)
suffix += "_" + os
if cpu:
suffix += "_" + cpu
if cpu != "universal2":
constraint_values.append("@platforms//cpu:" + cpu)
_dist_config_settings(
suffix = suffix,
plat_flag_values = _plat_flag_values(
os = os,
cpu = cpu,
osx_versions = osx_versions,
glibc_versions = glibc_versions,
muslc_versions = muslc_versions,
),
constraint_values = constraint_values,
python_version = python_version,
**kwargs
)
def _dist_config_settings(*, suffix, plat_flag_values, python_version, **kwargs):
flag_values = {
Label("//python/config_settings:python_version_major_minor"): python_version,
}
cpv = "cp" + python_version.replace(".", "")
prefix = "is_{}".format(cpv)
_dist_config_setting(
name = prefix + suffix,
flag_values = flag_values,
**kwargs
)
flag_values[_flags.dist] = ""
# First create an sdist, we will be building upon the flag values, which
# will ensure that each sdist config setting is the least specialized of
# all. However, we need at least one flag value to cover the case where we
# have `sdist` for any platform, hence we have a non-empty `flag_values`
# here.
_dist_config_setting(
name = "{}_sdist{}".format(prefix, suffix),
flag_values = flag_values,
compatible_with = (FLAGS.is_pip_whl_no, FLAGS.is_pip_whl_auto),
**kwargs
)
used_flags = {}
# NOTE @aignas 2024-12-01: the abi3 is not compatible with freethreaded
# builds as per PEP703 (https://peps.python.org/pep-0703/#backwards-compatibility)
#
# The discussion here also reinforces this notion:
# https://discuss.python.org/t/pep-703-making-the-global-interpreter-lock-optional-3-12-updates/26503/99
for name, f, compatible_with in [
("py_none", _flags.whl, None),
("py3_none", _flags.whl_py3, None),
("py3_abi3", _flags.whl_py3_abi3, (FLAGS.is_py_non_freethreaded,)),
("none", _flags.whl_pycp3x, None),
("abi3", _flags.whl_pycp3x_abi3, (FLAGS.is_py_non_freethreaded,)),
# The below are not specializations of one another, they are variants
(cpv, _flags.whl_pycp3x_abicp, (FLAGS.is_py_non_freethreaded,)),
(cpv + "t", _flags.whl_pycp3x_abicp, (FLAGS.is_py_freethreaded,)),
]:
if (f, compatible_with) in used_flags:
# This should never happen as all of the different whls should have
# unique flag values
fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
else:
flag_values[f] = "yes" if f == _flags.whl else ""
used_flags[(f, compatible_with)] = True
_dist_config_setting(
name = "{}_{}_any{}".format(prefix, name, suffix),
flag_values = flag_values,
compatible_with = compatible_with,
**kwargs
)
generic_flag_values = flag_values
generic_used_flags = used_flags
for (suffix, flag_values) in plat_flag_values:
used_flags = {(f, None): True for f in flag_values} | generic_used_flags
flag_values = flag_values | generic_flag_values
for name, f, compatible_with in [
("py_none", _flags.whl_plat, None),
("py3_none", _flags.whl_plat_py3, None),
("py3_abi3", _flags.whl_plat_py3_abi3, (FLAGS.is_py_non_freethreaded,)),
("none", _flags.whl_plat_pycp3x, None),
("abi3", _flags.whl_plat_pycp3x_abi3, (FLAGS.is_py_non_freethreaded,)),
# The below are not specializations of one another, they are variants
(cpv, _flags.whl_plat_pycp3x_abicp, (FLAGS.is_py_non_freethreaded,)),
(cpv + "t", _flags.whl_plat_pycp3x_abicp, (FLAGS.is_py_freethreaded,)),
]:
if (f, compatible_with) in used_flags:
# This should never happen as all of the different whls should have
# unique flag values.
fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
else:
flag_values[f] = ""
used_flags[(f, compatible_with)] = True
_dist_config_setting(
name = "{}_{}_{}".format(prefix, name, suffix),
flag_values = flag_values,
compatible_with = compatible_with,
**kwargs
)
def _to_version_string(version, sep = "."):
if not version:
return ""
return "{}{}{}".format(version[0], sep, version[1])
def _plat_flag_values(os, cpu, osx_versions, glibc_versions, muslc_versions):
ret = []
if os == "":
return []
elif os == "windows":
ret.append(("{}_{}".format(os, cpu), {}))
elif os == "osx":
for osx_version in osx_versions:
flags = {
FLAGS.pip_whl_osx_version: _to_version_string(osx_version),
}
if cpu != "universal2":
flags[FLAGS.pip_whl_osx_arch] = UniversalWhlFlag.ARCH
if not osx_version:
suffix = "{}_{}".format(os, cpu)
else:
suffix = "{}_{}_{}".format(os, _to_version_string(osx_version, "_"), cpu)
ret.append((suffix, flags))
elif os == "linux":
for os_prefix, linux_libc in {
os: LibcFlag.GLIBC,
"many" + os: LibcFlag.GLIBC,
"musl" + os: LibcFlag.MUSL,
}.items():
if linux_libc == LibcFlag.GLIBC:
libc_versions = glibc_versions
libc_flag = FLAGS.pip_whl_glibc_version
elif linux_libc == LibcFlag.MUSL:
libc_versions = muslc_versions
libc_flag = FLAGS.pip_whl_muslc_version
else:
fail("Unsupported libc type: {}".format(linux_libc))
for libc_version in libc_versions:
if libc_version and os_prefix == os:
continue
elif libc_version:
suffix = "{}_{}_{}".format(os_prefix, _to_version_string(libc_version, "_"), cpu)
else:
suffix = "{}_{}".format(os_prefix, cpu)
ret.append((
suffix,
{
FLAGS.py_linux_libc: linux_libc,
libc_flag: _to_version_string(libc_version),
},
))
else:
fail("Unsupported os: {}".format(os))
return ret
def _dist_config_setting(*, name, compatible_with = None, native = native, **kwargs):
"""A macro to create a target for matching Python binary and source distributions.
Args:
name: The name of the public target.
compatible_with: {type}`tuple[Label]` A collection of config settings that are
compatible with the given dist config setting. For example, if only
non-freethreaded python builds are allowed, add
FLAGS.is_py_non_freethreaded here.
native (struct): The struct containing alias and config_setting rules
to use for creating the objects. Can be overridden for unit tests
reasons.
**kwargs: The kwargs passed to the config_setting rule. Visibility of
the main alias target is also taken from the kwargs.
"""
if compatible_with:
dist_config_setting_name = "_" + name
native.alias(
name = name,
actual = select(
{setting: dist_config_setting_name for setting in compatible_with} | {
_DEFAULT: _INCOMPATIBLE,
},
),
visibility = kwargs.get("visibility"),
)
name = dist_config_setting_name
native.config_setting(name = name, **kwargs)