Skip to content

Commit 0a0c878

Browse files
authored
Merge pull request #13541 from sbidoul/fix-wheel-cache-concurrency-sbi
Address wheel cache concurrency and performance when temp and cache are not on the same filesystem
2 parents 4dd68aa + 1d3a037 commit 0a0c878

File tree

6 files changed

+31
-23
lines changed

6 files changed

+31
-23
lines changed

news/13540.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Avoid concurrency issues and improve performance when storing wheels into the
2+
cache of locally built wheels, when the temporary build directory is on a
3+
different filesystem than the cache directory. To this end, the wheel directory
4+
passed to the build backend is now a temporary subdirectory inside the cache
5+
directory.

src/pip/_internal/operations/build/wheel.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,25 @@ def build_wheel_pep517(
1414
name: str,
1515
backend: BuildBackendHookCaller,
1616
metadata_directory: str,
17-
tempd: str,
17+
wheel_directory: str,
1818
) -> str | None:
1919
"""Build one InstallRequirement using the PEP 517 build process.
2020
2121
Returns path to wheel if successfully built. Otherwise, returns None.
2222
"""
2323
assert metadata_directory is not None
2424
try:
25-
logger.debug("Destination directory: %s", tempd)
25+
logger.debug("Destination directory: %s", wheel_directory)
2626

2727
runner = runner_with_spinner_message(
2828
f"Building wheel for {name} (pyproject.toml)"
2929
)
3030
with backend.subprocess_runner(runner):
3131
wheel_name = backend.build_wheel(
32-
tempd,
32+
wheel_directory=wheel_directory,
3333
metadata_directory=metadata_directory,
3434
)
3535
except Exception:
3636
logger.error("Failed building wheel for %s", name)
3737
return None
38-
return os.path.join(tempd, wheel_name)
38+
return os.path.join(wheel_directory, wheel_name)

src/pip/_internal/operations/build/wheel_editable.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ def build_wheel_editable(
1414
name: str,
1515
backend: BuildBackendHookCaller,
1616
metadata_directory: str,
17-
tempd: str,
17+
wheel_directory: str,
1818
) -> str | None:
1919
"""Build one InstallRequirement using the PEP 660 build process.
2020
2121
Returns path to wheel if successfully built. Otherwise, returns None.
2222
"""
2323
assert metadata_directory is not None
2424
try:
25-
logger.debug("Destination directory: %s", tempd)
25+
logger.debug("Destination directory: %s", wheel_directory)
2626

2727
runner = runner_with_spinner_message(
2828
f"Building editable for {name} (pyproject.toml)"
2929
)
3030
with backend.subprocess_runner(runner):
3131
try:
3232
wheel_name = backend.build_editable(
33-
tempd,
33+
wheel_directory=wheel_directory,
3434
metadata_directory=metadata_directory,
3535
)
3636
except HookMissing as e:
@@ -44,4 +44,4 @@ def build_wheel_editable(
4444
except Exception:
4545
logger.error("Failed building editable for %s", name)
4646
return None
47-
return os.path.join(tempd, wheel_name)
47+
return os.path.join(wheel_directory, wheel_name)

src/pip/_internal/operations/build/wheel_legacy.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def format_command_result(
3333

3434
def get_legacy_build_wheel_path(
3535
names: list[str],
36-
temp_dir: str,
36+
wheel_directory: str,
3737
name: str,
3838
command_args: list[str],
3939
command_output: str,
@@ -55,7 +55,7 @@ def get_legacy_build_wheel_path(
5555
msg += format_command_result(command_args, command_output)
5656
logger.warning(msg)
5757

58-
return os.path.join(temp_dir, names[0])
58+
return os.path.join(wheel_directory, names[0])
5959

6060

6161
def build_wheel_legacy(
@@ -64,7 +64,7 @@ def build_wheel_legacy(
6464
source_dir: str,
6565
global_options: list[str],
6666
build_options: list[str],
67-
tempd: str,
67+
wheel_directory: str,
6868
) -> str | None:
6969
"""Build one unpacked package using the "legacy" build process.
7070
@@ -89,12 +89,12 @@ def build_wheel_legacy(
8989
setup_py_path,
9090
global_options=global_options,
9191
build_options=build_options,
92-
destination_dir=tempd,
92+
destination_dir=wheel_directory,
9393
)
9494

9595
spin_message = f"Building wheel for {name} (setup.py)"
9696
with open_spinner(spin_message) as spinner:
97-
logger.debug("Destination directory: %s", tempd)
97+
logger.debug("Destination directory: %s", wheel_directory)
9898

9999
try:
100100
output = call_subprocess(
@@ -108,10 +108,10 @@ def build_wheel_legacy(
108108
logger.error("Failed building wheel for %s", name)
109109
return None
110110

111-
names = os.listdir(tempd)
111+
names = os.listdir(wheel_directory)
112112
wheel_path = get_legacy_build_wheel_path(
113113
names=names,
114-
temp_dir=tempd,
114+
wheel_directory=wheel_directory,
115115
name=name,
116116
command_args=wheel_args,
117117
command_output=output,

src/pip/_internal/wheel_builder.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import logging
66
import os.path
77
import re
8-
import shutil
98
from collections.abc import Iterable
9+
from tempfile import TemporaryDirectory
1010

1111
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
1212
from pip._vendor.packaging.version import InvalidVersion, Version
@@ -24,7 +24,6 @@
2424
from pip._internal.utils.misc import ensure_dir, hash_file
2525
from pip._internal.utils.setuptools_build import make_setuptools_clean_args
2626
from pip._internal.utils.subprocess import call_subprocess
27-
from pip._internal.utils.temp_dir import TempDirectory
2827
from pip._internal.utils.urls import path_to_url
2928
from pip._internal.vcs import vcs
3029

@@ -189,7 +188,7 @@ def _build_one_inside_env(
189188
global_options: list[str],
190189
editable: bool,
191190
) -> str | None:
192-
with TempDirectory(kind="wheel") as temp_dir:
191+
with TemporaryDirectory(dir=output_dir) as wheel_directory:
193192
assert req.name
194193
if req.use_pep517:
195194
assert req.metadata_directory
@@ -207,14 +206,14 @@ def _build_one_inside_env(
207206
name=req.name,
208207
backend=req.pep517_backend,
209208
metadata_directory=req.metadata_directory,
210-
tempd=temp_dir.path,
209+
wheel_directory=wheel_directory,
211210
)
212211
else:
213212
wheel_path = build_wheel_pep517(
214213
name=req.name,
215214
backend=req.pep517_backend,
216215
metadata_directory=req.metadata_directory,
217-
tempd=temp_dir.path,
216+
wheel_directory=wheel_directory,
218217
)
219218
else:
220219
wheel_path = build_wheel_legacy(
@@ -223,15 +222,19 @@ def _build_one_inside_env(
223222
source_dir=req.unpacked_source_directory,
224223
global_options=global_options,
225224
build_options=build_options,
226-
tempd=temp_dir.path,
225+
wheel_directory=wheel_directory,
227226
)
228227

229228
if wheel_path is not None:
230229
wheel_name = os.path.basename(wheel_path)
231230
dest_path = os.path.join(output_dir, wheel_name)
232231
try:
233232
wheel_hash, length = hash_file(wheel_path)
234-
shutil.move(wheel_path, dest_path)
233+
# We can do a replace here because wheel_path is guaranteed to
234+
# be in the same filesystem as output_dir. This will perform an
235+
# atomic rename, which is necessary to avoid concurrency issues
236+
# when populating the cache.
237+
os.replace(wheel_path, dest_path)
235238
logger.info(
236239
"Created wheel for %s: filename=%s size=%d sha256=%s",
237240
req.name,

tests/unit/test_wheel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def call_get_legacy_build_wheel_path(
4646
) -> str | None:
4747
wheel_path = get_legacy_build_wheel_path(
4848
names=names,
49-
temp_dir="/tmp/abcd",
49+
wheel_directory="/tmp/abcd",
5050
name="pendulum",
5151
command_args=["arg1", "arg2"],
5252
command_output="output line 1\noutput line 2\n",

0 commit comments

Comments
 (0)