xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/pypi/config_settings.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
1# Copyright 2024 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16This module is used to construct the config settings for selecting which distribution is used in the pip hub repository.
17
18Bazel's selects work by selecting the most-specialized configuration setting
19that matches the target platform. We can leverage this fact to ensure that the
20most specialized wheels are used by default with the users being able to
21configure string_flag values to select the less specialized ones.
22
23The list of specialization of the dists goes like follows:
24* sdist
25* py*-none-any.whl
26* py*-abi3-any.whl
27* py*-cpxy-any.whl
28* cp*-none-any.whl
29* cp*-abi3-any.whl
30* cp*-cpxy-plat.whl
31* py*-none-plat.whl
32* py*-abi3-plat.whl
33* py*-cpxy-plat.whl
34* cp*-none-plat.whl
35* cp*-abi3-plat.whl
36* cp*-cpxy-plat.whl
37
38Note, that here the specialization of musl vs manylinux wheels is the same in
39order to ensure that the matching fails if the user requests for `musl` and we don't have it or vice versa.
40"""
41
42load(":flags.bzl", "INTERNAL_FLAGS", "UniversalWhlFlag", "WhlLibcFlag")
43
44FLAGS = struct(
45    **{
46        f: str(Label("//python/config_settings:" + f))
47        for f in [
48            "python_version",
49            "pip_whl_glibc_version",
50            "pip_whl_muslc_version",
51            "pip_whl_osx_arch",
52            "pip_whl_osx_version",
53            "py_linux_libc",
54            "is_pip_whl_no",
55            "is_pip_whl_only",
56            "is_pip_whl_auto",
57        ]
58    }
59)
60
61# Here we create extra string flags that are just to work with the select
62# selecting the most specialized match. We don't allow the user to change
63# them.
64_flags = struct(
65    **{
66        f: str(Label("//python/config_settings:_internal_pip_" + f))
67        for f in INTERNAL_FLAGS
68    }
69)
70
71def config_settings(
72        *,
73        python_versions = [],
74        glibc_versions = [],
75        muslc_versions = [],
76        osx_versions = [],
77        target_platforms = [],
78        name = None,
79        visibility = None,
80        native = native):
81    """Generate all of the pip config settings.
82
83    Args:
84        name (str): Currently unused.
85        python_versions (list[str]): The list of python versions to configure
86            config settings for.
87        glibc_versions (list[str]): The list of glibc version of the wheels to
88            configure config settings for.
89        muslc_versions (list[str]): The list of musl version of the wheels to
90            configure config settings for.
91        osx_versions (list[str]): The list of OSX OS versions to configure
92            config settings for.
93        target_platforms (list[str]): The list of "{os}_{cpu}" for deriving
94            constraint values for each condition.
95        visibility (list[str], optional): The visibility to be passed to the
96            exposed labels. All other labels will be private.
97        native (struct): The struct containing alias and config_setting rules
98            to use for creating the objects. Can be overridden for unit tests
99            reasons.
100    """
101
102    glibc_versions = [""] + glibc_versions
103    muslc_versions = [""] + muslc_versions
104    osx_versions = [""] + osx_versions
105    target_platforms = [("", "")] + [
106        t.split("_", 1)
107        for t in target_platforms
108    ]
109
110    for python_version in [""] + python_versions:
111        is_python = "is_python_{}".format(python_version or "version_unset")
112        native.alias(
113            name = is_python,
114            actual = Label("//python/config_settings:" + is_python),
115            visibility = visibility,
116        )
117
118        for os, cpu in target_platforms:
119            constraint_values = []
120            suffix = ""
121            if os:
122                constraint_values.append("@platforms//os:" + os)
123                suffix += "_" + os
124            if cpu:
125                constraint_values.append("@platforms//cpu:" + cpu)
126                suffix += "_" + cpu
127
128            _dist_config_settings(
129                suffix = suffix,
130                plat_flag_values = _plat_flag_values(
131                    os = os,
132                    cpu = cpu,
133                    osx_versions = osx_versions,
134                    glibc_versions = glibc_versions,
135                    muslc_versions = muslc_versions,
136                ),
137                constraint_values = constraint_values,
138                python_version = python_version,
139                is_python = is_python,
140                visibility = visibility,
141                native = native,
142            )
143
144def _dist_config_settings(*, suffix, plat_flag_values, **kwargs):
145    flag_values = {_flags.dist: ""}
146
147    # First create an sdist, we will be building upon the flag values, which
148    # will ensure that each sdist config setting is the least specialized of
149    # all. However, we need at least one flag value to cover the case where we
150    # have `sdist` for any platform, hence we have a non-empty `flag_values`
151    # here.
152    _dist_config_setting(
153        name = "sdist{}".format(suffix),
154        flag_values = flag_values,
155        is_pip_whl = FLAGS.is_pip_whl_no,
156        **kwargs
157    )
158
159    for name, f in [
160        ("py_none", _flags.whl_py2_py3),
161        ("py3_none", _flags.whl_py3),
162        ("py3_abi3", _flags.whl_py3_abi3),
163        ("cp3x_none", _flags.whl_pycp3x),
164        ("cp3x_abi3", _flags.whl_pycp3x_abi3),
165        ("cp3x_cp", _flags.whl_pycp3x_abicp),
166    ]:
167        if f in flag_values:
168            # This should never happen as all of the different whls should have
169            # unique flag values.
170            fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
171        else:
172            flag_values[f] = ""
173
174        _dist_config_setting(
175            name = "{}_any{}".format(name, suffix),
176            flag_values = flag_values,
177            is_pip_whl = FLAGS.is_pip_whl_only,
178            **kwargs
179        )
180
181    generic_flag_values = flag_values
182
183    for (suffix, flag_values) in plat_flag_values:
184        flag_values = flag_values | generic_flag_values
185
186        for name, f in [
187            ("py_none", _flags.whl_plat),
188            ("py3_none", _flags.whl_plat_py3),
189            ("py3_abi3", _flags.whl_plat_py3_abi3),
190            ("cp3x_none", _flags.whl_plat_pycp3x),
191            ("cp3x_abi3", _flags.whl_plat_pycp3x_abi3),
192            ("cp3x_cp", _flags.whl_plat_pycp3x_abicp),
193        ]:
194            if f in flag_values:
195                # This should never happen as all of the different whls should have
196                # unique flag values.
197                fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
198            else:
199                flag_values[f] = ""
200
201            _dist_config_setting(
202                name = "{}_{}".format(name, suffix),
203                flag_values = flag_values,
204                is_pip_whl = FLAGS.is_pip_whl_only,
205                **kwargs
206            )
207
208def _to_version_string(version, sep = "."):
209    if not version:
210        return ""
211
212    return "{}{}{}".format(version[0], sep, version[1])
213
214def _plat_flag_values(os, cpu, osx_versions, glibc_versions, muslc_versions):
215    ret = []
216    if os == "":
217        return []
218    elif os == "windows":
219        ret.append(("{}_{}".format(os, cpu), {}))
220    elif os == "osx":
221        for cpu_, arch in {
222            cpu: UniversalWhlFlag.ARCH,
223            cpu + "_universal2": UniversalWhlFlag.UNIVERSAL,
224        }.items():
225            for osx_version in osx_versions:
226                flags = {
227                    FLAGS.pip_whl_osx_version: _to_version_string(osx_version),
228                }
229                if arch == UniversalWhlFlag.ARCH:
230                    flags[FLAGS.pip_whl_osx_arch] = arch
231
232                if not osx_version:
233                    suffix = "{}_{}".format(os, cpu_)
234                else:
235                    suffix = "{}_{}_{}".format(os, _to_version_string(osx_version, "_"), cpu_)
236
237                ret.append((suffix, flags))
238
239    elif os == "linux":
240        for os_prefix, linux_libc in {
241            os: WhlLibcFlag.GLIBC,
242            "many" + os: WhlLibcFlag.GLIBC,
243            "musl" + os: WhlLibcFlag.MUSL,
244        }.items():
245            if linux_libc == WhlLibcFlag.GLIBC:
246                libc_versions = glibc_versions
247                libc_flag = FLAGS.pip_whl_glibc_version
248            elif linux_libc == WhlLibcFlag.MUSL:
249                libc_versions = muslc_versions
250                libc_flag = FLAGS.pip_whl_muslc_version
251            else:
252                fail("Unsupported libc type: {}".format(linux_libc))
253
254            for libc_version in libc_versions:
255                if libc_version and os_prefix == os:
256                    continue
257                elif libc_version:
258                    suffix = "{}_{}_{}".format(os_prefix, _to_version_string(libc_version, "_"), cpu)
259                else:
260                    suffix = "{}_{}".format(os_prefix, cpu)
261
262                ret.append((
263                    suffix,
264                    {
265                        FLAGS.py_linux_libc: linux_libc,
266                        libc_flag: _to_version_string(libc_version),
267                    },
268                ))
269    else:
270        fail("Unsupported os: {}".format(os))
271
272    return ret
273
274def _dist_config_setting(*, name, is_pip_whl, is_python, python_version, native = native, **kwargs):
275    """A macro to create a target that matches is_pip_whl_auto and one more value.
276
277    Args:
278        name: The name of the public target.
279        is_pip_whl: The config setting to match in addition to
280            `is_pip_whl_auto` when evaluating the config setting.
281        is_python: The python version config_setting to match.
282        python_version: The python version name.
283        native (struct): The struct containing alias and config_setting rules
284            to use for creating the objects. Can be overridden for unit tests
285            reasons.
286        **kwargs: The kwargs passed to the config_setting rule. Visibility of
287            the main alias target is also taken from the kwargs.
288    """
289    _name = "_is_" + name
290
291    visibility = kwargs.get("visibility")
292    native.alias(
293        name = "is_cp{}_{}".format(python_version, name) if python_version else "is_{}".format(name),
294        actual = select({
295            # First match by the python version
296            is_python: _name,
297            "//conditions:default": is_python,
298        }),
299        visibility = visibility,
300    )
301
302    if python_version:
303        # Reuse the config_setting targets that we use with the default
304        # `python_version` setting.
305        return
306
307    config_setting_name = _name + "_setting"
308    native.config_setting(name = config_setting_name, **kwargs)
309
310    # Next match by the `pip_whl` flag value and then match by the flags that
311    # are intrinsic to the distribution.
312    native.alias(
313        name = _name,
314        actual = select({
315            "//conditions:default": FLAGS.is_pip_whl_auto,
316            FLAGS.is_pip_whl_auto: config_setting_name,
317            is_pip_whl: config_setting_name,
318        }),
319        visibility = visibility,
320    )
321