Repository: pypa/pip
Pull request #11919 (2023-04-02 16:52:03)

Deprecate legacy projects ignoring config settings
towards #11915


Patch:
diff --git a/.github/workflows/label-merge-conflicts.yml b/.github/workflows/label-merge-conflicts.yml
new file mode 100644
index 000000000..1de897ca1
--- /dev/null
+++ b/.github/workflows/label-merge-conflicts.yml
@@ -0,0 +1,19 @@
+name: Autolabel merge conflicts
+
+permissions:
+  issues: write
+  pull-requests: write
+
+on:
+  push:
+    branches: [main]
+
+jobs:
+  label-merge-conflicts:
+    if: github.repository_owner == 'pypa'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: pradyunsg/auto-label-merge-conflicts@v3
+        with:
+          CONFLICT_LABEL_NAME: "needs rebase or merge"
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2fc455b9d..7a37d18c5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/'
 
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.4.0
+  rev: v4.3.0
   hooks:
   - id: check-builtin-literals
   - id: check-added-large-files
@@ -17,18 +17,18 @@ repos:
     exclude: .patch
 
 - repo: https://github.com/psf/black
-  rev: 23.1.0
+  rev: 22.6.0
   hooks:
   - id: black
 
 - repo: https://github.com/PyCQA/flake8
-  rev: 6.0.0
+  rev: 4.0.1
   hooks:
   - id: flake8
     additional_dependencies: [
-        'flake8-bugbear',
-        'flake8-logging-format',
-        'flake8-implicit-str-concat',
+        'flake8-bugbear==22.10.27',
+        'flake8-logging-format==0.9.0',
+        'flake8-implicit-str-concat==0.3.0',
     ]
     exclude: tests/data
 
@@ -56,7 +56,7 @@ repos:
     ]
 
 - repo: https://github.com/pre-commit/pygrep-hooks
-  rev: v1.10.0
+  rev: v1.9.0
   hooks:
   - id: python-no-log-warn
   - id: python-no-eval
diff --git a/docs/html/getting-started.md b/docs/html/getting-started.md
index 2b3f0bc93..0967b0eb9 100644
--- a/docs/html/getting-started.md
+++ b/docs/html/getting-started.md
@@ -98,5 +98,5 @@ Successfully uninstalled sampleproject
 ## Next Steps
 
 It is recommended to learn about what virtual environments are and how to use
-them. This is covered in the {doc}`Installing Packages <pypug:tutorials/installing-packages>`
+them. This is covered in the ["Installing Packages"](pypug:tutorials/installing-packages)
 tutorial on packaging.python.org.
diff --git a/docs/html/reference/build-system/setup-py.md b/docs/html/reference/build-system/setup-py.md
index 0103a3a6a..53917b8a4 100644
--- a/docs/html/reference/build-system/setup-py.md
+++ b/docs/html/reference/build-system/setup-py.md
@@ -24,6 +24,8 @@ 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.
 
@@ -56,6 +58,13 @@ 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/docs/pip_sphinxext.py b/docs/pip_sphinxext.py
index 2e5597022..f398b7d09 100644
--- a/docs/pip_sphinxext.py
+++ b/docs/pip_sphinxext.py
@@ -254,6 +254,7 @@ class PipCLIDirective(rst.Directive):
         lines = []
         # Create a tab for each OS
         for os, variant in os_variants.items():
+
             # Unpack the values
             prompt = variant["prompt"]
             highlighter = variant["highlighter"]
diff --git a/docs/requirements.txt b/docs/requirements.txt
index ef72c8fb7..fa3a7390c 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-sphinx ~= 6.0
+sphinx ~= 4.2, != 4.4.0
 towncrier
 furo
 myst_parser
diff --git a/news/11453.removal.rst b/news/11453.removal.rst
deleted file mode 100644
index 91ebfda04..000000000
--- a/news/11453.removal.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-``--no-binary`` does not disable the cache of locally built wheels anymore. It only
-means "don't download wheels".
diff --git a/news/11719.bugfix.rst b/news/11719.bugfix.rst
deleted file mode 100644
index c2ae8bc1d..000000000
--- a/news/11719.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Normalize paths before checking if installed scripts are on PATH.
diff --git a/news/11908.feature.rst b/news/11908.feature.rst
deleted file mode 100644
index 2b9ec18d9..000000000
--- a/news/11908.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Reduce the number of resolver rounds, since backjumping makes the resolver more efficient in finding solutions. This also makes pathological cases fail quicker.
diff --git a/news/11915.removal.rst b/news/11915.removal.rst
new file mode 100644
index 000000000..e54b5d574
--- /dev/null
+++ b/news/11915.removal.rst
@@ -0,0 +1,3 @@
+Using ``--config-settings`` with projects that don't have a ``pyproject.toml`` now print
+a deprecation warning. In the future the presence of config settings will automatically
+enable the default build backend for legacy projects and pass the setttings to it.
diff --git a/news/8368.removal.rst b/news/8368.removal.rst
deleted file mode 100644
index 44ee33aa7..000000000
--- a/news/8368.removal.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Remove ``setup.py install`` fallback when building a wheel failed for projects without
-``pyproject.toml``.
diff --git a/noxfile.py b/noxfile.py
index 565a50399..5c4683b7d 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -133,7 +133,6 @@ def docs(session: nox.Session) -> None:
         # fmt: off
         return [
             "sphinx-build",
-            "--keep-going",
             "-W",
             "-c", "docs/html",  # see note above
             "-d", "docs/build/doctrees/" + kind,
diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py
index 05f0a9acb..c53b7f023 100644
--- a/src/pip/_internal/cache.py
+++ b/src/pip/_internal/cache.py
@@ -6,13 +6,14 @@ import json
 import logging
 import os
 from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Set
 
 from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
 from pip._vendor.packaging.utils import canonicalize_name
 
 from pip._internal.exceptions import InvalidWheelFilename
 from pip._internal.models.direct_url import DirectUrl
+from pip._internal.models.format_control import FormatControl
 from pip._internal.models.link import Link
 from pip._internal.models.wheel import Wheel
 from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
@@ -32,13 +33,25 @@ def _hash_dict(d: Dict[str, str]) -> str:
 class Cache:
     """An abstract class - provides cache directories for data from links
 
+
     :param cache_dir: The root of the cache.
+    :param format_control: An object of FormatControl class to limit
+        binaries being read from the cache.
+    :param allowed_formats: which formats of files the cache should store.
+        ('binary' and 'source' are the only allowed values)
     """
 
-    def __init__(self, cache_dir: str) -> None:
+    def __init__(
+        self, cache_dir: str, format_control: FormatControl, allowed_formats: Set[str]
+    ) -> None:
         super().__init__()
         assert not cache_dir or os.path.isabs(cache_dir)
         self.cache_dir = cache_dir or None
+        self.format_control = format_control
+        self.allowed_formats = allowed_formats
+
+        _valid_formats = {"source", "binary"}
+        assert self.allowed_formats.union(_valid_formats) == _valid_formats
 
     def _get_cache_path_parts(self, link: Link) -> List[str]:
         """Get parts of part that must be os.path.joined with cache_dir"""
@@ -78,6 +91,10 @@ class Cache:
         if can_not_cache:
             return []
 
+        formats = self.format_control.get_allowed_formats(canonical_package_name)
+        if not self.allowed_formats.intersection(formats):
+            return []
+
         candidates = []
         path = self.get_path_for_link(link)
         if os.path.isdir(path):
@@ -104,8 +121,8 @@ class Cache:
 class SimpleWheelCache(Cache):
     """A cache of wheels for future installs."""
 
-    def __init__(self, cache_dir: str) -> None:
-        super().__init__(cache_dir)
+    def __init__(self, cache_dir: str, format_control: FormatControl) -> None:
+        super().__init__(cache_dir, format_control, {"binary"})
 
     def get_path_for_link(self, link: Link) -> str:
         """Return a directory to store cached wheels for link
@@ -174,13 +191,13 @@ class SimpleWheelCache(Cache):
 class EphemWheelCache(SimpleWheelCache):
     """A SimpleWheelCache that creates it's own temporary cache directory"""
 
-    def __init__(self) -> None:
+    def __init__(self, format_control: FormatControl) -> None:
         self._temp_dir = TempDirectory(
             kind=tempdir_kinds.EPHEM_WHEEL_CACHE,
             globally_managed=True,
         )
 
-        super().__init__(self._temp_dir.path)
+        super().__init__(self._temp_dir.path, format_control)
 
 
 class CacheEntry:
@@ -204,10 +221,14 @@ class WheelCache(Cache):
     when a certain link is not found in the simple wheel cache first.
     """
 
-    def __init__(self, cache_dir: str) -> None:
-        super().__init__(cache_dir)
-        self._wheel_cache = SimpleWheelCache(cache_dir)
-        self._ephem_cache = EphemWheelCache()
+    def __init__(
+        self, cache_dir: str, format_control: Optional[FormatControl] = None
+    ) -> None:
+        if format_control is None:
+            format_control = FormatControl()
+        super().__init__(cache_dir, format_control, {"binary"})
+        self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
+        self._ephem_cache = EphemWheelCache(format_control)
 
     def get_path_for_link(self, link: Link) -> str:
         return self._wheel_cache.get_path_for_link(link)
diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py
index 637fba18c..5bd7e67e6 100644
--- a/src/pip/_internal/cli/base_command.py
+++ b/src/pip/_internal/cli/base_command.py
@@ -122,15 +122,6 @@ class Command(CommandContextMixIn):
             user_log_file=options.log,
         )
 
-        always_enabled_features = set(options.features_enabled) & set(
-            cmdoptions.ALWAYS_ENABLED_FEATURES
-        )
-        if always_enabled_features:
-            logger.warning(
-                "The following features are always enabled: %s. ",
-                ", ".join(sorted(always_enabled_features)),
-            )
-
         # TODO: Try to get these passing down from the command?
         #       without resorting to os.environ to hold these.
         #       This also affects isolated builds and it should.
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 7f72332db..c27ba1c6a 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -994,11 +994,6 @@ no_python_version_warning: Callable[..., Option] = partial(
 )
 
 
-# Features that are now always on. A warning is printed if they are used.
-ALWAYS_ENABLED_FEATURES = [
-    "no-binary-enable-wheel-cache",  # always on since 23.1
-]
-
 use_new_feature: Callable[..., Option] = partial(
     Option,
     "--use-feature",
@@ -1009,8 +1004,8 @@ use_new_feature: Callable[..., Option] = partial(
     choices=[
         "fast-deps",
         "truststore",
-    ]
-    + ALWAYS_ENABLED_FEATURES,
+        "no-binary-enable-wheel-cache",
+    ],
     help="Enable new functionality, that may be backward incompatible.",
 )
 
diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py
index e96d2b492..c5f03302d 100644
--- a/src/pip/_internal/commands/cache.py
+++ b/src/pip/_internal/commands/cache.py
@@ -37,6 +37,7 @@ class CacheCommand(Command):
     """
 
     def add_options(self) -> None:
+
         self.cmd_opts.add_option(
             "--format",
             action="store",
diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py
index 584df9f55..3864220b2 100644
--- a/src/pip/_internal/commands/check.py
+++ b/src/pip/_internal/commands/check.py
@@ -20,6 +20,7 @@ class CheckCommand(Command):
       %prog [options]"""
 
     def run(self, options: Values, args: List[str]) -> int:
+
         package_set, parsing_probs = create_package_set_from_installed()
         missing, conflicting = check_package_set(package_set)
 
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 36e947c8c..90388d118 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -76,6 +76,7 @@ class DownloadCommand(RequirementCommand):
 
     @with_cleanup
     def run(self, options: Values, args: List[str]) -> int:
+
         options.ignore_installed = True
         # editable doesn't really make sense for `pip download`, but the bowels
         # of the RequirementSet code require that property.
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 3c15ed415..4783f807f 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -30,6 +30,10 @@ from pip._internal.req.req_install import (
     check_legacy_setup_py_options,
 )
 from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.deprecation import (
+    LegacyInstallReasonFailedBdistWheel,
+    deprecated,
+)
 from pip._internal.utils.filesystem import test_writable_dir
 from pip._internal.utils.logging import getLogger
 from pip._internal.utils.misc import (
@@ -342,7 +346,24 @@ class InstallCommand(RequirementCommand):
             reqs = self.get_requirements(args, options, finder, session)
             check_legacy_setup_py_options(options, reqs)
 
-            wheel_cache = WheelCache(options.cache_dir)
+            if "no-binary-enable-wheel-cache" in options.features_enabled:
+                # TODO: remove format_control from WheelCache when the deprecation cycle
+                # is over
+                wheel_cache = WheelCache(options.cache_dir)
+            else:
+                if options.format_control.no_binary:
+                    deprecated(
+                        reason=(
+                            "--no-binary currently disables reading from "
+                            "the cache of locally built wheels. In the future "
+                            "--no-binary will not influence the wheel cache."
+                        ),
+                        replacement="to use the --no-cache-dir option",
+                        feature_flag="no-binary-enable-wheel-cache",
+                        issue=11453,
+                        gone_in="23.1",
+                    )
+                wheel_cache = WheelCache(options.cache_dir, options.format_control)
 
             # Only when installing is it permitted to use PEP 660.
             # In other circumstances (pip wheel, pip download) we generate
@@ -422,14 +443,26 @@ class InstallCommand(RequirementCommand):
                 global_options=global_options,
             )
 
-            if build_failures:
+            # 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:
                 raise InstallationError(
                     "Could not build wheels for {}, which is required to "
                     "install pyproject.toml-based projects".format(
-                        ", ".join(r.name for r in build_failures)  # type: ignore
+                        ", ".join(pep517_build_failure_names)
                     )
                 )
 
+            # 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/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index c6a588ff0..a8483559c 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -14,6 +14,7 @@ from pip._internal.req.req_install import (
     InstallRequirement,
     check_legacy_setup_py_options,
 )
+from pip._internal.utils.deprecation import deprecated
 from pip._internal.utils.misc import ensure_dir, normalize_path
 from pip._internal.utils.temp_dir import TempDirectory
 from pip._internal.wheel_builder import build, should_build_for_wheel_command
@@ -42,6 +43,7 @@ class WheelCommand(RequirementCommand):
       %prog [options] <archive url/path> ..."""
 
     def add_options(self) -> None:
+
         self.cmd_opts.add_option(
             "-w",
             "--wheel-dir",
@@ -105,6 +107,7 @@ class WheelCommand(RequirementCommand):
         session = self.get_default_session(options)
 
         finder = self._build_package_finder(options, session)
+        wheel_cache = WheelCache(options.cache_dir, options.format_control)
 
         options.wheel_dir = normalize_path(options.wheel_dir)
         ensure_dir(options.wheel_dir)
@@ -120,7 +123,24 @@ class WheelCommand(RequirementCommand):
         reqs = self.get_requirements(args, options, finder, session)
         check_legacy_setup_py_options(options, reqs)
 
-        wheel_cache = WheelCache(options.cache_dir)
+        if "no-binary-enable-wheel-cache" in options.features_enabled:
+            # TODO: remove format_control from WheelCache when the deprecation cycle
+            # is over
+            wheel_cache = WheelCache(options.cache_dir)
+        else:
+            if options.format_control.no_binary:
+                deprecated(
+                    reason=(
+                        "--no-binary currently disables reading from "
+                        "the cache of locally built wheels. In the future "
+                        "--no-binary will not influence the wheel cache."
+                    ),
+                    replacement="to use the --no-cache-dir option",
+                    feature_flag="no-binary-enable-wheel-cache",
+                    issue=11453,
+                    gone_in="23.1",
+                )
+            wheel_cache = WheelCache(options.cache_dir, options.format_control)
 
         preparer = self.make_requirement_preparer(
             temp_build_dir=directory,
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 7d92ba699..d4527295d 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -361,6 +361,20 @@ class MetadataInconsistent(InstallationError):
         )
 
 
+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/index/sources.py b/src/pip/_internal/index/sources.py
index cd9cb8d40..eec3f12f7 100644
--- a/src/pip/_internal/index/sources.py
+++ b/src/pip/_internal/index/sources.py
@@ -171,6 +171,7 @@ def build_source(
     expand_dir: bool,
     cache_link_parsing: bool,
 ) -> Tuple[Optional[str], Optional[LinkSource]]:
+
     path: Optional[str] = None
     url: Optional[str] = None
     if os.path.exists(location):  # Is a local path.
diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py
index fe61e8116..a64af7389 100644
--- a/src/pip/_internal/models/search_scope.py
+++ b/src/pip/_internal/models/search_scope.py
@@ -79,6 +79,7 @@ class SearchScope:
         redacted_index_urls = []
         if self.index_urls and self.index_urls != [PyPI.simple_url]:
             for url in self.index_urls:
+
                 redacted_index_url = redact_auth_from_url(url)
 
                 # Parse the URL
diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py
index 6c40ade15..e512ac784 100644
--- a/src/pip/_internal/network/session.py
+++ b/src/pip/_internal/network/session.py
@@ -316,6 +316,7 @@ class InsecureCacheControlAdapter(CacheControlAdapter):
 
 
 class PipSession(requests.Session):
+
     timeout: Optional[int] = None
 
     def __init__(
diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py
new file mode 100644
index 000000000..0b108d0ca
--- /dev/null
+++ b/src/pip/_internal/operations/install/legacy.py
@@ -0,0 +1,118 @@
+"""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/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index a8cd1330f..c79941398 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -143,18 +143,16 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
 
     # We don't want to warn for directories that are on PATH.
     not_warn_dirs = [
-        os.path.normcase(os.path.normpath(i)).rstrip(os.sep)
+        os.path.normcase(i).rstrip(os.sep)
         for i in os.environ.get("PATH", "").split(os.pathsep)
     ]
     # If an executable sits with sys.executable, we don't warn for it.
     #     This covers the case of venv invocations without activating the venv.
-    not_warn_dirs.append(
-        os.path.normcase(os.path.normpath(os.path.dirname(sys.executable)))
-    )
+    not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
     warn_for: Dict[str, Set[str]] = {
         parent_dir: scripts
         for parent_dir, scripts in grouped_by_dir.items()
-        if os.path.normcase(os.path.normpath(parent_dir)) not in not_warn_dirs
+        if os.path.normcase(parent_dir) not in not_warn_dirs
     }
     if not warn_for:
         return None
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
index 57fef5707..eb8e12b2d 100644
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -91,7 +91,7 @@ def load_pyproject_toml(
     # If we haven't worked out whether to use PEP 517 yet,
     # and the user hasn't explicitly stated a preference,
     # we do so if the project has a pyproject.toml file
-    # or if we cannot import setuptools.
+    # or if we cannot import setuptools or wheels.
 
     # We fallback to PEP 517 when without setuptools or without the wheel package,
     # so setuptools can be installed as a default build backend.
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index dc82a7e4f..37dbd32e7 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -211,6 +211,7 @@ def install_req_from_editable(
     permit_editable_wheels: bool = False,
     config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
 ) -> InstallRequirement:
+
     parts = parse_req_from_editable(editable_req)
 
     return InstallRequirement(
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index aced95e64..f8f07b0cd 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -165,6 +165,7 @@ def handle_requirement_line(
     line: ParsedLine,
     options: Optional[optparse.Values] = None,
 ) -> ParsedRequirement:
+
     # preserve for the nested code path
     line_comes_from = "{} {} (line {})".format(
         "-c" if line.constraint else "-r",
@@ -209,6 +210,7 @@ def handle_option_line(
     options: Optional[optparse.Values] = None,
     session: Optional[PipSession] = None,
 ) -> None:
+
     if options:
         # percolate options upward
         if opts.require_hashes:
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index baa671638..2c628d36e 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -20,7 +20,7 @@ from pip._vendor.packaging.version import parse as parse_version
 from pip._vendor.pyproject_hooks import BuildBackendHookCaller
 
 from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
-from pip._internal.exceptions import InstallationError
+from pip._internal.exceptions import InstallationError, LegacyInstallFailure
 from pip._internal.locations import get_scheme
 from pip._internal.metadata import (
     BaseDistribution,
@@ -39,10 +39,15 @@ from pip._internal.operations.build.metadata_legacy import (
 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 deprecated
+from pip._internal.utils.deprecation import LegacyInstallReason, deprecated
+from pip._internal.utils.direct_url_helpers import (
+    direct_url_for_editable,
+    direct_url_from_link,
+)
 from pip._internal.utils.hashes import Hashes
 from pip._internal.utils.misc import (
     ConfiguredBuildBackendHookCaller,
@@ -92,6 +97,7 @@ class InstallRequirement:
         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
@@ -471,6 +477,15 @@ class InstallRequirement:
         )
 
         if pyproject_toml_data is None:
+            if self.config_settings:
+                deprecated(
+                    reason=f"Config settings are ignored for project {self}.",
+                    replacement=(
+                        "to use --use-pep517 or add a "
+                        "pyproject.toml file to the project"
+                    ),
+                    gone_in="23.3",
+                )
             self.use_pep517 = False
             return
 
@@ -755,9 +770,10 @@ class InstallRequirement:
             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 if global_options is not None else [],
+                global_options=global_options,
                 prefix=prefix,
                 home=home,
                 use_user_site=use_user_site,
@@ -770,23 +786,80 @@ class InstallRequirement:
             self.install_succeeded = True
             return
 
-        assert self.is_wheel
-        assert self.local_file_path
+        if self.is_wheel:
+            assert self.local_file_path
+            direct_url = None
+            # TODO this can be refactored to direct_url = self.download_info
+            if self.editable:
+                direct_url = direct_url_for_editable(self.unpacked_source_directory)
+            elif self.original_link:
+                direct_url = direct_url_from_link(
+                    self.original_link,
+                    self.source_dir,
+                    self.original_link_is_in_wheel_cache,
+                )
+            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=direct_url,
+                requested=self.user_supplied,
+            )
+            self.install_succeeded = True
+            return
 
-        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
+        # 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
+
+        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)
 
 
 def check_invalid_constraint_type(req: InstallRequirement) -> str:
+
     # Check for unsupported forms
     problem = ""
     if not req.name:
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index 0ad4641b1..a4c24b52a 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -632,6 +632,7 @@ class Factory:
         e: "ResolutionImpossible[Requirement, Candidate]",
         constraints: Dict[str, Constraint],
     ) -> InstallationError:
+
         assert e.causes, "Installation error reported with no cause"
 
         # If one of the things we can't solve is "we need Python X.Y",
diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py
index 06addc0dd..f561f1f1e 100644
--- a/src/pip/_internal/resolution/resolvelib/requirements.py
+++ b/src/pip/_internal/resolution/resolvelib/requirements.py
@@ -64,6 +64,7 @@ class SpecifierRequirement(Requirement):
         return format_name(self.project_name, self._extras)
 
     def format_for_error(self) -> str:
+
         # Convert comma-separated specifiers into "A, B, ..., F and G"
         # This makes the specifier a bit more "human readable", without
         # risking a change in meaning. (Hopefully! Not all edge cases have
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
index 47bbfecce..a605d6c25 100644
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -88,9 +88,9 @@ class Resolver(BaseResolver):
         )
 
         try:
-            limit_how_complex_resolution_can_be = 200000
+            try_to_avoid_resolution_too_deep = 2000000
             result = self._result = resolver.resolve(
-                collected.requirements, max_rounds=limit_how_complex_resolution_can_be
+                collected.requirements, max_rounds=try_to_avoid_resolution_too_deep
             )
 
         except ResolutionImpossible as e:
diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py
index 72bd6f25a..db6daf718 100644
--- a/src/pip/_internal/utils/deprecation.py
+++ b/src/pip/_internal/utils/deprecation.py
@@ -118,3 +118,44 @@ 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 96d1b2460..0662915cb 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -144,3 +144,48 @@ 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


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 57dd7e68a..13011f4fd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -432,6 +432,7 @@ def virtualenv_template(
     wheel_install: Path,
     coverage_install: Path,
 ) -> Iterator[VirtualEnvironment]:
+
     venv_type: VirtualEnvironmentType
     if request.config.getoption("--use-venv"):
         venv_type = "venv"
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 22a71cd32..93a6b930f 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -106,6 +106,7 @@ def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
 
 
 def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
+
     create_basic_wheel_for_package(script, "foo", "2.0")
     create_basic_wheel_for_package(script, "bar", "1.0")
     create_basic_wheel_for_package(script, "bar", "3.0")
@@ -205,6 +206,7 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
 
 @pytest.mark.usefixtures("enable_user_site")
 def test_build_env_isolation(script: PipTestEnvironment) -> None:
+
     # Create dummy `pkg` wheel.
     pkg_whl = create_basic_wheel_for_package(script, "pkg", "1.0")
 
diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py
index 788abdd2b..7d20f5e31 100644
--- a/tests/functional/test_cache.py
+++ b/tests/functional/test_cache.py
@@ -107,7 +107,7 @@ def list_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
           `- foo-1.2.3-py3-none-any.whl `."""
     lines = result.stdout.splitlines()
     expected = f" - {wheel_name}-py3-none-any.whl "
-    return any(map(lambda line: line.startswith(expected), lines))
+    return any(map(lambda l: l.startswith(expected), lines))
 
 
 def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
@@ -121,9 +121,7 @@ def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
     expected = f"{wheel_name}-py3-none-any.whl"
     return any(
         map(
-            lambda line: (
-                os.path.basename(line).startswith(expected) and os.path.exists(line)
-            ),
+            lambda l: os.path.basename(l).startswith(expected) and os.path.exists(l),
             lines,
         )
     )
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 72c72f35c..97ccfa911 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -829,10 +829,7 @@ 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_error=True,  # build is going to fail because of --version
+        "install", "--global-option=--version", "INITools==0.1", expect_stderr=True
     )
     assert "INITools==0.1\n" in result.stdout
     assert not result.files_created
@@ -1160,6 +1157,7 @@ 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)
@@ -1500,12 +1498,15 @@ def test_install_subprocess_output_handling(
     # This error is emitted 3 times:
     # - by setup.py bdist_wheel
     # - by setup.py clean
-    assert 2 == result.stderr.count("I DIE, I DIE")
+    # - 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")
 
     result = script.pip(
         *(args + ["--global-option=--fail", "--verbose"]), expect_error=True
     )
-    assert 2 == result.stderr.count("I DIE, I DIE")
+    assert 3 == result.stderr.count("I DIE, I DIE")
 
 
 def test_install_log(script: PipTestEnvironment, data: TestData, tmpdir: Path) -> None:
@@ -1525,9 +1526,22 @@ 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", expect_error=True)
+    res = script.pip_install_local("wheelbrokenafter", allow_stderr_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)
@@ -1554,26 +1568,38 @@ def test_install_builds_wheels(script: PipTestEnvironment, data: TestData) -> No
         "-f",
         data.find_links,
         to_install,
-        expect_error=True,  # error building wheelbroken
+        allow_stderr_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)
     wheels: List[str] = []
     for _, _, files in os.walk(wheels_cache):
         wheels.extend(f for f in files if f.endswith(".whl"))
-    # Built wheel for upper
+    # and built wheels for upper and wheelbroken
     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_builds_wheels(
+def test_install_no_binary_disables_building_wheels(
     script: PipTestEnvironment, data: TestData
 ) -> None:
     to_install = data.packages.joinpath("requires_wheelbroken_upper")
@@ -1584,14 +1610,22 @@ def test_install_no_binary_builds_wheels(
         "-f",
         data.find_links,
         to_install,
-        expect_error=True,  # error building wheelbroken
+        allow_stderr_error=True,  # error building wheelbroken
+    )
+    expected = (
+        "Successfully installed requires-wheelbroken-upper-0"
+        " upper-2.0 wheelbroken-0.1"
     )
-    # Wheels are built for all requirements
+    # Must have installed it all
+    assert expected in str(res), str(res)
+    # and built wheels for wheelbroken only
     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)
-    # Wheelbroken failed to build
-    assert "Failed to build wheelbroken" 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)
 
 
 @pytest.mark.network
@@ -1605,6 +1639,7 @@ 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
@@ -1621,7 +1656,7 @@ def test_install_no_binary_uses_local_backend(
     assert os.path.isfile(marker), "Local PEP 517 backend not used"
 
 
-def test_install_no_binary_uses_cached_wheels(
+def test_install_no_binary_disables_cached_wheels(
     script: PipTestEnvironment, data: TestData
 ) -> None:
     # Seed the cache
@@ -1638,7 +1673,7 @@ def test_install_no_binary_uses_cached_wheels(
     )
     assert "Successfully installed upper-2.0" in str(res), str(res)
     # upper is built and not obtained from cache
-    assert "Building wheel for upper" not in str(res), str(res)
+    assert "Building wheel for upper" in str(res), str(res)
 
 
 def test_install_editable_with_wrong_egg_name(
@@ -2182,6 +2217,7 @@ def test_install_skip_work_dir_pkg(script: PipTestEnvironment, data: TestData) -
 def test_install_verify_package_name_normalization(
     script: PipTestEnvironment, package_name: str
 ) -> None:
+
     """
     Test that install of a package again using a name which
     normalizes to the original package name, is a no-op
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 3ad1909fe..3ad953481 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -325,6 +325,7 @@ 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
 
 
@@ -646,7 +647,7 @@ def test_install_distribution_union_with_constraints(
         msg = "Unnamed requirements are not allowed as constraints"
         assert msg in result.stderr
     else:
-        assert "Building wheel for LocalExtras" in result.stdout
+        assert "Running setup.py install 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 971526c51..d7e8c2602 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=--quiet",
+        "--global-option=--version",
         local_url,
         allow_stderr_warning=True,
     )
diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py
index b8ec0510a..a7e9022a5 100644
--- a/tests/functional/test_show.py
+++ b/tests/functional/test_show.py
@@ -1,17 +1,14 @@
 import os
 import pathlib
 import re
-import textwrap
 
 from pip import __version__
 from pip._internal.commands.show import search_packages_info
-from pip._internal.utils.unpacking import untar_file
-from tests.lib import (
-    PipTestEnvironment,
-    TestData,
-    create_test_package_with_setup,
-    pyversion,
+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
 
 
 def test_basic_show(script: PipTestEnvironment) -> None:
@@ -80,19 +77,10 @@ def test_show_with_files_from_legacy(
         str(setuptools_record),
         cwd=source_dir,
     )
-    # 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
-            """
-        )
+    write_installed_files_from_setuptools_record(
+        setuptools_record.read_text().splitlines(),
+        root=None,
+        req_description="simple==1.0",
     )
 
     result = script.pip("show", "--files", "simple")
diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py
index daec5fc6c..71a50fca6 100644
--- a/tests/unit/test_base_command.py
+++ b/tests/unit/test_base_command.py
@@ -22,6 +22,7 @@ def fixed_time() -> Iterator[None]:
 
 
 class FakeCommand(Command):
+
     _name = "fake"
 
     def __init__(
diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py
index d0fee69c3..f27daa266 100644
--- a/tests/unit/test_cache.py
+++ b/tests/unit/test_cache.py
@@ -4,12 +4,13 @@ from pathlib import Path
 from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
 
 from pip._internal.cache import WheelCache, _hash_dict
+from pip._internal.models.format_control import FormatControl
 from pip._internal.models.link import Link
 from pip._internal.utils.misc import ensure_dir
 
 
 def test_falsey_path_none() -> None:
-    wc = WheelCache("")
+    wc = WheelCache("", FormatControl())
     assert wc.cache_dir is None
 
 
@@ -17,7 +18,7 @@ def test_subdirectory_fragment() -> None:
     """
     Test the subdirectory URL fragment is part of the cache key.
     """
-    wc = WheelCache("/tmp/.foo/")
+    wc = WheelCache("/tmp/.foo/", FormatControl())
     link1 = Link("git+https://g.c/o/r#subdirectory=d1")
     link2 = Link("git+https://g.c/o/r#subdirectory=d2")
     assert wc.get_path_for_link(link1) != wc.get_path_for_link(link2)
@@ -28,7 +29,7 @@ def test_wheel_name_filter(tmpdir: Path) -> None:
     Test the wheel cache filters on wheel name when several wheels
     for different package are stored under the same cache directory.
     """
-    wc = WheelCache(os.fspath(tmpdir))
+    wc = WheelCache(os.fspath(tmpdir), FormatControl())
     link = Link("https://g.c/package.tar.gz")
     cache_path = wc.get_path_for_link(link)
     ensure_dir(cache_path)
@@ -56,7 +57,7 @@ def test_link_to_cache(tmpdir: Path) -> None:
     Test that Link.from_json() produces Links with consistent cache
     locations
     """
-    wc = WheelCache(os.fspath(tmpdir))
+    wc = WheelCache(os.fspath(tmpdir), FormatControl())
     # Define our expectations for stable cache path.
     i_name = interpreter_name()
     i_version = interpreter_version()
@@ -94,7 +95,7 @@ def test_link_to_cache(tmpdir: Path) -> None:
 
 
 def test_get_cache_entry(tmpdir: Path) -> None:
-    wc = WheelCache(os.fspath(tmpdir))
+    wc = WheelCache(os.fspath(tmpdir), FormatControl())
     persi_link = Link("https://g.c/o/r/persi")
     persi_path = wc.get_path_for_link(persi_link)
     ensure_dir(persi_path)
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 26a2ce4b9..47307c00e 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -1014,7 +1014,6 @@ def test_link_collector_create_find_links_expansion(
     """
     Test "~" expansion in --find-links paths.
     """
-
     # This is a mock version of expanduser() that expands "~" to the tmpdir.
     def expand_path(path: str) -> str:
         if path.startswith("~/"):
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
index 311be5888..df4957d59 100644
--- a/tests/unit/test_link.py
+++ b/tests/unit/test_link.py
@@ -108,7 +108,7 @@ class TestLink:
     )
     def test_invalid_egg_fragments(self, fragment: str) -> None:
         url = f"git+https://example.com/package#egg={fragment}"
-        with pytest.raises(ValueError):
+        with pytest.raises(Exception):
             Link(url)
 
     @pytest.mark.parametrize(
diff --git a/tests/unit/test_network_cache.py b/tests/unit/test_network_cache.py
index a5519864f..8764b1343 100644
--- a/tests/unit/test_network_cache.py
+++ b/tests/unit/test_network_cache.py
@@ -24,6 +24,7 @@ class TestSafeFileCache:
     """
 
     def test_cache_roundtrip(self, cache_tmpdir: Path) -> None:
+
         cache = SafeFileCache(os.fspath(cache_tmpdir))
         assert cache.get("test key") is None
         cache.set("test key", b"a test string")
diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py
index 9e3a0a5d6..39396512a 100644
--- a/tests/unit/test_options.py
+++ b/tests/unit/test_options.py
@@ -454,6 +454,7 @@ class TestCountOptions(AddFakeCommandMixin):
 
 
 class TestGeneralOptions(AddFakeCommandMixin):
+
     # the reason to specifically test general options is due to the
     # extra processing they receive, and the number of bugs we've had
 
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index a5286c13a..c46883dc2 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -25,6 +25,7 @@ from pip._internal.exceptions import (
 from pip._internal.index.package_finder import PackageFinder
 from pip._internal.metadata import select_backend
 from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
+from pip._internal.models.format_control import FormatControl
 from pip._internal.models.link import Link
 from pip._internal.network.session import PipSession
 from pip._internal.operations.build.build_tracker import get_build_tracker
@@ -402,7 +403,7 @@ class TestRequirementSet:
         """Test download_info hash is not set for an archive with legacy cache entry."""
         url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri()
         finder = make_test_finder()
-        wheel_cache = WheelCache(str(tmp_path / "cache"))
+        wheel_cache = WheelCache(str(tmp_path / "cache"), FormatControl())
         cache_entry_dir = wheel_cache.get_path_for_link(Link(url))
         Path(cache_entry_dir).mkdir(parents=True)
         wheel.make_wheel(name="simple", version="1.0").save_to_dir(cache_entry_dir)
@@ -425,7 +426,7 @@ class TestRequirementSet:
         url = shared_data.packages.joinpath("simple-1.0.tar.gz").as_uri()
         hash = "sha256=ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6"
         finder = make_test_finder()
-        wheel_cache = WheelCache(str(tmp_path / "cache"))
+        wheel_cache = WheelCache(str(tmp_path / "cache"), FormatControl())
         cache_entry_dir = wheel_cache.get_path_for_link(Link(url))
         Path(cache_entry_dir).mkdir(parents=True)
         Path(cache_entry_dir).joinpath("origin.json").write_text(
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 4d5bee249..30cbcf71c 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -1,6 +1,7 @@
 import collections
 import logging
 import os
+import subprocess
 import textwrap
 from optparse import Values
 from pathlib import Path
@@ -612,6 +613,7 @@ class TestBreakOptionsArgs:
 
 
 class TestOptionVariants:
+
     # this suite is really just testing optparse, but added it anyway
 
     def test_variant1(
@@ -879,4 +881,14 @@ class TestParseRequirements:
                 )
             )
 
-        assert req.global_options == [global_option]
+        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")
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index a67a7c110..1daaecbf4 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -53,6 +53,7 @@ class Tests_EgglinkPath:
     "util.egg_link_path_from_location() tests"
 
     def setup_method(self) -> None:
+
         project = "foo"
 
         self.mock_dist = Mock(project_name=project)
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index 6d6d1a3dc..c5a8f3be4 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -518,6 +518,7 @@ class TestInstallUnpackedWheel:
 
 
 class TestMessageAboutScriptsNotOnPATH:
+
     tilde_warning_msg = (
         "NOTE: The current PATH contains path(s) starting with `~`, "
         "which may not be expanded by all applications."
@@ -588,12 +589,6 @@ class TestMessageAboutScriptsNotOnPATH:
         )
         assert retval is None
 
-    def test_PATH_check_path_normalization(self) -> None:
-        retval = self._template(
-            paths=["/a/./b/../b//c/", "/d/e/bin"], scripts=["/a/b/c/foo"]
-        )
-        assert retval is None
-
     def test_single_script__single_dir_on_PATH(self) -> None:
         retval = self._template(paths=["/a/b", "/c/d/bin"], scripts=["/a/b/foo"])
         assert retval is None