blob: d6f2121438e98824c427ada83da202778aa53bdf [file] [log] [blame] [edit]
"""Repository rule for creating an external repository `name` with C-language
dependencies from the Python package `pkg`, published by rules_python. Set
`pkg` using the `requirement` function from rules_python.
The top-level Bazel package in the created repository provides two targets for
use as dependencies in C-language targets elsewhere.
* `:cc_headers`--for including headers
* `:cc_library`--for including headers and linking against a library
The mandatory `includes` attribute should be set to a list of
include dirs to be added to the compile command line.
The optional `libs` attribute should be set to a list of libraries to link with
the binary target.
Specify all paths relative to the parent directory in which the package is
extracted (e.g., site-packages/). Thus paths will begin with the package's
Python namespace or module name. Note this name may differ from the Python
distribution package name---e.g., the distribution package `tensorflow-gpu`
distributes the Python namespace package `tensorflow`. To see what paths are
available, it might help to examine the directory tree in the external
repository created for the package by rules_python. The external repository is
created in the bazel cache; in the example below, in a subdirectory
`external/tflm_pip_deps_numpy`.
For example, to use the headers from NumPy:
1. Add Python dependencies (numpy is named in python_requirements.txt), via the
usual method, to an external repository named `tflm_pip_deps` via @rules_python
in the WORKSPACE:
```
load("@rules_python//python:pip.bzl", "pip_parse")
pip_parse(
name = "tflm_pip_deps",
requirements_lock = "@//third_party:python_requirements.txt",
)
load("@tflm_pip_deps//:requirements.bzl", "install_deps")
install_deps()
```
2. Use the repository rule `py_pkg_cc_deps` in the WORKSPACE to create an
external repository with a target `@numpy_cc_deps//:cc_headers`, passing the
`:pkg` target from @tflm_pip_deps, obtained via requirement(), and an
`includes` path based on an examination of the package and the desired #include
paths in the C code:
```
load("@tflm_pip_deps//:requirements.bzl", "requirement")
load("@//python:py_pkg_cc_deps.bzl", "py_pkg_cc_deps")
py_pkg_cc_deps(
name = "numpy_cc_deps",
pkg = requirement("numpy"),
includes = ["numpy/core/include"],
)
```
3. Use the cc_library target `@numpy_cc_deps//:cc_headers` in a BUILD file as
a dependency to a rule that needs the headers, e.g., the cc_library()-based
pybind_library():
```
pybind_library(
name = "your_extension_lib",
srcs = [...],
deps = ["@numpy_cc_deps//:cc_headers", ...],
)
```
See the test target //python/tests:cc_dep_link_test elsewhere for an example
which links against a library shipped in a Python package.
"""
# This extends the standard rules_python rules to expose C-language dependences
# contained in some Python packages like NumPy. It extends rules_python to
# avoid duplicating the download mechanism, and to ensure the Python package
# versions used throughout the WORKSPACE are consistent.
def _rules_python_path(ctx, pkg):
# Make an absolute path to the rules_python repository for the Python
# package `pkg`.
# WARNING: To get a filesystem path via ctx.path(), its argument must be a
# label to a non-generated file. ctx.path() does not work on non-file
# A standard technique for finding the path to a repository (see,
# e.g., rules_go) is to use the repository's BUILD file; however, the exact
# name of the build file is an implementation detail of rules_python.
build_file = pkg.relative(":BUILD.bazel")
abspath = ctx.path(build_file).dirname
return abspath
def _join_paths(a, b):
result = ""
if type(a) == "string" and type(b) == "string":
result = "/".join((a, b))
elif type(a) == "path" and type(b) == "string":
# Append components of string b to path a, because path.get_child()
# requires one component at a time.
result = a
for x in b.split("/"):
result = result.get_child(x)
return result
def _make_build_file(basedir, include_paths, libs):
template = """\
package(
default_visibility = ["//visibility:public"],
)
cc_library(
name = "cc_headers",
hdrs = glob(%s, allow_empty=False, exclude_directories=1),
includes = %s,
)
cc_library(
name = "cc_library",
srcs = %s,
deps = [":cc_headers"],
)
"""
hdrs = [(_join_paths(basedir, inc) + "/**") for inc in include_paths]
includes = [_join_paths(basedir, inc) for inc in include_paths]
srcs = [_join_paths(basedir, lib) for lib in libs]
return template % (hdrs, includes, srcs)
def _py_pkg_cc_deps(ctx):
# Create a repository with the directory tree:
# repository/
# |- _site --> @specific_rules_python_pkg/site-packages
# \_ BUILD
#
# When debugging, it might help to examine the tree and BUILD file of this
# repository, created in the bazel cache.
# Symlink to the rules_python repository of pkg
srcdir = _join_paths(_rules_python_path(ctx, ctx.attr.pkg), "site-packages")
destdir = "_site"
ctx.symlink(srcdir, destdir)
# Write a BUILD file publishing targets
ctx.file(
"BUILD",
content = _make_build_file(destdir, ctx.attr.includes, ctx.attr.libs),
executable = False,
)
py_pkg_cc_deps = repository_rule(
implementation = _py_pkg_cc_deps,
local = True,
attrs = {
"pkg": attr.label(
doc = "Python package target via rules_python's requirement()",
mandatory = True,
),
"includes": attr.string_list(
doc = "list of include dirs",
mandatory = True,
allow_empty = False,
),
"libs": attr.string_list(
doc = "list of libraries against which to link",
mandatory = False,
),
},
)