prepare.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. """Prepares a distribution for installation
  2. """
  3. # The following comment should be removed at some point in the future.
  4. # mypy: strict-optional=False
  5. import logging
  6. import mimetypes
  7. import os
  8. import shutil
  9. from pip._vendor import requests
  10. from pip._vendor.six import PY2
  11. from pip._internal.distributions import (
  12. make_distribution_for_install_requirement,
  13. )
  14. from pip._internal.distributions.installed import InstalledDistribution
  15. from pip._internal.exceptions import (
  16. DirectoryUrlHashUnsupported,
  17. HashMismatch,
  18. HashUnpinned,
  19. InstallationError,
  20. PreviousBuildDirError,
  21. VcsHashUnsupported,
  22. )
  23. from pip._internal.utils.hashes import MissingHashes
  24. from pip._internal.utils.logging import indent_log
  25. from pip._internal.utils.misc import display_path, hide_url
  26. from pip._internal.utils.temp_dir import TempDirectory
  27. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  28. from pip._internal.utils.unpacking import unpack_file
  29. from pip._internal.vcs import vcs
  30. if MYPY_CHECK_RUNNING:
  31. from typing import (
  32. Callable, List, Optional, Tuple,
  33. )
  34. from mypy_extensions import TypedDict
  35. from pip._internal.distributions import AbstractDistribution
  36. from pip._internal.index.package_finder import PackageFinder
  37. from pip._internal.models.link import Link
  38. from pip._internal.network.download import Downloader
  39. from pip._internal.req.req_install import InstallRequirement
  40. from pip._internal.req.req_tracker import RequirementTracker
  41. from pip._internal.utils.hashes import Hashes
  42. if PY2:
  43. CopytreeKwargs = TypedDict(
  44. 'CopytreeKwargs',
  45. {
  46. 'ignore': Callable[[str, List[str]], List[str]],
  47. 'symlinks': bool,
  48. },
  49. total=False,
  50. )
  51. else:
  52. CopytreeKwargs = TypedDict(
  53. 'CopytreeKwargs',
  54. {
  55. 'copy_function': Callable[[str, str], None],
  56. 'ignore': Callable[[str, List[str]], List[str]],
  57. 'ignore_dangling_symlinks': bool,
  58. 'symlinks': bool,
  59. },
  60. total=False,
  61. )
  62. logger = logging.getLogger(__name__)
  63. def _get_prepared_distribution(
  64. req, # type: InstallRequirement
  65. req_tracker, # type: RequirementTracker
  66. finder, # type: PackageFinder
  67. build_isolation # type: bool
  68. ):
  69. # type: (...) -> AbstractDistribution
  70. """Prepare a distribution for installation.
  71. """
  72. abstract_dist = make_distribution_for_install_requirement(req)
  73. with req_tracker.track(req):
  74. abstract_dist.prepare_distribution_metadata(finder, build_isolation)
  75. return abstract_dist
  76. def unpack_vcs_link(link, location):
  77. # type: (Link, str) -> None
  78. vcs_backend = vcs.get_backend_for_scheme(link.scheme)
  79. assert vcs_backend is not None
  80. vcs_backend.unpack(location, url=hide_url(link.url))
  81. class File(object):
  82. def __init__(self, path, content_type):
  83. # type: (str, str) -> None
  84. self.path = path
  85. self.content_type = content_type
  86. def get_http_url(
  87. link, # type: Link
  88. downloader, # type: Downloader
  89. download_dir=None, # type: Optional[str]
  90. hashes=None, # type: Optional[Hashes]
  91. ):
  92. # type: (...) -> File
  93. temp_dir = TempDirectory(kind="unpack", globally_managed=True)
  94. # If a download dir is specified, is the file already downloaded there?
  95. already_downloaded_path = None
  96. if download_dir:
  97. already_downloaded_path = _check_download_dir(
  98. link, download_dir, hashes
  99. )
  100. if already_downloaded_path:
  101. from_path = already_downloaded_path
  102. content_type = mimetypes.guess_type(from_path)[0]
  103. else:
  104. # let's download to a tmp dir
  105. from_path, content_type = _download_http_url(
  106. link, downloader, temp_dir.path, hashes
  107. )
  108. return File(from_path, content_type)
  109. def get_file_url(
  110. link, # type: Link
  111. download_dir=None, # type: Optional[str]
  112. hashes=None # type: Optional[Hashes]
  113. ):
  114. # type: (...) -> File
  115. """Get file and optionally check its hash.
  116. """
  117. # If a download dir is specified, is the file already there and valid?
  118. already_downloaded_path = None
  119. if download_dir:
  120. already_downloaded_path = _check_download_dir(
  121. link, download_dir, hashes
  122. )
  123. if already_downloaded_path:
  124. from_path = already_downloaded_path
  125. else:
  126. from_path = link.file_path
  127. # If --require-hashes is off, `hashes` is either empty, the
  128. # link's embedded hash, or MissingHashes; it is required to
  129. # match. If --require-hashes is on, we are satisfied by any
  130. # hash in `hashes` matching: a URL-based or an option-based
  131. # one; no internet-sourced hash will be in `hashes`.
  132. if hashes:
  133. hashes.check_against_path(from_path)
  134. content_type = mimetypes.guess_type(from_path)[0]
  135. return File(from_path, content_type)
  136. def unpack_url(
  137. link, # type: Link
  138. location, # type: str
  139. downloader, # type: Downloader
  140. download_dir=None, # type: Optional[str]
  141. hashes=None, # type: Optional[Hashes]
  142. ):
  143. # type: (...) -> Optional[File]
  144. """Unpack link into location, downloading if required.
  145. :param hashes: A Hashes object, one of whose embedded hashes must match,
  146. or HashMismatch will be raised. If the Hashes is empty, no matches are
  147. required, and unhashable types of requirements (like VCS ones, which
  148. would ordinarily raise HashUnsupported) are allowed.
  149. """
  150. # non-editable vcs urls
  151. if link.is_vcs:
  152. unpack_vcs_link(link, location)
  153. return None
  154. # If it's a url to a local directory, we build in-place.
  155. # There is nothing to be done here.
  156. if link.is_existing_dir():
  157. return None
  158. # file urls
  159. if link.is_file:
  160. file = get_file_url(link, download_dir, hashes=hashes)
  161. # http urls
  162. else:
  163. file = get_http_url(
  164. link,
  165. downloader,
  166. download_dir,
  167. hashes=hashes,
  168. )
  169. # unpack the archive to the build dir location. even when only downloading
  170. # archives, they have to be unpacked to parse dependencies
  171. unpack_file(file.path, location, file.content_type)
  172. return file
  173. def _download_http_url(
  174. link, # type: Link
  175. downloader, # type: Downloader
  176. temp_dir, # type: str
  177. hashes, # type: Optional[Hashes]
  178. ):
  179. # type: (...) -> Tuple[str, str]
  180. """Download link url into temp_dir using provided session"""
  181. download = downloader(link)
  182. file_path = os.path.join(temp_dir, download.filename)
  183. with open(file_path, 'wb') as content_file:
  184. for chunk in download.chunks:
  185. content_file.write(chunk)
  186. if hashes:
  187. hashes.check_against_path(file_path)
  188. return file_path, download.response.headers.get('content-type', '')
  189. def _check_download_dir(link, download_dir, hashes):
  190. # type: (Link, str, Optional[Hashes]) -> Optional[str]
  191. """ Check download_dir for previously downloaded file with correct hash
  192. If a correct file is found return its path else None
  193. """
  194. download_path = os.path.join(download_dir, link.filename)
  195. if not os.path.exists(download_path):
  196. return None
  197. # If already downloaded, does its hash match?
  198. logger.info('File was already downloaded %s', download_path)
  199. if hashes:
  200. try:
  201. hashes.check_against_path(download_path)
  202. except HashMismatch:
  203. logger.warning(
  204. 'Previously-downloaded file %s has bad hash. '
  205. 'Re-downloading.',
  206. download_path
  207. )
  208. os.unlink(download_path)
  209. return None
  210. return download_path
  211. class RequirementPreparer(object):
  212. """Prepares a Requirement
  213. """
  214. def __init__(
  215. self,
  216. build_dir, # type: str
  217. download_dir, # type: Optional[str]
  218. src_dir, # type: str
  219. wheel_download_dir, # type: Optional[str]
  220. build_isolation, # type: bool
  221. req_tracker, # type: RequirementTracker
  222. downloader, # type: Downloader
  223. finder, # type: PackageFinder
  224. require_hashes, # type: bool
  225. use_user_site, # type: bool
  226. ):
  227. # type: (...) -> None
  228. super(RequirementPreparer, self).__init__()
  229. self.src_dir = src_dir
  230. self.build_dir = build_dir
  231. self.req_tracker = req_tracker
  232. self.downloader = downloader
  233. self.finder = finder
  234. # Where still-packed archives should be written to. If None, they are
  235. # not saved, and are deleted immediately after unpacking.
  236. self.download_dir = download_dir
  237. # Where still-packed .whl files should be written to. If None, they are
  238. # written to the download_dir parameter. Separate to download_dir to
  239. # permit only keeping wheel archives for pip wheel.
  240. self.wheel_download_dir = wheel_download_dir
  241. # NOTE
  242. # download_dir and wheel_download_dir overlap semantically and may
  243. # be combined if we're willing to have non-wheel archives present in
  244. # the wheelhouse output by 'pip wheel'.
  245. # Is build isolation allowed?
  246. self.build_isolation = build_isolation
  247. # Should hash-checking be required?
  248. self.require_hashes = require_hashes
  249. # Should install in user site-packages?
  250. self.use_user_site = use_user_site
  251. @property
  252. def _download_should_save(self):
  253. # type: () -> bool
  254. if not self.download_dir:
  255. return False
  256. if os.path.exists(self.download_dir):
  257. return True
  258. logger.critical('Could not find download directory')
  259. raise InstallationError(
  260. "Could not find or access download directory '{}'"
  261. .format(self.download_dir))
  262. def prepare_linked_requirement(
  263. self,
  264. req, # type: InstallRequirement
  265. ):
  266. # type: (...) -> AbstractDistribution
  267. """Prepare a requirement that would be obtained from req.link
  268. """
  269. assert req.link
  270. link = req.link
  271. # TODO: Breakup into smaller functions
  272. if link.scheme == 'file':
  273. path = link.file_path
  274. logger.info('Processing %s', display_path(path))
  275. else:
  276. logger.info('Collecting %s', req.req or req)
  277. download_dir = self.download_dir
  278. if link.is_wheel and self.wheel_download_dir:
  279. # when doing 'pip wheel` we download wheels to a
  280. # dedicated dir.
  281. download_dir = self.wheel_download_dir
  282. if link.is_wheel:
  283. if download_dir:
  284. # When downloading, we only unpack wheels to get
  285. # metadata.
  286. autodelete_unpacked = True
  287. else:
  288. # When installing a wheel, we use the unpacked
  289. # wheel.
  290. autodelete_unpacked = False
  291. else:
  292. # We always delete unpacked sdists after pip runs.
  293. autodelete_unpacked = True
  294. with indent_log():
  295. # Since source_dir is only set for editable requirements.
  296. assert req.source_dir is None
  297. if link.is_existing_dir():
  298. # Build local directories in place.
  299. req.source_dir = link.file_path
  300. else:
  301. req.ensure_has_source_dir(self.build_dir, autodelete_unpacked)
  302. # If a checkout exists, it's unwise to keep going. version
  303. # inconsistencies are logged later, but do not fail the
  304. # installation.
  305. # FIXME: this won't upgrade when there's an existing
  306. # package unpacked in `req.source_dir`
  307. if os.path.exists(os.path.join(req.source_dir, 'setup.py')):
  308. raise PreviousBuildDirError(
  309. "pip can't proceed with requirements '{}' due to a"
  310. " pre-existing build directory ({}). This is "
  311. "likely due to a previous installation that failed"
  312. ". pip is being responsible and not assuming it "
  313. "can delete this. Please delete it and try again."
  314. .format(req, req.source_dir)
  315. )
  316. # Now that we have the real link, we can tell what kind of
  317. # requirements we have and raise some more informative errors
  318. # than otherwise. (For example, we can raise VcsHashUnsupported
  319. # for a VCS URL rather than HashMissing.)
  320. if self.require_hashes:
  321. # We could check these first 2 conditions inside
  322. # unpack_url and save repetition of conditions, but then
  323. # we would report less-useful error messages for
  324. # unhashable requirements, complaining that there's no
  325. # hash provided.
  326. if link.is_vcs:
  327. raise VcsHashUnsupported()
  328. elif link.is_existing_dir():
  329. raise DirectoryUrlHashUnsupported()
  330. if not req.original_link and not req.is_pinned:
  331. # Unpinned packages are asking for trouble when a new
  332. # version is uploaded. This isn't a security check, but
  333. # it saves users a surprising hash mismatch in the
  334. # future.
  335. #
  336. # file:/// URLs aren't pinnable, so don't complain
  337. # about them not being pinned.
  338. raise HashUnpinned()
  339. hashes = req.hashes(trust_internet=not self.require_hashes)
  340. if self.require_hashes and not hashes:
  341. # Known-good hashes are missing for this requirement, so
  342. # shim it with a facade object that will provoke hash
  343. # computation and then raise a HashMissing exception
  344. # showing the user what the hash should be.
  345. hashes = MissingHashes()
  346. try:
  347. local_file = unpack_url(
  348. link, req.source_dir, self.downloader, download_dir,
  349. hashes=hashes,
  350. )
  351. except requests.HTTPError as exc:
  352. logger.critical(
  353. 'Could not install requirement %s because of error %s',
  354. req,
  355. exc,
  356. )
  357. raise InstallationError(
  358. 'Could not install requirement {} because of HTTP '
  359. 'error {} for URL {}'.format(req, exc, link)
  360. )
  361. # For use in later processing, preserve the file path on the
  362. # requirement.
  363. if local_file:
  364. req.local_file_path = local_file.path
  365. abstract_dist = _get_prepared_distribution(
  366. req, self.req_tracker, self.finder, self.build_isolation,
  367. )
  368. if download_dir:
  369. if link.is_existing_dir():
  370. logger.info('Link is a directory, ignoring download_dir')
  371. elif local_file:
  372. download_location = os.path.join(
  373. download_dir, link.filename
  374. )
  375. if not os.path.exists(download_location):
  376. shutil.copy(local_file.path, download_location)
  377. logger.info(
  378. 'Saved %s', display_path(download_location)
  379. )
  380. if self._download_should_save:
  381. # Make a .zip of the source_dir we already created.
  382. if link.is_vcs:
  383. req.archive(self.download_dir)
  384. return abstract_dist
  385. def prepare_editable_requirement(
  386. self,
  387. req, # type: InstallRequirement
  388. ):
  389. # type: (...) -> AbstractDistribution
  390. """Prepare an editable requirement
  391. """
  392. assert req.editable, "cannot prepare a non-editable req as editable"
  393. logger.info('Obtaining %s', req)
  394. with indent_log():
  395. if self.require_hashes:
  396. raise InstallationError(
  397. 'The editable requirement {} cannot be installed when '
  398. 'requiring hashes, because there is no single file to '
  399. 'hash.'.format(req)
  400. )
  401. req.ensure_has_source_dir(self.src_dir)
  402. req.update_editable(not self._download_should_save)
  403. abstract_dist = _get_prepared_distribution(
  404. req, self.req_tracker, self.finder, self.build_isolation,
  405. )
  406. if self._download_should_save:
  407. req.archive(self.download_dir)
  408. req.check_if_exists(self.use_user_site)
  409. return abstract_dist
  410. def prepare_installed_requirement(
  411. self,
  412. req, # type: InstallRequirement
  413. skip_reason # type: str
  414. ):
  415. # type: (...) -> AbstractDistribution
  416. """Prepare an already-installed requirement
  417. """
  418. assert req.satisfied_by, "req should have been satisfied but isn't"
  419. assert skip_reason is not None, (
  420. "did not get skip reason skipped but req.satisfied_by "
  421. "is set to {}".format(req.satisfied_by)
  422. )
  423. logger.info(
  424. 'Requirement %s: %s (%s)',
  425. skip_reason, req, req.satisfied_by.version
  426. )
  427. with indent_log():
  428. if self.require_hashes:
  429. logger.debug(
  430. 'Since it is already installed, we are trusting this '
  431. 'package without checking its hash. To ensure a '
  432. 'completely repeatable environment, install into an '
  433. 'empty virtualenv.'
  434. )
  435. abstract_dist = InstalledDistribution(req)
  436. return abstract_dist