Skip to content

Commit 1b59002

Browse files
author
Alex Eagle
authored
Document how to vendor a pip_parse requirements.bzl file (#655)
* Document how to vendor a pip_parse requirements.bzl file fixes #608 * code review feedback
1 parent fc05103 commit 1b59002

File tree

5 files changed

+85
-16
lines changed

5 files changed

+85
-16
lines changed

docs/pip.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ pip_parse(<a href="#pip_parse-requirements_lock">requirements_lock</a>, <a href=
156156
Accepts a locked/compiled requirements file and installs the dependencies listed within.
157157

158158
Those dependencies become available in a generated `requirements.bzl` file.
159+
You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
159160

160161
This macro runs a repository rule that invokes `pip`. In your WORKSPACE file:
161162

@@ -218,6 +219,31 @@ alias(
218219
)
219220
```
220221

222+
## Vendoring the requirements.bzl file
223+
224+
In some cases you may not want to generate the requirements.bzl file as a repository rule
225+
while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
226+
such as a ruleset, you may want to include the requirements.bzl file rather than make your users
227+
install the WORKSPACE setup to generate it.
228+
See https://github.com/bazelbuild/rules_python/issues/608
229+
230+
This is the same workflow as Gazelle, which creates `go_repository` rules with
231+
[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
232+
233+
Simply run the same tool that the `pip_parse` repository rule calls.
234+
You can find the arguments in the generated BUILD file in the pip_parse repo,
235+
for example in `$(bazel info output_base)/external/pypi/BUILD.bazel` for a repo
236+
named `pypi`.
237+
238+
The command will look like this:
239+
```shell
240+
bazel run -- @rules_python//python/pip_install/parse_requirements_to_bzl \
241+
--requirements_lock ./requirements_lock.txt \
242+
--quiet False --timeout 120 --repo pypi --repo-prefix pypi_ > requirements.bzl
243+
```
244+
245+
Then load the requirements.bzl file directly, without using `pip_parse` in the WORKSPACE.
246+
221247

222248
**PARAMETERS**
223249

python/pip.bzl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def pip_parse(requirements_lock, name = "pip_parsed_deps", **kwargs):
105105
"""Accepts a locked/compiled requirements file and installs the dependencies listed within.
106106
107107
Those dependencies become available in a generated `requirements.bzl` file.
108+
You can instead check this `requirements.bzl` file into your repo, see the "vendoring" section below.
108109
109110
This macro runs a repository rule that invokes `pip`. In your WORKSPACE file:
110111
@@ -167,6 +168,31 @@ def pip_parse(requirements_lock, name = "pip_parsed_deps", **kwargs):
167168
)
168169
```
169170
171+
## Vendoring the requirements.bzl file
172+
173+
In some cases you may not want to generate the requirements.bzl file as a repository rule
174+
while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module
175+
such as a ruleset, you may want to include the requirements.bzl file rather than make your users
176+
install the WORKSPACE setup to generate it.
177+
See https://github.com/bazelbuild/rules_python/issues/608
178+
179+
This is the same workflow as Gazelle, which creates `go_repository` rules with
180+
[`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos)
181+
182+
Simply run the same tool that the `pip_parse` repository rule calls.
183+
You can find the arguments in the generated BUILD file in the pip_parse repo,
184+
for example in `$(bazel info output_base)/external/pypi/BUILD.bazel` for a repo
185+
named `pypi`.
186+
187+
The command will look like this:
188+
```shell
189+
bazel run -- @rules_python//python/pip_install/parse_requirements_to_bzl \\
190+
--requirements_lock ./requirements_lock.txt \\
191+
--quiet False --timeout 120 --repo pypi --repo-prefix pypi_ > requirements.bzl
192+
```
193+
194+
Then load the requirements.bzl file directly, without using `pip_parse` in the WORKSPACE.
195+
170196
Args:
171197
requirements_lock (Label): A fully resolved 'requirements.txt' pip requirement file
172198
containing the transitive set of your dependencies. If this file is passed instead

python/pip_install/parse_requirements_to_bzl/__init__.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import textwrap
66
from pathlib import Path
7-
from typing import Any, Dict, List, Optional, Tuple
7+
from typing import Any, Dict, List, TextIO, Tuple
88

99
from pip._internal.network.session import PipSession
1010
from pip._internal.req import constructors
@@ -166,7 +166,11 @@ def coerce_to_bool(option):
166166
return str(option).lower() == "true"
167167

168168

169-
def main() -> None:
169+
def main(output: TextIO) -> None:
170+
"""Args:
171+
172+
output: where to write the resulting starlark, such as sys.stdout or an open file
173+
"""
170174
parser = argparse.ArgumentParser(
171175
description="Create rules to incrementally fetch needed \
172176
dependencies from a fully resolved requirements lock file."
@@ -216,7 +220,7 @@ def main() -> None:
216220
args.requirements_lock, whl_library_args["extra_pip_args"]
217221
)
218222
req_names = sorted([req.name for req, _ in install_requirements])
219-
annotations = args.annotations.collect(req_names)
223+
annotations = args.annotations.collect(req_names) if args.annotations else {}
220224

221225
# Write all rendered annotation files and generate a list of the labels to write to the requirements file
222226
annotated_requirements = dict()
@@ -231,12 +235,11 @@ def main() -> None:
231235
}
232236
)
233237

234-
with open("requirements.bzl", "w") as requirement_file:
235-
requirement_file.write(
236-
generate_parsed_requirements_contents(
237-
requirements_lock=args.requirements_lock,
238-
repo_prefix=args.repo_prefix,
239-
whl_library_args=whl_library_args,
240-
annotations=annotated_requirements,
241-
)
238+
output.write(
239+
generate_parsed_requirements_contents(
240+
requirements_lock=args.requirements_lock,
241+
repo_prefix=args.repo_prefix,
242+
whl_library_args=whl_library_args,
243+
annotations=annotated_requirements,
242244
)
245+
)
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
"""Main entry point."""
2+
import os
3+
import sys
4+
25
from python.pip_install.parse_requirements_to_bzl import main
36

47
if __name__ == "__main__":
5-
main()
8+
# Under `bazel run`, just print the generated starlark code.
9+
# This allows users to check that into their repository rather than
10+
# call pip_parse to generate as a repository rule.
11+
if "BUILD_WORKING_DIRECTORY" in os.environ:
12+
os.chdir(os.environ["BUILD_WORKING_DIRECTORY"])
13+
main(sys.stdout)
14+
else:
15+
with open("requirements.bzl", "w") as requirement_file:
16+
main(requirement_file)

python/pip_install/pip_repository.bzl

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,6 @@ def _pip_repository_impl(rctx):
125125
if rctx.attr.incremental and not rctx.attr.requirements_lock:
126126
fail("Incremental mode requires a requirements_lock attribute be specified.")
127127

128-
# We need a BUILD file to load the generated requirements.bzl
129-
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS)
130-
131128
# Write the annotations file to pass to the wheel maker
132129
annotations = {package: json.decode(data) for (package, data) in rctx.attr.annotations.items()}
133130
annotations_file = rctx.path("annotations.json")
@@ -152,7 +149,7 @@ def _pip_repository_impl(rctx):
152149
args += ["--python_interpreter", _get_python_interpreter_attr(rctx)]
153150
if rctx.attr.python_interpreter_target:
154151
args += ["--python_interpreter_target", str(rctx.attr.python_interpreter_target)]
155-
152+
progress_message = "Parsing requirements to starlark"
156153
else:
157154
args = [
158155
python_interpreter,
@@ -163,10 +160,13 @@ def _pip_repository_impl(rctx):
163160
"--annotations",
164161
annotations_file,
165162
]
163+
progress_message = "Extracting wheels"
166164

167165
args += ["--repo", rctx.attr.name, "--repo-prefix", rctx.attr.repo_prefix]
168166
args = _parse_optional_attrs(rctx, args)
169167

168+
rctx.report_progress(progress_message)
169+
170170
result = rctx.execute(
171171
args,
172172
# Manually construct the PYTHONPATH since we cannot use the toolchain here
@@ -178,6 +178,9 @@ def _pip_repository_impl(rctx):
178178
if result.return_code:
179179
fail("rules_python failed: %s (%s)" % (result.stdout, result.stderr))
180180

181+
# We need a BUILD file to load the generated requirements.bzl
182+
rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS + "\n# The requirements.bzl file was generated by running:\n# " + " ".join([str(a) for a in args]))
183+
181184
return
182185

183186
common_env = [

0 commit comments

Comments
 (0)