__init__.py 15 KB

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