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]