1"""Module for platform-specific constants and implementations, as well as 2compatibility layers to make use of the 'best' implementation available 3on a platform. 4""" 5import os 6import sys 7import ctypes 8import signal 9import pathlib 10import builtins 11import platform 12import functools 13import subprocess 14import collections 15import importlib.util 16 17from xonsh.lazyasd import LazyBool, lazyobject, lazybool 18 19# do not import any xonsh-modules here to avoid circular dependencies 20 21FD_STDIN = 0 22FD_STDOUT = 1 23FD_STDERR = 2 24 25 26@lazyobject 27def distro(): 28 try: 29 import distro as d 30 except ImportError: 31 d = None 32 except Exception: 33 raise 34 return d 35 36 37# 38# OS 39# 40ON_DARWIN = LazyBool(lambda: platform.system() == "Darwin", globals(), "ON_DARWIN") 41"""``True`` if executed on a Darwin platform, else ``False``. """ 42ON_LINUX = LazyBool(lambda: platform.system() == "Linux", globals(), "ON_LINUX") 43"""``True`` if executed on a Linux platform, else ``False``. """ 44ON_WINDOWS = LazyBool(lambda: platform.system() == "Windows", globals(), "ON_WINDOWS") 45"""``True`` if executed on a native Windows platform, else ``False``. """ 46ON_CYGWIN = LazyBool(lambda: sys.platform == "cygwin", globals(), "ON_CYGWIN") 47"""``True`` if executed on a Cygwin Windows platform, else ``False``. """ 48ON_MSYS = LazyBool(lambda: sys.platform == "msys", globals(), "ON_MSYS") 49"""``True`` if executed on a MSYS Windows platform, else ``False``. """ 50ON_POSIX = LazyBool(lambda: (os.name == "posix"), globals(), "ON_POSIX") 51"""``True`` if executed on a POSIX-compliant platform, else ``False``. """ 52ON_FREEBSD = LazyBool( 53 lambda: (sys.platform.startswith("freebsd")), globals(), "ON_FREEBSD" 54) 55"""``True`` if on a FreeBSD operating system, else ``False``.""" 56ON_NETBSD = LazyBool( 57 lambda: (sys.platform.startswith("netbsd")), globals(), "ON_NETBSD" 58) 59"""``True`` if on a NetBSD operating system, else ``False``.""" 60 61 62@lazybool 63def ON_BSD(): 64 """``True`` if on a BSD operating system, else ``False``.""" 65 return bool(ON_FREEBSD) or bool(ON_NETBSD) 66 67 68@lazybool 69def ON_BEOS(): 70 """True if we are on BeOS or Haiku.""" 71 return sys.platform == "beos5" or sys.platform == "haiku1" 72 73 74# 75# Python & packages 76# 77 78PYTHON_VERSION_INFO = sys.version_info[:3] 79""" Version of Python interpreter as three-value tuple. """ 80 81 82@lazyobject 83def PYTHON_VERSION_INFO_BYTES(): 84 """The python version info tuple in a canonical bytes form.""" 85 return ".".join(map(str, sys.version_info)).encode() 86 87 88ON_ANACONDA = LazyBool( 89 lambda: any(s in sys.version for s in {"Anaconda", "Continuum", "conda-forge"}), 90 globals(), 91 "ON_ANACONDA", 92) 93""" ``True`` if executed in an Anaconda instance, else ``False``. """ 94CAN_RESIZE_WINDOW = LazyBool( 95 lambda: hasattr(signal, "SIGWINCH"), globals(), "CAN_RESIZE_WINDOW" 96) 97"""``True`` if we can resize terminal window, as provided by the presense of 98signal.SIGWINCH, else ``False``. 99""" 100 101 102@lazybool 103def HAS_PYGMENTS(): 104 """``True`` if `pygments` is available, else ``False``.""" 105 spec = importlib.util.find_spec("pygments") 106 return spec is not None 107 108 109@functools.lru_cache(1) 110def pygments_version(): 111 """pygments.__version__ version if available, else None.""" 112 if HAS_PYGMENTS: 113 import pygments 114 115 v = pygments.__version__ 116 else: 117 v = None 118 return v 119 120 121@functools.lru_cache(1) 122def pygments_version_info(): 123 """ Returns `pygments`'s version as tuple of integers. """ 124 if HAS_PYGMENTS: 125 return tuple(int(x) for x in pygments_version().strip("<>+-=.").split(".")) 126 else: 127 return None 128 129 130@functools.lru_cache(1) 131def has_prompt_toolkit(): 132 """Tests if the `prompt_toolkit` is available.""" 133 spec = importlib.util.find_spec("prompt_toolkit") 134 return spec is not None 135 136 137@functools.lru_cache(1) 138def ptk_version(): 139 """Returns `prompt_toolkit.__version__` if available, else ``None``.""" 140 if has_prompt_toolkit(): 141 import prompt_toolkit 142 143 return getattr(prompt_toolkit, "__version__", "<0.57") 144 else: 145 return None 146 147 148@functools.lru_cache(1) 149def ptk_version_info(): 150 """ Returns `prompt_toolkit`'s version as tuple of integers. """ 151 if has_prompt_toolkit(): 152 return tuple(int(x) for x in ptk_version().strip("<>+-=.").split(".")) 153 else: 154 return None 155 156 157@functools.lru_cache(1) 158def ptk_above_min_supported(): 159 minimum_required_ptk_version = (1, 0) 160 return ptk_version_info()[:2] >= minimum_required_ptk_version 161 162 163@functools.lru_cache(1) 164def ptk_shell_type(): 165 """Returns the prompt_toolkit shell type based on the installed version.""" 166 if ptk_version_info()[:2] < (2, 0): 167 return "prompt_toolkit1" 168 else: 169 return "prompt_toolkit2" 170 171 172@functools.lru_cache(1) 173def win_ansi_support(): 174 if ON_WINDOWS: 175 try: 176 from prompt_toolkit.utils import is_windows_vt100_supported, is_conemu_ansi 177 except ImportError: 178 return False 179 return is_conemu_ansi() or is_windows_vt100_supported() 180 else: 181 return False 182 183 184@functools.lru_cache(1) 185def ptk_below_max_supported(): 186 ptk_max_version_cutoff = (2, 0) 187 return ptk_version_info()[:2] < ptk_max_version_cutoff 188 189 190@functools.lru_cache(1) 191def best_shell_type(): 192 if ON_WINDOWS or has_prompt_toolkit(): 193 return "prompt_toolkit" 194 else: 195 return "readline" 196 197 198@functools.lru_cache(1) 199def is_readline_available(): 200 """Checks if readline is available to import.""" 201 spec = importlib.util.find_spec("readline") 202 return spec is not None 203 204 205@lazyobject 206def seps(): 207 """String of all path separators.""" 208 s = os.path.sep 209 if os.path.altsep is not None: 210 s += os.path.altsep 211 return s 212 213 214def pathsplit(p): 215 """This is a safe version of os.path.split(), which does not work on input 216 without a drive. 217 """ 218 n = len(p) 219 while n and p[n - 1] not in seps: 220 n -= 1 221 pre = p[:n] 222 pre = pre.rstrip(seps) or pre 223 post = p[n:] 224 return pre, post 225 226 227def pathbasename(p): 228 """This is a safe version of os.path.basename(), which does not work on 229 input without a drive. This version does. 230 """ 231 return pathsplit(p)[-1] 232 233 234@lazyobject 235def expanduser(): 236 """Dispatches to the correct platform-dependent expanduser() function.""" 237 if ON_WINDOWS: 238 return windows_expanduser 239 else: 240 return os.path.expanduser 241 242 243def windows_expanduser(path): 244 """A Windows-specific expanduser() function for xonsh. This is needed 245 since os.path.expanduser() does not check on Windows if the user actually 246 exists. This restricts expanding the '~' if it is not followed by a 247 separator. That is only '~/' and '~\' are expanded. 248 """ 249 if not path.startswith("~"): 250 return path 251 elif len(path) < 2 or path[1] in seps: 252 return os.path.expanduser(path) 253 else: 254 return path 255 256 257# termios tc(get|set)attr indexes. 258IFLAG = 0 259OFLAG = 1 260CFLAG = 2 261LFLAG = 3 262ISPEED = 4 263OSPEED = 5 264CC = 6 265 266 267# 268# Dev release info 269# 270 271 272@functools.lru_cache(1) 273def githash(): 274 """Returns a tuple contains two strings: the hash and the date.""" 275 install_base = os.path.dirname(__file__) 276 githash_file = "{}/dev.githash".format(install_base) 277 if not os.path.exists(githash_file): 278 return None, None 279 sha = None 280 date_ = None 281 try: 282 with open(githash_file) as f: 283 sha, date_ = f.read().strip().split("|") 284 except ValueError: 285 pass 286 return sha, date_ 287 288 289# 290# Encoding 291# 292 293DEFAULT_ENCODING = sys.getdefaultencoding() 294""" Default string encoding. """ 295 296 297if PYTHON_VERSION_INFO < (3, 5, 0): 298 299 class DirEntry: 300 def __init__(self, directory, name): 301 self.__path__ = pathlib.Path(directory) / name 302 self.name = name 303 self.path = str(self.__path__) 304 self.is_symlink = self.__path__.is_symlink 305 306 def inode(self): 307 return os.stat(self.path, follow_symlinks=False).st_ino 308 309 def is_dir(self, *, follow_symlinks=True): 310 if follow_symlinks: 311 return self.__path__.is_dir() 312 else: 313 return not self.__path__.is_symlink() and self.__path__.is_dir() 314 315 def is_file(self, *, follow_symlinks=True): 316 if follow_symlinks: 317 return self.__path__.is_file() 318 else: 319 return not self.__path__.is_symlink() and self.__path__.is_file() 320 321 def stat(self, *, follow_symlinks=True): 322 return os.stat(self.path, follow_symlinks=follow_symlinks) 323 324 def scandir(path): 325 """ Compatibility layer for `os.scandir` from Python 3.5+. """ 326 return (DirEntry(path, x) for x in os.listdir(path)) 327 328 329else: 330 scandir = os.scandir 331 332 333# 334# Linux distro 335# 336 337 338@functools.lru_cache(1) 339def linux_distro(): 340 """The id of the Linux distribution running on, possibly 'unknown'. 341 None on non-Linux platforms. 342 """ 343 if ON_LINUX: 344 if distro: 345 ld = distro.id() 346 elif PYTHON_VERSION_INFO < (3, 7, 0): 347 ld = platform.linux_distribution()[0] or "unknown" 348 elif "-ARCH-" in platform.platform(): 349 ld = "arch" # that's the only one we need to know for now 350 else: 351 ld = "unknown" 352 else: 353 ld = None 354 return ld 355 356 357# 358# Windows 359# 360 361 362@functools.lru_cache(1) 363def git_for_windows_path(): 364 """Returns the path to git for windows, if available and None otherwise.""" 365 import winreg 366 367 try: 368 key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\GitForWindows") 369 gfwp, _ = winreg.QueryValueEx(key, "InstallPath") 370 except FileNotFoundError: 371 gfwp = None 372 return gfwp 373 374 375@functools.lru_cache(1) 376def windows_bash_command(): 377 """Determines the command for Bash on windows.""" 378 # Check that bash is on path otherwise try the default directory 379 # used by Git for windows 380 wbc = "bash" 381 cmd_cache = builtins.__xonsh_commands_cache__ 382 bash_on_path = cmd_cache.lazy_locate_binary("bash", ignore_alias=True) 383 if bash_on_path: 384 try: 385 out = subprocess.check_output( 386 [bash_on_path, "--version"], 387 stderr=subprocess.PIPE, 388 universal_newlines=True, 389 ) 390 except subprocess.CalledProcessError: 391 bash_works = False 392 else: 393 # Check if Bash is from the "Windows Subsystem for Linux" (WSL) 394 # which can't be used by xonsh foreign-shell/completer 395 bash_works = out and "pc-linux-gnu" not in out.splitlines()[0] 396 397 if bash_works: 398 wbc = bash_on_path 399 else: 400 gfwp = git_for_windows_path() 401 if gfwp: 402 bashcmd = os.path.join(gfwp, "bin\\bash.exe") 403 if os.path.isfile(bashcmd): 404 wbc = bashcmd 405 return wbc 406 407 408# 409# Environment variables defaults 410# 411 412if ON_WINDOWS: 413 414 class OSEnvironCasePreserving(collections.MutableMapping): 415 """ Case-preserving wrapper for os.environ on Windows. 416 It uses nt.environ to get the correct cased keys on 417 initialization. It also preserves the case of any variables 418 add after initialization. 419 """ 420 421 def __init__(self): 422 import nt 423 424 self._upperkeys = dict((k.upper(), k) for k in nt.environ) 425 426 def _sync(self): 427 """ Ensure that the case sensitive map of the keys are 428 in sync with os.environ 429 """ 430 envkeys = set(os.environ.keys()) 431 for key in envkeys.difference(self._upperkeys): 432 self._upperkeys[key] = key.upper() 433 for key in set(self._upperkeys).difference(envkeys): 434 del self._upperkeys[key] 435 436 def __contains__(self, k): 437 self._sync() 438 return k.upper() in self._upperkeys 439 440 def __len__(self): 441 self._sync() 442 return len(self._upperkeys) 443 444 def __iter__(self): 445 self._sync() 446 return iter(self._upperkeys.values()) 447 448 def __getitem__(self, k): 449 self._sync() 450 return os.environ[k] 451 452 def __setitem__(self, k, v): 453 self._sync() 454 self._upperkeys[k.upper()] = k 455 os.environ[k] = v 456 457 def __delitem__(self, k): 458 self._sync() 459 if k.upper() in self._upperkeys: 460 del self._upperkeys[k.upper()] 461 del os.environ[k] 462 463 def getkey_actual_case(self, k): 464 self._sync() 465 return self._upperkeys.get(k.upper()) 466 467 468@lazyobject 469def os_environ(): 470 """This dispatches to the correct, case-sensitive version of os.environ. 471 This is mainly a problem for Windows. See #2024 for more details. 472 This can probably go away once support for Python v3.5 or v3.6 is 473 dropped. 474 """ 475 if ON_WINDOWS: 476 return OSEnvironCasePreserving() 477 else: 478 return os.environ 479 480 481@functools.lru_cache(1) 482def bash_command(): 483 """Determines the command for Bash on the current platform.""" 484 if ON_WINDOWS: 485 bc = windows_bash_command() 486 else: 487 bc = "bash" 488 return bc 489 490 491@lazyobject 492def BASH_COMPLETIONS_DEFAULT(): 493 """A possibly empty tuple with default paths to Bash completions known for 494 the current platform. 495 """ 496 if ON_LINUX or ON_CYGWIN or ON_MSYS: 497 bcd = ("/usr/share/bash-completion/bash_completion",) 498 elif ON_DARWIN: 499 bcd = ( 500 "/usr/local/share/bash-completion/bash_completion", # v2.x 501 "/usr/local/etc/bash_completion", 502 ) # v1.x 503 elif ON_WINDOWS and git_for_windows_path(): 504 bcd = ( 505 os.path.join( 506 git_for_windows_path(), "usr\\share\\bash-completion\\bash_completion" 507 ), 508 os.path.join( 509 git_for_windows_path(), 510 "mingw64\\share\\git\\completion\\" "git-completion.bash", 511 ), 512 ) 513 else: 514 bcd = () 515 return bcd 516 517 518@lazyobject 519def PATH_DEFAULT(): 520 if ON_LINUX or ON_CYGWIN or ON_MSYS: 521 if linux_distro() == "arch": 522 pd = ( 523 "/usr/local/sbin", 524 "/usr/local/bin", 525 "/usr/bin", 526 "/usr/bin/site_perl", 527 "/usr/bin/vendor_perl", 528 "/usr/bin/core_perl", 529 ) 530 else: 531 pd = ( 532 os.path.expanduser("~/bin"), 533 "/usr/local/sbin", 534 "/usr/local/bin", 535 "/usr/sbin", 536 "/usr/bin", 537 "/sbin", 538 "/bin", 539 "/usr/games", 540 "/usr/local/games", 541 ) 542 elif ON_DARWIN: 543 pd = ("/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin") 544 elif ON_WINDOWS: 545 import winreg 546 547 key = winreg.OpenKey( 548 winreg.HKEY_LOCAL_MACHINE, 549 r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", 550 ) 551 pd = tuple(winreg.QueryValueEx(key, "Path")[0].split(os.pathsep)) 552 else: 553 pd = () 554 return pd 555 556 557# 558# libc 559# 560@lazyobject 561def LIBC(): 562 """The platform dependent libc implementation.""" 563 global ctypes 564 if ON_DARWIN: 565 import ctypes.util 566 567 libc = ctypes.CDLL(ctypes.util.find_library("c")) 568 elif ON_CYGWIN: 569 libc = ctypes.CDLL("cygwin1.dll") 570 elif ON_MSYS: 571 libc = ctypes.CDLL("msys-2.0.dll") 572 elif ON_BSD: 573 try: 574 libc = ctypes.CDLL(ctypes.util.find_library("c")) 575 except AttributeError: 576 libc = None 577 except OSError: 578 # OS X; can't use ctypes.util.find_library because that creates 579 # a new process on Linux, which is undesirable. 580 try: 581 libc = ctypes.CDLL("libc.dylib") 582 except OSError: 583 libc = None 584 elif ON_POSIX: 585 try: 586 libc = ctypes.CDLL("libc.so") 587 except AttributeError: 588 libc = None 589 except OSError: 590 # Debian and derivatives do the wrong thing because /usr/lib/libc.so 591 # is a GNU ld script rather than an ELF object. To get around this, we 592 # have to be more specific. 593 # We don't want to use ctypes.util.find_library because that creates a 594 # new process on Linux. We also don't want to try too hard because at 595 # this point we're already pretty sure this isn't Linux. 596 try: 597 libc = ctypes.CDLL("libc.so.6") 598 except OSError: 599 libc = None 600 if not hasattr(libc, "sysinfo"): 601 # Not Linux. 602 libc = None 603 elif ON_WINDOWS: 604 if hasattr(ctypes, "windll") and hasattr(ctypes.windll, "kernel32"): 605 libc = ctypes.windll.kernel32 606 else: 607 try: 608 # Windows CE uses the cdecl calling convention. 609 libc = ctypes.CDLL("coredll.lib") 610 except (AttributeError, OSError): 611 libc = None 612 elif ON_BEOS: 613 libc = ctypes.CDLL("libroot.so") 614 else: 615 libc = None 616 return libc 617