'''

Layout directory for buildozer:

    build/
        <targetname>/
            platform/ - all the platform files necessary
            app/ - compiled application


'''

__version__ = '0.4'

import fcntl
import os
import re
import shelve
import socket
import sys
import zipfile
from select import select
from sys import stdout, stderr, stdin, exit
from urllib import urlretrieve
from re import search
from ConfigParser import SafeConfigParser
from os.path import join, exists, dirname, realpath, splitext, expanduser
from subprocess import Popen, PIPE
from os import environ, unlink, rename, walk, sep, listdir, makedirs
from copy import copy
from shutil import copyfile, rmtree, copytree

# windows does not have termios...
try:
    import termios
    import tty
    has_termios = True
except ImportError:
    has_termios = False

RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;{0}m"
BOLD_SEQ = "\033[1m"
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
USE_COLOR = 'NO_COLOR' not in environ
# error, info, debug
LOG_LEVELS_C = (RED, BLUE, BLACK)
LOG_LEVELS_T = 'EID'


class BuildozerCommandException(Exception):
    pass


class Buildozer(object):

    standard_cmds = ('clean', 'update', 'debug', 'release', 'deploy', 'run')

    def __init__(self, filename='buildozer.spec', target=None):
        super(Buildozer, self).__init__()
        self.log_level = 1
        self.environ = {}
        self.specfilename = filename
        self.state = None
        self.build_id = None
        self.config = SafeConfigParser()
        self.config.getlist = self._get_config_list
        self.config.getdefault = self._get_config_default
        self.config.getbooldefault = self._get_config_bool

        if exists(filename):
            self.config.read(filename)
            self.check_configuration_tokens()


        try:
            self.log_level = int(self.config.getdefault(
                'buildozer', 'log_level', '1'))
        except:
            pass

        self.targetname = None
        self.target = None
        if target:
            self.set_target(target)

    def set_target(self, target):
        '''Set the target to use (one of buildozer.targets, such as "android")
        '''
        self.targetname = target
        m = __import__('buildozer.targets.{0}'.format(target),
                fromlist=['buildozer'])
        self.target = m.get_target(self)
        self.check_build_layout()
        self.check_configuration_tokens()
        self.target.check_configuration_tokens()

    def prepare_for_build(self):
        '''Prepare the build.
        '''
        assert(self.target is not None)
        if hasattr(self.target, '_build_prepared'):
            return

        self.info('Preparing build')

        self.info('Check requirements for {0}'.format(self.targetname))
        self.target.check_requirements()

        self.info('Install platform')
        self.target.install_platform()

        self.info('Check application requirements')
        self.check_application_requirements()

        self.info('Compile platform')
        self.target.compile_platform()

        # flag to prevent multiple build
        self.target._build_prepared = True

    def build(self):
        '''Do the build.

        The target can set build_mode to 'release' or 'debug' before calling
        this method.

        (:meth:`prepare_for_build` must have been call before.)
        '''
        assert(self.target is not None)
        assert(hasattr(self.target, '_build_prepared'))

        if hasattr(self.target, '_build_done'):
            return

        # increment the build number
        self.build_id = int(self.state.get('cache.build_id', '0')) + 1
        self.state['cache.build_id'] = str(self.build_id)
        # FIXME WHY the hell we need to close/reopen the state to sync the build
        # id ???
        self.state.close()
        self.state = shelve.open(join(self.buildozer_dir, 'state.db'))

        self.info('Build the application #{}'.format(self.build_id))
        self.build_application()

        self.info('Package the application')
        self.target.build_package()

        # flag to prevent multiple build
        self.target._build_done = True

    #
    # Log functions
    #

    def log(self, level, msg):
        if level > self.log_level:
            return
        if USE_COLOR:
            color = COLOR_SEQ.format(30 + LOG_LEVELS_C[level])
            print ''.join((RESET_SEQ, color, '# ', msg, RESET_SEQ))
        else:
            print LOG_LEVELS_T[level], msg

    def debug(self, msg):
        self.log(2, msg)

    def info(self, msg):
        self.log(1, msg)

    def error(self, msg):
        self.log(0, msg)

    #
    # Internal check methods
    #

    def checkbin(self, msg, fn):
        self.debug('Search for {0}'.format(msg))
        if exists(fn):
            return realpath(fn)
        for dn in environ['PATH'].split(':'):
            rfn = realpath(join(dn, fn))
            if exists(rfn):
                self.debug(' -> found at {0}'.format(rfn))
                return rfn
        raise Exception(msg + 'not found')

    def cmd(self, command, **kwargs):
        #print ' '.join(['{0}={1}'.format(*args) for args in
        #    self.environ.iteritems()])

        # prepare the environ, based on the system + our own env
        env = copy(environ)
        env.update(self.environ)

        # prepare the process
        kwargs.setdefault('env', env)
        kwargs.setdefault('stdout', PIPE)
        kwargs.setdefault('stderr', PIPE)
        kwargs.setdefault('close_fds', True)
        kwargs.setdefault('shell', True)
        kwargs.setdefault('show_output', self.log_level > 1)

        show_output = kwargs.pop('show_output')
        get_stdout = kwargs.pop('get_stdout', False)
        get_stderr = kwargs.pop('get_stderr', False)
        break_on_error = kwargs.pop('break_on_error', True)
        sensible = kwargs.pop('sensible', False)

        if not sensible:
            self.debug('Run {0!r}'.format(command))
        else:
            if type(command) in (list, tuple):
                self.debug('Run {0!r} ...'.format(command[0]))
            else:
                self.debug('Run {0!r} ...'.format(command.split()[0]))
        self.debug('Cwd {}'.format(kwargs.get('cwd')))

        # open the process
        process = Popen(command, **kwargs)

        # prepare fds
        fd_stdout = process.stdout.fileno()
        fd_stderr = process.stderr.fileno()
        fcntl.fcntl(
            fd_stdout, fcntl.F_SETFL,
            fcntl.fcntl(fd_stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
        fcntl.fcntl(
            fd_stderr, fcntl.F_SETFL,
            fcntl.fcntl(fd_stderr, fcntl.F_GETFL) | os.O_NONBLOCK)

        ret_stdout = [] if get_stdout else None
        ret_stderr = [] if get_stderr else None
        while True:
            readx = select([fd_stdout, fd_stderr], [], [])[0]
            if fd_stdout in readx:
                chunk = process.stdout.read()
                if chunk == '':
                    break
                if get_stdout:
                    ret_stdout.append(chunk)
                if show_output:
                    stdout.write(chunk)
            if fd_stderr in readx:
                chunk = process.stderr.read()
                if chunk == '':
                    break
                if get_stderr:
                    ret_stderr.append(chunk)
                if show_output:
                    stderr.write(chunk)

        stdout.flush()
        stderr.flush()

        process.communicate()
        if process.returncode != 0 and break_on_error:
            self.error('Command failed: {0}'.format(command))
            raise BuildozerCommandException()
        if ret_stdout:
            ret_stdout = ''.join(ret_stdout)
        if ret_stderr:
            ret_stderr = ''.join(ret_stderr)
        return (ret_stdout, ret_stderr, process.returncode)

    def check_configuration_tokens(self):
        '''Ensure the spec file is 'correct'.
        '''
        self.info('Check configuration tokens')
        get = self.config.getdefault
        errors = []
        adderror = errors.append
        if not get('app', 'title', ''):
            adderror('[app] "title" is missing')
        if not get('app', 'package.name', ''):
            adderror('[app] "package.name" is missing')
        if not get('app', 'source.dir', ''):
            adderror('[app] "source.dir" is missing')

        version = get('app', 'version', '')
        version_regex = get('app', 'version.regex', '')
        if not version and not version_regex:
            adderror('[app] One of "version" or "version.regex" must be set')
        if version and version_regex:
            adderror('[app] Conflict between "version" and "version.regex"'
                    ', only one can be used.')
        if version_regex and not get('app', 'version.filename', ''):
            adderror('[app] "version.filename" is missing'
                ', required by "version.regex"')

        orientation = get('app', 'orientation', 'landscape')
        if orientation not in ('landscape', 'portrait', 'all'):
            adderror('[app] "orientation" have an invalid value')

        if errors:
            self.error('{0} error(s) found in the buildozer.spec'.format(
                len(errors)))
            for error in errors:
                print error
            exit(1)


    def check_build_layout(self):
        '''Ensure the build (local and global) directory layout and files are
        ready.
        '''
        self.info('Ensure build layout')

        if not exists(self.specfilename):
            print 'No {0} found in the current directory. Abandon.'.format(
                    self.specfilename)
            exit(1)

        # create global dir
        self.mkdir(self.global_buildozer_dir)
        self.mkdir(self.global_cache_dir)

        # create local dir
        specdir = dirname(self.specfilename)
        self.mkdir(join(specdir, '.buildozer'))
        self.mkdir(join(specdir, 'bin'))
        self.mkdir(self.applibs_dir)
        self.state = shelve.open(join(self.buildozer_dir, 'state.db'))

        if self.targetname:
            target = self.targetname
            self.mkdir(join(self.global_platform_dir, target, 'platform'))
            self.mkdir(join(specdir, '.buildozer', target, 'platform'))
            self.mkdir(join(specdir, '.buildozer', target, 'app'))

    def check_application_requirements(self):
        '''Ensure the application requirements are all available and ready to be
        packaged as well.
        '''
        requirements = self.config.getlist('app', 'requirements', '')
        target_available_packages = self.target.get_available_packages()

        # remove all the requirements that the target can compile
        requirements = [x for x in requirements if x not in
                target_available_packages]

        # did we already installed the libs ?
        if exists(self.applibs_dir) and \
            self.state.get('cache.applibs', '') == requirements:
                self.debug('Application requirements already installed, pass')
                return

        # recreate applibs
        self.rmdir(self.applibs_dir)
        self.mkdir(self.applibs_dir)

        # ok now check the availability of all requirements
        for requirement in requirements:
            self._install_application_requirement(requirement)

        # everything goes as expected, save this state!
        self.state['cache.applibs'] = requirements

    def _install_application_requirement(self, module):
        self._ensure_virtualenv()
        self.debug('Install requirement {} in virtualenv'.format(module))
        self.cmd('pip-2.7 install --download-cache={} --target={} {}'.format(
                self.global_cache_dir, self.applibs_dir, module),
                env=self.env_venv,
                cwd=self.buildozer_dir)

    def _ensure_virtualenv(self):
        if hasattr(self, 'venv'):
            return
        self.venv = join(self.buildozer_dir, 'venv')
        if not self.file_exists(self.venv):
            self.cmd('virtualenv-2.7 --python=python2.7 ./venv',
                    cwd=self.buildozer_dir)

        # read virtualenv output and parse it
        output = self.cmd('bash -c "source venv/bin/activate && env"',
                get_stdout=True,
                cwd=self.buildozer_dir)
        self.env_venv = copy(self.environ)
        for line in output[0].splitlines():
            args = line.split('=', 1)
            if len(args) != 2:
                continue
            key, value = args
            if key in ('VIRTUAL_ENV', 'PATH'):
                self.env_venv[key] = value
        if 'PYTHONHOME' in self.env_venv:
            del self.env_venv['PYTHONHOME']

        # ensure any sort of compilation will fail
        self.env_venv['CC'] = '/bin/false'
        self.env_venv['CXX'] = '/bin/false'

    def mkdir(self, dn):
        if exists(dn):
            return
        self.debug('Create directory {0}'.format(dn))
        makedirs(dn)

    def rmdir(self, dn):
        if not exists(dn):
            return
        self.debug('Remove directory and subdirectory {}'.format(dn))
        rmtree(dn)

    def file_exists(self, *args):
        return exists(join(*args))

    def file_rename(self, source, target, cwd=None):
        if cwd:
            source = join(cwd, source)
            target = join(cwd, target)
        self.debug('Rename {0} to {1}'.format(source, target))
        rename(source, target)

    def file_copy(self, source, target, cwd=None):
        if cwd:
            source = join(cwd, source)
            target = join(cwd, target)
        self.debug('Copy {0} to {1}'.format(source, target))
        copyfile(source, target)

    def file_extract(self, archive, cwd=None):
        if archive.endswith('.tgz') or archive.endswith('.tar.gz'):
            # XXX tarfile doesn't work for NDK-r8c :(
            #tf = tarfile.open(archive, 'r:*')
            #tf.extractall(path=cwd)
            #tf.close()
            self.cmd('tar xzf {0}'.format(archive), cwd=cwd)
            return

        if archive.endswith('.tbz2') or archive.endswith('.tar.bz2'):
            # XXX same as before
            self.cmd('tar xjf {0}'.format(archive), cwd=cwd)
            return

        if archive.endswith('.zip'):
            archive = join(cwd, archive)
            zf = zipfile.ZipFile(archive)
            zf.extractall(path=cwd)
            zf.close()
            return

        raise Exception('Unhandled extraction for type {0}'.format(archive))

    def clean_platform(self):
        self.info('Clean the platform build directory')
        if not exists(self.platform_dir):
            return
        rmtree(self.platform_dir)

    def download(self, url, filename, cwd=None):
        def report_hook(index, blksize, size):
            if size <= 0:
                progression = '{0} bytes'.format(index * blksize)
            else:
                progression = '{0:.2f}%'.format(
                        index * blksize * 100. / float(size))
            print '- Download', progression, '\r',
            stdout.flush()

        url = url + filename
        if cwd:
            filename = join(cwd, filename)
        if self.file_exists(filename):
            unlink(filename)

        self.debug('Downloading {0}'.format(url))
        urlretrieve(url, filename, report_hook)
        return filename

    def get_version(self):
        c = self.config
        has_version = c.has_option('app', 'version')
        has_regex = c.has_option('app', 'version.regex')
        has_filename = c.has_option('app', 'version.filename')

        # version number specified
        if has_version:
            if has_regex or has_filename:
                raise Exception(
                    'version.regex and version.filename conflict with version')
            return c.get('app', 'version')

        # search by regex
        if has_regex or has_filename:
            if has_regex and not has_filename:
                raise Exception('version.filename is missing')
            if has_filename and not has_regex:
                raise Exception('version.regex is missing')

            fn = c.get('app', 'version.filename')
            with open(fn) as fd:
                data = fd.read()
                regex = c.get('app', 'version.regex')
                match = search(regex, data)
                if not match:
                    raise Exception(
                        'Unable to found capture version in {0}'.format(fn))
                version = match.groups()[0]
                self.debug('Captured version: {0}'.format(version))
                return version

        raise Exception('Missing version or version.regex + version.filename')

    def build_application(self):
        self._copy_application_sources()
        self._copy_application_libs()
        self._patch_application_sources()

    def _copy_application_sources(self):
        source_dir = realpath(self.config.getdefault('app', 'source.dir', '.'))
        include_exts = self.config.getlist('app', 'source.include_exts', '')
        exclude_exts = self.config.getlist('app', 'source.exclude_exts', '')
        app_dir = self.app_dir

        self.debug('Copy application source from {}'.format(source_dir))

        rmtree(self.app_dir)

        for root, dirs, files in walk(source_dir):
            # avoid hidden directory
            if True in [x.startswith('.') for x in root.split(sep)]:
                continue

            for fn in files:
                # avoid hidden files
                if fn.startswith('.'):
                    continue

                # filter based on the extension
                # TODO more filters
                basename, ext = splitext(fn)
                if ext:
                    ext = ext[1:]
                    if include_exts and ext not in include_exts:
                        continue
                    if exclude_exts and ext in exclude_exts:
                        continue

                sfn = join(root, fn)
                rfn = realpath(join(app_dir, root[len(source_dir) + 1:], fn))

                # ensure the directory exists
                dfn = dirname(rfn)
                self.mkdir(dfn)

                # copy!
                self.debug('Copy {0}'.format(sfn))
                copyfile(sfn, rfn)

    def _copy_application_libs(self):
        # copy also the libs
        copytree(self.applibs_dir, join(self.app_dir, '_applibs'))

    def _patch_application_sources(self):
        # patch the main.py
        main_py = join(self.app_dir, 'main.py')
        if not self.file_exists(main_py):
            self.error('Unable to patch main_py to add applibs directory.')
            return

        header = ('import sys, os; '
                  'sys.path = [os.path.join(os.path.dirname(__file__),'
                  '"_applibs")] + sys.path\n')
        with open(main_py, 'rb') as fd:
            data = fd.read()
        data = header + data
        with open(main_py, 'wb') as fd:
            fd.write(data)
        self.info('Patched main.py to include applibs')

    def namify(self, name):
        '''Return a "valid" name from a name with lot of invalid chars
        (allowed characters: a-z, A-Z, 0-9, -, _)
        '''
        return re.sub('[^a-zA-Z0-9_\-]', '_', name)

    @property
    def root_dir(self):
        return realpath(join(dirname(self.specfilename)))

    @property
    def buildozer_dir(self):
        return join(self.root_dir, '.buildozer')

    @property
    def bin_dir(self):
        return join(self.root_dir, 'bin')

    @property
    def platform_dir(self):
        return join(self.buildozer_dir, self.targetname, 'platform')

    @property
    def app_dir(self):
        return join(self.buildozer_dir, self.targetname, 'app')

    @property
    def applibs_dir(self):
        return join(self.buildozer_dir, 'applibs')

    @property
    def global_buildozer_dir(self):
        return join(expanduser('~'), '.buildozer')

    @property
    def global_platform_dir(self):
        return join(self.global_buildozer_dir, self.targetname, 'platform')

    @property
    def global_cache_dir(self):
        return join(self.global_buildozer_dir, 'cache')

    @property
    def package_full_name(self):
        package_name = self.config.getdefault('app', 'package.name', '')
        package_domain = self.config.getdefault('app', 'package.domain', '')
        if package_domain == '':
            return package_name
        return '{}.{}'.format(package_domain, package_name)


    #
    # command line invocation
    #

    def targets(self):
        for fn in listdir(join(dirname(__file__), 'targets')):
            if fn.startswith('.') or fn.startswith('__'):
                continue
            if not fn.endswith('.py'):
                continue
            target = fn[:-3]
            try:
                m = __import__('buildozer.targets.{0}'.format(target),
                        fromlist=['buildozer'])
                yield target, m
            except:
                raise
                pass

    def usage(self):
        print 'Usage: buildozer [--verbose] [target] [command1] [command2]'
        print
        print 'Available targets:'
        targets = list(self.targets())
        for target, m in targets:
            doc = m.__doc__.strip().splitlines()[0].strip()
            print '  {0:<18} {1}'.format(target, doc)

        print
        print 'Global commands (without target):'
        cmds = [x for x in dir(self) if x.startswith('cmd_')]
        for cmd in cmds:
            name = cmd[4:]
            meth = getattr(self, cmd)

            doc = [x for x in
                    meth.__doc__.strip().splitlines()][0].strip()
            print '  {0:<18} {1}'.format(name, doc)

        print
        print 'Target commands:'
        print '  clean              Clean the target environment'
        print '  update             Update the target dependencies'
        print '  debug              Build the application in debug mode'
        print '  release            Build the application in release mode'
        print '  deploy             Deploy the application on the device'
        print '  run                Run the application on the device'

        for target, m in targets:
            mt = m.get_target(self)
            commands = mt.get_custom_commands()
            if not commands:
                continue
            print
            print 'Target "{0}" commands:'.format(target)
            for command, doc in commands:
                doc = doc.strip().splitlines()[0].strip()
                print '  {0:<18} {1}'.format(command, doc)

        print


    def run_default(self):
        self.check_build_layout()
        if 'buildozer:defaultcommand' not in self.state:
            print 'No default command set.'
            print 'Use "buildozer setdefault <command args...>"'
            print 'Use "buildozer help" for a list of all commands"'
            exit(1)
        cmd = self.state['buildozer:defaultcommand']
        self.run_command(cmd)

    def run_command(self, args):
        while args:
            if not args[0].startswith('-'):
                break
            arg = args.pop(0)

            if arg in ('-v', '--verbose'):
                self.log_level = 2

            if arg in ('-h', '--help'):
                self.usage()
                exit(0)

            if arg == '--version':
                print 'Buildozer {0}'.format(__version__)
                exit(0)

        if not args:
            self.run_default()
            return

        command, args = args[0], args[1:]
        cmd = 'cmd_{0}'.format(command)

        # internal commands ?
        if hasattr(self, cmd):
            getattr(self, cmd)(*args)
            return

        # maybe it's a target?
        targets = [x[0] for x in self.targets()]
        if command not in targets:
            print 'Unknow command/target', command
            exit(1)

        self.set_target(command)
        self.target.run_commands(args)

    def cmd_init(self, *args):
        '''Create a initial buildozer.spec in the current directory
        '''
        if exists('buildozer.spec'):
            print 'ERROR: You already have a buildozer.spec file.'
            exit(1)
        copyfile(join(dirname(__file__), 'default.spec'), 'buildozer.spec')
        print 'File buildozer.spec created, ready to customize!'

    def cmd_clean(self, *args):
        '''Clean the whole Buildozer environment.
        '''
        pass

    def cmd_help(self, *args):
        '''Show the Buildozer help.
        '''
        self.usage()

    def cmd_setdefault(self, *args):
        '''Set the default command to do when to arguments are given
        '''
        self.check_build_layout()
        self.state['buildozer:defaultcommand'] = args

    def cmd_version(self, *args):
        '''Show the Buildozer version
        '''
        print 'Buildozer {0}'.format(__version__)

    #
    # Private
    #

    def _get_config_list(self, section, token, default=None):
        # monkey-patch method for ConfigParser
        # get a key as a list of string, seperated from the comma
        values = self.config.getdefault(section, token, '')
        if not values:
            return default
        values = values.split(',')
        if not values:
            return default
        return [x.strip() for x in values]

    def _get_config_default(self, section, token, default=None):
        # monkey-patch method for ConfigParser
        # get a key in a section, or the default
        if not self.config.has_section(section):
            return default
        if not self.config.has_option(section, token):
            return default
        return self.config.get(section, token)

    def _get_config_bool(self, section, token, default=False):
        # monkey-patch method for ConfigParser
        # get a key in a section, or the default
        if not self.config.has_section(section):
            return default
        if not self.config.has_option(section, token):
            return default
        return self.config.getboolean(section, token)

class BuildozerRemote(Buildozer):
    def run_command(self, args):
        while args:
            if not args[0].startswith('-'):
                break
            arg = args.pop(0)

            if arg in ('-v', '--verbose'):
                self.log_level = 2

            if arg in ('-h', '--help'):
                self.usage()
                exit(0)

            if arg == '--version':
                print 'Buildozer (remote) {0}'.format(__version__)
                exit(0)

        if len(args) < 2:
            self.usage()
            return

        remote_name = args[0]
        remote_section = 'remote:{}'.format(remote_name)
        if not self.config.has_section(remote_section):
            self.error('Unknow remote "{}", must be configured first.'.format(
                remote_name))
            return

        self.remote_host = remote_host = self.config.get(
                remote_section, 'host', '')
        self.remote_user = remote_user = self.config.get(
                remote_section, 'user', '')
        self.remote_build_dir = remote_build_dir = self.config.get(
                remote_section, 'build_directory', '')
        if not remote_host:
            self.error('Missing "host = " for {}'.format(remote_section))
            return
        if not remote_user:
            self.error('Missing "user = " for {}'.format(remote_section))
            return
        if not remote_build_dir:
            self.error('Missing "build_directory = " for {}'.format(remote_section))
            return

        # fake the target
        self.targetname = 'remote'
        self.check_build_layout()

        # prepare our source code
        self.info('Prepare source code to sync')
        self._copy_application_sources()
        self._ssh_connect()
        try:
            self._ensure_buildozer()
            self._sync_application_sources()
            self._do_remote_commands(args[1:])
            self._ssh_sync(os.getcwd(), mode='get')
        finally:
            self._ssh_close()

    def _ssh_connect(self):
        self.info('Connecting to {}'.format(self.remote_host))
        import paramiko
        self._ssh_client = client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.load_system_host_keys()
        client.connect(self.remote_host, username=self.remote_user)
        self._sftp_client = client.open_sftp()

    def _ssh_close(self):
        self.debug('Closing remote connection')
        self._sftp_client.close()
        self._ssh_client.close()

    def _ensure_buildozer(self):
        s = self._sftp_client
        root_dir = s.normalize('.')
        self.remote_build_dir = join(root_dir, self.remote_build_dir,
                self.package_full_name)
        self.debug('Remote build directory: {}'.format(self.remote_build_dir))
        self._ssh_mkdir(self.remote_build_dir)
        self._ssh_sync(__path__[0])

    def _sync_application_sources(self):
        self.info('Synchronize application sources')
        self._ssh_sync(self.app_dir)

        # create custom buildozer.spec
        self.info('Create custom buildozer.spec')
        config = SafeConfigParser()
        config.read('buildozer.spec')
        config.set('app', 'source.dir', 'app')

        fn = join(self.remote_build_dir, 'buildozer.spec')
        fd = self._sftp_client.open(fn, 'wb')
        config.write(fd)
        fd.close()

    def _do_remote_commands(self, args):
        self.info('Execute remote buildozer')
        cmd = (
            'source ~/.profile;'
            'cd {0};'
            'env PYTHONPATH={0}:$PYTHONPATH '
            'python -c "import buildozer, sys;'
            'buildozer.Buildozer().run_command(sys.argv[1:])" {1} {2} 2>&1').format(
            self.remote_build_dir,
            '--verbose' if self.log_level == 2 else '',
            ' '.join(args),
            )
        self._ssh_command(cmd)

    def _ssh_mkdir(self, *args):
        directory = join(*args)
        self.debug('Create remote directory {}'.format(directory))
        try:
            self._sftp_client.mkdir(directory)
        except IOError:
            # already created?
            try:
                self._sftp_client.stat(directory)
            except IOError:
                self.error('Unable to create remote directory {}'.format(directory))
                raise

    def _ssh_sync(self, directory, mode='put'):
        self.debug('Syncing {} directory'.format(directory))
        directory = realpath(directory)
        base_strip = directory.rfind('/')
        if mode == 'get':
            local_dir = join(directory,'bin')
            remote_dir = join(self.remote_build_dir, 'bin')
            if not os.path.exists(local_dir):
                os.path.makedirs(local_dir)
            for _file in self._sftp_client.listdir(path=remote_dir):
                self._sftp_client.get(join(remote_dir, _file),
                                      join(local_dir, _file))
            return
        for root, dirs, files in walk(directory):
            self._ssh_mkdir(self.remote_build_dir, root[base_strip + 1:])
            for fn in files:
                if splitext(fn)[1] in ('.pyo', '.pyc', '.swp'):
                    continue
                local_file = join(root, fn)
                remote_file = join(self.remote_build_dir, root[base_strip + 1:], fn)
                self.debug('Sync {} -> {}'.format(local_file, remote_file))
                self._sftp_client.put(local_file, remote_file)

    def _ssh_command(self, command):
        self.debug('Execute remote command {}'.format(command))
        #shell = self._ssh_client.invoke_shell()
        #shell.sendall(command)
        #shell.sendall('\nexit\n')
        transport = self._ssh_client.get_transport()
        channel = transport.open_session()
        try:
            channel.exec_command(command)
            self._interactive_shell(channel)
        finally:
            channel.close()

    def usage(self):
        print 'Usage: buildozer-remote [--verbose] [remote-name] [buildozer args]'


    def _interactive_shell(self, chan):
        if has_termios:
            self._posix_shell(chan)
        else:
            self._windows_shell(chan)

    def _posix_shell(self, chan):
        oldtty = termios.tcgetattr(stdin)
        try:
            #tty.setraw(stdin.fileno())
            #tty.setcbreak(stdin.fileno())
            chan.settimeout(0.0)

            while True:
                r, w, e = select([chan, stdin], [], [])
                if chan in r:
                    try:
                        x = chan.recv(128)
                        if len(x) == 0:
                            print '\r\n*** EOF\r\n',
                            break
                        stdout.write(x)
                        stdout.flush()
                        #print len(x), repr(x)
                    except socket.timeout:
                        pass
                if stdin in r:
                    x = stdin.read(1)
                    if len(x) == 0:
                        break
                    chan.sendall(x)
        finally:
            termios.tcsetattr(stdin, termios.TCSADRAIN, oldtty)

    # thanks to Mike Looijmans for this code
    def _windows_shell(self,chan):
        import threading

        stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")

        def writeall(sock):
            while True:
                data = sock.recv(256)
                if not data:
                    stdout.write('\r\n*** EOF ***\r\n\r\n')
                    stdout.flush()
                    break
                stdout.write(data)
                stdout.flush()

        writer = threading.Thread(target=writeall, args=(chan,))
        writer.start()

        try:
            while True:
                d = stdin.read(1)
                if not d:
                    break
                chan.send(d)
        except EOFError:
            # user hit ^Z or F6
            pass

def run():
    try:
        Buildozer().run_command(sys.argv[1:])
    except BuildozerCommandException:
        # don't show the exception in the command line. The log already show the
        # command failed.
        pass

def run_remote():
    try:
        BuildozerRemote().run_command(sys.argv[1:])
    except BuildozerCommandException:
        pass
