"""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.lib.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 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]
def bash_command():
"""Determines the command for Bash on the current platform."""
if (bc := os.getenv("XONSH_BASH_PATH_OVERRIDE", None)) is not None:
bc = str(bc) # for pathlib Paths
elif 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