__init__.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. from __future__ import annotations
  2. import functools
  3. import logging
  4. import os
  5. import pathlib
  6. import sys
  7. import sysconfig
  8. from typing import Any
  9. from pip._internal.models.scheme import SCHEME_KEYS, Scheme
  10. from pip._internal.utils.compat import WINDOWS
  11. from pip._internal.utils.deprecation import deprecated
  12. from pip._internal.utils.virtualenv import running_under_virtualenv
  13. from . import _sysconfig
  14. from .base import (
  15. USER_CACHE_DIR,
  16. get_major_minor_version,
  17. get_src_prefix,
  18. is_osx_framework,
  19. site_packages,
  20. user_site,
  21. )
  22. __all__ = [
  23. "USER_CACHE_DIR",
  24. "get_bin_prefix",
  25. "get_bin_user",
  26. "get_major_minor_version",
  27. "get_platlib",
  28. "get_purelib",
  29. "get_scheme",
  30. "get_src_prefix",
  31. "site_packages",
  32. "user_site",
  33. ]
  34. logger = logging.getLogger(__name__)
  35. _PLATLIBDIR: str = getattr(sys, "platlibdir", "lib")
  36. _USE_SYSCONFIG_DEFAULT = sys.version_info >= (3, 10)
  37. def _should_use_sysconfig() -> bool:
  38. """This function determines the value of _USE_SYSCONFIG.
  39. By default, pip uses sysconfig on Python 3.10+.
  40. But Python distributors can override this decision by setting:
  41. sysconfig._PIP_USE_SYSCONFIG = True / False
  42. Rationale in https://github.com/pypa/pip/issues/10647
  43. This is a function for testability, but should be constant during any one
  44. run.
  45. """
  46. return bool(getattr(sysconfig, "_PIP_USE_SYSCONFIG", _USE_SYSCONFIG_DEFAULT))
  47. _USE_SYSCONFIG = _should_use_sysconfig()
  48. if not _USE_SYSCONFIG:
  49. # Import distutils lazily to avoid deprecation warnings,
  50. # but import it soon enough that it is in memory and available during
  51. # a pip reinstall.
  52. from . import _distutils
  53. # Be noisy about incompatibilities if this platforms "should" be using
  54. # sysconfig, but is explicitly opting out and using distutils instead.
  55. if _USE_SYSCONFIG_DEFAULT and not _USE_SYSCONFIG:
  56. _MISMATCH_LEVEL = logging.WARNING
  57. else:
  58. _MISMATCH_LEVEL = logging.DEBUG
  59. def _looks_like_bpo_44860() -> bool:
  60. """The resolution to bpo-44860 will change this incorrect platlib.
  61. See <https://bugs.python.org/issue44860>.
  62. """
  63. from distutils.command.install import INSTALL_SCHEMES
  64. try:
  65. unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"]
  66. except KeyError:
  67. return False
  68. return unix_user_platlib == "$usersite"
  69. def _looks_like_red_hat_patched_platlib_purelib(scheme: dict[str, str]) -> bool:
  70. platlib = scheme["platlib"]
  71. if "/$platlibdir/" in platlib:
  72. platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
  73. if "/lib64/" not in platlib:
  74. return False
  75. unpatched = platlib.replace("/lib64/", "/lib/")
  76. return unpatched.replace("$platbase/", "$base/") == scheme["purelib"]
  77. @functools.cache
  78. def _looks_like_red_hat_lib() -> bool:
  79. """Red Hat patches platlib in unix_prefix and unix_home, but not purelib.
  80. This is the only way I can see to tell a Red Hat-patched Python.
  81. """
  82. from distutils.command.install import INSTALL_SCHEMES
  83. return all(
  84. k in INSTALL_SCHEMES
  85. and _looks_like_red_hat_patched_platlib_purelib(INSTALL_SCHEMES[k])
  86. for k in ("unix_prefix", "unix_home")
  87. )
  88. @functools.cache
  89. def _looks_like_debian_scheme() -> bool:
  90. """Debian adds two additional schemes."""
  91. from distutils.command.install import INSTALL_SCHEMES
  92. return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES
  93. @functools.cache
  94. def _looks_like_red_hat_scheme() -> bool:
  95. """Red Hat patches ``sys.prefix`` and ``sys.exec_prefix``.
  96. Red Hat's ``00251-change-user-install-location.patch`` changes the install
  97. command's ``prefix`` and ``exec_prefix`` to append ``"/local"``. This is
  98. (fortunately?) done quite unconditionally, so we create a default command
  99. object without any configuration to detect this.
  100. """
  101. from distutils.command.install import install
  102. from distutils.dist import Distribution
  103. cmd: Any = install(Distribution())
  104. cmd.finalize_options()
  105. return (
  106. cmd.exec_prefix == f"{os.path.normpath(sys.exec_prefix)}/local"
  107. and cmd.prefix == f"{os.path.normpath(sys.prefix)}/local"
  108. )
  109. @functools.cache
  110. def _looks_like_slackware_scheme() -> bool:
  111. """Slackware patches sysconfig but fails to patch distutils and site.
  112. Slackware changes sysconfig's user scheme to use ``"lib64"`` for the lib
  113. path, but does not do the same to the site module.
  114. """
  115. if user_site is None: # User-site not available.
  116. return False
  117. try:
  118. paths = sysconfig.get_paths(scheme="posix_user", expand=False)
  119. except KeyError: # User-site not available.
  120. return False
  121. return "/lib64/" in paths["purelib"] and "/lib64/" not in user_site
  122. @functools.cache
  123. def _looks_like_msys2_mingw_scheme() -> bool:
  124. """MSYS2 patches distutils and sysconfig to use a UNIX-like scheme.
  125. However, MSYS2 incorrectly patches sysconfig ``nt`` scheme. The fix is
  126. likely going to be included in their 3.10 release, so we ignore the warning.
  127. See msys2/MINGW-packages#9319.
  128. MSYS2 MINGW's patch uses lowercase ``"lib"`` instead of the usual uppercase,
  129. and is missing the final ``"site-packages"``.
  130. """
  131. paths = sysconfig.get_paths("nt", expand=False)
  132. return all(
  133. "Lib" not in p and "lib" in p and not p.endswith("site-packages")
  134. for p in (paths[key] for key in ("platlib", "purelib"))
  135. )
  136. @functools.cache
  137. def _warn_mismatched(old: pathlib.Path, new: pathlib.Path, *, key: str) -> None:
  138. issue_url = "https://github.com/pypa/pip/issues/10151"
  139. message = (
  140. "Value for %s does not match. Please report this to <%s>"
  141. "\ndistutils: %s"
  142. "\nsysconfig: %s"
  143. )
  144. logger.log(_MISMATCH_LEVEL, message, key, issue_url, old, new)
  145. def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool:
  146. if old == new:
  147. return False
  148. _warn_mismatched(old, new, key=key)
  149. return True
  150. @functools.cache
  151. def _log_context(
  152. *,
  153. user: bool = False,
  154. home: str | None = None,
  155. root: str | None = None,
  156. prefix: str | None = None,
  157. ) -> None:
  158. parts = [
  159. "Additional context:",
  160. "user = %r",
  161. "home = %r",
  162. "root = %r",
  163. "prefix = %r",
  164. ]
  165. logger.log(_MISMATCH_LEVEL, "\n".join(parts), user, home, root, prefix)
  166. def get_scheme(
  167. dist_name: str,
  168. user: bool = False,
  169. home: str | None = None,
  170. root: str | None = None,
  171. isolated: bool = False,
  172. prefix: str | None = None,
  173. ) -> Scheme:
  174. new = _sysconfig.get_scheme(
  175. dist_name,
  176. user=user,
  177. home=home,
  178. root=root,
  179. isolated=isolated,
  180. prefix=prefix,
  181. )
  182. if _USE_SYSCONFIG:
  183. return new
  184. old = _distutils.get_scheme(
  185. dist_name,
  186. user=user,
  187. home=home,
  188. root=root,
  189. isolated=isolated,
  190. prefix=prefix,
  191. )
  192. warning_contexts = []
  193. for k in SCHEME_KEYS:
  194. old_v = pathlib.Path(getattr(old, k))
  195. new_v = pathlib.Path(getattr(new, k))
  196. if old_v == new_v:
  197. continue
  198. # distutils incorrectly put PyPy packages under ``site-packages/python``
  199. # in the ``posix_home`` scheme, but PyPy devs said they expect the
  200. # directory name to be ``pypy`` instead. So we treat this as a bug fix
  201. # and not warn about it. See bpo-43307 and python/cpython#24628.
  202. skip_pypy_special_case = (
  203. sys.implementation.name == "pypy"
  204. and home is not None
  205. and k in ("platlib", "purelib")
  206. and old_v.parent == new_v.parent
  207. and old_v.name.startswith("python")
  208. and new_v.name.startswith("pypy")
  209. )
  210. if skip_pypy_special_case:
  211. continue
  212. # sysconfig's ``osx_framework_user`` does not include ``pythonX.Y`` in
  213. # the ``include`` value, but distutils's ``headers`` does. We'll let
  214. # CPython decide whether this is a bug or feature. See bpo-43948.
  215. skip_osx_framework_user_special_case = (
  216. user
  217. and is_osx_framework()
  218. and k == "headers"
  219. and old_v.parent.parent == new_v.parent
  220. and old_v.parent.name.startswith("python")
  221. )
  222. if skip_osx_framework_user_special_case:
  223. continue
  224. # On Red Hat and derived Linux distributions, distutils is patched to
  225. # use "lib64" instead of "lib" for platlib.
  226. if k == "platlib" and _looks_like_red_hat_lib():
  227. continue
  228. # On Python 3.9+, sysconfig's posix_user scheme sets platlib against
  229. # sys.platlibdir, but distutils's unix_user incorrectly continues
  230. # using the same $usersite for both platlib and purelib. This creates a
  231. # mismatch when sys.platlibdir is not "lib".
  232. skip_bpo_44860 = (
  233. user
  234. and k == "platlib"
  235. and not WINDOWS
  236. and _PLATLIBDIR != "lib"
  237. and _looks_like_bpo_44860()
  238. )
  239. if skip_bpo_44860:
  240. continue
  241. # Slackware incorrectly patches posix_user to use lib64 instead of lib,
  242. # but not usersite to match the location.
  243. skip_slackware_user_scheme = (
  244. user
  245. and k in ("platlib", "purelib")
  246. and not WINDOWS
  247. and _looks_like_slackware_scheme()
  248. )
  249. if skip_slackware_user_scheme:
  250. continue
  251. # Both Debian and Red Hat patch Python to place the system site under
  252. # /usr/local instead of /usr. Debian also places lib in dist-packages
  253. # instead of site-packages, but the /usr/local check should cover it.
  254. skip_linux_system_special_case = (
  255. not (user or home or prefix or running_under_virtualenv())
  256. and old_v.parts[1:3] == ("usr", "local")
  257. and len(new_v.parts) > 1
  258. and new_v.parts[1] == "usr"
  259. and (len(new_v.parts) < 3 or new_v.parts[2] != "local")
  260. and (_looks_like_red_hat_scheme() or _looks_like_debian_scheme())
  261. )
  262. if skip_linux_system_special_case:
  263. continue
  264. # MSYS2 MINGW's sysconfig patch does not include the "site-packages"
  265. # part of the path. This is incorrect and will be fixed in MSYS.
  266. skip_msys2_mingw_bug = (
  267. WINDOWS and k in ("platlib", "purelib") and _looks_like_msys2_mingw_scheme()
  268. )
  269. if skip_msys2_mingw_bug:
  270. continue
  271. # CPython's POSIX install script invokes pip (via ensurepip) against the
  272. # interpreter located in the source tree, not the install site. This
  273. # triggers special logic in sysconfig that's not present in distutils.
  274. # https://github.com/python/cpython/blob/8c21941ddaf/Lib/sysconfig.py#L178-L194
  275. skip_cpython_build = (
  276. sysconfig.is_python_build(check_home=True)
  277. and not WINDOWS
  278. and k in ("headers", "include", "platinclude")
  279. )
  280. if skip_cpython_build:
  281. continue
  282. warning_contexts.append((old_v, new_v, f"scheme.{k}"))
  283. if not warning_contexts:
  284. return old
  285. # Check if this path mismatch is caused by distutils config files. Those
  286. # files will no longer work once we switch to sysconfig, so this raises a
  287. # deprecation message for them.
  288. default_old = _distutils.distutils_scheme(
  289. dist_name,
  290. user,
  291. home,
  292. root,
  293. isolated,
  294. prefix,
  295. ignore_config_files=True,
  296. )
  297. if any(default_old[k] != getattr(old, k) for k in SCHEME_KEYS):
  298. deprecated(
  299. reason=(
  300. "Configuring installation scheme with distutils config files "
  301. "is deprecated and will no longer work in the near future. If you "
  302. "are using a Homebrew or Linuxbrew Python, please see discussion "
  303. "at https://github.com/Homebrew/homebrew-core/issues/76621"
  304. ),
  305. replacement=None,
  306. gone_in=None,
  307. )
  308. return old
  309. # Post warnings about this mismatch so user can report them back.
  310. for old_v, new_v, key in warning_contexts:
  311. _warn_mismatched(old_v, new_v, key=key)
  312. _log_context(user=user, home=home, root=root, prefix=prefix)
  313. return old
  314. def get_bin_prefix() -> str:
  315. new = _sysconfig.get_bin_prefix()
  316. if _USE_SYSCONFIG:
  317. return new
  318. old = _distutils.get_bin_prefix()
  319. if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"):
  320. _log_context()
  321. return old
  322. def get_bin_user() -> str:
  323. return _sysconfig.get_scheme("", user=True).scripts
  324. def _looks_like_deb_system_dist_packages(value: str) -> bool:
  325. """Check if the value is Debian's APT-controlled dist-packages.
  326. Debian's ``distutils.sysconfig.get_python_lib()`` implementation returns the
  327. default package path controlled by APT, but does not patch ``sysconfig`` to
  328. do the same. This is similar to the bug worked around in ``get_scheme()``,
  329. but here the default is ``deb_system`` instead of ``unix_local``. Ultimately
  330. we can't do anything about this Debian bug, and this detection allows us to
  331. skip the warning when needed.
  332. """
  333. if not _looks_like_debian_scheme():
  334. return False
  335. if value == "/usr/lib/python3/dist-packages":
  336. return True
  337. return False
  338. def get_purelib() -> str:
  339. """Return the default pure-Python lib location."""
  340. new = _sysconfig.get_purelib()
  341. if _USE_SYSCONFIG:
  342. return new
  343. old = _distutils.get_purelib()
  344. if _looks_like_deb_system_dist_packages(old):
  345. return old
  346. if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"):
  347. _log_context()
  348. return old
  349. def get_platlib() -> str:
  350. """Return the default platform-shared lib location."""
  351. new = _sysconfig.get_platlib()
  352. if _USE_SYSCONFIG:
  353. return new
  354. from . import _distutils
  355. old = _distutils.get_platlib()
  356. if _looks_like_deb_system_dist_packages(old):
  357. return old
  358. if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"):
  359. _log_context()
  360. return old