Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_internal/build_env.py: 30%
132 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
1"""Build Environment used for isolation during sdist building
2"""
4import logging
5import os
6import pathlib
7import site
8import sys
9import textwrap
10from collections import OrderedDict
11from types import TracebackType
12from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
14from pip._vendor.certifi import where
15from pip._vendor.packaging.requirements import Requirement
16from pip._vendor.packaging.version import Version
18from pip import __file__ as pip_location
19from pip._internal.cli.spinners import open_spinner
20from pip._internal.locations import get_platlib, get_purelib, get_scheme
21from pip._internal.metadata import get_default_environment, get_environment
22from pip._internal.utils.subprocess import call_subprocess
23from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
25if TYPE_CHECKING:
26 from pip._internal.index.package_finder import PackageFinder
28logger = logging.getLogger(__name__)
31def _dedup(a: str, b: str) -> Union[Tuple[str], Tuple[str, str]]:
32 return (a, b) if a != b else (a,)
35class _Prefix:
36 def __init__(self, path: str) -> None:
37 self.path = path
38 self.setup = False
39 scheme = get_scheme("", prefix=path)
40 self.bin_dir = scheme.scripts
41 self.lib_dirs = _dedup(scheme.purelib, scheme.platlib)
44def get_runnable_pip() -> str:
45 """Get a file to pass to a Python executable, to run the currently-running pip.
47 This is used to run a pip subprocess, for installing requirements into the build
48 environment.
49 """
50 source = pathlib.Path(pip_location).resolve().parent
52 if not source.is_dir():
53 # This would happen if someone is using pip from inside a zip file. In that
54 # case, we can use that directly.
55 return str(source)
57 return os.fsdecode(source / "__pip-runner__.py")
60def _get_system_sitepackages() -> Set[str]:
61 """Get system site packages
63 Usually from site.getsitepackages,
64 but fallback on `get_purelib()/get_platlib()` if unavailable
65 (e.g. in a virtualenv created by virtualenv<20)
67 Returns normalized set of strings.
68 """
69 if hasattr(site, "getsitepackages"):
70 system_sites = site.getsitepackages()
71 else:
72 # virtualenv < 20 overwrites site.py without getsitepackages
73 # fallback on get_purelib/get_platlib.
74 # this is known to miss things, but shouldn't in the cases
75 # where getsitepackages() has been removed (inside a virtualenv)
76 system_sites = [get_purelib(), get_platlib()]
77 return {os.path.normcase(path) for path in system_sites}
80class BuildEnvironment:
81 """Creates and manages an isolated environment to install build deps"""
83 def __init__(self) -> None:
84 temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
86 self._prefixes = OrderedDict(
87 (name, _Prefix(os.path.join(temp_dir.path, name)))
88 for name in ("normal", "overlay")
89 )
91 self._bin_dirs: List[str] = []
92 self._lib_dirs: List[str] = []
93 for prefix in reversed(list(self._prefixes.values())):
94 self._bin_dirs.append(prefix.bin_dir)
95 self._lib_dirs.extend(prefix.lib_dirs)
97 # Customize site to:
98 # - ensure .pth files are honored
99 # - prevent access to system site packages
100 system_sites = _get_system_sitepackages()
102 self._site_dir = os.path.join(temp_dir.path, "site")
103 if not os.path.exists(self._site_dir):
104 os.mkdir(self._site_dir)
105 with open(
106 os.path.join(self._site_dir, "sitecustomize.py"), "w", encoding="utf-8"
107 ) as fp:
108 fp.write(
109 textwrap.dedent(
110 """
111 import os, site, sys
113 # First, drop system-sites related paths.
114 original_sys_path = sys.path[:]
115 known_paths = set()
116 for path in {system_sites!r}:
117 site.addsitedir(path, known_paths=known_paths)
118 system_paths = set(
119 os.path.normcase(path)
120 for path in sys.path[len(original_sys_path):]
121 )
122 original_sys_path = [
123 path for path in original_sys_path
124 if os.path.normcase(path) not in system_paths
125 ]
126 sys.path = original_sys_path
128 # Second, add lib directories.
129 # ensuring .pth file are processed.
130 for path in {lib_dirs!r}:
131 assert not path in sys.path
132 site.addsitedir(path)
133 """
134 ).format(system_sites=system_sites, lib_dirs=self._lib_dirs)
135 )
137 def __enter__(self) -> None:
138 self._save_env = {
139 name: os.environ.get(name, None)
140 for name in ("PATH", "PYTHONNOUSERSITE", "PYTHONPATH")
141 }
143 path = self._bin_dirs[:]
144 old_path = self._save_env["PATH"]
145 if old_path:
146 path.extend(old_path.split(os.pathsep))
148 pythonpath = [self._site_dir]
150 os.environ.update(
151 {
152 "PATH": os.pathsep.join(path),
153 "PYTHONNOUSERSITE": "1",
154 "PYTHONPATH": os.pathsep.join(pythonpath),
155 }
156 )
158 def __exit__(
159 self,
160 exc_type: Optional[Type[BaseException]],
161 exc_val: Optional[BaseException],
162 exc_tb: Optional[TracebackType],
163 ) -> None:
164 for varname, old_value in self._save_env.items():
165 if old_value is None:
166 os.environ.pop(varname, None)
167 else:
168 os.environ[varname] = old_value
170 def check_requirements(
171 self, reqs: Iterable[str]
172 ) -> Tuple[Set[Tuple[str, str]], Set[str]]:
173 """Return 2 sets:
174 - conflicting requirements: set of (installed, wanted) reqs tuples
175 - missing requirements: set of reqs
176 """
177 missing = set()
178 conflicting = set()
179 if reqs:
180 env = (
181 get_environment(self._lib_dirs)
182 if hasattr(self, "_lib_dirs")
183 else get_default_environment()
184 )
185 for req_str in reqs:
186 req = Requirement(req_str)
187 # We're explicitly evaluating with an empty extra value, since build
188 # environments are not provided any mechanism to select specific extras.
189 if req.marker is not None and not req.marker.evaluate({"extra": ""}):
190 continue
191 dist = env.get_distribution(req.name)
192 if not dist:
193 missing.add(req_str)
194 continue
195 if isinstance(dist.version, Version):
196 installed_req_str = f"{req.name}=={dist.version}"
197 else:
198 installed_req_str = f"{req.name}==={dist.version}"
199 if not req.specifier.contains(dist.version, prereleases=True):
200 conflicting.add((installed_req_str, req_str))
201 # FIXME: Consider direct URL?
202 return conflicting, missing
204 def install_requirements(
205 self,
206 finder: "PackageFinder",
207 requirements: Iterable[str],
208 prefix_as_string: str,
209 *,
210 kind: str,
211 ) -> None:
212 prefix = self._prefixes[prefix_as_string]
213 assert not prefix.setup
214 prefix.setup = True
215 if not requirements:
216 return
217 self._install_requirements(
218 get_runnable_pip(),
219 finder,
220 requirements,
221 prefix,
222 kind=kind,
223 )
225 @staticmethod
226 def _install_requirements(
227 pip_runnable: str,
228 finder: "PackageFinder",
229 requirements: Iterable[str],
230 prefix: _Prefix,
231 *,
232 kind: str,
233 ) -> None:
234 args: List[str] = [
235 sys.executable,
236 pip_runnable,
237 "install",
238 "--ignore-installed",
239 "--no-user",
240 "--prefix",
241 prefix.path,
242 "--no-warn-script-location",
243 ]
244 if logger.getEffectiveLevel() <= logging.DEBUG:
245 args.append("-v")
246 for format_control in ("no_binary", "only_binary"):
247 formats = getattr(finder.format_control, format_control)
248 args.extend(
249 (
250 "--" + format_control.replace("_", "-"),
251 ",".join(sorted(formats or {":none:"})),
252 )
253 )
255 index_urls = finder.index_urls
256 if index_urls:
257 args.extend(["-i", index_urls[0]])
258 for extra_index in index_urls[1:]:
259 args.extend(["--extra-index-url", extra_index])
260 else:
261 args.append("--no-index")
262 for link in finder.find_links:
263 args.extend(["--find-links", link])
265 for host in finder.trusted_hosts:
266 args.extend(["--trusted-host", host])
267 if finder.allow_all_prereleases:
268 args.append("--pre")
269 if finder.prefer_binary:
270 args.append("--prefer-binary")
271 args.append("--")
272 args.extend(requirements)
273 extra_environ = {"_PIP_STANDALONE_CERT": where()}
274 with open_spinner(f"Installing {kind}") as spinner:
275 call_subprocess(
276 args,
277 command_desc=f"pip subprocess to install {kind}",
278 spinner=spinner,
279 extra_environ=extra_environ,
280 )
283class NoOpBuildEnvironment(BuildEnvironment):
284 """A no-op drop-in replacement for BuildEnvironment"""
286 def __init__(self) -> None:
287 pass
289 def __enter__(self) -> None:
290 pass
292 def __exit__(
293 self,
294 exc_type: Optional[Type[BaseException]],
295 exc_val: Optional[BaseException],
296 exc_tb: Optional[TracebackType],
297 ) -> None:
298 pass
300 def cleanup(self) -> None:
301 pass
303 def install_requirements(
304 self,
305 finder: "PackageFinder",
306 requirements: Iterable[str],
307 prefix_as_string: str,
308 *,
309 kind: str,
310 ) -> None:
311 raise NotImplementedError()