Skip to content

Commit 9d7aabc

Browse files
authored
fix: pdm add requests pip-system-certs fails nondeterministically (#2765)
* fix: `pdm add requests pip-system-certs` fails nondeterministically Fixes #2762 Signed-off-by: Frost Ming <[email protected]> * fix pth files after done Signed-off-by: Frost Ming <[email protected]> --------- Signed-off-by: Frost Ming <[email protected]>
1 parent f1f50a0 commit 9d7aabc

File tree

4 files changed

+58
-25
lines changed

4 files changed

+58
-25
lines changed

news/2762.bugfix.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a race condition where pth files take effect when multiple packages are installed in parallel.

src/pdm/installers/installers.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from installer import install as _install
1111
from installer._core import _process_WHEEL_file
12-
from installer.destinations import SchemeDictionaryDestination
12+
from installer.destinations import SchemeDictionaryDestination, WheelDestination
1313
from installer.exceptions import InvalidWheelSource
1414
from installer.records import RecordEntry
1515
from installer.sources import WheelContentElement, WheelSource
@@ -97,12 +97,14 @@ def __init__(
9797
self,
9898
*args: Any,
9999
link_method: LinkMethod = "copy",
100+
rename_pth: bool = False,
100101
**kwargs: Any,
101102
) -> None:
102103
super().__init__(*args, **kwargs)
103104
self.link_method = link_method
105+
self.rename_pth = rename_pth
104106

105-
def write_to_fs(self, scheme: Scheme, path: str | Path, stream: BinaryIO, is_executable: bool) -> RecordEntry:
107+
def write_to_fs(self, scheme: Scheme, path: str, stream: BinaryIO, is_executable: bool) -> RecordEntry:
106108
from installer.records import Hash
107109
from installer.utils import copyfileobj_with_hashing, make_file_executable
108110

@@ -112,6 +114,10 @@ def write_to_fs(self, scheme: Scheme, path: str | Path, stream: BinaryIO, is_exe
112114

113115
os.makedirs(os.path.dirname(target_path), exist_ok=True)
114116

117+
if self.rename_pth and target_path.endswith(".pth") and "/" not in path:
118+
# Postpone the creation of pth files since it may cause race condition
119+
# when multiple packages are installed at the same time.
120+
target_path += ".pdmtmp"
115121
if self.link_method == "copy" or not hasattr(stream, "name"):
116122
with open(target_path, "wb") as f:
117123
hash_, size = copyfileobj_with_hashing(stream, f, self.hash_algorithm)
@@ -147,6 +153,7 @@ def install_package(
147153
environment: BaseEnvironment,
148154
direct_url: dict[str, Any] | None = None,
149155
install_links: bool = True,
156+
rename_pth: bool = False,
150157
) -> str:
151158
"""Only create .pth files referring to the cached package.
152159
If the cache doesn't exist, create one.
@@ -175,6 +182,7 @@ def install_package(
175182
interpreter=interpreter,
176183
script_kind=script_kind,
177184
link_method=link_method,
185+
rename_pth=rename_pth,
178186
)
179187
source = PackageWheelSource(package)
180188
dist_info_dir = install(source, destination=destination, additional_metadata=additional_metadata)
@@ -184,7 +192,7 @@ def install_package(
184192

185193

186194
def install(
187-
source: WheelSource, destination: InstallDestination, additional_metadata: dict[str, bytes] | None = None
195+
source: WheelSource, destination: WheelDestination, additional_metadata: dict[str, bytes] | None = None
188196
) -> str:
189197
"""A lower level installation method that is copied from installer
190198
but is controlled by extra parameters.
@@ -198,7 +206,7 @@ def install(
198206

199207
def install_wheel(wheel: str, environment: BaseEnvironment, direct_url: dict[str, Any] | None = None) -> str:
200208
"""Install a wheel into the environment, return the .dist-info path"""
201-
destination = InstallDestination(
209+
destination = SchemeDictionaryDestination(
202210
scheme_dict=environment.get_paths(_get_dist_name(wheel)),
203211
interpreter=str(environment.interpreter.executable),
204212
script_kind=environment.script_kind,

src/pdm/installers/manager.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,22 @@ class InstallManager:
1919
# The packages below are needed to load paths and thus should not be cached.
2020
NO_CACHE_PACKAGES = ("editables",)
2121

22-
def __init__(self, environment: BaseEnvironment, *, use_install_cache: bool = False) -> None:
22+
def __init__(
23+
self, environment: BaseEnvironment, *, use_install_cache: bool = False, rename_pth: bool = False
24+
) -> None:
2325
self.environment = environment
2426
self.use_install_cache = use_install_cache
27+
self.rename_pth = rename_pth
2528

2629
def install(self, candidate: Candidate) -> Distribution:
2730
"""Install a candidate into the environment, return the distribution"""
2831
prepared = candidate.prepare(self.environment)
2932
dist_info = install_package(
30-
prepared.get_cached_package(), self.environment, prepared.direct_url(), self.use_install_cache
33+
prepared.get_cached_package(),
34+
self.environment,
35+
direct_url=prepared.direct_url(),
36+
install_links=self.use_install_cache,
37+
rename_pth=self.rename_pth,
3138
)
3239
return Distribution.at(dist_info)
3340

src/pdm/installers/synchronizers.py

+36-19
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def __init__(
122122
self.no_editable = no_editable
123123
self.install_self = install_self
124124
if use_install_cache is None:
125-
use_install_cache = environment.project.config["install.cache"]
125+
use_install_cache = bool(environment.project.config["install.cache"])
126126
self.use_install_cache = use_install_cache
127127
self.reinstall = reinstall
128128
self.only_keep = only_keep
@@ -177,7 +177,7 @@ def manager(self) -> InstallManager:
177177

178178
def get_manager(self) -> InstallManager:
179179
return self.environment.project.core.install_manager_class(
180-
self.environment, use_install_cache=self.use_install_cache
180+
self.environment, use_install_cache=self.use_install_cache, rename_pth=True
181181
)
182182

183183
@property
@@ -378,6 +378,19 @@ def _show_summary(self, packages: dict[str, list[str]]) -> None:
378378
else:
379379
self.ui.echo("All packages are synced to date, nothing to do.")
380380

381+
def _fix_pth_files(self) -> None:
382+
"""Remove the .pdmtmp suffix from the installed packages"""
383+
from pathlib import Path
384+
385+
lib_paths = self.environment.get_paths()
386+
for scheme in ["purelib", "platlib"]:
387+
for path in list(Path(lib_paths[scheme]).iterdir()):
388+
if path.suffix == ".pdmtmp":
389+
target_path = path.with_suffix("")
390+
if target_path.exists():
391+
target_path.unlink()
392+
path.rename(target_path)
393+
381394
def synchronize(self) -> None:
382395
to_add, to_update, to_remove = self.compare_with_working_set()
383396
to_do = {"remove": to_remove, "update": to_update, "add": to_add}
@@ -443,21 +456,25 @@ def update_progress(future: Future | DummyFuture, kind: str, key: str) -> None:
443456
state.errors.clear()
444457
live.console.print("Retry failed jobs")
445458

446-
if state.errors:
447-
if self.ui.verbosity < termui.Verbosity.DETAIL:
448-
live.console.print("\n[error]ERRORS[/]:")
449-
live.console.print("".join(state.errors), end="")
450-
raise InstallationError("Some package operations are not complete yet")
451-
452-
if self.install_self:
453-
self_key = self.self_key
454-
assert self_key
455-
self.candidates[self_key] = self.self_candidate
456-
word = "a" if self.no_editable else "an editable"
457-
live.console.print(f"Installing the project as {word} package...")
458-
if self_key in self.working_set:
459-
self.update_candidate(self_key, progress)
460-
else:
461-
self.install_candidate(self_key, progress)
459+
try:
460+
if state.errors:
461+
if self.ui.verbosity < termui.Verbosity.DETAIL:
462+
live.console.print("\n[error]ERRORS[/]:")
463+
live.console.print("".join(state.errors), end="")
464+
raise InstallationError("Some package operations are not complete yet")
465+
466+
if self.install_self:
467+
self_key = self.self_key
468+
assert self_key
469+
self.candidates[self_key] = self.self_candidate
470+
word = "a" if self.no_editable else "an editable"
471+
live.console.print(f"Installing the project as {word} package...")
472+
if self_key in self.working_set:
473+
self.update_candidate(self_key, progress)
474+
else:
475+
self.install_candidate(self_key, progress)
462476

463-
live.console.print(f"\n{termui.Emoji.POPPER} All complete!")
477+
live.console.print(f"\n{termui.Emoji.POPPER} All complete!")
478+
finally:
479+
# Now we remove the .pdmtmp suffix from the installed packages
480+
self._fix_pth_files()

0 commit comments

Comments
 (0)