1from __future__ import annotations
2
3import os
4import site
5import sys
6import textwrap
7from collections import OrderedDict
8from collections.abc import Iterable
9from types import TracebackType
10from typing import TYPE_CHECKING
11
12from pip._internal.build_env.base import (
13 BuildEnvironment,
14 BuildEnvironmentInstaller,
15 Prefix,
16)
17from pip._internal.utils.temp_dir import TempDirectory, TempDirectoryKind
18
19if TYPE_CHECKING:
20 from pip._internal.req.req_install import InstallRequirement
21
22
23def get_system_sitepackages() -> set[str]:
24 """Get system site packages, as a normalized set of strings."""
25 system_sites = site.getsitepackages()
26 return {os.path.normcase(path) for path in system_sites}
27
28
29class VirtualBuildEnvironment(BuildEnvironment):
30 """Legacy build environment implementation.
31
32 Patches sys.path and uses sitecustomize.py to isolate Python processes. It has
33 known bugs and weird edge cases, but it exists as a fallback for platforms where
34 the venv module is unavailable or dysfunctional.
35 """
36
37 def __init__(self, installer: BuildEnvironmentInstaller) -> None:
38 self.python_executable = sys.executable
39 self.installer = installer
40 temp_dir = TempDirectory(
41 kind=TempDirectoryKind.BUILD_ENV, globally_managed=True
42 )
43
44 self._prefixes = OrderedDict(
45 (name, Prefix(os.path.join(temp_dir.path, name)))
46 for name in ("normal", "overlay")
47 )
48
49 self._bin_dirs: list[str] = []
50 self.lib_dirs: list[str] = []
51 for prefix in reversed(list(self._prefixes.values())):
52 self._bin_dirs.append(prefix.bin_dir)
53 self.lib_dirs.extend(prefix.lib_dirs)
54
55 # Customize site to:
56 # - ensure .pth files are honored
57 # - prevent access to system site packages
58 system_sites = get_system_sitepackages()
59
60 self._site_dir = os.path.join(temp_dir.path, "site")
61 if not os.path.exists(self._site_dir):
62 os.mkdir(self._site_dir)
63 with open(
64 os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
65 ) as fp:
66 fp.write(textwrap.dedent("""
67 import os, site, sys
68
69 # First, discover all system-sites related paths.
70 original_sys_path = sys.path[:]
71 # Clear sys.path so addsitedir() will add system site paths and paths
72 # added by contained .pth files to sys.path reliably. This is necessary
73 # since Python 3.15, which notably no longer re-executes .pth files for
74 # known paths.
75 sys.path = []
76 known_paths = set()
77 for path in {system_sites!r}:
78 site.addsitedir(path, known_paths=known_paths)
79 system_paths = set(os.path.normcase(path) for path in sys.path)
80
81 # Drop discovered system-sites related paths.
82 original_sys_path = [
83 path for path in original_sys_path
84 if os.path.normcase(path) not in system_paths
85 ]
86 sys.path = original_sys_path
87
88 # Second, add lib directories.
89 # ensuring .pth file are processed.
90 for path in {lib_dirs!r}:
91 assert not path in sys.path
92 site.addsitedir(path)
93 """).format(system_sites=system_sites, lib_dirs=self.lib_dirs))
94
95 def __enter__(self) -> None:
96 self._save_env = {
97 name: os.environ.get(name, None)
98 for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
99 }
100
101 path = self._bin_dirs[:]
102 old_path = self._save_env["PATH"]
103 if old_path:
104 path.extend(old_path.split(os.pathsep))
105
106 pythonpath = [self._site_dir]
107
108 os.environ.update(
109 {
110 "PATH": os.pathsep.join(path),
111 "PYTHONNOUSERSITE": "1",
112 "PYTHONPATH": os.pathsep.join(pythonpath),
113 }
114 )
115
116 def __exit__(
117 self,
118 exc_type: type[BaseException] | None,
119 exc_val: BaseException | None,
120 exc_tb: TracebackType | None,
121 ) -> None:
122 for varname, old_value in self._save_env.items():
123 if old_value is None:
124 os.environ.pop(varname, None)
125 else:
126 os.environ[varname] = old_value
127
128 def install_requirements(
129 self,
130 requirements: Iterable[str],
131 prefix_as_string: str,
132 *,
133 kind: str,
134 for_req: InstallRequirement | None = None,
135 ) -> None:
136 prefix = self._prefixes[prefix_as_string]
137 assert not prefix.setup
138 prefix.setup = True
139 if not requirements:
140 return
141 self.installer.install(requirements, prefix, kind=kind, for_req=for_req)