mirror of https://github.com/grpc/grpc.git
Merge pull request #15992 from ghostwriternr/python_bazel_1
Basic setup to build gRPC Python with Bazel.pull/16184/head
commit
a87b1fb3ef
9 changed files with 505 additions and 1 deletions
@ -1,5 +1,44 @@ |
||||
workspace(name = "com_github_grpc_grpc") |
||||
workspace(name="com_github_grpc_grpc") |
||||
|
||||
load("//bazel:grpc_deps.bzl", "grpc_deps", "grpc_test_only_deps") |
||||
grpc_deps() |
||||
grpc_test_only_deps() |
||||
|
||||
new_http_archive( |
||||
name="cython", |
||||
sha256="d68138a2381afbdd0876c3cb2a22389043fa01c4badede1228ee073032b07a27", |
||||
urls=[ |
||||
"https://github.com/cython/cython/archive/c2b80d87658a8525ce091cbe146cb7eaa29fed5c.tar.gz", |
||||
], |
||||
strip_prefix="cython-c2b80d87658a8525ce091cbe146cb7eaa29fed5c", |
||||
build_file="//third_party:cython.BUILD", |
||||
) |
||||
|
||||
load("//third_party/py:python_configure.bzl", "python_configure") |
||||
python_configure(name="local_config_python") |
||||
|
||||
git_repository( |
||||
name="io_bazel_rules_python", |
||||
remote="https://github.com/bazelbuild/rules_python.git", |
||||
commit="8b5d0683a7d878b28fffe464779c8a53659fc645", |
||||
) |
||||
|
||||
load("@io_bazel_rules_python//python:pip.bzl", "pip_repositories", "pip_import") |
||||
|
||||
pip_repositories() |
||||
pip_import( |
||||
name="grpc_python_dependencies", |
||||
requirements="//:requirements.bazel.txt", |
||||
) |
||||
|
||||
load("@grpc_python_dependencies//:requirements.bzl", "pip_install") |
||||
pip_install() |
||||
|
||||
git_repository( |
||||
name="org_pubref_rules_protobuf", |
||||
remote="https://github.com/pubref/rules_protobuf", |
||||
tag="v0.8.2", |
||||
) |
||||
|
||||
load("@org_pubref_rules_protobuf//python:rules.bzl", "py_proto_repositories") |
||||
py_proto_repositories() |
||||
|
@ -0,0 +1,74 @@ |
||||
"""Custom rules for gRPC Python""" |
||||
|
||||
|
||||
# Adapted with modifications from |
||||
# tensorflow/tensorflow/core/platform/default/build_config.bzl |
||||
# Native Bazel rules don't exist yet to compile Cython code, but rules have |
||||
# been written at cython/cython and tensorflow/tensorflow. We branch from |
||||
# Tensorflow's version as it is more actively maintained and works for gRPC |
||||
# Python's needs. |
||||
def pyx_library(name, deps=[], py_deps=[], srcs=[], **kwargs): |
||||
"""Compiles a group of .pyx / .pxd / .py files. |
||||
|
||||
First runs Cython to create .cpp files for each input .pyx or .py + .pxd |
||||
pair. Then builds a shared object for each, passing "deps" to each cc_binary |
||||
rule (includes Python headers by default). Finally, creates a py_library rule |
||||
with the shared objects and any pure Python "srcs", with py_deps as its |
||||
dependencies; the shared objects can be imported like normal Python files. |
||||
|
||||
Args: |
||||
name: Name for the rule. |
||||
deps: C/C++ dependencies of the Cython (e.g. Numpy headers). |
||||
py_deps: Pure Python dependencies of the final library. |
||||
srcs: .py, .pyx, or .pxd files to either compile or pass through. |
||||
**kwargs: Extra keyword arguments passed to the py_library. |
||||
""" |
||||
# First filter out files that should be run compiled vs. passed through. |
||||
py_srcs = [] |
||||
pyx_srcs = [] |
||||
pxd_srcs = [] |
||||
for src in srcs: |
||||
if src.endswith(".pyx") or (src.endswith(".py") and |
||||
src[:-3] + ".pxd" in srcs): |
||||
pyx_srcs.append(src) |
||||
elif src.endswith(".py"): |
||||
py_srcs.append(src) |
||||
else: |
||||
pxd_srcs.append(src) |
||||
if src.endswith("__init__.py"): |
||||
pxd_srcs.append(src) |
||||
|
||||
# Invoke cython to produce the shared object libraries. |
||||
for filename in pyx_srcs: |
||||
native.genrule( |
||||
name=filename + "_cython_translation", |
||||
srcs=[filename], |
||||
outs=[filename.split(".")[0] + ".cpp"], |
||||
# Optionally use PYTHON_BIN_PATH on Linux platforms so that python 3 |
||||
# works. Windows has issues with cython_binary so skip PYTHON_BIN_PATH. |
||||
cmd= |
||||
"PYTHONHASHSEED=0 $(location @cython//:cython_binary) --cplus $(SRCS) --output-file $(OUTS)", |
||||
tools=["@cython//:cython_binary"] + pxd_srcs, |
||||
) |
||||
|
||||
shared_objects = [] |
||||
for src in pyx_srcs: |
||||
stem = src.split(".")[0] |
||||
shared_object_name = stem + ".so" |
||||
native.cc_binary( |
||||
name=shared_object_name, |
||||
srcs=[stem + ".cpp"], |
||||
deps=deps + ["@local_config_python//:python_headers"], |
||||
linkshared=1, |
||||
) |
||||
shared_objects.append(shared_object_name) |
||||
|
||||
# Now create a py_library with these shared objects as data. |
||||
native.py_library( |
||||
name=name, |
||||
srcs=py_srcs, |
||||
deps=py_deps, |
||||
srcs_version="PY2AND3", |
||||
data=shared_objects, |
||||
**kwargs) |
||||
|
@ -0,0 +1,10 @@ |
||||
# GRPC Python setup requirements |
||||
coverage>=4.0 |
||||
cython==0.28.3 |
||||
enum34>=1.0.4 |
||||
protobuf>=3.5.0.post1 |
||||
six>=1.10 |
||||
wheel>=0.29 |
||||
futures>=2.2.0 |
||||
google-auth>=1.0.0 |
||||
oauth2client==4.1.0 |
@ -0,0 +1,29 @@ |
||||
# Adapted with modifications from tensorflow/third_party/cython.BUILD |
||||
|
||||
py_library( |
||||
name="cython_lib", |
||||
srcs=glob( |
||||
["Cython/**/*.py"], |
||||
exclude=[ |
||||
"**/Tests/*.py", |
||||
], |
||||
) + ["cython.py"], |
||||
data=glob([ |
||||
"Cython/**/*.pyx", |
||||
"Cython/Utility/*.*", |
||||
"Cython/Includes/**/*.pxd", |
||||
]), |
||||
srcs_version="PY2AND3", |
||||
visibility=["//visibility:public"], |
||||
) |
||||
|
||||
# May not be named "cython", since that conflicts with Cython/ on OSX |
||||
py_binary( |
||||
name="cython_binary", |
||||
srcs=["cython.py"], |
||||
main="cython.py", |
||||
srcs_version="PY2AND3", |
||||
visibility=["//visibility:public"], |
||||
deps=["cython_lib"], |
||||
) |
||||
|
@ -0,0 +1,36 @@ |
||||
# Adapted with modifications from tensorflow/third_party/py/ |
||||
|
||||
package(default_visibility=["//visibility:public"]) |
||||
|
||||
# To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib |
||||
# See https://docs.python.org/3/extending/windows.html |
||||
cc_import( |
||||
name="python_lib", |
||||
interface_library=select({ |
||||
":windows": ":python_import_lib", |
||||
# A placeholder for Unix platforms which makes --no_build happy. |
||||
"//conditions:default": "not-existing.lib", |
||||
}), |
||||
system_provided=1, |
||||
) |
||||
|
||||
cc_library( |
||||
name="python_headers", |
||||
hdrs=[":python_include"], |
||||
deps=select({ |
||||
":windows": [":python_lib"], |
||||
"//conditions:default": [], |
||||
}), |
||||
includes=["python_include"], |
||||
) |
||||
|
||||
config_setting( |
||||
name="windows", |
||||
values={"cpu": "x64_windows"}, |
||||
visibility=["//visibility:public"], |
||||
) |
||||
|
||||
%{PYTHON_INCLUDE_GENRULE} |
||||
%{PYTHON_IMPORT_LIB_GENRULE} |
||||
|
||||
|
@ -0,0 +1,305 @@ |
||||
# Adapted with modifications from tensorflow/third_party/py/ |
||||
"""Repository rule for Python autoconfiguration. |
||||
|
||||
`python_configure` depends on the following environment variables: |
||||
|
||||
* `PYTHON_BIN_PATH`: location of python binary. |
||||
* `PYTHON_LIB_PATH`: Location of python libraries. |
||||
""" |
||||
|
||||
_BAZEL_SH = "BAZEL_SH" |
||||
_PYTHON_BIN_PATH = "PYTHON_BIN_PATH" |
||||
_PYTHON_LIB_PATH = "PYTHON_LIB_PATH" |
||||
_PYTHON_CONFIG_REPO = "PYTHON_CONFIG_REPO" |
||||
|
||||
|
||||
def _tpl(repository_ctx, tpl, substitutions={}, out=None): |
||||
if not out: |
||||
out = tpl |
||||
repository_ctx.template(out, Label("//third_party/py:%s.tpl" % tpl), |
||||
substitutions) |
||||
|
||||
|
||||
def _fail(msg): |
||||
"""Output failure message when auto configuration fails.""" |
||||
red = "\033[0;31m" |
||||
no_color = "\033[0m" |
||||
fail("%sPython Configuration Error:%s %s\n" % (red, no_color, msg)) |
||||
|
||||
|
||||
def _is_windows(repository_ctx): |
||||
"""Returns true if the host operating system is windows.""" |
||||
os_name = repository_ctx.os.name.lower() |
||||
return os_name.find("windows") != -1 |
||||
|
||||
|
||||
def _execute(repository_ctx, |
||||
cmdline, |
||||
error_msg=None, |
||||
error_details=None, |
||||
empty_stdout_fine=False): |
||||
"""Executes an arbitrary shell command. |
||||
|
||||
Args: |
||||
repository_ctx: the repository_ctx object |
||||
cmdline: list of strings, the command to execute |
||||
error_msg: string, a summary of the error if the command fails |
||||
error_details: string, details about the error or steps to fix it |
||||
empty_stdout_fine: bool, if True, an empty stdout result is fine, otherwise |
||||
it's an error |
||||
Return: |
||||
the result of repository_ctx.execute(cmdline) |
||||
""" |
||||
result = repository_ctx.execute(cmdline) |
||||
if result.stderr or not (empty_stdout_fine or result.stdout): |
||||
_fail("\n".join([ |
||||
error_msg.strip() if error_msg else "Repository command failed", |
||||
result.stderr.strip(), error_details if error_details else "" |
||||
])) |
||||
else: |
||||
return result |
||||
|
||||
|
||||
def _read_dir(repository_ctx, src_dir): |
||||
"""Returns a string with all files in a directory. |
||||
|
||||
Finds all files inside a directory, traversing subfolders and following |
||||
symlinks. The returned string contains the full path of all files |
||||
separated by line breaks. |
||||
""" |
||||
if _is_windows(repository_ctx): |
||||
src_dir = src_dir.replace("/", "\\") |
||||
find_result = _execute( |
||||
repository_ctx, |
||||
["cmd.exe", "/c", "dir", src_dir, "/b", "/s", "/a-d"], |
||||
empty_stdout_fine=True) |
||||
# src_files will be used in genrule.outs where the paths must |
||||
# use forward slashes. |
||||
return find_result.stdout.replace("\\", "/") |
||||
else: |
||||
find_result = _execute( |
||||
repository_ctx, ["find", src_dir, "-follow", "-type", "f"], |
||||
empty_stdout_fine=True) |
||||
return find_result.stdout |
||||
|
||||
|
||||
def _genrule(src_dir, genrule_name, command, outs): |
||||
"""Returns a string with a genrule. |
||||
|
||||
Genrule executes the given command and produces the given outputs. |
||||
""" |
||||
return ('genrule(\n' + ' name = "' + genrule_name + '",\n' + |
||||
' outs = [\n' + outs + '\n ],\n' + ' cmd = """\n' + |
||||
command + '\n """,\n' + ')\n') |
||||
|
||||
|
||||
def _normalize_path(path): |
||||
"""Returns a path with '/' and remove the trailing slash.""" |
||||
path = path.replace("\\", "/") |
||||
if path[-1] == "/": |
||||
path = path[:-1] |
||||
return path |
||||
|
||||
|
||||
def _symlink_genrule_for_dir(repository_ctx, |
||||
src_dir, |
||||
dest_dir, |
||||
genrule_name, |
||||
src_files=[], |
||||
dest_files=[]): |
||||
"""Returns a genrule to symlink(or copy if on Windows) a set of files. |
||||
|
||||
If src_dir is passed, files will be read from the given directory; otherwise |
||||
we assume files are in src_files and dest_files |
||||
""" |
||||
if src_dir != None: |
||||
src_dir = _normalize_path(src_dir) |
||||
dest_dir = _normalize_path(dest_dir) |
||||
files = '\n'.join( |
||||
sorted(_read_dir(repository_ctx, src_dir).splitlines())) |
||||
# Create a list with the src_dir stripped to use for outputs. |
||||
dest_files = files.replace(src_dir, '').splitlines() |
||||
src_files = files.splitlines() |
||||
command = [] |
||||
outs = [] |
||||
for i in range(len(dest_files)): |
||||
if dest_files[i] != "": |
||||
# If we have only one file to link we do not want to use the dest_dir, as |
||||
# $(@D) will include the full path to the file. |
||||
dest = '$(@D)/' + dest_dir + dest_files[i] if len( |
||||
dest_files) != 1 else '$(@D)/' + dest_files[i] |
||||
# On Windows, symlink is not supported, so we just copy all the files. |
||||
cmd = 'cp -f' if _is_windows(repository_ctx) else 'ln -s' |
||||
command.append(cmd + ' "%s" "%s"' % (src_files[i], dest)) |
||||
outs.append(' "' + dest_dir + dest_files[i] + '",') |
||||
return _genrule(src_dir, genrule_name, " && ".join(command), |
||||
"\n".join(outs)) |
||||
|
||||
|
||||
def _get_python_bin(repository_ctx): |
||||
"""Gets the python bin path.""" |
||||
python_bin = repository_ctx.os.environ.get(_PYTHON_BIN_PATH) |
||||
if python_bin != None: |
||||
return python_bin |
||||
python_bin_path = repository_ctx.which("python") |
||||
if python_bin_path != None: |
||||
return str(python_bin_path) |
||||
_fail("Cannot find python in PATH, please make sure " + |
||||
"python is installed and add its directory in PATH, or --define " + |
||||
"%s='/something/else'.\nPATH=%s" % |
||||
(_PYTHON_BIN_PATH, repository_ctx.os.environ.get("PATH", ""))) |
||||
|
||||
|
||||
def _get_bash_bin(repository_ctx): |
||||
"""Gets the bash bin path.""" |
||||
bash_bin = repository_ctx.os.environ.get(_BAZEL_SH) |
||||
if bash_bin != None: |
||||
return bash_bin |
||||
else: |
||||
bash_bin_path = repository_ctx.which("bash") |
||||
if bash_bin_path != None: |
||||
return str(bash_bin_path) |
||||
else: |
||||
_fail( |
||||
"Cannot find bash in PATH, please make sure " + |
||||
"bash is installed and add its directory in PATH, or --define " |
||||
+ "%s='/path/to/bash'.\nPATH=%s" % |
||||
(_BAZEL_SH, repository_ctx.os.environ.get("PATH", ""))) |
||||
|
||||
|
||||
def _get_python_lib(repository_ctx, python_bin): |
||||
"""Gets the python lib path.""" |
||||
python_lib = repository_ctx.os.environ.get(_PYTHON_LIB_PATH) |
||||
if python_lib != None: |
||||
return python_lib |
||||
print_lib = ( |
||||
"<<END\n" + "from __future__ import print_function\n" + |
||||
"import site\n" + "import os\n" + "\n" + "try:\n" + |
||||
" input = raw_input\n" + "except NameError:\n" + " pass\n" + "\n" + |
||||
"python_paths = []\n" + "if os.getenv('PYTHONPATH') is not None:\n" + |
||||
" python_paths = os.getenv('PYTHONPATH').split(':')\n" + "try:\n" + |
||||
" library_paths = site.getsitepackages()\n" + |
||||
"except AttributeError:\n" + |
||||
" from distutils.sysconfig import get_python_lib\n" + |
||||
" library_paths = [get_python_lib()]\n" + |
||||
"all_paths = set(python_paths + library_paths)\n" + "paths = []\n" + |
||||
"for path in all_paths:\n" + " if os.path.isdir(path):\n" + |
||||
" paths.append(path)\n" + "if len(paths) >=1:\n" + |
||||
" print(paths[0])\n" + "END") |
||||
cmd = '%s - %s' % (python_bin, print_lib) |
||||
result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) |
||||
return result.stdout.strip('\n') |
||||
|
||||
|
||||
def _check_python_lib(repository_ctx, python_lib): |
||||
"""Checks the python lib path.""" |
||||
cmd = 'test -d "%s" -a -x "%s"' % (python_lib, python_lib) |
||||
result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) |
||||
if result.return_code == 1: |
||||
_fail("Invalid python library path: %s" % python_lib) |
||||
|
||||
|
||||
def _check_python_bin(repository_ctx, python_bin): |
||||
"""Checks the python bin path.""" |
||||
cmd = '[[ -x "%s" ]] && [[ ! -d "%s" ]]' % (python_bin, python_bin) |
||||
result = repository_ctx.execute([_get_bash_bin(repository_ctx), "-c", cmd]) |
||||
if result.return_code == 1: |
||||
_fail("--define %s='%s' is not executable. Is it the python binary?" % |
||||
(_PYTHON_BIN_PATH, python_bin)) |
||||
|
||||
|
||||
def _get_python_include(repository_ctx, python_bin): |
||||
"""Gets the python include path.""" |
||||
result = _execute( |
||||
repository_ctx, [ |
||||
python_bin, "-c", 'from __future__ import print_function;' + |
||||
'from distutils import sysconfig;' + |
||||
'print(sysconfig.get_python_inc())' |
||||
], |
||||
error_msg="Problem getting python include path.", |
||||
error_details=( |
||||
"Is the Python binary path set up right? " + "(See ./configure or " |
||||
+ _PYTHON_BIN_PATH + ".) " + "Is distutils installed?")) |
||||
return result.stdout.splitlines()[0] |
||||
|
||||
|
||||
def _get_python_import_lib_name(repository_ctx, python_bin): |
||||
"""Get Python import library name (pythonXY.lib) on Windows.""" |
||||
result = _execute( |
||||
repository_ctx, [ |
||||
python_bin, "-c", |
||||
'import sys;' + 'print("python" + str(sys.version_info[0]) + ' + |
||||
' str(sys.version_info[1]) + ".lib")' |
||||
], |
||||
error_msg="Problem getting python import library.", |
||||
error_details=("Is the Python binary path set up right? " + |
||||
"(See ./configure or " + _PYTHON_BIN_PATH + ".) ")) |
||||
return result.stdout.splitlines()[0] |
||||
|
||||
|
||||
def _create_local_python_repository(repository_ctx): |
||||
"""Creates the repository containing files set up to build with Python.""" |
||||
python_bin = _get_python_bin(repository_ctx) |
||||
_check_python_bin(repository_ctx, python_bin) |
||||
python_lib = _get_python_lib(repository_ctx, python_bin) |
||||
_check_python_lib(repository_ctx, python_lib) |
||||
python_include = _get_python_include(repository_ctx, python_bin) |
||||
python_include_rule = _symlink_genrule_for_dir( |
||||
repository_ctx, python_include, 'python_include', 'python_include') |
||||
python_import_lib_genrule = "" |
||||
# To build Python C/C++ extension on Windows, we need to link to python import library pythonXY.lib |
||||
# See https://docs.python.org/3/extending/windows.html |
||||
if _is_windows(repository_ctx): |
||||
python_include = _normalize_path(python_include) |
||||
python_import_lib_name = _get_python_import_lib_name( |
||||
repository_ctx, python_bin) |
||||
python_import_lib_src = python_include.rsplit( |
||||
'/', 1)[0] + "/libs/" + python_import_lib_name |
||||
python_import_lib_genrule = _symlink_genrule_for_dir( |
||||
repository_ctx, None, '', 'python_import_lib', |
||||
[python_import_lib_src], [python_import_lib_name]) |
||||
_tpl( |
||||
repository_ctx, "BUILD", { |
||||
"%{PYTHON_INCLUDE_GENRULE}": python_include_rule, |
||||
"%{PYTHON_IMPORT_LIB_GENRULE}": python_import_lib_genrule, |
||||
}) |
||||
|
||||
|
||||
def _create_remote_python_repository(repository_ctx, remote_config_repo): |
||||
"""Creates pointers to a remotely configured repo set up to build with Python. |
||||
""" |
||||
_tpl(repository_ctx, "remote.BUILD", { |
||||
"%{REMOTE_PYTHON_REPO}": remote_config_repo, |
||||
}, "BUILD") |
||||
|
||||
|
||||
def _python_autoconf_impl(repository_ctx): |
||||
"""Implementation of the python_autoconf repository rule.""" |
||||
if _PYTHON_CONFIG_REPO in repository_ctx.os.environ: |
||||
_create_remote_python_repository( |
||||
repository_ctx, repository_ctx.os.environ[_PYTHON_CONFIG_REPO]) |
||||
else: |
||||
_create_local_python_repository(repository_ctx) |
||||
|
||||
|
||||
python_configure = repository_rule( |
||||
implementation=_python_autoconf_impl, |
||||
environ=[ |
||||
_BAZEL_SH, |
||||
_PYTHON_BIN_PATH, |
||||
_PYTHON_LIB_PATH, |
||||
_PYTHON_CONFIG_REPO, |
||||
], |
||||
) |
||||
"""Detects and configures the local Python. |
||||
|
||||
Add the following to your WORKSPACE FILE: |
||||
|
||||
```python |
||||
python_configure(name = "local_config_python") |
||||
``` |
||||
|
||||
Args: |
||||
name: A unique name for this workspace rule. |
||||
""" |
||||
|
@ -0,0 +1,10 @@ |
||||
# Adapted with modifications from tensorflow/third_party/py/ |
||||
|
||||
package(default_visibility=["//visibility:public"]) |
||||
|
||||
alias( |
||||
name="python_headers", |
||||
actual="%{REMOTE_PYTHON_REPO}:python_headers", |
||||
) |
||||
|
||||
|
Loading…
Reference in new issue