| # 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) |