blob: d6f2121438e98824c427ada83da202778aa53bdf [file] [log] [blame]
TANMAY DAS20d202f2023-03-08 21:06:49 +00001"""Repository rule for creating an external repository `name` with C-language
2dependencies from the Python package `pkg`, published by rules_python. Set
3`pkg` using the `requirement` function from rules_python.
4
5The top-level Bazel package in the created repository provides two targets for
6use as dependencies in C-language targets elsewhere.
7
8 * `:cc_headers`--for including headers
9 * `:cc_library`--for including headers and linking against a library
10
11The mandatory `includes` attribute should be set to a list of
12include dirs to be added to the compile command line.
13
14The optional `libs` attribute should be set to a list of libraries to link with
15the binary target.
16
17Specify all paths relative to the parent directory in which the package is
18extracted (e.g., site-packages/). Thus paths will begin with the package's
19Python namespace or module name. Note this name may differ from the Python
20distribution package name---e.g., the distribution package `tensorflow-gpu`
21distributes the Python namespace package `tensorflow`. To see what paths are
22available, it might help to examine the directory tree in the external
23repository created for the package by rules_python. The external repository is
24created in the bazel cache; in the example below, in a subdirectory
25`external/tflm_pip_deps_numpy`.
26
27For example, to use the headers from NumPy:
28
291. Add Python dependencies (numpy is named in python_requirements.txt), via the
30usual method, to an external repository named `tflm_pip_deps` via @rules_python
31in the WORKSPACE:
32```
33 load("@rules_python//python:pip.bzl", "pip_parse")
34 pip_parse(
35 name = "tflm_pip_deps",
36 requirements_lock = "@//third_party:python_requirements.txt",
37 )
38 load("@tflm_pip_deps//:requirements.bzl", "install_deps")
39 install_deps()
40```
41
422. Use the repository rule `py_pkg_cc_deps` in the WORKSPACE to create an
43external repository with a target `@numpy_cc_deps//:cc_headers`, passing the
44`:pkg` target from @tflm_pip_deps, obtained via requirement(), and an
45`includes` path based on an examination of the package and the desired #include
46paths in the C code:
47```
48 load("@tflm_pip_deps//:requirements.bzl", "requirement")
49 load("@//python:py_pkg_cc_deps.bzl", "py_pkg_cc_deps")
50 py_pkg_cc_deps(
51 name = "numpy_cc_deps",
52 pkg = requirement("numpy"),
53 includes = ["numpy/core/include"],
54 )
55```
56
573. Use the cc_library target `@numpy_cc_deps//:cc_headers` in a BUILD file as
58a dependency to a rule that needs the headers, e.g., the cc_library()-based
59pybind_library():
60```
61 pybind_library(
62 name = "your_extension_lib",
63 srcs = [...],
64 deps = ["@numpy_cc_deps//:cc_headers", ...],
65 )
66```
67
68See the test target //python/tests:cc_dep_link_test elsewhere for an example
69which links against a library shipped in a Python package.
70"""
Ryan Kuester17aae9e2023-03-05 00:25:58 -060071
72# This extends the standard rules_python rules to expose C-language dependences
73# contained in some Python packages like NumPy. It extends rules_python to
74# avoid duplicating the download mechanism, and to ensure the Python package
75# versions used throughout the WORKSPACE are consistent.
76
77def _rules_python_path(ctx, pkg):
78 # Make an absolute path to the rules_python repository for the Python
79 # package `pkg`.
80
81 # WARNING: To get a filesystem path via ctx.path(), its argument must be a
82 # label to a non-generated file. ctx.path() does not work on non-file
83 # A standard technique for finding the path to a repository (see,
84 # e.g., rules_go) is to use the repository's BUILD file; however, the exact
85 # name of the build file is an implementation detail of rules_python.
86 build_file = pkg.relative(":BUILD.bazel")
87 abspath = ctx.path(build_file).dirname
88 return abspath
89
90def _join_paths(a, b):
TANMAY DAS20d202f2023-03-08 21:06:49 +000091 result = ""
Ryan Kuester17aae9e2023-03-05 00:25:58 -060092 if type(a) == "string" and type(b) == "string":
93 result = "/".join((a, b))
94
95 elif type(a) == "path" and type(b) == "string":
96 # Append components of string b to path a, because path.get_child()
97 # requires one component at a time.
98 result = a
99 for x in b.split("/"):
100 result = result.get_child(x)
101
102 return result
103
104def _make_build_file(basedir, include_paths, libs):
105 template = """\
106package(
107 default_visibility = ["//visibility:public"],
108)
109
110cc_library(
111 name = "cc_headers",
112 hdrs = glob(%s, allow_empty=False, exclude_directories=1),
113 includes = %s,
114)
115
116cc_library(
117 name = "cc_library",
118 srcs = %s,
119 deps = [":cc_headers"],
120)
121"""
122 hdrs = [(_join_paths(basedir, inc) + "/**") for inc in include_paths]
123 includes = [_join_paths(basedir, inc) for inc in include_paths]
124 srcs = [_join_paths(basedir, lib) for lib in libs]
125
126 return template % (hdrs, includes, srcs)
127
128def _py_pkg_cc_deps(ctx):
129 # Create a repository with the directory tree:
130 # repository/
131 # |- _site --> @specific_rules_python_pkg/site-packages
132 # \_ BUILD
133 #
134 # When debugging, it might help to examine the tree and BUILD file of this
135 # repository, created in the bazel cache.
136
137 # Symlink to the rules_python repository of pkg
138 srcdir = _join_paths(_rules_python_path(ctx, ctx.attr.pkg), "site-packages")
139 destdir = "_site"
140 ctx.symlink(srcdir, destdir)
141
142 # Write a BUILD file publishing targets
143 ctx.file(
144 "BUILD",
145 content = _make_build_file(destdir, ctx.attr.includes, ctx.attr.libs),
146 executable = False,
147 )
148
149py_pkg_cc_deps = repository_rule(
150 implementation = _py_pkg_cc_deps,
151 local = True,
152 attrs = {
153 "pkg": attr.label(
154 doc = "Python package target via rules_python's requirement()",
155 mandatory = True,
156 ),
157 "includes": attr.string_list(
158 doc = "list of include dirs",
159 mandatory = True,
160 allow_empty = False,
161 ),
162 "libs": attr.string_list(
163 doc = "list of libraries against which to link",
164 mandatory = False,
165 ),
166 },
167)