Repository: pypa/pip

Issue:
Validate build dependencies when using `--no-build-isolation`
**What's the problem this feature will solve?**

Prevent builds from running when all required build dependencies are not present.

**Describe the solution you'd like**

`pip` fails to build with a descriptive message when a build dependency is missing, providing a nicer user experience (IMHO).

**Alternative Solutions**

1. Use python-build which does exactly that.
2. Manually parse pyproject.toml + generate egg info, get additional build requires and validate those are in the environment.

**Additional context**
I see the pip docs on `--no-build-isolation` state "Build dependencies specified by PEP 518 must be already installed if this option is used.". I was wondering whether pip would like to mimic the behaviour that `python-build` has on [`--no-isolation`](https://pypa-build.readthedocs.io/en/latest/#-no-isolation)(Fail nicely if build dependencies are not present) and if so I'm happy to try and push a patch.

Related: https://github.com/pypa/pip/issues/6718


Discussion:
A sanity check to make sure the user actually sets up the environment correctly makes sense to me.
@mariocj89 and I are interested in helping out with this,
@uranusjr @pradyunsg please make sure to tag the issue appropriately?
@uranusjr @pradyunsg would you be able to please give a few pointers on where I should start looking in the code and what this change is going to look like?
https://github.com/pypa/pip/blob/main/src/pip/_internal/operations/build/wheel.py and https://github.com/pypa/pip/blob/main/src/pip/_internal/operations/build/metadata.py -- that's where you wanna look for PEP 517 build logic.

If you wanna have a longer chat/discussion about this code, ping me sometime during a work day. :)

The change needed here is calling `build_env.check_requirements(build_deps)` at some point in the codebase, since https://github.com/pypa/pip/blob/main/src/pip/_internal/build_env.py#L158 has the relevant logic already IIUC.

I'm pretty sure the things to figure out here are:

1. Where to make that call.
2. How to get `build_deps` to make that call.


Resolving patch:
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -20,7 +20,7 @@
 from pip import __file__ as pip_location
 from pip._internal.cli.spinners import open_spinner
 from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib
-from pip._internal.metadata import get_environment
+from pip._internal.metadata import get_default_environment, get_environment
 from pip._internal.utils.subprocess import call_subprocess
 from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
 
@@ -168,7 +168,11 @@ def check_requirements(
         missing = set()
         conflicting = set()
         if reqs:
-            env = get_environment(self._lib_dirs)
+            env = (
+                get_environment(self._lib_dirs)
+                if hasattr(self, "_lib_dirs")
+                else get_default_environment()
+            )
             for req_str in reqs:
                 req = Requirement(req_str)
                 dist = env.get_distribution(req.name)
diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py
--- a/src/pip/_internal/distributions/sdist.py
+++ b/src/pip/_internal/distributions/sdist.py
@@ -43,7 +43,16 @@ def prepare_distribution_metadata(
             self.req.isolated_editable_sanity_check()
             # Install the dynamic build requirements.
             self._install_build_reqs(finder)
-
+        elif self.req.use_pep517:
+            pyproject_requires = self.req.pyproject_requires
+            assert pyproject_requires is not None
+            conflicting, missing = self.req.build_env.check_requirements(
+                pyproject_requires
+            )
+            if conflicting:
+                self._raise_conflicts("the backend dependencies", conflicting)
+            if missing:
+                self._raise_missing_reqs(missing)
         self.req.prepare_metadata()
 
     def _prepare_build_backend(self, finder: PackageFinder) -> None:
@@ -125,3 +134,12 @@ def _raise_conflicts(
             ),
         )
         raise InstallationError(error_message)
+
+    def _raise_missing_reqs(self, missing: Set[str]) -> None:
+        format_string = (
+            "Some build dependencies for {requirement} are missing: {missing}."
+        )
+        error_message = format_string.format(
+            requirement=self.req, missing=", ".join(map(repr, sorted(missing)))
+        )
+        raise InstallationError(error_message)


Test patch:
diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py
--- a/tests/functional/test_pep517.py
+++ b/tests/functional/test_pep517.py
@@ -160,6 +160,56 @@ def test_conflicting_pep517_backend_requirements(
     assert result.returncode != 0 and msg in result.stderr, str(result)
 
 
+def test_validate_missing_pep517_backend_requirements(
+    script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+    project_dir = make_project(
+        tmpdir, requires=["test_backend", "simplewheel==1.0"], backend="test_backend"
+    )
+    result = script.pip(
+        "install",
+        "--no-index",
+        "-f",
+        data.backends,
+        "-f",
+        data.packages,
+        "--no-build-isolation",
+        project_dir,
+        expect_error=True,
+    )
+    msg = (
+        "Some build dependencies for {url} are missing: "
+        "'simplewheel==1.0', 'test_backend'.".format(url=path_to_url(project_dir))
+    )
+    assert result.returncode != 0 and msg in result.stderr, str(result)
+
+
+def test_validate_conflicting_pep517_backend_requirements(
+    script: PipTestEnvironment, tmpdir: Path, data: TestData
+) -> None:
+    project_dir = make_project(
+        tmpdir, requires=["simplewheel==1.0"], backend="test_backend"
+    )
+    script.pip("install", "simplewheel==2.0", "--no-index", "-f", data.packages)
+    result = script.pip(
+        "install",
+        "--no-index",
+        "-f",
+        data.backends,
+        "-f",
+        data.packages,
+        "--no-build-isolation",
+        project_dir,
+        expect_error=True,
+    )
+    msg = (
+        "Some build dependencies for {url} conflict with the backend "
+        "dependencies: simplewheel==2.0 is incompatible with "
+        "simplewheel==1.0.".format(url=path_to_url(project_dir))
+    )
+    assert result.returncode != 0 and msg in result.stderr, str(result)
+
+
 def test_pep517_backend_requirements_already_satisfied(
     script: PipTestEnvironment, tmpdir: Path, data: TestData
 ) -> None: