• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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