Skip to content
tools.py 24.9 KiB
Newer Older
Seblu's avatar
Seblu committed
# -*- python -*-
Seblu's avatar
Seblu committed
# -*- coding: utf-8 -*-
# This file is part of Installsystems.
# Installsystems is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Installsystems is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with Installsystems.  If not, see <http://www.gnu.org/licenses/>.
Seblu's avatar
Seblu committed

'''
InstallSystems Generic Tools Library
'''

Sébastien Luttringer's avatar
Sébastien Luttringer committed
from hashlib import md5
from installsystems import VERSION, CANONICAL_NAME
from installsystems.exception import ISError
from installsystems.printer import VERBOSITY, warn, debug, arrow
from itertools import takewhile
Sébastien Luttringer's avatar
Sébastien Luttringer committed
from jinja2 import Template
from locale import getpreferredencoding
from math import log
from os import environ, pathsep, walk, rename, symlink, unlink
from os import stat, lstat, fstat, makedirs, chown, chmod, utime
from os.path import exists, join, isdir, ismount, splitext
from progressbar import Bar, BouncingBar, ETA, UnknownLength
Sébastien Luttringer's avatar
Sébastien Luttringer committed
from progressbar import FileTransferSpeed
from progressbar import Widget, ProgressBar, Percentage
from re import match, compile
from shutil import copy
from socket import getdefaulttimeout
from stat import S_ISDIR, S_ISREG
from subprocess import call, check_call, CalledProcessError
from time import mktime, gmtime, strftime, strptime
from urllib2 import urlopen, Request
Seblu's avatar
Seblu committed

################################################################################
# Classes
################################################################################

class PipeFile(object):
Seblu's avatar
Seblu committed
    '''
Aurélien Dunand's avatar
Aurélien Dunand committed
    Pipe file object if a file object with extended capabilities
    like printing progress bar or compute file size, md5 on the fly
Seblu's avatar
Seblu committed
    '''
    class FileTransferSize(Widget):
        '''
        Custom progressbar widget
        Widget for showing the transfer size (useful for file transfers)
        '''

        format = '%6.2f %s%s'
        prefixes = ' kMGTPEZY'
        __slots__ = ('unit', 'format')

        def __init__(self, unit='B'):
            self.unit = unit

        def update(self, pbar):
            '''
            Updates the widget with the current SI prefixed speed
            '''
            if pbar.currval < 2e-6: # =~ 0
                scaled = power = 0
            else:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                power = int(log(pbar.currval, 1000))
                scaled = pbar.currval / 1000.**power
            return self.format % (scaled, self.prefixes[power], self.unit)


    def __init__(self, path=None, mode="r", fileobj=None, timeout=None,
                 progressbar=False):
        self.open(path, mode, fileobj, timeout, progressbar)
    def open(self, path=None, mode="r", fileobj=None, timeout=None, progressbar=False):
        if path is None and fileobj is None:
            raise AttributeError("You must have a path or a fileobj to open")
        if mode not in ("r", "w"):
            raise AttributeError("Invalid open mode. Must be r or w")
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        self.timeout = timeout or getdefaulttimeout()
        self.mode = mode
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        self._md5 = md5()
        self.size = 0
        self.mtime = None
        self.consumed_size = 0
Aurélien Dunand's avatar
Aurélien Dunand committed
        # we already have a fo, nothing to open
        if fileobj is not None:
            self.fo = fileobj
            # seek to 0 and compute filesize if we have and fd
            if hasattr(self.fo, "fileno"):
                self.seek(0)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                self.size = fstat(self.fo.fileno()).st_size
        # we need to open the path
        else:
            ftype = pathtype(path)
            if ftype == "file":
                self._open_local(path)
            elif ftype == "http":
                self._open_http(path)
            elif ftype == "ftp":
                self._open_ftp(path)
            elif ftype == "ssh":
                self._open_ssh(path)
                raise ISError("URL type not supported")
        # init progress bar
        # we use 0 because a null file is cannot show a progression during write
        if self.size == 0:
            widget = [ self.FileTransferSize(), " ",
                       BouncingBar(), " ", FileTransferSpeed() ]
            maxval = UnknownLength
        else:
            widget = [ Percentage(), " ", Bar(), " ", FileTransferSpeed(), " ", ETA() ]
            maxval = self.size
        self._progressbar = ProgressBar(widgets=widget, maxval=maxval)
        # enable displaying of progressbar
        self.progressbar = progressbar

    def _open_local(self, path):
        '''
        Open file on the local filesystem
        '''
        self.fo = open(path, self.mode)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        sta = fstat(self.fo.fileno())
        self.size = sta.st_size
        self.mtime = sta.st_mtime

    def _open_http(self, path):
        '''
        Open a file accross an http server
        '''
        try:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            headers = {"User-Agent": "%s v%s" % (CANONICAL_NAME, VERSION)}
            request = Request(path, None, headers)
            self.fo = urlopen(request, timeout=self.timeout)
        except Exception as e:
            raise ISError("Unable to open %s" % path, e)
        # get file size
        if "Content-Length" in self.fo.headers:
            self.size = int(self.fo.headers["Content-Length"])
        else:
            self.size = 0
        # get mtime
        try:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            self.mtime = int(mktime(strptime(self.fo.headers["Last-Modified"],
                                                       "%a, %d %b %Y %H:%M:%S %Z")))
        except:
            self.mtime = None

    def _open_ftp(self, path):
        '''
        Open file via ftp
        '''
        try:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            self.fo = urlopen(path, timeout=self.timeout)
        except Exception as e:
            raise ISError("Unable to open %s" % path, e)
        # get file size
        try:
            self.size = int(self.fo.headers["content-length"])
        except:
            self.size = 0

    def _open_ssh(self, path):
        '''
        Open current fo from an ssh connection
        '''
        # try to load paramiko
        try:
            import paramiko
        except ImportError:
            raise ISError("URL type not supported. Paramiko is missing")
        # parse url
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        (login, passwd, host, port, path) = match(
            "ssh://(([^:]+)(:([^@]+))?@)?([^/:]+)(:(\d+))?(/.*)?", path).group(2, 4, 5, 7, 8)
        if port is None: port = 22
        if path is None: path = "/"
        try:
            # open ssh connection
            # we need to keep it inside the object unless it was cutted
            self._ssh = paramiko.SSHClient()
            self._ssh.load_system_host_keys()
            self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            # Here there is a bug arround conect with allow_agent if agent is not able to open with a key
            self._ssh.connect(host, port=port, username=login, password=passwd, allow_agent=True,
                              look_for_keys=True, timeout=self.timeout)
            # swith in sftp mode
            sftp = self._ssh.open_sftp()
            # get the file infos
            sta = sftp.stat(path)
            self.size = sta.st_size
            self.mtime = sta.st_mtime
            # open the file
            self.fo = sftp.open(path, self.mode)
            # this is needed to have correct file transfert speed
            self.fo.set_pipelined(True)
        except Exception as e:
            # FIXME: unable to open file
            raise ISError(e)

    def close(self):
        if self.progressbar:
            self._progressbar.finish()
        debug(u"MD5: %s" % self.md5)
        debug(u"Size: %s" % self.consumed_size)
        self.fo.close()

    def read(self, size=None):
        if self.mode == "w":
            raise ISError("Unable to read in w mode")
        buf = self.fo.read(size)
        length = len(buf)
        self._md5.update(buf)
        self.consumed_size += length
        if self.progressbar and length > 0:
            self._progressbar.update(self.consumed_size)
        return buf

    def flush(self):
        if hasattr(self.fo, "flush"):
            return self.fo.flush()

    def write(self, buf):
        if self.mode == "r":
            raise ISError("Unable to write in r mode")
        self.fo.write(buf)
        length = len(buf)
        self._md5.update(buf)
        self.consumed_size += length
        if self.progressbar and length > 0:
            self._progressbar.update(self.consumed_size)
        return None

    def consume(self, fo=None):
        if PipeFile is in read mode:
          Read all data from PipeFile and write it to fo
          if fo is None, data are discarded. This is useful to obtain md5 and size
        if PipeFile is in write mode:
          Read all data from fo and write it to PipeFile
        '''
        if self.mode == "w":
            if fo is None:
                raise TypeError("Unable to consume NoneType")
            while True:
                buf = fo.read(1048576) # 1MiB
                if len(buf) == 0:
                    break
                self.write(buf)
        else:
            while True:
                buf = self.read(1048576) # 1MiB
                if len(buf) == 0:
                    break
                if fo is not None:
                    fo.write(buf)

    @property
    def progressbar(self):
        '''
        Return is progressbar have been started
        '''
        return hasattr(self, "_progressbar_started")

    @progressbar.setter
    def progressbar(self, val):
        '''
        Set this property to true enable progress bar
        '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        if VERBOSITY == 0:
        if val == True and not hasattr(self, "_progressbar_started"):
            self._progressbar_started = True
            self._progressbar.start()

    @property
    def md5(self):
        '''
        Return the md5 of read/write of the file
        '''
        return self._md5.hexdigest()

    @property
    def read_size(self):
        '''
        Return the current read size
        '''
        return self.consumed_size

    @property
    def write_size(self):
        '''
        Return the current wrote size
        '''
        return self.consumed_size

################################################################################
# Functions
################################################################################
def smd5sum(buf):
    '''
    Compute md5 of a string
    '''
    if isinstance(buf, unicode):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        buf = buf.encode(getpreferredencoding())
    m = md5()
    m.update(buf)
    return m.hexdigest()

def mkdir(path, uid=None, gid=None, mode=None):
Seblu's avatar
Seblu committed
    '''
    Create a directory and set rights
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    makedirs(path)
    chrights(path, uid, gid, mode)

Seblu's avatar
Seblu committed
def chrights(path, uid=None, gid=None, mode=None, mtime=None):
Seblu's avatar
Seblu committed
    '''
    Set rights on a file
    '''
    if uid is not None:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        chown(path, uid, -1)
    if gid is not None:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        chown(path, -1, gid)
    if mode is not None:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        chmod(path, mode)
Seblu's avatar
Seblu committed
    if mtime is not None:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        utime(path, (mtime, mtime))

def pathtype(path):
Seblu's avatar
Seblu committed
    '''
Aurélien Dunand's avatar
Aurélien Dunand committed
    Return path type. This is useful to know what kind of path is given
Seblu's avatar
Seblu committed
    '''
Seblu's avatar
Seblu committed
    if path.startswith("http://") or path.startswith("https://"):
        return "http"
    if path.startswith("ftp://") or path.startswith("ftps://"):
        return "ftp"
Seblu's avatar
Seblu committed
    elif path.startswith("ssh://"):
        return "ssh"
Seblu's avatar
Seblu committed
    else:
Seblu's avatar
Seblu committed
        return "file"

def pathsearch(name, path=None):
    '''
    Search PATH for a binary
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    path = path or environ["PATH"]
    for d in path.split(pathsep):
        if exists(join(d, name)):
            return join(abspath(d), name)
Seblu's avatar
Seblu committed
def isfile(path):
    '''
    Return True if path is of type file
    '''
    return pathtype(path) == "file"

def abspath(path):
Seblu's avatar
Seblu committed
    '''
    Format a path to be absolute
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    import os
    ptype = pathtype(path)
    if ptype in ("http", "ftp", "ssh"):
Seblu's avatar
Seblu committed
        return path
    elif ptype == "file":
        if path.startswith("file://"):
Aurélien Dunand's avatar
Aurélien Dunand committed
            path = path[len("file://"):]
Seblu's avatar
Seblu committed
        return os.path.abspath(path)
    else:
        return None
Seblu's avatar
Seblu committed
def getsize(path):
Seblu's avatar
Seblu committed
    '''
    Get size of a path. Recurse if directory
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    import os
Seblu's avatar
Seblu committed
    total_sz = os.path.getsize(path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    if isdir(path):
        for root, dirs, files in walk(path):
Seblu's avatar
Seblu committed
            for filename in dirs + files:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                filepath = join(root, filename)
                filestat = lstat(filepath)
                if S_ISDIR(filestat.st_mode) or S_ISREG(filestat.st_mode):
Seblu's avatar
Seblu committed
                    total_sz += filestat.st_size
    return total_sz
def human_size(num, unit='B'):
    '''
    Return human readable size
    '''
    prefixes = ('','Ki', 'Mi', 'Gi', 'Ti','Pi', 'Ei', 'Zi', 'Yi')
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    power = int(log(num, 1024))
    # max is YiB
    if power >= len(prefixes):
        power = len(prefixes) - 1
    scaled = num / float(1024 ** power)
    return u"%3.1f%s%s" % (scaled, prefixes[power], unit)
def time_rfc2822(timestamp):
    '''
    Return a rfc2822 format time string from an unix timestamp
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    return strftime("%a, %d %b %Y %H:%M:%S %z", gmtime(timestamp))
def guess_distro(path):
Sebastien Luttringer's avatar
Sebastien Luttringer committed
    '''
    Try to detect which distro is inside a directory
Sebastien Luttringer's avatar
Sebastien Luttringer committed
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    if exists(join(path, "etc", "debian_version")):
        return "debian"
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    elif exists(join(path, "etc", "arch-release")):
        return "archlinux"
    return None

def prepare_chroot(path, mount=True):
    '''
    Prepare a chroot environment by mounting /{proc,sys,dev,dev/pts}
Aurélien Dunand's avatar
Aurélien Dunand committed
    and try to guess dest os to avoid daemon launching
    # try to mount /proc /sys /dev /dev/pts /dev/shm
Sebastien Luttringer's avatar
Sebastien Luttringer committed
    if mount:
        mps = ("proc", "sys", "dev", "dev/pts", "dev/shm")
Sebastien Luttringer's avatar
Sebastien Luttringer committed
        for mp in mps:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            target = join(path, mp)
            if ismount(target):
                warn(u"%s is already a mountpoint, skipped" % target)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            elif ismount(origin) and isdir(target):
                arrow(u"%s -> %s" % (origin, target), 1)
Sebastien Luttringer's avatar
Sebastien Luttringer committed
                try:
                    check_call(["mount",  "--bind", origin, target], close_fds=True)
                except CalledProcessError as e:
                    warn(u"Mount failed: %s.\n" % e)
    # check path is a kind of linux FHS
    if not exists(join(path, "etc")) or not exists(join(path, "usr")):
        resolv_path = join(path, "etc", "resolv.conf")
        resolv_backup_path = join(path, "etc", "resolv.conf.isbackup")
        resolv_trick_path = join(path, "etc", "resolv.conf.istrick")
        if (exists("/etc/resolv.conf")
            and not exists(resolv_backup_path)
            and not exists(resolv_trick_path)):
            arrow("resolv.conf", 1)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                rename(resolv_path, resolv_backup_path)
            else:
                open(resolv_trick_path, "wb")
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            copy("/etc/resolv.conf", resolv_path)
        warn(u"resolv.conf tricks fail: %s" % e)
        mtab_path = join(path, "etc", "mtab")
        mtab_backup_path = join(path, "etc", "mtab.isbackup")
        mtab_trick_path = join(path, "etc", "mtab.istrick")
        if not exists(mtab_backup_path) and not exists(mtab_trick_path):
            arrow("mtab", 1)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            if exists(mtab_path):
                rename(mtab_path, mtab_backup_path)
            symlink("/proc/self/mounts", mtab_path)
    except Exception as e:
        warn(u"mtab tricks fail: %s" % e)
    # try to guest distro
    distro = guess_distro(path)
Sebastien Luttringer's avatar
Sebastien Luttringer committed
    # in case of debian disable policy
    if distro == "debian":
        arrow("Debian specific", 1)
Sebastien Luttringer's avatar
Sebastien Luttringer committed
        # create a chroot header
        try: open(join(path, "etc", "debian_chroot"), "w").write("CHROOT")
Sebastien Luttringer's avatar
Sebastien Luttringer committed
        except: pass
        # fake policy-rc.d. It must exit 101, it's an expected exitcode.
        policy_path = join(path, "usr", "sbin", "policy-rc.d")
        try: open(policy_path, "w").write("#!/bin/bash\nexit 101\n")
Sebastien Luttringer's avatar
Sebastien Luttringer committed
        except: pass
        # policy-rc.d needs to be executable
        chrights(policy_path, mode=0755)

def unprepare_chroot(path, mount=True):
    '''
    Rollback preparation of a chroot environment inside a directory
    '''
    # check path is a kind of linux FHS
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    if exists(join(path, "etc")) and exists(join(path, "usr")):
        # untrick mtab
        mtab_path = join(path, "etc", "mtab")
        mtab_backup_path = join(path, "etc", "mtab.isbackup")
        mtab_trick_path = join(path, "etc", "mtab.istrick")
        if exists(mtab_backup_path) or exists(mtab_trick_path):
            arrow("mtab", 1)
            # order matter !
            if exists(mtab_trick_path):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                try: unlink(mtab_path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    unlink(mtab_trick_path)
                    warn(u"Unable to remove %s" % mtab_trick_path)
            if exists(mtab_backup_path):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                try: unlink(mtab_path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    rename(mtab_backup_path, mtab_path)
                    warn(u"Unable to restore %s" % mtab_backup_path)
        # untrick resolv.conf
        resolv_path = join(path, "etc", "resolv.conf")
        resolv_backup_path = join(path, "etc", "resolv.conf.isbackup")
        resolv_trick_path = join(path, "etc", "resolv.conf.istrick")
        if exists(resolv_backup_path) or exists(resolv_trick_path):
            arrow("resolv.conf", 1)
            # order matter !
            if exists(resolv_trick_path):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                try: unlink(resolv_path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    unlink(resolv_trick_path)
                    warn(u"Unable to remove %s" % resolv_trick_path)
            if exists(resolv_backup_path):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                try: unlink(resolv_path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    rename(resolv_backup_path, resolv_path)
                    warn(u"Unable to restore %s" % resolv_backup_path)
        # try to guest distro
        distro = guess_distro(path)
        # cleaning debian stuff
        if distro == "debian":
            arrow("Debian specific", 1)
            for f in ("etc/debian_chroot", "usr/sbin/policy-rc.d"):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                try: unlink(join(path, f))
Sebastien Luttringer's avatar
Sebastien Luttringer committed
    # unmounting
    if mount:
        mps = ("proc", "sys", "dev", "dev/pts", "dev/shm")
Sebastien Luttringer's avatar
Sebastien Luttringer committed
        for mp in reversed(mps):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            if ismount(target):
Sebastien Luttringer's avatar
Sebastien Luttringer committed
                arrow(target, 1)
                call(["umount", target], close_fds=True)

def chroot(path, shell="/bin/bash", mount=True):
    '''
    Chroot inside a directory and call shell
    if mount is true, mount /{proc,dev,sys} inside the chroot
    '''
    # prepare to chroot
    prepare_chroot(path, mount)
    # chrooting
    arrow(u"Chrooting inside %s and running %s" % (path, shell))
    call(["chroot", path, shell], close_fds=True)
    # revert preparation of chroot
    unprepare_chroot(path, mount)
def is_version(version):
    '''
    Check if version is valid
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    if match("^(\d+)(?:([-~+]).*)?$", version) is None:
        raise TypeError(u"Invalid version format %s" % version)
def compare_versions(v1, v2):
    '''
    This function compare version :param v1: and version :param v2:
    Compare v1 and v2
    return > 0 if v1 > v2
    return < 0 if v2 > v1
    return = 0 if v1 == v2
    '''

    # Ensure versions have the right format
    for version in v1, v2:
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        iv = match("^(\d+(?:\.\d+)*)(?:([~+]).*)?$", str(version))
        if iv is None:
            raise TypeError(u"Invalid version format: %s" % version)

Sébastien Luttringer's avatar
Sébastien Luttringer committed
    digitregex = compile(r'^([0-9]*)(.*)$')
    nondigitregex = compile(r'^([^0-9]*)(.*)$')

    digits = True
    while v1 or v2:
        pattern = digitregex if digits else nondigitregex
        sub_v1, v1 = pattern.findall(str(v1))[0]
        sub_v2, v2 = pattern.findall(str(v2))[0]

        if digits:
            sub_v1 = int(sub_v1 if sub_v1 else 0)
            sub_v2 = int(sub_v2 if sub_v2 else 0)
            if sub_v1 < sub_v2:
                rv = -1
            elif sub_v1 > sub_v2:
                rv = 1
                rv = 0
            if rv != 0:
                return rv
            rv = strvercmp(sub_v1, sub_v2)
            if rv != 0:
                return rv

        digits = not digits
    return 0

def strvercmp(lhs, rhs):
    '''
    Compare string part of a version number
    '''
    size = max(len(lhs), len(rhs))
    lhs_array = str_version_array(lhs, size)
    rhs_array = str_version_array(rhs, size)
    if lhs_array > rhs_array:
        return 1
    elif lhs_array < rhs_array:
        return -1
    else:
        return 0

def str_version_array(str_version, size):
    '''
    Turns a string into an array of numeric values kind-of corresponding to
    the ASCII numeric values of the characters in the string.  I say 'kind-of'
    because any character which is not an alphabetic character will be
    it's ASCII value + 256, and the tilde (~) character will have the value
    -1.
    Additionally, the +size+ parameter specifies how long the array needs to
    be; any elements in the array beyond the length of the string will be 0.

    This method has massive ASCII assumptions. Use with caution.
    '''
    a = [0] * size
    for i, char in enumerate(str_version):
        char = ord(char)
        if ((char >= ord('a') and char <= ord('z')) or
            (char >= ord('A') and char <= ord('Z'))):
            a[i] = char
        elif char == ord('~'):
            a[i] = -1
        else:
            a[i] = char + 256
    return a

def get_compressor_path(name, compress=True, level=None):
    '''
    Return better compressor argv from its generic compressor name
    e.g: bzip2 can return pbzip2 if available or bzip2 if not
    '''
    compressors = {"none": [["cat"]],
                   "gzip": [["gzip", "--no-name", "--stdout"]],
                   "bzip2": [["pbzip2", "--stdout"],
                             ["bzip2", "--compress", "--stdout"]],
                   "xz": [["xz", "--compress", "--stdout"]]}
    decompressors = {"none": [["cat"]],
                     "gzip": [["gzip", "--decompress", "--stdout"]],
                     "bzip2": [["pbzip2","--decompress", "--stdout"],
                               ["bzip2", "--decompress", "--stdout"]],
                     "xz": [["xz", "--decompress", "--stdout"]]}
    # no compress level for decompression
    if not compress:
        level = None
    allcompressors = compressors if compress else decompressors
    # check compressor exists
    if name not in allcompressors.keys():
        raise ISError(u"Invalid compressor name: %s" % name)
    # get valid compressors
    for compressor in allcompressors[name]:
        path = pathsearch(compressor[0])
        if path is None:
            continue
        if level is not None:
            compressor.append("-%d" % level)
        return compressor
    raise ISError(u"No external decompressor for %s" % name)

def render_templates(target, context, tpl_ext=".istpl", force=False, keep=False):
    '''
    Render templates according to tpl_ext
    Apply template mode/uid/gid to the generated file
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    for path in walk(target):
Sébastien Luttringer's avatar
Sébastien Luttringer committed
            name, ext = splitext(filename)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                tpl_path = join(path[0], filename)
                file_path = join(path[0], name)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                if exists(file_path) and not force:
                    raise ISError(u"%s will be overwritten, cancel template "
                                  "generation (set force=True if you know "
                                  "what you do)" % file_path)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                        template = Template(tpl_file.read())
                        with open(file_path, "w") as rendered_file:
                            rendered_file.write(template.render(context))
                except Exception as e:
                    raise ISError(u"Render template fail", e)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                st = stat(tpl_path)
                chown(file_path, st.st_uid, st.st_gid)
                chmod(file_path, st.st_mode)
Sébastien Luttringer's avatar
Sébastien Luttringer committed
                    unlink(tpl_path)

def argv():
    '''
    Return system argv after an unicode transformation with locale preference
    '''
Sébastien Luttringer's avatar
Sébastien Luttringer committed
    from sys import argv
Sébastien Luttringer's avatar
Sébastien Luttringer committed
        return [unicode(x, encoding=getpreferredencoding()) for x in argv]
    except UnicodeDecodeError as e:
        raise ISError("Invalid character encoding in command line")

def strcspn(string, pred):
    '''
    Python implementation of libc strcspn
    '''
    return len(list(takewhile(lambda x: x not in pred, string)))