#!/usr/bin/env python
# Copyright (c) 2004-20010 ActiveState Software Inc.
# Written by: Trent Mick <trentm@gmail.com>
# License: MIT License (http://www.opensource.org/licenses/mit-license.php)

"""
    platinfo.py -- standardized determination of platform info

        >>> from platinfo import PlatInfo
        >>> pi = PlatInfo()
        >>> pi.os
        'linux'
        >>> pi.arch
        'x86'

        # A number of pieces of info gathered (some of them plat-dependent).
        >>> pi.as_dict()
        {'arch': 'x86',
         'distro': 'SuSE',
         'distro_desc': 'SuSE Linux 9.0 (i586)',
         'distro_ver': '9.0',
         'libcpp': 'libcpp5',
         'lsb_version': '1.3',
         'name': 'linux-x86',
         'os': 'linux',
         'os_ver': '2.4.21'}

        # The default name is "<os>-<arch>"...
        >>> pi.name()   # default
        'linux-x86'
        >>> print pi
        linux-x86

        # ...but that can be customized with some rules.
        >>> pi.name('os', 'distro', 'arch')
        'linux-suse-x86'
        >>> pi.name('os', 'distro+distro_ver', 'arch')
        'linux-suse9-x86'
        >>> pi.name('os+os_ver[:2]', 'arch')
        'linux2.4-x86'
        >>> pi.name('os', 'arch', sep='/')
        'linux/x86'

        # The full name provide a little bit more info.
        >>> pi.fullname()
        'linux2.4-suse9-x86'

        # platname() is a shortcut for PlatInfo.name(...).
        >>> from platinfo import platname
        >>> platname('os', 'distro', 'arch')
        'linux-suse-x86'

    This module determines and returns standardized names for
    platforms, where the "standard" is Trent Mick's reasoning :)
    from experience building ActivePython on a fairly large number of
    platforms.
    
    The driving goal is to provide platform names that are:
    - relatively short
    - readable (as much as possible making matching the given name to an
      actually machine self-explanatory)
    - be capable enough to produce all names needed to distinguish all
      platform-specific application builds
    - generally safe for usage in filenames
    - not ugly (e.g. "MSWin32" is ugly)

    Generally some of these names match those used for ActiveTcl and
    ActivePerl where that makes sense (for example, "windows" is used
    instead of Perl's burdensome "MSWin32"). See the particular method
    docstrings for more details.
"""
# Development notes:
# - The name of this module is intentionally not "platform" to not
#   conflict with (Marc-Andre Lemburg's?) platform.py in the stdlib.
# - YAGNI: Having a "quick/terse" mode. Will always gather all possible
#   information unless come up with a case to NOT do so.

__version_info__ = (2, 0, 0)
__version__ = '.'.join(map(str, __version_info__))

import os
import sys
import re
import tempfile
import logging
import errno
import subprocess
from pprint import pprint
from os.path import exists
import warnings


log = logging.getLogger("platinfo")



#---- exceptions

class Error(Exception):
    pass

class InternalError(Error):
    def __str__(self):
        return Error.__str__(self) + """

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Please report this error by adding a bug here:
*     http://code.google.com/p/platinfo/issues/list
* or, by sending an email to <trentm@gmail.com>.
*
* I'd like to keep improving `platinfo2.py' to cover as many platforms
* as possible. Please be sure to include the error message above and
* any addition information you think might be relevant. Thanks!
* -- Trent
*
* platinfo version: %s
* python version: %s
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *""" % (
    __version_info__, sys.version_info)

class LinuxDistroVersionWarning(RuntimeWarning):
    pass
warnings.simplefilter("once", LinuxDistroVersionWarning)



#---- public API

class PlatInfo(object):
    """Platform information for the current machine."""
    #TODO: allow incoming 'win32' and 'win64'?
    _known_oses = set(
        "windows hpux linux macosx aix solaris freebsd openbsd".split())
    # Note: `ia64_32` is the name being used for 32-bit builds on
    # HP-UX IA64. This is a hacky name (mixing plat and build info), but
    # it isn't the first one.
    _known_archs = set(
        "x86 powerpc powerpc64 ppc x64 x86_64 ia64 ia64_32 sparc sparc64 parisc".split())

    @classmethod
    def from_name(cls, name):
        """Create a PlatInfo instance from a platname string.

        This only knows how to deal with "os[os_ver]-arch[arch_ver]".
        For example:
            GOOD: windows-x86, hpux-parisc2.0, aix5-powerpc
            BAD:  linux-libcpp5-x86
        """
        parts = name.split('-')
        if len(parts) != 2:
            raise Error("cannot parse a platname that doesn't match "
                        "'os[os_ver]-arch[arch_ver]': %r" % name)
        data = {}
        if parts[0] in cls._known_oses:
            data["os"] = parts[0]
        else:
            for known_os in cls._known_oses:
                if parts[0].startswith(known_os):
                    data["os"] = known_os
                    data["os_ver"] = parts[0][len(known_os):]
                    break
            else:
                raise Error("could not part os-part of platform name: %r"
                            % parts[0])
        if parts[1] in cls._known_archs:
            data["arch"] = parts[1]
        else:
            for known_arch in cls._known_archs:
                if parts[1].startswith(known_arch):
                    data["arch"] = known_arch
                    data["arch_ver"] = parts[1][len(known_arch):]
                    break
            else:
                raise Error("could not part arch-part of platform name: %r"
                            % parts[1])
        return cls(**data)

    def __init__(self, **kwargs):
        """If created with no arguments, all available data for the current
        platform will be determine. If called with arguments, the PlatInfo
        will just use those as all platform info. E.g.,

            >>> p = PlatInfo(os='windows', arch='x86')
            >>> p.name()
            'windows-x86'
        """
        if kwargs:
            self.__dict__ = kwargs
        elif sys.platform == "win32":
            self._init_windows()
        elif sys.platform.startswith("linux"):
            self._init_linux()
        elif sys.platform.startswith("sunos"):
            self._init_solaris()
        elif sys.platform.startswith("hp-ux"):
            self._init_hpux()
        elif sys.platform.startswith("aix"):
            self._init_aix()
        elif sys.platform == "darwin":
            self._init_mac()
        elif sys.platform.startswith("freebsd"):
            self._init_freebsd()
        elif sys.platform.startswith("openbsd"):
            self._init_openbsd()
        elif sys.platform.startswith("netbsd"):
            self._init_netbsd()
        else:
            raise InternalError("unknown platform: '%s'" % sys.platform)

    def __str__(self):
        return self.name()

    def __repr__(self):
        args = ['%s=%r' % item for item in self.__dict__.items()]
        class_parts = [self.__class__.__name__]
        if self.__class__.__module__ != "__main__":
            class_parts.insert(0, self.__class__.__module__)
        return "%s(%s)" % ('.'.join(class_parts), ', '.join(args))

    def match(self, **kwargs):
        for name, value in kwargs.items():
            if getattr(self, name) != value:
                return False
        else:
            return True

    def name(self, *rules, **kwargs):
        """name([rules...]) --> platform name

        Return a string representation for this platform.

        Keyword args:
          'sep' is a string to use for joining platform data.
          'errors' is a string defining what to do if a given platform
              data does not exist for this platform. E.g. if the rule
              "distro" is given for Windows. Valid values are:
                  "ignore" - just skip that datum (this is the default)
                  "strict" - raise an Error
          'filesafe' is a boolean (default False) indicating if the
              returned name should be made safe for usage in a filename.
          'lower' is a boolean (default True) indicating if the returned
              name should be lowercased.

        Rule Syntax:
          os              # just a datum name
          os+os_ver       # use '+' to join without the 'sep'
        """
        sep = kwargs.get("sep", "-")
        errors = kwargs.get("errors", "ignore")
        filesafe = kwargs.get("filesafe", False)
        if filesafe:
            raise InternalError("name(..., filesafe=True) not yet implemented")
        lower = kwargs.get("lower", True)
        if not rules:
            rules = ("os", "arch")
        #print "RULES:", rules
        bits = []
        for rule in rules:
            bit = self._eval_rule(rule, errors=errors)
            if bit:
                bits.append(bit)
        if lower:
            return sep.join(bits).lower()
        else:
            return sep.join(bits)

    def fullname(self):
        parts = []
        if sys.platform == "win32":
            parts = ["os", "os_name"]
        else:
            parts = ["os+os_ver[:2]"]
        parts += ["distro+distro_ver", "libc", "glibc+glibc_ver[:2]",
                  "libcpp", "arch+arch_ver"]
        return self.name(*parts)

    _token_parser = re.compile(r"^([\w]+)(\[[\d:]+\])?$")
    def _eval_rule(self, rule, errors):
        assert errors in ("strict", "ignore")
        bits = []
        for token in rule.split('+'):
            m = self._token_parser.search(token)
            if not m:
                if errors == "strict":
                    raise Error("illegal token: '%s'" % token)
                elif errors == "ignore":
                    continue

            item_name, slice_str = m.groups()
            try:
                value = getattr(self, item_name)
            except AttributeError:
                if errors == "strict":
                    raise Error("no '%s' info for this platform" % item_name)
                elif errors == "ignore":
                    continue

            if slice_str and not item_name.endswith("_ver"):
                if errors == "strict":
                    raise Error("slicing only allowed on '*_ver' items: '%s'"
                                % token)
                elif errors == "ignore":
                    continue
            elif slice_str:
                parts = _split_ver(value)
                value = _join_ver( eval(str(parts)+slice_str) )

            bits.append(value)
        return ''.join(bits)

    def as_dict(self):
        """Return a dict representation of the platform info."""
        d = self.__dict__.copy()
        assert "name" not in d, "conflict with `name` datum"
        d["name"] = self.name()
        return d

    def as_xml(self):
        from xml.sax.saxutils import escape
        indent = ' '*2
        s = '<platinfo version="%s">\n' % __version__
        for key, value in self.as_dict().items():
            s += indent + '<%s>%s</%s>\n' % (key, escape(value), key)
        s += '</platinfo>'
        return s

    def as_yaml(self):
        # prefix with '---\n' YAML doc-separator?
        s = '--- platinfo version="%s"\n' % __version__
        parts = ["%s: %s" % i for i in self.as_dict().items()]
        s += '\n'.join(parts)
        return s

    def _init_windows(self):
        #XXX Right answer here is GetSystemInfo().
        #XXX Does this work on all Windows flavours?
        self.os = "windows"
        PROCESSOR_ARCHITECTURE = os.environ.get("PROCESSOR_ARCHITECTURE")
        if PROCESSOR_ARCHITECTURE == "IA64":
            self.arch = "ia64"
        elif PROCESSOR_ARCHITECTURE == "x86":
            self.arch = "x86"
        elif PROCESSOR_ARCHITECTURE == "AMD64":
            self.arch = "x64"
        else:
            raise InternalError("unknown Windows PROCESSOR_ARCHITECTURE: %r"
                                % PROCESSOR_ARCHITECTURE)

        # Get some additional info from Python's core platform.py, if
        # available.
        #XXX Would be nice to extend platform.py's win32_ver to use
        #    the extra OSVERSIONINFOEX structure elements (esp. service
        #    package version).
        try:
            import platform
        except ImportError:
            log.debug("cannot get extra windows os info: no platform.py")
        else:
            release, version, csd, ptype = platform.win32_ver()
            if not release:
                log.debug("platform.py could not get extra windows os info")
            if release: self.os_name = release
            if version: self.os_ver = version
            if csd:     self.os_csd = csd

    def _init_freebsd(self):
        self.os = "freebsd"
        uname = os.uname()
        self.os_ver = uname[2].split('-', 1)[0]

        arch = uname[-1]
        if re.match(r"i\d86", arch):
            self.arch = "x86"
        else:
            raise InternalError("unknown FreeBSD architecture: '%s'" % arch)

    def _init_openbsd(self):
        self.os = "openbsd"
        uname = os.uname()
        self.os_ver = uname[2].split('-', 1)[0]

        arch = uname[-1]
        if re.match(r"i\d86", arch):
            self.arch = "x86"
        elif arch == "amd64":
            self.arch = "x86_64"
        else:
            raise InternalError("unknown OpenBSD architecture: '%s'" % arch)

    def _init_netbsd(self):
        self.os = "netbsd"
        uname = os.uname()
        self.os_ver = uname[2].split('-', 1)[0]

        arch = uname[-1]
        if re.match(r"i\d86", arch):
            self.arch = "x86"
        else:
            raise InternalError("unknown NetBSD architecture: '%s'" % arch)

    def _init_linux(self):
        self.os = "linux"
        uname = os.uname()
        self.os_ver = uname[2].split('-', 1)[0]

        # Determine hardware type from 'uname -m' -- os.uname() is not
        # reliable: reports "i686" on iron (a Linux/IA64 box).
        o = os.popen('uname -m 2> /dev/null')
        arch = o.read().strip()
        o.close()
        if arch == "ia64":
            self.arch = "ia64"
        elif re.match("i\d86", arch):
            self.arch = "x86"
        elif arch == "x86_64":
            self.arch = "x86_64"
        elif arch == "ppc":
            self.arch = "ppc"
        else:
            raise InternalError("unknown Linux architecture: '%s'" % arch)
        self._set_linux_distro_info()
        lib_info = _get_linux_lib_info()
        if "libstdc++" in lib_info:
            self.libcpp = "libcpp" + lib_info["libstdc++"]
        if "libc" in lib_info:
            # For now, only the major 'libc' version number is used.
            self.libc = "libc" + lib_info["libc"].split('.')[0]
        if "glibc" in lib_info:
            self.glibc = "glibc"
            self.glibc_ver = lib_info["glibc"]

    def _init_solaris(self):
        self.os = "solaris"
        uname = os.uname()
        if uname[2].startswith("5."):
            self.os_ver = uname[2].split(".", 1)[1]
        else:
            raise InternalError("unknown Solaris version: '%s'" % uname[2])
        if uname[4].startswith("sun4"):
            self.arch = "sparc"
            arch_ver = _get_sparc_arch_ver()
            if arch_ver is not None:
                self.arch_ver = arch_ver
                if int(arch_ver) >= 9:
                    self.arch = "sparc64"
        elif uname[4].startswith("i86pc"):
            self.arch = "x86"
        else:
            raise InternalError("unknown Solaris architecture: '%s'" % uname[4])

    def _init_hpux(self):
        self.os = "hpux"
        uname = os.uname()

        if uname[4] == "ia64":
            self.arch = "ia64"
        elif uname[4] == "9000/800":
            self.arch = "parisc"
            self.arch_ver = _get_hpux_parisc_arch_ver()
        else:
            raise InternalError("unknown HP-UX architecture: '%s'" % uname[4])

        try:
            self.os_ver = uname[2].split('.', 1)[1]   # e.g. "B.11.00"
        except IndexError:
            raise InternalError("unknown HP-UX version: could not "
                                "parse '%s' from uname" % uname[2])

    def _init_aix(self):
        uname = os.uname()
        self.os = "aix"
        self.os_ver = "%s.%s" % (uname[3], uname[2])
                    
        # Determine processor type from 'uname -p' -- os.uname() does not
        # have this.
        o = os.popen('uname -p 2> /dev/null')
        arch = o.read().strip()
        o.close()

        # 32-bit or 64-bit?
        # Ideas from http://www.stata.com/support/faqs/win/64bit.html
        o = os.popen('getconf -v KERNEL_BITMODE 2> /dev/null')
        kbitmode = o.read().strip()
        o.close()
        if kbitmode:
            sixtyfour = '64' in kbitmode
        else:
            o = os.popen('file /usr/lib/boot/unix* 2> /dev/null')
            listing = o.read().strip()
            o.close()
            sixtyfour = '64-bit XCOFF executable' in listing

        self.arch = arch + (sixtyfour and '64' or '')


    def _init_mac(self):
        # Determine processor type from 'uname -p' -- os.uname() does not
        # have this.
        o = os.popen('uname -p 2> /dev/null')
        self.arch = {"powerpc": "powerpc",
                     "i386":    "x86"}[o.read().strip()]
        o.close()

        # Historically Python code (e.g. platform.py) has used the
        # Gestalt Manager to retrieve Mac OS system info. However, this
        # has problems on macosx-x86. As well,
        # <http://tinyurl.com/9ssrn> says this:
        #
        #   A better way to obtain version information in Mac OS X is to
        #   read the system version information from the following file:
        #
        #   /System/Library/CoreServices/SystemVersion.plist
        #
        # Note: If there end up being problems using this, then try
        # getting info from `sw_vers`.
        getters = [
            self._get_macos_ver_info_from_plist,
            self._get_macos_ver_info_from_gestalt,
        ]
        for getter in getters:
            ver_info = getter()
            if ver_info:
                assert "os_ver" in ver_info
                for n,v in ver_info.items():
                    setattr(self, n, v)
                if self.os_ver.startswith("10."):
                    self.os = "macosx"
                else:
                    self.os = "macos"
                break
        else:
            self.os = "macos"
            import warnings
            warnings.warn("could not determine Mac OS version info: "
                          "consider adding support for parsing `sw_vers`")

        self.darwin_ver = os.uname()[2] 

    def _get_macos_ver_info_from_gestalt(self):
        import gestalt
        import MacOS
        try:
            sysv = gestalt.gestalt("sysv")
        except MacOS.Error:
            # On Mac OS X/Intel (at least on the early release dev
            # boxes) the Gestalt Manager does not seem to be intialized
            # with the standard selectors -- or the equivalent.
            # Requesting information on "sysv", "sysu", etc. all return:
            #   gestaltUndefSelectorErr (-5551)
            #   Specifies an undefined selector was passed to the
            #   Gestalt Manager.
            pass
        else:
            def bcd2int(bcd): return int(hex(bcd)[2:])
            major = bcd2int((sysv & 0xFF00) >> 8)
            minor = (sysv & 0x00F0) >> 4
            patch = (sysv & 0x000F)
            return {
                "os_ver": "%d.%d.%d" % (major, minor, patch),
            }

    def _get_macos_ver_info_from_plist(self):
        """Retrive Mac OS system information from
            /System/Library/CoreServices/SystemVersion.plist
        as suggested here:
            http://tinyurl.com/9ssrn
        """
        plist_path = "/System/Library/CoreServices/SystemVersion.plist"
        if not exists(plist_path):
            return
        try:
            from plistlib import Plist
        except ImportError:
            return
        plist = Plist.fromFile(plist_path)
        return {
            "os_ver": plist["ProductVersion"],
            "os_build": plist["ProductBuildVersion"],
            "os_name": plist["ProductName"],
        }

    def _get_linux_lsb_release_info(self):
        """Some Linux distros have a `lsb_release` that provides some
        data about the distro.
        """
        try:
            try:
                import subprocess
            except ImportError:
                i,o,e = os.popen3("lsb_release --all")
                i.close()
                stdout = o.read()
                stderr = e.read()
                o.close()
                retval = e.close()
            else:
                p = subprocess.Popen(["lsb_release", "--all"],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                stdout, stderr = p.communicate()
                retval = p.wait()
            stdout = stdout.decode('utf-8')
        except OSError:
            # Can happen if "lsb_release" did not exist, bug 82403.
            retval = 1   # an error

        d = {}
        if retval:
            return {} # Running lsb_release failed
        patterns = {
            "distro": re.compile("^Distributor ID:\s+(.*?)\s*$"),
            "distro_desc": re.compile("^Description:\s+(.*?)\s*$"),
            "distro_ver": re.compile("^Release:\s+(.*?)\s*$"),
            "distro_codename": re.compile("^Codename:\s+(.*?)\s*$"),
            "lsb_version": re.compile("^LSB Version:\s+(.*?)\s*$"),
        }
        for line in stdout.splitlines(0):
            for name, pattern in patterns.items():
                match = pattern.search(line)
                if match:
                    value = match.group(1)
                    if value != "n/a":
                        d[name] = value
        return d

    def _get_linux_release_file_info(self):
        try:
            etc_files = os.listdir("/etc")
        except EnvironmentError:
            return {}

        # A good list of release-files for various Linux distros is
        # here: http://linuxmafia.com/faq/Admin/release-files.html
        d = {}
        release_file_pat = re.compile(r'(\w+)[-_](release|version)')
        candidate_etc_files = []
        for etc_file in etc_files:
            m = release_file_pat.match(etc_file)
            if m:
                candidate_etc_files.append((m.group(1), "/etc/"+etc_file))
        if not candidate_etc_files:
            return {}

        patterns = {
            "redhat": re.compile("^Red Hat Linux release ([\d\.]+)"),
            # As of release 7, "Fedora Core" is not called "Fedora".
            "fedora": re.compile("^Fedora release ([\d\.]+)"),
            "fedoracore": re.compile("^Fedora Core release ([\d\.]+)"),
            "mandrake": re.compile("^Mandrake Linux release ([\d\.]+)"),
            # Ignoring the different RHEL flavours (AS, ES, WS) for now.
            "rhel": re.compile("^Red Hat Enterprise Linux \w{2} release ([\d\.]+)"),
            "suse": re.compile("^SuSE Linux ([\d\.]+)"),
            "opensuse": re.compile("^openSUSE ([\d\.]+)"),
            "debian": re.compile("^([\d\.]+)"),
            "slackware": re.compile("^Slackware ([\d\.]+)"),
            "gentoo": re.compile("^Gentoo Base System release ([\d\.]+)"),
        }

        errmsgs = []
        for distro_family, etc_path in candidate_etc_files:
            f = open(etc_path, 'r')
            first_line = f.readline().rstrip()
            f.close()
            for distro, pattern in patterns.items():
                m = pattern.search(first_line)
                if m:
                    d["distro_family"] = distro_family
                    d["distro"] = distro
                    d["distro_ver"] = m.group(1)
                    if first_line.strip() != m.group(1):
                        d["distro_desc"] = first_line.strip()
                    break
            errmsgs.append("first line of '%s' (%s)" % (etc_path, first_line))
            if d:
                break
        else:            
            # If we have a release-file, just fill in "distro_family"
            # and "distro" and move on. For example, Arch Linux's
            # release file is *empty*.
            if candidate_etc_files:
                d["distro_family"] = distro_family = candidate_etc_files[0][0]
                d["distro"] = distro_family.lower()
                warnings.warn("could not determine linux distro_ver %s"
                                % " or ".join(errmsgs),
                              LinuxDistroVersionWarning)
        return d

    def _set_linux_distro_info(self):
        """Determine the following Linux distribution information:

            distro
            distro_ver (maybe)
            distro_family (maybe)
                Distro families are "redhat", "debian", and "suse". For
                example, Mandrake Linux is a member of the "redhat"
                distro family, Ubuntu is a member of the "debian" family.
            distro_codename (maybe)
            distro_description (maybe)
        """
        assert sys.platform.startswith("linux")

        # First try to use `lsb_release`.
        # - Ubuntu Linux includes "/etc/debian_version" but has its
        #   useful version info in "/etc/lsb-release" (best parsed from
        #   `lsb_release`).
        # - Is there reason to prefer "/etc/foo[-_](release|version)"
        #   info over `lsb_release` for any Linux distro?
        d = self._get_linux_lsb_release_info()
        if "distro" in d and "distro_ver" in d:
            for k, v in d.items():
                setattr(self, k, v)
            return

        # Then try to find a release/version file in "/etc".
        # - Algorithm borrows from platform.py.
        d = self._get_linux_release_file_info()
        if "distro" in d:
            for k, v in d.items():
                setattr(self, k, v)
            return

        # Then try to use Python's platform.py to help.
        try:
            import platform
        except ImportError:
            pass
        else:
            distro, distro_ver, distro_id = platform.dist()
            if distro and distro_ver:
                self.distro = distro
                self.distro_ver = distro_ver
                return

        raise InternalError("unknown Linux distro: no `lsb_release`, "
                            "couldn't find a '/etc/*[-_](version|release)' "
                            "file, and Python's platform.py couldn't "
                            "identify the distro either")


def platname(*rules, **kwargs):
    """platname([rules...]) --> platform name

    Return a string representation for this platform.

    Keyword args:
      'sep' is a string to use for joining platform data.
      'errors' is a string defining what to do if a given platform
          data does not exist for this platform. E.g. if the rule
          "distro" is given for Windows. Valid values are:
              "ignore" - just skip that datum (this is the default)
              "strict" - raise an Error
      'filesafe' is a boolean (default False) indicating if the
          returned name should be made safe for usage in a filename.
      'lower' is a boolean (default True) indicating if the returned
          name should be lowercased.

    Rule Syntax:
      os              # just a datum name
      os+os_ver       # use '+' to join without the 'sep'
    """
    return PlatInfo().name(*rules, **kwargs)



#---- internal support stuff

# Note: Not using `subprocess.CalledProcessError` because that isn't in
# Python 2.4.
class RunError(Exception): pass
class ExecutableNotFoundError(RunError): pass
class NonZeroReturnCodeError(RunError): pass

def _run(args, ignore_stderr=False):
    """Run the given command.

    @param args {str|list} Command strong or sequence of program arguments. The
          program to execute is normally the first item in the args
          sequence or the string if a string is given.
    @param ignore_stderr {bool} If True, return only stdout; otherwise
        return both stdout and stderr combined (2>&1)
    @returns {str} The program output.
    @raises {RunError} `ExecutableNotFoundError` or `NonZeroReturnCodeError`.
    """
    if ignore_stderr:
        stderr_pipe = subprocess.PIPE
    else:
        stderr_pipe = subprocess.STDOUT
        
    try:
        p = subprocess.Popen(args=args, 
                shell=False, # prevent obtrusive shell warnings
                stdout=subprocess.PIPE, stderr=stderr_pipe)
    except OSError:
        _, e, _ = sys.exc_info()
        if e.errno == errno.ENOENT:
            # `exe` not found
            raise ExecutableNotFoundError('The command "%s" cannot be run: %s'
                % (args, e))
        raise

    stdout, stderr = p.communicate()
    if p.returncode:
        raise NonZeroReturnCodeError('"%s" returned non-zero return code (%d)' 
            % (args, p.returncode))
    return stdout


# Recipe: ver (0.1) in C:\trentm\tm\recipes\cookbook
def _split_ver(ver_str):
    """Parse the given version into a tuple of "significant" parts.
   
        >>> _split_ver("4.1.0")
        ('4', '1', '0')
        >>> _split_ver("1.3a2")
        ('1', '3', 'a', '2')
    """
    bits = [b for b in re.split("(\.|[a-z])", ver_str) if b != '.']
    return tuple(bits)

def _join_ver(ver_tuple):
    """Join the given version-tuple, inserting '.' as appropriate.
    
        >>> _join_ver( ('4', '1', '0') )
        "4.1.0"
        >>> _join_ver( ('1', '3', 'a', '2') )
        "1.3a2"
    """
    def isint(s):
        try:
            int(s)
        except ValueError:
            return False
        else:
            return True

    dotted = []
    for bit in ver_tuple:
        if dotted and isint(dotted[-1]) and isint(bit):
            dotted.append('.')
        dotted.append(str(bit))
    return ''.join(dotted)

def _get_linux_lib_info():
    """Return a dict of default lib versions for a build on linux.
    
    For example:
        {"libstdc++": "5", "libc": "6", "glibc": "2.3.3"}
    
    The 'glibc' version is only returned if 'libc' is >=6.
    
    Some notes on Linux libc versions
    ---------------------------------
    
    From http://sourceware.org/glibc/glibc-faq.html#s-2.1
        libc-4      a.out libc
        libc-5      original ELF libc