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