1# Copyright (c) 2009-2021, Google LLC 2# All rights reserved. 3# 4# Use of this source code is governed by a BSD-style 5# license that can be found in the LICENSE file or at 6# https://developers.google.com/open-source/licenses/bsd 7 8"""Repository rule for using Python 3.x headers from the system.""" 9 10# Mock out rules_python's pip.bzl for cases where no system python is found. 11_mock_pip = """ 12def _pip_install_impl(repository_ctx): 13 repository_ctx.file("BUILD.bazel", ''' 14py_library( 15 name = "noop", 16 visibility = ["//visibility:public"], 17) 18''') 19 repository_ctx.file("requirements.bzl", ''' 20def install_deps(*args, **kwargs): 21 print("WARNING: could not install pip dependencies") 22 23def requirement(*args, **kwargs): 24 return "@{}//:noop" 25'''.format(repository_ctx.attr.name)) 26pip_install = repository_rule( 27 implementation = _pip_install_impl, 28 attrs = { 29 "requirements": attr.string(), 30 "requirements_overrides": attr.string_dict(), 31 "python_interpreter_target": attr.string(), 32 }, 33) 34pip_parse = pip_install 35""" 36 37# Alias rules_python's pip.bzl for cases where a system python is found. 38_alias_pip = """ 39load("@rules_python//python:pip.bzl", _pip_parse = "pip_parse") 40 41def _get_requirements(requirements, requirements_overrides): 42 for version, override in requirements_overrides.items(): 43 if version in "{python_version}": 44 requirements = override 45 break 46 return requirements 47 48def pip_parse(requirements, requirements_overrides={{}}, **kwargs): 49 _pip_parse( 50 python_interpreter_target = "@{repo}//:interpreter", 51 requirements_lock = _get_requirements(requirements, requirements_overrides), 52 **kwargs, 53 ) 54 55pip_install = pip_parse 56""" 57 58_mock_fuzzing_py = """ 59def fuzzing_py_install_deps(): 60 print("WARNING: could not install fuzzing_py dependencies") 61""" 62 63# Alias rules_fuzzing's requirements.bzl for cases where a system python is found. 64_alias_fuzzing_py = """ 65load("@fuzzing_py_deps//:requirements.bzl", _fuzzing_py_install_deps = "install_deps") 66 67def fuzzing_py_install_deps(): 68 _fuzzing_py_install_deps() 69""" 70 71_build_file = """ 72load("@bazel_skylib//lib:selects.bzl", "selects") 73load("@bazel_skylib//rules:common_settings.bzl", "string_flag") 74load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair") 75 76cc_library( 77 name = "python_headers", 78 hdrs = glob(["python/**/*.h"], allow_empty = True), 79 includes = ["python"], 80 visibility = ["//visibility:public"], 81) 82 83string_flag( 84 name = "internal_python_support", 85 build_setting_default = "{support}", 86 values = [ 87 "None", 88 "Supported", 89 "Unsupported", 90 ] 91) 92 93config_setting( 94 name = "none", 95 flag_values = {{ 96 ":internal_python_support": "None", 97 }}, 98 visibility = ["//visibility:public"], 99) 100 101config_setting( 102 name = "supported", 103 flag_values = {{ 104 ":internal_python_support": "Supported", 105 }}, 106 visibility = ["//visibility:public"], 107) 108 109config_setting( 110 name = "unsupported", 111 flag_values = {{ 112 ":internal_python_support": "Unsupported", 113 }}, 114 visibility = ["//visibility:public"], 115) 116 117selects.config_setting_group( 118 name = "exists", 119 match_any = [":supported", ":unsupported"], 120 visibility = ["//visibility:public"], 121) 122 123sh_binary( 124 name = "interpreter", 125 srcs = ["interpreter"], 126 visibility = ["//visibility:public"], 127) 128 129py_runtime( 130 name = "py3_runtime", 131 interpreter_path = "{interpreter}", 132 python_version = "PY3", 133) 134 135py_runtime_pair( 136 name = "runtime_pair", 137 py3_runtime = ":py3_runtime", 138) 139 140toolchain( 141 name = "python_toolchain", 142 toolchain = ":runtime_pair", 143 toolchain_type = "@rules_python//python:toolchain_type", 144) 145""" 146 147_register = """ 148def register_system_python(): 149 native.register_toolchains("@{}//:python_toolchain") 150""" 151 152_mock_register = """ 153def register_system_python(): 154 pass 155""" 156 157def _get_python_version(repository_ctx): 158 py_program = "import sys; print(str(sys.version_info.major) + '.' + str(sys.version_info.minor) + '.' + str(sys.version_info.micro))" 159 result = repository_ctx.execute(["python3", "-c", py_program]) 160 return (result.stdout).strip().split(".") 161 162def _get_python_path(repository_ctx): 163 py_program = "import sysconfig; print(sysconfig.get_config_var('%s'), end='')" 164 result = repository_ctx.execute(["python3", "-c", py_program % ("INCLUDEPY")]) 165 if result.return_code != 0: 166 return None 167 return result.stdout 168 169def _populate_package(ctx, path, python3, python_version): 170 ctx.symlink(path, "python") 171 supported = True 172 for idx, v in enumerate(ctx.attr.minimum_python_version.split(".")): 173 if int(python_version[idx]) < int(v): 174 supported = False 175 break 176 if "win" in ctx.os.name: 177 # buildifier: disable=print 178 print("WARNING: python is not supported on Windows") 179 supported = False 180 181 build_file = _build_file.format( 182 interpreter = python3, 183 support = "Supported" if supported else "Unsupported", 184 ) 185 186 ctx.file("interpreter", "#!/bin/sh\nexec {} \"$@\"".format(python3)) 187 ctx.file("BUILD.bazel", build_file) 188 ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = '{}{}'".format(python_version[0], python_version[1])) 189 ctx.file("register.bzl", _register.format(ctx.attr.name)) 190 if supported: 191 ctx.file("pip.bzl", _alias_pip.format( 192 python_version = ".".join(python_version), 193 repo = ctx.attr.name, 194 )) 195 ctx.file("fuzzing_py.bzl", _alias_fuzzing_py) 196 else: 197 # Dependencies are unlikely to be satisfiable for unsupported versions of python. 198 ctx.file("pip.bzl", _mock_pip) 199 ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) 200 201def _populate_empty_package(ctx): 202 # Mock out all the entrypoints we need to run from WORKSPACE. Targets that 203 # actually need python should use `target_compatible_with` and the generated 204 # @system_python//:exists or @system_python//:supported constraints. 205 ctx.file( 206 "BUILD.bazel", 207 _build_file.format( 208 interpreter = "", 209 support = "None", 210 ), 211 ) 212 ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = 'None'") 213 ctx.file("register.bzl", _mock_register) 214 ctx.file("pip.bzl", _mock_pip) 215 ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) 216 217def _system_python_impl(repository_ctx): 218 path = _get_python_path(repository_ctx) 219 python3 = repository_ctx.which("python3") 220 python_version = _get_python_version(repository_ctx) 221 222 if path and python_version[0] == "3": 223 _populate_package(repository_ctx, path, python3, python_version) 224 else: 225 # buildifier: disable=print 226 print("WARNING: no system python available, builds against system python will fail") 227 _populate_empty_package(repository_ctx) 228 229# The system_python() repository rule exposes information from the version of python installed in the current system. 230# 231# In WORKSPACE: 232# system_python( 233# name = "system_python_repo", 234# minimum_python_version = "3.7", 235# ) 236# 237# This repository exposes some repository rules for configuring python in Bazel. The python toolchain 238# *must* be registered in your WORKSPACE: 239# load("@system_python_repo//:register.bzl", "register_system_python") 240# register_system_python() 241# 242# Pip dependencies can optionally be specified using a wrapper around rules_python's repository rules: 243# load("@system_python//:pip.bzl", "pip_install") 244# pip_install( 245# name="pip_deps", 246# requirements = "@com_google_protobuf//python:requirements.txt", 247# ) 248# An optional argument `requirements_overrides` takes a dictionary mapping python versions to alternate 249# requirements files. This works around the requirement for fully pinned dependencies in python_rules. 250# 251# Four config settings are exposed from this repository to help declare target compatibility in Bazel. 252# For example, `@system_python_repo//:exists` will be true if a system python version has been found. 253# The `none` setting will be true only if no python version was found, and `supported`/`unsupported` 254# correspond to whether or not the system version is compatible with `minimum_python_version`. 255# 256# This repository also exposes a header rule that you can depend on from BUILD files: 257# cc_library( 258# name = "foobar", 259# srcs = ["foobar.cc"], 260# deps = ["@system_python_repo//:python_headers"], 261# ) 262# 263# The headers should correspond to the version of python obtained by running 264# the `python3` command on the system. 265system_python = repository_rule( 266 implementation = _system_python_impl, 267 local = True, 268 attrs = { 269 "minimum_python_version": attr.string(default = "3.8"), 270 }, 271) 272