Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

venv base path does not resolve symlinks using realpath() #106045

Open
nascheme opened this issue Jun 23, 2023 · 14 comments
Open

venv base path does not resolve symlinks using realpath() #106045

nascheme opened this issue Jun 23, 2023 · 14 comments
Labels
topic-venv Related to the venv module type-bug An unexpected behavior, bug, or error

Comments

@nascheme
Copy link
Member

nascheme commented Jun 23, 2023

It seems there is a bug in venv (and a similar one in virtualenv) where the "base" path in pyvenv.cfg is set incorrectly. If Python is installed in a non-standard folder, e.g. /usr/local/python-X.Y.Z and then symlinked into /usr/local/bin/python3, the venv package does not work correctly. I believe the source of the trouble is the setting of "home" variable. Specifically, this line:

dirname, exename = os.path.split(os.path.abspath(executable))

The dirname result is used to set "home". If the executable path is /usr/local/bin/python3 (actually a symlink to /usr/local/python-X.Y.Z/bin/python3), the "home" should not be set to /usr/local. Changing the above line (in the ensure_directories()) function to:

dirname, exename = os.path.split(os.path.realpath(executable))

This fixes the problem. I believe this is consistent with what the getpath.py module in Python does.

I noticed with problem when running the most recent Debian OS, version 12. It includes Python 3.11 and therefore /usr/lib/python3.11 exists. With the above bug, the venv tries to use /usr/lib/python3.11 as the sys.path. Importing the struct module fails with the mysterious error:

>>> import struct
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.11/struct.py", line 13, in <module>
    from _struct import *
ModuleNotFoundError: No module named '_struct'

Linked PRs

@nascheme nascheme added the type-bug An unexpected behavior, bug, or error label Jun 23, 2023
@iritkatriel iritkatriel added the topic-venv Related to the venv module label Nov 24, 2023
@mayeut
Copy link

mayeut commented Jan 14, 2024

I've run into this as well:
Python built from source & installed in /opt/python-3.11, symlink in /usr/local/bin/python3.11.

In my case, it was on ubuntu 22.04, the venv could not be created with a failure to import subprocess because indeed it was trying to use /usr/lib/python3.11 for lib-dynload in sys.path.
Changing home manually or patching the venv module worked.

reproducer in GHA: https://github.com/mayeut/sandbox/actions/runs/7518304095/job/20465487706#step:3:81

@cjolowicz
Copy link

Note that resolving symlinks will break Homebrew, see astral-sh/uv#1640

@konstin
Copy link

konstin commented Mar 3, 2024

I noticed with problem when running the most recent Debian OS, version 12. It includes Python 3.11 and therefore /usr/lib/python3.11 exists. With the above bug, the venv tries to use /usr/lib/python3.11 as the sys.path. Importing the struct module fails with the mysterious error:

Do you have instructions how to reproduce this? We'd like to avoid this problem in uv.

From docker run --rm debian:12

$ apt update
$ apt install -y python3-venv
$ which python3
/usr/bin/python3
$ python3 --version
Python 3.11.2
$ ln -s /usr/bin/python3 /usr/local/bin/python3

Then venv/bin/python -c "import struct; import subprocess" works fine for me.

Python built from source & installed in /opt/python-3.11, symlink in /usr/local/bin/python3.11.

Same questions, do you have instructions how to reproduce this (outside the github actions one)? I tried this with a python checkout (4d3ee77):

./configure --prefix=/opt/cpython1 --enable-optimizations
make -s -j32
sudo make install

Then i tried:

mkdir a
cd a
ln -s /opt/cpython1/bin/python3 python3
cd ..
mkdir b
cd b
../a/python3 -m venv venv
venv/bin/python -c "import struct; import subprocess"
cd ..
mkdir c
cd c
virtualenv -p ../a/python3 venv
venv/bin/python -c "import struct; import subprocess"

But this all passed.

@mayeut
Copy link

mayeut commented Mar 4, 2024

@konstin,

On ubuntu 22.04, install sudo apt-get install -y python3-distutils, create a symlink to your python3.11 build in /usr/local/bin/python3.11.

I think the fact that the symlink is in /usr/local/bin folder matters. Just creating a symlink elsewhere does not necessarily reveals the issue.

@konstin
Copy link

konstin commented Mar 4, 2024

Do you have more information about where the python3.11 came from? I still can't reproduce

FROM ubuntu:22.04
RUN apt update
RUN apt-get install -y python3-distutils
ENV DEBIAN_FRONTEND=noninteractive
RUN apt install -yy git build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev curl libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
WORKDIR /root
RUN git clone https://github.com/python/cpython/
WORKDIR /root/cpython
RUN git checkout v3.11.8
RUN ./configure --prefix=/opt/cpython1
RUN make -s -j32
RUN make install
WORKDIR /root
RUN /opt/cpython1/bin/python3 --version
RUN ln -s /opt/cpython1/bin/python3 /usr/local/bin/python3
RUN /usr/local/bin/python3 --version
RUN /usr/local/bin/python3 -c "import struct; import subprocess"
RUN /usr/local/bin/python3 -m venv venv1
RUN venv1/bin/python -c "import struct; import subprocess"
RUN venv1/bin/python -m venv venv2
RUN venv2/bin/python -c "import struct; import subprocess"

@mayeut
Copy link

mayeut commented Mar 4, 2024

I could reproduce it using your dockerfile with a slight modification.
The folder /usr/lib/python3.11/lib-dynload should exist for the venv creation to fail.
You can mkdir the folder or also add python3-gdbm to the list of packages to install.

@konstin
Copy link

konstin commented Mar 4, 2024

To summarize, the error only happens (editing this to 3.13 to be actionable on main) when you have a custom built python that is linked to exactly /usr/local/bin/python3, but there's also a system python of the same version installed in /usr installed that created /usr/lib/python3.13/lib-dynload?

I've tried to trace the cause and it seems that installed_platbase gets switched to /usr on venv creation:

$ /opt/cpython1/bin/python3 -m sysconfig | grep -i installed_platbase
        installed_platbase = "/opt/cpython1"
$ venv1/bin/python -m sysconfig | grep -i installed_platbase
        installed_platbase = "/usr"

This in turn changes platinclude to the wrong python:

$ /opt/cpython1/bin/python3 -m sysconfig | head -n 14
Platform: "linux-x86_64"
Python version: "3.13"
Current installation scheme: "posix_prefix"

Paths: 
        data = "/opt/cpython1"
        include = "/opt/cpython1/include/python3.13"
        platinclude = "/opt/cpython1/include/python3.13"
        platlib = "/opt/cpython1/lib/python3.13/site-packages"
        platstdlib = "/opt/cpython1/lib/python3.13"
        purelib = "/opt/cpython1/lib/python3.13/site-packages"
        scripts = "/opt/cpython1/bin"
        stdlib = "/opt/cpython1/lib/python3.13"

$ venv1/bin/python3 -m sysconfig | head -n 14
Platform: "linux-x86_64"
Python version: "3.13"
Current installation scheme: "venv"

Paths: 
        data = "/root/venv1"
        include = "/opt/cpython1/include/python3.13"
        platinclude = "/usr/include/python3.13"
        platlib = "/root/venv1/lib/python3.13/site-packages"
        platstdlib = "/root/venv1/lib/python3.13"
        purelib = "/root/venv1/lib/python3.13/site-packages"
        scripts = "/root/venv1/bin"
        stdlib = "/opt/cpython1/lib/python3.13"

I unfortunately don't understand enough about the python sysconfig machinery to understand how/why installed_platbase is set.

Note that for the system (ubuntu apt install python3.11 python3.11-venv) python, all paths are (correctly) /usr based and stay this way:

$ /usr/bin/python3.11 -m sysconfig | head -n 14
Platform: "linux-x86_64"
Python version: "3.11"
Current installation scheme: "posix_local"

Paths: 
        data = "/usr/local"
        include = "/usr/include/python3.11"
        platinclude = "/usr/include/python3.11"
        platlib = "/usr/local/lib/python3.11/dist-packages"
        platstdlib = "/usr/lib/python3.11"
        purelib = "/usr/local/lib/python3.11/dist-packages"
        scripts = "/usr/local/bin"
        stdlib = "/usr/lib/python3.11"
$ venv-sys/bin/python -m sysconfig | head -n 14
Platform: "linux-x86_64"
Python version: "3.11"
Current installation scheme: "venv"

Paths: 
        data = "/root/venv-sys"
        include = "/usr/include/python3.11"
        platinclude = "/usr/include/python3.11"
        platlib = "/root/venv-sys/lib/python3.11/site-packages"
        platstdlib = "/root/venv-sys/lib/python3.11"
        purelib = "/root/venv-sys/lib/python3.11/site-packages"
        scripts = "/root/venv-sys/bin"
        stdlib = "/usr/lib/python3.11"

@cjolowicz
Copy link

cjolowicz commented Mar 4, 2024

I haven't looked into this in detail, but might the real issue here be that you're effectively running the interpreter in a broken system-wide environment under /usr/local? In other words, don't just symlink the interpreter. (Disclaimer: I'm not an expert in the sysconfig machinery either.)

The actual command used to launch the interpreter determines what's considered the environment, even if it's a symbolic link into a valid installation. To my knowledge, sysconfig uses the nominal (unresolved) interpreter path to expand the location templates in installation schemes. (not sure about that, should double check)

@nascheme
Copy link
Member Author

nascheme commented Mar 5, 2024

I haven't looked into this in detail, but might the real issue here be that you're effectively running the interpreter in a broken system-wide environment under /usr/local?

I don't know what a "broken system-wide environment" means. As I said in the description, I think this is a bug in venv since it doesn't match what getpath.c does.

Rather than having venv have complicated logic to figure out what "home" should be, perhaps it should just run something like: python3 -c "import sysconfig; print(sysconfig.get_paths())".

I've worked around the issue by creating wrapper shell scripts for executables, rather than using symlinks, e.g. usr/local/bin/python3 contains:

#!/bin/sh
exec /usr/local/python-3.12.2/bin/python3.12 "$@"

mayeut added a commit to mayeut/cpython that referenced this issue May 11, 2024
mayeut added a commit to mayeut/cpython that referenced this issue Sep 28, 2024
@mayeut
Copy link

mayeut commented Sep 28, 2024

In order not to break usage involving symlinks to a python install tree, rather than resolving the realpath, only executable symlinks can be checked. xref pypa/virtualenv#2770 (comment)

@pelson
Copy link
Contributor

pelson commented Jan 8, 2025

To summarize, the error only happens (editing this to 3.13 to be actionable on main) when you have a custom built python that is linked to exactly /usr/local/bin/python3, but there's also a system python of the same version installed in /usr installed that created /usr/lib/python3.13/lib-dynload

Yes, I've been tracking this down, and it boils down to sys.base_exec_prefix being wrong. In turn, there are docs at

Return the *exec-prefix* for installed platform-*dependent* files. This is
which state:

Return the exec-prefix for installed platform-dependent files. This is
derived through a number of complicated rules from the program name set with
:c:member:PyConfig.program_name and some environment variables;

I note also that setting home in pyvenv.cfg to f'{sys.base_prefix}/bin (instead of dirname(sys.executable)) also works-around this issue (IMO this is a little "less-wrong" than resolving the symlink to sys.executable, but still not correct).

I've spent a lot of time digging into the logic of getpath.py where the venv path work is done, and realised that the fundamental problem here is that having a symlinked executable outside of an installation prefix is not something that was considered by PEP-405. Specifically:

If a home key is found, this signifies that the Python binary belongs to a virtual environment, and the value of the home key is the directory containing the Python executable used to create this virtual environment.
In this case, prefix-finding continues as normal using the value of the home key as the effective Python binary location, which finds the prefix of the base installation

Clearly if the executable (symlinked) is in a different location to the prefix, then the documented approach is going to result in a computed prefix from the symlink (if that prefix has the appropriate landmark "lib-dynlib" directory)

This explains why resolving the symlink to executable before using that as home does work in many cases, but that solution misses the subtlety of symlinked prefixes that getpath handles well for venv-less prefix resolution (which doesn't fully resolve symlinks, but instead stops descending as soon as it finds a suitable prefix).

Note that today, CPython has no problem with non-venv symlinked executables living outside of the prefix. Using exactly the same setup as above, we get the desired behaviour:

$ /usr/local/bin/python3 -c "import sys; print(f'{sys.executable=}\n{sys.prefix=}')"
sys.executable='/usr/local/bin/python3'
sys.prefix='/opt/cpython1'

Perhaps @FFY00 (given their recent changes to getpath) has some idea of whether we can use exactly the same logic for venv base_prefix calculation as we do for non-venv prefix calculation?

Fundamentally, my belief is that the definition of home in PEP-405 should really be updated if it were to properly support symlinked executables outside of their prefix - the reason is that we don't record the actual base executable, and have to guess it based on heuristics (and the original symlink could have been named arbitrarily) . The OP's proposal to automatically resolve the symlink would be inconsistent with what Python does for non-venvs, and in my opinion, would be the wrong thing to do (as mentioned by others in this issue - it would break certain established / reasonable usages).

Given that today it works well to run a symlinked executable which is outside of its prefix for non-venv cases, I would propose that the PEP is updated to replace home with a base_executable key (which is simply the value of sys.base_executable), upon which the existing getpath logic for determining its prefix and lib directories can be applied.

I'd be happy to turn this into a discussion on python.org, if this is the best way to establish a solid rationale and consensus (please confirm if this is the best approach).

@pelson
Copy link
Contributor

pelson commented Jan 8, 2025

To follow-up to my very long last message - I think following what CPython is doing for prefix resolution (for non-venvs) at startup would work well for all cases that I've seen. Namely, it uses a non-path segment recursive symlink resolve on the executable, which then is then used to search for the stdlib.

This would require no change in venv - it should continue to report dirname(executable) as the home in pyvenv.cfg, as it is the interpreter's responsibility to determine the correct location for the venv's prefix based on that executable location.

It turns out (after a lot of searching), that this is a two-line fix to getpath.py:

diff --git a/Modules/getpath.py b/Modules/getpath.py
index c34101e7208..e82ba23c4c2 100644
--- a/Modules/getpath.py
+++ b/Modules/getpath.py
@@ -412,6 +412,9 @@ def search_up(prefix, *landmarks, test=isfile):
                             if isfile(candidate):
                                 base_executable = candidate
                                 break
+                if base_executable:
+                    # Update the executable directory to be based on the resolved base executable
+                    executable_dir = basename(base_executable)
             break
     else:
         # We didn't find a 'home' key in pyvenv.cfg (no break), reset venv_prefix.

I'll dig into how to write a test for this, and submit.

One thing to note: I think the calculation of sys._base_executable is over-resolving symlinks today - this is a separate bug which I guess means that homebrew users can't take a venv from a venv and have it survive beyond the next patch update.

@FFY00
Copy link
Member

FFY00 commented Jan 8, 2025

Perhaps @FFY00 (given their recent changes to getpath) has some idea of whether we can use exactly the same logic for venv base_prefix calculation as we do for non-venv prefix calculation?

Fundamentally, my belief is that the definition of home in PEP-405 should really be updated if it were to properly support symlinked executables outside of their prefix - the reason is that we don't record the actual base executable, and have to guess it based on heuristics (and the original symlink could have been named arbitrarily) . The OP's proposal to automatically resolve the symlink would be inconsistent with what Python does for non-venvs, and in my opinion, would be the wrong thing to do (as mentioned by others in this issue - it would break certain established / reasonable usages).

Given that today it works well to run a symlinked executable which is outside of its prefix for non-venv cases, I would propose that the PEP is updated to replace home with a base_executable key (which is simply the value of sys.base_executable), upon which the existing getpath logic for determining its prefix and lib directories can be applied.

Yeah, it's known that the home key is problematic. We could replace it with a base_executable key, as you propose, but we'd still be relying on the IMO faulty "search up until we find something 🤷" heuristic. I'd rather just go away with all heuristics and replace the home key with base-prefix and base-exec-prefix keys, which directly gives us the prefixes. I opened GH-127895 with this proposal.

It turns out (after a lot of searching), that this is a two-line fix to getpath.py:

Please submit it! I think we can treat it as a bug, so we can still backport the fix to 3.12 and 3.13.

It seems there is a bug in venv (and a similar one in virtualenv) where the "base" path in pyvenv.cfg is set incorrectly. If Python is installed in a non-standard folder, e.g. /usr/local/python-X.Y.Z and then symlinked into /usr/local/bin/python3, the venv package does not work correctly.

With GH-127972 and GH-127974, on POSIX builds with a shared libpython, this should now be mitigated by looking at the libpython directory before resorting to the base interpreter, which should be much more reliable.

@mayeut
Copy link

mayeut commented Jan 11, 2025

on POSIX builds with a shared libpython, this should now be mitigated by looking at the libpython directory before resorting to the base interpreter, which should be much more reliable.

It does not seem to be helping in this case (on macOS):

mkdir -p ../temp-test/bin
mkdir -p ../temp-test/lib/python3.14/lib-dynload
touch ../temp-test/lib/python3.14/os.py
./configure --enable-shared --with-pydebug --prefix=$(pwd)/build/install  && make -j install
ln -sf $(pwd)/build/install/bin/python3 ../temp-test/bin/python
../temp-test/bin/python -m venv ../temp-test/venv   
Error: Command '['../temp-test/venv/bin/python', '-m', 'ensurepip', '--upgrade', '--default-pip']' returned non-zero exit status 1.

With #115237 resolving home differently, the venv works with both shared & static builds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-venv Related to the venv module type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

7 participants