You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

build_env.py 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """Build Environment used for isolation during sdist building
  2. """
  3. import logging
  4. import os
  5. import sys
  6. import textwrap
  7. from collections import OrderedDict
  8. from distutils.sysconfig import get_python_lib
  9. from sysconfig import get_paths
  10. from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet
  11. from pip import __file__ as pip_location
  12. from pip._internal.utils.misc import call_subprocess
  13. from pip._internal.utils.temp_dir import TempDirectory
  14. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  15. from pip._internal.utils.ui import open_spinner
  16. if MYPY_CHECK_RUNNING:
  17. from typing import Tuple, Set, Iterable, Optional, List # noqa: F401
  18. from pip._internal.index import PackageFinder # noqa: F401
  19. logger = logging.getLogger(__name__)
  20. class _Prefix:
  21. def __init__(self, path):
  22. # type: (str) -> None
  23. self.path = path
  24. self.setup = False
  25. self.bin_dir = get_paths(
  26. 'nt' if os.name == 'nt' else 'posix_prefix',
  27. vars={'base': path, 'platbase': path}
  28. )['scripts']
  29. # Note: prefer distutils' sysconfig to get the
  30. # library paths so PyPy is correctly supported.
  31. purelib = get_python_lib(plat_specific=False, prefix=path)
  32. platlib = get_python_lib(plat_specific=True, prefix=path)
  33. if purelib == platlib:
  34. self.lib_dirs = [purelib]
  35. else:
  36. self.lib_dirs = [purelib, platlib]
  37. class BuildEnvironment(object):
  38. """Creates and manages an isolated environment to install build deps
  39. """
  40. def __init__(self):
  41. # type: () -> None
  42. self._temp_dir = TempDirectory(kind="build-env")
  43. self._temp_dir.create()
  44. self._prefixes = OrderedDict((
  45. (name, _Prefix(os.path.join(self._temp_dir.path, name)))
  46. for name in ('normal', 'overlay')
  47. ))
  48. self._bin_dirs = [] # type: List[str]
  49. self._lib_dirs = [] # type: List[str]
  50. for prefix in reversed(list(self._prefixes.values())):
  51. self._bin_dirs.append(prefix.bin_dir)
  52. self._lib_dirs.extend(prefix.lib_dirs)
  53. # Customize site to:
  54. # - ensure .pth files are honored
  55. # - prevent access to system site packages
  56. system_sites = {
  57. os.path.normcase(site) for site in (
  58. get_python_lib(plat_specific=False),
  59. get_python_lib(plat_specific=True),
  60. )
  61. }
  62. self._site_dir = os.path.join(self._temp_dir.path, 'site')
  63. if not os.path.exists(self._site_dir):
  64. os.mkdir(self._site_dir)
  65. with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
  66. fp.write(textwrap.dedent(
  67. '''
  68. import os, site, sys
  69. # First, drop system-sites related paths.
  70. original_sys_path = sys.path[:]
  71. known_paths = set()
  72. for path in {system_sites!r}:
  73. site.addsitedir(path, known_paths=known_paths)
  74. system_paths = set(
  75. os.path.normcase(path)
  76. for path in sys.path[len(original_sys_path):]
  77. )
  78. original_sys_path = [
  79. path for path in original_sys_path
  80. if os.path.normcase(path) not in system_paths
  81. ]
  82. sys.path = original_sys_path
  83. # Second, add lib directories.
  84. # ensuring .pth file are processed.
  85. for path in {lib_dirs!r}:
  86. assert not path in sys.path
  87. site.addsitedir(path)
  88. '''
  89. ).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
  90. def __enter__(self):
  91. self._save_env = {
  92. name: os.environ.get(name, None)
  93. for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
  94. }
  95. path = self._bin_dirs[:]
  96. old_path = self._save_env['PATH']
  97. if old_path:
  98. path.extend(old_path.split(os.pathsep))
  99. pythonpath = [self._site_dir]
  100. os.environ.update({
  101. 'PATH': os.pathsep.join(path),
  102. 'PYTHONNOUSERSITE': '1',
  103. 'PYTHONPATH': os.pathsep.join(pythonpath),
  104. })
  105. def __exit__(self, exc_type, exc_val, exc_tb):
  106. for varname, old_value in self._save_env.items():
  107. if old_value is None:
  108. os.environ.pop(varname, None)
  109. else:
  110. os.environ[varname] = old_value
  111. def cleanup(self):
  112. # type: () -> None
  113. self._temp_dir.cleanup()
  114. def check_requirements(self, reqs):
  115. # type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
  116. """Return 2 sets:
  117. - conflicting requirements: set of (installed, wanted) reqs tuples
  118. - missing requirements: set of reqs
  119. """
  120. missing = set()
  121. conflicting = set()
  122. if reqs:
  123. ws = WorkingSet(self._lib_dirs)
  124. for req in reqs:
  125. try:
  126. if ws.find(Requirement.parse(req)) is None:
  127. missing.add(req)
  128. except VersionConflict as e:
  129. conflicting.add((str(e.args[0].as_requirement()),
  130. str(e.args[1])))
  131. return conflicting, missing
  132. def install_requirements(
  133. self,
  134. finder, # type: PackageFinder
  135. requirements, # type: Iterable[str]
  136. prefix_as_string, # type: str
  137. message # type: Optional[str]
  138. ):
  139. # type: (...) -> None
  140. prefix = self._prefixes[prefix_as_string]
  141. assert not prefix.setup
  142. prefix.setup = True
  143. if not requirements:
  144. return
  145. args = [
  146. sys.executable, os.path.dirname(pip_location), 'install',
  147. '--ignore-installed', '--no-user', '--prefix', prefix.path,
  148. '--no-warn-script-location',
  149. ] # type: List[str]
  150. if logger.getEffectiveLevel() <= logging.DEBUG:
  151. args.append('-v')
  152. for format_control in ('no_binary', 'only_binary'):
  153. formats = getattr(finder.format_control, format_control)
  154. args.extend(('--' + format_control.replace('_', '-'),
  155. ','.join(sorted(formats or {':none:'}))))
  156. if finder.index_urls:
  157. args.extend(['-i', finder.index_urls[0]])
  158. for extra_index in finder.index_urls[1:]:
  159. args.extend(['--extra-index-url', extra_index])
  160. else:
  161. args.append('--no-index')
  162. for link in finder.find_links:
  163. args.extend(['--find-links', link])
  164. for _, host, _ in finder.secure_origins:
  165. args.extend(['--trusted-host', host])
  166. if finder.allow_all_prereleases:
  167. args.append('--pre')
  168. args.append('--')
  169. args.extend(requirements)
  170. with open_spinner(message) as spinner:
  171. call_subprocess(args, show_stdout=False, spinner=spinner)
  172. class NoOpBuildEnvironment(BuildEnvironment):
  173. """A no-op drop-in replacement for BuildEnvironment
  174. """
  175. def __init__(self):
  176. pass
  177. def __enter__(self):
  178. pass
  179. def __exit__(self, exc_type, exc_val, exc_tb):
  180. pass
  181. def cleanup(self):
  182. pass
  183. def install_requirements(self, finder, requirements, prefix, message):
  184. raise NotImplementedError()