#! /usr/bin/python

import fnmatch, glob, os, re, string, sys, time, cStringIO
from optparse import OptionParser
from ConfigParser import SafeConfigParser

sys.path[0:0] = ['/usr/share/pycentral-data', '/usr/share/python']
import pyversions

try:
    SetType = set
except NameError:
    import sets
    SetType = sets.Set
    set = sets.Set

program = os.path.basename(sys.argv[0])

shared_base = '/usr/share/pycentral/'
shared_base2 = '/usr/share/pyshared/'
pycentral_version = '0.6.8'
req_pycentral_version = '0.6.7'

def samefs(path1, path2):
    if not (os.path.exists(path1) and os.path.exists(path2)):
        return False
    while path1 != os.path.dirname(path1):
        if os.path.ismount(path1):
            break
        path1 = os.path.dirname(path1)
    while path2 != os.path.dirname(path2):
        if os.path.ismount(path2):
            break
        path2 = os.path.dirname(path2)
    return path1 == path2

def version2depends(vinfo):
    if isinstance(vinfo, set):
        vinfo = list(vinfo)
    if isinstance(vinfo, list):
        vinfo = vinfo[:]
        vinfo.sort()
        nv = [int(s) for s in vinfo[-1].split('.')]
        deps = 'python (>= %s), python (<< %d.%d)' % (vinfo[0], nv[0], nv[1]+1)
    elif vinfo in ('all', 'current'):
        supported = [d[6:] for d in pyversions.supported_versions()
                     if re.match(r'python\d\.\d', d)]
        supported.sort()
        deps = 'python (>= %s)' % supported[0]
    elif vinfo == 'current_ext':
        cv = pyversions.default_version(version_only=True)
        nv = [int(s) for s in cv.split('.')]
        deps = 'python (>= %s), python (<< %d.%d)' % (cv, nv[0], nv[1]+1)
    else:
        raise ValueError, 'unknown version info %s' % vinfo
    return deps + ', python-central (>= %s)' % req_pycentral_version
        

def read_dpkg_status(verbose=False):
        """Read the dpkg status file, return a list of packages depending
        on python-central and having a Python-Version information field."""
        packages = []
        rx = re.compile(r'\bpython-central\b')
        pkgname = version = None
        depends = ''
        status = []
        for line in file('/var/lib/dpkg/status'):
            if line.startswith('Package:'):
                if version != None and 'installed' in status:
                    if 'python-support' in depends:
                        pass
                    elif rx.search(depends):
                        packages.append((pkgname, version))
                        if verbose:
                            print "    %s: %s (%s)" % (pkgname, version, status)
                version = None
                status = []
                pkgname = line.split(':', 1)[1].strip()
            elif line.startswith('Python-Version:'):
                version = line.split(':', 1)[1].strip()
            elif line.startswith('Depends:'):
                depends = line.split(':', 1)[1].strip()
            elif line.startswith('Status:'):
                status = line.split(':', 1)[1].strip().split()
        if version != None and 'installed' in status:
            if rx.search(depends):
                packages.append((pkgname, version))
                if verbose:
                    print "    %s: %s (%s)" % (pkgname, version, status)
        return packages


class PyCentralError(Exception):
    """Python Central Exception"""
    pass

class PyCentralVersionMissingError(PyCentralError):
    """Python Central Version Missing Exception"""
    pass

class PythonRuntime:
    def __init__(self, name, version, interp, prefix):
        self.name = name
        self.version = version
        if name.startswith('python'):
            self.short_name = name[6:]
        else:
            self.short_name = name
        self.interp = interp
        if prefix.endswith('/'):
            self.prefix = prefix + 'site-packages/'
        else:
            self.prefix = prefix + '/site-packages/'

    def byte_compile_dirs(self, dirs, bc_option, exclude=None):
        """call compileall.py -x <exclude regexp> <dirs> according
        to bc_options"""
        
        logging.debug('\tbyte-compile directories')
        errors = False
        cmd = [self.interp, self.prefix + '/compileall.py', '-q']
        if exclude:
            cmd.extend(['-x', exclude])
        cmd.extend(dirs)
        for opt in ('standard', 'optimize'):
            if not opt in bc_option:
                continue
            if opt == 'optimize':
                cmd[1:1] = ['-O']
            rv = os.spawnv(os.P_WAIT, self.interp, cmd[1:])
            if rv:
                raise PyCentralError

    def byte_compile(self, files, bc_option, exclude=None, ignore_errors=False, force=False):
        errors = False
        if exclude:
            rx = re.compile(exclude)
            files2 = []
            for fn in files:
                mo = rx.search(fn)
                if mo:
                    continue
                files2.append(fn)
        else:
            files2 = files
        if not files2:
            logging.info('\tno files to byte-compile')
            return
        logging.debug('\tbyte-compile files (%d/%d) %s' \
                      % (len(files), len(files2), self.name))
        debug_files = files2[:min(2, len(files2))]
        if len(files2) > 2:
            debug_files.append('...')
        logging.debug('\t    %s' % debug_files)
        cmd = [self.interp, '/usr/bin/py_compilefiles', '-q']
        if ignore_errors:
            cmd.append('-i')
        if force:
            cmd.append('-f')
        cmd.append('-')
        for opt in ('standard', 'optimize'):
            if not opt in bc_option:
                continue
            if opt == 'optimize':
                cmd[1:1] = ['-O']
            try:
                import subprocess
                p = subprocess.Popen(cmd, bufsize=1,
                                     shell=False, stdin=subprocess.PIPE)
                fd = p.stdin
            except ImportError:
                p = None
                fd = os.popen(' '.join(cmd), 'w')
            for fn in files2:
                fd.write(fn + '\n')
            rv = fd.close()
            if p:
                p.wait()
                errors = p.returncode != 0
            else:
                errors = rv != None
            if errors:
                raise PyCentralError, 'error byte-compiling files (%d)' % len(files2)

    def remove_byte_code(self, files):
        errors = False
        logging.debug('\tremove byte-code files (%d)' % (len(files)))
        for ext in ('c', 'o'):
            for fn in files:
                fnc = fn + ext
                if os.path.exists(fnc):
                    try:
                        os.unlink(fnc)
                    except OSError, e:
                        print "Sorry", e
                        errors = True
        if errors:
            raise PyCentralError, 'error removing the byte-code files'

installed_runtimes = None
default_runtime = None

def get_installed_runtimes(with_unsupported=True):
    global installed_runtimes
    global default_runtime

    if not installed_runtimes:
        import glob
        installed_runtimes = []
        default_version = pyversions.default_version(version_only=True)
        supported = pyversions.supported_versions()
        old = pyversions.old_versions()
        if with_unsupported:
            unsupported = pyversions.unsupported_versions()
        else:
            unsupported = []
        for interp in glob.glob('/usr/bin/python[0-9].[0-9]'):
            if old and os.path.basename(interp) in old:
                print "INFO: using old version '%s'" % interp
            elif unsupported and os.path.basename(interp) in unsupported:
                print "INFO: using unsupported version '%s'" % interp
            if not (os.path.basename(interp) in supported
                    or (old and os.path.basename(interp) in old)
                    or (unsupported and os.path.basename(interp) in unsupported)):
                continue
            version = interp[-3:]
            rt = PythonRuntime('python' + version,
                               version,
                               '/usr/bin/python' + version,
                               '/usr/lib/python' + version)
            installed_runtimes.append(rt)
            if version == default_version:
                default_runtime = rt
    return installed_runtimes

def get_default_runtime():
    get_installed_runtimes()
    return default_runtime

def get_runtime_for_version(version):
    if version == 'current':
        return get_default_runtime()
    for rt in get_installed_runtimes():
        if rt.version == version:
            return rt
    return None
    
debian_config = None
def get_debian_config():
    global debian_config
    if debian_config is not None:
        return debian_config

    config = SafeConfigParser()
    fn = '/etc/python/debian_config'
    if os.path.exists(fn):
        try:
            config.readfp(open(fn))
        except Error:
            logging.error("error reading config file `%s'" % fn)
            sys.exit(1)
    # checks
    if not config.has_option('DEFAULT', 'byte-compile'):
        config.set('DEFAULT', 'byte-compile', 'standard')
    bc_option = config.get('DEFAULT', 'byte-compile')
    bc_values = set([v.strip() for v in bc_option.split(',')])
    bc_unknown = bc_values - set(['standard', 'optimize'])
    if bc_unknown:
        sys.stderr.write("%s: `%s': unknown values `%s'"
                         " in `byte-compile option'\n"
                         % (program, fn, ', '.join(list(bc_unknown))))
        sys.exit(1)
    config.set('DEFAULT', 'byte-compile', ', '.join(bc_values))
    if config.has_option('DEFAULT', 'overwrite-local'):
        val = config.get('DEFAULT', 'overwrite-local').strip().lower()
        overwrite_local = val in ('yes', '1', 'true')
    else:
        overwrite_local = False
    config.set('DEFAULT', 'overwrite-local', overwrite_local and '1' or '0')
    debian_config = config
    return debian_config

class DebPackage:
    def __init__(self, kind, name,
                 oldstyle=False, default_runtime=None,
                 pkgdir=None, pkgconfig=None, parse_versions=True):
        self.kind = kind
        self.name = name
        self.version_field = None
        self.oldstyle = oldstyle
        self.parse_versions = parse_versions
        self.default_runtime = default_runtime
        self.shared_prefix = None
        self.pkgdir = pkgdir
        self.pkgconfig = pkgconfig
        self.has_shared_extension = {}
        self.has_private_extension = False
        self.has_shared_module = {}
        self.has_private_module = False
        if pkgdir:
            self.read_control()
        else:
            self.read_pyfiles()
            #self.print_info()

    def read_pyfiles(self):
        self.shared_files = []
        self.pylib_files = {}
        self.other_pylib_files = []
        self.private_files = []
        self.omitted_files = []
        self.pysupport_files = []
        if self.pkgdir:
            lines = []
            for root, dirs, files in os.walk(self.pkgdir):
                if root.endswith('DEBIAN'):
                    continue
                if root != self.pkgdir:
                    d = root[len(self.pkgdir):]
                    lines.append(d)
                for name in files:
                    lines.append(os.path.join(d, name))
        else:
            config_file = '/usr/share/pyshared-data/%s' % self.name
            if self.pkgconfig:
                lines = [fn for fn, t in self.pkgconfig.items('files')]
                lines.sort()
            elif os.path.isfile(config_file):
                logging.debug("reading %s" % config_file)
                self.pkgconfig = SafeConfigParser()
                self.pkgconfig.optionxform = str
                self.pkgconfig.readfp(file(config_file))
                lines = [fn for fn, t in self.pkgconfig.items('files')]
                lines.sort()
            else:
                lines = self.read_preinst_pkgconfig()
            if lines:
                pass
            elif os.environ.has_key("PYCENTRAL_NO_DPKG_QUERY"):
                logging.debug("Not using dpkg-query as requested")
                lines = map(string.strip, open('/var/lib/dpkg/info/%s.list' % self.name).readlines())
            else:
                cmd = ['/usr/bin/dpkg-query', '-L', self.name]
                try:
                    import subprocess
                    p = subprocess.Popen(cmd, bufsize=1,
                                         shell=False, stdout=subprocess.PIPE)
                    fd = p.stdout
                except ImportError:
                    fd = os.popen(' '.join(cmd))
                lines = [s[:-1] for s in fd.readlines()]
            if not self.pkgconfig:
                pc = SafeConfigParser()
                pc.optionxform = str
                pc.add_section('python-package')
                pc.set('python-package', 'format', '1')
                pc.add_section('pycentral')
                pc.set('pycentral', 'version', req_pycentral_version)
                pc.add_section('files')
                for line in lines:
                    if os.path.isdir(line) and not os.path.islink(line):
                        pc.set('files', line, 'd')
                    elif os.path.exists(line):
                        pc.set('files', line, 'f')
                    else:
                        pass # should not happen
                self.pkgconfig = pc

        old_shared_base = shared_base + self.name + '/site-packages/'
        found_old_base = found_base2 = False
        for line in lines:
            fn = line
            if fn.startswith(shared_base2):
                # keep _all_ files and directories
                self.shared_files.append(fn)
                if fn.endswith('.py'):
                    self.has_shared_module['all'] = True
                found_base2 = True
                self.shared_prefix = shared_base2
                continue
            elif fn.startswith(old_shared_base):
                # keep _all_ files and directories
                self.shared_files.append(fn)
                if fn.endswith('.py'):
                    self.has_shared_module['all'] = True
                found_old_base = True
                self.shared_prefix = old_shared_base
                continue
            elif fn.startswith('/usr/share/python-support') \
                     or fn.startswith('/usr/lib/python-support'):
                self.pysupport_files.append(fn)
                continue
            elif not fn.endswith('.py') and not fn.endswith('.so'):
                if re.match(r'/usr/lib/python\d\.\d/', fn):
                    self.other_pylib_files.append(fn)
                continue
            elif fn.startswith('/etc/') or fn.startswith('/usr/share/doc/'):
                # omit files in /etc and /usr/share/doc
                self.omitted_files.append(fn)
                continue
            elif re.search(r'/s?bin/', fn):
                # omit files located in directories
                self.omitted_files.append(fn)
                continue
            elif fn.startswith('/usr/lib/site-python/'):
                version = pyversions.default_version(version_only=True)
                self.pylib_files.setdefault(version, []).append(fn)
                continue
            elif re.match(r'/usr/lib/python\d\.\d/', fn):
                version = fn[15:18]
                if fn.endswith('.so'):
                    self.has_shared_extension[version] = True
                if fn.endswith('.py'):
                    self.has_shared_module[version] = True
                    self.pylib_files.setdefault(version, []).append(fn)
                continue
            else:
                self.private_files.append(fn)
                if fn.endswith('.py'):
                    self.has_private_module = True

        if found_old_base and found_base2:
            raise PyCentralError, \
                  'shared files found in old (%s) and new locations (%s)' % (old_shared_base, shared_base2)

    def read_preinst_pkgconfig(self):
        try:
            fd = open('/var/lib/dpkg/info/%s.preinst' % self.name, 'r')
        except:
            return None
        buffer = None
        for line in fd.readlines():
            if line == '[python-package]\n':
                buffer = cStringIO.StringIO()
            if line in ('PYEOF\n', 'EOF\n'):
                break
            if buffer:
                buffer.write(line)
        if buffer is None:
            return None
        self.pkgconfig = SafeConfigParser()
        self.pkgconfig.optionxform = str
        self.pkgconfig.readfp(cStringIO.StringIO(buffer.getvalue()))
        files = [fn for fn, t in self.pkgconfig.items('files')]
        files.sort()
        return files

    def print_info(self, fd=sys.stdout):
        fd.write('Package: %s\n' % self.name)
        fd.write('    shared files  :%4d\n' % len(self.shared_files))
        fd.write('    private files :%4d\n' % len(self.private_files))
        for ver, files in self.pylib_files.items():
            fd.write('    pylib%s files:%4d\n' % (ver, len(files)))

    def read_control(self):
        """read the debian/control file, extract the XS-Python-Version
        field; check that XB-Python-Version exists for the package."""
        if not os.path.exists('debian/control'):
            raise PyCentralError, "debian/control not found"
        self.version_field = None
        self.sversion_field = None
        try:
            section = None
            for line in file('debian/control'):
                line = line.strip()
                if line == '':
                    section = None
                elif line.startswith('Source:'):
                    section = 'Source'
                elif line.startswith('Package: ' + self.name):
                    section = self.name
                elif line.startswith('XS-Python-Version:'):
                    if section != 'Source':
                        raise PyCentralError, \
                              'attribute XS-Python-Version not in Source section'
                    self.sversion_field = line.split(':', 1)[1].strip()
                elif line.startswith('XB-Python-Version:'):
                    if section == self.name:
                        self.version_field = line.split(':', 1)[1].strip()
        except:
            pass
        if self.version_field == None:
            raise PyCentralVersionMissingError, \
                  'missing XB-Python-Version attribute in package %s' % self.name
        if self.sversion_field == None:
            raise PyCentralError, 'missing XS-Python-Version attribute'
        if self.parse_versions:
            self.sversion_info = parse_versions(self.sversion_field)
        else:
            self.sversion_info = 'all' # dummy
        self.has_private_extension = self.sversion_info == 'current_ext'

    def move_files(self):
        """move files from the installation directory to the pycentral location"""
        import shutil

        dsttop = self.pkgdir + shared_base2
        try:
            os.makedirs(dsttop)
        except OSError:
            pass
        pversions = pyversions.supported_versions() \
                    + pyversions.unsupported_versions() + pyversions.old_versions()
        pversions = list(set(pversions))

        # rename .egg-info files and dirs, remove *.py[co] files
        rename = 'norename' not in os.environ.get('DH_PYCENTRAL', '')
        vrx = re.compile(r'(.*)(-py\d\.\d)(.*)(\.egg-info|\.pth)$')
        for pversion in pversions:
            srctop = os.path.join(self.pkgdir, 'usr/lib', pversion, 'site-packages')
            for root, dirs, files in os.walk(srctop, topdown=False):
                for name in files:
                    m = vrx.match(name)
                    if m and rename:
                        name2 = ''.join(m.group(1, 3, 4))
                        print "rename: %s -> %s" % (name, name2)
                        os.rename(os.path.join(root, name), os.path.join (root, name2))
                    elif name.endswith('.pyc') or name.endswith('.pyo'):
                        os.unlink(os.path.join(root, name))
                for name in dirs:
                    m = vrx.match(name)
                    if m and rename:
                        name2 = ''.join(m.group(1, 3, 4))
                        print "rename: %s -> %s" % (name, name2)
                        os.rename(os.path.join(root, name), os.path.join (root, name2))

        # search for differences
        import filecmp
        class MyDircmp(filecmp.dircmp):
            def report(self):
                if self.left_only or self.right_only or self.diff_files or self.funny_files:
                    self.differs = True
                    print 'diff', self.left, self.right
                if self.left_only:
                    self.left_only.sort()
                    print 'Only in', self.left, ':', self.left_only
                if self.right_only:
                    self.right_only.sort()
                    print 'Only in', self.right, ':', self.right_only
                if self.diff_files:
                    self.diff_files.sort()
                    print 'Differing files :', self.diff_files
                if self.funny_files:
                    self.funny_files.sort()
                    print 'Trouble with common files :', self.funny_files

        for pv1 in pversions:
            for pv2 in pversions[pversions.index(pv1)+1:]:
                site1 = os.path.join(self.pkgdir, 'usr/lib', pv1, 'site-packages')
                site2 = os.path.join(self.pkgdir, 'usr/lib', pv2, 'site-packages')
                if not (os.path.isdir(site1) and os.path.isdir(site2)):
                    continue
                dco = MyDircmp(site1, site2)
                dco.differs = False
                dco.report_full_closure()
                                 
        # move around
        for pversion in pversions:
            srctop = os.path.join(self.pkgdir, 'usr/lib', pversion, 'site-packages')
            for root, dirs, files in os.walk(srctop):
                if root == srctop:
                    d = '.'
                else:
                    d = root[len(srctop)+1:]
                for name in dirs:
                    src = os.path.join(root, name)
                    dst = os.path.join(dsttop, d, name)
                    try:
                        os.mkdir(dst)
                        shutil.copymode(src, dst)
                    except OSError:
                        pass
                for name in files:
                    src = os.path.join(root, name)
                    dst = os.path.join(dsttop, d, name)
                    if re.search(r'\.so(\.\d+)*?$', name):
                        continue
                    # TODO: if dst already exists, make sure, src == dst
                    os.rename(src, dst)
            # remove empty dirs in /usr/lib/pythonX.Y
            for root, dirs, files in os.walk(self.pkgdir + '/usr/lib', topdown=False):
                try:
		    if re.match("/usr/lib/python\d\.\d($|/)", root.replace(self.pkgdir, "")):
			os.rmdir(root)
                except OSError:
                    pass
            try:
                os.rmdir(self.pkgdir + '/usr/lib')
            except OSError:
                pass
        # remove empty dirs in /usr/share/pyshared
        for root, dirs, files in os.walk(self.pkgdir + shared_base2, topdown=False):
            try:
                os.rmdir(root)
            except OSError:
                pass
        try:
            os.rmdir(self.pkgdir + '/usr/share/pyshared')
        except OSError:
            pass

    def gen_substvars(self):
        supported = [d[6:] for d in pyversions.supported_versions()
                     if re.match(r'python\d\.\d', d)]
        versions = ''
        prversions = ''
        self.depends = None
        if len(self.has_shared_module) or len(self.has_shared_extension):
            # shared modules / extensions
            if len(self.has_shared_extension):
                versions = self.has_shared_extension.keys()
            else:
                if self.sversion_info in ('current', 'current_ext'):
                    versions = 'current'
                elif self.sversion_info == 'all':
                    versions = 'all'
                    prversions = supported
                else:
                    versions = self.sversion_field
                    prversions = list(self.sversion_info.intersection(supported))
                    self.depends = version2depends(self.sversion_info)
        elif self.has_private_module or self.has_private_extension:
            if self.sversion_info == 'all':
                versions = 'current'
            elif self.sversion_info == 'current':
                versions = 'current'
            elif self.sversion_info == 'current_ext':
                versions = [pyversions.default_version(version_only=True)]
            elif isinstance(self.sversion_info, list) or isinstance(self.sversion_info, set):
                # exact version info required, no enumeration, no relops
                if len(self.sversion_info) != 1 or not re.match(r'\d\.\d', self.sversion_info[0]):
                    raise PyCentralError, 'no exact version for package with private modules'
                versions = [list(self.sversion_info)[0]]
            else:
                raise PyCentralError, 'version error for package with private modules'
        else:
            # just "copy" it from the source field
            if self.sversion_info == 'current':
                versions = 'current'
            elif self.sversion_info == 'current_ext':
                versions = [pyversions.default_version(version_only=True)]
            elif self.sversion_info == 'all':
                versions = 'all'
                prversions = supported
            else:
                versions = self.sversion_field
                prversions = list(self.sversion_info.intersection(supported))
                self.depends = version2depends(self.sversion_info)

        if (len(self.has_shared_module) or len(self.has_shared_extension)) \
           and self.has_private_module or self.has_private_extension:
            # use case? use the information for the shared stuff
            pass
        if versions == '':
            raise PyCentralError, 'unable to determine Python-Version attribute'
        if isinstance(versions, list) or isinstance(versions, set):
            self.version_field = ', '.join(versions)
        else:
            self.version_field = versions
        if not self.depends:
            self.depends = version2depends(versions)
        if self.name.startswith('python-'):
            if prversions == '':
                prversions = versions
            self.provides = ', '.join([self.name.replace('python-', 'python%s-' % ver)
                                       for ver in prversions])
    
    def set_version_field(self, version_field):
        self.version_field = version_field
        if self.parse_versions:
            self.version_info = pyversions.parse_versions(version_field)

    def read_version_info(self):
        """Read the Python-Version information field"""
        if self.version_field:
            return
        if self.pkgconfig and self.pkgconfig.has_option('python-package', 'python-version'):
            self.version_field = self.pkgconfig.get('python-package', 'python-version')
            logging.debug("Using python-version from pkgconfig: %s" % self.version_field)
        elif os.environ.has_key("PYCENTRAL_NO_DPKG_QUERY"):
            logging.debug("Not using dpkg-query as requested")
            needle = "Package: %s\n" % self.name
            for block in open("/var/lib/dpkg/status").read().split("\n\n"):
                if needle in block:
                    for line in block.split("\n"):
                        if line.startswith('Python-Version:'):
                            self.version_field = line.split(':', 1)[1].strip()
                            break
        else:
            logging.debug("dpkg-query -s %s" % self.name)
            cmd = ['/usr/bin/dpkg-query', '-s', self.name]
            try:
                import subprocess
                p = subprocess.Popen(cmd, bufsize=1,
                                     shell=False, stdout=subprocess.PIPE)
                fd = p.stdout
            except ImportError:
                fd = os.popen(' '.join(cmd))
            
            for line in fd:
                if line.startswith('Python-Version:'):
                    self.version_field = line.split(':', 1)[1].strip()
                    break
            fd.close()
        # now verify/parse it
        if not self.version_field:
            raise PyCentralError, "package has no field Python-Version"
        if self.parse_versions:
            self.version_info = pyversions.parse_versions(self.version_field)

    def set_default_runtime_from_version_info(self):
        versions = list(pyversions.requested_versions(self.version_field, version_only=True))
        if not versions:
            #raise PyCentralError, "no matching runtime for `%s'" % self.version_field
            logging.warn("%s: no matching runtime for `%s', using default"
                         % (self.name, self.version_field))
            self.default_runtime = get_default_runtime()
        if len(versions) == 1:
            self.default_runtime = get_runtime_for_version(versions[0])
        elif pyversions.default_version(version_only=True) in versions:
            self.default_runtime = get_default_runtime()
        else:
            self.default_runtime = get_runtime_for_version(versions[0])

    def byte_compile(self, runtimes, bc_option, exclude_regex, ignore_errors=False):
        """byte compiles all files not handled by pycentral"""

        logging.debug("    byte-compile %s" % self.name)
        if self.shared_files:
            ppos = len(self.shared_prefix)
            for rt in runtimes:
                linked_files = [ rt.prefix + fn[ppos:]
                                 for fn in self.shared_files
                                 if fn[-3:] == '.py']
                rt.byte_compile(linked_files, bc_option, exclude_regex, ignore_errors)

        for pyver, files in self.pylib_files.items():
            logging.debug("bc for v%s (%d files)" % (pyver, len(files)))
            rt = get_runtime_for_version(pyver)
            if rt in runtimes:
                rt.byte_compile(files, bc_option, exclude_regex)

        if self.private_files:
            logging.debug("bc %s private (%d files)" %
                          (self.default_runtime.version, len(self.private_files)))
            rt = self.default_runtime
            rt.byte_compile(self.private_files, bc_option, exclude_regex)

    def remove_bytecode(self):
        """remove all byte-compiled files not handled by pycentral"""
        
        assert self.oldstyle

        logging.debug("    remove byte-code for %s" % self.name)
        pyfiles = []
        for files in self.pylib_files.values():
            pyfiles.extend(files)
        pyfiles.extend(self.private_files)

        errors = False
        for ext in ('c', 'o'):
            for fn in pyfiles:
                fnc = fn + ext
                if os.path.exists(fnc):
                    try:
                        os.unlink(fnc)
                    except OSError, e:
                        print "Sorry", e
                        errors = True
        if errors:
            raise PyCentralError

    def link_shared_files(self, rt):
        #if samefs(rt.prefix, self.shared_files[0]):
        #    link_cmd = os.link
        #else:
        #    link_cmd = os.symlink
        logging.debug("\tlink shared files %s/%s" % (rt.name, self.name))
        if not self.shared_files:
            return []
        link_cmd = os.symlink
        ppos = len(self.shared_prefix)
        existing_files = []
        for fn in self.shared_files:
            fn2 = rt.prefix + fn[ppos:]
            if os.path.isdir(fn) and not os.path.islink(fn):
                continue
            if os.path.exists(fn2):
                link = abs_link = None
                if os.path.islink(fn2):
                    link = abs_link = os.readlink(fn2)
                    if link.startswith('../'):
                        abs_link = os.path.normpath(os.path.join(os.path.dirname(fn2), link))
                if abs_link == fn or link == fn:
                    continue
                if not link or not (abs_link.startswith(shared_base2) or abs_link.startswith(shared_base)):
                    existing_files.append(fn2)

        if existing_files:
            conf = get_debian_config()
            overwrite_local = conf.get('DEFAULT', 'overwrite-local') == '1'
            if overwrite_local:
                print "overwriting local files"

        linked_files = []
        try:
            for fn in self.shared_files:
                fn2 = rt.prefix + fn[ppos:]
                if os.path.isdir(fn) and not os.path.islink(fn):
                    if os.path.isdir(fn2):
                        continue
                    os.makedirs(fn2)
                    linked_files.append(fn2)
                else:
                    if os.path.exists(fn2):
                        msg = "already exists: %s" % fn2
                        link = abs_link = None
                        if os.path.islink(fn2):
                            link = abs_link = os.readlink(fn2)
                            if link.startswith('../'):
                                abs_link = os.path.normpath(os.path.join(os.path.dirname(fn2), link))
                        if abs_link == fn or link == fn:
                            linked_files.append(fn2)
                            continue
                        if not link or not (abs_link.startswith(shared_base2) or abs_link.startswith(shared_base)):
                            msg = msg + " -> %s" % link
                            if overwrite_local:
                                print "warning:", msg
                                os.unlink(fn2)
                            else:
                                continue # raise PyCentralError, msg at end of method
                    # make sure that fn2 really does not exist; this is a
                    # special hack to make pycentral work with fakechroot,
                    # which has a slightly weird treatment of symlinks
                    # now needed to switch between old and new prefix
                    try:
                        os.unlink(fn2)
                    except OSError:
                        pass
                    link_cmd(fn, fn2)
                    linked_files.append(fn2)
        except PyCentralError, msg:
            raise
        except Exception, msg:
            print msg
            # FIXME: undo
            linked_files.reverse()
            return []
        else:
            if existing_files and not overwrite_local:
                raise PyCentralError, "not overwriting local files"
            return linked_files

    def unlink_shared_files(self, rt):
        logging.debug('\tunlink_shared_files %s/%s' % (rt.name, self.name))
        if not self.shared_files:
            return
        ppos = len(self.shared_prefix)
        shared_files = self.shared_files[:]
        shared_files.reverse()
        for fn in shared_files:
            fn2 = rt.prefix + fn[ppos:]
            if os.path.isdir(fn2) and not os.path.islink(fn2):
                try:
                    os.removedirs(fn2)
                except OSError:
                    pass
            else:
                if os.path.exists(fn2):
                    os.unlink(fn2)


    def install(self, runtimes, bc_option, exclude_regex,
                byte_compile_default=True, ignore_errors=False):
        logging.debug('\tinstall package %s' % self.name)
        # install shared .py files
        if self.shared_files:
            for rt in runtimes:
                linked_files = self.link_shared_files(rt)
                rt.byte_compile(linked_files, bc_option, exclude_regex, ignore_errors)
        # byte compile files inside prefix
        if self.pylib_files:
            for pyver, files in self.pylib_files.items():
                rt = get_runtime_for_version(pyver)
                if rt in runtimes:
                    rt.byte_compile(files, bc_option, exclude_regex, ignore_errors)
        # byte compile with the default runtime for the package
        if byte_compile_default:
            if self.private_files:
                self.default_runtime.byte_compile(self.private_files, bc_option,
                                                  exclude_regex, ignore_errors)

    def prepare(self, runtimes, old_runtimes, old_pkg, ignore_errors=False):
        logging.debug('\tprepare package %s' % self.name)

        if old_pkg and old_pkg.private_files:
            fs_in_old = set([fn for fn in old_pkg.private_files if fn[-3:] == '.py'])
            fs_in_new = set([fn for fn in self.private_files if fn[-3:] == '.py'])
            removed_fs = list(fs_in_old.difference(fs_in_new))
            if removed_fs:
                logging.debug_list('\t', 'removed private', removed_fs)
                default_runtime.remove_byte_code(removed_fs)

        old_pylib_fs = []
        if old_pkg and old_pkg.pylib_files:
            for pyver, files in old_pkg.pylib_files.items():
                fs_in_old = set([fn for fn in files if fn[-3:] == '.py'])
                fs_in_new = set([fn for fn in self.pylib_files.get(pyver, []) if fn[-3:] == '.py'])
                removed_fs = list(fs_in_old.difference(fs_in_new))
                if removed_fs:
                    logging.debug_list('\t', 'removed pylib', removed_fs)
                    default_runtime.remove_byte_code(removed_fs)
                old_pylib_fs += files
            old_pylib_fs += old_pkg.other_pylib_files

        if old_pkg and old_pkg.shared_files:
            for rt in old_runtimes:
                if rt in runtimes:
                    continue
                linked_files = [ rt.prefix + fn[ppos:]
                                 for fn in old_pkg.shared_files if fn[-3:] == '.py']
                if linked_files:
                    logging.debug_list('\t', 'removed runtimes', linked_files)
                    default_runtime.remove_byte_code(linked_files)
                    self.unlink_files(rt)

        if not self.shared_files:
            return

        dirs_in_new = set([fn for fn, t in self.pkgconfig.items('files')
                           if t == 'd' if fn.startswith(self.shared_prefix)])
        dirs_in_old = set()
        if old_pkg:
            dirs_in_old = set([fn for fn, t in old_pkg.pkgconfig.items('files')
                               if t == 'd' if fn.startswith(self.shared_prefix)])
        new_dirs = list(dirs_in_new.difference(dirs_in_old))
        new_dirs.sort()
        removed_dirs = list(dirs_in_old.difference(dirs_in_new))
        removed_dirs.sort()
        
        fs_in_new = set([fn for fn, t in self.pkgconfig.items('files')
                         if t == 'f' if fn.startswith(self.shared_prefix)])
        fs_in_old = set()
        if old_pkg:
            fs_in_old = set([fn for fn, t in old_pkg.pkgconfig.items('files')
                             if t == 'f' if fn.startswith(self.shared_prefix)])
        new_fs = list(fs_in_new.difference(fs_in_old))
        new_fs.sort()
        removed_fs = list(fs_in_old.difference(fs_in_new))
        removed_fs.sort()

        logging.debug_list('\t', 'new      dirs', new_dirs)
        logging.debug_list('\t', 'removed  dirs', removed_dirs)
        logging.debug_list('\t', 'new     files', new_fs)
        logging.debug_list('\t', 'removed files', removed_fs)
        
        link_cmd = os.symlink
        ppos = len(self.shared_prefix)
        existing_files = []
        for rt in runtimes:
            for f1 in new_fs:
                f2 = rt.prefix + f1[ppos:]
                if os.path.exists(f2):
                    link = abs_link = None
                    if os.path.islink(f2):
                        link = abs_link = os.readlink(f2)
                        if link.startswith('../'):
                            abs_link = os.path.normpath(os.path.join(os.path.dirname(f2), link))
                    if abs_link == f1 or link == f1:
                        continue
                    if not link or not (abs_link.startswith(shared_base2) or abs_link.startswith(shared_base)):
                        existing_files.append(f2)
        if existing_files:
            # if the current installed version does not use python-central
            # then having a file here is expected and harmless
            if not self.name in [p for (p,v) in read_dpkg_status()]:
                logging.info("%s: upgrade from package version not using python-central" % self.name)
                return
            # if all existing files are found in the old package in
            # /usr/lib/pythonX.Y/site-packages, and moved to the shared area,
            # do nothing.
            not_in_same_pkg = set(existing_files)
            if old_pkg:
                not_in_same_pkg.difference_update(old_pylib_fs)
            if not_in_same_pkg == set():
                logging.info("%s: upgrade from package version with unmoved files" % self.name)
                return
            conf = get_debian_config()
            overwrite_local = conf.get('DEFAULT', 'overwrite-local') == '1'
            if overwrite_local:
                print "overwriting local files"

        for rt in runtimes:
            for d1 in new_dirs:
                d2 = rt.prefix + d1[ppos:]
                if os.path.isdir(d2):
                    continue
                os.makedirs(d2)
            for f1 in new_fs:
                f2 = rt.prefix + f1[ppos:]
                if os.path.exists(f2):
                    msg = "already exists: %s" % f2
                    link = abs_link = None
                    if os.path.islink(f2):
                        link = abs_link = os.readlink(f2)
                        if link.startswith('../'):
                            abs_link = os.path.normpath(os.path.join(os.path.dirname(f2), link))
                    if abs_link == f1 or link == f1:
                        continue
                    if not link or not (abs_link.startswith(shared_base2) or abs_link.startswith(shared_base)):
                        msg = msg + " -> %s" % link
                        if overwrite_local:
                            print "warning:", msg
                            os.unlink(f2)
                        else:
                            continue # raise PyCentralError, msg at end of loop
                # hack to make pycentral work with fakechroot
                # now needed to switch between old and new prefix
                try:
                    os.unlink(f2)
                except OSError:
                    pass
                try:
                    d = os.path.dirname(f2)
                    if not os.path.isdir(d):
                        print "create directory %s" % d
                        os.makedirs(d)
                    link_cmd(f1, f2)
                except OSError:
                    print "unable to create symlink %s" % f2
                    raise
        if existing_files and not overwrite_local:
            raise PyCentralError, "not overwriting local files"

        for rt in runtimes:
            for f1 in removed_fs:
                f2 = rt.prefix + f1[ppos:]
                try:
                    os.unlink(f2)
                    os.unlink(f2 + 'c')
                    os.unlink(f2 + 'o')
                except OSError:
                    pass
            for d1 in removed_dirs:
                d2 = rt.prefix + d1[ppos:]
                try:
                    os.rmdir(d2)
                except OSError:
                    pass

        return

    def remove(self, runtimes, remove_script_files=True):
        logging.debug('\tremove package %s' % self.name)
        # remove shared .py files
        if self.shared_files:
            ppos = len(self.shared_prefix)
            for rt in runtimes:
                linked_files = [ rt.prefix + fn[ppos:]
                                 for fn in self.shared_files
                                 if fn[-3:] == '.py']
                #print self.shared_files
                #print linked_files
                default_runtime.remove_byte_code(linked_files)
                self.unlink_shared_files(rt)
        # remove byte compiled files inside prefix
        if self.pylib_files:
            for pyver, files in self.pylib_files.items():
                rt = get_runtime_for_version(pyver)
                if rt in runtimes:
                    default_runtime.remove_byte_code(files)
        # remove byte code for script files
        if remove_script_files:
            if self.private_files:
                default_runtime.remove_byte_code(self.private_files)

    def update_bytecode_files(self, runtimes, rt_default, bc_option):
        # byte-compile with default python version
        logging.debug('\tupdate byte-code for %s' % self.name)
        exclude_regex = None
        # update shared .py files
        if self.shared_files:
            ppos = len(self.shared_prefix)
            for rt in runtimes:
                if rt == rt_default:
                    linked_files = self.link_shared_files(rt)
                    rt.byte_compile(linked_files, bc_option, exclude_regex)
                else:
                    linked_files = [ rt.prefix + fn[ppos:]
                                     for fn in self.shared_files
                                     if fn[-3:] == '.py']
                    rt.remove_byte_code(linked_files)
                    self.unlink_shared_files(rt)
        # byte compile with the default runtime for the package
        if self.private_files:
            self.default_runtime.byte_compile(self.private_files,
                                              bc_option, exclude_regex, force=True)

known_actions = {}
def register_action(action_class):
    known_actions[action_class.name] = action_class

class Action:
    _option_parser = None
    name = None
    help = ""
    usage = "<options>"
    def __init__(self):
        self.errors_occured = 0
        parser = self.get_option_parser()
        parser.set_usage(
            'usage: %s [<options> ...] %s %s' % (program, self.name, self.usage))

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            self._option_parser = p
        return self._option_parser

    def info(self, msg, stream=sys.stderr):
        logging.info('%s %s: %s' % (program, self.name, msg))

    def warn(self, msg, stream=sys.stderr):
        logging.warn('%s %s: %s' % (program, self.name, msg))

    def error(self, msg, stream=sys.stderr, go_on=False):
        logging.error('%s %s: %s' % (program, self.name, msg))
        self.errors_occured += 1
        if not go_on:
            sys.exit(1)

    def parse_args(self, arguments):
        self.options, self.args = self._option_parser.parse_args(arguments)
        return self.options, self.args

    def check_args(self, global_options):
        return self.errors_occured

    def run(self, global_opts):
        pass


class ActionByteCompile(Action):
    """byte compile the *.py files in <package> using the the
    default python version (or use the version specified with -v.
    Any additional directory arguments are ignored (only files
    found in the package are byte compiled. Files in
    /usr/lib/pythonX.Y are compiled with the matching python version.

    bccompile is a replacement for the current byte compilation
    generated by the dh_python debhelper script.
    """
    name = 'bccompile'
    help = 'byte compile .py files in a package'
    usage = '[<options>] <package> [<dir> ...]'

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-x', '--exclude',
                         help="skip files matching the regular expression",
                         default=None, action='store', dest='exclude')
            p.add_option('-V', '--version',
                         help="byte compile using this python version",
                         default='current', action='store', dest='version')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) < 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        self.runtime = get_runtime_for_version(self.options.version)
        if not self.runtime:
            self.error("unknown runtime version %s" % self.options.version)

        if not os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
            self.error("package %s is not installed" % self.pkgname)
        self.pkg = DebPackage('package', self.pkgname, oldstyle=False,
                              default_runtime=self.runtime)
        self.pkg.read_version_info()
        return self.errors_occured

    def run(self, global_options):
        logging.debug('bccompile %s' % self.pkgname)
        runtimes = get_installed_runtimes()
        config = get_debian_config()
        bc_option = config.get('DEFAULT', 'byte-compile')
        requested = pyversions.requested_versions_for_runtime(self.pkg.version_field, version_only=True)
        used_runtimes = [rt for rt in runtimes if rt.short_name in requested]
        # called with directories as arguments
        if 0 and self.directories:
            try:
                for version, dirs in self.pylib_dirs.items():
                    rt = get_runtime_for_version(version)
                    rt.byte_compile_dirs(dirs, bc_option, self.options.exclude)
                if self.private_dirs:
                    version = pkg.version_field
                    if version == 'current':
                        version = pyversions.default_version(version_only=True)
                    rt = get_runtime_for_version(version)
                    rt.byte_compile_dirs(private_dirs, bc_option, self.options.exclude)
            except PyCentralError:
                self.error("error byte-compiling package `%s'" % self.pkgname)
            return

        try:
            self.pkg.byte_compile(used_runtimes, bc_option, self.options.exclude)
        except PyCentralError:
            self.error("error byte-compiling package `%s'" % self.pkgname)

register_action(ActionByteCompile)

class ActionPkgInstall(Action):
    name = 'pkginstall'
    help = 'make a package available for all supported runtimes'
    usage = '[<options>] <package>'
    
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-x', '--exclude',
                         help="skip files matching the regular expression",
                         default=None, action='store', dest='exclude')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        if not os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
            self.error("package %s is not installed" % self.pkgname)
        return self.errors_occured

    def run(self, global_options):
        runtimes = get_installed_runtimes()
        config = get_debian_config()
        bc_option = config.get('DEFAULT', 'byte-compile')
        pkg = DebPackage('package', self.args[0], oldstyle=False)
        pkg.read_version_info()
        requested = pyversions.requested_versions_for_runtime(pkg.version_field, version_only=True)
        used_runtimes = [rt for rt in runtimes if rt.short_name in requested]
        try:
            pkg.set_default_runtime_from_version_info()
        except ValueError:
	    # Package doesn't provide support for any supported runtime
	    if len(used_runtimes) == 0:
		self.error('%s needs unavailable runtime (%s)'
			   % (self.pkgname, pkg.version_field))
	    else:
		# Still byte compile for the available runtimes (with the
		# first matching runtime)
 		pkg.default_runtime = get_runtime_for_version(used_runtimes[0])
        logging.debug('\tavail=%s, pkg=%s, install=%s'
                      % ([rt.short_name for rt in runtimes],
                         pkg.version_field,
                         [rt.short_name for rt in used_runtimes]))
        try:
            pkg.install(used_runtimes, bc_option,
                        self.options.exclude, byte_compile_default=True)
        except PyCentralError, msg:
            self.error(msg)
            

register_action(ActionPkgInstall)


class ActionPkgPrepare(Action):
    name = 'pkgprepare'
    help = 'prepare a package for all supported runtimes'
    usage = '[<options>] <package>'
    
    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-x', '--exclude',
                         help="skip files matching the regular expression",
                         default=None, action='store', dest='exclude')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        # FIXME: run from the preinst, package may not exist
        #if not os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
        #    self.error("package %s is not installed" % self.pkgname)
        return self.errors_occured

    def run(self, global_options):
        runtimes = get_installed_runtimes()
        config = get_debian_config()
        pkgconfig = SafeConfigParser()
        pkgconfig.optionxform = str
        pkgconfig.readfp(sys.stdin)
        version_field = pkgconfig.get('python-package', 'python-version')
        try:
            requested = pyversions.requested_versions_for_runtime(version_field, version_only=True)
        except pyversions.PyCentralEmptyValueError, msg:
            # cannot install yet; remove the symlinked files and byte code files from the old
            # version, rely on the pkginstall in the postinst.
            print "pycentral: required runtimes not yet installed, skip pkgprepare, call pkgremove"
            runtimes = get_installed_runtimes(with_unsupported=True)
            pkg = DebPackage('package', self.args[0], oldstyle=False)
            pkg.read_version_info()
            pkg.default_runtime = get_default_runtime()
            try:
                pkg.remove(runtimes, remove_script_files=True)
            except PyCentralError, msg:
                self.warn(msg)
            return
        used_runtimes = [rt for rt in runtimes if rt.short_name in requested]
        pkg = DebPackage('package', self.args[0], oldstyle=False, pkgconfig=pkgconfig)
        pkg.set_version_field(version_field)
        try:
            pkg.set_default_runtime_from_version_info()
        except ValueError:
	    # Package doesn't provide support for any supported runtime
	    if len(used_runtimes) == 0:
		self.error('%s needs unavailable runtime (%s)'
			   % (self.pkgname, pkg.version_field))
	    else:
		# Still byte compile for the available runtimes (with the
		# first matching runtime)
 		pkg.default_runtime = get_runtime_for_version(used_runtimes[0])

        if os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
            old_pkg = DebPackage('package', self.args[0], oldstyle=False)
            try:
                old_pkg.read_version_info()
            except PyCentralError:
                old_pkg.set_version_field(version_field)
                
            old_requested = pyversions.requested_versions_for_runtime(old_pkg.version_field, version_only=True)
            old_used_runtimes = [rt for rt in runtimes if rt.short_name in requested]
        else:
            old_pkg = None
            old_used_runtimes = []
        logging.debug('\tavail=%s, pkg=%s, prepare=%s'
                      % ([rt.short_name for rt in runtimes],
                         version_field,
                         [rt.short_name for rt in used_runtimes]))
        try:
            pkg.prepare(used_runtimes, old_used_runtimes, old_pkg)
        except PyCentralError, msg:
            self.error(msg)

register_action(ActionPkgPrepare)


class ActionBCRemove(Action):
    """remove the byte-compiled files in <package>.

    bccompile is a replacement for the current byte compilation
    generated by the dh_python debhelper script.
    """
    name = 'bcremove'
    help = 'remove the byte compiled .py files'
    usage = '<package>'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        if not os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
            self.error("package %s is not installed" % self.pkgname)
        return self.errors_occured

    def run(self, global_options):
        pkg = DebPackage('package', self.args[0], oldstyle=True)
        try:
            pkg.remove_bytecode()
        except PyCentralError, msg:
            self.error(msg)

register_action(ActionBCRemove)


class ActionPkgRemove(Action):
    """
    """
    name = 'pkgremove'
    help = 'remove a package installed for all supported runtimes'
    usage = '<package>'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        if not os.path.exists('/var/lib/dpkg/info/%s.list' % self.pkgname):
            self.error("package %s is not installed" % self.pkgname)
        return self.errors_occured

    def run(self, global_options):
        runtimes = get_installed_runtimes(with_unsupported=True)
        pkg = DebPackage('package', self.args[0], oldstyle=False)
        pkg.read_version_info()
        try:
            pkg.set_default_runtime_from_version_info()
        except ValueError:
            # original runtime is already removed, use the default for removal
            pkg.default_runtime = get_default_runtime()
        try:
            pkg.remove(runtimes, remove_script_files=True)
        except PyCentralError, msg:
            self.error(msg)

register_action(ActionPkgRemove)


class ActionRuntimeInstall(Action):
    name = 'rtinstall'
    help = 'make installed packages available for this runtime'

    def get_option_parser(self):
        if not self._option_parser:
            p = OptionParser()
            p.add_option('-i', '--ignore-errors',
                         help="ignore errors during byte compilations",
                         default=None, action='store', dest='ignore')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.rtname = self.args[0]
        if self.rtname[-8:] == '-minimal':
            self.rtname = self.rtname[:-8]
        self.runtime = None
        for rt in get_installed_runtimes():
            if rt.name == self.rtname:
                self.runtime = rt
                break
        if not self.runtime:
            self.error('installed runtime %s not found' % self.rtname)
        return self.errors_occured

    def run(self, global_options):
        packages = [(p, v) for p, v in read_dpkg_status()
                    if not p in (self.rtname, self.rtname+'-minimal')]
        needed_packages = []
        for pkgname, vstring in packages:
            try:
                requested = list(pyversions.requested_versions(vstring, version_only=True))
            except ValueError:
                logging.info('\tunsupported for %s: %s (%s)' % (self.rtname, pkgname, vstring))
                continue
            if self.runtime.short_name in requested:
                needed_packages.append((pkgname, vstring, requested))
        logging.info('\t%d packages with Python-Version info installed, %d for %s'
                     % (len(packages), len(needed_packages), self.rtname))

        # XXX not needed for an upgrade of a runtime
        byte_compile_for_default = (self.runtime == default_runtime)

        bc_option = get_debian_config().get('DEFAULT', 'byte-compile')
        for pkgname, vstring, vinfo in needed_packages:
            try:
                logging.info('\tsupport %s for %s' % (pkgname, self.rtname))
                pkg = DebPackage('package', pkgname, oldstyle=False)
                pkg.read_version_info()
                try:
                    pkg.set_default_runtime_from_version_info()
                except ValueError:
                    logging.warn('\t%s not available for %s (%s)'
                                 % (pkgname, self.rtname, pkg.version_field))
                pkg.install([self.runtime], bc_option, None,
                            byte_compile_for_default,
                            ignore_errors = self.options.ignore != None)
            except PyCentralError, msg:
                self.error('package %s: %s' % (pkgname, msg))

register_action(ActionRuntimeInstall)

class ActionRuntimeRemove(Action):
    name = 'rtremove'
    help = 'remove packages installed for this runtime'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.rtname = self.args[0]
        if self.rtname[-8:] == '-minimal':
            self.rtname = self.rtname[:-8]
        self.runtime = None
        for rt in get_installed_runtimes(with_unsupported=True):
            if rt.name == self.rtname:
                self.runtime = rt
                break
        if not self.runtime:
            self.error('installed runtime %s not found' % self.rtname)
        return self.errors_occured

    def run(self, global_options):
        packages = [(p, v) for p, v in read_dpkg_status(verbose=True)
                    if not p in (self.rtname, self.rtname+'-minimal')]
        needed_packages = []
        import subprocess
        for pkgname, vstring in packages:
            if not os.path.exists('/var/lib/dpkg/info/%s.list' % pkgname):
                # already removed, but /var/lib/dpkg/status not yet updated
                continue
            cmd = ['/usr/bin/dpkg-query', '-W', '-f', '${Status}\n', pkgname]
            p = subprocess.Popen(cmd, bufsize=1,
                                 shell=False, stdout=subprocess.PIPE)
            fd = p.stdout
            status = fd.readline().strip().split()
            fd.close()
            if not 'installed' in status:
                # already removed, but /var/lib/dpkg/status not yet updated
                continue
            try:
                requested = list(pyversions.requested_versions_for_runtime(vstring, version_only=True))
            except ValueError:
                logging.info('\tunsupported for %s: %s (%s)' % (self.rtname, pkgname, vstring))
                continue
            if self.runtime.short_name in requested:
                needed_packages.append((pkgname, vstring, requested))
        logging.info('\t%d pycentral supported packages installed, %d for %s'
                     % (len(packages), len(needed_packages), self.rtname))
        failed = []
        for pkgname, vstring, vinfo in needed_packages:
            logging.info('\trtremove: remove package %s for %s' % (pkgname, self.rtname))
            pkg = DebPackage('package', pkgname)
            pkg.set_version_field(vstring)
            try:
                pkg.set_default_runtime_from_version_info()
            except ValueError:
                # original runtime is already removed, use the default for removal
                pkg.default_runtime = get_default_runtime()
            try:
                pkg.remove([self.runtime], remove_script_files=False)
            except PyCentralError, msg:
                self.error('failed to remove %s support for package %s' % (self.rtname, pkgname), go_on=True)
                failed.append(pkgname)
        if failed:
            self.error('failed to remove %s support for %d packages' % len(failed))

register_action(ActionRuntimeRemove)


class ActionUpdateDefault(Action):
    name = 'updatedefault'
    help = 'update the default python version'
    usage = '<old runtime> <new runtime>'
    
    def check_args(self, global_options):
        if len(self.args) != 2:
            self._option_parser.print_help()
            sys.exit(1)
        self.oldrtname = self.args[0]
        self.rtname = self.args[1]
        packages = read_dpkg_status()
        self.needed_packages = []
        for pkgname, vstring in packages:
            if vstring.find('current') == -1:
                continue
            try:
                versions = pyversions.requested_versions(vstring, version_only=True)
            except ValueError:
                self.error("package %s is not ready to be updated for %s"
                           % (pkgname, self.rtname))
                continue
            pkg = DebPackage('package', pkgname)
            self.needed_packages.append(pkg)
        return self.errors_occured

    def run(self, global_options):
        logging.info('\tupdate default: update %d packages for %s'
                     % (len(self.needed_packages), self.rtname))
        runtimes = get_installed_runtimes()
        default_rt = get_default_runtime()
        bc_option = get_debian_config().get('DEFAULT', 'byte-compile')
        try:
            for pkg in self.needed_packages:
                pkg.read_version_info()
                pkg.set_default_runtime_from_version_info()
                if pkg.shared_files or pkg.private_files:
                    pkg.update_bytecode_files(runtimes, default_rt, bc_option)
        except PyCentralError, msg:
            self.error(msg)

register_action(ActionUpdateDefault)


class ActionShowDefault(Action):
    name = 'showdefault'
    help = 'Show default python version number'

    def check_args(self, global_options):
        if len(self.args) != 0:
            self._option_parser.print_help()
            sys.exit(1)
        return self.errors_occured

    def run(self, global_options):
        print pyversions.default_version(version_only=True)
        sys.stderr.write("pycentral showdefault is deprecated, use `pyversions -vd'\n")

register_action(ActionShowDefault)


class ActionShowVersions(Action):
    name = 'showversions'
    help = 'Show version numbers of supported python versions'

    def check_args(self, global_options):
        if len(self.args) != 0:
            self._option_parser.print_help()
            sys.exit(1)

        return self.errors_occured

    def run(self, global_options):
        supported = pyversions.supported_versions()
        versions = [d[6:] for d in supported if re.match(r'python\d\.\d', d)]
        print ' '.join(versions)
        sys.stderr.write("pycentral showversions is deprecated, use `pyversions -vs'\n")

register_action(ActionShowVersions)

class ActionShowSupported(Action):
    name = 'showsupported'
    help = 'Show the supported python versions'

    def check_args(self, global_options):
        if len(self.args) != 0:
            self._option_parser.print_help()
            sys.exit(1)
        return self.errors_occured

    def run(self, global_options):
        supported = pyversions.supported_versions()
        print ' '.join(supported)
        sys.stderr.write("pycentral showsupported is deprecated, use `pyversions -s'\n")

register_action(ActionShowSupported)


class ActionPyCentralDir(Action):
    name = 'pycentraldir'
    help = 'Show the pycentral installation directory for the package'
    usage = '<package>'

    def check_args(self, global_options):
        if len(self.args) != 1:
            self._option_parser.print_help()
            sys.exit(1)
        self.pkgname = self.args[0]
        return self.errors_occured

    def run(self, global_options):
        if shared_base2[-1] == '/':
            print shared_base2[:-1]
        else:
            print shared_base2

register_action(ActionPyCentralDir)


class ActionVersion(Action):
    name = 'version'
    help = 'Show the pycentral version'

    def check_args(self, global_options):
        if len(self.args) != 0:
            self._option_parser.print_help()
            sys.exit(1)

        return self.errors_occured

    def run(self, global_options):
        sys.stdout.write("%s\n" % pycentral_version)

register_action(ActionVersion)


class ActionDebhelper(Action):
    name = 'debhelper'
    help = 'move files to pycentral location, variable substitutions'
    usage = '[-p|--provides] [--no-move] <package> [<package directory>]'

    def get_option_parser(self):
        if not self._option_parser:
            envvar = os.environ.get('DH_PYCENTRAL', '')
            substvars_default = 'no'
            if 'substvars=file' in envvar:
                substvars_default = 'file'
            if 'substvars=stdout' in envvar:
                substvars_default = 'stdout'

            p = OptionParser()
            p.add_option('-p', '--provides',
                         help="generate substitution for python:Provides",
                         default='add-provides' in envvar, action='store_true', dest='provides')
            p.add_option('--no-move', '--nomove',
                         help="do not move files to pycentral location",
                         default='no-move' in envvar or 'nomove' in envvar, action='store_true', dest='nomove')
            p.add_option('--stdout',
                         help="just print substitution variables to stdout",
                         default='stdout' in envvar, action='store_true', dest='stdout')
            p.add_option('--substvars',
                         help="where to print substitution vars (no, file, stdout)",
                         default=substvars_default, dest='substvars')
            p.add_option('--no-act', '--dry-run',
                         help="dry run",
                         default=('dry-run' in envvar) or ('no-act' in envvar),
                         action='store_true', dest='dryrun')
            self._option_parser = p
        return self._option_parser

    def check_args(self, global_options):
        if not len(self.args) in (1, 2):
            self._option_parser.print_help()
            sys.exit(1)
        if 'file' in self.options.substvars:
            self.options.substvars = 'file'
        if 'stdout' in self.options.substvars:
            self.options.substvars = 'stdout'
        return self.errors_occured

    def run(self, global_options):
        if len(self.args) < 2:
            pkgdir = 'debian/' + self.args[0]
        else:
            pkgdir = self.args[1]
        try:
            pkg = DebPackage('package', self.args[0], pkgdir=pkgdir,
                             parse_versions=self.options.substvars!='no')
            if not self.options.nomove:
                pkg.move_files()
            pkg.read_pyfiles()
            if self.options.substvars!='no':
                pkg.gen_substvars()
        except PyCentralVersionMissingError, msg:
            self.warn(msg)
            return
        except PyCentralError, msg:
            self.error(msg)

        out = None
        if self.options.stdout or self.options.substvars == 'stdout':
            out = sys.stdout
        elif self.options.substvars == 'file':
            out = file('debian/%s.substvars' % pkg.name, 'a+')
        if out:
            out.write('python:Versions=%s\n' % pkg.version_field)
            out.write('python:Depends=%s\n' % pkg.depends)
            out.write('python:Provides=%s\n' % pkg.provides)


register_action(ActionDebhelper)

# match a string with the list of available actions
def action_matches(action, actions):
    prog = re.compile('[^-]*?-'.join(action.split('-')))
    return [a for a in actions if prog.match(a)]

def usage(stream, msg=None):
    print >>stream, msg
    print >>stream, "use `%s help' for help on actions and arguments" % program
    print >>stream
    sys.exit(1)

# parse command line arguments
def parse_options(args):
    shortusage = 'usage: %s [<option> ...] <action> <pkgname>' % program
    parser = OptionParser(usage=shortusage)
    parser.disable_interspersed_args()

    # setup the parsers object
    parser.remove_option('-h')
    parser.add_option('-h', '--help',
                      help='help screen',
                      action='store_true', dest='help')
    parser.add_option('-v', '--verbose',
                      help='verbose mode',
                      action='store_true', dest='verbose')

    global_options, args = parser.parse_args()
    # Print the help screen and exit
    if len(args) == 0 or global_options.help:
        parser.print_help()
        print "\nactions:"
        action_names = known_actions.keys()
        action_names.sort()
        for n in action_names:
            print "  %-21s %s" % (n, known_actions[n].help)
        print ""
        sys.exit(1)

    # check if the specified action really exists
    action_name = args[0]
    del args[0]
    matching_actions = action_matches(action_name, known_actions.keys())
    if len(matching_actions) == 0:
        usage(sys.stderr, "unknown action `%s'" % action_name)
    elif len(matching_actions) > 1:
        usage(sys.stderr,
              "ambiguous action `%s', matching actions: %s"
              % (action_name, strlist(matching_actions)))
    else:
        action_name = matching_actions[0]

    # instantiate an object for the action and parse the remaining arguments
    action = known_actions[action_name]()
    action_options, action_names = action.parse_args(args)

    return global_options, action

class Logging:
    DEBUG, INFO, WARN, ERROR = range(4)
    def __init__(self, level=WARN):
        self.fd = None
        self.level = level
        try:
            self.fd = file('/var/log/pycentral.log', 'a+')
        except IOError:
            self.fd = None
    def msg(self, level, s):
        if level < self.level:
            return
        d = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        if self.fd:
            self.fd.write('%s %s %s\n' % (d, level, s))
            self.fd.flush()
        sys.stdout.write('pycentral: %s\n' % (s))
        sys.stdout.flush()
    def info(self, s):
        self.msg(self.INFO, s)
    def warn(self, s):
        self.msg(self.WARN, s)
    def error(self, s):
        self.msg(self.ERROR, s)
        sys.stderr.write('%s\n' % s)
    def debug(self, s):
        self.msg(self.DEBUG, s)

    def debug_list(self, tab, s, l, n=4):
        l2 = l[:min(n, len(l))]
        if len(l) > n:
            l2.append('...')
        self.msg(self.DEBUG, "%s%s (%s/%s)" % (tab, s, len(l2), len(l)))
        if len(l2) > 0:
            logging.debug('%s    %s' % (tab, l2))

def setup_logging(loglevel=Logging.WARN, verbose=False):
    levels = ['debug', 'info', 'warn', 'error']
    env_level = os.environ.get('PYCENTRAL', 'warn').lower()
    for i in range(len(levels)):
        if env_level.find(levels[i]) != -1:
            loglevel = i
    if verbose:
        loglevel = Logging.DEBUG
    global logging
    logging = Logging(loglevel)

def main():
    global_options, action = parse_options(sys.argv[1:])

    os.umask(0022)

    # setup logging stuff
    setup_logging(Logging.WARN, global_options.verbose)
    if action.name == 'debhelper' or action.name.startswith('show'):
        pass
    else:
        logging.debug('pycentral ' + ' '.join(sys.argv[1:]))

    # check the arguments according to the action called
    if action.check_args(global_options):
        sys.exit(1)

    # run the action and exit
    rv = action.run(global_options)
    sys.exit(rv)


# call the main routine
if __name__ == '__main__':
    main()
