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.

prepare.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. """Prepares a distribution for installation
  2. """
  3. import logging
  4. import os
  5. from pip._vendor import pkg_resources, requests
  6. from pip._internal.build_env import BuildEnvironment
  7. from pip._internal.download import (
  8. is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path,
  9. )
  10. from pip._internal.exceptions import (
  11. DirectoryUrlHashUnsupported, HashUnpinned, InstallationError,
  12. PreviousBuildDirError, VcsHashUnsupported,
  13. )
  14. from pip._internal.utils.compat import expanduser
  15. from pip._internal.utils.hashes import MissingHashes
  16. from pip._internal.utils.logging import indent_log
  17. from pip._internal.utils.misc import display_path, normalize_path
  18. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  19. from pip._internal.vcs import vcs
  20. if MYPY_CHECK_RUNNING:
  21. from typing import Any, Optional # noqa: F401
  22. from pip._internal.req.req_install import InstallRequirement # noqa: F401
  23. from pip._internal.index import PackageFinder # noqa: F401
  24. from pip._internal.download import PipSession # noqa: F401
  25. from pip._internal.req.req_tracker import RequirementTracker # noqa: F401
  26. logger = logging.getLogger(__name__)
  27. def make_abstract_dist(req):
  28. # type: (InstallRequirement) -> DistAbstraction
  29. """Factory to make an abstract dist object.
  30. Preconditions: Either an editable req with a source_dir, or satisfied_by or
  31. a wheel link, or a non-editable req with a source_dir.
  32. :return: A concrete DistAbstraction.
  33. """
  34. if req.editable:
  35. return IsSDist(req)
  36. elif req.link and req.link.is_wheel:
  37. return IsWheel(req)
  38. else:
  39. return IsSDist(req)
  40. class DistAbstraction(object):
  41. """Abstracts out the wheel vs non-wheel Resolver.resolve() logic.
  42. The requirements for anything installable are as follows:
  43. - we must be able to determine the requirement name
  44. (or we can't correctly handle the non-upgrade case).
  45. - we must be able to generate a list of run-time dependencies
  46. without installing any additional packages (or we would
  47. have to either burn time by doing temporary isolated installs
  48. or alternatively violate pips 'don't start installing unless
  49. all requirements are available' rule - neither of which are
  50. desirable).
  51. - for packages with setup requirements, we must also be able
  52. to determine their requirements without installing additional
  53. packages (for the same reason as run-time dependencies)
  54. - we must be able to create a Distribution object exposing the
  55. above metadata.
  56. """
  57. def __init__(self, req):
  58. # type: (InstallRequirement) -> None
  59. self.req = req # type: InstallRequirement
  60. def dist(self):
  61. # type: () -> Any
  62. """Return a setuptools Dist object."""
  63. raise NotImplementedError
  64. def prep_for_dist(self, finder, build_isolation):
  65. # type: (PackageFinder, bool) -> Any
  66. """Ensure that we can get a Dist for this requirement."""
  67. raise NotImplementedError
  68. class IsWheel(DistAbstraction):
  69. def dist(self):
  70. # type: () -> pkg_resources.Distribution
  71. return list(pkg_resources.find_distributions(
  72. self.req.source_dir))[0]
  73. def prep_for_dist(self, finder, build_isolation):
  74. # type: (PackageFinder, bool) -> Any
  75. # FIXME:https://github.com/pypa/pip/issues/1112
  76. pass
  77. class IsSDist(DistAbstraction):
  78. def dist(self):
  79. return self.req.get_dist()
  80. def prep_for_dist(self, finder, build_isolation):
  81. # type: (PackageFinder, bool) -> None
  82. # Prepare for building. We need to:
  83. # 1. Load pyproject.toml (if it exists)
  84. # 2. Set up the build environment
  85. self.req.load_pyproject_toml()
  86. should_isolate = self.req.use_pep517 and build_isolation
  87. def _raise_conflicts(conflicting_with, conflicting_reqs):
  88. raise InstallationError(
  89. "Some build dependencies for %s conflict with %s: %s." % (
  90. self.req, conflicting_with, ', '.join(
  91. '%s is incompatible with %s' % (installed, wanted)
  92. for installed, wanted in sorted(conflicting))))
  93. if should_isolate:
  94. # Isolate in a BuildEnvironment and install the build-time
  95. # requirements.
  96. self.req.build_env = BuildEnvironment()
  97. self.req.build_env.install_requirements(
  98. finder, self.req.pyproject_requires, 'overlay',
  99. "Installing build dependencies"
  100. )
  101. conflicting, missing = self.req.build_env.check_requirements(
  102. self.req.requirements_to_check
  103. )
  104. if conflicting:
  105. _raise_conflicts("PEP 517/518 supported requirements",
  106. conflicting)
  107. if missing:
  108. logger.warning(
  109. "Missing build requirements in pyproject.toml for %s.",
  110. self.req,
  111. )
  112. logger.warning(
  113. "The project does not specify a build backend, and "
  114. "pip cannot fall back to setuptools without %s.",
  115. " and ".join(map(repr, sorted(missing)))
  116. )
  117. # Install any extra build dependencies that the backend requests.
  118. # This must be done in a second pass, as the pyproject.toml
  119. # dependencies must be installed before we can call the backend.
  120. with self.req.build_env:
  121. # We need to have the env active when calling the hook.
  122. self.req.spin_message = "Getting requirements to build wheel"
  123. reqs = self.req.pep517_backend.get_requires_for_build_wheel()
  124. conflicting, missing = self.req.build_env.check_requirements(reqs)
  125. if conflicting:
  126. _raise_conflicts("the backend dependencies", conflicting)
  127. self.req.build_env.install_requirements(
  128. finder, missing, 'normal',
  129. "Installing backend dependencies"
  130. )
  131. self.req.prepare_metadata()
  132. self.req.assert_source_matches_version()
  133. class Installed(DistAbstraction):
  134. def dist(self):
  135. # type: () -> pkg_resources.Distribution
  136. return self.req.satisfied_by
  137. def prep_for_dist(self, finder, build_isolation):
  138. # type: (PackageFinder, bool) -> Any
  139. pass
  140. class RequirementPreparer(object):
  141. """Prepares a Requirement
  142. """
  143. def __init__(
  144. self,
  145. build_dir, # type: str
  146. download_dir, # type: Optional[str]
  147. src_dir, # type: str
  148. wheel_download_dir, # type: Optional[str]
  149. progress_bar, # type: str
  150. build_isolation, # type: bool
  151. req_tracker # type: RequirementTracker
  152. ):
  153. # type: (...) -> None
  154. super(RequirementPreparer, self).__init__()
  155. self.src_dir = src_dir
  156. self.build_dir = build_dir
  157. self.req_tracker = req_tracker
  158. # Where still packed archives should be written to. If None, they are
  159. # not saved, and are deleted immediately after unpacking.
  160. self.download_dir = download_dir
  161. # Where still-packed .whl files should be written to. If None, they are
  162. # written to the download_dir parameter. Separate to download_dir to
  163. # permit only keeping wheel archives for pip wheel.
  164. if wheel_download_dir:
  165. wheel_download_dir = normalize_path(wheel_download_dir)
  166. self.wheel_download_dir = wheel_download_dir
  167. # NOTE
  168. # download_dir and wheel_download_dir overlap semantically and may
  169. # be combined if we're willing to have non-wheel archives present in
  170. # the wheelhouse output by 'pip wheel'.
  171. self.progress_bar = progress_bar
  172. # Is build isolation allowed?
  173. self.build_isolation = build_isolation
  174. @property
  175. def _download_should_save(self):
  176. # type: () -> bool
  177. # TODO: Modify to reduce indentation needed
  178. if self.download_dir:
  179. self.download_dir = expanduser(self.download_dir)
  180. if os.path.exists(self.download_dir):
  181. return True
  182. else:
  183. logger.critical('Could not find download directory')
  184. raise InstallationError(
  185. "Could not find or access download directory '%s'"
  186. % display_path(self.download_dir))
  187. return False
  188. def prepare_linked_requirement(
  189. self,
  190. req, # type: InstallRequirement
  191. session, # type: PipSession
  192. finder, # type: PackageFinder
  193. upgrade_allowed, # type: bool
  194. require_hashes # type: bool
  195. ):
  196. # type: (...) -> DistAbstraction
  197. """Prepare a requirement that would be obtained from req.link
  198. """
  199. # TODO: Breakup into smaller functions
  200. if req.link and req.link.scheme == 'file':
  201. path = url_to_path(req.link.url)
  202. logger.info('Processing %s', display_path(path))
  203. else:
  204. logger.info('Collecting %s', req)
  205. with indent_log():
  206. # @@ if filesystem packages are not marked
  207. # editable in a req, a non deterministic error
  208. # occurs when the script attempts to unpack the
  209. # build directory
  210. req.ensure_has_source_dir(self.build_dir)
  211. # If a checkout exists, it's unwise to keep going. version
  212. # inconsistencies are logged later, but do not fail the
  213. # installation.
  214. # FIXME: this won't upgrade when there's an existing
  215. # package unpacked in `req.source_dir`
  216. # package unpacked in `req.source_dir`
  217. if os.path.exists(os.path.join(req.source_dir, 'setup.py')):
  218. raise PreviousBuildDirError(
  219. "pip can't proceed with requirements '%s' due to a"
  220. " pre-existing build directory (%s). This is "
  221. "likely due to a previous installation that failed"
  222. ". pip is being responsible and not assuming it "
  223. "can delete this. Please delete it and try again."
  224. % (req, req.source_dir)
  225. )
  226. req.populate_link(finder, upgrade_allowed, require_hashes)
  227. # We can't hit this spot and have populate_link return None.
  228. # req.satisfied_by is None here (because we're
  229. # guarded) and upgrade has no impact except when satisfied_by
  230. # is not None.
  231. # Then inside find_requirement existing_applicable -> False
  232. # If no new versions are found, DistributionNotFound is raised,
  233. # otherwise a result is guaranteed.
  234. assert req.link
  235. link = req.link
  236. # Now that we have the real link, we can tell what kind of
  237. # requirements we have and raise some more informative errors
  238. # than otherwise. (For example, we can raise VcsHashUnsupported
  239. # for a VCS URL rather than HashMissing.)
  240. if require_hashes:
  241. # We could check these first 2 conditions inside
  242. # unpack_url and save repetition of conditions, but then
  243. # we would report less-useful error messages for
  244. # unhashable requirements, complaining that there's no
  245. # hash provided.
  246. if is_vcs_url(link):
  247. raise VcsHashUnsupported()
  248. elif is_file_url(link) and is_dir_url(link):
  249. raise DirectoryUrlHashUnsupported()
  250. if not req.original_link and not req.is_pinned:
  251. # Unpinned packages are asking for trouble when a new
  252. # version is uploaded. This isn't a security check, but
  253. # it saves users a surprising hash mismatch in the
  254. # future.
  255. #
  256. # file:/// URLs aren't pinnable, so don't complain
  257. # about them not being pinned.
  258. raise HashUnpinned()
  259. hashes = req.hashes(trust_internet=not require_hashes)
  260. if require_hashes and not hashes:
  261. # Known-good hashes are missing for this requirement, so
  262. # shim it with a facade object that will provoke hash
  263. # computation and then raise a HashMissing exception
  264. # showing the user what the hash should be.
  265. hashes = MissingHashes()
  266. try:
  267. download_dir = self.download_dir
  268. # We always delete unpacked sdists after pip ran.
  269. autodelete_unpacked = True
  270. if req.link.is_wheel and self.wheel_download_dir:
  271. # when doing 'pip wheel` we download wheels to a
  272. # dedicated dir.
  273. download_dir = self.wheel_download_dir
  274. if req.link.is_wheel:
  275. if download_dir:
  276. # When downloading, we only unpack wheels to get
  277. # metadata.
  278. autodelete_unpacked = True
  279. else:
  280. # When installing a wheel, we use the unpacked
  281. # wheel.
  282. autodelete_unpacked = False
  283. unpack_url(
  284. req.link, req.source_dir,
  285. download_dir, autodelete_unpacked,
  286. session=session, hashes=hashes,
  287. progress_bar=self.progress_bar
  288. )
  289. except requests.HTTPError as exc:
  290. logger.critical(
  291. 'Could not install requirement %s because of error %s',
  292. req,
  293. exc,
  294. )
  295. raise InstallationError(
  296. 'Could not install requirement %s because of HTTP '
  297. 'error %s for URL %s' %
  298. (req, exc, req.link)
  299. )
  300. abstract_dist = make_abstract_dist(req)
  301. with self.req_tracker.track(req):
  302. abstract_dist.prep_for_dist(finder, self.build_isolation)
  303. if self._download_should_save:
  304. # Make a .zip of the source_dir we already created.
  305. if req.link.scheme in vcs.all_schemes:
  306. req.archive(self.download_dir)
  307. return abstract_dist
  308. def prepare_editable_requirement(
  309. self,
  310. req, # type: InstallRequirement
  311. require_hashes, # type: bool
  312. use_user_site, # type: bool
  313. finder # type: PackageFinder
  314. ):
  315. # type: (...) -> DistAbstraction
  316. """Prepare an editable requirement
  317. """
  318. assert req.editable, "cannot prepare a non-editable req as editable"
  319. logger.info('Obtaining %s', req)
  320. with indent_log():
  321. if require_hashes:
  322. raise InstallationError(
  323. 'The editable requirement %s cannot be installed when '
  324. 'requiring hashes, because there is no single file to '
  325. 'hash.' % req
  326. )
  327. req.ensure_has_source_dir(self.src_dir)
  328. req.update_editable(not self._download_should_save)
  329. abstract_dist = make_abstract_dist(req)
  330. with self.req_tracker.track(req):
  331. abstract_dist.prep_for_dist(finder, self.build_isolation)
  332. if self._download_should_save:
  333. req.archive(self.download_dir)
  334. req.check_if_exists(use_user_site)
  335. return abstract_dist
  336. def prepare_installed_requirement(self, req, require_hashes, skip_reason):
  337. # type: (InstallRequirement, bool, Optional[str]) -> DistAbstraction
  338. """Prepare an already-installed requirement
  339. """
  340. assert req.satisfied_by, "req should have been satisfied but isn't"
  341. assert skip_reason is not None, (
  342. "did not get skip reason skipped but req.satisfied_by "
  343. "is set to %r" % (req.satisfied_by,)
  344. )
  345. logger.info(
  346. 'Requirement %s: %s (%s)',
  347. skip_reason, req, req.satisfied_by.version
  348. )
  349. with indent_log():
  350. if require_hashes:
  351. logger.debug(
  352. 'Since it is already installed, we are trusting this '
  353. 'package without checking its hash. To ensure a '
  354. 'completely repeatable environment, install into an '
  355. 'empty virtualenv.'
  356. )
  357. abstract_dist = Installed(req)
  358. return abstract_dist