sdist.py 6.8 KB

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