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 15KB

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