forked from bazel-contrib/rules_python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpip_repository.bzl
More file actions
269 lines (232 loc) · 8.52 KB
/
Copy pathpip_repository.bzl
File metadata and controls
269 lines (232 loc) · 8.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
""
load("//python/pip_install:repositories.bzl", "all_requirements")
def _construct_pypath(rctx):
"""Helper function to construct a PYTHONPATH.
Contains entries for code in this repo as well as packages downloaded from //python/pip_install:repositories.bzl.
This allows us to run python code inside repository rule implementations.
Args:
rctx: Handle to the repository_context.
Returns: String of the PYTHONPATH.
"""
# Get the root directory of these rules
rules_root = rctx.path(Label("//:BUILD")).dirname
thirdparty_roots = [
# Includes all the external dependencies from repositories.bzl
rctx.path(Label("@" + repo + "//:BUILD.bazel")).dirname
for repo in all_requirements
]
separator = ":" if not "windows" in rctx.os.name.lower() else ";"
pypath = separator.join([str(p) for p in [rules_root] + thirdparty_roots])
return pypath
def _parse_optional_attrs(rctx, args):
"""Helper function to parse common attributes of pip_repository and whl_library repository rules.
Args:
rctx: Handle to the rule repository context.
args: A list of parsed args for the rule.
Returns: Augmented args list.
"""
if rctx.attr.extra_pip_args:
args += [
"--extra_pip_args",
struct(args = rctx.attr.extra_pip_args).to_json(),
]
if rctx.attr.pip_data_exclude:
args += [
"--pip_data_exclude",
struct(exclude = rctx.attr.pip_data_exclude).to_json(),
]
if rctx.attr.enable_implicit_namespace_pkgs:
args.append("--enable_implicit_namespace_pkgs")
return args
_BUILD_FILE_CONTENTS = """\
package(default_visibility = ["//visibility:public"])
# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it
exports_files(["requirements.bzl"])
"""
def _pip_repository_impl(rctx):
python_interpreter = rctx.attr.python_interpreter
if rctx.attr.python_interpreter_target != None:
target = rctx.attr.python_interpreter_target
python_interpreter = rctx.path(target)
else:
if "/" not in python_interpreter:
python_interpreter = rctx.which(python_interpreter)
if not python_interpreter:
fail("python interpreter not found")
if rctx.attr.incremental and not rctx.attr.requirements_lock:
fail("Incremental mode requires a requirements_lock attribute be specified.")
# We need a BUILD file to load the generated requirements.bzl
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
pypath = _construct_pypath(rctx)
if rctx.attr.incremental:
args = [
python_interpreter,
"-m",
"python.pip_install.parse_requirements_to_bzl",
"--requirements_lock",
rctx.path(rctx.attr.requirements_lock),
# pass quiet and timeout args through to child repos.
"--quiet",
str(rctx.attr.quiet),
"--timeout",
str(rctx.attr.timeout),
]
else:
args = [
python_interpreter,
"-m",
"python.pip_install.extract_wheels",
"--requirements",
rctx.path(rctx.attr.requirements),
]
args += ["--repo", rctx.attr.name]
args = _parse_optional_attrs(rctx, args)
result = rctx.execute(
args,
environment = {
# Manually construct the PYTHONPATH since we cannot use the toolchain here
"PYTHONPATH": pypath,
},
timeout = rctx.attr.timeout,
quiet = rctx.attr.quiet,
)
if result.return_code:
fail("rules_python failed: %s (%s)" % (result.stdout, result.stderr))
return
common_attrs = {
"enable_implicit_namespace_pkgs": attr.bool(
default = False,
doc = """
If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary
and py_test targets must specify either `legacy_create_init=False` or the global Bazel option
`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory.
This option is required to support some packages which cannot handle the conversion to pkg-util style.
""",
),
"extra_pip_args": attr.string_list(
doc = "Extra arguments to pass on to pip. Must not contain spaces.",
),
"pip_data_exclude": attr.string_list(
doc = "Additional data exclusion parameters to add to the pip packages BUILD file.",
),
"python_interpreter": attr.string(default = "python3"),
"python_interpreter_target": attr.label(
allow_single_file = True,
doc = """
If you are using a custom python interpreter built by another repository rule,
use this attribute to specify its BUILD target. This allows pip_repository to invoke
pip using the same interpreter as your toolchain. If set, takes precedence over
python_interpreter.
""",
),
"quiet": attr.bool(
default = True,
doc = "If True, suppress printing stdout and stderr output to the terminal.",
),
# 600 is documented as default here: https://docs.bazel.build/versions/master/skylark/lib/repository_ctx.html#execute
"timeout": attr.int(
default = 600,
doc = "Timeout (in seconds) on the rule's execution duration.",
),
}
pip_repository_attrs = {
"incremental": attr.bool(
default = False,
doc = "Create the repository in incremental mode.",
),
"requirements": attr.label(
allow_single_file = True,
doc = "A 'requirements.txt' pip requirements file.",
),
"requirements_lock": attr.label(
allow_single_file = True,
doc = """
A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead
of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that
wheels are fetched/built only for the targets specified by 'build/run/test'.
""",
),
}
pip_repository_attrs.update(**common_attrs)
pip_repository = repository_rule(
attrs = pip_repository_attrs,
doc = """A rule for importing `requirements.txt` dependencies into Bazel.
This rule imports a `requirements.txt` file and generates a new
`requirements.bzl` file. This is used via the `WORKSPACE` pattern:
```python
pip_repository(
name = "foo",
requirements = ":requirements.txt",
)
```
You can then reference imported dependencies from your `BUILD` file with:
```python
load("@foo//:requirements.bzl", "requirement")
py_library(
name = "bar",
...
deps = [
"//my/other:dep",
requirement("requests"),
requirement("numpy"),
],
)
```
Or alternatively:
```python
load("@foo//:requirements.bzl", "all_requirements")
py_binary(
name = "baz",
...
deps = [
":foo",
] + all_requirements,
)
```
""",
implementation = _pip_repository_impl,
)
def _impl_whl_library(rctx):
# pointer to parent repo so these rules rerun if the definitions in requirements.bzl change.
_parent_repo_label = Label("@{parent}//:requirements.bzl".format(parent = rctx.attr.repo))
pypath = _construct_pypath(rctx)
args = [
rctx.attr.python_interpreter,
"-m",
"python.pip_install.parse_requirements_to_bzl.extract_single_wheel",
"--requirement",
rctx.attr.requirement,
"--repo",
rctx.attr.repo,
]
args = _parse_optional_attrs(rctx, args)
result = rctx.execute(
args,
environment = {
# Manually construct the PYTHONPATH since we cannot use the toolchain here
"PYTHONPATH": pypath,
},
quiet = rctx.attr.quiet,
timeout = rctx.attr.timeout,
)
if result.return_code:
fail("whl_library %s failed: %s (%s)" % (rctx.attr.name, result.stdout, result.stderr))
return
whl_library_attrs = {
"repo": attr.string(
mandatory = True,
doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.",
),
"requirement": attr.string(
mandatory = True,
doc = "Python requirement string describing the package to make available",
),
}
whl_library_attrs.update(**common_attrs)
whl_library = repository_rule(
attrs = whl_library_attrs,
doc = """
Download and extracts a single wheel based into a bazel repo based on the requirement string passed in.
Instantiated from pip_repository and inherits config options from there.""",
implementation = _impl_whl_library,
)