blob: 1f5f1d5a54df758874cc0e4b8ccdde0ec24fdf1d [file] [log] [blame]
Philipp Schrader868070a2022-09-06 22:51:13 -07001From 843248c52d335f7ed51209cd7eee009a743b3488 Mon Sep 17 00:00:00 2001
2From: Philipp Schrader <philipp.schrader@gmail.com>
3Date: Sun, 11 Sep 2022 22:04:47 -0700
4Subject: [PATCH] Support overriding individual packages
5
6---
7 .../extract_wheels/extract_single_wheel.py | 60 ++++++++++---------
8 .../parse_requirements_to_bzl.py | 42 ++++++++++++-
9 python/pip_install/pip_repository.bzl | 34 +++++++++++
10 3 files changed, 107 insertions(+), 29 deletions(-)
11
12diff --git a/python/pip_install/extract_wheels/extract_single_wheel.py b/python/pip_install/extract_wheels/extract_single_wheel.py
13index a7cc672..26d4368 100644
14--- a/python/pip_install/extract_wheels/extract_single_wheel.py
15+++ b/python/pip_install/extract_wheels/extract_single_wheel.py
16@@ -28,41 +28,47 @@ def main() -> None:
17 type=annotation_from_str_path,
18 help="A json encoded file containing annotations for rendered packages.",
19 )
20+ parser.add_argument(
21+ "--pre-downloaded",
22+ action="store_true",
23+ help="If set, skips the pip download step. The .whl file is assumbed to be downloaded by bazel.",
24+ )
25 arguments.parse_common_args(parser)
26 args = parser.parse_args()
27 deserialized_args = dict(vars(args))
28 arguments.deserialize_structured_args(deserialized_args)
29
30- configure_reproducible_wheels()
31+ if not args.pre_downloaded:
32+ configure_reproducible_wheels()
33
34- pip_args = (
35- [sys.executable, "-m", "pip"]
36- + (["--isolated"] if args.isolated else [])
37- + ["download" if args.download_only else "wheel", "--no-deps"]
38- + deserialized_args["extra_pip_args"]
39- )
40+ pip_args = (
41+ [sys.executable, "-m", "pip"]
42+ + (["--isolated"] if args.isolated else [])
43+ + ["download" if args.download_only else "wheel", "--no-deps"]
44+ + deserialized_args["extra_pip_args"]
45+ )
46
47- requirement_file = NamedTemporaryFile(mode="wb", delete=False)
48- try:
49- requirement_file.write(args.requirement.encode("utf-8"))
50- requirement_file.flush()
51- # Close the file so pip is allowed to read it when running on Windows.
52- # For more information, see: https://bugs.python.org/issue14243
53- requirement_file.close()
54- # Requirement specific args like --hash can only be passed in a requirements file,
55- # so write our single requirement into a temp file in case it has any of those flags.
56- pip_args.extend(["-r", requirement_file.name])
57-
58- env = os.environ.copy()
59- env.update(deserialized_args["environment"])
60- # Assumes any errors are logged by pip so do nothing. This command will fail if pip fails
61- subprocess.run(pip_args, check=True, env=env)
62- finally:
63+ requirement_file = NamedTemporaryFile(mode="wb", delete=False)
64 try:
65- os.unlink(requirement_file.name)
66- except OSError as e:
67- if e.errno != errno.ENOENT:
68- raise
69+ requirement_file.write(args.requirement.encode("utf-8"))
70+ requirement_file.flush()
71+ # Close the file so pip is allowed to read it when running on Windows.
72+ # For more information, see: https://bugs.python.org/issue14243
73+ requirement_file.close()
74+ # Requirement specific args like --hash can only be passed in a requirements file,
75+ # so write our single requirement into a temp file in case it has any of those flags.
76+ pip_args.extend(["-r", requirement_file.name])
77+
78+ env = os.environ.copy()
79+ env.update(deserialized_args["environment"])
80+ # Assumes any errors are logged by pip so do nothing. This command will fail if pip fails
81+ subprocess.run(pip_args, check=True, env=env)
82+ finally:
83+ try:
84+ os.unlink(requirement_file.name)
85+ except OSError as e:
86+ if e.errno != errno.ENOENT:
87+ raise
88
89 name, extras_for_pkg = requirements._parse_requirement_for_extra(args.requirement)
90 extras = {name: extras_for_pkg} if extras_for_pkg and name else dict()
91diff --git a/python/pip_install/extract_wheels/parse_requirements_to_bzl.py b/python/pip_install/extract_wheels/parse_requirements_to_bzl.py
92index 5762cf5..07642ca 100644
93--- a/python/pip_install/extract_wheels/parse_requirements_to_bzl.py
94+++ b/python/pip_install/extract_wheels/parse_requirements_to_bzl.py
95@@ -4,7 +4,7 @@ import shlex
96 import sys
97 import textwrap
98 from pathlib import Path
99-from typing import Any, Dict, List, TextIO, Tuple
100+from typing import Any, Dict, List, Optional, TextIO, Tuple
101
102 from pip._internal.network.session import PipSession
103 from pip._internal.req import constructors
104@@ -81,7 +81,7 @@ def parse_whl_library_args(args: argparse.Namespace) -> Dict[str, Any]:
105 whl_library_args.setdefault("python_interpreter", sys.executable)
106
107 # These arguments are not used by `whl_library`
108- for arg in ("requirements_lock", "requirements_lock_label", "annotations"):
109+ for arg in ("requirements_lock", "requirements_lock_label", "annotations", "overrides", "require_overrides"):
110 if arg in whl_library_args:
111 whl_library_args.pop(arg)
112
113@@ -93,6 +93,8 @@ def generate_parsed_requirements_contents(
114 repo_prefix: str,
115 whl_library_args: Dict[str, Any],
116 annotations: Dict[str, str] = dict(),
117+ overrides: Optional[Dict[str, Dict[str, str]]] = None,
118+ require_overrides: bool = False,
119 ) -> str:
120 """
121 Parse each requirement from the requirements_lock file, and prepare arguments for each
122@@ -131,6 +133,13 @@ def generate_parsed_requirements_contents(
123 _packages = {repo_names_and_reqs}
124 _config = {args}
125 _annotations = {annotations}
126+ _overrides = {overrides}
127+ _require_overrides = {require_overrides}
128+
129+ _NOP_OVERRIDE = {{
130+ "url": None,
131+ "sha256": None,
132+ }}
133
134 def _clean_name(name):
135 return name.replace("-", "_").replace(".", "_").lower()
136@@ -160,10 +169,20 @@ def generate_parsed_requirements_contents(
137
138 def install_deps():
139 for name, requirement in _packages:
140+ override_name = requirement.split(" ")[0]
141+ override = _overrides.get(override_name)
142+ if not override:
143+ if _require_overrides:
144+ fail("Failed to find an override for \\"{{}}\\" in the \\"overrides\\" JSON file".format(override_name))
145+ else:
146+ override = _NOP_OVERRIDE
147+
148 whl_library(
149 name = name,
150 requirement = requirement,
151 annotation = _get_annotation(requirement),
152+ url = override["url"],
153+ sha256 = override["sha256"],
154 **_config
155 )
156 """.format(
157@@ -178,6 +197,8 @@ def generate_parsed_requirements_contents(
158 repo_names_and_reqs=repo_names_and_reqs,
159 repo_prefix=repo_prefix,
160 wheel_file_label=bazel.WHEEL_FILE_LABEL,
161+ overrides=overrides or {},
162+ require_overrides=require_overrides,
163 )
164 )
165
166@@ -234,6 +255,16 @@ If set, it will take precedence over python_interpreter.",
167 type=annotation.annotations_map_from_str_path,
168 help="A json encoded file containing annotations for rendered packages.",
169 )
170+ parser.add_argument(
171+ "--overrides",
172+ type=Path,
173+ help="A json encoded file containing URL overrides for packages.",
174+ )
175+ parser.add_argument(
176+ "--require-overrides",
177+ action="store_true",
178+ help="If set, requires that every requirement has a URL override in the --overrides JSON file.",
179+ )
180 arguments.parse_common_args(parser)
181 args = parser.parse_args()
182
183@@ -259,6 +290,11 @@ If set, it will take precedence over python_interpreter.",
184 }
185 )
186
187+ if args.overrides:
188+ overrides = json.loads(args.overrides.read_text())
189+ else:
190+ overrides = None
191+
192 output.write(
193 textwrap.dedent(
194 """\
195@@ -278,6 +314,8 @@ If set, it will take precedence over python_interpreter.",
196 repo_prefix=args.repo_prefix,
197 whl_library_args=whl_library_args,
198 annotations=annotated_requirements,
199+ overrides=overrides,
200+ require_overrides=args.require_overrides,
201 )
202 )
203
204diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
205index d729ae9..afe3102 100644
206--- a/python/pip_install/pip_repository.bzl
207+++ b/python/pip_install/pip_repository.bzl
208@@ -257,6 +257,11 @@ def _pip_repository_impl(rctx):
209 args += ["--python_interpreter", _get_python_interpreter_attr(rctx)]
210 if rctx.attr.python_interpreter_target:
211 args += ["--python_interpreter_target", str(rctx.attr.python_interpreter_target)]
212+ if rctx.attr.overrides:
213+ overrides_file = rctx.path(rctx.attr.overrides).realpath
214+ args += ["--overrides", overrides_file]
215+ if rctx.attr.require_overrides:
216+ args += ["--require-overrides"]
217 progress_message = "Parsing requirements to starlark"
218 else:
219 args = [
220@@ -391,6 +396,14 @@ pip_repository_attrs = {
221 default = False,
222 doc = "Create the repository in incremental mode.",
223 ),
224+ "overrides": attr.label(
225+ allow_single_file = True,
226+ doc = "A JSON file containing overrides. TBD",
227+ ),
228+ "require_overrides": attr.bool(
229+ default = False,
230+ doc = "If True, every requirement must have an entry in the \"overrides\" JSON file.",
231+ ),
232 "requirements": attr.label(
233 allow_single_file = True,
234 doc = "A 'requirements.txt' pip requirements file.",
235@@ -483,6 +496,16 @@ def _whl_library_impl(rctx):
236 "--annotation",
237 rctx.path(rctx.attr.annotation),
238 ])
239+ if rctx.attr.url:
240+ basename = rctx.attr.url.split("/")[-1]
241+ download_result = rctx.download(
242+ output = basename,
243+ url = rctx.attr.url,
244+ sha256 = rctx.attr.sha256 or None,
245+ )
246+ if not download_result.success:
247+ fail("Failed to download {}".format(rctx.attr.url))
248+ args.append("--pre-downloaded")
249
250 args = _parse_optional_attrs(rctx, args)
251
252@@ -515,6 +538,17 @@ whl_library_attrs = {
253 mandatory = True,
254 doc = "Python requirement string describing the package to make available",
255 ),
256+ "url": attr.string(
257+ doc = (
258+ "Set this to download the package from the specified URL instead of using pip. "
259+ ),
260+ ),
261+ "sha256": attr.string(
262+ doc = (
263+ "Optionally set this when using the 'url' parameter. " +
264+ "Must be the expected checksum of the downloaded file."
265+ ),
266+ )
267 }
268
269 whl_library_attrs.update(**common_attrs)