2
2
3
3
import collections
4
4
import contextlib
5
+ import os
5
6
import pathlib
6
7
import sys
7
8
import tempfile
@@ -119,6 +120,7 @@ def build_project_metadata(
119
120
src_file : pathlib .Path ,
120
121
build_targets : tuple [str , ...],
121
122
* ,
123
+ upgrade_packages : tuple [str , ...] | None = None ,
122
124
attempt_static_parse : bool ,
123
125
isolated : bool ,
124
126
quiet : bool ,
@@ -159,7 +161,12 @@ def build_project_metadata(
159
161
return project_metadata
160
162
161
163
src_dir = src_file .parent
162
- with _create_project_builder (src_dir , isolated = isolated , quiet = quiet ) as builder :
164
+ with _create_project_builder (
165
+ src_dir ,
166
+ upgrade_packages = upgrade_packages ,
167
+ isolated = isolated ,
168
+ quiet = quiet ,
169
+ ) as builder :
163
170
metadata = _build_project_wheel_metadata (builder )
164
171
extras = tuple (metadata .get_all ("Provides-Extra" ) or ())
165
172
requirements = tuple (
@@ -180,9 +187,80 @@ def build_project_metadata(
180
187
)
181
188
182
189
190
+ @contextlib .contextmanager
191
+ def _env_var (
192
+ env_var_name : str ,
193
+ env_var_value : str ,
194
+ / ,
195
+ ) -> Iterator [None ]:
196
+ sentinel = object ()
197
+ original_pip_constraint = os .getenv (env_var_name , sentinel )
198
+ pip_constraint_was_unset = original_pip_constraint is sentinel
199
+
200
+ os .environ [env_var_name ] = env_var_value
201
+ try :
202
+ yield
203
+ finally :
204
+ if pip_constraint_was_unset :
205
+ del os .environ [env_var_name ]
206
+ return
207
+
208
+ # Assert here is necessary because MyPy can't infer type
209
+ # narrowing in the complex case.
210
+ assert isinstance (original_pip_constraint , str )
211
+ os .environ [env_var_name ] = original_pip_constraint
212
+
213
+
214
+ @contextlib .contextmanager
215
+ def _temporary_constraints_file_set_for_pip (
216
+ upgrade_packages : tuple [str , ...],
217
+ ) -> Iterator [None ]:
218
+ with tempfile .NamedTemporaryFile (
219
+ mode = "w+t" ,
220
+ delete = False , # FIXME: switch to `delete_on_close` in Python 3.12+
221
+ ) as tmpfile :
222
+ # NOTE: `delete_on_close=False` here (or rather `delete=False`,
223
+ # NOTE: temporarily) is important for cross-platform execution. It is
224
+ # NOTE: required on Windows so that the underlying `pip install`
225
+ # NOTE: invocation by pypa/build will be able to access the constraint
226
+ # NOTE: file via a subprocess and not fail installing it due to a
227
+ # NOTE: permission error related to this file handle still open in our
228
+ # NOTE: parent process. To achieve this, we `.close()` the file
229
+ # NOTE: descriptor before we hand off the control to the build frontend
230
+ # NOTE: and with `delete_on_close=False`, the
231
+ # NOTE: `tempfile.NamedTemporaryFile()` context manager does not remove
232
+ # NOTE: it from disk right away.
233
+ # NOTE: Due to support of versions below Python 3.12, we are forced to
234
+ # NOTE: temporarily resort to using `delete=False`, meaning that the CM
235
+ # NOTE: never attempts removing the file from disk, not even on exit.
236
+ # NOTE: So we do this manually until we can migrate to using the more
237
+ # NOTE: ergonomic argument `delete_on_close=False`.
238
+
239
+ # Write packages to upgrade to a temporary file to set as
240
+ # constraints for the installation to the builder environment,
241
+ # in case build requirements are among them
242
+ tmpfile .write ("\n " .join (upgrade_packages ))
243
+
244
+ # FIXME: replace `delete` with `delete_on_close` in Python 3.12+
245
+ # FIXME: and replace `.close()` with `.flush()`
246
+ tmpfile .close ()
247
+
248
+ try :
249
+ with _env_var ("PIP_CONSTRAINT" , tmpfile .name ):
250
+ yield
251
+ finally :
252
+ # FIXME: replace `delete` with `delete_on_close` in Python 3.12+
253
+ # FIXME: and drop this manual deletion
254
+ os .unlink (tmpfile .name )
255
+
256
+
183
257
@contextlib .contextmanager
184
258
def _create_project_builder (
185
- src_dir : pathlib .Path , * , isolated : bool , quiet : bool
259
+ src_dir : pathlib .Path ,
260
+ * ,
261
+ upgrade_packages : tuple [str , ...] | None = None ,
262
+ isolated : bool ,
263
+ quiet : bool ,
186
264
) -> Iterator [build .ProjectBuilder ]:
187
265
if quiet :
188
266
runner = pyproject_hooks .quiet_subprocess_runner
@@ -193,7 +271,13 @@ def _create_project_builder(
193
271
yield build .ProjectBuilder (src_dir , runner = runner )
194
272
return
195
273
196
- with build .env .DefaultIsolatedEnv () as env :
274
+ maybe_pip_constrained_context = (
275
+ contextlib .nullcontext ()
276
+ if upgrade_packages is None
277
+ else _temporary_constraints_file_set_for_pip (upgrade_packages )
278
+ )
279
+
280
+ with maybe_pip_constrained_context , build .env .DefaultIsolatedEnv () as env :
197
281
builder = build .ProjectBuilder .from_isolated_env (env , src_dir , runner )
198
282
env .install (builder .build_system_requires )
199
283
env .install (builder .get_requires_for_build ("wheel" ))
0 commit comments