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