Skip to content

Commit 98b1022

Browse files
authored
Merge pull request #10722 from pradyunsg/stop-backtracking-on-build-failures
2 parents fdba4c2 + 9d0db88 commit 98b1022

File tree

9 files changed

+132
-6
lines changed

9 files changed

+132
-6
lines changed

news/10655.bugfix.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Stop backtracking on build failures, by instead surfacing them to the
2+
user and failing immediately. This behaviour is more forgiving when
3+
a package cannot be built due to missing build dependencies or platform
4+
incompatibility.

src/pip/_internal/cli/cmdoptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,7 @@ def check_list_path_option(options: Values) -> None:
964964
metavar="feature",
965965
action="append",
966966
default=[],
967-
choices=["legacy-resolver", "out-of-tree-build"],
967+
choices=["legacy-resolver", "out-of-tree-build", "backtrack-on-build-failures"],
968968
help=("Enable deprecated functionality, that will be removed in the future."),
969969
)
970970

src/pip/_internal/cli/req_command.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,31 @@ def determine_resolver_variant(options: Values) -> str:
227227

228228
return "2020-resolver"
229229

230+
@staticmethod
231+
def determine_build_failure_suppression(options: Values) -> bool:
232+
"""Determines whether build failures should be suppressed and backtracked on."""
233+
if "backtrack-on-build-failures" not in options.deprecated_features_enabled:
234+
return False
235+
236+
if "legacy-resolver" in options.deprecated_features_enabled:
237+
raise CommandError("Cannot backtrack with legacy resolver.")
238+
239+
deprecated(
240+
reason=(
241+
"Backtracking on build failures can mask issues related to how "
242+
"a package generates metadata or builds a wheel. This flag will "
243+
"be removed in pip 22.2."
244+
),
245+
gone_in=None,
246+
replacement=(
247+
"avoiding known-bad versions by explicitly telling pip to ignore them "
248+
"(either directly as requirements, or via a constraints file)"
249+
),
250+
feature_flag=None,
251+
issue=10655,
252+
)
253+
return True
254+
230255
@classmethod
231256
def make_requirement_preparer(
232257
cls,
@@ -323,6 +348,7 @@ def make_resolver(
323348
isolated=options.isolated_mode,
324349
use_pep517=use_pep517,
325350
)
351+
suppress_build_failures = cls.determine_build_failure_suppression(options)
326352
resolver_variant = cls.determine_resolver_variant(options)
327353
# The long import name and duplicated invocation is needed to convince
328354
# Mypy into correctly typechecking. Otherwise it would complain the
@@ -342,6 +368,7 @@ def make_resolver(
342368
force_reinstall=force_reinstall,
343369
upgrade_strategy=upgrade_strategy,
344370
py_version_info=py_version_info,
371+
suppress_build_failures=suppress_build_failures,
345372
)
346373
import pip._internal.resolution.legacy.resolver
347374

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def __init__(
9797
force_reinstall: bool,
9898
ignore_installed: bool,
9999
ignore_requires_python: bool,
100+
suppress_build_failures: bool,
100101
py_version_info: Optional[Tuple[int, ...]] = None,
101102
) -> None:
102103
self._finder = finder
@@ -107,6 +108,7 @@ def __init__(
107108
self._use_user_site = use_user_site
108109
self._force_reinstall = force_reinstall
109110
self._ignore_requires_python = ignore_requires_python
111+
self._suppress_build_failures = suppress_build_failures
110112

111113
self._build_failures: Cache[InstallationError] = {}
112114
self._link_candidate_cache: Cache[LinkCandidate] = {}
@@ -190,7 +192,7 @@ def _make_candidate_from_link(
190192
name=name,
191193
version=version,
192194
)
193-
except (InstallationSubprocessError, MetadataInconsistent) as e:
195+
except MetadataInconsistent as e:
194196
logger.info(
195197
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
196198
link,
@@ -199,6 +201,13 @@ def _make_candidate_from_link(
199201
)
200202
self._build_failures[link] = e
201203
return None
204+
except InstallationSubprocessError as e:
205+
if not self._suppress_build_failures:
206+
raise
207+
logger.warning("Discarding %s due to build failure: %s", link, e)
208+
self._build_failures[link] = e
209+
return None
210+
202211
base: BaseCandidate = self._editable_candidate_cache[link]
203212
else:
204213
if link not in self._link_candidate_cache:
@@ -210,7 +219,7 @@ def _make_candidate_from_link(
210219
name=name,
211220
version=version,
212221
)
213-
except (InstallationSubprocessError, MetadataInconsistent) as e:
222+
except MetadataInconsistent as e:
214223
logger.info(
215224
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
216225
link,
@@ -219,6 +228,12 @@ def _make_candidate_from_link(
219228
)
220229
self._build_failures[link] = e
221230
return None
231+
except InstallationSubprocessError as e:
232+
if not self._suppress_build_failures:
233+
raise
234+
logger.warning("Discarding %s due to build failure: %s", link, e)
235+
self._build_failures[link] = e
236+
return None
222237
base = self._link_candidate_cache[link]
223238

224239
if not extras:

src/pip/_internal/resolution/resolvelib/resolver.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(
4747
ignore_requires_python: bool,
4848
force_reinstall: bool,
4949
upgrade_strategy: str,
50+
suppress_build_failures: bool,
5051
py_version_info: Optional[Tuple[int, ...]] = None,
5152
):
5253
super().__init__()
@@ -61,6 +62,7 @@ def __init__(
6162
force_reinstall=force_reinstall,
6263
ignore_installed=ignore_installed,
6364
ignore_requires_python=ignore_requires_python,
65+
suppress_build_failures=suppress_build_failures,
6466
py_version_info=py_version_info,
6567
)
6668
self.ignore_dependencies = ignore_dependencies

tests/functional/test_new_resolver.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2308,3 +2308,65 @@ def test_new_resolver_respect_user_requested_if_extra_is_installed(
23082308
"pkg2",
23092309
)
23102310
script.assert_installed(pkg3="1.0", pkg2="2.0", pkg1="1.0")
2311+
2312+
2313+
def test_new_resolver_do_not_backtrack_on_build_failure(
2314+
script: PipTestEnvironment,
2315+
) -> None:
2316+
create_basic_sdist_for_package(script, "pkg1", "2.0", fails_egg_info=True)
2317+
create_basic_wheel_for_package(script, "pkg1", "1.0")
2318+
2319+
result = script.pip(
2320+
"install",
2321+
"--no-cache-dir",
2322+
"--no-index",
2323+
"--find-links",
2324+
script.scratch_path,
2325+
"pkg1",
2326+
expect_error=True,
2327+
)
2328+
2329+
assert "egg_info" in result.stderr
2330+
2331+
2332+
def test_new_resolver_flag_permits_backtracking_on_build_failure(
2333+
script: PipTestEnvironment,
2334+
) -> None:
2335+
create_basic_sdist_for_package(script, "pkg1", "2.0", fails_egg_info=True)
2336+
create_basic_wheel_for_package(script, "pkg1", "1.0")
2337+
2338+
script.pip(
2339+
"install",
2340+
"--use-deprecated=backtrack-on-build-failures",
2341+
"--no-cache-dir",
2342+
"--no-index",
2343+
"--find-links",
2344+
script.scratch_path,
2345+
"pkg1",
2346+
allow_stderr_warning=True,
2347+
)
2348+
2349+
script.assert_installed(pkg1="1.0")
2350+
2351+
2352+
def test_new_resolver_works_when_failing_package_builds_are_disallowed(
2353+
script: PipTestEnvironment,
2354+
) -> None:
2355+
create_basic_wheel_for_package(script, "pkg2", "1.0", depends=["pkg1"])
2356+
create_basic_sdist_for_package(script, "pkg1", "2.0", fails_egg_info=True)
2357+
create_basic_wheel_for_package(script, "pkg1", "1.0")
2358+
constraints_file = script.scratch_path / "constraints.txt"
2359+
constraints_file.write_text("pkg1 != 2.0")
2360+
2361+
script.pip(
2362+
"install",
2363+
"--no-cache-dir",
2364+
"--no-index",
2365+
"--find-links",
2366+
script.scratch_path,
2367+
"-c",
2368+
constraints_file,
2369+
"pkg2",
2370+
)
2371+
2372+
script.assert_installed(pkg2="1.0", pkg1="1.0")

tests/lib/__init__.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,22 +1183,36 @@ def create_basic_sdist_for_package(
11831183
name: str,
11841184
version: str,
11851185
extra_files: Optional[Dict[str, str]] = None,
1186+
*,
1187+
fails_egg_info: bool = False,
1188+
fails_bdist_wheel: bool = False,
11861189
) -> Path:
11871190
files = {
1188-
"setup.py": """
1191+
"setup.py": f"""\
1192+
import sys
11891193
from setuptools import find_packages, setup
1194+
1195+
fails_bdist_wheel = {fails_bdist_wheel!r}
1196+
fails_egg_info = {fails_egg_info!r}
1197+
1198+
if fails_egg_info and "egg_info" in sys.argv:
1199+
raise Exception("Simulated failure for generating metadata.")
1200+
1201+
if fails_bdist_wheel and "bdist_wheel" in sys.argv:
1202+
raise Exception("Simulated failure for building a wheel.")
1203+
11901204
setup(name={name!r}, version={version!r})
11911205
""",
11921206
}
11931207

11941208
# Some useful shorthands
1195-
archive_name = "{name}-{version}.tar.gz".format(name=name, version=version)
1209+
archive_name = f"{name}-{version}.tar.gz"
11961210

11971211
# Replace key-values with formatted values
11981212
for key, value in list(files.items()):
11991213
del files[key]
12001214
key = key.format(name=name)
1201-
files[key] = textwrap.dedent(value).format(name=name, version=version).strip()
1215+
files[key] = textwrap.dedent(value)
12021216

12031217
# Add new files after formatting
12041218
if extra_files:

tests/unit/resolution_resolvelib/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def factory(finder: PackageFinder, preparer: RequirementPreparer) -> Iterator[Fa
6363
force_reinstall=False,
6464
ignore_installed=False,
6565
ignore_requires_python=False,
66+
suppress_build_failures=False,
6667
py_version_info=None,
6768
)
6869

tests/unit/resolution_resolvelib/test_resolver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def resolver(preparer: RequirementPreparer, finder: PackageFinder) -> Resolver:
2929
ignore_requires_python=False,
3030
force_reinstall=False,
3131
upgrade_strategy="to-satisfy-only",
32+
suppress_build_failures=False,
3233
)
3334
return resolver
3435

0 commit comments

Comments
 (0)