blob: eb6ac2ba159d3aca6e7c39b738a050e00ac74391 [file] [log] [blame]
Philipp Schrader469a2f22022-10-29 14:25:58 -07001From edb6d0447edf6b6116c8aff73cde6117abbaf706 Mon Sep 17 00:00:00 2001
Philipp Schrader868070a2022-09-06 22:51:13 -07002From: 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 ++++++++++---------
Philipp Schrader469a2f22022-10-29 14:25:58 -07008 .../parse_requirements_to_bzl.py | 44 +++++++++++++-
Philipp Schrader868070a2022-09-06 22:51:13 -07009 python/pip_install/pip_repository.bzl | 34 +++++++++++
Philipp Schrader469a2f22022-10-29 14:25:58 -070010 3 files changed, 109 insertions(+), 29 deletions(-)
Philipp Schrader868070a2022-09-06 22:51:13 -070011
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
Philipp Schrader469a2f22022-10-29 14:25:58 -070092index 5762cf5..46a94f5 100644
Philipp Schrader868070a2022-09-06 22:51:13 -070093--- 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()
Philipp Schrader469a2f22022-10-29 14:25:58 -0700136@@ -160,10 +169,22 @@ def generate_parsed_requirements_contents(
Philipp Schrader868070a2022-09-06 22:51:13 -0700137
138 def install_deps():
139 for name, requirement in _packages:
Philipp Schrader469a2f22022-10-29 14:25:58 -0700140+ override_entry = requirement.split(" ")[0]
141+ override_name, _, version = override_entry.partition("==")
142+ override_key = "%s==%s" % (_clean_name(override_name), version)
143+ override = _overrides.get(override_key)
Philipp Schrader868070a2022-09-06 22:51:13 -0700144+ if not override:
145+ if _require_overrides:
Philipp Schrader469a2f22022-10-29 14:25:58 -0700146+ fail("Failed to find an override for \\"{{}}\\" in the \\"overrides\\" JSON file".format(override_key))
Philipp Schrader868070a2022-09-06 22:51:13 -0700147+ else:
148+ override = _NOP_OVERRIDE
149+
150 whl_library(
151 name = name,
152 requirement = requirement,
153 annotation = _get_annotation(requirement),
154+ url = override["url"],
155+ sha256 = override["sha256"],
156 **_config
157 )
158 """.format(
Philipp Schrader469a2f22022-10-29 14:25:58 -0700159@@ -178,6 +199,8 @@ def generate_parsed_requirements_contents(
Philipp Schrader868070a2022-09-06 22:51:13 -0700160 repo_names_and_reqs=repo_names_and_reqs,
161 repo_prefix=repo_prefix,
162 wheel_file_label=bazel.WHEEL_FILE_LABEL,
163+ overrides=overrides or {},
164+ require_overrides=require_overrides,
165 )
166 )
167
Philipp Schrader469a2f22022-10-29 14:25:58 -0700168@@ -234,6 +257,16 @@ If set, it will take precedence over python_interpreter.",
Philipp Schrader868070a2022-09-06 22:51:13 -0700169 type=annotation.annotations_map_from_str_path,
170 help="A json encoded file containing annotations for rendered packages.",
171 )
172+ parser.add_argument(
173+ "--overrides",
174+ type=Path,
175+ help="A json encoded file containing URL overrides for packages.",
176+ )
177+ parser.add_argument(
178+ "--require-overrides",
179+ action="store_true",
180+ help="If set, requires that every requirement has a URL override in the --overrides JSON file.",
181+ )
182 arguments.parse_common_args(parser)
183 args = parser.parse_args()
184
Philipp Schrader469a2f22022-10-29 14:25:58 -0700185@@ -259,6 +292,11 @@ If set, it will take precedence over python_interpreter.",
Philipp Schrader868070a2022-09-06 22:51:13 -0700186 }
187 )
188
189+ if args.overrides:
190+ overrides = json.loads(args.overrides.read_text())
191+ else:
192+ overrides = None
193+
194 output.write(
195 textwrap.dedent(
196 """\
Philipp Schrader469a2f22022-10-29 14:25:58 -0700197@@ -278,6 +316,8 @@ If set, it will take precedence over python_interpreter.",
Philipp Schrader868070a2022-09-06 22:51:13 -0700198 repo_prefix=args.repo_prefix,
199 whl_library_args=whl_library_args,
200 annotations=annotated_requirements,
201+ overrides=overrides,
202+ require_overrides=args.require_overrides,
203 )
204 )
205
206diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl
207index d729ae9..afe3102 100644
208--- a/python/pip_install/pip_repository.bzl
209+++ b/python/pip_install/pip_repository.bzl
210@@ -257,6 +257,11 @@ def _pip_repository_impl(rctx):
211 args += ["--python_interpreter", _get_python_interpreter_attr(rctx)]
212 if rctx.attr.python_interpreter_target:
213 args += ["--python_interpreter_target", str(rctx.attr.python_interpreter_target)]
214+ if rctx.attr.overrides:
215+ overrides_file = rctx.path(rctx.attr.overrides).realpath
216+ args += ["--overrides", overrides_file]
217+ if rctx.attr.require_overrides:
218+ args += ["--require-overrides"]
219 progress_message = "Parsing requirements to starlark"
220 else:
221 args = [
222@@ -391,6 +396,14 @@ pip_repository_attrs = {
223 default = False,
224 doc = "Create the repository in incremental mode.",
225 ),
226+ "overrides": attr.label(
227+ allow_single_file = True,
228+ doc = "A JSON file containing overrides. TBD",
229+ ),
230+ "require_overrides": attr.bool(
231+ default = False,
232+ doc = "If True, every requirement must have an entry in the \"overrides\" JSON file.",
233+ ),
234 "requirements": attr.label(
235 allow_single_file = True,
236 doc = "A 'requirements.txt' pip requirements file.",
237@@ -483,6 +496,16 @@ def _whl_library_impl(rctx):
238 "--annotation",
239 rctx.path(rctx.attr.annotation),
240 ])
241+ if rctx.attr.url:
242+ basename = rctx.attr.url.split("/")[-1]
243+ download_result = rctx.download(
244+ output = basename,
245+ url = rctx.attr.url,
246+ sha256 = rctx.attr.sha256 or None,
247+ )
248+ if not download_result.success:
249+ fail("Failed to download {}".format(rctx.attr.url))
250+ args.append("--pre-downloaded")
251
252 args = _parse_optional_attrs(rctx, args)
253
254@@ -515,6 +538,17 @@ whl_library_attrs = {
255 mandatory = True,
256 doc = "Python requirement string describing the package to make available",
257 ),
258+ "url": attr.string(
259+ doc = (
260+ "Set this to download the package from the specified URL instead of using pip. "
261+ ),
262+ ),
263+ "sha256": attr.string(
264+ doc = (
265+ "Optionally set this when using the 'url' parameter. " +
266+ "Must be the expected checksum of the downloaded file."
267+ ),
268+ )
269 }
270
271 whl_library_attrs.update(**common_attrs)