• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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("//python/private:flags.bzl", "LibcFlag")
43load(":flags.bzl", "INTERNAL_FLAGS", "UniversalWhlFlag")
44
45FLAGS = struct(
46    **{
47        f: str(Label("//python/config_settings:" + f))
48        for f in [
49            "python_version",
50            "pip_whl_glibc_version",
51            "pip_whl_muslc_version",
52            "pip_whl_osx_arch",
53            "pip_whl_osx_version",
54            "py_linux_libc",
55            "is_pip_whl_no",
56            "is_pip_whl_only",
57            "is_pip_whl_auto",
58        ]
59    }
60)
61
62# Here we create extra string flags that are just to work with the select
63# selecting the most specialized match. We don't allow the user to change
64# them.
65_flags = struct(
66    **{
67        f: str(Label("//python/config_settings:_internal_pip_" + f))
68        for f in INTERNAL_FLAGS
69    }
70)
71
72def config_settings(
73        *,
74        python_versions = [],
75        glibc_versions = [],
76        muslc_versions = [],
77        osx_versions = [],
78        target_platforms = [],
79        name = None,
80        visibility = None,
81        native = native):
82    """Generate all of the pip config settings.
83
84    Args:
85        name (str): Currently unused.
86        python_versions (list[str]): The list of python versions to configure
87            config settings for.
88        glibc_versions (list[str]): The list of glibc version of the wheels to
89            configure config settings for.
90        muslc_versions (list[str]): The list of musl version of the wheels to
91            configure config settings for.
92        osx_versions (list[str]): The list of OSX OS versions to configure
93            config settings for.
94        target_platforms (list[str]): The list of "{os}_{cpu}" for deriving
95            constraint values for each condition.
96        visibility (list[str], optional): The visibility to be passed to the
97            exposed labels. All other labels will be private.
98        native (struct): The struct containing alias and config_setting rules
99            to use for creating the objects. Can be overridden for unit tests
100            reasons.
101    """
102
103    glibc_versions = [""] + glibc_versions
104    muslc_versions = [""] + muslc_versions
105    osx_versions = [""] + osx_versions
106    target_platforms = [("", "")] + [
107        t.split("_", 1)
108        for t in target_platforms
109    ]
110
111    for python_version in [""] + python_versions:
112        is_python = "is_python_{}".format(python_version or "version_unset")
113
114        # The aliases defined in @rules_python//python/config_settings may not
115        # have config settings for the versions we need, so define our own
116        # config settings instead.
117        native.config_setting(
118            name = is_python,
119            flag_values = {
120                Label("//python/config_settings:python_version_major_minor"): python_version,
121            },
122            visibility = visibility,
123        )
124
125        for os, cpu in target_platforms:
126            constraint_values = []
127            suffix = ""
128            if os:
129                constraint_values.append("@platforms//os:" + os)
130                suffix += "_" + os
131            if cpu:
132                constraint_values.append("@platforms//cpu:" + cpu)
133                suffix += "_" + cpu
134
135            _dist_config_settings(
136                suffix = suffix,
137                plat_flag_values = _plat_flag_values(
138                    os = os,
139                    cpu = cpu,
140                    osx_versions = osx_versions,
141                    glibc_versions = glibc_versions,
142                    muslc_versions = muslc_versions,
143                ),
144                constraint_values = constraint_values,
145                python_version = python_version,
146                is_python = is_python,
147                visibility = visibility,
148                native = native,
149            )
150
151def _dist_config_settings(*, suffix, plat_flag_values, **kwargs):
152    if kwargs.get("constraint_values"):
153        # Add python version + platform config settings
154        _dist_config_setting(
155            name = suffix.strip("_"),
156            **kwargs
157        )
158
159    flag_values = {_flags.dist: ""}
160
161    # First create an sdist, we will be building upon the flag values, which
162    # will ensure that each sdist config setting is the least specialized of
163    # all. However, we need at least one flag value to cover the case where we
164    # have `sdist` for any platform, hence we have a non-empty `flag_values`
165    # here.
166    _dist_config_setting(
167        name = "sdist{}".format(suffix),
168        flag_values = flag_values,
169        is_pip_whl = FLAGS.is_pip_whl_no,
170        **kwargs
171    )
172
173    for name, f in [
174        ("py_none", _flags.whl_py2_py3),
175        ("py3_none", _flags.whl_py3),
176        ("py3_abi3", _flags.whl_py3_abi3),
177        ("cp3x_none", _flags.whl_pycp3x),
178        ("cp3x_abi3", _flags.whl_pycp3x_abi3),
179        ("cp3x_cp", _flags.whl_pycp3x_abicp),
180    ]:
181        if f in flag_values:
182            # This should never happen as all of the different whls should have
183            # unique flag values.
184            fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
185        else:
186            flag_values[f] = ""
187
188        _dist_config_setting(
189            name = "{}_any{}".format(name, suffix),
190            flag_values = flag_values,
191            is_pip_whl = FLAGS.is_pip_whl_only,
192            **kwargs
193        )
194
195    generic_flag_values = flag_values
196
197    for (suffix, flag_values) in plat_flag_values:
198        flag_values = flag_values | generic_flag_values
199
200        for name, f in [
201            ("py_none", _flags.whl_plat),
202            ("py3_none", _flags.whl_plat_py3),
203            ("py3_abi3", _flags.whl_plat_py3_abi3),
204            ("cp3x_none", _flags.whl_plat_pycp3x),
205            ("cp3x_abi3", _flags.whl_plat_pycp3x_abi3),
206            ("cp3x_cp", _flags.whl_plat_pycp3x_abicp),
207        ]:
208            if f in flag_values:
209                # This should never happen as all of the different whls should have
210                # unique flag values.
211                fail("BUG: the flag {} is attempted to be added twice to the list".format(f))
212            else:
213                flag_values[f] = ""
214
215            _dist_config_setting(
216                name = "{}_{}".format(name, suffix),
217                flag_values = flag_values,
218                is_pip_whl = FLAGS.is_pip_whl_only,
219                **kwargs
220            )
221
222def _to_version_string(version, sep = "."):
223    if not version:
224        return ""
225
226    return "{}{}{}".format(version[0], sep, version[1])
227
228def _plat_flag_values(os, cpu, osx_versions, glibc_versions, muslc_versions):
229    ret = []
230    if os == "":
231        return []
232    elif os == "windows":
233        ret.append(("{}_{}".format(os, cpu), {}))
234    elif os == "osx":
235        for cpu_, arch in {
236            cpu: UniversalWhlFlag.ARCH,
237            cpu + "_universal2": UniversalWhlFlag.UNIVERSAL,
238        }.items():
239            for osx_version in osx_versions:
240                flags = {
241                    FLAGS.pip_whl_osx_version: _to_version_string(osx_version),
242                }
243                if arch == UniversalWhlFlag.ARCH:
244                    flags[FLAGS.pip_whl_osx_arch] = arch
245
246                if not osx_version:
247                    suffix = "{}_{}".format(os, cpu_)
248                else:
249                    suffix = "{}_{}_{}".format(os, _to_version_string(osx_version, "_"), cpu_)
250
251                ret.append((suffix, flags))
252
253    elif os == "linux":
254        for os_prefix, linux_libc in {
255            os: LibcFlag.GLIBC,
256            "many" + os: LibcFlag.GLIBC,
257            "musl" + os: LibcFlag.MUSL,
258        }.items():
259            if linux_libc == LibcFlag.GLIBC:
260                libc_versions = glibc_versions
261                libc_flag = FLAGS.pip_whl_glibc_version
262            elif linux_libc == LibcFlag.MUSL:
263                libc_versions = muslc_versions
264                libc_flag = FLAGS.pip_whl_muslc_version
265            else:
266                fail("Unsupported libc type: {}".format(linux_libc))
267
268            for libc_version in libc_versions:
269                if libc_version and os_prefix == os:
270                    continue
271                elif libc_version:
272                    suffix = "{}_{}_{}".format(os_prefix, _to_version_string(libc_version, "_"), cpu)
273                else:
274                    suffix = "{}_{}".format(os_prefix, cpu)
275
276                ret.append((
277                    suffix,
278                    {
279                        FLAGS.py_linux_libc: linux_libc,
280                        libc_flag: _to_version_string(libc_version),
281                    },
282                ))
283    else:
284        fail("Unsupported os: {}".format(os))
285
286    return ret
287
288def _dist_config_setting(*, name, is_python, python_version, is_pip_whl = None, native = native, **kwargs):
289    """A macro to create a target that matches is_pip_whl_auto and one more value.
290
291    Args:
292        name: The name of the public target.
293        is_pip_whl: The config setting to match in addition to
294            `is_pip_whl_auto` when evaluating the config setting.
295        is_python: The python version config_setting to match.
296        python_version: The python version name.
297        native (struct): The struct containing alias and config_setting rules
298            to use for creating the objects. Can be overridden for unit tests
299            reasons.
300        **kwargs: The kwargs passed to the config_setting rule. Visibility of
301            the main alias target is also taken from the kwargs.
302    """
303    _name = "_is_" + name
304
305    visibility = kwargs.get("visibility")
306    native.alias(
307        name = "is_cp{}_{}".format(python_version, name) if python_version else "is_{}".format(name),
308        actual = select({
309            # First match by the python version
310            is_python: _name,
311            "//conditions:default": is_python,
312        }),
313        visibility = visibility,
314    )
315
316    if python_version:
317        # Reuse the config_setting targets that we use with the default
318        # `python_version` setting.
319        return
320
321    if not is_pip_whl:
322        native.config_setting(name = _name, **kwargs)
323        return
324
325    config_setting_name = _name + "_setting"
326    native.config_setting(name = config_setting_name, **kwargs)
327
328    # Next match by the `pip_whl` flag value and then match by the flags that
329    # are intrinsic to the distribution.
330    native.alias(
331        name = _name,
332        actual = select({
333            "//conditions:default": FLAGS.is_pip_whl_auto,
334            FLAGS.is_pip_whl_auto: config_setting_name,
335            is_pip_whl: config_setting_name,
336        }),
337        visibility = visibility,
338    )
339