Repository: pypa/pip

Issue:
regression: `--no-build-isolation` validation causes failure with setup deps installed from custom commit
### Description

After updating from pip `22.0` to `22.1`, `--no-build-isolation` is incompatible with packages installed from a custom commit.
We have setup code that depends on ctypesgen, which did not get a PyPI update for 3 years, while git main is improved regularly and considered stable. Therefore, we install ctypesgen from the latest sources, and then pass `--no-build-isolation` so that the newer ctypesgen is used, not a PyPI release downloaded into a venv. Since pip `22.1`, this fails with the following error:
```
ERROR: Some build dependencies for file:///home/mara/projects/pypdfium2 conflict with the backend dependencies: ctypesgen==1.0.3.dev98+g2120dbf is incompatible with ctypesgen.
```
This makes it impossible to install a package via `pip3 install` while using an unreleased version of a setup dependency.
I am aware that dependency validation was added on purpose, but this particular behaviour is neither justified nor desirable.


### Expected behavior

`ctypesgen==1.0.3.dev98+g2120dbf` fulfils the requirement `ctypesgen` defined in `pyproject.toml:build-system:requires`. It should thus not cause an error.

Commit in question: https://github.com/pypa/pip/commit/0c6d20f8c70a561fe7d13bd3baf3c82eb2cd2b0e

### pip version

22.1

### Python version

3.8.10

### OS

Ubuntu 20.04 (Linux)

### How to Reproduce

```bash
git clone "https://github.com/ctypesgen/ctypesgen.git"
cd ctypesgen
pip3 install .
cd ..
python3 -m pip install pip==22.1
git clone "https://github.com/pypdfium2-team/pypdfium2.git"
cd pypdfium2
pip3 install . -v --no-build-isolation
cd ..
rm -rf ctypesgen pypdfium2
```

### Output

```sh-session
mara@ub2020:/tmp$ git clone "https://github.com/ctypesgen/ctypesgen.git"
Klone nach 'ctypesgen' …
remote: Enumerating objects: 2014, done.
remote: Counting objects: 100% (199/199), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 2014 (delta 157), reused 145 (delta 145), pack-reused 1815
Empfange Objekte: 100% (2014/2014), 1.11 MiB | 6.06 MiB/s, fertig.
Löse Unterschiede auf: 100% (1295/1295), fertig.
mara@ub2020:/tmp$ cd ctypesgen
mara@ub2020:/tmp/ctypesgen$ pip3 install .
Defaulting to user installation because normal site-packages is not writeable
Processing /tmp/ctypesgen
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Installing backend dependencies ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: ctypesgen
  Building wheel for ctypesgen (pyproject.toml) ... done
  Created wheel for ctypesgen: filename=ctypesgen-1.0.3.dev98+g2120dbf-py3-none-any.whl size=124318 sha256=b456fcde59ffd9a4b32308b9f9edd79d44dba1a0951b2423b28a66a7b6c8d15f
  Stored in directory: /tmp/pip-ephem-wheel-cache-bahnw45i/wheels/ea/0e/da/227e0ec876339d2ee5e1ec3bf0476fd4eb2076109ca8d08ee0
Successfully built ctypesgen
Installing collected packages: ctypesgen
  Attempting uninstall: ctypesgen
    Found existing installation: ctypesgen 1.0.3.dev98+g2120dbf
    Uninstalling ctypesgen-1.0.3.dev98+g2120dbf:
      Successfully uninstalled ctypesgen-1.0.3.dev98+g2120dbf
Successfully installed ctypesgen-1.0.3.dev98+g2120dbf
mara@ub2020:/tmp/ctypesgen$ cd ..
mara@ub2020:/tmp$ python3 -m pip install pip==22.1
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: pip==22.1 in /home/mara/.local/lib/python3.8/site-packages (22.1)
mara@ub2020:/tmp$ git clone "https://github.com/pypdfium2-team/pypdfium2.git"
Klone nach 'pypdfium2' …
remote: Enumerating objects: 5138, done.
remote: Counting objects: 100% (2084/2084), done.
remote: Compressing objects: 100% (589/589), done.
remote: Total 5138 (delta 1542), reused 1824 (delta 1390), pack-reused 3054
Empfange Objekte: 100% (5138/5138), 1.19 MiB | 2.95 MiB/s, fertig.
Löse Unterschiede auf: 100% (3415/3415), fertig.
mara@ub2020:/tmp$ cd pypdfium2
mara@ub2020:/tmp/pypdfium2$ pip3 install . -v --no-build-isolation
Using pip 22.1 from /home/mara/.local/lib/python3.8/site-packages/pip (python 3.8)
Defaulting to user installation because normal site-packages is not writeable
Processing /tmp/pypdfium2
ERROR: Some build dependencies for file:///tmp/pypdfium2 conflict with the backend dependencies: ctypesgen==1.0.3.dev98+g2120dbf is incompatible with ctypesgen.
mara@ub2020:/tmp/pypdfium2$ cd ..
mara@ub2020:/tmp$ rm -rf ctypesgen pypdfium2
mara@ub2020:/tmp$
```


### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).


Discussion:
duplicate https://github.com/pypa/pip/issues/11116
> duplicate https://github.com/pypa/pip/issues/11116

This is another case broken by the change, but it's different, as you can deduce from the error message. I don't think this issue is a duplicate of it. I like the general idea behind validating dependencies with `--no-build-isolation`, but this is just a situation where it misbehaves, which should be fixed. Making validation optional is good, but won't fix the actual problem I reported here.
Its the same issue with another case: conflicting reqs. The fix is already merged #11117
This case is *claimed* to be a conflicting requirement, but it's erroneous:
We have the requirement `ctypesgen`, and `ctypesgen==1.0.3.dev98+g2120dbf` is supposed to fulfil this, but pip complains. So while https://github.com/pypa/pip/pull/11117 will restore the previous behaviour, it will not fix the actual bug in the validation.
Srry, yes, its another issue.
This is because `1.0.3.dev98+g2120dbf` is a pre-release which the `packaging` library, used to check whether a version exists in a range, discards by default.  This:

https://github.com/pypa/pip/blob/30af8074bf83d41a9dacdcd13fb6ca982856032d/src/pip/_internal/build_env.py#L190

... should be:

```py
                if not req.specifier.contains(dist.version, prereleases=True):
```

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
@@ -185,7 +185,7 @@ def check_requirements(
                     installed_req_str = f"{req.name}=={dist.version}"
                 else:
                     installed_req_str = f"{req.name}==={dist.version}"
-                if dist.version not in req.specifier:
+                if not req.specifier.contains(dist.version, prereleases=True):
                     conflicting.add((installed_req_str, req_str))
                 # FIXME: Consider direct URL?
         return conflicting, missing


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
@@ -5,7 +5,13 @@
 
 from pip._internal.build_env import BuildEnvironment
 from pip._internal.req import InstallRequirement
-from tests.lib import PipTestEnvironment, TestData, make_test_finder, path_to_url
+from tests.lib import (
+    PipTestEnvironment,
+    TestData,
+    create_basic_wheel_for_package,
+    make_test_finder,
+    path_to_url,
+)
 from tests.lib.path import Path
 
 
@@ -210,6 +216,25 @@ def test_validate_conflicting_pep517_backend_requirements(
     assert result.returncode != 0 and msg in result.stderr, str(result)
 
 
+def test_pep517_backend_requirements_satisfied_by_prerelease(
+    script: PipTestEnvironment,
+    data: TestData,
+) -> None:
+    create_basic_wheel_for_package(script, "myreq", "1.0a1")
+    script.pip("install", "myreq==1.0a1", "--no-index", "-f", script.scratch_path)
+    script.pip("install", "test_backend", "--no-index", "-f", data.backends)
+
+    project_dir = make_project(
+        script.temp_path,
+        requires=["test_backend", "myreq"],
+        backend="test_backend",
+    )
+    project_dir.joinpath("backend_reqs.txt").write_text("myreq")
+
+    result = script.pip("install", "--no-index", "--no-build-isolation", project_dir)
+    assert "Installing backend dependencies:" not in result.stdout
+
+
 def test_pep517_backend_requirements_already_satisfied(
     script: PipTestEnvironment, tmpdir: Path, data: TestData
 ) -> None: