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, tempdir_kinds
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(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
41
42 self._prefixes = OrderedDict(
43 (name, Prefix(os.path.join(temp_dir.path, name)))
44 for name in ("normal", "overlay")
45 )
46
47 self._bin_dirs: list[str] = []
48 self.lib_dirs: list[str] = []
49 for prefix in reversed(list(self._prefixes.values())):
50 self._bin_dirs.append(prefix.bin_dir)
51 self.lib_dirs.extend(prefix.lib_dirs)
52
53 # Customize site to:
54 # - ensure .pth files are honored
55 # - prevent access to system site packages
56 system_sites = get_system_sitepackages()
57
58 self._site_dir = os.path.join(temp_dir.path, "site")
59 if not os.path.exists(self._site_dir):
60 os.mkdir(self._site_dir)
61 with open(
62 os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
63 ) as fp:
64 fp.write(textwrap.dedent("""
65 import os, site, sys
66
67 # First, discover all system-sites related paths.
68 original_sys_path = sys.path[:]
69 # Clear sys.path so addsitedir() will add system site paths and paths
70 # added by contained .pth files to sys.path reliably. This is necessary
71 # since Python 3.15, which notably no longer re-executes .pth files for
72 # known paths.
73 sys.path = []
74 known_paths = set()
75 for path in {system_sites!r}:
76 site.addsitedir(path, known_paths=known_paths)
77 system_paths = set(os.path.normcase(path) for path in sys.path)
78
79 # Drop discovered system-sites related paths.
80 original_sys_path = [
81 path for path in original_sys_path
82 if os.path.normcase(path) not in system_paths
83 ]
84 sys.path = original_sys_path
85
86 # Second, add lib directories.
87 # ensuring .pth file are processed.
88 for path in {lib_dirs!r}:
89 assert not path in sys.path
90 site.addsitedir(path)
91 """).format(system_sites=system_sites, lib_dirs=self.lib_dirs))
92
93 def __enter__(self) -> None:
94 self._save_env = {
95 name: os.environ.get(name, None)
96 for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
97 }
98
99 path = self._bin_dirs[:]
100 old_path = self._save_env["PATH"]
101 if old_path:
102 path.extend(old_path.split(os.pathsep))
103
104 pythonpath = [self._site_dir]
105
106 os.environ.update(
107 {
108 "PATH": os.pathsep.join(path),
109 "PYTHONNOUSERSITE": "1",
110 "PYTHONPATH": os.pathsep.join(pythonpath),
111 }
112 )
113
114 def __exit__(
115 self,
116 exc_type: type[BaseException] | None,
117 exc_val: BaseException | None,
118 exc_tb: TracebackType | None,
119 ) -> None:
120 for varname, old_value in self._save_env.items():
121 if old_value is None:
122 os.environ.pop(varname, None)
123 else:
124 os.environ[varname] = old_value
125
126 def install_requirements(
127 self,
128 requirements: Iterable[str],
129 prefix_as_string: str,
130 *,
131 kind: str,
132 for_req: InstallRequirement | None = None,
133 ) -> None:
134 prefix = self._prefixes[prefix_as_string]
135 assert not prefix.setup
136 prefix.setup = True
137 if not requirements:
138 return
139 self.installer.install(requirements, prefix, kind=kind, for_req=for_req)