installer.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. from __future__ import annotations
  2. import glob
  3. import itertools
  4. import os
  5. import subprocess
  6. import sys
  7. import tempfile
  8. import packaging.requirements
  9. import packaging.utils
  10. from . import _reqs
  11. from ._importlib import metadata
  12. from .warnings import SetuptoolsDeprecationWarning
  13. from .wheel import Wheel
  14. from distutils import log
  15. from distutils.errors import DistutilsError
  16. def _fixup_find_links(find_links):
  17. """Ensure find-links option end-up being a list of strings."""
  18. if isinstance(find_links, str):
  19. return find_links.split()
  20. assert isinstance(find_links, (tuple, list))
  21. return find_links
  22. def fetch_build_egg(dist, req) -> metadata.Distribution | metadata.PathDistribution:
  23. """Fetch an egg needed for building.
  24. Use pip/wheel to fetch/build a wheel."""
  25. _DeprecatedInstaller.emit()
  26. _warn_wheel_not_available(dist)
  27. return _fetch_build_egg_no_warn(dist, req)
  28. def _present(req):
  29. return any(_dist_matches_req(dist, req) for dist in metadata.distributions())
  30. def _fetch_build_eggs(dist, requires: _reqs._StrOrIter) -> list[metadata.Distribution]:
  31. _DeprecatedInstaller.emit(stacklevel=3)
  32. _warn_wheel_not_available(dist)
  33. parsed_reqs = _reqs.parse(requires)
  34. missing_reqs = itertools.filterfalse(_present, parsed_reqs)
  35. needed_reqs = (
  36. req for req in missing_reqs if not req.marker or req.marker.evaluate()
  37. )
  38. resolved_dists = [_fetch_build_egg_no_warn(dist, req) for req in needed_reqs]
  39. for dist in resolved_dists:
  40. # dist.locate_file('') is the directory containing EGG-INFO, where the importabl
  41. # contents can be found.
  42. sys.path.insert(0, str(dist.locate_file('')))
  43. return resolved_dists
  44. def _dist_matches_req(egg_dist, req):
  45. return (
  46. packaging.utils.canonicalize_name(egg_dist.name)
  47. == packaging.utils.canonicalize_name(req.name)
  48. and egg_dist.version in req.specifier
  49. )
  50. def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) # FIXME
  51. # Ignore environment markers; if supplied, it is required.
  52. req = strip_marker(req)
  53. # Take easy_install options into account, but do not override relevant
  54. # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll
  55. # take precedence.
  56. opts = dist.get_option_dict('easy_install')
  57. if 'allow_hosts' in opts:
  58. raise DistutilsError(
  59. 'the `allow-hosts` option is not supported '
  60. 'when using pip to install requirements.'
  61. )
  62. quiet = 'PIP_QUIET' not in os.environ and 'PIP_VERBOSE' not in os.environ
  63. if 'PIP_INDEX_URL' in os.environ:
  64. index_url = None
  65. elif 'index_url' in opts:
  66. index_url = opts['index_url'][1]
  67. else:
  68. index_url = None
  69. find_links = (
  70. _fixup_find_links(opts['find_links'][1])[:] if 'find_links' in opts else []
  71. )
  72. if dist.dependency_links:
  73. find_links.extend(dist.dependency_links)
  74. eggs_dir = os.path.realpath(dist.get_egg_cache_dir())
  75. cached_dists = metadata.Distribution.discover(path=glob.glob(f'{eggs_dir}/*.egg'))
  76. for egg_dist in cached_dists:
  77. if _dist_matches_req(egg_dist, req):
  78. return egg_dist
  79. with tempfile.TemporaryDirectory() as tmpdir:
  80. cmd = [
  81. sys.executable,
  82. '-m',
  83. 'pip',
  84. '--disable-pip-version-check',
  85. 'wheel',
  86. '--no-deps',
  87. '-w',
  88. tmpdir,
  89. ]
  90. if quiet:
  91. cmd.append('--quiet')
  92. if index_url is not None:
  93. cmd.extend(('--index-url', index_url))
  94. for link in find_links or []:
  95. cmd.extend(('--find-links', link))
  96. # If requirement is a PEP 508 direct URL, directly pass
  97. # the URL to pip, as `req @ url` does not work on the
  98. # command line.
  99. cmd.append(req.url or str(req))
  100. try:
  101. subprocess.check_call(cmd)
  102. except subprocess.CalledProcessError as e:
  103. raise DistutilsError(str(e)) from e
  104. wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0])
  105. dist_location = os.path.join(eggs_dir, wheel.egg_name())
  106. wheel.install_as_egg(dist_location)
  107. return metadata.Distribution.at(dist_location + '/EGG-INFO')
  108. def strip_marker(req) -> packaging.requirements.Requirement:
  109. """
  110. Return a new requirement without the environment marker to avoid
  111. calling pip with something like `babel; extra == "i18n"`, which
  112. would always be ignored.
  113. """
  114. # create a copy to avoid mutating the input
  115. req = packaging.requirements.Requirement(str(req))
  116. req.marker = None
  117. return req
  118. def _warn_wheel_not_available(dist):
  119. try:
  120. metadata.distribution('wheel')
  121. except metadata.PackageNotFoundError:
  122. dist.announce('WARNING: The wheel package is not available.', log.WARN)
  123. class _DeprecatedInstaller(SetuptoolsDeprecationWarning):
  124. _SUMMARY = "setuptools.installer and fetch_build_eggs are deprecated."
  125. _DETAILS = """
  126. Requirements should be satisfied by a PEP 517 installer.
  127. If you are using pip, you can try `pip install --use-pep517`.
  128. """
  129. _DUE_DATE = 2025, 10, 31