1from __future__ import annotations
2
3import logging
4import os
5import sys
6import sysconfig
7from collections.abc import Callable
8
9from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid
10from pip._internal.models.scheme import SCHEME_KEYS, Scheme
11from pip._internal.utils.virtualenv import running_under_virtualenv
12
13from .base import change_root, get_major_minor_version, is_osx_framework
14
15logger = logging.getLogger(__name__)
16
17
18# Notes on _infer_* functions.
19# Unfortunately ``get_default_scheme()`` didn't exist before 3.10, so there's no
20# way to ask things like "what is the '_prefix' scheme on this platform". These
21# functions try to answer that with some heuristics while accounting for ad-hoc
22# platforms not covered by CPython's default sysconfig implementation. If the
23# ad-hoc implementation does not fully implement sysconfig, we'll fall back to
24# a POSIX scheme.
25
26_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names())
27
28_PREFERRED_SCHEME_API: Callable[[str], str] | None = getattr(
29 sysconfig, "get_preferred_scheme", None
30)
31
32
33def _should_use_osx_framework_prefix() -> bool:
34 """Check for Apple's ``osx_framework_library`` scheme.
35
36 Python distributed by Apple's Command Line Tools has this special scheme
37 that's used when:
38
39 * This is a framework build.
40 * We are installing into the system prefix.
41
42 This does not account for ``pip install --prefix`` (also means we're not
43 installing to the system prefix), which should use ``posix_prefix``, but
44 logic here means ``_infer_prefix()`` outputs ``osx_framework_library``. But
45 since ``prefix`` is not available for ``sysconfig.get_default_scheme()``,
46 which is the stdlib replacement for ``_infer_prefix()``, presumably Apple
47 wouldn't be able to magically switch between ``osx_framework_library`` and
48 ``posix_prefix``. ``_infer_prefix()`` returning ``osx_framework_library``
49 means its behavior is consistent whether we use the stdlib implementation
50 or our own, and we deal with this special case in ``get_scheme()`` instead.
51 """
52 return (
53 "osx_framework_library" in _AVAILABLE_SCHEMES
54 and not running_under_virtualenv()
55 and is_osx_framework()
56 )
57
58
59def _infer_prefix() -> str:
60 """Try to find a prefix scheme for the current platform.
61
62 This tries:
63
64 * A special ``osx_framework_library`` for Python distributed by Apple's
65 Command Line Tools, when not running in a virtual environment.
66 * Implementation + OS, used by PyPy on Windows (``pypy_nt``).
67 * Implementation without OS, used by PyPy on POSIX (``pypy``).
68 * OS + "prefix", used by CPython on POSIX (``posix_prefix``).
69 * Just the OS name, used by CPython on Windows (``nt``).
70
71 If none of the above works, fall back to ``posix_prefix``.
72 """
73 if _PREFERRED_SCHEME_API:
74 return _PREFERRED_SCHEME_API("prefix")
75 if _should_use_osx_framework_prefix():
76 return "osx_framework_library"
77 implementation_suffixed = f"{sys.implementation.name}_{os.name}"
78 if implementation_suffixed in _AVAILABLE_SCHEMES:
79 return implementation_suffixed
80 if sys.implementation.name in _AVAILABLE_SCHEMES:
81 return sys.implementation.name
82 suffixed = f"{os.name}_prefix"
83 if suffixed in _AVAILABLE_SCHEMES:
84 return suffixed
85 if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt".
86 return os.name
87 return "posix_prefix"
88
89
90def _infer_user() -> str:
91 """Try to find a user scheme for the current platform."""
92 if _PREFERRED_SCHEME_API:
93 return _PREFERRED_SCHEME_API("user")
94 if is_osx_framework() and not running_under_virtualenv():
95 suffixed = "osx_framework_user"
96 else:
97 suffixed = f"{os.name}_user"
98 if suffixed in _AVAILABLE_SCHEMES:
99 return suffixed
100 if "posix_user" not in _AVAILABLE_SCHEMES: # User scheme unavailable.
101 raise UserInstallationInvalid()
102 return "posix_user"
103
104
105def _infer_home() -> str:
106 """Try to find a home for the current platform."""
107 if _PREFERRED_SCHEME_API:
108 return _PREFERRED_SCHEME_API("home")
109 suffixed = f"{os.name}_home"
110 if suffixed in _AVAILABLE_SCHEMES:
111 return suffixed
112 return "posix_home"
113
114
115# Update these keys if the user sets a custom home.
116_HOME_KEYS = [
117 "installed_base",
118 "base",
119 "installed_platbase",
120 "platbase",
121 "prefix",
122 "exec_prefix",
123]
124if sysconfig.get_config_var("userbase") is not None:
125 _HOME_KEYS.append("userbase")
126
127
128def get_scheme(
129 dist_name: str,
130 user: bool = False,
131 home: str | None = None,
132 root: str | None = None,
133 isolated: bool = False,
134 prefix: str | None = None,
135) -> Scheme:
136 """
137 Get the "scheme" corresponding to the input parameters.
138
139 :param dist_name: the name of the package to retrieve the scheme for, used
140 in the headers scheme path
141 :param user: indicates to use the "user" scheme
142 :param home: indicates to use the "home" scheme
143 :param root: root under which other directories are re-based
144 :param isolated: ignored, but kept for distutils compatibility (where
145 this controls whether the user-site pydistutils.cfg is honored)
146 :param prefix: indicates to use the "prefix" scheme and provides the
147 base directory for the same
148 """
149 if user and prefix:
150 raise InvalidSchemeCombination("--user", "--prefix")
151 if home and prefix:
152 raise InvalidSchemeCombination("--home", "--prefix")
153
154 if home is not None:
155 scheme_name = _infer_home()
156 elif user:
157 scheme_name = _infer_user()
158 else:
159 scheme_name = _infer_prefix()
160
161 # Special case: When installing into a custom prefix, use posix_prefix
162 # instead of posix_local because it should *only* be used to redirecting
163 # to /usr/local for system packager installs. Notably, posix_local is used
164 # on Debian and its derivatives.
165 # See also: https://discuss.python.org/t/18240
166 if prefix is not None and scheme_name == "posix_local":
167 scheme_name = "posix_prefix"
168
169 # Special case: When installing into a custom prefix, use posix_prefix
170 # instead of osx_framework_library. See _should_use_osx_framework_prefix()
171 # docstring for details.
172 if prefix is not None and scheme_name == "osx_framework_library":
173 scheme_name = "posix_prefix"
174
175 if home is not None:
176 variables = {k: home for k in _HOME_KEYS}
177 elif prefix is not None:
178 variables = {k: prefix for k in _HOME_KEYS}
179 else:
180 variables = {}
181
182 paths = sysconfig.get_paths(scheme=scheme_name, vars=variables)
183
184 # Logic here is very arbitrary, we're doing it for compatibility, don't ask.
185 # 1. Pip historically uses a special header path in virtual environments.
186 # 2. If the distribution name is not known, distutils uses 'UNKNOWN'. We
187 # only do the same when not running in a virtual environment because
188 # pip's historical header path logic (see point 1) did not do this.
189 if running_under_virtualenv():
190 if user:
191 base = variables.get("userbase", sys.prefix)
192 else:
193 base = variables.get("base", sys.prefix)
194 python_xy = f"python{get_major_minor_version()}"
195 paths["include"] = os.path.join(base, "include", "site", python_xy)
196 elif not dist_name:
197 dist_name = "UNKNOWN"
198
199 scheme = Scheme(
200 platlib=paths["platlib"],
201 purelib=paths["purelib"],
202 headers=os.path.join(paths["include"], dist_name),
203 scripts=paths["scripts"],
204 data=paths["data"],
205 )
206 if root is not None:
207 converted_keys = {}
208 for key in SCHEME_KEYS:
209 converted_keys[key] = change_root(root, getattr(scheme, key))
210 scheme = Scheme(**converted_keys)
211 return scheme
212
213
214def get_bin_prefix() -> str:
215 # Forcing to use /usr/local/bin for standard macOS framework installs.
216 if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/":
217 return "/usr/local/bin"
218 return sysconfig.get_paths()["scripts"]
219
220
221def get_purelib() -> str:
222 return sysconfig.get_paths()["purelib"]
223
224
225def get_platlib() -> str:
226 return sysconfig.get_paths()["platlib"]