1from __future__ import annotations
2
3import logging
4import os
5import sys
6import sysconfig
7from typing 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 osx_framework_library. See _should_use_osx_framework_prefix()
163 # docstring for details.
164 if prefix is not None and scheme_name == "osx_framework_library":
165 scheme_name = "posix_prefix"
166
167 if home is not None:
168 variables = {k: home for k in _HOME_KEYS}
169 elif prefix is not None:
170 variables = {k: prefix for k in _HOME_KEYS}
171 else:
172 variables = {}
173
174 paths = sysconfig.get_paths(scheme=scheme_name, vars=variables)
175
176 # Logic here is very arbitrary, we're doing it for compatibility, don't ask.
177 # 1. Pip historically uses a special header path in virtual environments.
178 # 2. If the distribution name is not known, distutils uses 'UNKNOWN'. We
179 # only do the same when not running in a virtual environment because
180 # pip's historical header path logic (see point 1) did not do this.
181 if running_under_virtualenv():
182 if user:
183 base = variables.get("userbase", sys.prefix)
184 else:
185 base = variables.get("base", sys.prefix)
186 python_xy = f"python{get_major_minor_version()}"
187 paths["include"] = os.path.join(base, "include", "site", python_xy)
188 elif not dist_name:
189 dist_name = "UNKNOWN"
190
191 scheme = Scheme(
192 platlib=paths["platlib"],
193 purelib=paths["purelib"],
194 headers=os.path.join(paths["include"], dist_name),
195 scripts=paths["scripts"],
196 data=paths["data"],
197 )
198 if root is not None:
199 converted_keys = {}
200 for key in SCHEME_KEYS:
201 converted_keys[key] = change_root(root, getattr(scheme, key))
202 scheme = Scheme(**converted_keys)
203 return scheme
204
205
206def get_bin_prefix() -> str:
207 # Forcing to use /usr/local/bin for standard macOS framework installs.
208 if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/":
209 return "/usr/local/bin"
210 return sysconfig.get_paths()["scripts"]
211
212
213def get_purelib() -> str:
214 return sysconfig.get_paths()["purelib"]
215
216
217def get_platlib() -> str:
218 return sysconfig.get_paths()["platlib"]