# Copyright (c) 2009-2021, Google LLC # All rights reserved. # # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd """Repository rule for using Python 3.x headers from the system.""" # Mock out rules_python's pip.bzl for cases where no system python is found. _mock_pip = """ def _pip_install_impl(repository_ctx): repository_ctx.file("BUILD.bazel", ''' py_library( name = "noop", visibility = ["//visibility:public"], ) ''') repository_ctx.file("requirements.bzl", ''' def install_deps(*args, **kwargs): print("WARNING: could not install pip dependencies") def requirement(*args, **kwargs): return "@{}//:noop" '''.format(repository_ctx.attr.name)) pip_install = repository_rule( implementation = _pip_install_impl, attrs = { "requirements": attr.string(), "requirements_overrides": attr.string_dict(), "python_interpreter_target": attr.string(), }, ) pip_parse = pip_install """ # Alias rules_python's pip.bzl for cases where a system python is found. _alias_pip = """ load("@rules_python//python:pip.bzl", _pip_parse = "pip_parse") def _get_requirements(requirements, requirements_overrides): for version, override in requirements_overrides.items(): if version in "{python_version}": requirements = override break return requirements def pip_parse(requirements, requirements_overrides={{}}, **kwargs): _pip_parse( python_interpreter_target = "@{repo}//:interpreter", requirements_lock = _get_requirements(requirements, requirements_overrides), **kwargs, ) pip_install = pip_parse """ _mock_fuzzing_py = """ def fuzzing_py_install_deps(): print("WARNING: could not install fuzzing_py dependencies") """ # Alias rules_fuzzing's requirements.bzl for cases where a system python is found. _alias_fuzzing_py = """ load("@fuzzing_py_deps//:requirements.bzl", _fuzzing_py_install_deps = "install_deps") def fuzzing_py_install_deps(): _fuzzing_py_install_deps() """ _build_file = """ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@bazel_tools//tools/python:toolchain.bzl", "py_runtime_pair") cc_library( name = "python_headers", hdrs = glob(["python/**/*.h"], allow_empty = True), includes = ["python"], visibility = ["//visibility:public"], ) string_flag( name = "internal_python_support", build_setting_default = "{support}", values = [ "None", "Supported", "Unsupported", ] ) config_setting( name = "none", flag_values = {{ ":internal_python_support": "None", }}, visibility = ["//visibility:public"], ) config_setting( name = "supported", flag_values = {{ ":internal_python_support": "Supported", }}, visibility = ["//visibility:public"], ) config_setting( name = "unsupported", flag_values = {{ ":internal_python_support": "Unsupported", }}, visibility = ["//visibility:public"], ) selects.config_setting_group( name = "exists", match_any = [":supported", ":unsupported"], visibility = ["//visibility:public"], ) sh_binary( name = "interpreter", srcs = ["interpreter"], visibility = ["//visibility:public"], ) py_runtime( name = "py3_runtime", interpreter_path = "{interpreter}", python_version = "PY3", ) py_runtime_pair( name = "runtime_pair", py3_runtime = ":py3_runtime", ) toolchain( name = "python_toolchain", toolchain = ":runtime_pair", toolchain_type = "@rules_python//python:toolchain_type", ) """ _register = """ def register_system_python(): native.register_toolchains("@{}//:python_toolchain") """ _mock_register = """ def register_system_python(): pass """ def _get_python_version(repository_ctx): py_program = "import sys; print(str(sys.version_info.major) + '.' + str(sys.version_info.minor) + '.' + str(sys.version_info.micro))" result = repository_ctx.execute(["python3", "-c", py_program]) return (result.stdout).strip().split(".") def _get_python_path(repository_ctx): py_program = "import sysconfig; print(sysconfig.get_config_var('%s'), end='')" result = repository_ctx.execute(["python3", "-c", py_program % ("INCLUDEPY")]) if result.return_code != 0: return None return result.stdout def _populate_package(ctx, path, python3, python_version): ctx.symlink(path, "python") supported = True for idx, v in enumerate(ctx.attr.minimum_python_version.split(".")): if int(python_version[idx]) < int(v): supported = False break if "win" in ctx.os.name: # buildifier: disable=print print("WARNING: python is not supported on Windows") supported = False build_file = _build_file.format( interpreter = python3, support = "Supported" if supported else "Unsupported", ) ctx.file("interpreter", "#!/bin/sh\nexec {} \"$@\"".format(python3)) ctx.file("BUILD.bazel", build_file) ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = '{}{}'".format(python_version[0], python_version[1])) ctx.file("register.bzl", _register.format(ctx.attr.name)) if supported: ctx.file("pip.bzl", _alias_pip.format( python_version = ".".join(python_version), repo = ctx.attr.name, )) ctx.file("fuzzing_py.bzl", _alias_fuzzing_py) else: # Dependencies are unlikely to be satisfiable for unsupported versions of python. ctx.file("pip.bzl", _mock_pip) ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) def _populate_empty_package(ctx): # Mock out all the entrypoints we need to run from WORKSPACE. Targets that # actually need python should use `target_compatible_with` and the generated # @system_python//:exists or @system_python//:supported constraints. ctx.file( "BUILD.bazel", _build_file.format( interpreter = "", support = "None", ), ) ctx.file("version.bzl", "SYSTEM_PYTHON_VERSION = 'None'") ctx.file("register.bzl", _mock_register) ctx.file("pip.bzl", _mock_pip) ctx.file("fuzzing_py.bzl", _mock_fuzzing_py) def _system_python_impl(repository_ctx): path = _get_python_path(repository_ctx) python3 = repository_ctx.which("python3") python_version = _get_python_version(repository_ctx) if path and python_version[0] == "3": _populate_package(repository_ctx, path, python3, python_version) else: # buildifier: disable=print print("WARNING: no system python available, builds against system python will fail") _populate_empty_package(repository_ctx) # The system_python() repository rule exposes information from the version of python installed in the current system. # # In WORKSPACE: # system_python( # name = "system_python_repo", # minimum_python_version = "3.7", # ) # # This repository exposes some repository rules for configuring python in Bazel. The python toolchain # *must* be registered in your WORKSPACE: # load("@system_python_repo//:register.bzl", "register_system_python") # register_system_python() # # Pip dependencies can optionally be specified using a wrapper around rules_python's repository rules: # load("@system_python//:pip.bzl", "pip_install") # pip_install( # name="pip_deps", # requirements = "@com_google_protobuf//python:requirements.txt", # ) # An optional argument `requirements_overrides` takes a dictionary mapping python versions to alternate # requirements files. This works around the requirement for fully pinned dependencies in python_rules. # # Four config settings are exposed from this repository to help declare target compatibility in Bazel. # For example, `@system_python_repo//:exists` will be true if a system python version has been found. # The `none` setting will be true only if no python version was found, and `supported`/`unsupported` # correspond to whether or not the system version is compatible with `minimum_python_version`. # # This repository also exposes a header rule that you can depend on from BUILD files: # cc_library( # name = "foobar", # srcs = ["foobar.cc"], # deps = ["@system_python_repo//:python_headers"], # ) # # The headers should correspond to the version of python obtained by running # the `python3` command on the system. system_python = repository_rule( implementation = _system_python_impl, local = True, attrs = { "minimum_python_version": attr.string(default = "3.8"), }, )