]>
Commit | Line | Data |
---|---|---|
e0df8241 JR |
1 | import logging |
2 | from typing import Iterable, Set, Tuple | |
3 | ||
4 | from pip._internal.build_env import BuildEnvironment | |
5 | from pip._internal.distributions.base import AbstractDistribution | |
6 | from pip._internal.exceptions import InstallationError | |
7 | from pip._internal.index.package_finder import PackageFinder | |
8 | from pip._internal.metadata import BaseDistribution | |
9 | from pip._internal.utils.subprocess import runner_with_spinner_message | |
10 | ||
11 | logger = logging.getLogger(__name__) | |
12 | ||
13 | ||
14 | class SourceDistribution(AbstractDistribution): | |
15 | """Represents a source distribution. | |
16 | ||
17 | The preparation step for these needs metadata for the packages to be | |
18 | generated, either using PEP 517 or using the legacy `setup.py egg_info`. | |
19 | """ | |
20 | ||
21 | def get_metadata_distribution(self) -> BaseDistribution: | |
22 | return self.req.get_dist() | |
23 | ||
24 | def prepare_distribution_metadata( | |
25 | self, | |
26 | finder: PackageFinder, | |
27 | build_isolation: bool, | |
28 | check_build_deps: bool, | |
29 | ) -> None: | |
30 | # Load pyproject.toml, to determine whether PEP 517 is to be used | |
31 | self.req.load_pyproject_toml() | |
32 | ||
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(finder) | |
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(finder) | |
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 | ||
63 | def _prepare_build_backend(self, finder: PackageFinder) -> None: | |
64 | # Isolate in a BuildEnvironment and install the build-time | |
65 | # requirements. | |
66 | pyproject_requires = self.req.pyproject_requires | |
67 | assert pyproject_requires is not None | |
68 | ||
69 | self.req.build_env = BuildEnvironment() | |
70 | self.req.build_env.install_requirements( | |
71 | finder, pyproject_requires, "overlay", kind="build dependencies" | |
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 | ||
89 | def _get_build_requires_wheel(self) -> Iterable[str]: | |
90 | with self.req.build_env: | |
91 | runner = runner_with_spinner_message("Getting requirements to build wheel") | |
92 | backend = self.req.pep517_backend | |
93 | assert backend is not None | |
94 | with backend.subprocess_runner(runner): | |
95 | return backend.get_requires_for_build_wheel() | |
96 | ||
97 | def _get_build_requires_editable(self) -> Iterable[str]: | |
98 | with self.req.build_env: | |
99 | runner = runner_with_spinner_message( | |
100 | "Getting requirements to build editable" | |
101 | ) | |
102 | backend = self.req.pep517_backend | |
103 | assert backend is not None | |
104 | with backend.subprocess_runner(runner): | |
105 | return backend.get_requires_for_build_editable() | |
106 | ||
107 | def _install_build_reqs(self, finder: PackageFinder) -> None: | |
108 | # Install any extra build dependencies that the backend requests. | |
109 | # This must be done in a second pass, as the pyproject.toml | |
110 | # dependencies must be installed before we can call the backend. | |
111 | if ( | |
112 | self.req.editable | |
113 | and self.req.permit_editable_wheels | |
114 | and self.req.supports_pyproject_editable() | |
115 | ): | |
116 | build_reqs = self._get_build_requires_editable() | |
117 | else: | |
118 | build_reqs = self._get_build_requires_wheel() | |
119 | conflicting, missing = self.req.build_env.check_requirements(build_reqs) | |
120 | if conflicting: | |
121 | self._raise_conflicts("the backend dependencies", conflicting) | |
122 | self.req.build_env.install_requirements( | |
123 | finder, missing, "normal", kind="backend dependencies" | |
124 | ) | |
125 | ||
126 | def _raise_conflicts( | |
127 | self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]] | |
128 | ) -> None: | |
129 | format_string = ( | |
130 | "Some build dependencies for {requirement} " | |
131 | "conflict with {conflicting_with}: {description}." | |
132 | ) | |
133 | error_message = format_string.format( | |
134 | requirement=self.req, | |
135 | conflicting_with=conflicting_with, | |
136 | description=", ".join( | |
137 | f"{installed} is incompatible with {wanted}" | |
138 | for installed, wanted in sorted(conflicting_reqs) | |
139 | ), | |
140 | ) | |
141 | raise InstallationError(error_message) | |
142 | ||
143 | def _raise_missing_reqs(self, missing: Set[str]) -> None: | |
144 | format_string = ( | |
145 | "Some build dependencies for {requirement} are missing: {missing}." | |
146 | ) | |
147 | error_message = format_string.format( | |
148 | requirement=self.req, missing=", ".join(map(repr, sorted(missing))) | |
149 | ) | |
150 | raise InstallationError(error_message) |