Skip to content

pip fails to build project from backend-path when MetaPathFinder is present and installing from VCS url #11812

Open
@abravalheri

Description

@abravalheri

Description

pip will fail to build a project in the following circumstances:

  • A build-backend is installed using a MetaPathFinder (e.g. as if it was installed in editable mode) that is inserted before importlib.machinery.PathFinder.
  • The same virtual environment is used to perform an installation from a VCS URL of a project which matches the name of the build-backend previously installed (to a target directory).
Real-world scenario/motivation:

I run into this problem when trying to address pypa/setuptools#3806 and pypa/setuptools#3828. In those issues, users describe problems with the precedence of editable installs with MetaPathFinder. My initial approach was to "bump" the precedence by changing the insert position in sys.meta_path and sys.path_hook.

However, when running the tests with pytest-perf, I received an error message from pip:

pip._vendor.pyproject_hooks._impl.BackendInvalid: Backend was not loaded from backend-path

Note that pytest-perf will install the project from the tracking VCS URL to use as a baseline for the performance comparison.

References

Implementation: pypa/setuptools#3829
Failed tests: https://github.com/pypa/setuptools/actions/runs/4187531113/jobs/7257489290#step:7:646

Expected behavior

pip would build the project in an isolated environment that is not contaminated by the virtual environment where pip is installed. This should be inline with PEP 517 description:

A build frontend SHOULD, by default, create an isolated environment for each build, containing only the standard library and any explicitly requested build-dependencies.

(or at least use importlib.util.spec_from_file_location to ensure the build-backend is loaded from the place specified in pyproject.toml > [build-system] > backend-path)

pip version

23.0.1

Python version

Tested in 3.10.6 and 3.10.9

OS

Tested in Ubuntu 20.04.5 LTS and also python:3.10 container (Debian GNU/Linux 11 - bullseye)

How to Reproduce

  1. First we create a dummy backend that uses backend-path
    > docker run --rm -it python:3.10 /bin/bash
    
    rm -rf /tmp/my_backend
    mkdir -p /tmp/my_backend
    cd /tmp/my_backend
    cat <<EOF > pyproject.toml
    [build-system]
    requires = ["flit_core >=3.8.0,<4"]
    backend-path = ["."]
    build-backend = "my_backend"
    
    [project]
    name = "my_backend"
    version = "0.0.0"
    description = "foobar"
    EOF
    
    # Let's simulate a custom build backend ...
    # Assume it has a `build_editable` that uses a MetaPathFinder
    # inserted at position 0 (omitted for the sake of brevity).
    echo 'from flit_core.buildapi import *' > my_backend.py
  2. Then we simulate the backend installation in "editable mode" using a MetaPathFinder:
    rm -rf /tmp/.venv
    python3.10 -m venv /tmp/.venv
    /tmp/.venv/bin/python -m pip install -U 'pip==23.0.1'
    /tmp/.venv/bin/python -m pip install 'flit_core >=3.8.0,<4'
    
    # Let's simulate the custom build backend was previously installed in editable mode
    cat <<EOF > /tmp/.venv/lib/python3.10/site-packages/_editable_impl_my_backend.py
    import sys
    from importlib.util import spec_from_file_location
    
    MAPPING = {"my_backend": "/tmp/my_backend/my_backend.py"}
    
    class _EditableFinder:  # MetaPathFinder
        @classmethod
        def find_spec(cls, fullname, path=None, target=None):
            if fullname in MAPPING:
                return spec_from_file_location(fullname, MAPPING[fullname])
    
    def install():
        if not any(finder == _EditableFinder for finder in sys.meta_path):
            sys.meta_path.insert(0, _EditableFinder)
    EOF
    echo 'import _editable_impl_my_backend; _editable_impl_my_backend.install()' > /tmp/.venv/lib/python3.10/site-packages/my_backend.pth
    
    mkdir /tmp/other
    cd /tmp/other
    /tmp/.venv/bin/python -c 'import my_backend; print(my_backend)'
    # => <module 'my_backend' from '/tmp/my_backend/my_backend.py'>
  3. Finally, we try to install a different version of the same backend from a VCS URL - which will result in an error:
     rm -rf /tmp/install_target
     /tmp/.venv/bin/python -m pip -vvv install -t /tmp/install_target "my_backend @ git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git"
     # ...
     # pip._vendor.pyproject_hooks._impl.BackendInvalid: Backend was not loaded from backend-path

Please note that the backend is not really invalid in this case... What happens is that the build frontend is loading it from a different location instead of loading it from backend-path.

Output

# docker run --rm -it python:3.10 /bin/bash
$ rm -rf /tmp/my_backend
$ mkdir -p /tmp/my_backend
$ cd /tmp/my_backend
$ cat <<EOF > pyproject.toml
[build-system]
requires = ["flit_core >=3.8.0,<4"]
backend-path = ["."]
build-backend = "my_backend"

[project]
name = "my_backend"
version = "0.0.0"
description = "foobar"
EOF
$ echo 'from flit_core.buildapi import *' > my_backend.py
$ rm -rf /tmp/.venv
$ python3.10 -m venv /tmp/.venv
$ /tmp/.venv/bin/python -m pip install -U 'pip==23.0.1'
Collecting pip==23.0.1
  Downloading pip-23.0.1-py3-none-any.whl (2.1 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 1.2 MB/s eta 0:00:00
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 22.2.1
    Uninstalling pip-22.2.1:
      Successfully uninstalled pip-22.2.1
Successfully installed pip-23.0.1
$ /tmp/.venv/bin/python -m pip install 'flit_core >=3.8.0,<4'
Collecting flit_core<4,>=3.8.0
  Downloading flit_core-3.8.0-py3-none-any.whl (62 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 1.1 MB/s eta 0:00:00
Installing collected packages: flit_core
Successfully installed flit_core-3.8.0
$ cat <<EOF > /tmp/.venv/lib/python3.10/site-packages/_editable_impl_my_backend.py
import sys
from importlib.util import spec_from_file_location

MAPPING = {"my_backend": "/tmp/my_backend/my_backend.py"}

class _EditableFinder:  # MetaPathFinder
    @classmethod
    def find_spec(cls, fullname, path=None, target=None):
        if fullname in MAPPING:
            return spec_from_file_location(fullname, MAPPING[fullname])

def install():
    if not any(finder == _EditableFinder for finder in sys.meta_path):
        sys.meta_path.insert(0, _EditableFinder)
EOF
$ echo 'import _editable_impl_my_backend; _editable_impl_my_backend.install()' > /tmp/.venv/lib/python3.10/site-packages/my_backend.pth
$ mkdir /tmp/other
$ cd /tmp/other
$ /tmp/.venv/bin/python -c 'import my_backend; print(my_backend)'
<module 'my_backend' from '/tmp/my_backend/my_backend.py'>
$ rm -rf /tmp/install_target
$ /tmp/.venv/bin/python -m pip -vvv install -t /tmp/install_target "my_backend @ git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git"
Using pip 23.0.1 from /tmp/.venv/lib/python3.10/site-packages/pip (python 3.10)
Non-user install due to --prefix or --target option
Created temporary directory: /tmp/pip-target-rb_hedkt
Created temporary directory: /tmp/pip-build-tracker-vpe84uob
Initialized build tracking at /tmp/pip-build-tracker-vpe84uob
Created build tracker: /tmp/pip-build-tracker-vpe84uob
Entered build tracker: /tmp/pip-build-tracker-vpe84uob
Created temporary directory: /tmp/pip-install-rjiqcf3e
Created temporary directory: /tmp/pip-ephem-wheel-cache-thvjcdal
Collecting my_backend@ git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git
  Cloning https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git to /tmp/pip-install-rjiqcf3e/my-backend_90e570b192394d7287e6c771158ffeb3
  Running command git version
  git version 2.30.2
  Running command git clone --filter=blob:none --verbose --progress https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git /tmp/pip-install-rjiqcf3e/my-backend_90e570b192394d7287e6c771158ffeb3
  Cloning into '/tmp/pip-install-rjiqcf3e/my-backend_90e570b192394d7287e6c771158ffeb3'...
  POST git-upload-pack (164 bytes)
  POST git-upload-pack (272 bytes)
  remote: Enumerating objects: 2, done.
  remote: Counting objects:  50% (1/2)
  remote: Counting objects: 100% (2/2)
  remote: Counting objects: 100% (2/2), done.
  remote: Compressing objects:  50% (1/2)
  remote: Compressing objects: 100% (2/2)
  remote: Compressing objects: 100% (2/2), done.
  remote: Total 2 (delta 0), reused 2 (delta 0), pack-reused 0
  Receiving objects:  50% (1/2)
  Receiving objects: 100% (2/2)
  Receiving objects: 100% (2/2), done.
  Running command git rev-parse HEAD
  1577d5b524e74766a2b9b8cf6a7cc73e630794e7
  Resolved https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git to commit 1577d5b524e74766a2b9b8cf6a7cc73e630794e7
  Running command git rev-parse HEAD
  1577d5b524e74766a2b9b8cf6a7cc73e630794e7
  Added my_backend@ git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git from git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git to build tracker '/tmp/pip-build-tracker-vpe84uob'
  Created temporary directory: /tmp/pip-build-env-wbowle8_
  Running command pip subprocess to install build dependencies
  Using pip 23.0.1 from /tmp/.venv/lib/python3.10/site-packages/pip (python 3.10)
  Collecting flit_core<4,>=3.8.0
    Using cached flit_core-3.8.0-py3-none-any.whl (62 kB)
  Installing collected packages: flit_core
  Successfully installed flit_core-3.8.0
  Installing build dependencies ... done
  Running command Getting requirements to build wheel
  Getting requirements to build wheel ... done
ERROR: Exception:
Traceback (most recent call last):
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/cli/base_command.py", line 160, in exc_logging_wrapper
    status = run_func(*args)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/cli/req_command.py", line 247, in wrapper
    return func(self, options, args)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/commands/install.py", line 419, in run
    requirement_set = resolver.resolve(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 73, in resolve
    collected = self.factory.collect_root_requirements(root_reqs)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 491, in collect_root_requirements
    req = self._make_requirement_from_install_req(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 453, in _make_requirement_from_install_req
    cand = self._make_candidate_from_link(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 206, in _make_candidate_from_link
    self._link_candidate_cache[link] = LinkCandidate(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 297, in __init__
    super().__init__(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 162, in __init__
    self.dist = self._prepare()
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 231, in _prepare
    dist = self._prepare_distribution()
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 308, in _prepare_distribution
    return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/operations/prepare.py", line 491, in prepare_linked_requirement
    return self._prepare_linked_requirement(req, parallel_builds)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/operations/prepare.py", line 577, in _prepare_linked_requirement
    dist = _get_prepared_distribution(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/operations/prepare.py", line 69, in _get_prepared_distribution
    abstract_dist.prepare_distribution_metadata(
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/distributions/sdist.py", line 48, in prepare_distribution_metadata
    self._install_build_reqs(finder)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/distributions/sdist.py", line 118, in _install_build_reqs
    build_reqs = self._get_build_requires_wheel()
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/distributions/sdist.py", line 95, in _get_build_requires_wheel
    return backend.get_requires_for_build_wheel()
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_internal/utils/misc.py", line 701, in get_requires_for_build_wheel
    return super().get_requires_for_build_wheel(config_settings=cs)
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_vendor/pyproject_hooks/_impl.py", line 166, in get_requires_for_build_wheel
    return self._call_hook('get_requires_for_build_wheel', {
  File "/tmp/.venv/lib/python3.10/site-packages/pip/_vendor/pyproject_hooks/_impl.py", line 323, in _call_hook
    raise BackendInvalid(
pip._vendor.pyproject_hooks._impl.BackendInvalid: Backend was not loaded from backend-path
Remote version of pip: 23.0.1
Local version of pip:  23.0.1
Was pip installed by pip? True
Removed my_backend@ git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git from git+https://gist.github.com/4f1f86d879179e2d185b6e82e4f50d17.git from build tracker '/tmp/pip-build-tracker-vpe84uob'
Removed build tracker: '/tmp/pip-build-tracker-vpe84uob'

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    C: build logicStuff related to metadata generation / wheel generationtype: bugA confirmed bug or unintended behavior

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions