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