build_env.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. """Build Environment used for isolation during sdist building
  2. """
  3. import contextlib
  4. import logging
  5. import os
  6. import pathlib
  7. import sys
  8. import textwrap
  9. import zipfile
  10. from collections import OrderedDict
  11. from sysconfig import get_paths
  12. from types import TracebackType
  13. from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type
  14. from pip._vendor.certifi import where
  15. from pip._vendor.packaging.requirements import Requirement
  16. from pip._vendor.packaging.version import Version
  17. from pip import __file__ as pip_location
  18. from pip._internal.cli.spinners import open_spinner
  19. from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib
  20. from pip._internal.metadata import get_environment
  21. from pip._internal.utils.subprocess import call_subprocess
  22. from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
  23. if TYPE_CHECKING:
  24. from pip._internal.index.package_finder import PackageFinder
  25. logger = logging.getLogger(__name__)
  26. class _Prefix:
  27. def __init__(self, path):
  28. # type: (str) -> None
  29. self.path = path
  30. self.setup = False
  31. self.bin_dir = get_paths(
  32. 'nt' if os.name == 'nt' else 'posix_prefix',
  33. vars={'base': path, 'platbase': path}
  34. )['scripts']
  35. self.lib_dirs = get_prefixed_libs(path)
  36. @contextlib.contextmanager
  37. def _create_standalone_pip() -> Iterator[str]:
  38. """Create a "standalone pip" zip file.
  39. The zip file's content is identical to the currently-running pip.
  40. It will be used to install requirements into the build environment.
  41. """
  42. source = pathlib.Path(pip_location).resolve().parent
  43. # Return the current instance if `source` is not a directory. We can't build
  44. # a zip from this, and it likely means the instance is already standalone.
  45. if not source.is_dir():
  46. yield str(source)
  47. return
  48. with TempDirectory(kind="standalone-pip") as tmp_dir:
  49. pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip")
  50. kwargs = {}
  51. if sys.version_info >= (3, 8):
  52. kwargs["strict_timestamps"] = False
  53. with zipfile.ZipFile(pip_zip, "w", **kwargs) as zf:
  54. for child in source.rglob("*"):
  55. zf.write(child, child.relative_to(source.parent).as_posix())
  56. yield os.path.join(pip_zip, "pip")
  57. class BuildEnvironment:
  58. """Creates and manages an isolated environment to install build deps
  59. """
  60. def __init__(self):
  61. # type: () -> None
  62. temp_dir = TempDirectory(
  63. kind=tempdir_kinds.BUILD_ENV, globally_managed=True
  64. )
  65. self._prefixes = OrderedDict(
  66. (name, _Prefix(os.path.join(temp_dir.path, name)))
  67. for name in ('normal', 'overlay')
  68. )
  69. self._bin_dirs = [] # type: List[str]
  70. self._lib_dirs = [] # type: List[str]
  71. for prefix in reversed(list(self._prefixes.values())):
  72. self._bin_dirs.append(prefix.bin_dir)
  73. self._lib_dirs.extend(prefix.lib_dirs)
  74. # Customize site to:
  75. # - ensure .pth files are honored
  76. # - prevent access to system site packages
  77. system_sites = {
  78. os.path.normcase(site) for site in (get_purelib(), get_platlib())
  79. }
  80. self._site_dir = os.path.join(temp_dir.path, 'site')
  81. if not os.path.exists(self._site_dir):
  82. os.mkdir(self._site_dir)
  83. with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
  84. fp.write(textwrap.dedent(
  85. '''
  86. import os, site, sys
  87. # First, drop system-sites related paths.
  88. original_sys_path = sys.path[:]
  89. known_paths = set()
  90. for path in {system_sites!r}:
  91. site.addsitedir(path, known_paths=known_paths)
  92. system_paths = set(
  93. os.path.normcase(path)
  94. for path in sys.path[len(original_sys_path):]
  95. )
  96. original_sys_path = [
  97. path for path in original_sys_path
  98. if os.path.normcase(path) not in system_paths
  99. ]
  100. sys.path = original_sys_path
  101. # Second, add lib directories.
  102. # ensuring .pth file are processed.
  103. for path in {lib_dirs!r}:
  104. assert not path in sys.path
  105. site.addsitedir(path)
  106. '''
  107. ).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
  108. def __enter__(self):
  109. # type: () -> None
  110. self._save_env = {
  111. name: os.environ.get(name, None)
  112. for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
  113. }
  114. path = self._bin_dirs[:]
  115. old_path = self._save_env['PATH']
  116. if old_path:
  117. path.extend(old_path.split(os.pathsep))
  118. pythonpath = [self._site_dir]
  119. os.environ.update({
  120. 'PATH': os.pathsep.join(path),
  121. 'PYTHONNOUSERSITE': '1',
  122. 'PYTHONPATH': os.pathsep.join(pythonpath),
  123. })
  124. def __exit__(
  125. self,
  126. exc_type, # type: Optional[Type[BaseException]]
  127. exc_val, # type: Optional[BaseException]
  128. exc_tb # type: Optional[TracebackType]
  129. ):
  130. # type: (...) -> None
  131. for varname, old_value in self._save_env.items():
  132. if old_value is None:
  133. os.environ.pop(varname, None)
  134. else:
  135. os.environ[varname] = old_value
  136. def check_requirements(self, reqs):
  137. # type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
  138. """Return 2 sets:
  139. - conflicting requirements: set of (installed, wanted) reqs tuples
  140. - missing requirements: set of reqs
  141. """
  142. missing = set()
  143. conflicting = set()
  144. if reqs:
  145. env = get_environment(self._lib_dirs)
  146. for req_str in reqs:
  147. req = Requirement(req_str)
  148. dist = env.get_distribution(req.name)
  149. if not dist:
  150. missing.add(req_str)
  151. continue
  152. if isinstance(dist.version, Version):
  153. installed_req_str = f"{req.name}=={dist.version}"
  154. else:
  155. installed_req_str = f"{req.name}==={dist.version}"
  156. if dist.version not in req.specifier:
  157. conflicting.add((installed_req_str, req_str))
  158. # FIXME: Consider direct URL?
  159. return conflicting, missing
  160. def install_requirements(
  161. self,
  162. finder, # type: PackageFinder
  163. requirements, # type: Iterable[str]
  164. prefix_as_string, # type: str
  165. message # type: str
  166. ):
  167. # type: (...) -> None
  168. prefix = self._prefixes[prefix_as_string]
  169. assert not prefix.setup
  170. prefix.setup = True
  171. if not requirements:
  172. return
  173. with contextlib.ExitStack() as ctx:
  174. # TODO: Remove this block when dropping 3.6 support. Python 3.6
  175. # lacks importlib.resources and pep517 has issues loading files in
  176. # a zip, so we fallback to the "old" method by adding the current
  177. # pip directory to the child process's sys.path.
  178. if sys.version_info < (3, 7):
  179. pip_runnable = os.path.dirname(pip_location)
  180. else:
  181. pip_runnable = ctx.enter_context(_create_standalone_pip())
  182. self._install_requirements(
  183. pip_runnable,
  184. finder,
  185. requirements,
  186. prefix,
  187. message,
  188. )
  189. @staticmethod
  190. def _install_requirements(
  191. pip_runnable: str,
  192. finder: "PackageFinder",
  193. requirements: Iterable[str],
  194. prefix: _Prefix,
  195. message: str,
  196. ) -> None:
  197. args = [
  198. sys.executable, pip_runnable, 'install',
  199. '--ignore-installed', '--no-user', '--prefix', prefix.path,
  200. '--no-warn-script-location',
  201. ] # type: List[str]
  202. if logger.getEffectiveLevel() <= logging.DEBUG:
  203. args.append('-v')
  204. for format_control in ('no_binary', 'only_binary'):
  205. formats = getattr(finder.format_control, format_control)
  206. args.extend(('--' + format_control.replace('_', '-'),
  207. ','.join(sorted(formats or {':none:'}))))
  208. index_urls = finder.index_urls
  209. if index_urls:
  210. args.extend(['-i', index_urls[0]])
  211. for extra_index in index_urls[1:]:
  212. args.extend(['--extra-index-url', extra_index])
  213. else:
  214. args.append('--no-index')
  215. for link in finder.find_links:
  216. args.extend(['--find-links', link])
  217. for host in finder.trusted_hosts:
  218. args.extend(['--trusted-host', host])
  219. if finder.allow_all_prereleases:
  220. args.append('--pre')
  221. if finder.prefer_binary:
  222. args.append('--prefer-binary')
  223. args.append('--')
  224. args.extend(requirements)
  225. extra_environ = {"_PIP_STANDALONE_CERT": where()}
  226. with open_spinner(message) as spinner:
  227. call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
  228. class NoOpBuildEnvironment(BuildEnvironment):
  229. """A no-op drop-in replacement for BuildEnvironment
  230. """
  231. def __init__(self):
  232. # type: () -> None
  233. pass
  234. def __enter__(self):
  235. # type: () -> None
  236. pass
  237. def __exit__(
  238. self,
  239. exc_type, # type: Optional[Type[BaseException]]
  240. exc_val, # type: Optional[BaseException]
  241. exc_tb # type: Optional[TracebackType]
  242. ):
  243. # type: (...) -> None
  244. pass
  245. def cleanup(self):
  246. # type: () -> None
  247. pass
  248. def install_requirements(
  249. self,
  250. finder, # type: PackageFinder
  251. requirements, # type: Iterable[str]
  252. prefix_as_string, # type: str
  253. message # type: str
  254. ):
  255. # type: (...) -> None
  256. raise NotImplementedError()