Repository: pypa/pip
Pull request #13451 (2025-06-27 23:27:11)

Add rich based spinner
Broken out of https://github.com/pypa/pip/pull/13450. Our legacy spinners can't be used for that PR because they require that their `spin()` method is called manually (and thus are only really functional with the subprocess utility). Instead, let rich handle the updates in the background for us. 

Can be tested manually like so:

```python
import time
import logging.config  # this is a bug with pip's logging stack
from pip._internal.cli.spinners import open_rich_spinner
from pip._internal.utils.logging import setup_logging

setup_logging(0, no_color=False, user_log_file=None)
with open_rich_spinner("doing work"):
		time.sleep(2)
try:
		with open_rich_spinner("doing work"):
				1/0
except BaseException:
		pass
try:
		with open_rich_spinner("doing work"):
				raise KeyboardInterrupt
except BaseException:
		pass
```


Patch:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e45b66dc9..35bbede70 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
 # Documentation for this file can be found at:
 # https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository
 
-blank_issues_enabled: false
-contact_links:
-  - name: "(maintainers only) Blank issue"
-    url: https://github.com/pypa/pip/issues/new
-    about: For maintainers only.
+blank_issues_enabled: true
+# contact_links:
+#   - name: "(maintainers only) Blank issue"
+#     url: https://github.com/pypa/pip/issues/new
+#     about: For maintainers only.
diff --git a/.gitignore b/.gitignore
index 79b8ab84b..d2d5eadfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
-# Byte-compiled / optimized / DLL files
+# Byte-compiled files
 __pycache__/
-*.py[cod]
 *$py.class
 
 # Distribution / packaging
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ec9510f23..c43dc5ef1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,13 +22,13 @@ repos:
   - id: black
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.11.12
+  rev: v0.12.2
   hooks:
     - id: ruff-check
       args: [--fix]
 
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.16.0
+  rev: v1.16.1
   hooks:
   - id: mypy
     exclude: tests/data
diff --git a/MANIFEST.in b/MANIFEST.in
index 95e966d96..998cb4f48 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,3 @@
-include AUTHORS.txt
-include LICENSE.txt
 include NEWS.rst
 include README.rst
 include SECURITY.md
@@ -12,8 +10,6 @@ include build-project/.python-version
 
 include src/pip/_vendor/README.rst
 include src/pip/_vendor/vendor.txt
-recursive-include  src/pip/_vendor *LICENSE*
-recursive-include  src/pip/_vendor *COPYING*
 
 include docs/requirements.txt
 
diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst
index b2f6f1d13..cf8cceb33 100644
--- a/docs/html/development/contributing.rst
+++ b/docs/html/development/contributing.rst
@@ -35,6 +35,15 @@ Examples include re-flowing text in comments or documentation, or adding or
 removing blank lines or whitespace within lines. Such changes can be made
 separately, as a "formatting cleanup" PR, if needed.
 
+.. attention::
+
+   While contributors may use whatever tools they like when developing a pull
+   request, it is the contributor's responsibility to ensure that submitted
+   code meets the project requirements, and that they understand the submitted
+   code well enough to respond to review comments.
+
+   In particular, we will mark LLM-generated slop as spam without additional
+   discussion.
 
 Automated Testing
 =================
diff --git a/docs/html/ux-research-design/guidance.md b/docs/html/ux-research-design/guidance.md
index 035df4c73..d5f058a2b 100644
--- a/docs/html/ux-research-design/guidance.md
+++ b/docs/html/ux-research-design/guidance.md
@@ -344,6 +344,24 @@ Designing for pip includes:
 - Writing pip's _output_ - establishing how pip responds to commands and what information it provides the user. This includes writing success and error messages.
 - Providing supplemental materials - e.g. documentation that helps users understand pip's operation
 
+### Error Message Format
+
+A good error message should mention:
+
+* what the user has tried to do
+* possible next steps to try and solve the error
+  * possible steps need to go from "easiest" to "most complicated"
+* why the error has happened - include a way to see more information
+  about the situation
+
+A [sample `ResolutionImpossible` error that follows this guidance
+is available](resolution-impossible-example).
+
+**Further reading**
+
+- <https://uxplanet.org/how-to-write-good-error-messages-858e4551cd4>
+- <https://www.nngroup.com/articles/error-message-guidelines/>
+
 ### Design Principles / Usability Heuristics
 
 There are many interaction design principles that help designers design great experiences. Nielsen Norman's [10 Usability Heuristics for User Interface Design](https://www.nngroup.com/articles/ten-usability-heuristics) is a great place to start. Here are some of the ways these principles apply to pip:
diff --git a/docs/html/ux-research-design/resolution-impossible-example.md b/docs/html/ux-research-design/resolution-impossible-example.md
new file mode 100644
index 000000000..a22bb93ec
--- /dev/null
+++ b/docs/html/ux-research-design/resolution-impossible-example.md
@@ -0,0 +1,92 @@
+---
+orphan:
+---
+
+# Example error: ResolutionImpossible
+
+## What if there are user-provided pinned packages?
+
+Where a user wants to install packages (with 1 or more pinned version)
+with default verbose level
+
+```
+$ pip install peach=1.0 apple=2.0
+
+Due to conflicting dependencies pip cannot install Peach1.0 and Apple2.0:
+
+* Peach 1.0 depends on Banana 3.0
+* Apple2.0 depends on Banana2.0
+
+There are a number of possible solutions. You can try:
+1. removing package versions from your requirements, and letting pip try to resolve the problem for you
+2. Trying  a version of Peach that depends on Banana2.0. Try `pip-search peach —dep banana2.0`
+3. replacing Apple or Peach with a different package altogether
+4. patching Apple2.0 to use Banana3.0
+5. force installing (Be aware!)
+
+For instructions on how to do these steps visit: https://pypa.io/SomeLink
+To debug this further you can run `pip-tree` to see all of your dependencies.
+```
+
+**with verbose level -vv**
+
+If a user ran the same pip command with more verbosity, what would they see?
+
+**with verbose level -vvv**
+
+If a user ran the same pip command with more verbosity, what would they see?
+
+## What if there are no user-provided version restrictions?
+
+NB: We are assuming this resolver behaviour gets implemented, based on [GH issues 8249](https://github.com/pypa/pip/issues/8249).
+
+**with default verbose level**
+
+```
+$ pip install apple peach
+
+Due to conflicting dependencies pip cannot install apple or peach. Both depend on banana, but pip can't find a version of either where they depend on the same banana version.
+
+There are a number of possible solutions. You can try:
+1. replacing apple or peach with a different package altogether
+2. patching apple or peach to use the same version of banana
+3. force installing (Be aware!)
+
+To debug this further you can run pip-tree to see all of your dependencies.
+```
+
+**with verbose level -vv**
+
+If a user ran the same pip command with more verbosity, what would they see?
+
+**with verbose level -vvv**
+
+If a user ran the same pip command with more verbosity, what would they see?
+
+**What should be in the "documentation" page?**
+
+* ways to swap a package for another
+* how to patch a package to support a version (various ways)
+
+## Recommendations
+
+* Write official documentation / guide "How to resolve dependency conflicts" explaining:
+  * Why conflicts can exist
+  * How you can avoid them (pinning)
+  * How you can resolve them
+    * Use alternative package
+    * Use older version
+    * Patch package
+* Introduce new commands to pip, inspired by poetry:
+  * Tree: Show full tree of dependencies
+  * Show `<package>` Show details of a package, including it's dependencies
+  * latest - shows latest vs installed versions
+  * outdated - shows only outdated versions
+* Expose commands / help link in output??
+  * when particular issue happens provide ways to move on (ala pipenv), e.g.
+    * run this command to see X
+    * is it your internet connection?
+    * is it the pypi website?
+* Aspirational commands
+  * `pip search PackageName —dep PackageNameVersion`
+    * a command that will search for a version of a package that has a dependency on another packageversion
diff --git a/news/10915.bugfix.rst b/news/10915.bugfix.rst
new file mode 100644
index 000000000..0f4bb5809
--- /dev/null
+++ b/news/10915.bugfix.rst
@@ -0,0 +1,5 @@
+Suppress the progress bar, when running with ``--log`` and ``--quiet``.
+
+Consequently, a new ``auto`` mode for ``--progress-bar`` has been added.
+``auto`` will enable progress bars unless suppressed by ``--quiet``,
+while ``on`` will always enable progress bars.
diff --git a/news/12099.bugfix.rst b/news/12099.bugfix.rst
new file mode 100644
index 000000000..b174df532
--- /dev/null
+++ b/news/12099.bugfix.rst
@@ -0,0 +1,2 @@
+``pip config debug`` now correctly separates options as set by the different files
+at the same level.
diff --git a/news/12283.feature.rst b/news/12283.feature.rst
new file mode 100644
index 000000000..1e2f2237c
--- /dev/null
+++ b/news/12283.feature.rst
@@ -0,0 +1,1 @@
+Remove warning when cloning from a Git reference that does not look like a commit hash.
diff --git a/news/13335.process.rst b/news/13335.process.rst
new file mode 100644
index 000000000..9a38395bb
--- /dev/null
+++ b/news/13335.process.rst
@@ -0,0 +1,3 @@
+pip's own licensing metadata now follows PEP 639.
+In addition, the licenses of pip's vendored dependencies are now included
+in the ``License-File`` metadata field and in the wheel.
diff --git a/news/13424.feature.rst b/news/13424.feature.rst
new file mode 100644
index 000000000..2829abeb9
--- /dev/null
+++ b/news/13424.feature.rst
@@ -0,0 +1,1 @@
+Minor performance improvement getting the order to install a very large number of interdependent packages.
diff --git a/news/13464.feature.rst b/news/13464.feature.rst
new file mode 100644
index 000000000..518b89e42
--- /dev/null
+++ b/news/13464.feature.rst
@@ -0,0 +1,1 @@
+Automatic download resumption and retrying is enabled by default.
diff --git a/news/627ddbb6-5268-4fd9-b04b-0485abc35295.trivial.rst b/news/627ddbb6-5268-4fd9-b04b-0485abc35295.trivial.rst
new file mode 100644
index 000000000..e69de29bb
diff --git a/noxfile.py b/noxfile.py
index 14097bf87..160e823e9 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -26,8 +26,6 @@ LOCATIONS = {
 }
 REQUIREMENTS = {
     "docs": "docs/requirements.txt",
-    "tests": "tests/requirements.txt",
-    "common-wheels": "tests/requirements-common_wheels.txt",
 }
 
 AUTHORS_FILE = "AUTHORS.txt"
@@ -54,10 +52,10 @@ def should_update_common_wheels() -> bool:
     if not os.path.exists(LOCATIONS["common-wheels"]):
         return True
 
-    # If the requirements was updated after cache, we'll repopulate it.
+    # If the pyproject.toml was updated after cache, we'll repopulate it.
     cache_last_populated_at = os.path.getmtime(LOCATIONS["common-wheels"])
-    requirements_updated_at = os.path.getmtime(REQUIREMENTS["common-wheels"])
-    need_to_repopulate = requirements_updated_at > cache_last_populated_at
+    pyproject_updated_at = os.path.getmtime("pyproject.toml")
+    need_to_repopulate = pyproject_updated_at > cache_last_populated_at
 
     # Clear the stale cache.
     if need_to_repopulate:
@@ -78,7 +76,7 @@ def test(session: nox.Session) -> None:
             session,
             "wheel",
             "-w", LOCATIONS["common-wheels"],
-            "-r", REQUIREMENTS["common-wheels"],
+            "--group", "test-common-wheels",
         )
         # fmt: on
     else:
@@ -115,7 +113,7 @@ def test(session: nox.Session) -> None:
     run_with_protected_pip(session, "install", generated_sdist)
 
     # Install test dependencies
-    run_with_protected_pip(session, "install", "-r", REQUIREMENTS["tests"])
+    run_with_protected_pip(session, "install", "--group", "test")
 
     # Parallelize tests as much as possible, by default.
     arguments = session.posargs or ["-n", "auto"]
@@ -156,6 +154,7 @@ def docs(session: nox.Session) -> None:
         ]
         # fmt: on
 
+    shutil.rmtree("docs/build", ignore_errors=True)
     session.run(*get_sphinx_build_command("html"))
     session.run(*get_sphinx_build_command("man"))
 
diff --git a/pyproject.toml b/pyproject.toml
index 1f74588d6..90f04307f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,11 +4,16 @@ dynamic = ["version"]
 name = "pip"
 description = "The PyPA recommended tool for installing Python packages."
 readme = "README.rst"
-license = {text = "MIT"}
+license = "MIT"
+license-files = [
+  "AUTHORS.txt",
+  "LICENSE.txt",
+  "src/pip/_vendor/**/*COPYING*",
+  "src/pip/_vendor/**/*LICENSE*",
+]
 classifiers = [
   "Development Status :: 5 - Production/Stable",
   "Intended Audience :: Developers",
-  "License :: OSI Approved :: MIT License",
   "Topic :: Software Development :: Build Tools",
   "Programming Language :: Python",
   "Programming Language :: Python :: 3",
@@ -40,10 +45,39 @@ Source = "https://github.com/pypa/pip"
 Changelog = "https://pip.pypa.io/en/stable/news/"
 
 [build-system]
-# The lower bound is for <https://github.com/pypa/setuptools/issues/3865>.
-requires = ["setuptools>=67.6.1"]
+requires = ["setuptools>=77"]
 build-backend = "setuptools.build_meta"
 
+[dependency-groups]
+test = [
+    "cryptography",
+    "freezegun",
+    "installer",
+    "pytest",
+    "pytest-cov",
+    "pytest-rerunfailures",
+    "pytest-xdist",
+    "scripttest",
+    "setuptools",
+    # macOS (darwin) arm64 always uses virtualenv >= 20.0
+    # for other platforms, it depends on python version
+    "virtualenv < 20.0 ; python_version < '3.10' and (sys_platform != 'darwin' or platform_machine != 'arm64')",
+    "virtualenv >= 20.0 ; python_version >= '3.10' or (sys_platform == 'darwin' and platform_machine == 'arm64')",
+    "werkzeug",
+    "wheel",
+    "tomli-w",
+    "proxy.py",
+]
+
+test-common-wheels = [
+    # We pin setuptools<80 because our test suite currently
+    # depends on setup.py develop to generate egg-link files.
+    "setuptools >= 40.8.0, != 60.6.0, <80",
+    "wheel",
+    # As required by pytest-cov.
+    "coverage >= 4.4",
+]
+
 [tool.setuptools]
 package-dir = {"" = "src"}
 include-package-data = false
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 69a2e72ce..3a246a1e3 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -11,7 +11,7 @@ import textwrap
 from collections import OrderedDict
 from collections.abc import Iterable
 from types import TracebackType
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Protocol
 
 from pip._vendor.packaging.version import Version
 
@@ -26,6 +26,7 @@ from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
 
 if TYPE_CHECKING:
     from pip._internal.index.package_finder import PackageFinder
+    from pip._internal.req.req_install import InstallRequirement
 
 logger = logging.getLogger(__name__)
 
@@ -79,10 +80,108 @@ def _get_system_sitepackages() -> set[str]:
     return {os.path.normcase(path) for path in system_sites}
 
 
+class BuildEnvironmentInstaller(Protocol):
+    """
+    Interface for installing build dependencies into an isolated build
+    environment.
+    """
+
+    def install(
+        self,
+        requirements: Iterable[str],
+        prefix: _Prefix,
+        *,
+        kind: str,
+        for_req: InstallRequirement | None,
+    ) -> None: ...
+
+
+class SubprocessBuildEnvironmentInstaller:
+    """
+    Install build dependencies by calling pip in a subprocess.
+    """
+
+    def __init__(self, finder: PackageFinder) -> None:
+        self.finder = finder
+
+    def install(
+        self,
+        requirements: Iterable[str],
+        prefix: _Prefix,
+        *,
+        kind: str,
+        for_req: InstallRequirement | None,
+    ) -> None:
+        finder = self.finder
+        args: list[str] = [
+            sys.executable,
+            get_runnable_pip(),
+            "install",
+            "--ignore-installed",
+            "--no-user",
+            "--prefix",
+            prefix.path,
+            "--no-warn-script-location",
+            "--disable-pip-version-check",
+            # As the build environment is ephemeral, it's wasteful to
+            # pre-compile everything, especially as not every Python
+            # module will be used/compiled in most cases.
+            "--no-compile",
+            # The prefix specified two lines above, thus
+            # target from config file or env var should be ignored
+            "--target",
+            "",
+        ]
+        if logger.getEffectiveLevel() <= logging.DEBUG:
+            args.append("-vv")
+        elif logger.getEffectiveLevel() <= VERBOSE:
+            args.append("-v")
+        for format_control in ("no_binary", "only_binary"):
+            formats = getattr(finder.format_control, format_control)
+            args.extend(
+                (
+                    "--" + format_control.replace("_", "-"),
+                    ",".join(sorted(formats or {":none:"})),
+                )
+            )
+
+        index_urls = finder.index_urls
+        if index_urls:
+            args.extend(["-i", index_urls[0]])
+            for extra_index in index_urls[1:]:
+                args.extend(["--extra-index-url", extra_index])
+        else:
+            args.append("--no-index")
+        for link in finder.find_links:
+            args.extend(["--find-links", link])
+
+        if finder.proxy:
+            args.extend(["--proxy", finder.proxy])
+        for host in finder.trusted_hosts:
+            args.extend(["--trusted-host", host])
+        if finder.custom_cert:
+            args.extend(["--cert", finder.custom_cert])
+        if finder.client_cert:
+            args.extend(["--client-cert", finder.client_cert])
+        if finder.allow_all_prereleases:
+            args.append("--pre")
+        if finder.prefer_binary:
+            args.append("--prefer-binary")
+        args.append("--")
+        args.extend(requirements)
+        with open_spinner(f"Installing {kind}") as spinner:
+            call_subprocess(
+                args,
+                command_desc=f"pip subprocess to install {kind}",
+                spinner=spinner,
+            )
+
+
 class BuildEnvironment:
     """Creates and manages an isolated environment to install build deps"""
 
-    def __init__(self) -> None:
+    def __init__(self, installer: BuildEnvironmentInstaller) -> None:
+        self.installer = installer
         temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV, globally_managed=True)
 
         self._prefixes = OrderedDict(
@@ -205,96 +304,18 @@ class BuildEnvironment:
 
     def install_requirements(
         self,
-        finder: PackageFinder,
         requirements: Iterable[str],
         prefix_as_string: str,
         *,
         kind: str,
+        for_req: InstallRequirement | None = None,
     ) -> None:
         prefix = self._prefixes[prefix_as_string]
         assert not prefix.setup
         prefix.setup = True
         if not requirements:
             return
-        self._install_requirements(
-            get_runnable_pip(),
-            finder,
-            requirements,
-            prefix,
-            kind=kind,
-        )
-
-    @staticmethod
-    def _install_requirements(
-        pip_runnable: str,
-        finder: PackageFinder,
-        requirements: Iterable[str],
-        prefix: _Prefix,
-        *,
-        kind: str,
-    ) -> None:
-        args: list[str] = [
-            sys.executable,
-            pip_runnable,
-            "install",
-            "--ignore-installed",
-            "--no-user",
-            "--prefix",
-            prefix.path,
-            "--no-warn-script-location",
-            "--disable-pip-version-check",
-            # As the build environment is ephemeral, it's wasteful to
-            # pre-compile everything, especially as not every Python
-            # module will be used/compiled in most cases.
-            "--no-compile",
-            # The prefix specified two lines above, thus
-            # target from config file or env var should be ignored
-            "--target",
-            "",
-        ]
-        if logger.getEffectiveLevel() <= logging.DEBUG:
-            args.append("-vv")
-        elif logger.getEffectiveLevel() <= VERBOSE:
-            args.append("-v")
-        for format_control in ("no_binary", "only_binary"):
-            formats = getattr(finder.format_control, format_control)
-            args.extend(
-                (
-                    "--" + format_control.replace("_", "-"),
-                    ",".join(sorted(formats or {":none:"})),
-                )
-            )
-
-        index_urls = finder.index_urls
-        if index_urls:
-            args.extend(["-i", index_urls[0]])
-            for extra_index in index_urls[1:]:
-                args.extend(["--extra-index-url", extra_index])
-        else:
-            args.append("--no-index")
-        for link in finder.find_links:
-            args.extend(["--find-links", link])
-
-        if finder.proxy:
-            args.extend(["--proxy", finder.proxy])
-        for host in finder.trusted_hosts:
-            args.extend(["--trusted-host", host])
-        if finder.custom_cert:
-            args.extend(["--cert", finder.custom_cert])
-        if finder.client_cert:
-            args.extend(["--client-cert", finder.client_cert])
-        if finder.allow_all_prereleases:
-            args.append("--pre")
-        if finder.prefer_binary:
-            args.append("--prefer-binary")
-        args.append("--")
-        args.extend(requirements)
-        with open_spinner(f"Installing {kind}") as spinner:
-            call_subprocess(
-                args,
-                command_desc=f"pip subprocess to install {kind}",
-                spinner=spinner,
-            )
+        self.installer.install(requirements, prefix, kind=kind, for_req=for_req)
 
 
 class NoOpBuildEnvironment(BuildEnvironment):
@@ -319,10 +340,10 @@ class NoOpBuildEnvironment(BuildEnvironment):
 
     def install_requirements(
         self,
-        finder: PackageFinder,
         requirements: Iterable[str],
         prefix_as_string: str,
         *,
         kind: str,
+        for_req: InstallRequirement | None = None,
     ) -> None:
         raise NotImplementedError()
diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py
index 735bc87ef..7acc29cb3 100644
--- a/src/pip/_internal/cli/base_command.py
+++ b/src/pip/_internal/cli/base_command.py
@@ -176,6 +176,9 @@ class Command(CommandContextMixIn):
         if options.debug_mode:
             self.verbosity = 2
 
+        if hasattr(options, "progress_bar") and options.progress_bar == "auto":
+            options.progress_bar = "on" if self.verbosity >= 0 else "off"
+
         reconfigure(no_color=options.no_color)
         level_number = setup_logging(
             verbosity=self.verbosity,
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 2476aca80..3519dadf1 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -228,9 +228,13 @@ progress_bar: Callable[..., Option] = partial(
     "--progress-bar",
     dest="progress_bar",
     type="choice",
-    choices=["on", "off", "raw"],
-    default="on",
-    help="Specify whether the progress bar should be used [on, off, raw] (default: on)",
+    choices=["auto", "on", "off", "raw"],
+    default="auto",
+    help=(
+        "Specify whether the progress bar should be used. In 'auto'"
+        " mode, --quiet will suppress all progress bars."
+        " [auto, on, off, raw] (default: auto)"
+    ),
 )
 
 log: Callable[..., Option] = partial(
@@ -290,7 +294,7 @@ resume_retries: Callable[..., Option] = partial(
     "--resume-retries",
     dest="resume_retries",
     type="int",
-    default=0,
+    default=5,
     help="Maximum attempts to resume or restart an incomplete download. "
     "(default: %default)",
 )
diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py
index 24cc22ba3..f8b8ac43b 100644
--- a/src/pip/_internal/cli/parser.py
+++ b/src/pip/_internal/cli/parser.py
@@ -188,23 +188,24 @@ class ConfigOptionParser(CustomOptionParser):
         section_items: dict[str, list[tuple[str, Any]]] = {
             name: [] for name in override_order
         }
-        for section_key, val in self.config.items():
-            # ignore empty values
-            if not val:
-                logger.debug(
-                    "Ignoring configuration key '%s' as it's value is empty.",
-                    section_key,
-                )
-                continue
 
-            section, key = section_key.split(".", 1)
-            if section in override_order:
-                section_items[section].append((key, val))
+        for _, value in self.config.items():  # noqa: PERF102
+            for section_key, val in value.items():
+                # ignore empty values
+                if not val:
+                    logger.debug(
+                        "Ignoring configuration key '%s' as its value is empty.",
+                        section_key,
+                    )
+                    continue
+
+                section, key = section_key.split(".", 1)
+                if section in override_order:
+                    section_items[section].append((key, val))
 
-        # Yield each group in their override order
-        for section in override_order:
-            for key, val in section_items[section]:
-                yield key, val
+            # Yield each group in their override order
+            for section in override_order:
+                yield from section_items[section]
 
     def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
         """Updates the given defaults with values from the config files and
diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py
index a1509ac7c..edc47e12c 100644
--- a/src/pip/_internal/cli/progress_bars.py
+++ b/src/pip/_internal/cli/progress_bars.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 import functools
 import sys
 from collections.abc import Generator, Iterable, Iterator
-from typing import Callable, TypeVar
+from typing import Callable, Literal, TypeVar
 
 from pip._vendor.rich.progress import (
     BarColumn,
@@ -25,12 +25,13 @@ from pip._internal.utils.logging import get_console, get_indentation
 
 T = TypeVar("T")
 ProgressRenderer = Callable[[Iterable[T]], Iterator[T]]
+BarType = Literal["on", "off", "raw"]
 
 
 def _rich_download_progress_bar(
     iterable: Iterable[bytes],
     *,
-    bar_type: str,
+    bar_type: BarType,
     size: int | None,
     initial_progress: int | None = None,
 ) -> Generator[bytes, None, None]:
@@ -112,7 +113,7 @@ def _raw_progress_bar(
 
 
 def get_download_progress_renderer(
-    *, bar_type: str, size: int | None = None, initial_progress: int | None = None
+    *, bar_type: BarType, size: int | None = None, initial_progress: int | None = None
 ) -> ProgressRenderer[bytes]:
     """Get an object that can be used to render the download progress.
 
@@ -136,7 +137,7 @@ def get_download_progress_renderer(
 
 
 def get_install_progress_renderer(
-    *, bar_type: str, total: int
+    *, bar_type: BarType, total: int
 ) -> ProgressRenderer[InstallRequirement]:
     """Get an object that can be used to render the install progress.
     Returns a callable, that takes an iterable to "wrap".
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index 272550988..dc1328ff0 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -12,6 +12,7 @@ from functools import partial
 from optparse import Values
 from typing import Any
 
+from pip._internal.build_env import SubprocessBuildEnvironmentInstaller
 from pip._internal.cache import WheelCache
 from pip._internal.cli import cmdoptions
 from pip._internal.cli.index_command import IndexGroupCommand
@@ -136,6 +137,7 @@ class RequirementCommand(IndexGroupCommand):
             src_dir=options.src_dir,
             download_dir=download_dir,
             build_isolation=options.build_isolation,
+            build_isolation_installer=SubprocessBuildEnvironmentInstaller(finder),
             check_build_deps=options.check_build_deps,
             build_tracker=build_tracker,
             session=session,
diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py
index 1d5c351d8..58aad2853 100644
--- a/src/pip/_internal/cli/spinners.py
+++ b/src/pip/_internal/cli/spinners.py
@@ -6,13 +6,26 @@ import logging
 import sys
 import time
 from collections.abc import Generator
-from typing import IO
+from typing import IO, Final
+
+from pip._vendor.rich.console import (
+    Console,
+    ConsoleOptions,
+    RenderableType,
+    RenderResult,
+)
+from pip._vendor.rich.live import Live
+from pip._vendor.rich.measure import Measurement
+from pip._vendor.rich.text import Text
 
 from pip._internal.utils.compat import WINDOWS
-from pip._internal.utils.logging import get_indentation
+from pip._internal.utils.logging import get_console, get_indentation
 
 logger = logging.getLogger(__name__)
 
+SPINNER_CHARS: Final = r"-\|/"
+SPINS_PER_SECOND: Final = 8
+
 
 class SpinnerInterface:
     def spin(self) -> None:
@@ -27,9 +40,9 @@ class InteractiveSpinner(SpinnerInterface):
         self,
         message: str,
         file: IO[str] | None = None,
-        spin_chars: str = "-\\|/",
+        spin_chars: str = SPINNER_CHARS,
         # Empirically, 8 updates/second looks nice
-        min_update_interval_seconds: float = 0.125,
+        min_update_interval_seconds: float = 1 / SPINS_PER_SECOND,
     ):
         self._message = message
         if file is None:
@@ -139,6 +152,66 @@ def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
         spinner.finish("done")
 
 
+class _PipRichSpinner:
+    """
+    Custom rich spinner that matches the style of the legacy spinners.
+
+    (*) Updates will be handled in a background thread by a rich live panel
+        which will call render() automatically at the appropriate time.
+    """
+
+    def __init__(self, label: str) -> None:
+        self.label = label
+        self._spin_cycle = itertools.cycle(SPINNER_CHARS)
+        self._spinner_text = ""
+        self._finished = False
+        self._indent = get_indentation() * " "
+
+    def __rich_console__(
+        self, console: Console, options: ConsoleOptions
+    ) -> RenderResult:
+        yield self.render()
+
+    def __rich_measure__(
+        self, console: Console, options: ConsoleOptions
+    ) -> Measurement:
+        text = self.render()
+        return Measurement.get(console, options, text)
+
+    def render(self) -> RenderableType:
+        if not self._finished:
+            self._spinner_text = next(self._spin_cycle)
+
+        return Text.assemble(self._indent, self.label, " ... ", self._spinner_text)
+
+    def finish(self, status: str) -> None:
+        """Stop spinning and set a final status message."""
+        self._spinner_text = status
+        self._finished = True
+
+
+@contextlib.contextmanager
+def open_rich_spinner(label: str, console: Console | None = None) -> Generator[None]:
+    if not logger.isEnabledFor(logging.INFO):
+        # Don't show spinner if --quiet is given.
+        yield
+        return
+
+    console = console or get_console()
+    spinner = _PipRichSpinner(label)
+    with Live(spinner, refresh_per_second=SPINS_PER_SECOND, console=console):
+        try:
+            yield
+        except KeyboardInterrupt:
+            spinner.finish("canceled")
+            raise
+        except Exception:
+            spinner.finish("error")
+            raise
+        else:
+            spinner.finish("done")
+
+
 HIDE_CURSOR = "\x1b[?25l"
 SHOW_CURSOR = "\x1b[?25h"
 
diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py
index 1738ed8b0..7bcea0434 100644
--- a/src/pip/_internal/commands/configuration.py
+++ b/src/pip/_internal/commands/configuration.py
@@ -177,7 +177,8 @@ class ConfigurationCommand(Command):
         self._get_n_args(args, "list", n=0)
 
         for key, value in sorted(self.configuration.items()):
-            write_output("%s=%r", key, value)
+            for key, value in sorted(value.items()):
+                write_output("%s=%r", key, value)
 
     def get_name(self, options: Values, args: list[str]) -> None:
         key = self._get_n_args(args, "get [name]", n=1)
@@ -211,13 +212,15 @@ class ConfigurationCommand(Command):
                     file_exists = os.path.exists(fname)
                     write_output("%s, exists: %r", fname, file_exists)
                     if file_exists:
-                        self.print_config_file_values(variant)
+                        self.print_config_file_values(variant, fname)
 
-    def print_config_file_values(self, variant: Kind) -> None:
+    def print_config_file_values(self, variant: Kind, fname: str) -> None:
         """Get key-value pairs from the file of a variant"""
         for name, value in self.configuration.get_values_in_config(variant).items():
             with indent_log():
-                write_output("%s: %s", name, value)
+                if name == fname:
+                    for confname, confvalue in value.items():
+                        write_output("%s: %s", confname, confvalue)
 
     def print_env_var_values(self) -> None:
         """Get key-values pairs present as environment variables"""
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 302bc63f6..056eb8866 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -27,7 +27,11 @@ from pip._internal.cli.req_command import (
     with_cleanup,
 )
 from pip._internal.cli.status_codes import ERROR, SUCCESS
-from pip._internal.exceptions import CommandError, InstallationError
+from pip._internal.exceptions import (
+    CommandError,
+    InstallationError,
+    InstallWheelBuildError,
+)
 from pip._internal.locations import get_scheme
 from pip._internal.metadata import get_environment
 from pip._internal.models.installation_report import InstallationReport
@@ -434,12 +438,7 @@ class InstallCommand(RequirementCommand):
             )
 
             if build_failures:
-                raise InstallationError(
-                    "Failed to build installable wheels for some "
-                    "pyproject.toml based projects ({})".format(
-                        ", ".join(r.name for r in build_failures)  # type: ignore
-                    )
-                )
+                raise InstallWheelBuildError(build_failures)
 
             to_install = resolver.get_installation_order(requirement_set)
 
diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py
index 6902f66ab..f581a2c95 100644
--- a/src/pip/_internal/configuration.py
+++ b/src/pip/_internal/configuration.py
@@ -117,7 +117,7 @@ class Configuration:
         self._parsers: dict[Kind, list[tuple[str, RawConfigParser]]] = {
             variant: [] for variant in OVERRIDE_ORDER
         }
-        self._config: dict[Kind, dict[str, Any]] = {
+        self._config: dict[Kind, dict[str, dict[str, Any]]] = {
             variant: {} for variant in OVERRIDE_ORDER
         }
         self._modified_parsers: list[tuple[str, RawConfigParser]] = []
@@ -148,7 +148,10 @@ class Configuration:
         orig_key = key
         key = _normalize_name(key)
         try:
-            return self._dictionary[key]
+            clean_config: dict[str, Any] = {}
+            for file_values in self._dictionary.values():
+                clean_config.update(file_values)
+            return clean_config[key]
         except KeyError:
             # disassembling triggers a more useful error message than simply
             # "No such key" in the case that the key isn't in the form command.option
@@ -171,7 +174,8 @@ class Configuration:
                 parser.add_section(section)
             parser.set(section, name, value)
 
-        self._config[self.load_only][key] = value
+        self._config[self.load_only].setdefault(fname, {})
+        self._config[self.load_only][fname][key] = value
         self._mark_as_modified(fname, parser)
 
     def unset_value(self, key: str) -> None:
@@ -181,11 +185,14 @@ class Configuration:
         self._ensure_have_load_only()
 
         assert self.load_only
-        if key not in self._config[self.load_only]:
-            raise ConfigurationError(f"No such key - {orig_key}")
-
         fname, parser = self._get_parser_to_modify()
 
+        if (
+            key not in self._config[self.load_only][fname]
+            and key not in self._config[self.load_only]
+        ):
+            raise ConfigurationError(f"No such key - {orig_key}")
+
         if parser is not None:
             section, name = _disassemble_key(key)
             if not (
@@ -200,8 +207,10 @@ class Configuration:
             if not parser.items(section):
                 parser.remove_section(section)
             self._mark_as_modified(fname, parser)
-
-        del self._config[self.load_only][key]
+        try:
+            del self._config[self.load_only][fname][key]
+        except KeyError:
+            del self._config[self.load_only][key]
 
     def save(self) -> None:
         """Save the current in-memory state."""
@@ -233,7 +242,7 @@ class Configuration:
         logger.debug("Will be working with %s variant only", self.load_only)
 
     @property
-    def _dictionary(self) -> dict[str, Any]:
+    def _dictionary(self) -> dict[str, dict[str, Any]]:
         """A dictionary representing the loaded configuration."""
         # NOTE: Dictionaries are not populated if not loaded. So, conditionals
         #       are not needed here.
@@ -273,7 +282,8 @@ class Configuration:
 
         for section in parser.sections():
             items = parser.items(section)
-            self._config[variant].update(self._normalized_keys(section, items))
+            self._config[variant].setdefault(fname, {})
+            self._config[variant][fname].update(self._normalized_keys(section, items))
 
         return parser
 
@@ -300,7 +310,8 @@ class Configuration:
 
     def _load_environment_vars(self) -> None:
         """Loads configuration from environment variables"""
-        self._config[kinds.ENV_VAR].update(
+        self._config[kinds.ENV_VAR].setdefault(":env:", {})
+        self._config[kinds.ENV_VAR][":env:"].update(
             self._normalized_keys(":env:", self.get_environ_vars())
         )
 
diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py
index 6ebf49ad5..ea61f3501 100644
--- a/src/pip/_internal/distributions/base.py
+++ b/src/pip/_internal/distributions/base.py
@@ -7,7 +7,7 @@ from pip._internal.metadata.base import BaseDistribution
 from pip._internal.req import InstallRequirement
 
 if TYPE_CHECKING:
-    from pip._internal.index.package_finder import PackageFinder
+    from pip._internal.build_env import BuildEnvironmentInstaller
 
 
 class AbstractDistribution(metaclass=abc.ABCMeta):
@@ -48,7 +48,7 @@ class AbstractDistribution(metaclass=abc.ABCMeta):
     @abc.abstractmethod
     def prepare_distribution_metadata(
         self,
-        finder: PackageFinder,
+        build_env_installer: BuildEnvironmentInstaller,
         build_isolation: bool,
         check_build_deps: bool,
     ) -> None:
diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py
index 8ceed3094..b6a67df24 100644
--- a/src/pip/_internal/distributions/installed.py
+++ b/src/pip/_internal/distributions/installed.py
@@ -1,9 +1,13 @@
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from pip._internal.distributions.base import AbstractDistribution
-from pip._internal.index.package_finder import PackageFinder
 from pip._internal.metadata import BaseDistribution
 
+if TYPE_CHECKING:
+    from pip._internal.build_env import BuildEnvironmentInstaller
+
 
 class InstalledDistribution(AbstractDistribution):
     """Represents an installed package.
@@ -22,7 +26,7 @@ class InstalledDistribution(AbstractDistribution):
 
     def prepare_distribution_metadata(
         self,
-        finder: PackageFinder,
+        build_env_installer: BuildEnvironmentInstaller,
         build_isolation: bool,
         check_build_deps: bool,
     ) -> None:
diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py
index 67468955e..e2821f89e 100644
--- a/src/pip/_internal/distributions/sdist.py
+++ b/src/pip/_internal/distributions/sdist.py
@@ -11,7 +11,7 @@ from pip._internal.metadata import BaseDistribution
 from pip._internal.utils.subprocess import runner_with_spinner_message
 
 if TYPE_CHECKING:
-    from pip._internal.index.package_finder import PackageFinder
+    from pip._internal.build_env import BuildEnvironmentInstaller
 
 logger = logging.getLogger(__name__)
 
@@ -34,7 +34,7 @@ class SourceDistribution(AbstractDistribution):
 
     def prepare_distribution_metadata(
         self,
-        finder: PackageFinder,
+        build_env_installer: BuildEnvironmentInstaller,
         build_isolation: bool,
         check_build_deps: bool,
     ) -> None:
@@ -46,7 +46,7 @@ class SourceDistribution(AbstractDistribution):
         if should_isolate:
             # Setup an isolated environment and install the build backend static
             # requirements in it.
-            self._prepare_build_backend(finder)
+            self._prepare_build_backend(build_env_installer)
             # Check that if the requirement is editable, it either supports PEP 660 or
             # has a setup.py or a setup.cfg. This cannot be done earlier because we need
             # to setup the build backend to verify it supports build_editable, nor can
@@ -56,7 +56,7 @@ class SourceDistribution(AbstractDistribution):
             # without setup.py nor setup.cfg.
             self.req.isolated_editable_sanity_check()
             # Install the dynamic build requirements.
-            self._install_build_reqs(finder)
+            self._install_build_reqs(build_env_installer)
         # Check if the current environment provides build dependencies
         should_check_deps = self.req.use_pep517 and check_build_deps
         if should_check_deps:
@@ -71,15 +71,17 @@ class SourceDistribution(AbstractDistribution):
                 self._raise_missing_reqs(missing)
         self.req.prepare_metadata()
 
-    def _prepare_build_backend(self, finder: PackageFinder) -> None:
+    def _prepare_build_backend(
+        self, build_env_installer: BuildEnvironmentInstaller
+    ) -> None:
         # Isolate in a BuildEnvironment and install the build-time
         # requirements.
         pyproject_requires = self.req.pyproject_requires
         assert pyproject_requires is not None
 
-        self.req.build_env = BuildEnvironment()
+        self.req.build_env = BuildEnvironment(build_env_installer)
         self.req.build_env.install_requirements(
-            finder, pyproject_requires, "overlay", kind="build dependencies"
+            pyproject_requires, "overlay", kind="build dependencies", for_req=self.req
         )
         conflicting, missing = self.req.build_env.check_requirements(
             self.req.requirements_to_check
@@ -115,7 +117,9 @@ class SourceDistribution(AbstractDistribution):
             with backend.subprocess_runner(runner):
                 return backend.get_requires_for_build_editable()
 
-    def _install_build_reqs(self, finder: PackageFinder) -> None:
+    def _install_build_reqs(
+        self, build_env_installer: BuildEnvironmentInstaller
+    ) -> None:
         # Install any extra build dependencies that the backend requests.
         # This must be done in a second pass, as the pyproject.toml
         # dependencies must be installed before we can call the backend.
@@ -131,7 +135,7 @@ class SourceDistribution(AbstractDistribution):
         if conflicting:
             self._raise_conflicts("the backend dependencies", conflicting)
         self.req.build_env.install_requirements(
-            finder, missing, "normal", kind="backend dependencies"
+            missing, "normal", kind="backend dependencies", for_req=self.req
         )
 
     def _raise_conflicts(
diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py
index 54e1e35f2..ee12bfadc 100644
--- a/src/pip/_internal/distributions/wheel.py
+++ b/src/pip/_internal/distributions/wheel.py
@@ -12,7 +12,7 @@ from pip._internal.metadata import (
 )
 
 if TYPE_CHECKING:
-    from pip._internal.index.package_finder import PackageFinder
+    from pip._internal.build_env import BuildEnvironmentInstaller
 
 
 class WheelDistribution(AbstractDistribution):
@@ -37,7 +37,7 @@ class WheelDistribution(AbstractDistribution):
 
     def prepare_distribution_metadata(
         self,
-        finder: PackageFinder,
+        build_env_installer: BuildEnvironmentInstaller,
         build_isolation: bool,
         check_build_deps: bool,
     ) -> None:
diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py
index 0661897e9..98f95494c 100644
--- a/src/pip/_internal/exceptions.py
+++ b/src/pip/_internal/exceptions.py
@@ -865,3 +865,17 @@ class ResolutionTooDeepError(DiagnosticPipError):
             ),
             link="https://pip.pypa.io/en/stable/topics/dependency-resolution/#handling-resolution-too-deep-errors",
         )
+
+
+class InstallWheelBuildError(DiagnosticPipError):
+    reference = "failed-wheel-build-for-install"
+
+    def __init__(self, failed: list[InstallRequirement]) -> None:
+        super().__init__(
+            message=(
+                "Failed to build installable wheels for some "
+                "pyproject.toml based projects"
+            ),
+            context=", ".join(r.name for r in failed),  # type: ignore
+            hint_stmt=None,
+        )
diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py
index a655c19c3..9881cc285 100644
--- a/src/pip/_internal/network/download.py
+++ b/src/pip/_internal/network/download.py
@@ -17,7 +17,7 @@ from pip._vendor.urllib3 import HTTPResponse as URLlib3Response
 from pip._vendor.urllib3._collections import HTTPHeaderDict
 from pip._vendor.urllib3.exceptions import ReadTimeoutError
 
-from pip._internal.cli.progress_bars import get_download_progress_renderer
+from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer
 from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError
 from pip._internal.models.index import PyPI
 from pip._internal.models.link import Link
@@ -47,7 +47,7 @@ def _get_http_response_etag_or_last_modified(resp: Response) -> str | None:
 def _log_download(
     resp: Response,
     link: Link,
-    progress_bar: str,
+    progress_bar: BarType,
     total_length: int | None,
     range_start: int | None = 0,
 ) -> Iterable[bytes]:
@@ -166,7 +166,7 @@ class Downloader:
     def __init__(
         self,
         session: PipSession,
-        progress_bar: str,
+        progress_bar: BarType,
         resume_retries: int,
     ) -> None:
         assert (
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 908331eba..00b1a33a0 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -10,9 +10,11 @@ import shutil
 from collections.abc import Iterable
 from dataclasses import dataclass
 from pathlib import Path
+from typing import TYPE_CHECKING
 
 from pip._vendor.packaging.utils import canonicalize_name
 
+from pip._internal.build_env import BuildEnvironmentInstaller
 from pip._internal.distributions import make_distribution_for_install_requirement
 from pip._internal.distributions.installed import InstalledDistribution
 from pip._internal.exceptions import (
@@ -54,13 +56,16 @@ from pip._internal.utils.temp_dir import TempDirectory
 from pip._internal.utils.unpacking import unpack_file
 from pip._internal.vcs import vcs
 
+if TYPE_CHECKING:
+    from pip._internal.cli.progress_bars import BarType
+
 logger = getLogger(__name__)
 
 
 def _get_prepared_distribution(
     req: InstallRequirement,
     build_tracker: BuildTracker,
-    finder: PackageFinder,
+    build_env_installer: BuildEnvironmentInstaller,
     build_isolation: bool,
     check_build_deps: bool,
 ) -> BaseDistribution:
@@ -70,7 +75,7 @@ def _get_prepared_distribution(
     if tracker_id is not None:
         with build_tracker.track(req, tracker_id):
             abstract_dist.prepare_distribution_metadata(
-                finder, build_isolation, check_build_deps
+                build_env_installer, build_isolation, check_build_deps
             )
     return abstract_dist.get_metadata_distribution()
 
@@ -220,16 +225,18 @@ def _check_download_dir(
 class RequirementPreparer:
     """Prepares a Requirement"""
 
-    def __init__(
+    def __init__(  # noqa: PLR0913 (too many parameters)
         self,
+        *,
         build_dir: str,
         download_dir: str | None,
         src_dir: str,
         build_isolation: bool,
+        build_isolation_installer: BuildEnvironmentInstaller,
         check_build_deps: bool,
         build_tracker: BuildTracker,
         session: PipSession,
-        progress_bar: str,
+        progress_bar: BarType,
         finder: PackageFinder,
         require_hashes: bool,
         use_user_site: bool,
@@ -253,6 +260,7 @@ class RequirementPreparer:
 
         # Is build isolation allowed?
         self.build_isolation = build_isolation
+        self.build_env_installer = build_isolation_installer
 
         # Should check build dependencies?
         self.check_build_deps = check_build_deps
@@ -644,7 +652,7 @@ class RequirementPreparer:
         dist = _get_prepared_distribution(
             req,
             self.build_tracker,
-            self.finder,
+            self.build_env_installer,
             self.build_isolation,
             self.check_build_deps,
         )
@@ -700,7 +708,7 @@ class RequirementPreparer:
             dist = _get_prepared_distribution(
                 req,
                 self.build_tracker,
-                self.finder,
+                self.build_env_installer,
                 self.build_isolation,
                 self.check_build_deps,
             )
diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py
index 956c1f2fa..e5050ee58 100644
--- a/src/pip/_internal/req/__init__.py
+++ b/src/pip/_internal/req/__init__.py
@@ -5,7 +5,7 @@ import logging
 from collections.abc import Generator, Sequence
 from dataclasses import dataclass
 
-from pip._internal.cli.progress_bars import get_install_progress_renderer
+from pip._internal.cli.progress_bars import BarType, get_install_progress_renderer
 from pip._internal.utils.logging import indent_log
 
 from .req_file import parse_requirements
@@ -44,7 +44,7 @@ def install_given_reqs(
     warn_script_location: bool,
     use_user_site: bool,
     pycompile: bool,
-    progress_bar: str,
+    progress_bar: BarType,
 ) -> list[InstallationResult]:
     """
     Install everything in the given list.
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
index 87feaf128..1ba70c2b3 100644
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -248,13 +248,24 @@ def get_topological_weights(
     requirement_keys.
     """
     path: set[str | None] = set()
-    weights: dict[str | None, int] = {}
+    weights: dict[str | None, list[int]] = {}
 
     def visit(node: str | None) -> None:
         if node in path:
             # We hit a cycle, so we'll break it here.
             return
 
+        # The walk is exponential and for pathologically connected graphs (which
+        # are the ones most likely to contain cycles in the first place) it can
+        # take until the heat-death of the universe. To counter this we limit
+        # the number of attempts to visit (i.e. traverse through) any given
+        # node. We choose a value here which gives decent enough coverage for
+        # fairly well behaved graphs, and still limits the walk complexity to be
+        # linear in nature.
+        cur_weights = weights.get(node, [])
+        if len(cur_weights) >= 5:
+            return
+
         # Time to visit the children!
         path.add(node)
         for child in graph.iter_children(node):
@@ -264,14 +275,14 @@ def get_topological_weights(
         if node not in requirement_keys:
             return
 
-        last_known_parent_count = weights.get(node, 0)
-        weights[node] = max(last_known_parent_count, len(path))
+        cur_weights.append(len(path))
+        weights[node] = cur_weights
 
-    # Simplify the graph, pruning leaves that have no dependencies.
-    # This is needed for large graphs (say over 200 packages) because the
-    # `visit` function is exponentially slower then, taking minutes.
+    # Simplify the graph, pruning leaves that have no dependencies. This is
+    # needed for large graphs (say over 200 packages) because the `visit`
+    # function is slower for large/densely connected graphs, taking minutes.
     # See https://github.com/pypa/pip/issues/10557
-    # We will loop until we explicitly break the loop.
+    # We repeat the pruning step until we have no more leaves to remove.
     while True:
         leaves = set()
         for key in graph:
@@ -291,12 +302,13 @@ def get_topological_weights(
         for leaf in leaves:
             if leaf not in requirement_keys:
                 continue
-            weights[leaf] = weight
+            weights[leaf] = [weight]
         # Remove the leaves from the graph, making it simpler.
         for leaf in leaves:
             graph.remove(leaf)
 
-    # Visit the remaining graph.
+    # Visit the remaining graph, this will only have nodes to handle if the
+    # graph had a cycle in it, which the pruning step above could not handle.
     # `None` is guaranteed to be the root node by resolvelib.
     visit(None)
 
@@ -305,7 +317,9 @@ def get_topological_weights(
     difference = set(weights.keys()).difference(requirement_keys)
     assert not difference, difference
 
-    return weights
+    # Now give back all the weights, choosing the largest ones from what we
+    # accumulated.
+    return {node: max(wgts) for (node, wgts) in weights.items()}
 
 
 def _req_set_item_sorter(
diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py
index 34fcf9713..1769da791 100644
--- a/src/pip/_internal/vcs/git.py
+++ b/src/pip/_internal/vcs/git.py
@@ -236,7 +236,7 @@ class Git(VersionControl):
         # Do not show a warning for the common case of something that has
         # the form of a Git commit hash.
         if not looks_like_hash(rev):
-            logger.warning(
+            logger.info(
                 "Did not find branch or tag '%s', assuming revision or ref.",
                 rev,
             )


Test patch:
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 1a1839b40..ec1b59f06 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -6,7 +6,11 @@ from textwrap import dedent
 
 import pytest
 
-from pip._internal.build_env import BuildEnvironment, _get_system_sitepackages
+from pip._internal.build_env import (
+    BuildEnvironment,
+    SubprocessBuildEnvironmentInstaller,
+    _get_system_sitepackages,
+)
 
 from tests.lib import (
     PipTestEnvironment,
@@ -33,7 +37,10 @@ def run_with_build_env(
             import subprocess
             import sys
 
-            from pip._internal.build_env import BuildEnvironment
+            from pip._internal.build_env import (
+                BuildEnvironment,
+                SubprocessBuildEnvironmentInstaller,
+            )
             from pip._internal.index.collector import LinkCollector
             from pip._internal.index.package_finder import PackageFinder
             from pip._internal.models.search_scope import SearchScope
@@ -56,7 +63,9 @@ def run_with_build_env(
             )
 
             with global_tempdir_manager():
-                build_env = BuildEnvironment()
+                build_env = BuildEnvironment(
+                    SubprocessBuildEnvironmentInstaller(finder)
+                )
             """
         )
         + indent(dedent(setup_script_contents), "    ")
@@ -81,30 +90,26 @@ def run_with_build_env(
 
 def test_build_env_allow_empty_requirements_install() -> None:
     finder = make_test_finder()
-    build_env = BuildEnvironment()
+    build_env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
     for prefix in ("normal", "overlay"):
-        build_env.install_requirements(
-            finder, [], prefix, kind="Installing build dependencies"
-        )
+        build_env.install_requirements([], prefix, kind="Installing build dependencies")
 
 
 def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
     create_basic_wheel_for_package(script, "foo", "1.0")
     create_basic_wheel_for_package(script, "bar", "1.0")
     finder = make_test_finder(find_links=[os.fspath(script.scratch_path)])
-    build_env = BuildEnvironment()
+    build_env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
     for prefix in ("normal", "overlay"):
         build_env.install_requirements(
-            finder, ["foo"], prefix, kind=f"installing foo in {prefix}"
+            ["foo"], prefix, kind=f"installing foo in {prefix}"
         )
         with pytest.raises(AssertionError):
             build_env.install_requirements(
-                finder, ["bar"], prefix, kind=f"installing bar in {prefix}"
+                ["bar"], prefix, kind=f"installing bar in {prefix}"
             )
         with pytest.raises(AssertionError):
-            build_env.install_requirements(
-                finder, [], prefix, kind=f"installing in {prefix}"
-            )
+            build_env.install_requirements([], prefix, kind=f"installing in {prefix}")
 
 
 def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
@@ -132,7 +137,7 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
     run_with_build_env(
         script,
         """
-        build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
+        build_env.install_requirements(['foo', 'bar==3.0'], 'normal',
                                        kind='installing foo in normal')
 
         r = build_env.check_requirements(['foo', 'bar', 'other'])
@@ -149,9 +154,9 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
     run_with_build_env(
         script,
         """
-        build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
+        build_env.install_requirements(['foo', 'bar==3.0'], 'normal',
                                        kind='installing foo in normal')
-        build_env.install_requirements(finder, ['bar==1.0'], 'overlay',
+        build_env.install_requirements(['bar==1.0'], 'overlay',
                                        kind='installing foo in overlay')
 
         r = build_env.check_requirements(['foo', 'bar', 'other'])
@@ -170,7 +175,6 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
         script,
         """
         build_env.install_requirements(
-            finder,
             ["bar==3.0"],
             "normal",
             kind="installing bar in normal",
@@ -193,9 +197,9 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
     result = run_with_build_env(
         script,
         """
-        build_env.install_requirements(finder, ['pkg==2.0'], 'overlay',
+        build_env.install_requirements(['pkg==2.0'], 'overlay',
                                        kind='installing pkg==2.0 in overlay')
-        build_env.install_requirements(finder, ['pkg==4.3'], 'normal',
+        build_env.install_requirements(['pkg==4.3'], 'normal',
                                        kind='installing pkg==4.3 in normal')
         """,
         """
diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py
index f049adf6b..af3000e7f 100644
--- a/tests/functional/test_configuration.py
+++ b/tests/functional/test_configuration.py
@@ -1,16 +1,43 @@
 """Tests for the config command"""
 
+from __future__ import annotations
+
+import ast
+import os
 import re
+import subprocess
+import sys
 import textwrap
 
 from pip._internal.cli.status_codes import ERROR
-from pip._internal.configuration import CONFIG_BASENAME, get_configuration_files
+from pip._internal.configuration import CONFIG_BASENAME, Kind
+from pip._internal.configuration import get_configuration_files as _get_config_files
+from pip._internal.utils.compat import WINDOWS
 
 from tests.lib import PipTestEnvironment
 from tests.lib.configuration_helpers import ConfigurationMixin, kinds
 from tests.lib.venv import VirtualEnvironment
 
 
+def get_configuration_files() -> dict[Kind, list[str]]:
+    """Wrapper over pip._internal.configuration.get_configuration_files()."""
+    if WINDOWS:
+        # The user configuration directory is updated in the isolate fixture using the
+        # APPDATA environment variable. This will only take effect in new subprocesses,
+        # however. To ensure that get_configuration_files() never returns stale data,
+        # call it in a subprocess on Windows.
+        code = (
+            "from pip._internal.configuration import get_configuration_files; "
+            "print(get_configuration_files())"
+        )
+        proc = subprocess.run(
+            [sys.executable, "-c", code], capture_output=True, encoding="utf-8"
+        )
+        return ast.literal_eval(proc.stdout)
+    else:
+        return _get_config_files()
+
+
 def test_no_options_passed_should_error(script: PipTestEnvironment) -> None:
     result = script.pip("config", expect_error=True)
     assert result.returncode == ERROR
@@ -148,3 +175,40 @@ class TestBasicLoading(ConfigurationMixin):
             "config", "edit", "--editor", "notrealeditor", expect_error=True
         )
         assert "notrealeditor" in result.stderr
+
+    def test_config_separated(
+        self, script: PipTestEnvironment, virtualenv: VirtualEnvironment
+    ) -> None:
+        """Test that the pip configuration values in the different config sections
+        are correctly assigned to their origin files.
+        """
+
+        # Use new config file
+        new_config_file = get_configuration_files()[kinds.USER][1]
+
+        # Get legacy config file and touch it for testing purposes
+        legacy_config_file = get_configuration_files()[kinds.USER][0]
+        os.makedirs(os.path.dirname(legacy_config_file))
+        open(legacy_config_file, "a").close()
+
+        # Site config file
+        site_config_file = virtualenv.location / CONFIG_BASENAME
+
+        script.pip("config", "--user", "set", "global.timeout", "60")
+        script.pip("config", "--site", "set", "freeze.timeout", "10")
+
+        result = script.pip("config", "debug")
+
+        assert (
+            f"{site_config_file}, exists: True\n    freeze.timeout: 10" in result.stdout
+        )
+        assert (
+            f"{new_config_file}, exists: True\n    global.timeout: 60" in result.stdout
+        )
+        assert re.search(
+            (
+                rf"{re.escape(legacy_config_file)}, "
+                rf"exists: True\n(  {re.escape(new_config_file)}.+\n)+"
+            ),
+            result.stdout,
+        )
diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py
index 29606075a..baee7e80d 100644
--- a/tests/functional/test_install_config.py
+++ b/tests/functional/test_install_config.py
@@ -539,3 +539,14 @@ def test_prompt_for_keyring_if_needed(
         assert function_name + " was called" in result.stderr
     else:
         assert function_name + " was called" not in result.stderr
+
+
+@pytest.mark.network
+def test_install_quiet_log(script: PipTestEnvironment, data: TestData) -> None:
+    """
+    Test suppressing the progress bar with --quiet and --log.
+    """
+    logfile = script.scratch_path / "log"
+    result = script.pip("install", "-qqq", "setuptools==62.0.0", "--log", logfile)
+    assert result.stdout == ""
+    assert result.stderr == ""
diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py
index 9be383424..47dae9681 100644
--- a/tests/functional/test_pep517.py
+++ b/tests/functional/test_pep517.py
@@ -7,7 +7,10 @@ from typing import Any
 import pytest
 import tomli_w
 
-from pip._internal.build_env import BuildEnvironment
+from pip._internal.build_env import (
+    BuildEnvironment,
+    SubprocessBuildEnvironmentInstaller,
+)
 from pip._internal.req import InstallRequirement
 
 from tests.lib import (
@@ -43,9 +46,9 @@ def test_backend(tmpdir: Path, data: TestData) -> None:
     req = InstallRequirement(None, None)
     req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
     req.load_pyproject_toml()
-    env = BuildEnvironment()
     finder = make_test_finder(find_links=[data.backends])
-    env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
+    env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
+    env.install_requirements(["dummy_backend"], "normal", kind="Installing")
     conflicting, missing = env.check_requirements(["dummy_backend"])
     assert not conflicting
     assert not missing
@@ -73,7 +76,7 @@ def test_backend_path(tmpdir: Path, data: TestData) -> None:
     req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
     req.load_pyproject_toml()
 
-    env = BuildEnvironment()
+    env = BuildEnvironment(object())  # type: ignore
     assert hasattr(req.pep517_backend, "build_wheel")
     with env:
         assert req.pep517_backend is not None
@@ -91,9 +94,9 @@ def test_backend_path_and_dep(tmpdir: Path, data: TestData) -> None:
     req = InstallRequirement(None, None)
     req.source_dir = os.fspath(project_dir)  # make req believe it has been unpacked
     req.load_pyproject_toml()
-    env = BuildEnvironment()
     finder = make_test_finder(find_links=[data.backends])
-    env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
+    env = BuildEnvironment(SubprocessBuildEnvironmentInstaller(finder))
+    env.install_requirements(["dummy_backend"], "normal", kind="Installing")
 
     assert hasattr(req.pep517_backend, "build_wheel")
     with env:
diff --git a/tests/lib/configuration_helpers.py b/tests/lib/configuration_helpers.py
index 2826f2ee1..e4dc2323f 100644
--- a/tests/lib/configuration_helpers.py
+++ b/tests/lib/configuration_helpers.py
@@ -28,7 +28,8 @@ class ConfigurationMixin:
         @functools.wraps(old)
         def overridden() -> None:
             # Manual Overload
-            self.configuration._config[variant].update(di)
+            self.configuration._config[variant].setdefault("fakefile", {})
+            self.configuration._config[variant]["fakefile"].update(di)
             # Configuration._parsers has type:
             # Dict[Kind, List[Tuple[str, RawConfigParser]]].
             # As a testing convenience, pass a special value.
diff --git a/tests/requirements-common_wheels.txt b/tests/requirements-common_wheels.txt
deleted file mode 100644
index 31450a0e8..000000000
--- a/tests/requirements-common_wheels.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-# Create local setuptools wheel files for testing by:
-# 1. Cloning setuptools and checking out the branch of interest
-# 2. Running `python3 bootstrap.py` in that directory
-# 3. Running `python3 -m pip wheel --no-cache -w /tmp/setuptools_build_meta_legacy/ .`
-# 4. Replacing the `setuptools` entry below with a `file:///...` URL
-# (Adjust artifact directory used based on preference and operating system)
-
-# We pin setuptools<80 because our test suite currently
-# depends on setup.py develop to generate egg-link files.
-setuptools >= 40.8.0, != 60.6.0, <80
-wheel
-# As required by pytest-cov.
-coverage >= 4.4
diff --git a/tests/requirements.txt b/tests/requirements.txt
deleted file mode 100644
index 0568aea51..000000000
--- a/tests/requirements.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-cryptography
-freezegun
-installer
-pytest
-pytest-cov
-pytest-rerunfailures
-pytest-xdist
-scripttest
-setuptools
-# macOS (darwin) arm64 always uses virtualenv >= 20.0
-# for other platforms, it depends on python version
-virtualenv < 20.0 ; python_version < '3.10' and (sys_platform != 'darwin' or platform_machine != 'arm64')
-virtualenv >= 20.0 ; python_version >= '3.10' or (sys_platform == 'darwin' and platform_machine == 'arm64')
-werkzeug
-wheel
-tomli-w
-proxy.py
diff --git a/tests/unit/test_cli_spinners.py b/tests/unit/test_cli_spinners.py
new file mode 100644
index 000000000..c196795da
--- /dev/null
+++ b/tests/unit/test_cli_spinners.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import logging
+from collections.abc import Generator
+from contextlib import contextmanager
+from io import StringIO
+from typing import Callable
+from unittest.mock import Mock
+
+import pytest
+
+from pip._vendor.rich.console import Console
+
+from pip._internal.cli import spinners
+from pip._internal.cli.spinners import open_rich_spinner
+
+
+@contextmanager
+def patch_logger_level(level: int) -> Generator[None]:
+    """Patch the spinner logger level temporarily."""
+    original_level = spinners.logger.level
+    spinners.logger.setLevel(level)
+    try:
+        yield
+    finally:
+        spinners.logger.setLevel(original_level)
+
+
+class TestRichSpinner:
+    @pytest.mark.parametrize(
+        "status, func",
+        [
+            ("done", lambda: None),
+            ("error", lambda: 1 / 0),
+            ("canceled", Mock(side_effect=KeyboardInterrupt)),
+        ],
+    )
+    def test_finish(self, status: str, func: Callable[[], None]) -> None:
+        """
+        Check that the spinner finish message is set correctly depending
+        on how the spinner came to a stop.
+        """
+        stream = StringIO()
+        try:
+            with patch_logger_level(logging.INFO):
+                with open_rich_spinner("working", Console(file=stream)):
+                    func()
+        except BaseException:
+            pass
+
+        output = stream.getvalue()
+        assert output == f"working ... {status}"
+
+    @pytest.mark.parametrize(
+        "level, visible",
+        [(logging.ERROR, False), (logging.INFO, True), (logging.DEBUG, True)],
+    )
+    def test_verbosity(self, level: int, visible: bool) -> None:
+        """Is the spinner hidden at the appropriate verbosity?"""
+        stream = StringIO()
+        with patch_logger_level(level):
+            with open_rich_spinner("working", Console(file=stream)):
+                pass
+
+        assert bool(stream.getvalue()) == visible
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index ab9b8ea71..054713113 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -17,6 +17,7 @@ import pytest
 from pip._vendor.packaging.markers import Marker
 from pip._vendor.packaging.requirements import Requirement
 
+from pip._internal.build_env import SubprocessBuildEnvironmentInstaller
 from pip._internal.cache import WheelCache
 from pip._internal.commands import create_command
 from pip._internal.commands.install import InstallCommand
@@ -97,11 +98,13 @@ class TestRequirementSet:
         session = PipSession()
 
         with get_build_tracker() as tracker:
+            installer = SubprocessBuildEnvironmentInstaller(finder)
             preparer = RequirementPreparer(
                 build_dir=os.path.join(self.tempdir, "build"),
                 src_dir=os.path.join(self.tempdir, "src"),
                 download_dir=None,
                 build_isolation=True,
+                build_isolation_installer=installer,
                 check_build_deps=False,
                 build_tracker=tracker,
                 session=session,
diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py
index 3726591ea..93f68fd7b 100644
--- a/tests/unit/test_vcs.py
+++ b/tests/unit/test_vcs.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import logging
 import os
 import pathlib
 from typing import Any
@@ -271,6 +272,7 @@ def test_git_resolve_revision_rev_not_found(get_sha_mock: mock.Mock) -> None:
 def test_git_resolve_revision_not_found_warning(
     get_sha_mock: mock.Mock, caplog: pytest.LogCaptureFixture
 ) -> None:
+    caplog.set_level(logging.INFO)
     get_sha_mock.return_value = (None, False)
     url = HiddenText("git+https://git.example.com", redacted="*")
     sha = 40 * "a"