blob: af89eccd0377bb90ddd3633b98c4127f4d5febb8 [file] [log] [blame]
Philipp Schrader9ebb7062022-12-07 20:17:15 -08001From 662f59afaecd7ecff5bd5234c8bbd9c219b7f24f 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 Schrader9ebb7062022-12-07 20:17:15 -08009 python/pip_install/pip_repository.bzl | 38 ++++++++++++
10 3 files changed, 114 insertions(+), 28 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
Philipp Schrader9ebb7062022-12-07 20:17:15 -080013index ff64291..8742d25 100644
Philipp Schrader868070a2022-09-06 22:51:13 -070014--- a/python/pip_install/extract_wheels/extract_single_wheel.py
15+++ b/python/pip_install/extract_wheels/extract_single_wheel.py
Philipp Schrader9ebb7062022-12-07 20:17:15 -080016@@ -50,41 +50,47 @@ def main() -> None:
Philipp Schrader868070a2022-09-06 22:51:13 -070017 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 Schrader9ebb7062022-12-07 20:17:15 -080092index 686a57d..60936a9 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
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800104@@ -86,6 +86,8 @@ def parse_whl_library_args(args: argparse.Namespace) -> Dict[str, Any]:
105 "requirements_lock_label",
106 "annotations",
107 "bzlmod",
108+ "overrides",
109+ "require_overrides",
110 ):
Philipp Schrader868070a2022-09-06 22:51:13 -0700111 if arg in whl_library_args:
112 whl_library_args.pop(arg)
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800113@@ -100,6 +102,8 @@ def generate_parsed_requirements_contents(
Philipp Schrader868070a2022-09-06 22:51:13 -0700114 whl_library_args: Dict[str, Any],
115 annotations: Dict[str, str] = dict(),
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800116 bzlmod: bool = False,
Philipp Schrader868070a2022-09-06 22:51:13 -0700117+ 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
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800122@@ -133,10 +137,22 @@ def generate_parsed_requirements_contents(
123 whl_config = dict(_config)
124 whl_config.update(whl_library_kwargs)
Philipp Schrader868070a2022-09-06 22:51:13 -0700125 for name, requirement in _packages:
Philipp Schrader469a2f22022-10-29 14:25:58 -0700126+ override_entry = requirement.split(" ")[0]
127+ override_name, _, version = override_entry.partition("==")
128+ override_key = "%s==%s" % (_clean_name(override_name), version)
129+ override = _overrides.get(override_key)
Philipp Schrader868070a2022-09-06 22:51:13 -0700130+ if not override:
131+ if _require_overrides:
Philipp Schrader469a2f22022-10-29 14:25:58 -0700132+ fail("Failed to find an override for \\"{{}}\\" in the \\"overrides\\" JSON file".format(override_key))
Philipp Schrader868070a2022-09-06 22:51:13 -0700133+ else:
134+ override = _NOP_OVERRIDE
135+
136 whl_library(
137 name = name,
138 requirement = requirement,
139 annotation = _get_annotation(requirement),
140+ url = override["url"],
141+ sha256 = override["sha256"],
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800142 **whl_config
Philipp Schrader868070a2022-09-06 22:51:13 -0700143 )
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800144 """
145@@ -154,6 +170,13 @@ def generate_parsed_requirements_contents(
146 _config = {args}
147 _annotations = {annotations}
148 _bzlmod = {bzlmod}
149+ _overrides = {overrides}
150+ _require_overrides = {require_overrides}
151+
152+ _NOP_OVERRIDE = {{
153+ "url": None,
154+ "sha256": None,
155+ }}
156
157 def _clean_name(name):
158 return name.replace("-", "_").replace(".", "_").lower()
159@@ -204,6 +227,8 @@ def generate_parsed_requirements_contents(
Philipp Schrader868070a2022-09-06 22:51:13 -0700160 repo_prefix=repo_prefix,
161 wheel_file_label=bazel.WHEEL_FILE_LABEL,
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800162 bzlmod=bzlmod,
Philipp Schrader868070a2022-09-06 22:51:13 -0700163+ overrides=overrides or {},
164+ require_overrides=require_overrides,
165 )
166 )
167
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800168@@ -266,6 +291,16 @@ If set, it will take precedence over python_interpreter.",
169 default=False,
170 help="Whether this script is run under bzlmod. Under bzlmod we don't generate the install_deps() macro as it isn't needed.",
Philipp Schrader868070a2022-09-06 22:51:13 -0700171 )
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 Schrader9ebb7062022-12-07 20:17:15 -0800185@@ -291,6 +326,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 Schrader9ebb7062022-12-07 20:17:15 -0800197@@ -313,6 +353,8 @@ If set, it will take precedence over python_interpreter.",
Philipp Schrader868070a2022-09-06 22:51:13 -0700198 whl_library_args=whl_library_args,
199 annotations=annotated_requirements,
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800200 bzlmod=args.bzlmod,
Philipp Schrader868070a2022-09-06 22:51:13 -0700201+ 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
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800207index 7fbf503..5af0731 100644
Philipp Schrader868070a2022-09-06 22:51:13 -0700208--- a/python/pip_install/pip_repository.bzl
209+++ b/python/pip_install/pip_repository.bzl
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800210@@ -322,6 +322,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
221 args += ["--repo", rctx.attr.name, "--repo-prefix", rctx.attr.repo_prefix]
222@@ -447,6 +452,18 @@ pip_repository_attrs = {
223 we do not create the install_deps() macro.
224 """,
Philipp Schrader868070a2022-09-06 22:51:13 -0700225 ),
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+ ),
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800234+ "requirements": attr.label(
235+ allow_single_file = True,
236+ doc = "A 'requirements.txt' pip requirements file.",
237+ ),
238 "requirements_darwin": attr.label(
Philipp Schrader868070a2022-09-06 22:51:13 -0700239 allow_single_file = True,
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800240 doc = "Override the requirements_lock attribute when the host platform is Mac OS",
241@@ -535,6 +552,16 @@ def _whl_library_impl(rctx):
Philipp Schrader868070a2022-09-06 22:51:13 -0700242 "--annotation",
243 rctx.path(rctx.attr.annotation),
244 ])
245+ if rctx.attr.url:
246+ basename = rctx.attr.url.split("/")[-1]
247+ download_result = rctx.download(
248+ output = basename,
249+ url = rctx.attr.url,
250+ sha256 = rctx.attr.sha256 or None,
251+ )
252+ if not download_result.success:
253+ fail("Failed to download {}".format(rctx.attr.url))
254+ args.append("--pre-downloaded")
255
256 args = _parse_optional_attrs(rctx, args)
257
Philipp Schrader9ebb7062022-12-07 20:17:15 -0800258@@ -567,6 +594,17 @@ whl_library_attrs = {
Philipp Schrader868070a2022-09-06 22:51:13 -0700259 mandatory = True,
260 doc = "Python requirement string describing the package to make available",
261 ),
262+ "url": attr.string(
263+ doc = (
264+ "Set this to download the package from the specified URL instead of using pip. "
265+ ),
266+ ),
267+ "sha256": attr.string(
268+ doc = (
269+ "Optionally set this when using the 'url' parameter. " +
270+ "Must be the expected checksum of the downloaded file."
271+ ),
272+ )
273 }
274
275 whl_library_attrs.update(**common_attrs)