Source code for xonsh.platform

"""Module for platform-specific constants and implementations, as well as
compatibility layers to make use of the 'best' implementation available
on a platform.
"""

import collections.abc as cabc
import ctypes  # noqa
import functools
import importlib.util
import os
import pathlib
import platform
import signal
import subprocess
import sys

from xonsh.lazyasd import LazyBool, lazybool, lazyobject

# do not import any xonsh-modules here to avoid circular dependencies

FD_STDIN = 0
FD_STDOUT = 1
FD_STDERR = 2


@lazyobject
def distro():
    try:
        import distro as d
    except ImportError:
        d = None
    except Exception:
        raise
    return d


#
# OS
#
ON_DARWIN = LazyBool(lambda: platform.system() == "Darwin", globals(), "ON_DARWIN")
"""``True`` if executed on a Darwin platform, else ``False``. """
ON_LINUX = LazyBool(lambda: platform.system() == "Linux", globals(), "ON_LINUX")
"""``True`` if executed on a Linux platform, else ``False``. """
ON_WINDOWS = LazyBool(lambda: platform.system() == "Windows", globals(), "ON_WINDOWS")
"""``True`` if executed on a native Windows platform, else ``False``. """
ON_CYGWIN = LazyBool(lambda: sys.platform == "cygwin", globals(), "ON_CYGWIN")
"""``True`` if executed on a Cygwin Windows platform, else ``False``. """
ON_MSYS = LazyBool(lambda: sys.platform == "msys", globals(), "ON_MSYS")
"""``True`` if executed on a MSYS Windows platform, else ``False``. """
ON_POSIX = LazyBool(lambda: (os.name == "posix"), globals(), "ON_POSIX")
"""``True`` if executed on a POSIX-compliant platform, else ``False``. """
ON_FREEBSD = LazyBool(
    lambda: (sys.platform.startswith("freebsd")), globals(), "ON_FREEBSD"
)
"""``True`` if on a FreeBSD operating system, else ``False``."""
ON_DRAGONFLY = LazyBool(
    lambda: (sys.platform.startswith("dragonfly")), globals(), "ON_DRAGONFLY"
)
"""``True`` if on a DragonFly BSD operating system, else ``False``."""
ON_NETBSD = LazyBool(
    lambda: (sys.platform.startswith("netbsd")), globals(), "ON_NETBSD"
)
"""``True`` if on a NetBSD operating system, else ``False``."""
ON_OPENBSD = LazyBool(
    lambda: (sys.platform.startswith("openbsd")), globals(), "ON_OPENBSD"
)
"""``True`` if on a OpenBSD operating system, else ``False``."""
IN_APPIMAGE = LazyBool(
    lambda: ("APPIMAGE" in os.environ and "APPDIR" in os.environ),
    globals(),
    "IN_APPIMAGE",
)
"""``True`` if in AppImage, else ``False``."""


@lazybool
def ON_BSD():
    """``True`` if on a BSD operating system, else ``False``."""
    return bool(ON_FREEBSD) or bool(ON_NETBSD) or bool(ON_OPENBSD) or bool(ON_DRAGONFLY)


@lazybool
def ON_BEOS():
    """True if we are on BeOS or Haiku."""
    return sys.platform == "beos5" or sys.platform == "haiku1"


@lazybool
def ON_WSL():
    """True if we are on Windows Subsystem for Linux (WSL)"""
    return "microsoft" in platform.release()


@lazybool
def ON_WSL1():
    return bool(ON_WSL) and not bool(ON_WSL2)


@lazybool
def ON_WSL2():
    return bool(ON_WSL) and "WSL2" in platform.release()


#
# Python & packages
#

PYTHON_VERSION_INFO = sys.version_info[:3]
""" Version of Python interpreter as three-value tuple. """


@lazyobject
def PYTHON_VERSION_INFO_BYTES():
    """The python version info tuple in a canonical bytes form."""
    return ".".join(map(str, sys.version_info)).encode()


ON_ANACONDA = LazyBool(
    lambda: pathlib.Path(sys.prefix).joinpath("conda-meta").exists(),
    globals(),
    "ON_ANACONDA",
)
""" ``True`` if executed in an Anaconda instance, else ``False``. """
CAN_RESIZE_WINDOW = LazyBool(
    lambda: hasattr(signal, "SIGWINCH"), globals(), "CAN_RESIZE_WINDOW"
)
"""``True`` if we can resize terminal window, as provided by the presense of
signal.SIGWINCH, else ``False``.
"""


@lazybool
def HAS_PYGMENTS():
    """``True`` if `pygments` is available, else ``False``."""
    spec = importlib.util.find_spec("pygments")
    return spec is not None


[docs] @functools.lru_cache(1) def pygments_version(): """pygments.__version__ version if available, else None.""" if HAS_PYGMENTS: import pygments v = pygments.__version__ else: v = None return v
[docs] @functools.lru_cache(1) def pygments_version_info(): """Returns `pygments`'s version as tuple of integers.""" if HAS_PYGMENTS: return tuple(int(x) for x in pygments_version().strip("<>+-=.").split(".")) else: return None
[docs] @functools.lru_cache(1) def has_prompt_toolkit(): """Tests if the `prompt_toolkit` is available.""" spec = importlib.util.find_spec("prompt_toolkit") return spec is not None
[docs] @functools.lru_cache(1) def ptk_version(): """Returns `prompt_toolkit.__version__` if available, else ``None``.""" if has_prompt_toolkit(): import prompt_toolkit return getattr(prompt_toolkit, "__version__", "<0.57") else: return None
[docs] @functools.lru_cache(1) def ptk_version_info(): """Returns `prompt_toolkit`'s version as tuple of integers.""" if has_prompt_toolkit(): return tuple(int(x) for x in ptk_version().strip("<>+-=.").split(".")) else: return None
minimum_required_ptk_version = (2, 0, 0) """Minimum version of prompt-toolkit supported by Xonsh"""
[docs] @functools.lru_cache(1) def ptk_above_min_supported(): return ptk_version_info() and ptk_version_info() >= minimum_required_ptk_version
[docs] @functools.lru_cache(1) def win_ansi_support(): if ON_WINDOWS: try: from prompt_toolkit.utils import is_conemu_ansi, is_windows_vt100_supported except ImportError: return False return is_conemu_ansi() or is_windows_vt100_supported() else: return False
[docs] @functools.lru_cache(1) def ptk_below_max_supported(): ptk_max_version_cutoff = (99999, 0) # currently, no limit. return ptk_version_info()[:2] < ptk_max_version_cutoff
[docs] @functools.lru_cache(1) def best_shell_type(): from xonsh.built_ins import XSH if XSH.env.get("TERM", "") == "dumb": return "dumb" if has_prompt_toolkit(): return "prompt_toolkit" return "readline"
[docs] @functools.lru_cache(1) def is_readline_available(): """Checks if readline is available to import.""" spec = importlib.util.find_spec("readline") return spec is not None
@lazyobject def seps(): """String of all path separators.""" s = os.path.sep if os.path.altsep is not None: s += os.path.altsep return s
[docs] def pathsplit(p): """This is a safe version of os.path.split(), which does not work on input without a drive. """ n = len(p) if n == 0: # lazy object seps does not get initialized when n is zero return "", "" while n and p[n - 1] not in seps: n -= 1 pre = p[:n] pre = pre.rstrip(seps) or pre post = p[n:] return pre, post
[docs] def pathbasename(p): """This is a safe version of os.path.basename(), which does not work on input without a drive. This version does. """ return pathsplit(p)[-1]
@lazyobject def expanduser(): """Dispatches to the correct platform-dependent expanduser() function.""" if ON_WINDOWS: return windows_expanduser else: return os.path.expanduser
[docs] def windows_expanduser(path): """A Windows-specific expanduser() function for xonsh. This is needed since os.path.expanduser() does not check on Windows if the user actually exists. This restricts expanding the '~' if it is not followed by a separator. That is only '~/' and '~\' are expanded. """ path = str(path) if not path.startswith("~"): return path elif len(path) < 2 or path[1] in seps: return os.path.expanduser(path) else: return path
# termios tc(get|set)attr indexes. IFLAG = 0 OFLAG = 1 CFLAG = 2 LFLAG = 3 ISPEED = 4 OSPEED = 5 CC = 6 # # Dev release info #
[docs] @functools.lru_cache(1) def githash(): """Returns a tuple contains two strings: the hash and the date.""" install_base = os.path.dirname(__file__) githash_file = f"{install_base}/dev.githash" if not os.path.exists(githash_file): return None, None sha = None date_ = None try: with open(githash_file) as f: sha, date_ = f.read().strip().split("|") except ValueError: pass return sha, date_
# # Encoding # DEFAULT_ENCODING = sys.getdefaultencoding() """ Default string encoding. """ # # Linux distro #
[docs] @functools.lru_cache(1) def linux_distro(): """The id of the Linux distribution running on, possibly 'unknown'. None on non-Linux platforms. """ if ON_LINUX: if distro: ld = distro.id() elif PYTHON_VERSION_INFO < (3, 6, 6): ld = platform.linux_distribution()[0] or "unknown" elif "-ARCH-" in platform.platform(): ld = "arch" # that's the only one we need to know for now else: ld = "unknown" else: ld = None return ld
# # Windows #
[docs] @functools.lru_cache(1) def git_for_windows_path(): """Returns the path to git for windows, if available and None otherwise.""" import winreg try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\GitForWindows") gfwp, _ = winreg.QueryValueEx(key, "InstallPath") except FileNotFoundError: gfwp = None return gfwp
[docs] @functools.lru_cache(1) def windows_bash_command(): """Determines the command for Bash on windows.""" # Check that bash is on path otherwise try the default directory # used by Git for windows from xonsh.built_ins import XSH wbc = "bash" cmd_cache = XSH.commands_cache bash_on_path = cmd_cache.lazy_locate_binary("bash", ignore_alias=True) if bash_on_path: try: out = subprocess.check_output( [bash_on_path, "--version"], stderr=subprocess.PIPE, text=True, ) except subprocess.CalledProcessError: bash_works = False else: # Check if Bash is from the "Windows Subsystem for Linux" (WSL) # which can't be used by xonsh foreign-shell/completer bash_works = out and "pc-linux-gnu" not in out.splitlines()[0] if bash_works: wbc = bash_on_path else: gfwp = git_for_windows_path() if gfwp: bashcmd = os.path.join(gfwp, "bin\\bash.exe") if os.path.isfile(bashcmd): wbc = bashcmd return wbc
# # Environment variables defaults # if ON_WINDOWS: class OSEnvironCasePreserving(cabc.MutableMapping): """Case-preserving wrapper for os.environ on Windows. It uses nt.environ to get the correct cased keys on initialization. It also preserves the case of any variables add after initialization. """ def __init__(self): import nt self._upperkeys = {k.upper(): k for k in nt.environ} def _sync(self): """Ensure that the case sensitive map of the keys are in sync with os.environ """ envkeys = set(os.environ.keys()) for key in envkeys.difference(self._upperkeys): self._upperkeys[key] = key.upper() for key in set(self._upperkeys).difference(envkeys): del self._upperkeys[key] def __contains__(self, k): self._sync() return k.upper() in self._upperkeys def __len__(self): self._sync() return len(self._upperkeys) def __iter__(self): self._sync() return iter(self._upperkeys.values()) def __getitem__(self, k): self._sync() return os.environ[k] def __setitem__(self, k, v): self._sync() self._upperkeys[k.upper()] = k os.environ[k] = v def __delitem__(self, k): self._sync() if k.upper() in self._upperkeys: del self._upperkeys[k.upper()] del os.environ[k] def getkey_actual_case(self, k): self._sync() return self._upperkeys.get(k.upper()) @lazyobject def os_environ(): """This dispatches to the correct, case-sensitive version of os.environ. This is mainly a problem for Windows. See #2024 for more details. This can probably go away once support for Python v3.5 or v3.6 is dropped. """ if ON_WINDOWS: return OSEnvironCasePreserving() else: return os.environ
[docs] @functools.lru_cache(1) def bash_command(): """Determines the command for Bash on the current platform.""" if ON_WINDOWS: bc = windows_bash_command() else: bc = "bash" return bc
@lazyobject def BASH_COMPLETIONS_DEFAULT(): """A possibly empty tuple with default paths to Bash completions known for the current platform. """ if ON_LINUX or ON_CYGWIN or ON_MSYS: bcd = ("/usr/share/bash-completion/bash_completion",) elif ON_DARWIN: bcd = ( "/usr/local/share/bash-completion/bash_completion", # v2.x "/usr/local/etc/bash_completion", # v1.x "/opt/homebrew/share/bash-completion/bash_completion", # v2.x on M1 "/opt/homebrew/etc/bash_completion", # v1.x on M1 ) elif ON_WINDOWS and git_for_windows_path(): bcd = ( os.path.join( git_for_windows_path(), "usr\\share\\bash-completion\\bash_completion" ), os.path.join( git_for_windows_path(), "mingw64\\share\\git\\completion\\" "git-completion.bash", ), ) else: bcd = () return bcd @lazyobject def PATH_DEFAULT(): if ON_LINUX or ON_CYGWIN or ON_MSYS: if linux_distro() == "arch": pd = ( "/usr/local/sbin", "/usr/local/bin", "/usr/bin", "/usr/bin/site_perl", "/usr/bin/vendor_perl", "/usr/bin/core_perl", ) else: pd = ( os.path.expanduser("~/bin"), "/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin", "/usr/games", "/usr/local/games", ) elif ON_DARWIN: pd = ("/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin") elif ON_WINDOWS: import winreg key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", ) pd = tuple(winreg.QueryValueEx(key, "Path")[0].split(os.pathsep)) else: pd = () return pd # # libc # @lazyobject def LIBC(): """The platform dependent libc implementation.""" global ctypes if ON_DARWIN: import ctypes.util libc = ctypes.CDLL(ctypes.util.find_library("c")) elif ON_CYGWIN: libc = ctypes.CDLL("cygwin1.dll") elif ON_MSYS: libc = ctypes.CDLL("msys-2.0.dll") elif ON_FREEBSD: try: libc = ctypes.CDLL("libc.so.7") except OSError: libc = None elif ON_BSD: try: libc = ctypes.CDLL("libc.so") except AttributeError: libc = None except OSError: # OS X; can't use ctypes.util.find_library because that creates # a new process on Linux, which is undesirable. try: libc = ctypes.CDLL("libc.dylib") except OSError: libc = None elif ON_POSIX: try: libc = ctypes.CDLL("libc.so") except AttributeError: libc = None except OSError: # Debian and derivatives do the wrong thing because /usr/lib/libc.so # is a GNU ld script rather than an ELF object. To get around this, we # have to be more specific. # We don't want to use ctypes.util.find_library because that creates a # new process on Linux. We also don't want to try too hard because at # this point we're already pretty sure this isn't Linux. try: libc = ctypes.CDLL("libc.so.6") except OSError: libc = None if not hasattr(libc, "sysinfo"): # Not Linux. libc = None elif ON_WINDOWS: if hasattr(ctypes, "windll") and hasattr(ctypes.windll, "kernel32"): libc = ctypes.windll.kernel32 else: try: # Windows CE uses the cdecl calling convention. libc = ctypes.CDLL("coredll.lib") except (AttributeError, OSError): libc = None elif ON_BEOS: libc = ctypes.CDLL("libroot.so") else: libc = None return libc