1# Copyright 2022 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"""Create a repository to hold the toolchains. 16 17This follows guidance here: 18https://docs.bazel.build/versions/main/skylark/deploying.html#registering-toolchains 19 20The "complex computation" in our case is simply downloading large artifacts. 21This guidance tells us how to avoid that: we put the toolchain targets in the 22alias repository with only the toolchain attribute pointing into the 23platform-specific repositories. 24""" 25 26load( 27 "//python:versions.bzl", 28 "LINUX_NAME", 29 "MACOS_NAME", 30 "PLATFORMS", 31 "WINDOWS_NAME", 32) 33load(":which.bzl", "which_with_fail") 34 35def get_repository_name(repository_workspace): 36 dummy_label = "//:_" 37 return str(repository_workspace.relative(dummy_label))[:-len(dummy_label)] or "@" 38 39def python_toolchain_build_file_content( 40 prefix, 41 python_version, 42 set_python_version_constraint, 43 user_repository_name, 44 rules_python): 45 """Creates the content for toolchain definitions for a build file. 46 47 Args: 48 prefix: Python toolchain name prefixes 49 python_version: Python versions for the toolchains 50 set_python_version_constraint: string, "True" if the toolchain should 51 have the Python version constraint added as a requirement for 52 matching the toolchain, "False" if not. 53 user_repository_name: names for the user repos 54 rules_python: rules_python label 55 56 Returns: 57 build_content: Text containing toolchain definitions 58 """ 59 if set_python_version_constraint == "True": 60 constraint = "{rules_python}//python/config_settings:is_python_{python_version}".format( 61 rules_python = rules_python, 62 python_version = python_version, 63 ) 64 target_settings = '["{}"]'.format(constraint) 65 elif set_python_version_constraint == "False": 66 target_settings = "[]" 67 else: 68 fail(("Invalid set_python_version_constraint value: got {} {}, wanted " + 69 "either the string 'True' or the string 'False'; " + 70 "(did you convert bool to string?)").format( 71 type(set_python_version_constraint), 72 repr(set_python_version_constraint), 73 )) 74 75 # We create a list of toolchain content from iterating over 76 # the enumeration of PLATFORMS. We enumerate PLATFORMS in 77 # order to get us an index to increment the increment. 78 return "".join([ 79 """ 80toolchain( 81 name = "{prefix}{platform}_toolchain", 82 target_compatible_with = {compatible_with}, 83 target_settings = {target_settings}, 84 toolchain = "@{user_repository_name}_{platform}//:python_runtimes", 85 toolchain_type = "@bazel_tools//tools/python:toolchain_type", 86) 87 88toolchain( 89 name = "{prefix}{platform}_py_cc_toolchain", 90 target_compatible_with = {compatible_with}, 91 target_settings = {target_settings}, 92 toolchain = "@{user_repository_name}_{platform}//:py_cc_toolchain", 93 toolchain_type = "@rules_python//python/cc:toolchain_type", 94 95) 96""".format( 97 compatible_with = meta.compatible_with, 98 platform = platform, 99 # We have to use a String value here because bzlmod is passing in a 100 # string as we cannot have list of bools in build rule attribues. 101 # This if statement does not appear to work unless it is in the 102 # toolchain file. 103 target_settings = target_settings, 104 user_repository_name = user_repository_name, 105 prefix = prefix, 106 ) 107 for platform, meta in PLATFORMS.items() 108 ]) 109 110def _toolchains_repo_impl(rctx): 111 build_content = """\ 112# Generated by python/private/toolchains_repo.bzl 113# 114# These can be registered in the workspace file or passed to --extra_toolchains 115# flag. By default all these toolchains are registered by the 116# python_register_toolchains macro so you don't normally need to interact with 117# these targets. 118 119""" 120 121 # Get the repository name 122 rules_python = get_repository_name(rctx.attr._rules_python_workspace) 123 124 toolchains = python_toolchain_build_file_content( 125 prefix = "", 126 python_version = rctx.attr.python_version, 127 set_python_version_constraint = str(rctx.attr.set_python_version_constraint), 128 user_repository_name = rctx.attr.user_repository_name, 129 rules_python = rules_python, 130 ) 131 132 rctx.file("BUILD.bazel", build_content + toolchains) 133 134toolchains_repo = repository_rule( 135 _toolchains_repo_impl, 136 doc = "Creates a repository with toolchain definitions for all known platforms " + 137 "which can be registered or selected.", 138 attrs = { 139 "python_version": attr.string(doc = "The Python version."), 140 "set_python_version_constraint": attr.bool(doc = "if target_compatible_with for the toolchain should set the version constraint"), 141 "user_repository_name": attr.string(doc = "what the user chose for the base name"), 142 "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), 143 }, 144) 145 146def _toolchain_aliases_impl(rctx): 147 (os_name, arch) = get_host_os_arch(rctx) 148 149 host_platform = get_host_platform(os_name, arch) 150 151 is_windows = (os_name == WINDOWS_NAME) 152 python3_binary_path = "python.exe" if is_windows else "bin/python3" 153 154 # Base BUILD file for this repository. 155 build_contents = """\ 156# Generated by python/private/toolchains_repo.bzl 157package(default_visibility = ["//visibility:public"]) 158load("@rules_python//python:versions.bzl", "PLATFORMS", "gen_python_config_settings") 159gen_python_config_settings() 160exports_files(["defs.bzl"]) 161alias(name = "files", actual = select({{":" + item: "@{py_repository}_" + item + "//:files" for item in PLATFORMS.keys()}})) 162alias(name = "includes", actual = select({{":" + item: "@{py_repository}_" + item + "//:includes" for item in PLATFORMS.keys()}})) 163alias(name = "libpython", actual = select({{":" + item: "@{py_repository}_" + item + "//:libpython" for item in PLATFORMS.keys()}})) 164alias(name = "py3_runtime", actual = select({{":" + item: "@{py_repository}_" + item + "//:py3_runtime" for item in PLATFORMS.keys()}})) 165alias(name = "python_headers", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_headers" for item in PLATFORMS.keys()}})) 166alias(name = "python_runtimes", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS.keys()}})) 167alias(name = "python3", actual = select({{":" + item: "@{py_repository}_" + item + "//:" + ("python.exe" if "windows" in item else "bin/python3") for item in PLATFORMS.keys()}})) 168""".format( 169 py_repository = rctx.attr.user_repository_name, 170 ) 171 if not is_windows: 172 build_contents += """\ 173alias(name = "pip", actual = select({{":" + item: "@{py_repository}_" + item + "//:python_runtimes" for item in PLATFORMS.keys() if "windows" not in item}})) 174""".format( 175 py_repository = rctx.attr.user_repository_name, 176 host_platform = host_platform, 177 ) 178 rctx.file("BUILD.bazel", build_contents) 179 180 # Expose a Starlark file so rules can know what host platform we used and where to find an interpreter 181 # when using repository_ctx.path, which doesn't understand aliases. 182 rctx.file("defs.bzl", content = """\ 183# Generated by python/private/toolchains_repo.bzl 184 185load("{rules_python}//python/config_settings:transition.bzl", _py_binary = "py_binary", _py_test = "py_test") 186load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") 187 188host_platform = "{host_platform}" 189interpreter = "@{py_repository}_{host_platform}//:{python3_binary_path}" 190 191def py_binary(name, **kwargs): 192 return _py_binary( 193 name = name, 194 python_version = "{python_version}", 195 **kwargs 196 ) 197 198def py_test(name, **kwargs): 199 return _py_test( 200 name = name, 201 python_version = "{python_version}", 202 **kwargs 203 ) 204 205def compile_pip_requirements(name, **kwargs): 206 return _compile_pip_requirements( 207 name = name, 208 py_binary = py_binary, 209 py_test = py_test, 210 **kwargs 211 ) 212 213""".format( 214 host_platform = host_platform, 215 py_repository = rctx.attr.user_repository_name, 216 python_version = rctx.attr.python_version, 217 python3_binary_path = python3_binary_path, 218 rules_python = get_repository_name(rctx.attr._rules_python_workspace), 219 )) 220 221toolchain_aliases = repository_rule( 222 _toolchain_aliases_impl, 223 doc = """Creates a repository with a shorter name meant for the host platform, which contains 224 a BUILD.bazel file declaring aliases to the host platform's targets. 225 """, 226 attrs = { 227 "python_version": attr.string(doc = "The Python version."), 228 "user_repository_name": attr.string( 229 mandatory = True, 230 doc = "The base name for all created repositories, like 'python38'.", 231 ), 232 "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), 233 }, 234) 235 236def _multi_toolchain_aliases_impl(rctx): 237 rules_python = rctx.attr._rules_python_workspace.workspace_name 238 239 for python_version, repository_name in rctx.attr.python_versions.items(): 240 file = "{}/defs.bzl".format(python_version) 241 rctx.file(file, content = """\ 242# Generated by python/private/toolchains_repo.bzl 243 244load( 245 "@{repository_name}//:defs.bzl", 246 _compile_pip_requirements = "compile_pip_requirements", 247 _host_platform = "host_platform", 248 _interpreter = "interpreter", 249 _py_binary = "py_binary", 250 _py_test = "py_test", 251) 252 253compile_pip_requirements = _compile_pip_requirements 254host_platform = _host_platform 255interpreter = _interpreter 256py_binary = _py_binary 257py_test = _py_test 258""".format( 259 repository_name = repository_name, 260 )) 261 rctx.file("{}/BUILD.bazel".format(python_version), "") 262 263 pip_bzl = """\ 264# Generated by python/private/toolchains_repo.bzl 265 266load("@{rules_python}//python:pip.bzl", "pip_parse", _multi_pip_parse = "multi_pip_parse") 267 268def multi_pip_parse(name, requirements_lock, **kwargs): 269 return _multi_pip_parse( 270 name = name, 271 python_versions = {python_versions}, 272 requirements_lock = requirements_lock, 273 **kwargs 274 ) 275 276""".format( 277 python_versions = rctx.attr.python_versions.keys(), 278 rules_python = rules_python, 279 ) 280 rctx.file("pip.bzl", content = pip_bzl) 281 rctx.file("BUILD.bazel", "") 282 283multi_toolchain_aliases = repository_rule( 284 _multi_toolchain_aliases_impl, 285 attrs = { 286 "python_versions": attr.string_dict(doc = "The Python versions."), 287 "_rules_python_workspace": attr.label(default = Label("//:WORKSPACE")), 288 }, 289) 290 291def sanitize_platform_name(platform): 292 return platform.replace("-", "_") 293 294def get_host_platform(os_name, arch): 295 """Gets the host platform. 296 297 Args: 298 os_name: the host OS name. 299 arch: the host arch. 300 Returns: 301 The host platform. 302 """ 303 host_platform = None 304 for platform, meta in PLATFORMS.items(): 305 if meta.os_name == os_name and meta.arch == arch: 306 host_platform = platform 307 if not host_platform: 308 fail("No platform declared for host OS {} on arch {}".format(os_name, arch)) 309 return host_platform 310 311def get_host_os_arch(rctx): 312 """Infer the host OS name and arch from a repository context. 313 314 Args: 315 rctx: Bazel's repository_ctx. 316 Returns: 317 A tuple with the host OS name and arch. 318 """ 319 os_name = rctx.os.name 320 321 # We assume the arch for Windows is always x86_64. 322 if "windows" in os_name.lower(): 323 arch = "x86_64" 324 325 # Normalize the os_name. E.g. os_name could be "OS windows server 2019". 326 os_name = WINDOWS_NAME 327 else: 328 # This is not ideal, but bazel doesn't directly expose arch. 329 arch = rctx.execute([which_with_fail("uname", rctx), "-m"]).stdout.strip() 330 331 # Normalize the os_name. 332 if "mac" in os_name.lower(): 333 os_name = MACOS_NAME 334 elif "linux" in os_name.lower(): 335 os_name = LINUX_NAME 336 337 return (os_name, arch) 338