| ... | ... | @@ -17,6 +17,7 @@ import subprocess | 
| 17 | 17 |  import sys
 | 
| 18 | 18 |  import sysconfig
 | 
| 19 | 19 |  import tempfile
 | 
|  | 20 | +import warnings
 | 
| 20 | 21 |  from contextlib import contextmanager
 | 
| 21 | 22 |  from pathlib import Path
 | 
| 22 | 23 |  from typing import Callable, Optional
 | 
| ... | ... | @@ -817,33 +818,75 @@ class PythonVirtualenv: | 
| 817 | 818 |      """Calculates paths of interest for general python virtual environments"""
 | 
| 818 | 819 |  
 | 
| 819 | 820 |      def __init__(self, prefix):
 | 
| 820 |  | -        if _is_windows:
 | 
| 821 |  | -            self.bin_path = os.path.join(prefix, "Scripts")
 | 
| 822 |  | -            self.python_path = os.path.join(self.bin_path, "python.exe")
 | 
| 823 |  | -        else:
 | 
| 824 |  | -            self.bin_path = os.path.join(prefix, "bin")
 | 
| 825 |  | -            self.python_path = os.path.join(self.bin_path, "python")
 | 
| 826 | 821 |          self.prefix = os.path.realpath(prefix)
 | 
|  | 822 | +        self.paths = self._get_sysconfig_paths(self.prefix)
 | 
| 827 | 823 |  
 | 
| 828 |  | -    @functools.lru_cache(maxsize=None)
 | 
| 829 |  | -    def resolve_sysconfig_packages_path(self, sysconfig_path):
 | 
| 830 |  | -        # macOS uses a different default sysconfig scheme based on whether it's using the
 | 
| 831 |  | -        # system Python or running in a virtualenv.
 | 
| 832 |  | -        # Manually define the scheme (following the implementation in
 | 
| 833 |  | -        # "sysconfig._get_default_scheme()") so that we're always following the
 | 
| 834 |  | -        # code path for a virtualenv directory structure.
 | 
| 835 |  | -        if os.name == "posix":
 | 
| 836 |  | -            scheme = "posix_prefix"
 | 
| 837 |  | -        else:
 | 
| 838 |  | -            scheme = os.name
 | 
|  | 824 | +        # Name of the Python executable to use in virtual environments.
 | 
|  | 825 | +        # An executable with the same name as sys.executable might not exist in
 | 
|  | 826 | +        # virtual environments. An executable with 'python' as the steam —
 | 
|  | 827 | +        # without version numbers or ABI flags — will always be present in
 | 
|  | 828 | +        # virtual environments, so we use that.
 | 
|  | 829 | +        python_exe_name = "python" + sysconfig.get_config_var("EXE")
 | 
|  | 830 | +
 | 
|  | 831 | +        self.bin_path = self.paths["scripts"]
 | 
|  | 832 | +        self.python_path = os.path.join(self.bin_path, python_exe_name)
 | 
| 839 | 833 |  
 | 
| 840 |  | -        sysconfig_paths = sysconfig.get_paths(scheme)
 | 
| 841 |  | -        data_path = Path(sysconfig_paths["data"])
 | 
| 842 |  | -        path = Path(sysconfig_paths[sysconfig_path])
 | 
| 843 |  | -        relative_path = path.relative_to(data_path)
 | 
|  | 834 | +    @staticmethod
 | 
|  | 835 | +    def _get_sysconfig_paths(prefix):
 | 
|  | 836 | +        """Calculate the sysconfig paths of a virtual environment in the given prefix.
 | 
| 844 | 837 |  
 | 
| 845 |  | -        # Path to virtualenv's "site-packages" directory for provided sysconfig path
 | 
| 846 |  | -        return os.path.normpath(os.path.normcase(Path(self.prefix) / relative_path))
 | 
|  | 838 | +        The virtual environment MUST be using the same Python distribution as us.
 | 
|  | 839 | +        """
 | 
|  | 840 | +        # Determine the sysconfig scheme used in virtual environments
 | 
|  | 841 | +        if "venv" in sysconfig.get_scheme_names():
 | 
|  | 842 | +            # A 'venv' scheme was added in Python 3.11 to allow users to
 | 
|  | 843 | +            # calculate the paths for a virtual environment, since the default
 | 
|  | 844 | +            # scheme may not always be the same as used on virtual environments.
 | 
|  | 845 | +            # Some common examples are the system Python distributed by macOS,
 | 
|  | 846 | +            # Debian, and Fedora.
 | 
|  | 847 | +            # For more information, see https://github.com/python/cpython/issues/89576
 | 
|  | 848 | +            venv_scheme = "venv"
 | 
|  | 849 | +        elif os.name == "nt":
 | 
|  | 850 | +            # We know that before the 'venv' scheme was added, on Windows,
 | 
|  | 851 | +            # the 'nt' scheme was used in virtual environments.
 | 
|  | 852 | +            venv_scheme = "nt"
 | 
|  | 853 | +        elif os.name == "posix":
 | 
|  | 854 | +            # We know that before the 'venv' scheme was added, on POSIX,
 | 
|  | 855 | +            # the 'posix_prefix' scheme was used in virtual environments.
 | 
|  | 856 | +            venv_scheme = "posix_prefix"
 | 
|  | 857 | +        else:
 | 
|  | 858 | +            # This should never happen with upstream Python, as the 'venv'
 | 
|  | 859 | +            # scheme should always be available on >=3.11, and no other
 | 
|  | 860 | +            # platforms are supported by the upstream on older Python versions.
 | 
|  | 861 | +            #
 | 
|  | 862 | +            # Since the 'venv' scheme isn't available, and we have no knowledge
 | 
|  | 863 | +            # of this platform/distribution, fallback to the default scheme.
 | 
|  | 864 | +            #
 | 
|  | 865 | +            # Hitting this will likely be the result of running a custom Python
 | 
|  | 866 | +            # distribution targetting a platform that is not supported by the
 | 
|  | 867 | +            # upstream.
 | 
|  | 868 | +            # In this case, unless the Python vendor patched the Python
 | 
|  | 869 | +            # distribution in such a way as the default scheme may not always be
 | 
|  | 870 | +            # the same scheme, using the default scheme should be correct.
 | 
|  | 871 | +            # If the vendor did patch Python as such, to work around this issue,
 | 
|  | 872 | +            # I would recommend them to define a 'venv' scheme that matches
 | 
|  | 873 | +            # the layout used on virtual environments in their Python distribution.
 | 
|  | 874 | +            # (rec. signed Filipe Laíns — upstream sysconfig maintainer)
 | 
|  | 875 | +            venv_scheme = sysconfig.get_default_scheme()
 | 
|  | 876 | +            warnings.warn(
 | 
|  | 877 | +                f"Unknown platform '{os.name}', using the default install scheme '{venv_scheme}'. "
 | 
|  | 878 | +                "If this is incorrect, please ask your Python vendor to add a 'venv' sysconfig scheme "
 | 
|  | 879 | +                "(see https://github.com/python/cpython/issues/89576, or check the code comment).",
 | 
|  | 880 | +                stacklevel=2,
 | 
|  | 881 | +            )
 | 
|  | 882 | +        # Build the sysconfig config_vars dictionary for the virtual environment.
 | 
|  | 883 | +        venv_vars = sysconfig.get_config_vars().copy()
 | 
|  | 884 | +        venv_vars["base"] = venv_vars["platbase"] = prefix
 | 
|  | 885 | +        # Get sysconfig paths for the virtual environment.
 | 
|  | 886 | +        return sysconfig.get_paths(venv_scheme, vars=venv_vars)
 | 
|  | 887 | +
 | 
|  | 888 | +    def resolve_sysconfig_packages_path(self, sysconfig_path):
 | 
|  | 889 | +        return self.paths[sysconfig_path]
 | 
| 847 | 890 |  
 | 
| 848 | 891 |      def site_packages_dirs(self):
 | 
| 849 | 892 |          dirs = []
 |