diff --git a/docs/html/reference/build-system/setup-py.md b/docs/html/reference/build-system/setup-py.md
index 53917b8a4c8..0103a3a6a92 100644
--- a/docs/html/reference/build-system/setup-py.md
+++ b/docs/html/reference/build-system/setup-py.md
@@ -24,8 +24,6 @@ The overall process for building a package is:
- Generate the package's metadata.
- Generate a wheel for the package.
- - If this fails and we're trying to install the package, attempt a direct
- installation.
The wheel can then be used to perform an installation, if necessary.
@@ -58,13 +56,6 @@ If this wheel generation fails, pip runs `setup.py clean` to clean up any build
artifacts that may have been generated. After that, pip will attempt a direct
installation.
-### Direct Installation
-
-When all else fails, pip will invoke `setup.py install` to install a package
-using setuptools' mechanisms to perform the installation. This is currently the
-last-resort fallback for projects that cannot be built into wheels, and may not
-be supported in the future.
-
### Editable Installation
For installing packages in "editable" mode
diff --git a/news/8368.removal.rst b/news/8368.removal.rst
new file mode 100644
index 00000000000..44ee33aa78c
--- /dev/null
+++ b/news/8368.removal.rst
@@ -0,0 +1,2 @@
+Remove ``setup.py install`` fallback when building a wheel failed for projects without
+``pyproject.toml``.
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index e9fc7ee3aa3..3c15ed4158c 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -30,7 +30,6 @@
check_legacy_setup_py_options,
)
from pip._internal.utils.compat import WINDOWS
-from pip._internal.utils.deprecation import LegacyInstallReasonFailedBdistWheel
from pip._internal.utils.filesystem import test_writable_dir
from pip._internal.utils.logging import getLogger
from pip._internal.utils.misc import (
@@ -423,26 +422,14 @@ def run(self, options: Values, args: List[str]) -> int:
global_options=global_options,
)
- # If we're using PEP 517, we cannot do a legacy setup.py install
- # so we fail here.
- pep517_build_failure_names: List[str] = [
- r.name for r in build_failures if r.use_pep517 # type: ignore
- ]
- if pep517_build_failure_names:
+ if build_failures:
raise InstallationError(
"Could not build wheels for {}, which is required to "
"install pyproject.toml-based projects".format(
- ", ".join(pep517_build_failure_names)
+ ", ".join(r.name for r in build_failures) # type: ignore
)
)
- # For now, we just warn about failures building legacy
- # requirements, as we'll fall through to a setup.py install for
- # those.
- for r in build_failures:
- if not r.use_pep517:
- r.legacy_install_reason = LegacyInstallReasonFailedBdistWheel
-
to_install = resolver.get_installation_order(requirement_set)
# Check for conflicts in the package set we're installing.
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index d4527295da3..7d92ba69983 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -361,20 +361,6 @@ def __str__(self) -> str:
)
-class LegacyInstallFailure(DiagnosticPipError):
- """Error occurred while executing `setup.py install`"""
-
- reference = "legacy-install-failure"
-
- def __init__(self, package_details: str) -> None:
- super().__init__(
- message="Encountered error while trying to install package.",
- context=package_details,
- hint_stmt="See above for output from the failure.",
- note_stmt="This is an issue with the package mentioned above, not pip.",
- )
-
-
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
"""A subprocess call failed."""
diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py
deleted file mode 100644
index 38bd542764e..00000000000
--- a/src/pip/_internal/operations/install/legacy.py
+++ /dev/null
@@ -1,117 +0,0 @@
-"""Legacy installation process, i.e. `setup.py install`.
-"""
-
-import logging
-import os
-from typing import List, Optional, Sequence
-
-from pip._internal.build_env import BuildEnvironment
-from pip._internal.exceptions import InstallationError, LegacyInstallFailure
-from pip._internal.locations.base import change_root
-from pip._internal.models.scheme import Scheme
-from pip._internal.utils.misc import ensure_dir
-from pip._internal.utils.setuptools_build import make_setuptools_install_args
-from pip._internal.utils.subprocess import runner_with_spinner_message
-from pip._internal.utils.temp_dir import TempDirectory
-
-logger = logging.getLogger(__name__)
-
-
-def write_installed_files_from_setuptools_record(
- record_lines: List[str],
- root: Optional[str],
- req_description: str,
-) -> None:
- def prepend_root(path: str) -> str:
- if root is None or not os.path.isabs(path):
- return path
- else:
- return change_root(root, path)
-
- for line in record_lines:
- directory = os.path.dirname(line)
- if directory.endswith(".egg-info"):
- egg_info_dir = prepend_root(directory)
- break
- else:
- message = (
- "{} did not indicate that it installed an "
- ".egg-info directory. Only setup.py projects "
- "generating .egg-info directories are supported."
- ).format(req_description)
- raise InstallationError(message)
-
- new_lines = []
- for line in record_lines:
- filename = line.strip()
- if os.path.isdir(filename):
- filename += os.path.sep
- new_lines.append(os.path.relpath(prepend_root(filename), egg_info_dir))
- new_lines.sort()
- ensure_dir(egg_info_dir)
- inst_files_path = os.path.join(egg_info_dir, "installed-files.txt")
- with open(inst_files_path, "w") as f:
- f.write("\n".join(new_lines) + "\n")
-
-
-def install(
- global_options: Sequence[str],
- root: Optional[str],
- home: Optional[str],
- prefix: Optional[str],
- use_user_site: bool,
- pycompile: bool,
- scheme: Scheme,
- setup_py_path: str,
- isolated: bool,
- req_name: str,
- build_env: BuildEnvironment,
- unpacked_source_directory: str,
- req_description: str,
-) -> bool:
- header_dir = scheme.headers
-
- with TempDirectory(kind="record") as temp_dir:
- try:
- record_filename = os.path.join(temp_dir.path, "install-record.txt")
- install_args = make_setuptools_install_args(
- setup_py_path,
- global_options=global_options,
- record_filename=record_filename,
- root=root,
- prefix=prefix,
- header_dir=header_dir,
- home=home,
- use_user_site=use_user_site,
- no_user_config=isolated,
- pycompile=pycompile,
- )
-
- runner = runner_with_spinner_message(
- f"Running setup.py install for {req_name}"
- )
- with build_env:
- runner(
- cmd=install_args,
- cwd=unpacked_source_directory,
- )
-
- if not os.path.exists(record_filename):
- logger.debug("Record file %s not found", record_filename)
- # Signal to the caller that we didn't install the new package
- return False
-
- except Exception as e:
- # Signal to the caller that we didn't install the new package
- raise LegacyInstallFailure(package_details=req_name) from e
-
- # At this point, we have successfully installed the requirement.
-
- # We intentionally do not use any encoding to read the file because
- # setuptools writes the file using distutils.file_util.write_file,
- # which does not specify an encoding.
- with open(record_filename) as f:
- record_lines = f.read().splitlines()
-
- write_installed_files_from_setuptools_record(record_lines, root, req_description)
- return True
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 940dbe02b73..baa6716381c 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -20,7 +20,7 @@
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
-from pip._internal.exceptions import InstallationError, LegacyInstallFailure
+from pip._internal.exceptions import InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import (
BaseDistribution,
@@ -39,11 +39,10 @@
from pip._internal.operations.install.editable_legacy import (
install_editable as install_editable_legacy,
)
-from pip._internal.operations.install.legacy import install as install_legacy
from pip._internal.operations.install.wheel import install_wheel
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
from pip._internal.req.req_uninstall import UninstallPathSet
-from pip._internal.utils.deprecation import LegacyInstallReason, deprecated
+from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
ConfiguredBuildBackendHookCaller,
@@ -93,7 +92,6 @@ def __init__(
self.constraint = constraint
self.editable = editable
self.permit_editable_wheels = permit_editable_wheels
- self.legacy_install_reason: Optional[LegacyInstallReason] = None
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
@@ -757,10 +755,9 @@ def install(
prefix=prefix,
)
- global_options = global_options if global_options is not None else []
if self.editable and not self.is_wheel:
install_editable_legacy(
- global_options=global_options,
+ global_options=global_options if global_options is not None else [],
prefix=prefix,
home=home,
use_user_site=use_user_site,
@@ -773,66 +770,20 @@ def install(
self.install_succeeded = True
return
- if self.is_wheel:
- assert self.local_file_path
- install_wheel(
- self.name,
- self.local_file_path,
- scheme=scheme,
- req_description=str(self.req),
- pycompile=pycompile,
- warn_script_location=warn_script_location,
- direct_url=self.download_info if self.original_link else None,
- requested=self.user_supplied,
- )
- self.install_succeeded = True
- return
-
- # TODO: Why don't we do this for editable installs?
-
- # Extend the list of global options passed on to
- # the setup.py call with the ones from the requirements file.
- # Options specified in requirements file override those
- # specified on the command line, since the last option given
- # to setup.py is the one that is used.
- global_options = list(global_options) + self.global_options
-
- try:
- if (
- self.legacy_install_reason is not None
- and self.legacy_install_reason.emit_before_install
- ):
- self.legacy_install_reason.emit_deprecation(self.name)
- success = install_legacy(
- global_options=global_options,
- root=root,
- home=home,
- prefix=prefix,
- use_user_site=use_user_site,
- pycompile=pycompile,
- scheme=scheme,
- setup_py_path=self.setup_py_path,
- isolated=self.isolated,
- req_name=self.name,
- build_env=self.build_env,
- unpacked_source_directory=self.unpacked_source_directory,
- req_description=str(self.req),
- )
- except LegacyInstallFailure as exc:
- self.install_succeeded = False
- raise exc
- except Exception:
- self.install_succeeded = True
- raise
+ assert self.is_wheel
+ assert self.local_file_path
- self.install_succeeded = success
-
- if (
- success
- and self.legacy_install_reason is not None
- and self.legacy_install_reason.emit_after_success
- ):
- self.legacy_install_reason.emit_deprecation(self.name)
+ install_wheel(
+ self.name,
+ self.local_file_path,
+ scheme=scheme,
+ req_description=str(self.req),
+ pycompile=pycompile,
+ warn_script_location=warn_script_location,
+ direct_url=self.download_info if self.original_link else None,
+ requested=self.user_supplied,
+ )
+ self.install_succeeded = True
def check_invalid_constraint_type(req: InstallRequirement) -> str:
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index db6daf7183d..72bd6f25a55 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -118,44 +118,3 @@ def deprecated(
raise PipDeprecationWarning(message)
warnings.warn(message, category=PipDeprecationWarning, stacklevel=2)
-
-
-class LegacyInstallReason:
- def __init__(
- self,
- reason: str,
- replacement: Optional[str] = None,
- gone_in: Optional[str] = None,
- feature_flag: Optional[str] = None,
- issue: Optional[int] = None,
- emit_after_success: bool = False,
- emit_before_install: bool = False,
- ):
- self._reason = reason
- self._replacement = replacement
- self._gone_in = gone_in
- self._feature_flag = feature_flag
- self._issue = issue
- self.emit_after_success = emit_after_success
- self.emit_before_install = emit_before_install
-
- def emit_deprecation(self, name: str) -> None:
- deprecated(
- reason=self._reason.format(name=name),
- replacement=self._replacement,
- gone_in=self._gone_in,
- feature_flag=self._feature_flag,
- issue=self._issue,
- )
-
-
-LegacyInstallReasonFailedBdistWheel = LegacyInstallReason(
- reason=(
- "{name} was installed using the legacy 'setup.py install' "
- "method, because a wheel could not be built for it."
- ),
- replacement="to fix the wheel build issue reported above",
- gone_in="23.1",
- issue=8368,
- emit_after_success=True,
-)
diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index 0662915cb05..96d1b246067 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -144,48 +144,3 @@ def make_setuptools_egg_info_args(
args += ["--egg-base", egg_info_dir]
return args
-
-
-def make_setuptools_install_args(
- setup_py_path: str,
- *,
- global_options: Sequence[str],
- record_filename: str,
- root: Optional[str],
- prefix: Optional[str],
- header_dir: Optional[str],
- home: Optional[str],
- use_user_site: bool,
- no_user_config: bool,
- pycompile: bool,
-) -> List[str]:
- assert not (use_user_site and prefix)
- assert not (use_user_site and root)
-
- args = make_setuptools_shim_args(
- setup_py_path,
- global_options=global_options,
- no_user_config=no_user_config,
- unbuffered_output=True,
- )
- args += ["install", "--record", record_filename]
- args += ["--single-version-externally-managed"]
-
- if root is not None:
- args += ["--root", root]
- if prefix is not None:
- args += ["--prefix", prefix]
- if home is not None:
- args += ["--home", home]
- if use_user_site:
- args += ["--user", "--prefix="]
-
- if pycompile:
- args += ["--compile"]
- else:
- args += ["--no-compile"]
-
- if header_dir:
- args += ["--install-headers", header_dir]
-
- return args
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 18446d89989..72c72f35c5d 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -829,7 +829,10 @@ def test_install_global_option(script: PipTestEnvironment) -> None:
(In particular those that disable the actual install action)
"""
result = script.pip(
- "install", "--global-option=--version", "INITools==0.1", expect_stderr=True
+ "install",
+ "--global-option=--version",
+ "INITools==0.1",
+ expect_error=True, # build is going to fail because of --version
)
assert "INITools==0.1\n" in result.stdout
assert not result.files_created
@@ -1157,7 +1160,6 @@ def test_install_package_with_prefix(
rel_prefix_path = script.scratch / "prefix"
install_path = join(
sysconfig.get_path("purelib", vars={"base": rel_prefix_path}),
- # we still test for egg-info because no-binary implies setup.py install
"simple-1.0.dist-info",
)
result.did_create(install_path)
@@ -1498,15 +1500,12 @@ def test_install_subprocess_output_handling(
# This error is emitted 3 times:
# - by setup.py bdist_wheel
# - by setup.py clean
- # - by setup.py install which is used as fallback when setup.py bdist_wheel failed
- # Before, it failed only once because it attempted only setup.py install.
- # TODO update this when we remove the last setup.py install code path.
- assert 3 == result.stderr.count("I DIE, I DIE")
+ assert 2 == result.stderr.count("I DIE, I DIE")
result = script.pip(
*(args + ["--global-option=--fail", "--verbose"]), expect_error=True
)
- assert 3 == result.stderr.count("I DIE, I DIE")
+ assert 2 == result.stderr.count("I DIE, I DIE")
def test_install_log(script: PipTestEnvironment, data: TestData, tmpdir: Path) -> None:
@@ -1526,22 +1525,9 @@ def test_install_topological_sort(script: PipTestEnvironment, data: TestData) ->
assert order1 in res or order2 in res, res
-def test_install_wheel_broken(script: PipTestEnvironment) -> None:
- res = script.pip_install_local("wheelbroken", allow_stderr_error=True)
- assert "ERROR: Failed building wheel for wheelbroken" in res.stderr
- # Fallback to setup.py install (https://github.com/pypa/pip/issues/8368)
- assert "Successfully installed wheelbroken-0.1" in str(res), str(res)
-
-
def test_cleanup_after_failed_wheel(script: PipTestEnvironment) -> None:
- res = script.pip_install_local("wheelbrokenafter", allow_stderr_error=True)
+ res = script.pip_install_local("wheelbrokenafter", expect_error=True)
assert "ERROR: Failed building wheel for wheelbrokenafter" in res.stderr
- # One of the effects of not cleaning up is broken scripts:
- script_py = script.bin_path / "script.py"
- assert script_py.exists(), script_py
- with open(script_py) as f:
- shebang = f.readline().strip()
- assert shebang != "#!python", shebang
# OK, assert that we *said* we were cleaning up:
# /!\ if in need to change this, also change test_pep517_no_legacy_cleanup
assert "Running setup.py clean for wheelbrokenafter" in str(res), str(res)
@@ -1568,38 +1554,26 @@ def test_install_builds_wheels(script: PipTestEnvironment, data: TestData) -> No
"-f",
data.find_links,
to_install,
- allow_stderr_error=True, # error building wheelbroken
- )
- expected = (
- "Successfully installed requires-wheelbroken-upper-0"
- " upper-2.0 wheelbroken-0.1"
+ expect_error=True, # error building wheelbroken
)
- # Must have installed it all
- assert expected in str(res), str(res)
wheels: List[str] = []
for _, _, files in os.walk(wheels_cache):
wheels.extend(f for f in files if f.endswith(".whl"))
- # and built wheels for upper and wheelbroken
+ # Built wheel for upper
assert "Building wheel for upper" in str(res), str(res)
+ # Built wheel for wheelbroken, but failed
assert "Building wheel for wheelb" in str(res), str(res)
+ assert "Failed to build wheelbroken" in str(res), str(res)
# Wheels are built for local directories, but not cached.
assert "Building wheel for requir" in str(res), str(res)
- # wheelbroken has to run install
# into the cache
assert wheels != [], str(res)
- # and installed from the wheel
- assert "Running setup.py install for upper" not in str(res), str(res)
- # Wheels are built for local directories, but not cached.
- assert "Running setup.py install for requir" not in str(res), str(res)
- # wheelbroken has to run install
- assert "Running setup.py install for wheelb" in str(res), str(res)
- # We want to make sure pure python wheels do not have an implementation tag
assert wheels == [
"Upper-2.0-py{}-none-any.whl".format(sys.version_info[0]),
]
-def test_install_no_binary_disables_building_wheels(
+def test_install_no_binary_builds_wheels(
script: PipTestEnvironment, data: TestData
) -> None:
to_install = data.packages.joinpath("requires_wheelbroken_upper")
@@ -1610,22 +1584,14 @@ def test_install_no_binary_disables_building_wheels(
"-f",
data.find_links,
to_install,
- allow_stderr_error=True, # error building wheelbroken
+ expect_error=True, # error building wheelbroken
)
- expected = (
- "Successfully installed requires-wheelbroken-upper-0"
- " upper-2.0 wheelbroken-0.1"
- )
- # Must have installed it all
- assert expected in str(res), str(res)
- # and built wheels for wheelbroken only
+ # Wheels are built for all requirements
assert "Building wheel for wheelb" in str(res), str(res)
- # Wheels are built for local directories, but not cached across runs
assert "Building wheel for requir" in str(res), str(res)
- # Don't build wheel for upper which was blacklisted
assert "Building wheel for upper" in str(res), str(res)
- # And these two fell back to sdist based installed.
- assert "Running setup.py install for wheelb" in str(res), str(res)
+ # Wheelbroken failed to build
+ assert "Failed to build wheelbroken" in str(res), str(res)
@pytest.mark.network
@@ -1639,7 +1605,6 @@ def test_install_no_binary_builds_pep_517_wheel(
assert expected in str(res), str(res)
assert "Building wheel for pep517-setup" in str(res), str(res)
- assert "Running setup.py install for pep517-set" not in str(res), str(res)
@pytest.mark.network
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 3ad9534810b..3ad1909fe7c 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -325,7 +325,6 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg(
"install", "--user", "--no-index", "-f", data.find_links, "requiresupper"
)
# Check that we are really installing a wheel
- assert "Running setup.py install for requiresupper" not in result.stdout
assert "installed requiresupper" in result.stdout
@@ -647,7 +646,7 @@ def test_install_distribution_union_with_constraints(
msg = "Unnamed requirements are not allowed as constraints"
assert msg in result.stderr
else:
- assert "Running setup.py install for LocalExtras" in result.stdout
+ assert "Building wheel for LocalExtras" in result.stdout
result.did_create(script.site_packages / "singlemodule.py")
diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py
index d7e8c26024f..971526c5181 100644
--- a/tests/functional/test_install_vcs_git.py
+++ b/tests/functional/test_install_vcs_git.py
@@ -392,7 +392,7 @@ def test_git_with_non_editable_unpacking(
)
result = script.pip(
"install",
- "--global-option=--version",
+ "--global-option=--quiet",
local_url,
allow_stderr_warning=True,
)
diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py
index a7e9022a5c4..b8ec0510a1e 100644
--- a/tests/functional/test_show.py
+++ b/tests/functional/test_show.py
@@ -1,14 +1,17 @@
import os
import pathlib
import re
+import textwrap
from pip import __version__
from pip._internal.commands.show import search_packages_info
-from pip._internal.operations.install.legacy import (
- write_installed_files_from_setuptools_record,
-)
from pip._internal.utils.unpacking import untar_file
-from tests.lib import PipTestEnvironment, TestData, create_test_package_with_setup
+from tests.lib import (
+ PipTestEnvironment,
+ TestData,
+ create_test_package_with_setup,
+ pyversion,
+)
def test_basic_show(script: PipTestEnvironment) -> None:
@@ -77,10 +80,19 @@ def test_show_with_files_from_legacy(
str(setuptools_record),
cwd=source_dir,
)
- write_installed_files_from_setuptools_record(
- setuptools_record.read_text().splitlines(),
- root=None,
- req_description="simple==1.0",
+ # Emulate the installed-files.txt generation which previous pip version did
+ # after running setup.py install (write_installed_files_from_setuptools_record).
+ egg_info_dir = script.site_packages_path / f"simple-1.0-py{pyversion}.egg-info"
+ egg_info_dir.joinpath("installed-files.txt").write_text(
+ textwrap.dedent(
+ """\
+ ../simple/__init__.py
+ PKG-INFO
+ SOURCES.txt
+ dependency_links.txt
+ top_level.txt
+ """
+ )
)
result = script.pip("show", "--files", "simple")
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index be369f1a051..4d5bee249b2 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -1,7 +1,6 @@
import collections
import logging
import os
-import subprocess
import textwrap
from optparse import Values
from pathlib import Path
@@ -880,14 +879,4 @@ def test_install_requirements_with_options(
)
)
- req.source_dir = os.curdir
- with mock.patch.object(subprocess, "Popen") as popen:
- popen.return_value.stdout.readline.return_value = b""
- try:
- req.install([])
- except Exception:
- pass
-
- last_call = popen.call_args_list[-1]
- args = last_call[0][0]
- assert 0 < args.index(global_option) < args.index("install")
+ assert req.global_options == [global_option]