1# Copyright (c) 2009-2021, Google LLC 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are met: 6# * Redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer. 8# * Redistributions in binary form must reproduce the above copyright 9# notice, this list of conditions and the following disclaimer in the 10# documentation and/or other materials provided with the distribution. 11# * Neither the name of Google LLC nor the 12# names of its contributors may be used to endorse or promote products 13# derived from this software without specific prior written permission. 14# 15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18# DISCLAIMED. IN NO EVENT SHALL Google LLC BE LIABLE FOR ANY 19# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 26"""Repository rule for using Python 3.x headers from the system.""" 27 28# Mock out rules_python's pip.bzl for cases where no system python is found. 29_mock_pip = """ 30def _pip_install_impl(repository_ctx): 31 repository_ctx.file("BUILD.bazel", ''' 32py_library( 33 name = "noop", 34 visibility = ["//visibility:public"], 35) 36''') 37 repository_ctx.file("requirements.bzl", ''' 38def install_deps(*args, **kwargs): 39 print("WARNING: could not install pip dependencies") 40 41def requirement(*args, **kwargs): 42 return "@{}//:noop" 43'''.format(repository_ctx.attr.name)) 44pip_install = repository_rule( 45 implementation = _pip_install_impl, 46 attrs = { 47 "requirements": attr.string(), 48 "requirements_overrides": attr.string_dict(), 49 "python_interpreter_target": attr.string(), 50 }, 51) 52pip_parse = pip_install 53""" 54 55# Alias rules_python's pip.bzl for cases where a system python is found. 56_alias_pip = """ 57load("@rules_python//python:pip.bzl", _pip_install = "pip_install", _pip_parse = "pip_parse") 58 59def _get_requirements(requirements, requirements_overrides): 60 for version, override in requirements_overrides.items(): 61 if version in "{python_version}": 62 requirements = override 63 break 64 return requirements 65 66def pip_install(requirements, requirements_overrides={{}}, **kwargs): 67 _pip_install( 68 python_interpreter_target = "@{repo}//:interpreter", 69 requirements = _get_requirements(requirements, requirements_overrides), 70 **kwargs, 71 ) 72def pip_parse(requirements, requirements_overrides={{}}, **kwargs): 73 _pip_parse( 74 python_interpreter_target = "@{repo}//:interpreter", 75 requirements = _get_requirements(requirements, requirements_overrides), 76 **kwargs, 77 ) 78""" 79 80_mock_fuzzing_py = """ 81def fuzzing_py_install_deps(): 82 print("WARNING: could not install fuzzing_py dependencies") 83""" 84 85# Alias rules_fuzzing's requirements.bzl for cases where a system python is found. 86_alias_fuzzing_py = """ 87load("@fuzzing_py_deps//:requirements.bzl", _fuzzing_py_install_deps = "install_deps") 88 89def fuzzing_py_install_deps(): 90 _fuzzing_py_install_deps() 91""" 92 93_build_file = """ 94load("@bazel_skylib//lib:selects.bzl", "selects") 95load("@bazel_skylib//rules:common_settings.bzl", "string_flag") 96load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair") 97 98cc_library( 99 name = "python_headers", 100 hdrs = glob(["python/**/*.h"]), 101 includes = ["python"], 102 visibility = ["//visibility:public"], 103) 104 105string_flag( 106 name = "internal_python_support", 107 build_setting_default = "{support}", 108 values = [ 109 "None", 110 "Supported", 111 "Unsupported", 112 ] 113) 114 115config_setting( 116 name = "none", 117 flag_values = {{ 118 ":internal_python_support": "None", 119 }}, 120 visibility = ["//visibility:public"], 121) 122 123config_setting( 124 name = "supported", 125 flag_values = {{ 126 ":internal_python_support": "Supported", 127 }}, 128 visibility = ["//visibility:public"], 129) 130 131config_setting( 132 name = "unsupported", 133 flag_values = {{ 134 ":internal_python_support": "Unsupported", 135 }}, 136 visibility = ["//visibility:public"], 137) 138 139selects.config_setting_group( 140 name = "exists", 141 match_any = [":supported", ":unsupported"], 142 visibility = ["//visibility:public"], 143) 144 145sh_binary( 146 name = "interpreter", 147 srcs = ["interpreter"], 148 visibility = ["//visibility:public"], 149) 150 151py_runtime( 152 name = "py3_runtime", 153 interpreter_path = "{interpreter}", 154 python_version = "PY3", 155) 156 157py_runtime_pair( 158 name = "runtime_pair", 159 py3_runtime = ":py3_runtime", 160) 161 162toolchain( 163 name = "python_toolchain", 164 toolchain = ":runtime_pair", 165 toolchain_type = "@rules_python//python:toolchain_type", 166) 167""" 168 169_register = """ 170def register_system_python(): 171 native.register_toolchains("@{}//:python_toolchain") 172""" 173 174_mock_register = """ 175def register_system_python(): 176 pass 177""" 178 179def _get_python_version(repository_ctx): 180 py_program = "import sys; print(str(sys.version_info.major) + '.' + str(sys.version_info.minor) + '.' + str(sys.version_info.micro))" 181 result = repository_ctx.execute(["python3", "-c", py_program]) 182 return (result.stdout).strip().split(".") 183 184def _get_python_path(repository_ctx): 185 py_program = "import sysconfig; print(sysconfig.get_config_var('%s'), end='')" 186 result = repository_ctx.execute(["python3", "-c", py_program % ("INCLUDEPY")]) 187 if result.return_code != 0: 188 return None 189 return result.stdout 190 191def _populate_package(ctx, path, python3, python_version): 192 ctx.symlink(path, "python") 193 supported = True 194 for idx, v in enumerate(ctx.attr.minimum_python_version.split(".")): 195 if int(python_version[idx]) < int(v): 196 supported = False 197 break 198 if "win" in ctx.os.name: 199 # buildifier: disable=print 200 print("WARNING: python is not supported on Windows") 201 supported = False 202 203 build_file = _build_file.format( 204 interpreter = python3, 205 support = "Supported" if supported else "Unsupported", 206 ) 207 208 ctx.file("interpreter", "exec {} \"$@\"".format(python3)) 209 ctx.file("BUILD.bazel", build_file) 210 ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = '{}{}'".format(python_version[0], python_version[1])) 211 ctx.file("register.bzl", _register.format(ctx.attr.name)) 212 if supported: 213 ctx.file("pip.bzl", _alias_pip.format( 214 python_version = ".".join(python_version), 215 repo = ctx.attr.name, 216 )) 217 ctx.file("fuzzing_py.bzl", _alias_fuzzing_py) 218 else: 219 # Dependencies are unlikely to be satisfiable for unsupported versions of python. 220 ctx.file("pip.bzl", _mock_pip) 221 ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) 222 223def _populate_empty_package(ctx): 224 # Mock out all the entrypoints we need to run from WORKSPACE. Targets that 225 # actually need python should use `target_compatible_with` and the generated 226 # @system_python//:exists or @system_python//:supported constraints. 227 ctx.file( 228 "BUILD.bazel", 229 _build_file.format( 230 interpreter = "", 231 support = "None", 232 ), 233 ) 234 ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = 'None'") 235 ctx.file("register.bzl", _mock_register) 236 ctx.file("pip.bzl", _mock_pip) 237 ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) 238 239def _system_python_impl(repository_ctx): 240 path = _get_python_path(repository_ctx) 241 python3 = repository_ctx.which("python3") 242 python_version = _get_python_version(repository_ctx) 243 244 if path and python_version[0] == "3": 245 _populate_package(repository_ctx, path, python3, python_version) 246 else: 247 # buildifier: disable=print 248 print("WARNING: no system python available, builds against system python will fail") 249 _populate_empty_package(repository_ctx) 250 251# The system_python() repository rule exposes information from the version of python installed in the current system. 252# 253# In WORKSPACE: 254# system_python( 255# name = "system_python_repo", 256# minimum_python_version = "3.7", 257# ) 258# 259# This repository exposes some repository rules for configuring python in Bazel. The python toolchain 260# *must* be registered in your WORKSPACE: 261# load("@system_python_repo//:register.bzl", "register_system_python") 262# register_system_python() 263# 264# Pip dependencies can optionally be specified using a wrapper around rules_python's repository rules: 265# load("@system_python//:pip.bzl", "pip_install") 266# pip_install( 267# name="pip_deps", 268# requirements = "@com_google_protobuf//python:requirements.txt", 269# ) 270# An optional argument `requirements_overrides` takes a dictionary mapping python versions to alternate 271# requirements files. This works around the requirement for fully pinned dependencies in python_rules. 272# 273# Four config settings are exposed from this repository to help declare target compatibility in Bazel. 274# For example, `@system_python_repo//:exists` will be true if a system python version has been found. 275# The `none` setting will be true only if no python version was found, and `supported`/`unsupported` 276# correspond to whether or not the system version is compatible with `minimum_python_version`. 277# 278# This repository also exposes a header rule that you can depend on from BUILD files: 279# cc_library( 280# name = "foobar", 281# srcs = ["foobar.cc"], 282# deps = ["@system_python_repo//:python_headers"], 283# ) 284# 285# The headers should correspond to the version of python obtained by running 286# the `python3` command on the system. 287system_python = repository_rule( 288 implementation = _system_python_impl, 289 local = True, 290 attrs = { 291 "minimum_python_version": attr.string(default = "3.7"), 292 }, 293) 294