sdist.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import logging
  2. from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
  3. from pip._internal.build_env import BuildEnvironment
  4. from pip._internal.distributions.base import AbstractDistribution
  5. from pip._internal.exceptions import InstallationError
  6. from pip._internal.metadata import BaseDistribution
  7. from pip._internal.utils.subprocess import runner_with_spinner_message
  8. if TYPE_CHECKING:
  9. from pip._internal.index.package_finder import PackageFinder
  10. logger = logging.getLogger(__name__)
  11. class SourceDistribution(AbstractDistribution):
  12. """Represents a source distribution.
  13. The preparation step for these needs metadata for the packages to be
  14. generated, either using PEP 517 or using the legacy `setup.py egg_info`.
  15. """
  16. @property
  17. def build_tracker_id(self) -> Optional[str]:
  18. """Identify this requirement uniquely by its link."""
  19. assert self.req.link
  20. return self.req.link.url_without_fragment
  21. def get_metadata_distribution(self) -> BaseDistribution:
  22. return self.req.get_dist()
  23. def prepare_distribution_metadata(
  24. self,
  25. finder: "PackageFinder",
  26. build_isolation: bool,
  27. check_build_deps: bool,
  28. ) -> None:
  29. # Load pyproject.toml, to determine whether PEP 517 is to be used
  30. self.req.load_pyproject_toml()
  31. # Set up the build isolation, if this requirement should be isolated
  32. should_isolate = self.req.use_pep517 and build_isolation
  33. if should_isolate:
  34. # Setup an isolated environment and install the build backend static
  35. # requirements in it.
  36. self._prepare_build_backend(finder)
  37. # Check that if the requirement is editable, it either supports PEP 660 or
  38. # has a setup.py or a setup.cfg. This cannot be done earlier because we need
  39. # to setup the build backend to verify it supports build_editable, nor can
  40. # it be done later, because we want to avoid installing build requirements
  41. # needlessly. Doing it here also works around setuptools generating
  42. # UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory
  43. # without setup.py nor setup.cfg.
  44. self.req.isolated_editable_sanity_check()
  45. # Install the dynamic build requirements.
  46. self._install_build_reqs(finder)
  47. # Check if the current environment provides build dependencies
  48. should_check_deps = self.req.use_pep517 and check_build_deps
  49. if should_check_deps:
  50. pyproject_requires = self.req.pyproject_requires
  51. assert pyproject_requires is not None
  52. conflicting, missing = self.req.build_env.check_requirements(
  53. pyproject_requires
  54. )
  55. if conflicting:
  56. self._raise_conflicts("the backend dependencies", conflicting)
  57. if missing:
  58. self._raise_missing_reqs(missing)
  59. self.req.prepare_metadata()
  60. def _prepare_build_backend(self, finder: "PackageFinder") -> None:
  61. # Isolate in a BuildEnvironment and install the build-time
  62. # requirements.
  63. pyproject_requires = self.req.pyproject_requires
  64. assert pyproject_requires is not None
  65. self.req.build_env = BuildEnvironment()
  66. self.req.build_env.install_requirements(
  67. finder, pyproject_requires, "overlay", kind="build dependencies"
  68. )
  69. conflicting, missing = self.req.build_env.check_requirements(
  70. self.req.requirements_to_check
  71. )
  72. if conflicting:
  73. self._raise_conflicts("PEP 517/518 supported requirements", conflicting)
  74. if missing:
  75. logger.warning(
  76. "Missing build requirements in pyproject.toml for %s.",
  77. self.req,
  78. )
  79. logger.warning(
  80. "The project does not specify a build backend, and "
  81. "pip cannot fall back to setuptools without %s.",
  82. " and ".join(map(repr, sorted(missing))),
  83. )
  84. def _get_build_requires_wheel(self) -> Iterable[str]:
  85. with self.req.build_env:
  86. runner = runner_with_spinner_message("Getting requirements to build wheel")
  87. backend = self.req.pep517_backend
  88. assert backend is not None
  89. with backend.subprocess_runner(runner):
  90. return backend.get_requires_for_build_wheel()
  91. def _get_build_requires_editable(self) -> Iterable[str]:
  92. with self.req.build_env:
  93. runner = runner_with_spinner_message(
  94. "Getting requirements to build editable"
  95. )
  96. backend = self.req.pep517_backend
  97. assert backend is not None
  98. with backend.subprocess_runner(runner):
  99. return backend.get_requires_for_build_editable()
  100. def _install_build_reqs(self, finder: "PackageFinder") -> None:
  101. # Install any extra build dependencies that the backend requests.
  102. # This must be done in a second pass, as the pyproject.toml
  103. # dependencies must be installed before we can call the backend.
  104. if (
  105. self.req.editable
  106. and self.req.permit_editable_wheels
  107. and self.req.supports_pyproject_editable
  108. ):
  109. build_reqs = self._get_build_requires_editable()
  110. else:
  111. build_reqs = self._get_build_requires_wheel()
  112. conflicting, missing = self.req.build_env.check_requirements(build_reqs)
  113. if conflicting:
  114. self._raise_conflicts("the backend dependencies", conflicting)
  115. self.req.build_env.install_requirements(
  116. finder, missing, "normal", kind="backend dependencies"
  117. )
  118. def _raise_conflicts(
  119. self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
  120. ) -> None:
  121. format_string = (
  122. "Some build dependencies for {requirement} "
  123. "conflict with {conflicting_with}: {description}."
  124. )
  125. error_message = format_string.format(
  126. requirement=self.req,
  127. conflicting_with=conflicting_with,
  128. description=", ".join(
  129. f"{installed} is incompatible with {wanted}"
  130. for installed, wanted in sorted(conflicting_reqs)
  131. ),
  132. )
  133. raise InstallationError(error_message)
  134. def _raise_missing_reqs(self, missing: Set[str]) -> None:
  135. format_string = (
  136. "Some build dependencies for {requirement} are missing: {missing}."
  137. )
  138. error_message = format_string.format(
  139. requirement=self.req, missing=", ".join(map(repr, sorted(missing)))
  140. )
  141. raise InstallationError(error_message)