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